Merge branch 'master' into horde
* master: manually fix ro translation Don't analyse unusable games translation: confirm page close on unsaved changes - fixes #330 add apple-touch-icon.png log puzzle png export remove debug remove CSS vendor prefixes highlight user flags in mod view Remove previous cheat evaluator. Farewell, good cop, you did a great job don't auto-mark great players with lots of rated games tweak auto analysis conditions remove redundant case Start auto-analysing games remove unused resolver also open analysed games in mobile app make the evaluator less mark-happy by tweaking percentages include border in sizes, fix when zoom >= 75% upgrade chessground hack-fix atomic premoves on explosions - closes #343 stop using deprecated chessground capture event
This commit is contained in:
commit
44d08507c6
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -16,9 +16,6 @@
|
|||
[submodule "submodules/boardcreator"]
|
||||
path = submodules/boardcreator
|
||||
url = https://github.com/clarkerubber/board-creator
|
||||
[submodule "submodules/evaluator"]
|
||||
path = submodules/evaluator
|
||||
url = https://github.com/ornicar/engine-evaluator
|
||||
[submodule "ui/chessli"]
|
||||
path = ui/chessli
|
||||
url = https://github.com/ornicar/chess.js
|
||||
|
|
|
@ -65,7 +65,6 @@ final class Env(
|
|||
Env.notification,
|
||||
Env.bookmark,
|
||||
Env.pref,
|
||||
Env.evaluation,
|
||||
Env.chat,
|
||||
Env.puzzle,
|
||||
Env.tv,
|
||||
|
@ -119,7 +118,6 @@ object Env {
|
|||
def relation = lila.relation.Env.current
|
||||
def report = lila.report.Env.current
|
||||
def pref = lila.pref.Env.current
|
||||
def evaluation = lila.evaluation.Env.current
|
||||
def chat = lila.chat.Env.current
|
||||
def puzzle = lila.puzzle.Env.current
|
||||
def coordinate = lila.coordinate.Env.current
|
||||
|
|
|
@ -148,18 +148,12 @@ object User extends LilaController {
|
|||
|
||||
def mod(username: String) = Secure(_.UserSpy) { implicit ctx =>
|
||||
me => OptionFuOk(UserRepo named username) { user =>
|
||||
(Env.evaluation.evaluator find user) zip (Env.security userSpy user.id) zip (Env.mod.assessApi.getPlayerAggregateAssessment(user.id)) map {
|
||||
case ((eval, spy), playerAggregateAssessment) => html.user.mod(user, spy, eval, playerAggregateAssessment)
|
||||
(Env.security userSpy user.id) zip (Env.mod.assessApi.getPlayerAggregateAssessment(user.id)) map {
|
||||
case (spy, playerAggregateAssessment) => html.user.mod(user, spy, playerAggregateAssessment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def evaluate(username: String) = Secure(_.UserEvaluate) { implicit ctx =>
|
||||
me => OptionFuResult(UserRepo named username) { user =>
|
||||
Env.evaluation.evaluator.generate(user.id, true) inject Redirect(routes.User.show(username).url + "?mod")
|
||||
}
|
||||
}
|
||||
|
||||
def writeNote(username: String) = AuthBody { implicit ctx =>
|
||||
me => OptionFuResult(UserRepo named username) { user =>
|
||||
implicit val req = ctx.body
|
||||
|
|
|
@ -1,34 +1,31 @@
|
|||
@(u: User, spy: lila.security.UserSpy, eval: Option[lila.evaluation.Evaluation], optionAggregateAssessment: Option[lila.evaluation.PlayerAggregateAssessment])(implicit ctx: Context)
|
||||
@(u: User, spy: lila.security.UserSpy, optionAggregateAssessment: Option[lila.evaluation.PlayerAggregateAssessment])(implicit ctx: Context)
|
||||
|
||||
@import lila.evaluation.Display
|
||||
|
||||
<div class="actions clearfix">
|
||||
@if(isGranted(_.UserEvaluate)) {
|
||||
<form method="post" action="@routes.User.evaluate(u.username)">
|
||||
<input class="button" type="submit" value="@{eval.isDefined.??("(Re)")}Evaluate (old)" />
|
||||
</form>
|
||||
<form method="post" action="@routes.Mod.refreshUserAssess(u.username)">
|
||||
<input class="button" type="submit" value="Evaluate (new)" />
|
||||
<input class="button" type="submit" value="Evaluate" />
|
||||
</form>
|
||||
}
|
||||
@if(isGranted(_.MarkEngine)) {
|
||||
<form method="post" action="@routes.Mod.engine(u.username)">
|
||||
<input class="confirm button" type="submit" value="@u.engine.fold("Un-engine", "Engine")" />
|
||||
<input class="confirm button@when(u.engine, " active")" 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")" />
|
||||
<input class="confirm button@when(u.booster, " active")" 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")" />
|
||||
<input class="confirm button@when(u.troll, " active")" type="submit" value="@u.troll.fold("Un-troll", "Troll")" />
|
||||
</form>
|
||||
}
|
||||
@if(isGranted(_.IpBan)) {
|
||||
<form method="post" action="@routes.Mod.ban(u.username)">
|
||||
<input class="confirm button" type="submit" value="@u.ipBan.fold("Revoke IP ban", "IP ban")" />
|
||||
<input class="confirm button@when(u.ipBan, " active")" type="submit" value="@u.ipBan.fold("Revoke IP ban", "IP ban")" />
|
||||
</form>
|
||||
}
|
||||
@if(!u.disabled) {
|
||||
|
@ -40,51 +37,18 @@
|
|||
} else {
|
||||
@if(isGranted(_.ReopenAccount)) {
|
||||
<form action="@routes.Mod.reopenAccount(u.username)" method="post">
|
||||
<input type="submit" class="button confirm" value="Reopen" />
|
||||
<input type="submit" class="button confirm active" value="Reopen" />
|
||||
</form>
|
||||
}
|
||||
}
|
||||
@if(isGranted(_.SetTitle)) {
|
||||
<form class="fide_title" method="post" action="@routes.Mod.setTitle(u.username)">
|
||||
@base.select(lila.user.DataForm.title.fill(u.title)("title"), lila.user.User.titles, "No FIDE title".some)
|
||||
@base.select(lila.user.DataForm.title.fill(u.title)("title"), lila.user.User.titles, "No title".some)
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="user_spy">
|
||||
@eval.map { e =>
|
||||
<div class="evaluation">
|
||||
<p>
|
||||
<strong>@e.verdict(u.perfs)</strong> @e.isDeep.fold("Thoroughly", "Quickly") evaluated @momentFromNow(e.date) as @e.percent%
|
||||
<br />
|
||||
<br />
|
||||
</p>
|
||||
@if(e.games.nonEmpty) {
|
||||
<table class="slist">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Most suspicious games found</th>
|
||||
<th>Move time</th>
|
||||
<th>Blur</th>
|
||||
<th>Analysis</th>
|
||||
<th>Bot</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@e.games.map { g =>
|
||||
<tr>
|
||||
<td><a href="@g.path">@g.path</a></td>
|
||||
<td>@g.moveTime.map(_ + "%")</td>
|
||||
<td>@g.blur.map(_ + "%")</td>
|
||||
<td>@g.error.map(_ + "%")</td>
|
||||
<td>@g.hold.map(_ + "%")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@optionAggregateAssessment.fold{
|
||||
<div class="evaluation">
|
||||
<p>
|
||||
|
|
|
@ -407,8 +407,8 @@ difficultyHard=Mare
|
|||
xLeftANoteOnY=%s a lăsat un mesaj pentru %s
|
||||
xCompetesInY=%s joacă în °s
|
||||
xAskedY=%s a întrebat `%s
|
||||
xAnsweredY=%a răspuns %s
|
||||
xCommentedY=%a comentat %s
|
||||
xAnsweredY=%s a răspuns %s
|
||||
xCommentedY=%s a comentat %s
|
||||
timeline=Activitate recentă
|
||||
seeAllTournaments=Vezi toate turneele
|
||||
starting=Începe în:
|
||||
|
|
|
@ -30,7 +30,6 @@ GET /rel/blocks controllers.Relation.blocks
|
|||
# User
|
||||
GET /@/:username/opponents controllers.User.opponents(username: String)
|
||||
GET /@/:username/mod controllers.User.mod(username: String)
|
||||
POST /@/:username/evaluate controllers.User.evaluate(username: String)
|
||||
POST /@/:username/note controllers.User.writeNote(username: String)
|
||||
GET /@/:username/mini controllers.User.showMini(username: String)
|
||||
GET /@/:username/tv controllers.User.tv(username: String)
|
||||
|
|
|
@ -20,7 +20,6 @@ case class ConcurrentAnalysisException(userId: String, progressId: String, gameI
|
|||
final class Analyser(
|
||||
ai: ActorSelection,
|
||||
indexer: ActorSelection,
|
||||
evaluator: ActorSelection,
|
||||
modActor: ActorSelection) {
|
||||
|
||||
def get(id: String): Fu[Option[Analysis]] = AnalysisRepo byId id flatMap evictStalled
|
||||
|
@ -77,9 +76,6 @@ final class Analyser(
|
|||
if (analysis.valid) {
|
||||
indexer ! InsertGame(game)
|
||||
AnalysisRepo.done(id, analysis) >>- {
|
||||
game.userIds foreach { userId =>
|
||||
evaluator ! lila.hub.actorApi.evaluation.Refresh(userId)
|
||||
}
|
||||
modActor ! actorApi.AnalysisReady(game, analysis)
|
||||
} >>- GameRepo.setAnalysed(game.id) inject analysis
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ final class Env(
|
|||
ai: ActorSelection,
|
||||
system: ActorSystem,
|
||||
indexer: ActorSelection,
|
||||
evaluator: ActorSelection,
|
||||
modActor: ActorSelection) {
|
||||
|
||||
private val CollectionAnalysis = config getString "collection.analysis"
|
||||
|
@ -28,7 +27,6 @@ final class Env(
|
|||
lazy val analyser = new Analyser(
|
||||
ai = ai,
|
||||
indexer = indexer,
|
||||
evaluator = evaluator,
|
||||
modActor = modActor)
|
||||
|
||||
lazy val paginator = new PaginatorBuilder(
|
||||
|
@ -66,6 +64,5 @@ object Env {
|
|||
ai = lila.hub.Env.current.actor.ai,
|
||||
system = lila.common.PlayApp.system,
|
||||
indexer = lila.hub.Env.current.actor.gameIndexer,
|
||||
evaluator = lila.hub.Env.current.actor.evaluator,
|
||||
modActor = lila.hub.Env.current.actor.mod)
|
||||
}
|
||||
|
|
|
@ -40,5 +40,5 @@ object HTTPRequest {
|
|||
private val fileExtensionPattern = """.+\.[a-z0-9]{2,4}$""".r.pattern
|
||||
|
||||
def hasFileExtension(req: RequestHeader) =
|
||||
fileExtensionPattern.matcher(req.path.pp).matches.pp
|
||||
fileExtensionPattern.matcher(req.path).matches
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
package lila.evaluation
|
||||
|
||||
import akka.actor._
|
||||
import com.typesafe.config.Config
|
||||
|
||||
final class Env(
|
||||
config: Config,
|
||||
db: lila.db.Env,
|
||||
hub: lila.hub.Env,
|
||||
system: ActorSystem) {
|
||||
|
||||
private val CollectionEvaluation = config getString "collection.evaluation"
|
||||
private val EvaluatorExecPath = config getString "evaluator.exec_path"
|
||||
private val ActorName = config getString "actor.name"
|
||||
private val ApiToken = config getString "api.token"
|
||||
private val ApiUrl = config getString "api.url"
|
||||
|
||||
lazy val evaluator = new Evaluator(
|
||||
coll = db(CollectionEvaluation),
|
||||
execPath = EvaluatorExecPath,
|
||||
reporter = hub.actor.report,
|
||||
analyser = hub.actor.analyser,
|
||||
marker = hub.actor.mod,
|
||||
token = ApiToken,
|
||||
apiUrl = ApiUrl)
|
||||
|
||||
system.actorOf(Props(new Listener(evaluator)), name = ActorName)
|
||||
}
|
||||
|
||||
object Env {
|
||||
|
||||
lazy val current = "[boot] evaluation" describes new Env(
|
||||
config = lila.common.PlayApp loadConfig "evaluation",
|
||||
db = lila.db.Env.current,
|
||||
hub = lila.hub.Env.current,
|
||||
system = lila.common.PlayApp.system)
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
package lila.evaluation
|
||||
|
||||
import scala.util.{ Try, Success, Failure }
|
||||
|
||||
import akka.actor.ActorSelection
|
||||
import akka.pattern.ask
|
||||
import org.joda.time.DateTime
|
||||
import play.api.libs.json._
|
||||
import play.api.libs.json.Json
|
||||
import play.modules.reactivemongo.json.ImplicitBSONHandlers._
|
||||
import reactivemongo.bson._
|
||||
|
||||
import lila.db.api._
|
||||
import lila.db.BSON.BSONJodaDateTimeHandler
|
||||
import lila.db.JsTube.Helpers.{ rename, writeDate, readDate }
|
||||
import lila.db.Types._
|
||||
import lila.rating.{ Perf, PerfType }
|
||||
import lila.user.{ User, UserRepo, Perfs }
|
||||
|
||||
final class Evaluator(
|
||||
coll: Coll,
|
||||
execPath: String,
|
||||
reporter: ActorSelection,
|
||||
analyser: ActorSelection,
|
||||
marker: ActorSelection,
|
||||
token: String,
|
||||
apiUrl: String) {
|
||||
|
||||
import Evaluation._, heuristics._
|
||||
|
||||
def find(user: User): Fu[Option[Evaluation]] =
|
||||
coll.find(BSONDocument("_id" -> user.id)).one[JsObject] map { _ map readEvaluation }
|
||||
|
||||
def evaluatedAt(user: User): Fu[Option[DateTime]] =
|
||||
coll.find(
|
||||
BSONDocument("_id" -> user.id),
|
||||
BSONDocument("date" -> true)
|
||||
).one[BSONDocument] map { _ flatMap (_.getAs[DateTime]("date")) }
|
||||
|
||||
def generate(userId: String, deep: Boolean): Fu[Option[Evaluation]] =
|
||||
UserRepo byId userId flatMap {
|
||||
_ ?? { user =>
|
||||
(run(userId, deep) match {
|
||||
case Failure(e: Exception) if e.getMessage.contains("exit value: 1") => fuccess(none)
|
||||
case Failure(e: Exception) if e.getMessage.contains("exit value: 2") => fuccess(none)
|
||||
case Failure(e: Exception) => fufail(e)
|
||||
case Success(output) => for {
|
||||
evalJs ← (Json parse output).transform(evaluationTransformer) match {
|
||||
case JsSuccess(v, _) => fuccess(v)
|
||||
case JsError(e) => fufail(lila.common.LilaException(s"Can't parse evaluator output: $e on $output"))
|
||||
}
|
||||
eval = readEvaluation(evalJs)
|
||||
_ ← coll.update(Json.obj("_id" -> userId), evalJs, upsert = true)
|
||||
} yield eval.some
|
||||
}) andThen {
|
||||
case Success(Some(eval)) if Evaluation.watchPerfs(user.perfs) exists eval.mark =>
|
||||
UserRepo byId userId foreach {
|
||||
_ filterNot (_.engine) foreach { user =>
|
||||
marker ! lila.hub.actorApi.mod.MarkCheater(user.id)
|
||||
reporter ! lila.hub.actorApi.report.Check(user.id)
|
||||
}
|
||||
}
|
||||
case Failure(e) => logger.warn(s"generate: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private[evaluation] def autoGenerate(
|
||||
user: User,
|
||||
perfType: PerfType,
|
||||
important: Boolean,
|
||||
forceRefresh: Boolean,
|
||||
suspiciousHold: Boolean) {
|
||||
val perf = user.perfs(perfType)
|
||||
if (!user.engine && (
|
||||
important || suspiciousHold ||
|
||||
(deviationIsLow(perf) && (progressIsHigh(perf) || ratingIsHigh(perf)))
|
||||
)) {
|
||||
evaluatedAt(user) foreach { date =>
|
||||
def freshness = if (progressIsVeryHigh(perf)) DateTime.now minusMinutes 20
|
||||
else if (progressIsHigh(perf)) DateTime.now minusHours 1
|
||||
else DateTime.now minusDays 2
|
||||
if (suspiciousHold || forceRefresh || date.fold(true)(_ isBefore freshness)) {
|
||||
generate(user.id, true) foreach {
|
||||
_ foreach { eval =>
|
||||
eval.gameIdsToAnalyse foreach { gameId =>
|
||||
analyser ! lila.hub.actorApi.ai.AutoAnalyse(gameId)
|
||||
if (eval report perf)
|
||||
reporter ! lila.hub.actorApi.report.Cheater(user.id, eval reportText 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private[evaluation] def autoGenerate(
|
||||
user: User,
|
||||
important: Boolean,
|
||||
forceRefresh: Boolean,
|
||||
suspiciousHold: Boolean) {
|
||||
user.perfs.bestPerf foreach {
|
||||
case (pt, _) => autoGenerate(user, pt, important, forceRefresh, suspiciousHold)
|
||||
}
|
||||
}
|
||||
private[evaluation] def autoGenerate(userId: String, important: Boolean, forceRefresh: Boolean) {
|
||||
UserRepo byId userId foreach {
|
||||
_ foreach { autoGenerate(_, important, forceRefresh, false) }
|
||||
}
|
||||
}
|
||||
|
||||
private def readEvaluation(js: JsValue): Evaluation =
|
||||
(readDate('date) andThen Evaluation.reader) reads js match {
|
||||
case JsSuccess(v, _) => v
|
||||
case JsError(e) => throw lila.common.LilaException(s"Can't parse evaluator json: $e on $js")
|
||||
}
|
||||
|
||||
private def run(userId: String, deep: Boolean): Try[String] = {
|
||||
import scala.sys.process._
|
||||
import java.io.File
|
||||
val exec = Process(Seq("php", "engine-evaluator.php", userId, deep.fold("true", "false"), token, s"$apiUrl/"), new File(execPath))
|
||||
Try {
|
||||
exec.!!
|
||||
} match {
|
||||
case Failure(e) => Failure(new Exception(s"$exec $e"))
|
||||
case x => x
|
||||
}
|
||||
}
|
||||
|
||||
private def evaluationTransformer =
|
||||
rename('userId, '_id) andThen
|
||||
rename('cheatIndex, 'shallow) andThen
|
||||
rename('deepIndex, 'deep) andThen
|
||||
rename('computerAnalysis, 'analysis) andThen
|
||||
rename('knownEngineIP, 'sharedIP) andThen
|
||||
__.json.update(
|
||||
__.read[JsObject].map { o => o ++ Json.obj("date" -> $date(DateTime.now)) }
|
||||
) andThen
|
||||
(__ \ 'Error).json.prune
|
||||
|
||||
private val logger = play.api.Logger("evaluator")
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package lila.evaluation
|
||||
|
||||
import akka.actor._
|
||||
import lila.game.PerfPicker
|
||||
import chess.{ Speed, White, Black }
|
||||
import lila.hub.actorApi.evaluation._
|
||||
import lila.user.User
|
||||
import lila.rating.PerfType
|
||||
|
||||
private[evaluation] final class Listener(evaluator: Evaluator) extends Actor {
|
||||
|
||||
context.system.lilaBus.subscribe(self, 'finishGame)
|
||||
|
||||
def receive = {
|
||||
|
||||
case lila.game.actorApi.FinishGame(game, white, black) =>
|
||||
PerfType(PerfPicker key game) ifTrue game.rated map { perfType =>
|
||||
List(
|
||||
game.whitePlayer -> white,
|
||||
game.blackPlayer -> black
|
||||
) foreach {
|
||||
case (p, Some(u)) => evaluator.autoGenerate(
|
||||
user = u,
|
||||
perfType = perfType,
|
||||
important = p.wins && game.isTournament && game.speed != Speed.Bullet,
|
||||
forceRefresh = false,
|
||||
suspiciousHold = p.hasSuspiciousHoldAlert)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
case user: User => evaluator.generate(user.id, true)
|
||||
|
||||
case AutoCheck(userId) => evaluator.autoGenerate(userId, true, false)
|
||||
|
||||
case Refresh(userId) => evaluator.autoGenerate(userId, false, true)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package lila.evaluation
|
||||
|
||||
import lila.user.User
|
||||
import chess.Color
|
||||
import org.joda.time.DateTime
|
||||
|
||||
|
@ -22,19 +23,21 @@ case class PlayerAssessment(
|
|||
val color = Color.apply(white)
|
||||
}
|
||||
|
||||
case class PlayerAggregateAssessment(playerAssessments: List[PlayerAssessment],
|
||||
case class PlayerAggregateAssessment(
|
||||
user: User,
|
||||
playerAssessments: List[PlayerAssessment],
|
||||
relatedUsers: List[String],
|
||||
relatedCheaters: List[String]) {
|
||||
import Statistics._
|
||||
import AccountAction._
|
||||
|
||||
def action = {
|
||||
val markable: Boolean = (
|
||||
val markable: Boolean = !isGreatUser && (
|
||||
(cheatingSum >= 2 || cheatingSum + likelyCheatingSum >= 4)
|
||||
// more than 5 percent of games are cheating
|
||||
&& (cheatingSum.toDouble / assessmentsCount >= 0.05 - relationModifier
|
||||
// or more than 10 percent of games are likely cheating
|
||||
|| (cheatingSum + likelyCheatingSum).toDouble / assessmentsCount >= 0.10 - relationModifier)
|
||||
// more than 10 percent of games are cheating
|
||||
&& (cheatingSum.toDouble / assessmentsCount >= 0.1 - relationModifier
|
||||
// or more than 20 percent of games are likely cheating
|
||||
|| (cheatingSum + likelyCheatingSum).toDouble / assessmentsCount >= 0.20 - relationModifier)
|
||||
)
|
||||
|
||||
val reportable: Boolean = (
|
||||
|
@ -112,6 +115,9 @@ case class PlayerAggregateAssessment(playerAssessments: List[PlayerAssessment],
|
|||
val sfAvgHold = sfAvgGiven(_.hold)
|
||||
val sfAvgNoHold = sfAvgGiven(!_.hold)
|
||||
|
||||
def isGreatUser = user.perfs.bestRating > 2200 &&
|
||||
user.count.rated >= 100
|
||||
|
||||
def reportText(maxGames: Int = 10): String = {
|
||||
val gameLinks: String = (playerAssessments.sortBy(-_.assessment.id).take(maxGames).map{ a =>
|
||||
a.assessment.emoticon + " http://lichess.org/" + a.gameId + "/" + a.color.name
|
||||
|
|
|
@ -27,7 +27,6 @@ final class Env(config: Config, system: ActorSystem) {
|
|||
val challenger = select("actor.challenger")
|
||||
val report = select("actor.report")
|
||||
val mod = select("actor.mod")
|
||||
val evaluator = select("actor.evaluator")
|
||||
val chat = select("actor.chat")
|
||||
val analyser = select("actor.analyser")
|
||||
val moveBroadcast = select("actor.move_broadcast")
|
||||
|
|
|
@ -2,11 +2,10 @@ package lila.mod
|
|||
|
||||
import akka.actor.ActorSelection
|
||||
import lila.analyse.{ Analysis, AnalysisRepo }
|
||||
import lila.db.Types.Coll
|
||||
import lila.db.BSON.BSONJodaDateTimeHandler
|
||||
import lila.db.Types.Coll
|
||||
import lila.evaluation.{ AccountAction, Analysed, GameAssessment, PlayerAssessment, PlayerAggregateAssessment, PlayerFlags, PlayerAssessments, Assessible }
|
||||
import lila.game.Game
|
||||
import lila.game.{ Game, GameRepo }
|
||||
import lila.game.{ Game, Player, GameRepo }
|
||||
import lila.user.{ User, UserRepo }
|
||||
|
||||
import org.joda.time.DateTime
|
||||
|
@ -21,9 +20,11 @@ final class AssessApi(
|
|||
logApi: ModlogApi,
|
||||
modApi: ModApi,
|
||||
reporter: ActorSelection,
|
||||
analyser: ActorSelection,
|
||||
userIdsSharingIp: String => Fu[List[String]]) {
|
||||
|
||||
import PlayerFlags.playerFlagsBSONHandler
|
||||
|
||||
private implicit val playerAssessmentBSONhandler = Macros.handler[PlayerAssessment]
|
||||
|
||||
def createPlayerAssessment(assessed: PlayerAssessment) =
|
||||
|
@ -51,11 +52,16 @@ final class AssessApi(
|
|||
def getPlayerAggregateAssessment(userId: String, nb: Int = 100): Fu[Option[PlayerAggregateAssessment]] = {
|
||||
val relatedUsers = userIdsSharingIp(userId)
|
||||
|
||||
UserRepo.byId(userId) zip
|
||||
getPlayerAssessmentsByUserId(userId, nb) zip
|
||||
relatedUsers zip
|
||||
(relatedUsers flatMap UserRepo.filterByEngine) map {
|
||||
case ((assessedGamesHead :: assessedGamesTail, relatedUs), relatedCheaters) =>
|
||||
Some(PlayerAggregateAssessment(assessedGamesHead :: assessedGamesTail, relatedUs, relatedCheaters))
|
||||
case (((Some(user), assessedGamesHead :: assessedGamesTail), relatedUs), relatedCheaters) =>
|
||||
Some(PlayerAggregateAssessment(
|
||||
user,
|
||||
assessedGamesHead :: assessedGamesTail,
|
||||
relatedUs,
|
||||
relatedCheaters))
|
||||
case _ => none
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +85,7 @@ final class AssessApi(
|
|||
}
|
||||
|
||||
def onAnalysisReady(game: Game, analysis: Analysis, assess: Boolean = true): Funit = {
|
||||
if (!game.isCorrespondence && game.turns >= 40 && game.mode.rated) {
|
||||
if (!game.isCorrespondence && game.playedTurns >= 40 && game.mode.rated) {
|
||||
val gameAssessments: PlayerAssessments = Assessible(Analysed(game, analysis)).assessments
|
||||
gameAssessments.white.fold(funit){createPlayerAssessment} >>
|
||||
gameAssessments.black.fold(funit){createPlayerAssessment}
|
||||
|
@ -100,6 +106,38 @@ final class AssessApi(
|
|||
case none => funit
|
||||
}
|
||||
|
||||
def onGameReady(game: Game): Funit = {
|
||||
import lila.evaluation.Statistics.{ skip, coefVariation }
|
||||
|
||||
def manyBlurs(player: Player) =
|
||||
player.blurs.toDouble / game.playedTurns >= 0.7
|
||||
|
||||
def moveTimes(color: Color): List[Int] =
|
||||
skip(game.moveTimes.toList, if (color == Color.White) 0 else 1)
|
||||
|
||||
def consistentMoveTimes(player: Player): Boolean =
|
||||
moveTimes(player.color).toNel.map(coefVariation).fold(false)(_ < 0.5)
|
||||
|
||||
def shouldAnalyse =
|
||||
if (game.isCorrespondence) false
|
||||
else if (game.playedTurns < 40) false
|
||||
else if (!game.mode.rated) false
|
||||
else if (!game.analysable) false
|
||||
// someone is using a bot
|
||||
else if (game.players.exists(_.hasSuspiciousHoldAlert)) true
|
||||
// don't analyse bullet games
|
||||
else if (game.speed == chess.Speed.Bullet) false
|
||||
// someone blurs a lot
|
||||
else if (game.players exists manyBlurs) true
|
||||
// someone has consistent move times
|
||||
else if (game.players exists consistentMoveTimes) true
|
||||
else false
|
||||
|
||||
if (shouldAnalyse) analyser ! lila.hub.actorApi.ai.AutoAnalyse(game.id)
|
||||
|
||||
funit
|
||||
}
|
||||
|
||||
private def withUser[A](username: String)(op: User => Fu[A]): Fu[A] =
|
||||
UserRepo named username flatten "[mod] missing user " + username flatMap op
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ final class Env(
|
|||
logApi = logApi,
|
||||
modApi = api,
|
||||
reporter = hub.actor.report,
|
||||
analyser = hub.actor.analyser,
|
||||
userIdsSharingIp = userIdsSharingIp)
|
||||
|
||||
// api actor
|
||||
|
@ -53,7 +54,8 @@ final class Env(
|
|||
assessApi.onAnalysisReady(game, analysis)
|
||||
case lila.game.actorApi.FinishGame(game, whiteUserOption, blackUserOption) =>
|
||||
(whiteUserOption |@| blackUserOption) apply {
|
||||
case (whiteUser, blackUser) => boosting.check(game, whiteUser, blackUser)
|
||||
case (whiteUser, blackUser) => boosting.check(game, whiteUser, blackUser) >>
|
||||
assessApi.onGameReady(game)
|
||||
}
|
||||
}
|
||||
}), name = ActorName)
|
||||
|
|
|
@ -6,7 +6,7 @@ import scala.sys.process._
|
|||
|
||||
object PngExport {
|
||||
|
||||
private val logger = ProcessLogger(_ => (), _ => ())
|
||||
private val logger = ProcessLogger(println, println)
|
||||
|
||||
def apply(execPath: String)(puzzle: Puzzle)(out: OutputStream) {
|
||||
val color = puzzle.color.letter.toString
|
||||
|
|
|
@ -16,7 +16,7 @@ final class Env(
|
|||
|
||||
lazy val forms = new DataForm(hub.actor.captcher)
|
||||
|
||||
lazy val api = new ReportApi(hub.actor.evaluator)
|
||||
lazy val api = new ReportApi
|
||||
|
||||
// api actor
|
||||
system.actorOf(Props(new Actor {
|
||||
|
|
|
@ -10,7 +10,7 @@ import lila.db.Implicits._
|
|||
import lila.user.{ User, UserRepo }
|
||||
import tube.reportTube
|
||||
|
||||
private[report] final class ReportApi(evaluator: ActorSelection) {
|
||||
private[report] final class ReportApi {
|
||||
|
||||
def create(setup: ReportSetup, by: User): Funit =
|
||||
Reason(setup.reason).fold[Funit](fufail("Invalid report reason " + setup.reason)) { reason =>
|
||||
|
@ -25,15 +25,9 @@ private[report] final class ReportApi(evaluator: ActorSelection) {
|
|||
selectRecent(user, reason),
|
||||
Json.obj("$set" -> (reportTube.toMongo(report).get - "processedBy" - "_id"))
|
||||
) flatMap { res =>
|
||||
(!res.updatedExisting) ?? {
|
||||
if (report.isCheat) evaluator ! user
|
||||
$insert(report)
|
||||
}
|
||||
(!res.updatedExisting) ?? $insert(report)
|
||||
}
|
||||
else {
|
||||
if (report.isCheat) evaluator ! user
|
||||
$insert(report)
|
||||
}
|
||||
else $insert(report)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -91,8 +91,7 @@ final class Env(
|
|||
renderer = hub.actor.renderer
|
||||
)), name = ReminderName),
|
||||
isOnline = isOnline,
|
||||
socketHub = socketHub,
|
||||
evaluator = hub.actor.evaluator
|
||||
socketHub = socketHub
|
||||
)), name = OrganizerName)
|
||||
|
||||
private val tournamentScheduler = system.actorOf(Props(new Scheduler(api)))
|
||||
|
@ -123,10 +122,6 @@ final class Env(
|
|||
organizer -> actorApi.StartedTournaments
|
||||
}
|
||||
|
||||
scheduler.message(6 minutes) {
|
||||
organizer -> actorApi.CheckLeaders
|
||||
}
|
||||
|
||||
scheduler.message(5 minutes) {
|
||||
tournamentScheduler -> actorApi.ScheduleNow
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@ private[tournament] final class Organizer(
|
|||
api: TournamentApi,
|
||||
reminder: ActorRef,
|
||||
isOnline: String => Boolean,
|
||||
socketHub: ActorRef,
|
||||
evaluator: ActorSelection) extends Actor {
|
||||
socketHub: ActorRef) extends Actor {
|
||||
|
||||
context.system.lilaBus.subscribe(self, 'finishGame, 'adjustCheater)
|
||||
|
||||
|
@ -42,12 +41,6 @@ private[tournament] final class Organizer(
|
|||
reminder ! RemindTournaments(tours)
|
||||
}
|
||||
|
||||
case CheckLeaders => TournamentRepo.started foreach {
|
||||
_.flatMap(_.leaders).map(_.id).distinct foreach { id =>
|
||||
evaluator ! lila.hub.actorApi.evaluation.AutoCheck(id)
|
||||
}
|
||||
}
|
||||
|
||||
case FinishGame(game, _, _) => api finishGame game
|
||||
|
||||
case lila.hub.actorApi.mod.MarkCheater(userId) => api ejectCheater userId
|
||||
|
|
|
@ -32,7 +32,6 @@ private[tournament] case class Connected(enumerator: JsEnumerator, member: Membe
|
|||
// organizer
|
||||
private[tournament] case object AllCreatedTournaments
|
||||
private[tournament] case object StartedTournaments
|
||||
private[tournament] case object CheckLeaders
|
||||
case class RemindTournaments(tours: List[Started])
|
||||
case class RemindTournament(tour: Started)
|
||||
case class TournamentTable(tours: List[Enterable])
|
||||
|
|
|
@ -14,7 +14,6 @@ object Dependencies {
|
|||
val sprayRepo = "spray repo" at "http://repo.spray.io"
|
||||
val local = "local repo" at home + "/local-repo"
|
||||
val roundeights = "RoundEights" at "http://maven.spikemark.net/roundeights"
|
||||
val snowplow = "SnowPlow Repo" at "http://maven.snplow.com/releases/"
|
||||
val prismic = "Prismic.io kits" at "https://s3.amazonaws.com/prismic-maven-kits/repository/maven/"
|
||||
|
||||
val commons = Seq(
|
||||
|
@ -25,7 +24,7 @@ object Dependencies {
|
|||
typesafe,
|
||||
roundeights,
|
||||
prismic,
|
||||
t2v, jgitMaven, sprayRepo, snowplow)
|
||||
t2v, jgitMaven, sprayRepo)
|
||||
}
|
||||
|
||||
val scalaz = "org.scalaz" %% "scalaz-core" % "7.1.1"
|
||||
|
|
BIN
public/apple-touch-icon.png
Normal file
BIN
public/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
|
@ -1896,6 +1896,7 @@ lichess.storage = {
|
|||
|
||||
function startAnalyse(element, cfg) {
|
||||
var data = cfg.data;
|
||||
if (lichess.openInMobileApp(data.game.id)) return;
|
||||
if (data.chat) $('#chat').chat({
|
||||
messages: data.chat,
|
||||
initialNote: data.note,
|
||||
|
|
|
@ -17,5 +17,27 @@ $(function() {
|
|||
$('div.locale_menu a.missing').click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var inputHash = function() {
|
||||
return Array.prototype.map.call(
|
||||
document.querySelectorAll('div.messages input'),
|
||||
function(el) {
|
||||
return el.value;
|
||||
}).join('');
|
||||
};
|
||||
var initialHash = inputHash();
|
||||
|
||||
var beforeUnload = function(e) {
|
||||
if (initialHash !== inputHash()) {
|
||||
var msg = 'There are unsaved translations!';
|
||||
(e || window.event).returnValue = msg;
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnload);
|
||||
|
||||
$('form.translation_form').on('submit', function() {
|
||||
window.removeEventListener('beforeunload', beforeUnload);
|
||||
});
|
||||
});
|
||||
|
|
4
public/javascripts/vendor/chessground.min.js
vendored
4
public/javascripts/vendor/chessground.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1271,15 +1271,17 @@ body #themepicker div.is3d,
|
|||
background: #f0f0f0;
|
||||
}
|
||||
.is2d #themepicker div.color_demo {
|
||||
width: 64px;
|
||||
height: 32px;
|
||||
width: 66px;
|
||||
height: 34px;
|
||||
background-size: 256px;
|
||||
border: 1px solid #d0d0d0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
.is3d #themepicker div.color_demo {
|
||||
width: 66px;
|
||||
height: 30px;
|
||||
margin-bottom: 4px;
|
||||
background-size: 264px;
|
||||
border: none;
|
||||
position: relative;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 16586282ce3d6cf67459582a612aa17571ffc9d0
|
|
@ -30,7 +30,7 @@
|
|||
"watchify": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"chessground": "1.10.1",
|
||||
"chessground": "1.10.2",
|
||||
"chessli.js": "file:../chessli",
|
||||
"game": "file:../game",
|
||||
"lodash-node": "^2.4.1",
|
||||
|
|
|
@ -30,7 +30,9 @@ function makeConfig(data, situation, onMove) {
|
|||
duration: data.pref.animationDuration
|
||||
},
|
||||
events: {
|
||||
capture: $.sound.take
|
||||
move: function(orig, dest, captured) {
|
||||
if (captured) $.sound.take();
|
||||
}
|
||||
},
|
||||
disableContextMenu: true
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"watchify": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"chessground": "1.10.1",
|
||||
"chessground": "1.10.2",
|
||||
"lodash-node": "^2.4.1",
|
||||
"mithril": "0.1.30"
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"watchify": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"chessground": "1.10.1",
|
||||
"chessground": "1.10.2",
|
||||
"lodash-node": "^2.4.1",
|
||||
"mithril": "0.1.30"
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"watchify": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"chessground": "1.10.1",
|
||||
"chessground": "1.10.2",
|
||||
"chessli.js": "file:../chessli",
|
||||
"lodash-node": "^2.4.1",
|
||||
"merge": "^1.2.0",
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"watchify": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"chessground": "1.10.1",
|
||||
"chessground": "1.10.2",
|
||||
"chessli.js": "file:../chessli",
|
||||
"lodash-node": "^2.4.1",
|
||||
"merge": "^1.2.0",
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"watchify": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"chessground": "1.10.1",
|
||||
"chessground": "1.10.2",
|
||||
"chessli.js": "file:../chessli",
|
||||
"game": "file:../game",
|
||||
"lodash-node": "^2.4.1",
|
||||
|
|
|
@ -58,12 +58,14 @@ module.exports = function(opts) {
|
|||
$.sound.move(this.data.player.color == 'white');
|
||||
}.bind(this);
|
||||
|
||||
var onCapture = function(key) {
|
||||
if (this.data.game.variant.key === 'atomic') atomic.capture(this, key);
|
||||
else $.sound.take();
|
||||
var onMove = function(orig, dest, captured) {
|
||||
if (captured) {
|
||||
if (this.data.game.variant.key === 'atomic') atomic.capture(this, dest, captured);
|
||||
else $.sound.take();
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
this.chessground = ground.make(this.data, opts.data.game.fen, onUserMove, onCapture);
|
||||
this.chessground = ground.make(this.data, opts.data.game.fen, onUserMove, onMove);
|
||||
|
||||
this.apiMove = function(o) {
|
||||
m.startComputation();
|
||||
|
|
|
@ -46,13 +46,13 @@ function makeConfig(data, fen, flip) {
|
|||
};
|
||||
}
|
||||
|
||||
function make(data, fen, userMove, onCapture) {
|
||||
function make(data, fen, userMove, onMove) {
|
||||
var config = makeConfig(data, fen);
|
||||
config.movable.events = {
|
||||
after: userMove
|
||||
};
|
||||
config.events = {
|
||||
capture: onCapture
|
||||
move: onMove
|
||||
};
|
||||
config.viewOnly = data.player.spectator;
|
||||
return new chessground.controller(config);
|
||||
|
|
|
@ -30,7 +30,12 @@ module.exports = function(send, ctrl) {
|
|||
ctrl.apiMove(o);
|
||||
},
|
||||
premove: function() {
|
||||
ctrl.chessground.playPremove();
|
||||
// atrocious hack to prevent race condition
|
||||
// with explosions and premoves
|
||||
// https://github.com/ornicar/lila/issues/343
|
||||
if (ctrl.data.game.variant.key === 'atomic')
|
||||
setTimeout(ctrl.chessground.playPremove, 100);
|
||||
else ctrl.chessground.playPremove();
|
||||
},
|
||||
castling: function(o) {
|
||||
if (ctrl.replay.active || ctrl.chessground.data.autoCastle) return;
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"game": "file:../game",
|
||||
"chessground": "1.10.1",
|
||||
"chessground": "1.10.2",
|
||||
"lodash-node": "^2.4.1",
|
||||
"mithril": "0.1.30"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue