Merge pull request #308 from ornicar/BoostingDetector

Boosting detector
This commit is contained in:
Thibault Duplessis 2015-02-14 15:24:29 +01:00
commit 424f740d72
18 changed files with 165 additions and 8 deletions

View file

@ -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)

View file

@ -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")" />

View file

@ -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>

View file

@ -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

View file

@ -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)

View file

@ -45,6 +45,7 @@ case class Check(userId: String)
package mod {
case class MarkCheater(userId: String)
case class MarkBooster(userId: String)
}
package setup {

View file

@ -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")

View file

@ -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 =>

View file

@ -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,

View file

@ -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)
}

View 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)
}

View file

@ -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 {

View file

@ -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) >>-

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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,

View file

@ -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)