playban WIP

pull/450/head
Thibault Duplessis 2015-04-25 23:06:44 +02:00
parent c723fbd2ca
commit cdf23799cd
5 changed files with 106 additions and 8 deletions

View File

@ -6,8 +6,10 @@ import reactivemongo.bson.Macros
import reactivemongo.core.commands._
import scala.concurrent.duration._
import chess.Color
import lila.db.BSON._
import lila.db.Types.Coll
import lila.game.{ Pov, Game, Player, Source }
final class PlaybanApi(coll: Coll) {
@ -17,23 +19,72 @@ final class PlaybanApi(coll: Coll) {
def read(bsonInt: BSONInteger): Outcome = Outcome(bsonInt.value) err s"No such playban outcome: ${bsonInt.value}"
def write(x: Outcome) = BSONInteger(x.id)
}
private implicit val banBSONHandler = Macros.handler[TempBan]
private implicit val UserRecordBSONHandler = Macros.handler[UserRecord]
def record(userId: String, outcome: Outcome): Funit = coll.db.command {
private case class Blame(player: Player, outcome: Outcome)
private def blameable(game: Game) = game.source == Source.Lobby && game.hasClock
def abort(pov: Pov): Funit = blameable(pov.game) ?? {
val blame =
if (pov.game olderThan 45) pov.game.playerWhoDidNotMove map { Blame(_, Outcome.NoPlay) }
else if (pov.game olderThan 15) none
else pov.player.some map { Blame(_, Outcome.Abort) }
blame match {
case None => pov.game.userIds.map(save(Outcome.Good)).sequenceFu.void
case Some(Blame(player, outcome)) =>
player.userId.??(save(outcome)) >>
pov.game.opponent(player).userId.??(save(Outcome.Good))
}
}
def currentBan(userId: String): Fu[Option[TempBan]] = coll.find(
BSONDocument("_id" -> userId, "b.0" -> BSONDocument("$exists" -> true)),
BSONDocument("_id" -> false, "b" -> BSONDocument("$slice" -> -1))
).one[BSONDocument].map {
_.flatMap(_.getAs[List[TempBan]]("b")).??(_.find(_.inEffect))
}
def bans(userId: String): Fu[List[TempBan]] = coll.find(
BSONDocument("_id" -> userId, "b.0" -> BSONDocument("$exists" -> true)),
BSONDocument("_id" -> false, "b" -> true)
).one[BSONDocument].map {
~_.flatMap(_.getAs[List[TempBan]]("b"))
}
private def save(outcome: Outcome): String => Funit = userId => coll.db.command {
FindAndModify(
collection = coll.name,
query = BSONDocument("_id" -> userId),
modify = Update(
update = BSONDocument("$push" -> BSONDocument(
"h" -> BSONDocument(
"o" -> BSONDocument(
"$each" -> List(outcome),
"$slice" -> -30)
"$slice" -> -20)
)),
fetchNewObject = true),
upsert = true
)
} map2 UserRecordBSONHandler.read flatMap {
case None => fufail(s"can't find record for user $userId")
case Some(userRecord) => funit
case None => fufail(s"can't find record for user $userId")
case Some(record) => legiferate(record)
} logFailure "PlaybanApi"
private def legiferate(record: UserRecord): Funit = record.newBan ?? { ban =>
loginfo(s"[playban] ban ${record.userId} for {$ban.mins} minutes")
coll.update(
BSONDocument("_id" -> record.userId),
BSONDocument(
"$unset" -> BSONDocument("o" -> true),
"$push" -> BSONDocument(
"b" -> BSONDocument(
"$each" -> List(ban),
"$slice" -> -30)
)
)
).void
}
}

View File

@ -1,11 +1,52 @@
package lila.playban
import org.joda.time.DateTime
case class UserRecord(
_id: String,
h: List[Outcome]) {
o: List[Outcome],
b: List[TempBan]) {
def userId = _id
def history = h
def outcomes = o
def bans = b
def banInEffect = bans.lastOption.??(_.inEffect)
lazy val nbOutcomes = outcomes.size
lazy val nbBadOutcomes = outcomes.count(_ != Outcome.Good)
def badOutcomeRatio: Double =
if (nbOutcomes == 0) 0
else nbBadOutcomes.toDouble / nbOutcomes
def nbBadOutcomesBeforeBan = if (bans.isEmpty) 5 else 3
def newBan: Option[TempBan] = {
!banInEffect &&
nbBadOutcomes >= nbBadOutcomesBeforeBan &&
badOutcomeRatio > 1d / 3
} option bans.lastOption.filterNot(_.isOld).fold(TempBan(5)) { prev =>
TempBan(prev.mins * 2)
}
}
case class TempBan(
date: DateTime,
mins: Int) {
lazy val endsAt = date plusMinutes mins
def remainingSeconds: Int = (endsAt.getSeconds - nowSeconds).toInt max 0
def inEffect = endsAt isBefore DateTime.now
def isOld = date isBefore DateTime.now.minusDays(1)
}
object TempBan {
def apply(minutes: Int): TempBan = TempBan(DateTime.now, minutes min 120)
}
sealed abstract class Outcome(

View File

@ -19,6 +19,7 @@ final class Env(
ai: lila.ai.Client,
aiPerfApi: lila.ai.AiPerfApi,
crosstableApi: lila.game.CrosstableApi,
playban: lila.playban.PlaybanApi,
lightUser: String => Option[lila.common.LightUser],
userJsonView: lila.user.JsonView,
uciMemo: lila.game.UciMemo,
@ -113,6 +114,7 @@ final class Env(
perfsUpdater = perfsUpdater,
aiPerfApi = aiPerfApi,
crosstableApi = crosstableApi,
playban = playban,
bus = system.lilaBus,
timeline = hub.actor.timeline,
casualOnly = CasualOnly)
@ -187,6 +189,7 @@ object Env {
ai = lila.ai.Env.current.client,
aiPerfApi = lila.ai.Env.current.aiPerfApi,
crosstableApi = lila.game.Env.current.crosstableApi,
playban = lila.playban.Env.current.api,
lightUser = lila.user.Env.current.lightUser,
userJsonView = lila.user.Env.current.jsonView,
uciMemo = lila.game.Env.current.uciMemo,

View File

@ -9,12 +9,14 @@ import lila.game.actorApi.{ FinishGame, AbortedBy }
import lila.game.tube.gameTube
import lila.game.{ GameRepo, Game, Pov, Event }
import lila.i18n.I18nKey.{ Select => SelectI18nKey }
import lila.playban.{ PlaybanApi, Outcome }
import lila.user.tube.userTube
import lila.user.{ User, UserRepo, Perfs }
private[round] final class Finisher(
messenger: Messenger,
perfsUpdater: PerfsUpdater,
playban: PlaybanApi,
aiPerfApi: lila.ai.AiPerfApi,
crosstableApi: lila.game.CrosstableApi,
bus: lila.common.Bus,
@ -22,6 +24,7 @@ private[round] final class Finisher(
casualOnly: Boolean) {
def abort(pov: Pov): Fu[Events] = apply(pov.game, _.Aborted) addEffect { _ =>
playban.abort(pov)
bus.publish(AbortedBy(pov), 'abortGame)
}

View File

@ -203,7 +203,7 @@ object ApplicationBuild extends Build {
libraryDependencies ++= provided(play.api, RM, PRM)
)
lazy val playban = project("playban", Seq(common, db)).settings(
lazy val playban = project("playban", Seq(common, db, game)).settings(
libraryDependencies ++= provided(play.api, RM, PRM)
)