msg shadowban

This commit is contained in:
Thibault Duplessis 2020-01-26 21:25:27 -06:00
parent 596574104d
commit d31ea7d929
6 changed files with 150 additions and 82 deletions

View file

@ -1,8 +1,10 @@
package lila.msg
import reactivemongo.api.bson._
import lila.user.User
import lila.db.dsl._
import lila.db.BSON
import reactivemongo.api.bson._
private object BsonHandlers {
@ -37,4 +39,9 @@ private object BsonHandlers {
"_id" -> ornicar.scalalib.Random.nextString(10),
"tid" -> threadId
)
def writeThread(thread: MsgThread, delBy: Option[User.ID]): Bdoc =
threadHandler.writeTry(thread).get ++ delBy.?? { by =>
$doc("del" -> List(by))
}
}

View file

@ -25,6 +25,8 @@ final class Env(
private lazy val notifier = wire[MsgNotify]
private lazy val security = wire[MsgSecurity]
lazy val api: MsgApi = wire[MsgApi]
lazy val search = wire[MsgSearch]

View file

@ -1,42 +1,26 @@
package lila.msg
import reactivemongo.api._
import play.api.data._
import play.api.data.Forms._
import reactivemongo.api._
import scala.concurrent.duration._
import org.joda.time.DateTime
import lila.common.{ Bus, LightUser }
import lila.db.dsl._
import lila.user.User
import lila.memo.RateLimit
final class MsgApi(
colls: MsgColls,
cacheApi: lila.memo.CacheApi,
lightUserApi: lila.user.LightUserApi,
relationApi: lila.relation.RelationApi,
prefApi: lila.pref.PrefApi,
json: MsgJson,
notifier: MsgNotify
notifier: MsgNotify,
security: MsgSecurity
)(implicit ec: scala.concurrent.ExecutionContext) {
import BsonHandlers._
private val CreateLimitPerUser = new RateLimit[User.ID](
credits = 20,
duration = 24 hour,
name = "PM creates per user",
key = "msg_create.user"
)
private val ReplyLimitPerUser = new RateLimit[User.ID](
credits = 20,
duration = 1 minute,
name = "PM replies per user",
key = "msg_reply.user"
)
def threadsOf(me: User): Fu[List[MsgThread]] =
colls.thread.ext
.find($doc("users" -> me.id, "del" $ne me.id))
@ -51,7 +35,7 @@ final class MsgApi(
_ <- setReadBy(threadId, me)
msgs <- threadMsgsFor(threadId, me)
relations <- relationApi.fetchRelations(me.id, userId)
postable <- canPost(me.id, userId)
postable <- security.may.post(me.id, userId)
} yield MsgConvo(contact, msgs, relations, postable)
}
@ -66,62 +50,43 @@ final class MsgApi(
val postForm = Form(single("text" -> nonEmptyText(maxLength = 10_000)))
private[msg] def post(orig: User.ID, dest: User.ID, text: String): Funit = canPost(orig, dest) flatMap {
_ ?? {
val msg = Msg.make(text, orig)
val threadId = MsgThread.id(orig, dest)
!colls.thread.exists($id(threadId)) flatMap { isNew =>
val msgWrite = colls.msg.insert.one(writeMsg(msg, threadId))
val threadWrite =
if (isNew)
colls.thread.insert.one(MsgThread.make(orig, dest, msg)).void
else
colls.thread.update
.one(
$id(threadId),
$set("lastMsg" -> msg.asLast) ++ $unset("del")
)
.void
(msgWrite zip threadWrite).void >>- {
notifier.onPost(threadId)
Bus.publish(
lila.hub.actorApi.socket.SendTo(
dest,
lila.socket.Socket.makeMessage("msgNew", json.renderMsg(msg))
),
"socketUsers"
)
}
private[msg] def post(orig: User.ID, dest: User.ID, text: String): Funit = {
val msg = Msg.make(text, orig)
val threadId = MsgThread.id(orig, dest)
!colls.thread.exists($id(threadId)) flatMap { isNew =>
security.post(orig, dest, msg, isNew) flatMap {
case MsgSecurity.Ok(mute) =>
val msgWrite = colls.msg.insert.one(writeMsg(msg, threadId))
val threadWrite =
if (isNew)
colls.thread.insert.one {
writeThread(MsgThread.make(orig, dest, msg), delBy = mute option dest)
}.void
else
colls.thread.update
.one(
$id(threadId),
$set("lastMsg" -> msg.asLast) ++ $pull(
// unset deleted by receiver unless the message is muted
"del" $in (orig :: (!mute).option(dest).toList)
)
)
.void
(msgWrite zip threadWrite).void >>- {
notifier.onPost(threadId)
Bus.publish(
lila.hub.actorApi.socket.SendTo(
dest,
lila.socket.Socket.makeMessage("msgNew", json.renderMsg(msg))
),
"socketUsers"
)
}
case _ => funit
}
}
}
private[msg] def canPost(orig: User.ID, dest: User.ID): Fu[Boolean] =
!relationApi.fetchBlocks(dest, orig) >>& {
canCreate(orig, dest) >>| canReply(orig, dest)
}
private def canCreate(orig: User.ID, dest: User.ID): Fu[Boolean] =
prefApi.getPref(dest, _.message) flatMap {
case lila.pref.Pref.Message.NEVER => fuccess(false)
case lila.pref.Pref.Message.FRIEND => relationApi.fetchFollows(dest, orig)
case lila.pref.Pref.Message.ALWAYS => fuccess(true)
}
// Even if the dest prefs disallow it,
// you can still reply if they recently messaged you,
// unless they deleted the thread.
private def canReply(orig: User.ID, dest: User.ID): Fu[Boolean] =
colls.thread.exists(
$id(MsgThread.id(orig, dest)) ++ $or(
"del" $ne dest,
$doc(
"lastMsg.user" -> dest,
"lastMsg.date" $gt DateTime.now.minusDays(3)
)
)
)
def setRead(userId: User.ID, contactId: User.ID): Funit = {
val threadId = MsgThread.id(userId, contactId)
colls.thread

View file

@ -40,15 +40,18 @@ final private class MsgNotify(
private def doNotify(threadId: MsgThread.Id): Funit =
colls.thread.byId[MsgThread](threadId.value) flatMap {
_ ?? { thread =>
val msg = thread.lastMsg
lila.common.Bus.publish(MsgThread.Unread(thread), "msgUnread")
notifyApi addNotification Notification.make(
Notification.Notifies(thread other msg.user),
PrivateMessage(
PrivateMessage.Sender(msg.user),
PrivateMessage.Text(shorten(msg.text, 80))
val msg = thread.lastMsg
val dest = thread other msg.user
!thread.delBy(dest) ?? {
lila.common.Bus.publish(MsgThread.Unread(thread), "msgUnread")
notifyApi addNotification Notification.make(
Notification.Notifies(dest),
PrivateMessage(
PrivateMessage.Sender(msg.user),
PrivateMessage.Text(shorten(msg.text, 80))
)
)
)
}
}
}
}

View file

@ -0,0 +1,88 @@
package lila.msg
import org.joda.time.DateTime
import scala.concurrent.duration._
import lila.db.dsl._
import lila.memo.RateLimit
import lila.user.User
final private class MsgSecurity(
colls: MsgColls,
prefApi: lila.pref.PrefApi,
userRepo: lila.user.UserRepo,
relationApi: lila.relation.RelationApi
)(implicit ec: scala.concurrent.ExecutionContext) {
import BsonHandlers._
import MsgSecurity._
private val CreateLimitPerUser = new RateLimit[User.ID](
credits = 20,
duration = 24 hour,
name = "PM creates per user",
key = "msg_create.user"
)
private val ReplyLimitPerUser = new RateLimit[User.ID](
credits = 20,
duration = 1 minute,
name = "PM replies per user",
key = "msg_reply.user"
)
def post(orig: User.ID, dest: User.ID, msg: Msg, isNew: Boolean): Fu[Verdict] =
may.post(orig, dest) flatMap {
case false => fuccess(Block)
case _ =>
val limiter = if (isNew) CreateLimitPerUser else ReplyLimitPerUser
if (!limiter(orig)(true)) fuccess(Limit)
else
muteTroll(orig, dest) map { troll =>
Ok(mute = troll)
}
}
private def muteTroll(orig: User.ID, dest: User.ID): Fu[Boolean] =
userRepo.isTroll(orig) >>&
!userRepo.isTroll(dest) >>&
!relationApi.fetchFollows(dest, orig)
object may {
def post(orig: User.ID, dest: User.ID): Fu[Boolean] =
!relationApi.fetchBlocks(dest, orig) >>& {
create(orig, dest) >>| reply(orig, dest)
}
private def create(orig: User.ID, dest: User.ID): Fu[Boolean] =
prefApi.getPref(dest, _.message) flatMap {
case lila.pref.Pref.Message.NEVER => fuccess(false)
case lila.pref.Pref.Message.FRIEND => relationApi.fetchFollows(dest, orig)
case lila.pref.Pref.Message.ALWAYS => fuccess(true)
}
// Even if the dest prefs disallow it,
// you can still reply if they recently messaged you,
// unless they deleted the thread.
private def reply(orig: User.ID, dest: User.ID): Fu[Boolean] =
colls.thread.exists(
$id(MsgThread.id(orig, dest)) ++ $or(
"del" $ne dest,
$doc(
"lastMsg.user" -> dest,
"lastMsg.date" $gt DateTime.now.minusDays(3)
)
)
)
}
}
private object MsgSecurity {
sealed trait Verdict
case class Ok(mute: Boolean) extends Verdict
case object Block extends Verdict
case object Limit extends Verdict
}

View file

@ -7,7 +7,8 @@ case class MsgThread(
id: MsgThread.Id,
user1: User.ID,
user2: User.ID,
lastMsg: Msg.Last
lastMsg: Msg.Last,
del: Option[List[User.ID]] = None
) {
def users = List(user1, user2)
@ -15,6 +16,8 @@ case class MsgThread(
def other(userId: User.ID): User.ID = if (user1 == userId) user2 else user1
def other(user: User): User.ID = other(user.id)
def other(user: LightUser): User.ID = other(user.id)
def delBy(userId: User.ID) = del.exists(_ contains userId)
}
object MsgThread {