better report muted threads

maestro-pieces
Thibault Duplessis 2019-09-05 10:13:31 +02:00
parent 8ba91adcf5
commit b39a1fdc28
9 changed files with 80 additions and 63 deletions

View File

@ -39,13 +39,16 @@ package socket {
def apply[A: Writes](userIds: Set[String], typ: String, data: A): SendTos =
SendTos(userIds, Json.obj("t" -> typ, "d" -> data))
}
case class RemoteSocketTellSriIn(sri: String, user: Option[String], msg: JsObject)
case class RemoteSocketTellSriOut(sri: String, payload: JsValue)
object remote {
case class TellSriIn(sri: String, user: Option[String], msg: JsObject)
case class TellSriOut(sri: String, payload: JsValue)
case class ConnectUser(userId: String)
}
}
package report {
case class Cheater(userId: String, text: String)
case class Shutup(userId: String, text: String)
case class Shutup(userId: String, text: String, major: Boolean)
case class Booster(winnerId: String, loserId: String)
}
@ -58,7 +61,7 @@ package security {
package shutup {
case class RecordPublicForumMessage(userId: String, text: String)
case class RecordTeamForumMessage(userId: String, text: String)
case class RecordPrivateMessage(userId: String, toUserId: String, text: String)
case class RecordPrivateMessage(userId: String, toUserId: String, text: String, muted: Boolean)
case class RecordPrivateChat(chatId: String, userId: String, text: String)
case class RecordPublicChat(userId: String, text: String, source: PublicSource)

View File

@ -72,7 +72,7 @@ final class MessageApi(
sendUnlessBlocked(thread, fromMod) flatMap {
_ ?? {
val text = s"${data.subject} ${data.text}"
shutup ! lila.hub.actorApi.shutup.RecordPrivateMessage(me.id, invited.id, text)
shutup ! lila.hub.actorApi.shutup.RecordPrivateMessage(me.id, invited.id, text, thread.looksMuted)
notify(thread)
}
} inject thread
@ -99,7 +99,7 @@ final class MessageApi(
val newThread = thread + post
coll.update($id(newThread.id), newThread) >> {
val toUserId = newThread otherUserId me
shutup ! lila.hub.actorApi.shutup.RecordPrivateMessage(me.id, toUserId, text)
shutup ! lila.hub.actorApi.shutup.RecordPrivateMessage(me.id, toUserId, text, muted = false)
notify(thread, post)
} inject newThread
}

View File

@ -6,15 +6,15 @@ import lila.shutup.Analyser
import lila.user.User
private[message] final class MessageSecurity(
follows: (String, String) => Fu[Boolean],
blocks: (String, String) => Fu[Boolean],
getPref: String => Fu[lila.pref.Pref],
follows: (User.ID, User.ID) => Fu[Boolean],
blocks: (User.ID, User.ID) => Fu[Boolean],
getPref: User.ID => Fu[lila.pref.Pref],
spam: lila.security.Spam
) {
import lila.pref.Pref.Message._
def canMessage(from: String, to: String): Fu[Boolean] =
def canMessage(from: User.ID, to: User.ID): Fu[Boolean] =
blocks(to, from) flatMap {
case true => fuFalse
case false => getPref(to).map(_.message) flatMap {
@ -27,7 +27,7 @@ private[message] final class MessageSecurity(
def muteThreadIfNecessary(thread: Thread, creator: User, invited: User): Fu[Thread] = {
val fullText = s"${thread.name} ${~thread.firstPost.map(_.text)}"
if (spam.detect(fullText)) {
logger.warn(s"PM spam from ${creator.username}: fullText")
logger.warn(s"PM spam from ${creator.username}: $fullText")
fuTrue
} else if (creator.troll) !follows(invited.id, creator.id)
else if (Analyser(fullText).dirty && creator.createdAt.isAfter(DateTime.now.minusDays(30))) {

View File

@ -87,13 +87,15 @@ case class Thread(
def deleteFor(user: User) = copy(
visibleByUserIds = visibleByUserIds filter (user.id !=),
deletedByUserIds = Some(deletedByUserIds.getOrElse(List()) ::: List(user.id))
deletedByUserIds = Some(user.id :: ~deletedByUserIds)
)
def isVisibleBy(userId: User.ID) = visibleByUserIds contains userId
def isVisibleByOther(user: User) = isVisibleBy(otherUserId(user))
def looksMuted = posts.length == 1 && (~deletedByUserIds).has(invitedId)
def hasPostsWrittenBy(userId: User.ID) = posts exists (_.isByCreator == (creatorId == userId))
def endsWith(post: Post) = posts.lastOption ?? post.similar

View File

@ -55,8 +55,8 @@ final class Env(
def receive = {
case lila.hub.actorApi.report.Cheater(userId, text) =>
api.autoCheatReport(userId, text)
case lila.hub.actorApi.report.Shutup(userId, text) =>
api.autoInsultReport(userId, text)
case lila.hub.actorApi.report.Shutup(userId, text, major) =>
api.autoInsultReport(userId, text, major)
case lila.hub.actorApi.report.Booster(winnerId, loserId) =>
api.autoBoostReport(winnerId, loserId)
}

View File

@ -132,6 +132,7 @@ object Report {
object Candidate {
case class Scored(candidate: Candidate, score: Score) {
def withScore(f: Score => Score) = copy(score = f(score))
def atom = Atom(
by = candidate.reporter.id,
text = candidate.text,

View File

@ -20,13 +20,14 @@ final class ReportApi(
) {
import BSONHandlers._
import Report.Candidate
private lazy val scorer = new ReportScore(getAccuracy = accuracy.of)
def create(c: Report.Candidate): Funit = !c.reporter.user.reportban ?? {
!isAlreadySlain(c) ?? {
scorer(c) flatMap {
case scored @ Report.Candidate.Scored(candidate, _) =>
def create(c: Candidate, score: Report.Score => Report.Score = identity): Funit =
(!c.reporter.user.reportban && !isAlreadySlain(c)) ?? {
scorer(c) map (_ withScore score) flatMap {
case scored @ Candidate.Scored(candidate, _) =>
coll.find($doc(
"user" -> candidate.suspect.user.id,
"reason" -> candidate.reason,
@ -39,9 +40,8 @@ final class ReportApi(
} >>- monitorOpen
}
}
}
def commFlag(reporter: Reporter, suspect: Suspect, resource: String, text: String) = create(Report.Candidate(
def commFlag(reporter: Reporter, suspect: Suspect, resource: String, text: String) = create(Candidate(
reporter,
suspect,
Reason.CommFlag,
@ -55,7 +55,7 @@ final class ReportApi(
}
}
private def isAlreadySlain(candidate: Report.Candidate) =
private def isAlreadySlain(candidate: Candidate) =
(candidate.isCheat && candidate.suspect.user.engine) ||
(candidate.isAutomatic && candidate.isOther && candidate.suspect.user.troll) ||
(candidate.isAboutComm && candidate.suspect.user.troll)
@ -76,7 +76,7 @@ final class ReportApi(
)) flatMap {
case true => funit // only report once
case _ => getSuspect(userId) zip getLichessReporter flatMap {
case (Some(suspect), reporter) => create(Report.Candidate(
case (Some(suspect), reporter) => create(Candidate(
reporter = reporter,
suspect = suspect,
reason = Reason.CheatPrint,
@ -92,7 +92,7 @@ final class ReportApi(
findRecent(1, selectRecent(SuspectId(userId), Reason.Cheat)).map(_.flatMap(_.atoms.toList)) flatMap {
case Some(suspect) ~ reporter ~ atoms if atoms.forall(_.byHuman) =>
lila.mon.cheat.autoReport.count()
create(Report.Candidate(
create(Candidate(
reporter = reporter,
suspect = suspect,
reason = Reason.Cheat,
@ -103,7 +103,7 @@ final class ReportApi(
def autoBotReport(userId: String, referer: Option[String], name: String): Funit =
getSuspect(userId) zip getLichessReporter flatMap {
case (Some(suspect), reporter) => create(Report.Candidate(
case (Some(suspect), reporter) => create(Candidate(
reporter = reporter,
suspect = suspect,
reason = Reason.Cheat,
@ -119,7 +119,7 @@ final class ReportApi(
UserRepo.byId(userId) zip
getLichessReporter zip
findRecent(1, selectRecent(SuspectId(userId), Reason.Playbans)) flatMap {
case Some(abuser) ~ reporter ~ past if past.size < 1 => create(Report.Candidate(
case Some(abuser) ~ reporter ~ past if past.size < 1 => create(Candidate(
reporter = reporter,
suspect = Suspect(abuser),
reason = Reason.Playbans,
@ -140,7 +140,7 @@ final class ReportApi(
def autoBoostReport(winnerId: User.ID, loserId: User.ID): Funit =
securityApi.shareIpOrPrint(winnerId, loserId) zip
UserRepo.byId(winnerId) zip UserRepo.byId(loserId) zip getLichessReporter flatMap {
case isSame ~ Some(winner) ~ Some(loser) ~ reporter => create(Report.Candidate(
case isSame ~ Some(winner) ~ Some(loser) ~ reporter => create(Candidate(
reporter = reporter,
suspect = Suspect(if (isSame) winner else loser),
reason = Reason.Boost,
@ -184,14 +184,17 @@ final class ReportApi(
multi = true
)
def autoInsultReport(userId: String, text: String): Funit = {
def autoInsultReport(userId: String, text: String, major: Boolean): Funit = {
getSuspect(userId) zip getLichessReporter flatMap {
case (Some(suspect), reporter) => create(Report.Candidate(
reporter = reporter,
suspect = suspect,
reason = Reason.Insult,
text = text
))
case (Some(suspect), reporter) => create(
Candidate(
reporter = reporter,
suspect = suspect,
reason = Reason.Insult,
text = text
),
score => if (major) Report.Score(score.value atLeast scoreThreshold()) else score
)
case _ => funit
}
} >>- monitorOpen
@ -303,7 +306,7 @@ final class ReportApi(
def of(reporter: ReporterId): Fu[Option[Accuracy]] =
cache get reporter.value map2 Accuracy.apply
def apply(candidate: Report.Candidate): Fu[Option[Accuracy]] =
def apply(candidate: Candidate): Fu[Option[Accuracy]] =
(candidate.reason == Reason.Cheat) ?? of(candidate.reporter.id)
def invalidate(selector: Bdoc): Funit =
@ -377,7 +380,7 @@ final class ReportApi(
def spontaneous(mod: Mod, sus: Suspect): Fu[Report] = ofModId(mod.user.id) flatMap { current =>
current.??(cancel(mod)) >> {
val report = Report.make(
Report.Candidate(
Candidate(
Reporter(mod.user),
sus,
Reason.Other,

View File

@ -30,8 +30,8 @@ final class Env(
api.publicForumMessage(userId, text)
case RecordTeamForumMessage(userId, text) =>
api.teamForumMessage(userId, text)
case RecordPrivateMessage(userId, toUserId, text) =>
api.privateMessage(userId, toUserId, text)
case RecordPrivateMessage(userId, toUserId, text, major) =>
api.privateMessage(userId, toUserId, text, major)
case RecordPrivateChat(chatId, userId, text) =>
api.privateChat(chatId, userId, text)
case RecordPublicChat(userId, text, source) =>

View File

@ -5,11 +5,11 @@ import reactivemongo.bson._
import lila.db.dsl._
import lila.game.GameRepo
import lila.hub.actorApi.shutup.PublicSource
import lila.user.UserRepo
import lila.user.{ User, UserRepo }
final class ShutupApi(
coll: Coll,
follows: (String, String) => Fu[Boolean],
follows: (User.ID, User.ID) => Fu[Boolean],
reporter: akka.actor.ActorSelection
) {
@ -17,27 +17,34 @@ final class ShutupApi(
private implicit val UserRecordBSONHandler = Macros.handler[UserRecord]
import PublicLine.PublicLineBSONHandler
def getPublicLines(userId: String): Fu[List[PublicLine]] =
def getPublicLines(userId: User.ID): Fu[List[PublicLine]] =
coll.find($doc("_id" -> userId), $doc("pub" -> 1))
.uno[Bdoc].map {
~_.flatMap(_.getAs[List[PublicLine]]("pub"))
}
def publicForumMessage(userId: String, text: String) = record(userId, text, TextType.PublicForumMessage)
def teamForumMessage(userId: String, text: String) = record(userId, text, TextType.TeamForumMessage)
def publicChat(userId: String, text: String, source: PublicSource) = record(userId, text, TextType.PublicChat, source.some)
def publicForumMessage(userId: User.ID, text: String) = record(userId, text, TextType.PublicForumMessage)
def teamForumMessage(userId: User.ID, text: String) = record(userId, text, TextType.TeamForumMessage)
def publicChat(userId: User.ID, text: String, source: PublicSource) = record(userId, text, TextType.PublicChat, source.some)
def privateChat(chatId: String, userId: String, text: String) =
def privateChat(chatId: String, userId: User.ID, text: String) =
GameRepo.getSourceAndUserIds(chatId) flatMap {
case (source, _) if source.has(lila.game.Source.Friend) => funit // ignore challenges
case (_, userIds) =>
record(userId, text, TextType.PrivateChat, none, userIds find (userId !=))
}
def privateMessage(userId: String, toUserId: String, text: String) =
def privateMessage(userId: User.ID, toUserId: User.ID, text: String, major: Boolean) =
record(userId, text, TextType.PrivateMessage, none, toUserId.some)
private def record(userId: String, text: String, textType: TextType, source: Option[PublicSource] = None, toUserId: Option[String] = None): Funit =
private def record(
userId: User.ID,
text: String,
textType: TextType,
source: Option[PublicSource] = None,
toUserId: Option[User.ID] = None,
major: Boolean = false
): Funit =
UserRepo isTroll userId flatMap {
case true => funit
case false => toUserId ?? { follows(_, userId) } flatMap {
@ -59,31 +66,32 @@ final class ShutupApi(
)
) ++ pushPublicLine
coll.findAndUpdate(
selector = $doc("_id" -> userId),
update = $doc("$push" -> push),
selector = $id(userId),
update = $push(push),
fetchNewObject = true,
upsert = true
).map(_.value) map2 UserRecordBSONHandler.read flatMap {
case None => fufail(s"can't find user record for $userId")
case Some(userRecord) => legiferate(userRecord)
case Some(userRecord) => legiferate(userRecord, major)
} logFailure lila.log("shutup")
}
}
private def legiferate(userRecord: UserRecord): Funit =
userRecord.reports.exists(_.unacceptable) ?? {
reporter ! lila.hub.actorApi.report.Shutup(userRecord.userId, reportText(userRecord))
coll.update(
$doc("_id" -> userRecord.userId),
$doc("$unset" -> $doc(
TextType.PublicForumMessage.key -> true,
TextType.TeamForumMessage.key -> true,
TextType.PrivateMessage.key -> true,
TextType.PrivateChat.key -> true,
TextType.PublicChat.key -> true
))
).void
}
private def legiferate(userRecord: UserRecord, major: Boolean): Funit = {
major || userRecord.reports.exists(_.unacceptable)
} ?? {
reporter ! lila.hub.actorApi.report.Shutup(userRecord.userId, reportText(userRecord), major)
coll.update(
$id(userRecord.userId),
$unset(
TextType.PublicForumMessage.key,
TextType.TeamForumMessage.key,
TextType.PrivateMessage.key,
TextType.PrivateChat.key,
TextType.PublicChat.key
)
).void
}
private def reportText(userRecord: UserRecord) =
userRecord.reports.collect {