report weight WIP

reportWeight
Thibault Duplessis 2017-11-30 23:18:44 -05:00
parent dfd410173e
commit 2fc0710f80
9 changed files with 130 additions and 139 deletions

View File

@ -425,11 +425,12 @@ object GameRepo {
F.createdAt $gt since
))
def lastGamesBetween(u1: String, u2: String, since: DateTime, nb: Int): Fu[List[Game]] =
coll.find($doc(
F.playerUids $all List(u1, u2),
F.createdAt $gt since
)).list[Game](nb, ReadPreference.secondaryPreferred)
def lastGamesBetween(u1: User, u2: User, since: DateTime, nb: Int): Fu[List[Game]] =
List(u1, u2).forall(_.count.game > 0) ??
coll.find($doc(
F.playerUids $all List(u1.id, u2.id),
F.createdAt $gt since
)).list[Game](nb, ReadPreference.secondaryPreferred)
def getUserIds(id: ID): Fu[List[User.ID]] =
coll.primitiveOne[List[User.ID]]($id(id), F.playerUids) map (~_)

View File

@ -7,7 +7,6 @@ import lila.user.{ User, UserRepo, Note, NoteApi }
case class Inquiry(
mod: LightUser,
report: Report,
accuracy: Option[Int],
moreReports: List[Report],
notes: List[Note],
history: List[lila.mod.Modlog],
@ -29,17 +28,14 @@ final class InquiryApi(
_ ?? { report =>
reportApi.moreLike(report, 10) zip
UserRepo.named(report.user) zip
reportApi.accuracy(report) zip
noteApi.forMod(report.user) zip
logApi.userHistory(report.user) map {
case moreReports ~ userOption ~ accuracy ~ notes ~ history =>
case moreReports ~ userOption ~ notes ~ history =>
userOption ?? { user =>
Inquiry(mod.light, report, accuracy, moreReports, notes, history, user).some
Inquiry(mod.light, report, moreReports, notes, history, user).some
}
}
}
}
}
// val inquiryWrites = Json.writes[Inquiry]
}

View File

