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:
Thibault Duplessis 2015-03-20 09:14:18 +01:00
commit 44d08507c6
40 changed files with 134 additions and 348 deletions

3
.gitmodules vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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