lila/modules/playban/src/main/PlaybanApi.scala

275 lines
9.6 KiB
Scala
Raw Normal View History

2015-04-25 12:48:13 -06:00
package lila.playban
2019-11-29 19:16:11 -07:00
import reactivemongo.api.bson._
2019-08-21 04:44:19 -06:00
import scala.concurrent.duration._
2015-04-25 12:48:13 -06:00
2019-12-13 07:30:20 -07:00
import chess.{ Color, Status }
2019-12-02 11:48:11 -07:00
import lila.common.{ Bus, Iso, Uptime }
import lila.db.dsl._
2019-12-13 07:30:20 -07:00
import lila.game.{ Game, Player, Pov, Source }
import lila.message.{ MessageApi, ModPreset }
import lila.user.{ User, UserRepo }
2015-04-25 12:48:13 -06:00
import org.joda.time.DateTime
2015-04-27 06:07:35 -06:00
final class PlaybanApi(
coll: Coll,
2017-09-11 23:09:46 -06:00
sandbag: SandbagWatch,
feedback: PlaybanFeedback,
2019-12-01 11:03:39 -07:00
userRepo: UserRepo,
2019-12-23 18:01:45 -07:00
cacheApi: lila.memo.CacheApi,
messenger: MessageApi
)(implicit ec: scala.concurrent.ExecutionContext) {
2015-04-25 12:48:13 -06:00
import lila.db.BSON.BSONJodaDateTimeHandler
2019-11-29 19:16:11 -07:00
import reactivemongo.api.bson.Macros
2019-12-13 07:30:20 -07:00
implicit private val OutcomeBSONHandler = tryHandler[Outcome](
2019-12-01 11:03:39 -07:00
{ case BSONInteger(v) => Outcome(v) toTry s"No such playban outcome: $v" },
x => BSONInteger(x.id)
)
2019-12-13 07:30:20 -07:00
implicit private val RageSitBSONHandler = intIsoHandler(Iso.int[RageSit](RageSit.apply, _.counter))
implicit private val BanBSONHandler = Macros.handler[TempBan]
implicit private val UserRecordBSONHandler = Macros.handler[UserRecord]
2015-04-25 12:48:13 -06:00
2015-04-25 15:06:44 -06:00
private case class Blame(player: Player, outcome: Outcome)
2017-09-11 23:09:46 -06:00
private val blameableSources: Set[Source] = Set(Source.Lobby, Source.Pool, Source.Tournament)
2016-11-29 18:07:23 -07:00
private def blameable(game: Game): Fu[Boolean] =
(game.source.exists(s => blameableSources(s)) && game.hasClock) ?? {
2018-03-16 17:20:53 -06:00
if (game.rated) fuTrue
else userRepo.containsEngine(game.userIds) dmap (!_)
}
private def IfBlameable[A: ornicar.scalalib.Zero](game: Game)(f: => Fu[A]): Fu[A] =
2019-12-02 11:48:11 -07:00
Uptime.startedSinceMinutes(10) ?? {
2018-03-12 06:16:58 -06:00
blameable(game) flatMap { _ ?? f }
}
2015-04-25 15:06:44 -06:00
def abort(pov: Pov, isOnGame: Set[Color]): Funit = IfBlameable(pov.game) {
pov.player.userId.ifTrue(isOnGame(pov.opponent.color)) ?? { userId =>
save(Outcome.Abort, userId, RageSit.Reset) >>- feedback.abort(pov)
}
}
def noStart(pov: Pov): Funit = IfBlameable(pov.game) {
pov.player.userId ?? { userId =>
save(Outcome.NoPlay, userId, RageSit.Reset) >>- feedback.noStart(pov)
}
2015-04-26 05:04:22 -06:00
}
2015-04-25 15:06:44 -06:00
2017-09-11 23:09:46 -06:00
def rageQuit(game: Game, quitterColor: Color): Funit =
sandbag(game, quitterColor) >> IfBlameable(game) {
game.player(quitterColor).userId ?? { userId =>
save(Outcome.RageQuit, userId, RageSit.imbalanceInc(game, quitterColor)) >>-
2019-09-26 13:45:53 -06:00
feedback.rageQuit(Pov(game, quitterColor))
}
2017-09-11 23:09:46 -06:00
}
2015-04-25 15:06:44 -06:00
2017-10-18 13:02:59 -06:00
def flag(game: Game, flaggerColor: Color): Funit = {
def unreasonableTime = game.clock map { c =>
(c.estimateTotalSeconds / 12) atLeast 15 atMost (3 * 60)
2017-10-18 13:02:59 -06:00
}
// flagged after waiting a long time
2019-12-13 07:30:20 -07:00
def sitting: Option[Funit] =
for {
userId <- game.player(flaggerColor).userId
seconds = nowSeconds - game.movedAt.getSeconds
if unreasonableTime.exists(seconds >= _)
} yield save(Outcome.Sitting, userId, RageSit.imbalanceInc(game, flaggerColor)) >>-
2019-12-13 07:30:20 -07:00
feedback.sitting(Pov(game, flaggerColor)) >>-
propagateSitting(game, userId)
2017-10-18 13:02:59 -06:00
// flagged after waiting a short time;
// but the previous move used a long time.
// assumes game was already checked for sitting
2019-12-13 07:30:20 -07:00
def sitMoving: Option[Funit] =
game.player(flaggerColor).userId.ifTrue {
~(for {
movetimes <- game moveTimes flaggerColor
lastMovetime <- movetimes.lastOption
limit <- unreasonableTime
} yield lastMovetime.toSeconds >= limit)
} map { userId =>
save(Outcome.SitMoving, userId, RageSit.imbalanceInc(game, flaggerColor)) >>-
2019-12-13 07:30:20 -07:00
feedback.sitting(Pov(game, flaggerColor)) >>-
propagateSitting(game, userId)
}
2017-10-18 13:02:59 -06:00
2017-10-18 17:16:52 -06:00
sandbag(game, flaggerColor) flatMap { isSandbag =>
IfBlameable(game) {
sitting orElse
sitMoving getOrElse
goodOrSandbag(game, flaggerColor, isSandbag)
2017-10-18 13:02:59 -06:00
}
2017-09-11 23:09:46 -06:00
}
2017-10-18 13:02:59 -06:00
}
2019-09-26 13:45:53 -06:00
private def propagateSitting(game: Game, userId: User.ID): Funit =
rageSitCache get userId map { rageSit =>
2019-11-29 17:07:51 -07:00
if (rageSit.isBad) Bus.publish(SittingDetected(game, userId), "playban")
}
2017-09-11 23:09:46 -06:00
def other(game: Game, status: Status.type => Status, winner: Option[Color]): Funit =
2019-12-13 07:30:20 -07:00
winner.?? { w =>
sandbag(game, !w)
} flatMap { isSandbag =>
2017-10-18 17:16:52 -06:00
IfBlameable(game) {
~(for {
2020-01-04 19:26:18 -07:00
w <- winner
loser = game.player(!w)
loserId <- loser.userId
} yield {
if (Status.NoStart is status)
save(Outcome.NoPlay, loserId, RageSit.Reset) >>- feedback.noStart(Pov(game, !w))
2020-01-04 19:26:18 -07:00
else
game.clock
.ifTrue(Status.Resign.is(status) && game.turnOf(loser))
.map { c =>
(c.estimateTotalSeconds / 10) atLeast 15 atMost (3 * 60)
}
2020-01-04 19:45:50 -07:00
.exists(_ < nowSeconds - game.movedAt.getSeconds)
2020-01-04 19:26:18 -07:00
.option {
2020-01-04 19:45:50 -07:00
save(Outcome.SitResign, loserId, RageSit.imbalanceInc(game, loser.color)) >>-
2020-01-04 19:26:18 -07:00
feedback.sitting(Pov(game, loser.color)) >>-
propagateSitting(game, loserId)
}
.getOrElse {
goodOrSandbag(game, !w, isSandbag)
}
})
2017-10-18 17:16:52 -06:00
}
2017-09-11 23:09:46 -06:00
}
2015-04-25 15:06:44 -06:00
2019-09-27 01:16:53 -06:00
private def goodOrSandbag(game: Game, loserColor: Color, isSandbag: Boolean): Funit =
game.player(loserColor).userId ?? { userId =>
if (isSandbag) feedback.sandbag(Pov(game, loserColor))
val rageSitDelta =
if (isSandbag) RageSit.Reset
else RageSit.redeem(game)
2019-09-27 01:16:53 -06:00
save(if (isSandbag) Outcome.Sandbag else Outcome.Good, userId, rageSitDelta)
2017-10-18 17:16:52 -06:00
}
2019-08-21 04:44:19 -06:00
// memorize users without any ban to save DB reads
private val cleanUserIds = new lila.memo.ExpireSetMemo(30 minutes)
def currentBan(userId: User.ID): Fu[Option[TempBan]] = !cleanUserIds.get(userId) ?? {
2019-12-13 07:30:20 -07:00
coll.ext
.find(
$doc("_id" -> userId, "b.0" $exists true),
$doc("_id" -> false, "b" -> $doc("$slice" -> -1))
)
.one[Bdoc]
.dmap {
2019-12-01 11:03:39 -07:00
_.flatMap(_.getAsOpt[List[TempBan]]("b")).??(_.find(_.inEffect))
2019-08-21 04:44:19 -06:00
} addEffect { ban =>
2019-12-13 07:30:20 -07:00
if (ban.isEmpty) cleanUserIds put userId
}
2019-08-21 04:44:19 -06:00
}
2015-04-25 15:06:44 -06:00
def hasCurrentBan(userId: User.ID): Fu[Boolean] = currentBan(userId).map(_.isDefined)
2017-08-04 08:12:21 -06:00
def completionRate(userId: User.ID): Fu[Option[Double]] =
2019-12-09 20:11:53 -07:00
coll.primitiveOne[Vector[Outcome]]($id(userId), "o").map(~_) map { outcomes =>
outcomes.collect {
case Outcome.RageQuit | Outcome.Sitting | Outcome.NoPlay | Outcome.Abort => false
2019-12-13 07:30:20 -07:00
case Outcome.Good => true
} match {
case c if c.size >= 5 => Some(c.count(identity).toDouble / c.size)
2019-12-13 07:30:20 -07:00
case _ => none
}
2015-04-25 15:06:44 -06:00
}
2019-12-13 07:30:20 -07:00
def bans(userIds: List[User.ID]): Fu[Map[User.ID, Int]] =
coll.ext
.find(
$inIds(userIds),
$doc("b" -> true)
)
.list[Bdoc]()
.map {
_.flatMap { obj =>
obj.getAsOpt[User.ID]("_id") flatMap { id =>
obj.getAsOpt[Barr]("b") map { id -> _.size }
}
}.toMap
}
2015-04-25 23:44:35 -06:00
2019-09-26 13:45:53 -06:00
def getRageSit(userId: User.ID) = rageSitCache get userId
private val rageSitCache = cacheApi[User.ID, RageSit](32768, "playban.ragesit") {
2019-12-23 18:01:45 -07:00
_.expireAfterAccess(20 minutes)
.buildAsyncFuture { userId =>
coll.primitiveOne[RageSit]($doc("_id" -> userId, "c" $exists true), "c").map(_ | RageSit.empty)
}
}
private def save(outcome: Outcome, userId: User.ID, rsUpdate: RageSit.Update): Funit = {
2019-12-10 14:01:18 -07:00
lila.mon.playban.outcome(outcome.key).increment()
2019-12-13 07:30:20 -07:00
coll.ext
.findAndUpdate(
selector = $id(userId),
update = $doc(
2020-01-04 19:45:50 -07:00
$push("o" -> $doc("$each" -> List(outcome), "$slice" -> -30)) ++ {
rsUpdate match {
case RageSit.Reset => $min("c" -> 0)
case RageSit.Inc(v) if v != 0 => $inc("c" -> v)
case _ => $empty
}
}
2019-12-13 07:30:20 -07:00
),
fetchNewObject = true,
upsert = true
)
.dmap(_.value flatMap UserRecordBSONHandler.readOpt) orFail
s"can't find newly created record for user $userId" flatMap { record =>
2019-12-13 07:30:20 -07:00
(outcome != Outcome.Good) ?? {
userRepo.createdAtById(userId).flatMap { _ ?? { legiferate(record, _) } }
} >>
registerRageSit(record, rsUpdate)
2019-12-13 07:30:20 -07:00
}
}.void logFailure lila.log("playban")
2015-04-25 15:06:44 -06:00
private def registerRageSit(record: UserRecord, update: RageSit.Update): Funit = update match {
case RageSit.Inc(delta) =>
rageSitCache.put(record.userId, fuccess(record.rageSit))
(delta < 0) ?? {
if (record.rageSit.isTerrible) funit
else if (record.rageSit.isVeryBad) for {
mod <- userRepo.lichess
user <- userRepo byId record.userId
} yield (mod zip user).headOption foreach {
case (m, u) =>
lila.log("ragesit").info(s"https://lichess.org/@/${u.username} ${record.rageSit.counterView}")
Bus.publish(lila.hub.actorApi.mod.AutoWarning(u.id, ModPreset.sittingAuto.subject), "autoWarning")
messenger.sendPreset(m, u, ModPreset.sittingAuto).void
}
else funit
2019-09-26 13:45:53 -06:00
}
case _ => funit
2019-09-26 13:45:53 -06:00
}
2019-12-13 07:30:20 -07:00
private def legiferate(record: UserRecord, accCreatedAt: DateTime): Funit =
record.bannable(accCreatedAt) ?? { ban =>
(!record.banInEffect) ?? {
lila.mon.playban.ban.count.increment()
2019-12-14 18:45:22 -07:00
lila.mon.playban.ban.mins.record(ban.mins)
2019-12-13 07:30:20 -07:00
Bus.publish(lila.hub.actorApi.playban.Playban(record.userId, ban.mins), "playban")
coll.update
.one(
$id(record.userId),
$unset("o") ++
$push(
"b" -> $doc(
"$each" -> List(ban),
"$slice" -> -30
)
)
2018-11-29 20:34:05 -07:00
)
2019-12-13 07:30:20 -07:00
.void >>- cleanUserIds.remove(record.userId)
}
2017-10-19 22:02:55 -06:00
}
2015-04-25 12:48:13 -06:00
}