@ -88,7 +88,7 @@ final class ModApi(
} yield ()
def garbageCollect(sus: Suspect, ipBan: Boolean): Funit = for {
mod <- reportApi.getLichess flatten "lichess user is missing"
mod <- reportApi.getLichessMod
_ <- setEngine(mod, sus, true)
_ <- setTroll(mod, sus, true)
_ <- ipBan ?? setBan(mod, sus, true)

View File

@ -10,28 +10,28 @@ final class AutoAnalysis(
system: akka.actor.ActorSystem
) {
def apply(r: Report): Funit =
if (r.isCheat) doItNow(r)
else if (r.isPrint) fuccess {
def apply(candidate: Report.Candidate): Funit =
if (candidate.isCheat) doItNow(candidate)
else if (candidate.isPrint) fuccess {
List(30, 90) foreach { minutes =>
system.scheduler.scheduleOnce(minutes minutes) { doItNow(r) }
system.scheduler.scheduleOnce(minutes minutes) { doItNow(candidate) }
}
}
else funit
private def doItNow(r: Report) =
gamesToAnalyse(r) map { games =>
private def doItNow(candidate: Report.Candidate) =
gamesToAnalyse(candidate) map { games =>
if (games.nonEmpty)
logger.info(s"Auto-analyse ${games.size} games after report ${r.lastAtom.at} -> ${r.user}")
logger.info(s"Auto-analyse ${games.size} games after report by ${candidate.reporter.user.id}")
games foreach { game =>
lila.mon.cheat.autoAnalysis.reason("Report")()
fishnet ! lila.hub.actorApi.fishnet.AutoAnalyse(game.id)
}
}
private def gamesToAnalyse(r: Report): Fu[List[Game]] = {
GameRepo.recentAnalysableGamesByUserId(r.user, 10) |+|
GameRepo.lastGamesBetween(r.user, r.lastAtom.by.userId, DateTime.now.minusHours(2), 10)
private def gamesToAnalyse(candidate: Report.Candidate): Fu[List[Game]] = {
GameRepo.recentAnalysableGamesByUserId(candidate.suspect.user.id, 10) |+|
GameRepo.lastGamesBetween(candidate.suspect.user, candidate.reporter.user, DateTime.now.minusHours(2), 10)
}.map {
_.filter { g => g.analysable && !g.metadata.analysed }
.distinct

View File

@ -42,6 +42,6 @@ private[report] case class ReportSetup(
def export = (user.username, reason, text, gameId, move)
def candidate(reporter: Reporter) = {
Report.Candidate(suspect, reporter, Reason(reason) err s"Invalid report reason ${reason}", text)
Report.Candidate(reporter, suspect, Reason(reason) err s"Invalid report reason ${reason}", text)
}
}

View File

@ -5,9 +5,9 @@ import lila.common.PimpedJson._
object JsonView {
// private implicit val reasonWrites = stringIsoWriter(Reason.reasonIso)
// private implicit val roomWrites = stringIsoWriter(Room.roomIso)
// private implicit val inquiryWrites: Writes[Report.Inquiry] = Json.writes[Report.Inquiry]
private implicit val reasonWrites = stringIsoWriter(Reason.reasonIso)
private implicit val roomWrites = stringIsoWriter(Room.roomIso)
private implicit val inquiryWrites: Writes[Report.Inquiry] = Json.writes[Report.Inquiry]
// implicit val reportWrites: Writes[Report] = Json.writes[Report]
implicit val reportWrites: Writes[Report] = Json.writes[Report]
}

View File

@ -5,6 +5,7 @@ import ornicar.scalalib.Random
import scalaz.NonEmptyList
import lila.user.{ User, Note }
import lila.user.UserRepo.lichessId
case class Report(
_id: Report.ID, // also the url slug
@ -15,7 +16,7 @@ case class Report(
score: Report.Score,
inquiry: Option[Report.Inquiry],
processedBy: Option[User.ID]
) extends WithReason {
) extends Reason.WithReason {
import Report.{ Atom, Score }
@ -29,7 +30,10 @@ case class Report(
score = atoms.toList.foldLeft(Score(0))(_ + _.score)
)
def lastAtom = atoms.last
def mostRecentAtom = atoms.head
def oldestAtom = atoms.last
def onlyAtom = atoms.tail.isEmpty option atoms.head
// def isCreator(user: User.ID) = user == createdBy
@ -39,7 +43,7 @@ case class Report(
def unprocessedInsult = unprocessed && isInsult
def unprocessedTrollOrInsult = unprocessed && isTrollOrInsult
// def isAutomatic = createdBy == "lichess"
// def isAutomatic = createdBy == lichessId
// def isManual = !isAutomatic
def process(by: User) = copy(processedBy = by.id.some)
@ -47,7 +51,7 @@ case class Report(
def unprocessed = processedBy.isEmpty
def processed = processedBy.isDefined
// def userIds = List(user, createdBy)
def userIds: List[User.ID] = user :: atoms.toList.map(_.by.value)
def bestAtom: Atom = atoms.toList.sortBy(-_.score.value).headOption | atoms.head
@ -75,25 +79,25 @@ object Report {
case class Inquiry(mod: User.ID, seenAt: DateTime)
case class WithUser(report: Report, user: User, isOnline: Boolean, accuracy: Option[Int]) {
case class WithSuspect(report: Report, suspect: Suspect, isOnline: Boolean) {
// def urgency: Int =
// (nowSeconds - report.createdAt.getSeconds).toInt +
// (isOnline ?? (86400 * 5)) +
// (report.processed ?? Int.MinValue)
def urgency: Int =
(nowSeconds - report.mostRecentAtom.at.getSeconds).toInt +
(isOnline ?? (86400 * 5)) +
(report.processed ?? Int.MinValue)
}
case class WithUserAndNotes(withUser: WithUser, notes: List[Note]) {
def report = withUser.report
def user = withUser.user
def hasLichessNote = notes.exists(_.from == "lichess")
case class WithSuspectAndNotes(withSuspect: WithSuspect, notes: List[Note]) {
def report = withSuspect.report
def suspect = withSuspect.suspect
def hasLichessNote = notes.exists(_.from == lichessId)
def hasIrwinNote = notes.exists(_.from == "irwin")
// def userIds = report.userIds ::: notes.flatMap(_.userIds)
def userIds = report.userIds ::: notes.flatMap(_.userIds)
}
case class ByAndAbout(by: List[Report], about: List[Report]) {
// def userIds = by.flatMap(_.userIds) ::: about.flatMap(_.userIds)
def userIds = by.flatMap(_.userIds) ::: about.flatMap(_.userIds)
}
case class Candidate(
@ -108,6 +112,7 @@ object Report {
score = getScore(this),
at = DateTime.now
)
def isAutomatic = reporter.user.id == lichessId
}
private[report] val spontaneousText = "Spontaneous inquiry"

View File

@ -30,29 +30,23 @@ final class ReportApi(
private implicit val ReportBSONHandler = Macros.handler[Report]
def create(candidate: Report.Candidate): Funit = !candidate.reporter.user.reportban ?? {
!isAlreadySlain(report, reported) ?? {
discarder(report, by, accuracy(report)).flatMap {
!isAlreadySlain(candidate) ?? {
discarder(candidate, accuracy(candidate)).flatMap {
case true =>
logger.info(s"Discarded report $report")
lila.mon.mod.report.discard(report.reason.key)()
logger.info(s"Discarded report $candidate")
lila.mon.mod.report.discard(candidate.reason.key)()
funit
case false =>
coll.find($doc(
"user" -> setup.suspect.user.id,
"reason" -> reason,
"processedBy" $exists false
)).one[Report] flatMap { existing =>
case false => coll.find($doc(
"user" -> candidate.suspect.user.id,
"reason" -> candidate.reason,
"processedBy" $exists false
)).one[Report] flatMap { existing =>
val report = Report.make(candidate, existing)
lila.mon.mod.report.create(report.reason.key)()
def insert = coll.insert(report).void >>
autoAnalysis(report) >>-
bus.publish(lila.hub.actorApi.report.Created(reported.user.id, report.reason.key, by.user.id), 'report)
if (List("irwin", UserRepo.lichessId) contains by.user.id) coll.update(
selectRecent(reported, report.reason),
$doc("$set" -> ReportBSONHandler.write(report).remove("processedBy", "_id"))
) flatMap { res => (res.n == 0) ?? insert }
else insert
coll.update($id(report.id), report, upsert = true).void >>
autoAnalysis(candidate) >>-
bus.publish(lila.hub.actorApi.report.Created(candidate.suspect.user.id, candidate.reason.key, candidate.reporter.user.id), 'report)
}
} >>- monitorUnprocessed
}
}
@ -64,71 +58,66 @@ final class ReportApi(
}
}
private def isAlreadySlain(candidate: Report.Candidate, suspect: Suspect) =
(candidate.isCheat && suspect.user.engine) ||
(candidate.isAutomatic && candidate.isOther && suspect.user.troll) ||
(reportcandidateisTrollOrInsult && suspect.user.troll)
private def isAlreadySlain(candidate: Report.Candidate) =
(candidate.isCheat && candidate.suspect.user.engine) ||
(candidate.isAutomatic && candidate.isOther && candidate.suspect.user.troll) ||
(candidate.isTrollOrInsult && candidate.suspect.user.troll)
def getMod(username: String): Fu[Option[Mod]] =
UserRepo named username map2 Mod.apply
def getLichess: Fu[Option[Mod]] = UserRepo.lichess map2 Mod.apply
def getLichessMod: Fu[Mod] = UserRepo.lichess map2 Mod.apply flatten "User lichess is missing"
def getLichessReporter: Fu[Reporter] = getLichessMod map { l => Reporter(l.user) }
def getSuspect(username: String): Fu[Option[Suspect]] =
UserRepo named username map2 Suspect.apply
def autoCheatPrintReport(userId: String): Funit = {
UserRepo byId userId zip UserRepo.lichess flatMap {
case (Some(user), Some(lichess)) => create(ReportSetup(
user = user,
reason = "cheatprint",
text = "Shares print with known cheaters",
gameId = "",
move = ""
), Reporter(lichess))
def autoCheatPrintReport(userId: String): Funit =
getSuspect(userId) zip getLichessReporter flatMap {
case (Some(suspect), reporter) => create(Report.Candidate(
reporter = reporter,
suspect = suspect,
reason = Reason.CheatPrint,
text = "Shares print with known cheaters"
))
case _ => funit
}
}
def autoCheatReport(userId: String, text: String): Funit = {
lila.mon.cheat.autoReport.count()
UserRepo byId userId zip UserRepo.lichess flatMap {
case (Some(user), Some(lichess)) => create(ReportSetup(
user = user,
reason = "cheat",
text = text,
gameId = "",
move = ""
), Reporter(lichess))
def autoCheatReport(userId: String, text: String): Funit =
getSuspect(userId) zip getLichessReporter flatMap {
case (Some(suspect), reporter) =>
lila.mon.cheat.autoReport.count()
create(Report.Candidate(
reporter = reporter,
suspect = suspect,
reason = Reason.Cheat,
text = text
))
case _ => funit
}
}
def autoBotReport(userId: String, referer: Option[String], name: String): Funit = {
UserRepo byId userId zip UserRepo.lichess flatMap {
case (Some(user), Some(lichess)) => create(ReportSetup(
user = user,
reason = "cheat",
text = s"""$name bot detected on ${referer | "?"}""",
gameId = "",
move = ""
), Reporter(lichess))
def autoBotReport(userId: String, referer: Option[String], name: String): Funit =
getSuspect(userId) zip getLichessReporter flatMap {
case (Some(suspect), reporter) => create(Report.Candidate(
reporter = reporter,
suspect = suspect,
reason = Reason.Cheat,
text = s"""$name bot detected on ${referer | "?"}"""
))
case _ => funit
}
}
def autoBoostReport(winnerId: User.ID, loserId: User.ID): Funit =
securityApi.shareIpOrPrint(winnerId, loserId) zip
UserRepo.byId(winnerId) zip UserRepo.byId(loserId) zip UserRepo.lichess flatMap {
case isSame ~ Some(winner) ~ Some(loser) ~ Some(lichess) => create(ReportSetup(
user = if (isSame) winner else loser,
reason = Reason.Boost.key,
UserRepo.byId(winnerId) zip UserRepo.byId(loserId) zip getLichessReporter flatMap {
case isSame ~ Some(winner) ~ Some(loser) ~ reporter => create(Report.Candidate(
reporter = reporter,
suspect = Suspect(if (isSame) winner else loser),
reason = Reason.Boost,
text =
if (isSame) s"Farms rating points from @${loser.username} (same IP or print)"
else s"Sandbagging - the winning player @${winner.username} has different IPs & prints",
gameId = "",
move = ""
), Reporter(lichess))
else s"Sandbagging - the winning player @${winner.username} has different IPs & prints"
))
case _ => funit
}
@ -164,14 +153,13 @@ final class ReportApi(
}
def autoInsultReport(userId: String, text: String): Funit = {
UserRepo byId userId zip UserRepo.lichess flatMap {
case (Some(user), Some(lichess)) => create(ReportSetup(
user = user,
reason = "insult",
text = text,
gameId = "",
move = ""
), Reporter(lichess))
getSuspect(userId) zip getLichessReporter flatMap {
case (Some(suspect), reporter) => create(Report.Candidate(
reporter = reporter,
suspect = suspect,
reason = Reason.Insult,
text = text
))
case _ => funit
}
} >>- monitorUnprocessed
@ -216,33 +204,27 @@ final class ReportApi(
).some,
ReadPreference.secondaryPreferred)
def unprocessedAndRecentWithFilter(nb: Int, room: Option[Room]): Fu[List[Report.WithUserAndNotes]] = for {
def unprocessedAndRecentWithFilter(nb: Int, room: Option[Room]): Fu[List[Report.WithSuspectAndNotes]] = for {
unprocessed <- findRecent(nb, unprocessedSelect ++ roomSelect(room))
nbProcessed = nb - unprocessed.size
processed <- if (room.has(Room.Xfiles) || nbProcessed == 0) fuccess(Nil)
else findRecent(nbProcessed, processedSelect ++ roomSelect(room))
withNotes <- addUsersAndNotes(unprocessed ++ processed)
withNotes <- addSuspectsAndNotes(unprocessed ++ processed)
} yield withNotes
def next(room: Room): Fu[Option[Report]] =
unprocessedAndRecentWithFilter(1, room.some) map (_.headOption.map(_.report))
private def addUsersAndNotes(reports: List[Report]): Fu[List[Report.WithUserAndNotes]] = for {
withUsers <- UserRepo byIdsSecondary reports.map(_.user).distinct map { users =>
reports.flatMap { r =>
users.find(_.id == r.user) map { u =>
accuracy(r) map { a =>
Report.WithUser(r, u, isOnline(u.id), a)
}
}
private def addSuspectsAndNotes(reports: List[Report]): Fu[List[Report.WithSuspectAndNotes]] = for {
users <- UserRepo byIdsSecondary reports.map(_.user).distinct
withSuspects = reports.flatMap { r =>
users.find(_.id == r.user) map { u =>
Report.WithSuspect(r, Suspect(u), isOnline(u.id))
}
}
sorted <- withUsers.sequenceFu map { wu =>
wu.sortBy(-_.urgency)
}
withNotes <- noteApi.byMod(sorted.map(_.user.id).distinct) map { notes =>
sorted.map { wu =>
Report.WithUserAndNotes(wu, notes.filter(_.to == wu.user.id))
}.sortBy(-_.urgency)
withNotes <- noteApi.byMod(withSuspects.map(_.suspect.user.id).distinct) map { notes =>
withSuspects.map { wu =>
Report.WithSuspectAndNotes(wu, notes.filter(_.to == wu.suspect.user.id))
}
}
} yield withNotes
@ -261,7 +243,7 @@ final class ReportApi(
"reason" -> Reason.Cheat.key,
"processedBy" $exists true
)).sort($sort.createdDesc).list[Report](20, ReadPreference.secondaryPreferred) flatMap { reports =>
if (reports.size < 5) fuccess(none) // not enough data to know
if (reports.size < 4) fuccess(none) // not enough data to know
else {
val userIds = reports.map(_.user).distinct
UserRepo countEngines userIds map { nbEngines =>
@ -270,9 +252,9 @@ final class ReportApi(
}
}
def apply(report: Report): Fu[Option[Int]] =
(report.reason == Reason.Cheat && !report.processed) ?? {
cache get report.createdBy
def apply(candidate: Report.Candidate): Fu[Option[Int]] =
(candidate.reason == Reason.Cheat) ?? {
cache get candidate.reporter.user.id
}
def invalidate(selector: Bdoc): Funit =
@ -337,7 +319,8 @@ final class ReportApi(
} yield !isSame option report
def cancel(mod: Mod)(report: Report): Funit =
if (report.isOther && report.createdBy == mod.user.id) coll.remove($id(report.id)).void
if (report.isOther && report.onlyAtom.map(_.by.value).has(mod.user.id))
coll.remove($id(report.id)).void // cancel spontaneous inquiry
else coll.update(
$id(report.id),
$unset("inquiry", "processedBy")
@ -346,7 +329,13 @@ final class ReportApi(
def spontaneous(mod: Mod, sus: Suspect): Fu[Report] = ofModId(mod.user.id) flatMap { current =>
current.??(cancel(mod)) >> {
val report = Report.make(
sus, Reason.Other, Report.spontaneousText, Reporter(mod.user)
Report.Candidate(
Reporter(mod.user),
sus,
Reason.Other,
Report.spontaneousText
),
none
).copy(inquiry = Report.Inquiry(mod.user.id, DateTime.now).some)
coll.insert(report) inject report
}

View File

@ -14,8 +14,8 @@ case class Victim(user: User) extends AnyVal
case class Reporter(user: User) extends AnyVal {
def id = ReporterId(user.id)
}
case class ReporterId(userId: User.ID) extends AnyVal
case class ReporterId(value: User.ID) extends AnyVal
object ReporterId {
implicit val reporterIdIso = lila.common.Iso.string[ReporterId](ReporterId.apply, _.userId)
implicit val reporterIdIso = lila.common.Iso.string[ReporterId](ReporterId.apply, _.value)
}