improve cheat detection

This commit is contained in:
Thibault Duplessis 2014-01-04 14:31:27 +01:00
parent 9b549b0a5a
commit c9851c74c6
7 changed files with 76 additions and 62 deletions

View file

@ -46,6 +46,6 @@ object $find {
// useful in capped collections
// def recent[A: TubeInColl](max: Int): Fu[List[A]] = (
// pimpQB($query.all).sort($sort.naturalOrder)
// pimpQB($query.all).sort($sort.naturalOrder)
// ).cursor[Option[A]] toList max map (_.flatten)
}

View file

@ -38,7 +38,7 @@ trait $operator {
def $all[A: Writes](values: Iterable[A]) = Json.obj("$all" -> values)
def $exists(bool: Boolean) = Json.obj("$exists" -> bool)
def $or[A: Writes](conditions: Iterable[A]) = Json.obj("$or" -> conditions)
def $or[A: Writes](conditions: Iterable[A]): JsObject = Json.obj("$or" -> conditions)
def $regex(value: String, flags: String = "") = BSONFormats toJSON BSONRegex(value, flags)

View file

@ -12,6 +12,8 @@ object $query {
def apply[A: InColl](q: JsObject) = builder query q
def apply[A: InColl](q: BSONDocument) = builder query q
def byId[A: InColl, B: Writes](id: B) = apply($select byId id)
def byIds[A: InColl, B: Writes](ids: Iterable[B]) = apply($select byIds ids)

View file

@ -9,7 +9,7 @@ import org.joda.time.DateTime
import play.api.libs.json._
import play.modules.reactivemongo.json.BSONFormats.toJSON
import play.modules.reactivemongo.json.ImplicitBSONHandlers.JsObjectWriter
import reactivemongo.bson.{ BSONDocument, BSONBinary, BSONInteger }
import reactivemongo.bson.{ BSONDocument, BSONArray, BSONBinary, BSONInteger }
import lila.common.PimpedJson._
import lila.db.api._
@ -29,6 +29,7 @@ trait GameRepo {
type ID = String
import Game._
import Game.{ BSONFields F }
def game(gameId: ID): Fu[Option[Game]] = $find byId gameId
@ -70,7 +71,7 @@ trait GameRepo {
)
def chronologicalFinishedByUser(userId: String): Fu[List[Game]] = $find(
$query(Query.finished ++ Query.rated ++ Query.user(userId)) sort ($sort asc BSONFields.createdAt)
$query(Query.finished ++ Query.rated ++ Query.user(userId)) sort ($sort asc F.createdAt)
)
def save(progress: Progress): Funit =
@ -87,22 +88,22 @@ trait GameRepo {
def setRatingDiffs(id: ID, white: Int, black: Int) =
$update($select(id), BSONDocument("$set" -> BSONDocument(
s"p0.${Player.BSONFields.ratingDiff}" -> BSONInteger(white),
s"p1.${Player.BSONFields.ratingDiff}" -> BSONInteger(black))))
s"${F.whitePlayer}.${Player.BSONFields.ratingDiff}" -> BSONInteger(white),
s"${F.blackPlayer}.${Player.BSONFields.ratingDiff}" -> BSONInteger(black))))
def setUsers(id: ID, white: Option[(String, Int)], black: Option[(String, Int)]) =
if (white.isDefined || black.isDefined) $update($select(id), BSONDocument("$set" -> BSONDocument(
s"p0.${Player.BSONFields.rating}" -> white.map(_._2).map(BSONInteger.apply),
s"p1.${Player.BSONFields.rating}" -> black.map(_._2).map(BSONInteger.apply),
BSONFields.playerUids -> lila.db.BSON.writer.listO(List(~white.map(_._1), ~black.map(_._1)))
s"${F.whitePlayer}.${Player.BSONFields.rating}" -> white.map(_._2).map(BSONInteger.apply),
s"${F.blackPlayer}.${Player.BSONFields.rating}" -> black.map(_._2).map(BSONInteger.apply),
F.playerUids -> lila.db.BSON.writer.listO(List(~white.map(_._1), ~black.map(_._1)))
)))
else funit
def setTv(id: ID) {
$update.fieldUnchecked(id, BSONFields.tvAt, $date(DateTime.now))
$update.fieldUnchecked(id, F.tvAt, $date(DateTime.now))
}
def onTv(nb: Int): Fu[List[Game]] = $find($query.all sort $sort.desc(BSONFields.tvAt), nb)
def onTv(nb: Int): Fu[List[Game]] = $find($query.all sort $sort.desc(F.tvAt), nb)
def incBookmarks(id: ID, value: Int) =
$update($select(id), $incBson("bm" -> value))
@ -111,11 +112,11 @@ trait GameRepo {
$select(id),
BSONDocument(
"$set" -> BSONDocument(
Game.BSONFields.winnerId -> winnerId,
Game.BSONFields.winnerColor -> winnerColor.map(_.white)
F.winnerId -> winnerId,
F.winnerColor -> winnerColor.map(_.white)
),
"$unset" -> BSONDocument(
Game.BSONFields.positionHashes -> true,
F.positionHashes -> true,
("p0." + Player.BSONFields.lastDrawOffer) -> true,
("p1." + Player.BSONFields.lastDrawOffer) -> true,
("p0." + Player.BSONFields.isOfferingDraw) -> true,
@ -143,7 +144,7 @@ trait GameRepo {
def saveNext(game: Game, nextId: ID): Funit = $update(
$select(game.id),
$set(BSONFields.next -> nextId) ++
$set(F.next -> nextId) ++
$unset("p0." + Player.BSONFields.isOfferingRematch, "p1." + Player.BSONFields.isOfferingRematch)
)
@ -152,8 +153,8 @@ trait GameRepo {
def featuredCandidates: Fu[List[Game]] = $find(
Query.playable ++ Query.clock(true) ++ Query.turnsGt(1) ++ Json.obj(
BSONFields.createdAt -> $gt($date(DateTime.now - 3.minutes)),
BSONFields.updatedAt -> $gt($date(DateTime.now - 15.seconds))
F.createdAt -> $gt($date(DateTime.now - 3.minutes)),
F.updatedAt -> $gt($date(DateTime.now - 15.seconds))
))
def count(query: Query.type JsObject): Fu[Int] = $count(query(Query))
@ -162,24 +163,23 @@ trait GameRepo {
((days to 1 by -1).toList map { day
val from = DateTime.now.withTimeAtStartOfDay - day.days
val to = from + 1.day
$count(Json.obj(BSONFields.createdAt -> ($gte($date(from)) ++ $lt($date(to)))))
$count(Json.obj(F.createdAt -> ($gte($date(from)) ++ $lt($date(to)))))
}).sequenceFu
def nowPlaying(userId: String): Fu[Option[Game]] =
$find.one(Query.status(Status.Started) ++ Query.user(userId) ++ Json.obj(
BSONFields.createdAt -> $gt($date(DateTime.now - 30.minutes))
F.createdAt -> $gt($date(DateTime.now - 30.minutes))
))
def bestOpponents(userId: String, limit: Int): Fu[List[(String, Int)]] = {
import reactivemongo.bson._
import reactivemongo.core.commands._
import BSONFields.playerUids
val command = Aggregate(gameTube.coll.name, Seq(
Match(BSONDocument(playerUids -> userId)),
Match(BSONDocument(playerUids -> BSONDocument("$size" -> 2))),
Unwind(playerUids),
Match(BSONDocument(playerUids -> BSONDocument("$ne" -> userId))),
GroupField(playerUids)("gs" -> SumValue(1)),
Match(BSONDocument(F.playerUids -> userId)),
Match(BSONDocument(F.playerUids -> BSONDocument("$size" -> 2))),
Unwind(F.playerUids),
Match(BSONDocument(F.playerUids -> BSONDocument("$ne" -> userId))),
GroupField(F.playerUids)("gs" -> SumValue(1)),
Sort(Seq(Descending("gs"))),
Limit(limit)
))
@ -195,18 +195,23 @@ trait GameRepo {
}
def random: Fu[Option[Game]] = $find.one(
Json.obj(BSONFields.playerUids -> $exists(true)),
Json.obj(F.playerUids -> $exists(true)),
_ sort Query.sortCreated skip (Random nextInt 1000)
)
def findMirror(game: Game): Fu[Option[Game]] = $find.one(
Query.users(game.userIds) ++ Query.status(Status.Started) ++ Json.obj(
BSONFields.turns -> game.turns,
BSONFields.id -> $ne(game.id),
BSONFields.binaryPieces -> game.binaryPieces,
BSONFields.createdAt -> $gt($date(DateTime.now - 1.hour)),
BSONFields.updatedAt -> $gt($date(DateTime.now - 5.minutes))
))
def findMirror(game: Game): Fu[Option[Game]] = $find.one($query(
BSONDocument(
F.id -> BSONDocument("$ne" -> game.id),
F.playerUids -> BSONDocument("$in" -> game.userIds),
F.status -> Status.Started.id,
F.createdAt -> BSONDocument("$gt" -> (DateTime.now - 15.minutes)),
F.updatedAt -> BSONDocument("$gt" -> (DateTime.now - 5.minutes)),
"$or" -> BSONArray(
BSONDocument(s"${F.whitePlayer}.ai" -> BSONDocument("$exists" -> true)),
BSONDocument(s"${F.blackPlayer}.ai" -> BSONDocument("$exists" -> true))
)
)
))
def getPgn(id: ID): Fu[PgnMoves] = getOptionPgn(id) map (~_)
@ -216,8 +221,8 @@ trait GameRepo {
def getOptionPgn(id: ID): Fu[Option[PgnMoves]] =
gameTube.coll.find(
$select(id), Json.obj(
BSONFields.id -> false,
BSONFields.binaryPgn -> true
F.id -> false,
F.binaryPgn -> true
)
).one[BSONDocument] map { _ flatMap extractPgnMoves }
@ -238,8 +243,8 @@ trait GameRepo {
def lastGameBetween(u1: String, u2: String, since: DateTime): Fu[Option[Game]] = {
$find.one(Json.obj(
BSONFields.playerUids -> Json.obj("$all" -> List(u1, u2)),
BSONFields.createdAt -> Json.obj("$gt" -> $date(since))
F.playerUids -> Json.obj("$all" -> List(u1, u2)),
F.createdAt -> Json.obj("$gt" -> $date(since))
))
}
@ -249,15 +254,15 @@ trait GameRepo {
import lila.db.BSON.BSONJodaDateTimeHandler
val command = Aggregate(gameTube.coll.name, Seq(
Match(BSONDocument(
BSONFields.createdAt -> BSONDocument("$gt" -> since),
BSONFields.status -> BSONDocument("$gte" -> chess.Status.Mate.id),
s"${BSONFields.playerUids}.0" -> BSONDocument("$exists" -> true)
F.createdAt -> BSONDocument("$gt" -> since),
F.status -> BSONDocument("$gte" -> chess.Status.Mate.id),
s"${F.playerUids}.0" -> BSONDocument("$exists" -> true)
)),
Unwind(BSONFields.playerUids),
Unwind(F.playerUids),
Match(BSONDocument(
BSONFields.playerUids -> BSONDocument("$ne" -> "")
F.playerUids -> BSONDocument("$ne" -> "")
)),
GroupField(Game.BSONFields.playerUids)("nb" -> SumValue(1)),
GroupField(F.playerUids)("nb" -> SumValue(1)),
Sort(Seq(Descending("nb"))),
Limit(max)
))
@ -294,7 +299,7 @@ trait GameRepo {
// }""",
// query = Some(JsObjectWriter write {
// Json.obj(
// BSONFields.createdAt -> $gte($date(DateTime.now - minutes.minutes))
// F.createdAt -> $gte($date(DateTime.now - minutes.minutes))
// ) ++ $or(List(Json.obj("p0.e" -> $exists(true)), Json.obj("p1.e" -> $exists(true))))
// })
// )
@ -306,7 +311,7 @@ trait GameRepo {
// }
private def extractPgnMoves(doc: BSONDocument) =
doc.getAs[BSONBinary](BSONFields.binaryPgn) map { bin
doc.getAs[BSONBinary](F.binaryPgn) map { bin
BinaryFormat.pgn read { ByteArray.ByteArrayBSONHandler read bin }
}
@ -321,10 +326,10 @@ trait GameRepo {
val userIds = List(user1, user2).sortBy(_._2).map(_._1)
val command = Aggregate(gameTube.coll.name, Seq(
Match(BSONDocument(
BSONFields.playerUids -> BSONDocument("$all" -> userIds),
BSONFields.status -> BSONDocument("$gte" -> chess.Status.Mate.id)
F.playerUids -> BSONDocument("$all" -> userIds),
F.status -> BSONDocument("$gte" -> chess.Status.Mate.id)
)),
GroupField(Game.BSONFields.winnerId)("nb" -> SumValue(1))
GroupField(F.winnerId)("nb" -> SumValue(1))
))
gameTube.coll.db.command(command) map { stream
val res = (stream.toList map { obj

View file

@ -48,18 +48,18 @@ object Query {
def clock(c: Boolean) = Json.obj(F.clock -> $exists(c))
def user(u: String) = Json.obj("us" -> u)
def users(u: Seq[String]) = Json.obj("us" -> $in(u))
def user(u: String) = Json.obj(F.playerUids -> u)
def users(u: Seq[String]) = Json.obj(F.playerUids -> $in(u))
// use the us index
def win(u: String) = user(u) ++ Json.obj("wid" -> u)
def win(u: String) = user(u) ++ Json.obj(F.winnerId -> u)
def loss(u: String) = user(u) ++
Json.obj(F.status -> $in(Status.finishedWithWinner map (_.id))) ++
Json.obj("wid" -> $ne(u))
Json.obj(F.winnerId -> $ne(u))
def opponents(u1: User, u2: User) =
Json.obj("us" -> $all(List(u1, u2).sortBy(_.count.game).map(_.id)))
Json.obj(F.playerUids -> $all(List(u1, u2).sortBy(_.count.game).map(_.id)))
def turnsGt(nb: Int) = Json.obj(F.turns -> $gt(nb))
@ -75,7 +75,7 @@ object Query {
}
def unplayed = Json.obj(
"t" -> $lt(2),
F.turns -> $lt(2),
F.createdAt -> (
$lt($date(DateTime.now - 24.hours)) ++
$gt($date(DateTime.now - 25.hours)))

View file

@ -10,14 +10,20 @@ private[round] final class CheatDetector {
def apply(game: Game): Fu[Option[Color]] = interresting(game) ?? {
GameRepo findMirror game map {
_ ?? { mirror
def playerUsingAi = mirror.hasAi ?? mirror.players find (_.isHuman)
def playerByIds = mirror.players find (p p.userId ?? game.userIds.contains)
(playerUsingAi orElse playerByIds) map (!_.color)
mirror.players find (p p.userId ?? game.userIds.contains) match {
case Some(player) {
val color = !player.color
play.api.Logger("cheat detector").info(s"$color @ ${game.id} uses ${mirror.id}")
Some(color)
}
case None None
}
}
}
}
private val TURNS = 12
private val TURNS_MODULUS = 10
private def interresting(game: Game) = game.turns == TURNS && game.rated
private def interresting(game: Game) =
game.rated && game.turns > 0 && (game.turns % TURNS_MODULUS == 0)
}

5
todo
View file

@ -114,9 +114,10 @@ hide chat help after /help typed
copy rematch chats
chat logs
chat visited link color
chat click on username tries a private chat
chat mod can query anybody
chat dark theme tick bg
chat system messages compaction
redirect user to stored language
fix auto cheat detection
deploy
------