Merge pull request #308 from ornicar/BoostingDetector
Boosting detector
This commit is contained in:
commit
424f740d72
|
@ -23,6 +23,10 @@ object Mod extends LilaController {
|
|||
me => modApi.adjust(me.id, username) inject redirect(username)
|
||||
}
|
||||
|
||||
def booster(username: String) = Secure(_.MarkBooster) { _ =>
|
||||
me => modApi.adjustBooster(me.id, username) inject redirect(username)
|
||||
}
|
||||
|
||||
def troll(username: String) = Secure(_.MarkTroll) { _ =>
|
||||
me =>
|
||||
modApi.troll(me.id, username) inject redirect(username)
|
||||
|
|
|
@ -13,6 +13,11 @@
|
|||
<input class="confirm button" type="submit" value="@u.engine.fold("Un-engine", "Engine")" />
|
||||
</form>
|
||||
}
|
||||
@if(isGranted(_.MarkBooster)) {
|
||||
<form method="post" action="@routes.Mod.booster(u.username)">
|
||||
<input class="confirm button" type="submit" value="@u.booster.fold("Un-booster", "Booster")" />
|
||||
</form>
|
||||
}
|
||||
@if(isGranted(_.MarkTroll)) {
|
||||
<form method="post" action="@routes.Mod.troll(u.username)">
|
||||
<input class="confirm button" type="submit" value="@u.troll.fold("Un-troll", "Troll")" />
|
||||
|
|
|
@ -164,11 +164,19 @@ openGraph = Map(
|
|||
</div>
|
||||
}
|
||||
<div class="user-infos scroll-shadow-hard">
|
||||
@if(u.engine && ctx.me.fold(true)(u !=)) {
|
||||
@if(ctx.me.fold(true)(u !=)) {
|
||||
@if(u.engine) {
|
||||
<div class="warning engine_warning">
|
||||
<span data-icon="j" class="is4"></span>
|
||||
@trans.thisPlayerUsesChessComputerAssistance()
|
||||
</div>
|
||||
}
|
||||
@if(u.booster) {
|
||||
<div class="warning engine_warning">
|
||||
<span data-icon="j" class"is4"></span>
|
||||
@trans.thisPlayerArtificiallyIncreasesTheirRating()
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
@u.title.flatMap(lila.user.User.titlesMap.get).map { title =>
|
||||
<p data-icon="E" class="honorific title"> @title</p>
|
||||
|
|
|
@ -154,6 +154,7 @@ ratingRange=Rating range
|
|||
giveNbSeconds=Give %s seconds
|
||||
premoveEnabledClickAnywhereToCancel=Premove enabled - Click anywhere to cancel
|
||||
thisPlayerUsesChessComputerAssistance=This player uses chess computer assistance
|
||||
thisPlayerArtificiallyIncreasesTheirRating=This player artificially increases their rating
|
||||
opening=Opening
|
||||
takeback=Takeback
|
||||
proposeATakeback=Propose a takeback
|
||||
|
|
|
@ -201,6 +201,7 @@ POST /password/reset/confirm/:token controllers.Auth.passwordResetConfirmAppl
|
|||
|
||||
# Mod
|
||||
POST /mod/:username/engine controllers.Mod.engine(username: String)
|
||||
POST /mod/:username/booster controllers.Mod.booster(username: String)
|
||||
POST /mod/:username/troll controllers.Mod.troll(username: String)
|
||||
POST /mod/:username/ban controllers.Mod.ban(username: String)
|
||||
POST /mod/:username/close controllers.Mod.closeAccount(username: String)
|
||||
|
|
|
@ -45,6 +45,7 @@ case class Check(userId: String)
|
|||
|
||||
package mod {
|
||||
case class MarkCheater(userId: String)
|
||||
case class MarkBooster(userId: String)
|
||||
}
|
||||
|
||||
package setup {
|
||||
|
|
|
@ -178,6 +178,7 @@ final class I18nKeys(translator: Translator) {
|
|||
val `giveNbSeconds` = new Key("giveNbSeconds")
|
||||
val `premoveEnabledClickAnywhereToCancel` = new Key("premoveEnabledClickAnywhereToCancel")
|
||||
val `thisPlayerUsesChessComputerAssistance` = new Key("thisPlayerUsesChessComputerAssistance")
|
||||
val `thisPlayerArtificiallyIncreasesTheirRating` = new Key("thisPlayerArtificiallyIncreasesTheirRating")
|
||||
val `opening` = new Key("opening")
|
||||
val `takeback` = new Key("takeback")
|
||||
val `proposeATakeback` = new Key("proposeATakeback")
|
||||
|
|
|
@ -75,7 +75,7 @@ private[lobby] object Biter {
|
|||
def canJoin(hook: Hook, user: Option[LobbyUser]): Boolean =
|
||||
hook.realMode.casual.fold(
|
||||
user.isDefined || hook.allowAnon,
|
||||
user ?? { _.engine == hook.engine }
|
||||
user ?? { _.lame == hook.lame }
|
||||
) &&
|
||||
!(hook.userId ?? (user ?? (_.blocking)).contains) &&
|
||||
!((user map (_.id)) ?? (hook.user ?? (_.blocking)).contains) &&
|
||||
|
@ -86,7 +86,7 @@ private[lobby] object Biter {
|
|||
}
|
||||
|
||||
def canJoin(seek: Seek, user: LobbyUser): Boolean =
|
||||
(seek.realMode.casual || user.engine == seek.user.engine) &&
|
||||
(seek.realMode.casual || user.lame == seek.user.lame) &&
|
||||
!(user.blocking contains seek.user.id) &&
|
||||
!(seek.user.blocking contains user.id) &&
|
||||
seek.realRatingRange.fold(true) { range =>
|
||||
|
|
|
@ -51,6 +51,8 @@ case class Hook(
|
|||
def username = user.fold(User.anonymous)(_.username)
|
||||
def rating = user flatMap { u => perfType map (_.key) flatMap u.ratingMap.get }
|
||||
def engine = user ?? (_.engine)
|
||||
def booster = user ?? (_.booster)
|
||||
def lame = engine || booster
|
||||
|
||||
def render: JsObject = Json.obj(
|
||||
"id" -> id,
|
||||
|
|
|
@ -10,8 +10,11 @@ private[lobby] case class LobbyUser(
|
|||
username: String,
|
||||
troll: Boolean,
|
||||
engine: Boolean,
|
||||
booster: Boolean,
|
||||
ratingMap: Map[String, Int],
|
||||
blocking: Set[String])
|
||||
blocking: Set[String]) {
|
||||
def lame = engine || booster
|
||||
}
|
||||
|
||||
private[lobby] object LobbyUser {
|
||||
|
||||
|
@ -20,6 +23,7 @@ private[lobby] object LobbyUser {
|
|||
username = user.username,
|
||||
troll = user.troll,
|
||||
engine = user.engine,
|
||||
booster = user.booster,
|
||||
ratingMap = user.perfs.ratingMap,
|
||||
blocking = blocking)
|
||||
}
|
||||
|
|
84
modules/mod/src/main/Boosting.scala
Normal file
84
modules/mod/src/main/Boosting.scala
Normal file
|
@ -0,0 +1,84 @@
|
|||
package lila.mod
|
||||
|
||||
import lila.db.Types.Coll
|
||||
import lila.game.Game
|
||||
import lila.user.User
|
||||
import chess.Color
|
||||
|
||||
import reactivemongo.bson._
|
||||
import scala.concurrent._
|
||||
|
||||
|
||||
final class BoostingApi(modApi: ModApi, collBoosting: Coll) {
|
||||
import BoostingApi._
|
||||
|
||||
private implicit val boostingRecordBSONHandler = Macros.handler[BoostingRecord]
|
||||
|
||||
def getBoostingRecord(id: String): Fu[Option[BoostingRecord]] =
|
||||
collBoosting.find(BSONDocument("_id" -> id))
|
||||
.one[BoostingRecord]
|
||||
|
||||
def createBoostRecord(record: BoostingRecord) =
|
||||
collBoosting.update(BSONDocument("_id" -> record.id), record, upsert = true).void
|
||||
|
||||
def determineBoosting(record: BoostingRecord, winner: User): Funit = {
|
||||
if (record.games >= 3) {
|
||||
modApi.autoBooster(winner.username)
|
||||
} else {
|
||||
funit
|
||||
}
|
||||
}
|
||||
|
||||
def boostingId(winner: User, loser: User): String = winner.id + "/" + loser.id
|
||||
|
||||
def check(game: Game, whiteUser: User, blackUser: User): Funit = {
|
||||
if (game.rated
|
||||
&& game.accountable
|
||||
&& game.playedTurns <= 10
|
||||
&& !game.isTournament
|
||||
&& game.winnerColor.isDefined) {
|
||||
game.winnerColor match {
|
||||
case Some(a) => {
|
||||
val result: GameResult = a match {
|
||||
case Color.White => GameResult(winner = whiteUser, loser = blackUser)
|
||||
case Color.Black => GameResult(winner = blackUser, loser = whiteUser)
|
||||
}
|
||||
val id = boostingId(result.winner, result.loser)
|
||||
getBoostingRecord(id).flatMap{
|
||||
case Some(record) =>
|
||||
val newRecord = BoostingRecord(
|
||||
_id = id,
|
||||
player = result.winner.id,
|
||||
games = record.games + 1
|
||||
)
|
||||
createBoostRecord(newRecord) >> determineBoosting(newRecord, result.winner)
|
||||
case none => createBoostRecord(BoostingRecord(
|
||||
_id = id,
|
||||
player = result.winner.id,
|
||||
games = 1
|
||||
))
|
||||
}
|
||||
}
|
||||
case none => funit
|
||||
}
|
||||
} else {
|
||||
funit
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object BoostingApi {
|
||||
|
||||
case class BoostingRecord(
|
||||
_id: String,
|
||||
player: String,
|
||||
games: Int) {
|
||||
def id = _id
|
||||
}
|
||||
|
||||
case class GameResult(
|
||||
winner: User,
|
||||
loser: User)
|
||||
|
||||
}
|
|
@ -15,6 +15,7 @@ final class Env(
|
|||
|
||||
private val CollectionPlayerAssessment = config getString "collection.crossref"
|
||||
private val CollectionResult = config getString "collection.result"
|
||||
private val CollectionBoosting = config getString "collection.boosting"
|
||||
private val CollectionModlog = config getString "collection.modlog"
|
||||
private val ActorName = config getString "actor.name"
|
||||
|
||||
|
@ -30,14 +31,23 @@ final class Env(
|
|||
firewall = firewall,
|
||||
lilaBus = system.lilaBus)
|
||||
|
||||
private lazy val boosting = new BoostingApi(
|
||||
modApi = api,
|
||||
collBoosting = db(CollectionBoosting))
|
||||
|
||||
// api actor
|
||||
system.actorOf(Props(new Actor {
|
||||
private val actorApi = system.actorOf(Props(new Actor {
|
||||
def receive = {
|
||||
case lila.hub.actorApi.mod.MarkCheater(userId) => api autoAdjust userId
|
||||
case lila.analyse.actorApi.AnalysisReady(game, analysis) =>
|
||||
assessApi.onAnalysisReady(game, analysis)
|
||||
case lila.game.actorApi.FinishGame(game, whiteUserOption, blackUserOption) =>
|
||||
(whiteUserOption |@| blackUserOption) apply {
|
||||
case (whiteUser, blackUser) => boosting.check(game, whiteUser, blackUser)
|
||||
}
|
||||
}
|
||||
}), name = ActorName)
|
||||
system.lilaBus.subscribe(actorApi, 'finishGame)
|
||||
}
|
||||
|
||||
object Env {
|
||||
|
|
|
@ -24,6 +24,18 @@ final class ModApi(
|
|||
case false => adjust("lichess", username)
|
||||
}
|
||||
|
||||
def adjustBooster(mod: String, username: String): Funit = withUser(username) { user =>
|
||||
logApi.booster(mod, user.id, !user.booster) zip
|
||||
UserRepo.toggleBooster(user.id) >>- {
|
||||
if (!user.booster) lilaBus.publish(lila.hub.actorApi.mod.MarkBooster(user.id), 'adjustBooster)
|
||||
} void
|
||||
}
|
||||
|
||||
def autoBooster(username: String): Funit = logApi.wasUnbooster(User.normalize(username)) flatMap {
|
||||
case true => funit
|
||||
case false => adjustBooster("lichess", username)
|
||||
}
|
||||
|
||||
def troll(mod: String, username: String): Fu[Boolean] = withUser(username) { u =>
|
||||
val user = u.copy(troll = !u.troll)
|
||||
((UserRepo updateTroll user) >>-
|
||||
|
|
|
@ -12,6 +12,8 @@ case class Modlog(
|
|||
def showAction = action match {
|
||||
case Modlog.engine => "mark as engine"
|
||||
case Modlog.unengine => "un-mark as engine"
|
||||
case Modlog.booster => "mark as booster"
|
||||
case Modlog.unbooster => "un-mark as booster"
|
||||
case Modlog.deletePost => "delete forum post"
|
||||
case Modlog.ban => "ban user"
|
||||
case Modlog.ipban => "ban IPs"
|
||||
|
@ -38,6 +40,8 @@ object Modlog {
|
|||
|
||||
val engine = "engine"
|
||||
val unengine = "unengine"
|
||||
val booster = "booster"
|
||||
val unbooster = "unbooster"
|
||||
val troll = "troll"
|
||||
val untroll = "untroll"
|
||||
val ban = "ban"
|
||||
|
|
|
@ -11,6 +11,10 @@ final class ModlogApi {
|
|||
Modlog(mod, user.some, v.fold(Modlog.engine, Modlog.unengine))
|
||||
}
|
||||
|
||||
def booster(mod: String, user: String, v: Boolean) = add {
|
||||
Modlog(mod, user.some, v.fold(Modlog.booster, Modlog.unbooster))
|
||||
}
|
||||
|
||||
def troll(mod: String, user: String, v: Boolean) = add {
|
||||
Modlog(mod, user.some, v.fold(Modlog.troll, Modlog.untroll))
|
||||
}
|
||||
|
@ -73,6 +77,11 @@ final class ModlogApi {
|
|||
"action" -> Modlog.unengine
|
||||
))
|
||||
|
||||
def wasUnbooster(userId: String) = $count.exists(Json.obj(
|
||||
"user" -> userId,
|
||||
"action" -> Modlog.unbooster
|
||||
))
|
||||
|
||||
def assessGame(mod: String, gameId: String, side: String, assessment: Int) = add {
|
||||
val assessmentString = assessment match {
|
||||
case 1 => "Not cheating"
|
||||
|
|
|
@ -18,6 +18,7 @@ object Permission {
|
|||
case object UserEvaluate extends Permission("ROLE_USER_EVALUATE")
|
||||
case object MarkTroll extends Permission("ROLE_CHAT_BAN", List(UserSpy))
|
||||
case object MarkEngine extends Permission("ROLE_ADJUST_CHEATER", List(UserSpy))
|
||||
case object MarkBooster extends Permission("ROLE_ADJUST_BOOSTER", List(UserSpy))
|
||||
case object IpBan extends Permission("ROLE_IP_BAN", List(UserSpy))
|
||||
case object CloseAccount extends Permission("ROLE_CLOSE_ACCOUNT", List(UserSpy))
|
||||
case object ReopenAccount extends Permission("ROLE_REOPEN_ACCOUNT", List(UserSpy))
|
||||
|
@ -25,10 +26,10 @@ object Permission {
|
|||
case object SeeReport extends Permission("ROLE_SEE_REPORT")
|
||||
|
||||
case object Hunter extends Permission("ROLE_HUNTER", List(
|
||||
ViewBlurs, MarkEngine, StaffForum, UserSpy, UserEvaluate, SeeReport))
|
||||
ViewBlurs, MarkEngine, MarkBooster, StaffForum, UserSpy, UserEvaluate, SeeReport))
|
||||
|
||||
case object Admin extends Permission("ROLE_ADMIN", List(
|
||||
ViewBlurs, MarkTroll, MarkEngine, StaffForum, ModerateForum, UserSpy,
|
||||
ViewBlurs, MarkTroll, MarkEngine, MarkBooster, StaffForum, ModerateForum, UserSpy,
|
||||
UserEvaluate, SeeReport, IpBan, CloseAccount, ReopenAccount, SetTitle,
|
||||
ModerateQa))
|
||||
|
||||
|
@ -36,7 +37,7 @@ object Permission {
|
|||
|
||||
private lazy val all: List[Permission] = List(
|
||||
SuperAdmin, Admin, Hunter, ViewBlurs, StaffForum, ModerateForum,
|
||||
UserSpy, MarkTroll, MarkEngine, IpBan, ModerateQa)
|
||||
UserSpy, MarkTroll, MarkEngine, MarkBooster, IpBan, ModerateQa)
|
||||
|
||||
private lazy val allByName: Map[String, Permission] = all map { p => (p.name, p) } toMap
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ case class User(
|
|||
roles: List[String],
|
||||
profile: Option[Profile] = None,
|
||||
engine: Boolean = false,
|
||||
booster: Boolean = false,
|
||||
toints: Int = 0,
|
||||
playTime: Option[User.PlayTime] = None,
|
||||
title: Option[String] = None,
|
||||
|
@ -63,6 +64,8 @@ case class User(
|
|||
def timeNoSee: Duration = seenAt.fold[Duration](Duration.Inf) { s =>
|
||||
(nowMillis - s.getMillis).millis
|
||||
}
|
||||
|
||||
def lame = booster || engine
|
||||
}
|
||||
|
||||
object User {
|
||||
|
@ -110,6 +113,7 @@ object User {
|
|||
val roles = "roles"
|
||||
val profile = "profile"
|
||||
val engine = "engine"
|
||||
val booster = "booster"
|
||||
val toints = "toints"
|
||||
val playTime = "time"
|
||||
val createdAt = "createdAt"
|
||||
|
@ -142,6 +146,7 @@ object User {
|
|||
roles = ~r.getO[List[String]](roles),
|
||||
profile = r.getO[Profile](profile),
|
||||
engine = r boolD engine,
|
||||
booster = r boolD booster,
|
||||
toints = r nIntD toints,
|
||||
playTime = r.getO[PlayTime](playTime),
|
||||
createdAt = r date createdAt,
|
||||
|
@ -160,6 +165,7 @@ object User {
|
|||
roles -> o.roles.some.filter(_.nonEmpty),
|
||||
profile -> o.profile,
|
||||
engine -> w.boolO(o.engine),
|
||||
booster -> w.boolO(o.booster),
|
||||
toints -> w.intO(o.toints),
|
||||
playTime -> o.playTime,
|
||||
createdAt -> o.createdAt,
|
||||
|
|
|
@ -222,6 +222,10 @@ trait UserRepo {
|
|||
$setBson("engine" -> BSONBoolean(!u.engine))
|
||||
}
|
||||
|
||||
def toggleBooster(id: ID): Funit = $update.docBson[ID, User](id) { u =>
|
||||
$setBson("booster" -> BSONBoolean(!u.booster))
|
||||
}
|
||||
|
||||
def toggleIpBan(id: ID) = $update.doc[ID, User](id) { u => $set("ipBan" -> !u.ipBan) }
|
||||
|
||||
def updateTroll(user: User) = $update.field(user.id, "troll", user.troll)
|
||||
|
|
Loading…
Reference in a new issue