diff --git a/.gitmodules b/.gitmodules index c2b11ee511..51d2845012 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "public/vendor/tagmanager"] path = public/vendor/tagmanager url = https://github.com/max-favilli/tagmanager -[submodule "submodules/pdfexporter"] - path = submodules/pdfexporter - url = https://github.com/ornicar/lichessPDFExporter [submodule "ui/chessli"] path = ui/chessli url = https://github.com/ornicar/chess.js diff --git a/README.md b/README.md index 8f97e6832f..56beba1ffa 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ name | type | default | description "clock":{ // all clock values are expressed in seconds "initial": 300, "increment": 8, - "totalTime": 540 // evaluation of the game duration = initial + 30 * increment + "totalTime": 620 // evaluation of the game duration = initial + 40 * increment }, "createdAt": 1389100907239, "lastMoveAt": 1389100907239, @@ -300,7 +300,7 @@ name | type | default | description "clock":{ // all clock values are expressed in seconds "initial": 300, "increment": 8, - "totalTime": 540 // evaluation of the game duration = initial + 30 * increment + "totalTime": 620 // evaluation of the game duration = initial + 40 * increment }, "createdAt": 1389100907239, "lastMoveAt": 1389100907239, diff --git a/app/controllers/Auth.scala b/app/controllers/Auth.scala index cdfc2ec10f..bccf7b887a 100644 --- a/app/controllers/Auth.scala +++ b/app/controllers/Auth.scala @@ -96,8 +96,9 @@ object Auth extends LilaController { } } - private def mustConfirmEmailByIP(ip: String): Fu[Boolean] = - api.recentByIpExists(ip) >>| + private def mustConfirmEmailByIP(ip: String, username: String): Fu[Boolean] = + fuccess(username.toLowerCase.contains("argeskent")) >>| + api.recentByIpExists(ip) >>| Mod.ipIntelCache(ip).map(80 <).recover { case _: Exception => false } def signupPost = OpenBody { implicit ctx => @@ -110,7 +111,10 @@ object Auth extends LilaController { data => env.recaptcha.verify(~data.recaptchaResponse, req).flatMap { case false => BadRequest(html.auth.signup(forms.signup.website fill data, env.RecaptchaPublicKey)).fuccess case true => - mustConfirmEmailByIP(HTTPRequest lastRemoteAddress ctx.req) flatMap { mustConfirmEmail => + mustConfirmEmailByIP( + ip = HTTPRequest lastRemoteAddress ctx.req, + username = data.username + ) flatMap { mustConfirmEmail => lila.mon.user.register.website() lila.mon.user.register.mustConfirmEmail(mustConfirmEmail)() val email = env.emailAddress.validate(data.email) err s"Invalid email ${data.email}" @@ -172,21 +176,20 @@ object Auth extends LilaController { html = Unauthorized(html.auth.tor()).fuccess, api = _ => Unauthorized(jsonError("Can't login from Tor, sorry!")).fuccess) - def setFingerprint(fp: String, ms: Int) = Auth { ctx => - me => - api.setFingerprint(ctx.req, fp) flatMap { - _ ?? { hash => - !me.lame ?? { - api.recentUserIdsByFingerprint(hash).map(_.filter(me.id!=)) flatMap { - case otherIds if otherIds.size >= 2 => UserRepo countEngines otherIds flatMap { - case nb if nb >= 2 && nb >= otherIds.size / 2 => Env.report.api.autoCheatPrintReport(me.id) - case _ => funit - } - case _ => funit + def setFingerprint(fp: String, ms: Int) = Auth { ctx => me => + api.setFingerprint(ctx.req, fp) flatMap { + _ ?? { hash => + !me.lame ?? { + api.recentUserIdsByFingerprint(hash).map(_.filter(me.id!=)) flatMap { + case otherIds if otherIds.size >= 2 => UserRepo countEngines otherIds flatMap { + case nb if nb >= 2 && nb >= otherIds.size / 2 => Env.report.api.autoCheatPrintReport(me.id) + case _ => funit } + case _ => funit } } - } inject Ok + } + } inject Ok } def passwordReset = Open { implicit ctx => diff --git a/app/controllers/Export.scala b/app/controllers/Export.scala index 66c79091c5..76a9820de3 100644 --- a/app/controllers/Export.scala +++ b/app/controllers/Export.scala @@ -43,25 +43,6 @@ object Export extends LilaController { }) } - private val PdfRateLimitGlobal = new lila.memo.RateLimit( - credits = 20, - duration = 1 minute, - name = "export PDF global", - key = "export.pdf.global") - - def pdf(id: String) = Open { implicit ctx => - OnlyHumans { - PdfRateLimitGlobal("-", msg = HTTPRequest lastRemoteAddress ctx.req) { - lila.mon.export.pdf() - OptionResult(GameRepo game id) { game => - Ok.chunked(Enumerator.outputStream(env.pdfExport(game.id))).withHeaders( - CONTENT_TYPE -> "application/pdf", - CACHE_CONTROL -> "max-age=7200") - } - } - } - } - private val PngRateLimitGlobal = new lila.memo.RateLimit( credits = 60, duration = 1 minute, diff --git a/app/controllers/LilaController.scala b/app/controllers/LilaController.scala index 3379676184..8180505b36 100644 --- a/app/controllers/LilaController.scala +++ b/app/controllers/LilaController.scala @@ -48,6 +48,9 @@ private[controllers] trait LilaController protected def NoCache(res: Result): Result = res.withHeaders( CACHE_CONTROL -> "no-cache, no-store, must-revalidate", EXPIRES -> "0" ) + protected def NoIframe(res: Result): Result = res.withHeaders( + "X-Frame-Options" -> "SAMEORIGIN" + ) protected def Socket[A: FrameFormatter](f: Context => Fu[(Iteratee[A, _], Enumerator[A])]) = WebSocket.tryAccept[A] { req => diff --git a/app/controllers/Mod.scala b/app/controllers/Mod.scala index 3f4b471586..356311138b 100644 --- a/app/controllers/Mod.scala +++ b/app/controllers/Mod.scala @@ -97,7 +97,7 @@ object Mod extends LilaController { ModExternalBot { OptionFuResult(UserRepo named username) { user => Env.mod.jsonView(user) flatMap { - case None => NotFound.fuccess + case None => NotFound.fuccess case Some(data) => Env.mod.userHistory(user) map { history => Ok(data + ("history" -> history)) } @@ -137,7 +137,8 @@ object Mod extends LilaController { val url = s"http://check.getipintel.net/check.php?ip=$ip&contact=$email" WS.url(url).get().map(_.body).mon(_.security.proxy.request.time).flatMap { str => parseFloatOption(str).fold[Fu[Int]](fufail(s"Invalid ratio ${str.take(140)}")) { ratio => - fuccess((ratio * 100).toInt) + if (ratio < 0) fufail(s"Error code $ratio") + else fuccess((ratio * 100).toInt) } }.addEffects( fail = _ => lila.mon.security.proxy.request.failure(), diff --git a/app/controllers/Tv.scala b/app/controllers/Tv.scala index 3372814830..620fc23f75 100644 --- a/app/controllers/Tv.scala +++ b/app/controllers/Tv.scala @@ -37,14 +37,16 @@ object Tv extends LilaController { val onTv = lila.round.OnTv(channel.key, flip) negotiate( html = { - Env.api.roundApi.watcher(pov, lila.api.Mobile.Api.currentVersion, tv = onTv.some) zip - Env.game.crosstableApi(game) zip - Env.tv.tv.getChampions map { - case ((data, cross), champions) => NoCache { + Env.api.roundApi.watcher(pov, lila.api.Mobile.Api.currentVersion, tv = onTv.some) zip + Env.game.crosstableApi(game) zip + Env.tv.tv.getChampions map { + case ((data, cross), champions) => NoCache { + NoIframe { // can be heavy as TV reloads for each game Ok(html.tv.index(channel, champions, pov, data, cross, flip, history)) } } - }, + } + }, api = apiVersion => Env.api.roundApi.watcher(pov, apiVersion, tv = onTv.some) map { Ok(_) } ) } diff --git a/app/controllers/User.scala b/app/controllers/User.scala index e208010026..645731c617 100644 --- a/app/controllers/User.scala +++ b/app/controllers/User.scala @@ -21,6 +21,7 @@ object User extends LilaController { private def gamePaginator = Env.game.paginator private def forms = lila.user.DataForm private def relationApi = Env.relation.api + private def ratingChartApi = Env.history.ratingChartApi private def userGameSearch = Env.gameSearch.userGameSearch def tv(username: String) = Open { implicit ctx => @@ -192,7 +193,7 @@ object User extends LilaController { } def top200(perfKey: String) = Open { implicit ctx => - lila.rating.PerfType(perfKey).fold(notFound) { perfType => + PerfType(perfKey).fold(notFound) { perfType => env.cached top200Perf perfType.id map { users => Ok(html.user.top200(perfType, users)) } @@ -276,7 +277,7 @@ object User extends LilaController { def perfStat(username: String, perfKey: String) = Open { implicit ctx => OptionFuResult(UserRepo named username) { u => if ((u.disabled || (u.lame && !ctx.is(u))) && !isGranted(_.UserSpy)) notFound - else lila.rating.PerfType(perfKey).fold(notFound) { perfType => + else PerfType(perfKey).fold(notFound) { perfType => for { perfStat <- Env.perfStat.get(u, perfType) ranks <- Env.user.cached.ranking.getAll(u.id) @@ -286,7 +287,13 @@ object User extends LilaController { data = Env.perfStat.jsonView(u, perfStat, ranks get perfType.key, distribution) response <- negotiate( html = Ok(html.user.perfStat(u, ranks, perfType, data)).fuccess, - api = _ => Ok(data).fuccess) + api = _ => + getBool("graph").?? { + Env.history.ratingChartApi.singlePerf(u, perfType).map(_.some) + } map { + _.fold(data) { graph => data + ("graph" -> graph) } + } map { Ok(_) } + ) } yield response } } diff --git a/app/templating/SetupHelper.scala b/app/templating/SetupHelper.scala index 92732ae6d6..89d7088c82 100644 --- a/app/templating/SetupHelper.scala +++ b/app/templating/SetupHelper.scala @@ -61,6 +61,7 @@ trait SetupHelper { self: I18nHelper => variantTuple(chess.variant.Chess960) :+ variantTuple(chess.variant.KingOfTheHill) :+ variantTuple(chess.variant.ThreeCheck) :+ + variantTuple(chess.variant.Antichess) :+ variantTuple(chess.variant.Atomic) :+ variantTuple(chess.variant.Horde) :+ variantTuple(chess.variant.RacingKings) :+ diff --git a/app/views/analyse/replay.scala.html b/app/views/analyse/replay.scala.html index 75b102502d..37d04b31c1 100644 --- a/app/views/analyse/replay.scala.html +++ b/app/views/analyse/replay.scala.html @@ -73,9 +73,6 @@ atom = atom.some) { @if(game.isPgnImport) { @trans.downloadImported() } - @if(false) { - @trans.printFriendlyPDF() - } @if(false && lila.game.Game.visualisableVariants(game.variant)) { Generate images } diff --git a/app/views/analyse/replayBot.scala.html b/app/views/analyse/replayBot.scala.html index ae22764d2a..db72e7a488 100644 --- a/app/views/analyse/replayBot.scala.html +++ b/app/views/analyse/replayBot.scala.html @@ -84,8 +84,6 @@ chessground = true) { / @trans.downloadImported() } - / - @trans.printFriendlyPDF()

@Html(nl2br(escapeHtml(pgn)))
diff --git a/app/views/base/fpmenu.scala.html b/app/views/base/fpmenu.scala.html index 207f3939d2..c5dc8954f1 100644 --- a/app/views/base/fpmenu.scala.html +++ b/app/views/base/fpmenu.scala.html @@ -53,16 +53,12 @@

@trans.learn()

@trans.training() @trans.coordinates() - @ctx.me.map { me => - Study - } - @trans.videoLibrary() Study + @trans.videoLibrary()

@trans.community()

@trans.players() - @trans.ratingStats() @NotForKids { @trans.teams() @trans.forum() diff --git a/app/views/insight/refreshForm.scala.html b/app/views/insight/refreshForm.scala.html index 804f94e82e..0756830980 100644 --- a/app/views/insight/refreshForm.scala.html +++ b/app/views/insight/refreshForm.scala.html @@ -8,11 +8,5 @@ Now crunching data just for you!

-

Would you like to watch
a live game while you wait?

-
- -
-
-

This can take a while,
maybe reload this page later!

diff --git a/bin/prod/deploy b/bin/prod/deploy index 54402ecee5..82655a09f0 100755 --- a/bin/prod/deploy +++ b/bin/prod/deploy @@ -73,7 +73,7 @@ fi lilalog "Rsync scripts, binaries and assets" stage="target/universal/stage" -include="bin $stage/bin $stage/lib public submodules" +include="bin $stage/bin $stage/lib public" rsync_command="rsync $RSYNC_OPTIONS $include $REMOTE:$REMOTE_DIR" echo "$rsync_command" $rsync_command diff --git a/conf/base.conf b/conf/base.conf index 7db85b9325..4fdfaf426b 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -389,7 +389,6 @@ game { } net.base_url = ${net.base_url} uci_memo.ttl = 2 minutes - pdf.exec_path = "submodules/pdfexporter" png { url = "http://homer.lichess.org:8080/board.png" size = 512 @@ -412,7 +411,7 @@ explorer { internal_endpoint = "http://expl.lichess.org" index_flow = false tablebase = { - endpoint = "https://expl.lichess.org/tablebase" + endpoint = "https://tablebase.lichess.org" } } gameSearch { @@ -761,11 +760,49 @@ kamon { } } + influxdb { + hostname = "127.0.0.1" + port = 8086 + + # The maximum packet size for one POST request, set to 0 to disable batching + max-packet-size = 16384 + + # The protocol, either http or udp + protocol = "udb" + + # The measurements will be named ${application-name}-timers and -counters + application-name = "dev" + + # Allow users to override the name of the hostname reported by kamon. When changed, the hostname tag will be + # the value given here. + hostname-override = none + + # For histograms, which percentiles to count + percentiles = [50.0, 70.0, 90.0, 95.0, 99.0, 99.9] + + # Subscription patterns used to select which metrics will be pushed to InfluxDB. Note that first, metrics + # collection for your desired entities must be activated under the kamon.metrics.filters settings. + subscriptions { + histogram = [ "**" ] + min-max-counter = [ "**" ] + gauge = [ "**" ] + counter = [ "**" ] + trace = [ "**" ] + trace-segment = [ "**" ] + akka-actor = [ "**" ] + akka-dispatcher = [ "**" ] + akka-router = [ "**" ] + system-metric = [ "**" ] + http-server = [ "**" ] + } + } + modules { kamon-statsd { auto-start = no - requires-aspectj = no - extension-id = "kamon.statsd.StatsD" + } + kamon-influxdb { + auto-start = no } } } diff --git a/conf/routes b/conf/routes index dc6631a3d2..ad5a17914b 100644 --- a/conf/routes +++ b/conf/routes @@ -256,7 +256,6 @@ POST /team/:id/close controllers.Team.close(id: String) POST /$gameId<\w{8}>/request-analysis controllers.Analyse.requestAnalysis(gameId: String) GET /game/export/$gameId<\w{8}>.pgn controllers.Export.pgn(gameId: String) -GET /game/export/pdf/$gameId<\w{8}>.pdf controllers.Export.pdf(gameId: String) GET /game/export/png/$gameId<\w{8}>.png controllers.Export.png(gameId: String) GET /game/visualizer/$gameId<\w{8}> controllers.Export.visualizer(gameId: String) diff --git a/modules/api/src/main/Env.scala b/modules/api/src/main/Env.scala index 0bca2b94f1..719c2620af 100644 --- a/modules/api/src/main/Env.scala +++ b/modules/api/src/main/Env.scala @@ -86,6 +86,7 @@ final class Env( relationApi = relationApi, bookmarkApi = bookmarkApi, crosstableApi = crosstableApi, + gameCache = gameCache, prefApi = prefApi) val gameApi = new GameApi( @@ -95,17 +96,18 @@ final class Env( gameCache = gameCache) val userGameApi = new UserGameApi( - bookmarkApi = bookmarkApi) + bookmarkApi = bookmarkApi, + lightUser = userEnv.lightUser) val roundApi = new RoundApiBalancer( api = new RoundApi( - jsonView = roundJsonView, - noteApi = noteApi, - forecastApi = forecastApi, - bookmarkApi = bookmarkApi, - getTourAndRanks = getTourAndRanks, - getSimul = getSimul, - lightUser = userEnv.lightUser), + jsonView = roundJsonView, + noteApi = noteApi, + forecastApi = forecastApi, + bookmarkApi = bookmarkApi, + getTourAndRanks = getTourAndRanks, + getSimul = getSimul, + lightUser = userEnv.lightUser), system = system, nbActors = math.max(1, math.min(16, Runtime.getRuntime.availableProcessors - 1))) diff --git a/modules/api/src/main/UserApi.scala b/modules/api/src/main/UserApi.scala index 472ed9198f..44f12e9cce 100644 --- a/modules/api/src/main/UserApi.scala +++ b/modules/api/src/main/UserApi.scala @@ -15,6 +15,7 @@ private[api] final class UserApi( relationApi: lila.relation.RelationApi, bookmarkApi: lila.bookmark.BookmarkApi, crosstableApi: lila.game.CrosstableApi, + gameCache: lila.game.Cached, prefApi: lila.pref.PrefApi, makeUrl: String => String) { @@ -32,8 +33,10 @@ private[api] final class UserApi( ctx.isAuth.?? { prefApi followable u.id } zip ctx.userId.?? { relationApi.fetchRelation(_, u.id) } zip ctx.userId.?? { relationApi.fetchFollows(u.id, _) } zip - bookmarkApi.countByUser(u) map { - case (((((((gameOption, nbGamesWithMe), following), followers), followable), relation), isFollowed), nbBookmarks) => + bookmarkApi.countByUser(u) zip + gameCache.nbPlaying(u.id) zip + gameCache.nbImportedBy(u.id) map { + case (((((((((gameOption, nbGamesWithMe), following), followers), followable), relation), isFollowed), nbBookmarks), nbPlaying), nbImported) => jsonView(u) ++ { Json.obj( "url" -> makeUrl(s"@/$username"), @@ -51,6 +54,8 @@ private[api] final class UserApi( "win" -> u.count.win, "winH" -> u.count.winH, "bookmark" -> nbBookmarks, + "playing" -> nbPlaying, + "import" -> nbImported, "me" -> nbGamesWithMe) ) ++ ctx.isAuth.??(Json.obj( "followable" -> followable, diff --git a/modules/api/src/main/UserGameApi.scala b/modules/api/src/main/UserGameApi.scala index 2db8db2f3a..194d1a3df7 100644 --- a/modules/api/src/main/UserGameApi.scala +++ b/modules/api/src/main/UserGameApi.scala @@ -3,13 +3,17 @@ package lila.api import play.api.libs.json._ import chess.format.Forsyth +import lila.common.LightUser import lila.common.paginator.Paginator import lila.common.PimpedJson._ import lila.game.{ Game, PerfPicker } -final class UserGameApi(bookmarkApi: lila.bookmark.BookmarkApi) { +final class UserGameApi( + bookmarkApi: lila.bookmark.BookmarkApi, + lightUser: LightUser.Getter) { import lila.round.JsonView._ + import LightUser.lightUserWrites def filter(filterName: String, pag: Paginator[Game])(implicit ctx: Context): Fu[JsObject] = bookmarkApi.filterGameIdsBookmarkedBy(pag.currentPageResults, ctx.me) map { bookmarkedIds => @@ -35,10 +39,11 @@ final class UserGameApi(bookmarkApi: lila.bookmark.BookmarkApi) { "correspondence" -> g.daysPerTurn.map { d => Json.obj("daysPerTurn" -> d) }, - "opening" -> g.opening, + "source" -> g.source.map(_.name), "players" -> JsObject(g.players map { p => p.color.name -> Json.obj( - "userId" -> p.userId, + "user" -> p.userId.flatMap(lightUser), + "userId" -> p.userId, // for BC "name" -> p.name, "aiLevel" -> p.aiLevel, "rating" -> p.rating, diff --git a/modules/challenge/src/main/Challenge.scala b/modules/challenge/src/main/Challenge.scala index b7e6da4030..bd6d0070c0 100644 --- a/modules/challenge/src/main/Challenge.scala +++ b/modules/challenge/src/main/Challenge.scala @@ -116,7 +116,7 @@ object Challenge { private val idSize = 8 - private def randomId = ornicar.scalalib.Random nextStringUppercase idSize + private def randomId = ornicar.scalalib.Random nextString idSize private def toRegistered(variant: Variant, timeControl: TimeControl)(u: User) = Registered(u.id, Rating(u.perfs(perfTypeOf(variant, timeControl)))) diff --git a/modules/chess b/modules/chess index ed23fd150d..ad672e9ae2 160000 --- a/modules/chess +++ b/modules/chess @@ -1 +1 @@ -Subproject commit ed23fd150d9d119f2089afbe3b2412973c58a152 +Subproject commit ad672e9ae20869a11e1d0db6f9913903bbbd8b53 diff --git a/modules/common/src/main/LilaCookie.scala b/modules/common/src/main/LilaCookie.scala index 1cc0f15d96..d3f49c419c 100644 --- a/modules/common/src/main/LilaCookie.scala +++ b/modules/common/src/main/LilaCookie.scala @@ -14,7 +14,7 @@ object LilaCookie { val sessionId = "sid" - def makeSessionId(implicit req: RequestHeader) = session(sessionId, Random nextStringUppercase 8) + def makeSessionId(implicit req: RequestHeader) = session(sessionId, Random secureString 10) def session(name: String, value: String)(implicit req: RequestHeader): Cookie = withSession { s => s + (name -> value) diff --git a/modules/db/src/main/ByteArray.scala b/modules/db/src/main/ByteArray.scala index 7ad5f121bb..9f2428781b 100644 --- a/modules/db/src/main/ByteArray.scala +++ b/modules/db/src/main/ByteArray.scala @@ -5,7 +5,7 @@ import scala.util.{ Try, Success, Failure } import reactivemongo.bson._ import reactivemongo.bson.utils.Converters -case class ByteArray(value: Array[Byte]) { +case class ByteArray(value: Array[Byte]) extends AnyVal { def isEmpty = value.isEmpty diff --git a/modules/event/src/main/Event.scala b/modules/event/src/main/Event.scala index b4535e4a58..95f5ef7ab4 100644 --- a/modules/event/src/main/Event.scala +++ b/modules/event/src/main/Event.scala @@ -40,7 +40,7 @@ case class Event( object Event { - def makeId = ornicar.scalalib.Random nextStringUppercase 8 + def makeId = ornicar.scalalib.Random nextString 8 case class UserId(value: String) extends AnyVal } diff --git a/modules/forum/src/main/Post.scala b/modules/forum/src/main/Post.scala index 15a88954bb..15a2a7ad27 100644 --- a/modules/forum/src/main/Post.scala +++ b/modules/forum/src/main/Post.scala @@ -77,7 +77,7 @@ object Post { hidden: Boolean): Post = { Post( - _id = Random nextStringUppercase idSize, + _id = Random nextString idSize, topicId = topicId, author = author, userId = userId, diff --git a/modules/forum/src/main/Topic.scala b/modules/forum/src/main/Topic.scala index 00c8c9586b..fd63e1ba4a 100644 --- a/modules/forum/src/main/Topic.scala +++ b/modules/forum/src/main/Topic.scala @@ -45,7 +45,7 @@ object Topic { def nameToId(name: String) = (lila.common.String slugify name) |> { slug => // if most chars are not latin, go for random slug - (slug.size > (name.size / 2)).fold(slug, Random nextStringUppercase 8) + (slug.size > (name.size / 2)).fold(slug, Random nextString 8) } val idSize = 8 diff --git a/modules/game/src/main/BSONHandlers.scala b/modules/game/src/main/BSONHandlers.scala index 32ffd0fa8b..671624f6bd 100644 --- a/modules/game/src/main/BSONHandlers.scala +++ b/modules/game/src/main/BSONHandlers.scala @@ -5,10 +5,12 @@ import org.joda.time.DateTime import reactivemongo.bson._ import chess.variant.{ Variant, Crazyhouse } -import chess.{ CheckCount, Color, Clock, White, Black, Status, Mode } +import chess.{ CheckCount, Color, Clock, White, Black, Status, Mode, UnmovedRooks } object BSONHandlers { + import lila.db.ByteArray.ByteArrayBSONHandler + private[game] implicit val checkCountWriter = new BSONWriter[CheckCount, BSONArray] { def write(cc: CheckCount) = BSONArray(cc.white, cc.black) } @@ -18,17 +20,26 @@ object BSONHandlers { def write(x: Status) = BSONInteger(x.id) } + private[game] implicit val unmovedRooksHandler = new BSONHandler[BSONBinary, UnmovedRooks] { + def read(bin: BSONBinary): UnmovedRooks = BinaryFormat.unmovedRooks.read { + ByteArrayBSONHandler.read(bin) + } + def write(x: UnmovedRooks): BSONBinary = ByteArrayBSONHandler.write { + BinaryFormat.unmovedRooks.write(x) + } + } + private[game] implicit val crazyhouseDataBSONHandler = new BSON[Crazyhouse.Data] { import Crazyhouse._ def reads(r: BSON.Reader) = Crazyhouse.Data( pockets = { - val (white, black) = r.str("p").toList.flatMap(chess.Piece.fromChar).partition(_ is chess.White) - Pockets( - white = Pocket(white.map(_.role)), - black = Pocket(black.map(_.role))) - }, + val (white, black) = r.str("p").toList.flatMap(chess.Piece.fromChar).partition(_ is chess.White) + Pockets( + white = Pocket(white.map(_.role)), + black = Pocket(black.map(_.role))) + }, promoted = r.str("t").toSet.flatMap(chess.Pos.piotr)) def writes(w: BSON.Writer, o: Crazyhouse.Data) = BSONDocument( @@ -79,6 +90,7 @@ object BSONHandlers { CheckCount(~counts.headOption, ~counts.lastOption) }, castleLastMoveTime = r.get[CastleLastMoveTime](castleLastMoveTime)(CastleLastMoveTime.castleLastMoveTimeBSONHandler), + unmovedRooks = r.getO[UnmovedRooks](unmovedRooks) | UnmovedRooks.default, daysPerTurn = r intO daysPerTurn, binaryMoveTimes = (r bytesO moveTimes) | ByteArray.empty, mode = Mode(r boolD rated), @@ -113,6 +125,7 @@ object BSONHandlers { positionHashes -> w.bytesO(o.positionHashes), checkCount -> o.checkCount.nonEmpty.option(o.checkCount), castleLastMoveTime -> CastleLastMoveTime.castleLastMoveTimeBSONHandler.write(o.castleLastMoveTime), + unmovedRooks -> o.unmovedRooks, daysPerTurn -> o.daysPerTurn, moveTimes -> (BinaryFormat.moveTime write o.moveTimes), rated -> w.boolO(o.mode.rated), @@ -131,8 +144,6 @@ object BSONHandlers { ) } - import lila.db.ByteArray.ByteArrayBSONHandler - private[game] def clockBSONReader(since: DateTime, whiteBerserk: Boolean, blackBerserk: Boolean) = new BSONReader[BSONBinary, Color => Clock] { def read(bin: BSONBinary) = BinaryFormat.clock(since).read( ByteArrayBSONHandler read bin, whiteBerserk, blackBerserk diff --git a/modules/game/src/main/BinaryFormat.scala b/modules/game/src/main/BinaryFormat.scala index 28855b7981..efde5fd623 100644 --- a/modules/game/src/main/BinaryFormat.scala +++ b/modules/game/src/main/BinaryFormat.scala @@ -226,6 +226,45 @@ object BinaryFormat { } } + object unmovedRooks { + + val emptyByteArray = ByteArray(Array(0, 0)) + + def write(o: UnmovedRooks): ByteArray = { + if (o.pos.isEmpty) emptyByteArray + else { + var white = 0 + var black = 0 + o.pos.foreach { pos => + if (pos.y == 1) white = white | (1 << (8 - pos.x)) + else black = black | (1 << (8 - pos.x)) + } + ByteArray(Array(white.toByte, black.toByte)) + } + } + + private def bitAt(n: Int, k: Int) = (n >> k) & 1 + + private val arrIndexes = 0 to 1 + private val bitIndexes = 0 to 7 + private val whiteStd = Set(Pos.A1, Pos.H1) + private val blackStd = Set(Pos.A8, Pos.H8) + + def read(ba: ByteArray) = UnmovedRooks { + var set = Set.empty[Pos] + arrIndexes.foreach { i => + val int = ba.value(i).toInt + if (int != 0) { + if (int == -127) set = if (i == 0) whiteStd else set ++ blackStd + else bitIndexes.foreach { j => + if (bitAt(int, j) == 1) set = set + Pos.posAt(8 - j, 1 + 7 * i).get + } + } + } + set + } + } + @inline private def toInt(b: Byte): Int = b & 0xff def writeInt8(int: Int) = math.min(255, int) diff --git a/modules/game/src/main/Env.scala b/modules/game/src/main/Env.scala index 8608746837..bd321bbeb8 100644 --- a/modules/game/src/main/Env.scala +++ b/modules/game/src/main/Env.scala @@ -29,7 +29,6 @@ final class Env( val JsPathCompiled = config getString "js_path.compiled" val UciMemoTtl = config duration "uci_memo.ttl" val netBaseUrl = config getString "net.base_url" - val PdfExecPath = config getString "pdf.exec_path" val PngUrl = config getString "png.url" val PngSize = config getInt "png.size" } @@ -39,8 +38,6 @@ final class Env( lazy val playTime = new PlayTime(gameColl) - lazy val pdfExport = PdfExport(PdfExecPath) _ - lazy val pngExport = new PngExport(PngUrl, PngSize) lazy val divider = new Divider diff --git a/modules/game/src/main/Game.scala b/modules/game/src/main/Game.scala index 666300d1c6..c8ce6512b1 100644 --- a/modules/game/src/main/Game.scala +++ b/modules/game/src/main/Game.scala @@ -5,7 +5,7 @@ import chess.format.{ Uci, FEN } import chess.opening.{ FullOpening, FullOpeningDB } import chess.Pos.piotr, chess.Role.forsyth import chess.variant.{ Variant, Crazyhouse } -import chess.{ History => ChessHistory, CheckCount, Castles, Role, Board, MoveOrDrop, Pos, Game => ChessGame, Clock, Status, Color, Piece, Mode, PositionHash } +import chess.{ History => ChessHistory, CheckCount, Castles, Role, Board, MoveOrDrop, Pos, Game => ChessGame, Clock, Status, Color, Piece, Mode, PositionHash, UnmovedRooks } import org.joda.time.DateTime import scala.concurrent.duration.FiniteDuration @@ -24,6 +24,7 @@ case class Game( startedAtTurn: Int, clock: Option[Clock], castleLastMoveTime: CastleLastMoveTime, + unmovedRooks: UnmovedRooks, daysPerTurn: Option[Int], positionHashes: PositionHash = Array(), checkCount: CheckCount = CheckCount(0, 0), @@ -147,7 +148,8 @@ case class Game( }, castles = castleLastMoveTime.castles, positionHashes = positionHashes, - checkCount = checkCount) + checkCount = checkCount, + unmovedRooks = unmovedRooks) def update( game: ChessGame, @@ -176,6 +178,7 @@ case class Game( lastMove = history.lastMove.map(_.origDest), lastMoveTime = Some(((nowMillis - createdAt.getMillis) / 100).toInt), check = situation.checkSquare), + unmovedRooks = game.board.unmovedRooks, binaryMoveTimes = isPgnImport.fold( ByteArray.empty, BinaryFormat.moveTime write lastMoveTime.fold(Vector(0)) { lmt => @@ -503,6 +506,7 @@ object Game { chess.variant.Chess960, chess.variant.KingOfTheHill, chess.variant.ThreeCheck, + chess.variant.Antichess, chess.variant.FromPosition, chess.variant.Horde, chess.variant.Atomic, @@ -556,14 +560,16 @@ object Game { id = IdGenerator.game, whitePlayer = whitePlayer, blackPlayer = blackPlayer, - binaryPieces = if (game.isStandardInit) BinaryFormat.piece.standard - else BinaryFormat.piece write game.board.pieces, + binaryPieces = + if (game.isStandardInit) BinaryFormat.piece.standard + else BinaryFormat.piece write game.board.pieces, binaryPgn = ByteArray.empty, status = Status.Created, turns = game.turns, startedAtTurn = game.startedAtTurn, clock = game.clock, castleLastMoveTime = CastleLastMoveTime.init.copy(castles = game.board.history.castles), + unmovedRooks = game.board.unmovedRooks, daysPerTurn = daysPerTurn, mode = mode, variant = variant, @@ -594,6 +600,7 @@ object Game { val positionHashes = "ph" val checkCount = "cc" val castleLastMoveTime = "cl" + val unmovedRooks = "ur" val daysPerTurn = "cd" val moveTimes = "mt" val rated = "ra" diff --git a/modules/game/src/main/GameDiff.scala b/modules/game/src/main/GameDiff.scala index 96245471c2..37d1d31bf5 100644 --- a/modules/game/src/main/GameDiff.scala +++ b/modules/game/src/main/GameDiff.scala @@ -1,18 +1,18 @@ package lila.game -import chess.{ Clock, Pos, CheckCount } +import chess.{ Clock, Pos, CheckCount, UnmovedRooks } import chess.variant.Crazyhouse import Game.BSONFields._ import org.joda.time.DateTime import reactivemongo.bson._ import lila.db.BSON.BSONJodaDateTimeHandler -import lila.db.ByteArray +import lila.db.ByteArray.ByteArrayBSONHandler private[game] object GameDiff { type Set = BSONElement // [String, BSONValue] - type Unset = BSONElement //[String, BSONBoolean] + type Unset = BSONElement // [String, BSONBoolean] def apply(a: Game, b: Game): (List[Set], List[Unset]) = { @@ -40,12 +40,13 @@ private[game] object GameDiff { val w = lila.db.BSON.writer - d(binaryPieces, _.binaryPieces, ByteArray.ByteArrayBSONHandler.write) - d(binaryPgn, _.binaryPgn, ByteArray.ByteArrayBSONHandler.write) + d(binaryPieces, _.binaryPieces, ByteArrayBSONHandler.write) + d(binaryPgn, _.binaryPgn, ByteArrayBSONHandler.write) d(status, _.status.id, w.int) d(turns, _.turns, w.int) d(castleLastMoveTime, _.castleLastMoveTime, CastleLastMoveTime.castleLastMoveTimeBSONHandler.write) - d(moveTimes, _.moveTimes, (x: Vector[Int]) => ByteArray.ByteArrayBSONHandler.write(BinaryFormat.moveTime write x)) + d(unmovedRooks, _.unmovedRooks, (x: UnmovedRooks) => ByteArrayBSONHandler.write(BinaryFormat.unmovedRooks write x)) + d(moveTimes, _.moveTimes, (x: Vector[Int]) => ByteArrayBSONHandler.write(BinaryFormat.moveTime write x)) dOpt(positionHashes, _.positionHashes, w.bytesO) dOpt(clock, _.clock, (o: Option[Clock]) => o map { c => BSONHandlers.clockBSONWrite(a.createdAt, c) diff --git a/modules/game/src/main/GameRepo.scala b/modules/game/src/main/GameRepo.scala index 1ec8e309ce..5903de4b8d 100644 --- a/modules/game/src/main/GameRepo.scala +++ b/modules/game/src/main/GameRepo.scala @@ -223,6 +223,7 @@ object GameRepo { val partialUnsets = $doc( F.positionHashes -> true, F.playingUids -> true, + F.unmovedRooks -> true, ("p0." + Player.BSONFields.lastDrawOffer) -> true, ("p1." + Player.BSONFields.lastDrawOffer) -> true, ("p0." + Player.BSONFields.isOfferingDraw) -> true, diff --git a/modules/game/src/main/IdGenerator.scala b/modules/game/src/main/IdGenerator.scala index 56df5c44a6..f2ec21b360 100644 --- a/modules/game/src/main/IdGenerator.scala +++ b/modules/game/src/main/IdGenerator.scala @@ -4,9 +4,7 @@ import ornicar.scalalib.Random object IdGenerator { - def game = Random nextStringUppercase Game.gameIdSize + def game = Random nextString Game.gameIdSize - def token = Random nextStringUppercase Game.tokenSize - - def player = Random nextStringUppercase Game.playerIdSize + def player = Random secureString Game.playerIdSize } diff --git a/modules/game/src/main/PdfExport.scala b/modules/game/src/main/PdfExport.scala deleted file mode 100644 index 3b208b0aab..0000000000 --- a/modules/game/src/main/PdfExport.scala +++ /dev/null @@ -1,14 +0,0 @@ -package lila.game - -import java.io.{ File, OutputStream } -import scala.sys.process._ - -object PdfExport { - - private val logger = ProcessLogger(_ => (), _ => ()) - - def apply(execPath: String)(id: String)(out: OutputStream) { - val exec = Process(Seq("php", "main.php", id), new File(execPath)) - exec #> out ! logger - } -} diff --git a/modules/game/src/main/Rewind.scala b/modules/game/src/main/Rewind.scala index 0e8415223c..5f52d1ea9f 100644 --- a/modules/game/src/main/Rewind.scala +++ b/modules/game/src/main/Rewind.scala @@ -32,6 +32,7 @@ object Rewind { lastMove = rewindedHistory.lastMove.map(_.origDest), lastMoveTime = Some(((nowMillis - game.createdAt.getMillis) / 100).toInt), check = if (rewindedSituation.check) rewindedSituation.kingPos else None), + unmovedRooks = rewindedGame.board.unmovedRooks, binaryMoveTimes = BinaryFormat.moveTime write (game.moveTimes take rewindedGame.turns), crazyData = rewindedSituation.board.crazyData, status = game.status, diff --git a/modules/game/src/test/BinaryPerfTest.scala b/modules/game/src/test/BinaryPerfTest.scala new file mode 100644 index 0000000000..7389995ba5 --- /dev/null +++ b/modules/game/src/test/BinaryPerfTest.scala @@ -0,0 +1,64 @@ +package lila.game + +import org.specs2.mutable.Specification + +import lila.db.ByteArray +import chess._ +import Pos._ + +class BinaryPerfTest extends Specification { + + sequential + + val format = BinaryFormat.unmovedRooks + + val dataset: List[UnmovedRooks] = List[Set[Pos]]( + Set(A1, H1, A8, H8), + Set(H1, A8, H8), + Set(A1, A8, H8), + Set(A1, H1, H8), + Set(A1, H1, A8), + // Set(A8, H8), + // Set(A1, H8), + // Set(A1, H1), + // Set(H1, A8), + Set() + ) map UnmovedRooks.apply + + val encodedDataset: List[ByteArray] = dataset map format.write + + val nbRuns = 10 + + type Run = () => Unit + + def readDataset() { encodedDataset foreach format.read } + def writeDataset() { dataset foreach format.write } + + def runTests(run: Run, name: String, iterations: Int) = { + println(s"$name warming up") + for (i <- 1 to iterations) run() + println(s"$name running") + val durations = for (i ← 1 to nbRuns) yield { + val start = System.currentTimeMillis + for (i <- 1 to iterations) run() + val duration = System.currentTimeMillis - start + println(s"$name $iterations times in $duration ms") + duration + } + val totalNb = iterations * nbRuns + val moveNanos = (1000000 * durations.sum) / totalNb + println(s"Average = $moveNanos nanoseconds each") + println(s" ${1000000000 / moveNanos} $name per second") + true === true + } + + "unmoved rooks" should { + "read" in { + runTests(readDataset, "read", 100000) + } + "write" in { + runTests(writeDataset, "write", 1000000) + } + } +} + diff --git a/modules/game/src/test/BinaryUnmovedRooksTest.scala b/modules/game/src/test/BinaryUnmovedRooksTest.scala new file mode 100644 index 0000000000..2f56a170e4 --- /dev/null +++ b/modules/game/src/test/BinaryUnmovedRooksTest.scala @@ -0,0 +1,57 @@ +package lila.game + +import scala.concurrent.duration._ + +import chess._ +import chess.Pos._ +import org.specs2.mutable._ +import org.specs2.specification._ + +import lila.db.ByteArray + +class BinaryUnmovedRooksTest extends Specification { + + val _0_ = "00000000" + val _1_ = "11111111" + def write(all: UnmovedRooks): List[String] = + (BinaryFormat.unmovedRooks write all).showBytes.split(',').toList + def read(bytes: List[String]): UnmovedRooks = + BinaryFormat.unmovedRooks read ByteArray.parseBytes(bytes) + + "binary unmovedRooks" should { + "write" in { + write(UnmovedRooks(Set(A1, H1, A8, H8))) must_== { + List("10000001", "10000001") + } + write(UnmovedRooks(Set.empty)) must_== { + List(_0_, _0_) + } + write(UnmovedRooks.default) must_== { + List(_1_, _1_) + } + write(UnmovedRooks(Set(A1, B1, C1))) must_== { + List("11100000", _0_) + } + write(UnmovedRooks(Set(A8, B8, C8))) must_== { + List(_0_, "11100000") + } + } + "read" in { + read(List("10000001", "10000001")) must_== { + UnmovedRooks(Set(A1, H1, A8, H8)) + } + read(List(_0_, _0_)) must_== { + UnmovedRooks(Set.empty) + } + read(List(_1_, _1_)) must_== { + UnmovedRooks.default + } + read(List("11100000", _0_)) must_== { + UnmovedRooks(Set(A1, B1, C1)) + } + read(List(_0_, "11100000")) must_== { + UnmovedRooks(Set(A8, B8, C8)) + } + } + } +} diff --git a/modules/history/src/main/HistoryApi.scala b/modules/history/src/main/HistoryApi.scala index 60e3ceaee5..41997bcc41 100644 --- a/modules/history/src/main/HistoryApi.scala +++ b/modules/history/src/main/HistoryApi.scala @@ -51,7 +51,7 @@ final class HistoryApi(coll: Coll) { ).void } - def daysBetween(from: DateTime, to: DateTime): Int = + private def daysBetween(from: DateTime, to: DateTime): Int = Days.daysBetween(from.withTimeAtStartOfDay, to.withTimeAtStartOfDay).getDays def get(userId: String): Fu[Option[History]] = coll.uno[History]($id(userId)) diff --git a/modules/history/src/main/RatingChartApi.scala b/modules/history/src/main/RatingChartApi.scala index b0866c0fec..3588f79dc3 100644 --- a/modules/history/src/main/RatingChartApi.scala +++ b/modules/history/src/main/RatingChartApi.scala @@ -18,6 +18,11 @@ final class RatingChartApi( chart.nonEmpty option chart } + def singlePerf(user: User, perfType: PerfType): Fu[JsArray] = + historyApi.ratingsMap(user, perfType) map { + ratingsMapToJson(user, _) + } map JsArray.apply + private val cache = mongoCache[User, String]( prefix = "history:rating", f = (user: User) => build(user) map (~_), @@ -25,35 +30,24 @@ final class RatingChartApi( timeToLive = cacheTtl, keyToString = _.id) - private val columns = Json stringify { - Json.arr( - Json.arr("string", "Date"), - Json.arr("number", "Standard"), - Json.arr("number", "Opponent Rating"), - Json.arr("number", "Average") - ) + private def ratingsMapToJson(user: User, ratingsMap: RatingsMap) = ratingsMap.map { + case (days, rating) => + val date = user.createdAt plusDays days + Json.arr(date.getYear, date.getMonthOfYear - 1, date.getDayOfMonth, rating) } - private def build(user: User): Fu[Option[String]] = { - - def ratingsMapToJson(perfType: PerfType, ratingsMap: RatingsMap) = Json obj ( - "name" -> perfType.name, - "points" -> ratingsMap.map { - case (days, rating) => - val date = user.createdAt plusDays days - Json.arr(date.getYear, date.getMonthOfYear - 1, date.getDayOfMonth, rating) - } - ) - + private def build(user: User): Fu[Option[String]] = historyApi get user.id map2 { (history: History) => Json stringify { Json.toJson { import lila.rating.PerfType._ List(Bullet, Blitz, Classical, Correspondence, Chess960, KingOfTheHill, ThreeCheck, Antichess, Atomic, Horde, RacingKings, Crazyhouse, Puzzle) map { pt => - ratingsMapToJson(pt, history(pt)) + Json.obj( + "name" -> pt.name, + "points" -> ratingsMapToJson(user, history(pt)) + ) } } } } - } } diff --git a/modules/i18n/messages/messages.af b/modules/i18n/messages/messages.af index 798f4f6376..70d9819be3 100644 --- a/modules/i18n/messages/messages.af +++ b/modules/i18n/messages/messages.af @@ -453,3 +453,20 @@ tournamentNotFound=Toernooi nie te vinde tournamentDoesNotExist=Dié toernooi bestaan nie. tournamentMayHaveBeenCanceled=Miskien was dit gekanselleer, as alle spelers die toernooi verlaat het voor die aanvang. returnToTournamentsHomepage=Keer terug na die toernooie tuisblad +youHaveAlreadyRegisteredTheEmail=Jy het reeds geregistreer die e-pos:%s +kidMode=Kid af +playChessEverywhere=Speel skaak oral +asFreeAsLichess=So vry soos lichess +builtForTheLoveOfChessNotMoney=Gebou vir die liefde van skaak, nie geld +everybodyGetsAllFeaturesForFree=Almal kry al die funksies vir gratis +zeroAdvertisement=zero advertensie +fullFeatured=volledige +phoneAndTablet=Selfoon en tablet +bulletBlitzClassical=Bullet, blitz, klassieke +correspondenceChess=korrespondensie skaak +onlineAndOfflinePlay=Aanlyn en aflyn speel +correspondenceAndUnlimited=Korrespondensie en onbeperkte +viewTheSolution=Kyk na die oplossing +followAndChallengeFriends=Volg en vriende uit te daag +availableInNbLanguages=Beskikbaar in%s tale! +gameAnalysis=game analise diff --git a/modules/i18n/messages/messages.am b/modules/i18n/messages/messages.am new file mode 100644 index 0000000000..696fc7d844 --- /dev/null +++ b/modules/i18n/messages/messages.am @@ -0,0 +1,4 @@ +playWithAFriend=ከጓደኛ ጋር ይጫወቱ +playWithTheMachine=ማሽኑ ጋር ይጫወቱ m +toInviteSomeoneToPlayGiveThisUrl=ለማጫወት አንድ ሰው ለመጋበዝ, ይህን ዩአርኤል መስጠት +computersAreNotAllowedToPlay=ኮምፒዩተሮች እና ኮምፒውተር-ድጋፍ ተጫዋቾች መጫወት አይፈቀድላቸውም. በማጫወት ላይ ሳለ, ወይም ከሌሎች ተጫዋቾች ከ የቼዝ ፕሮግራሞች, ጎታዎች እርዳታ ማግኘት አይስጡ. በተጨማሪም በርካታ መለያዎች ማድረግ በጥብቅ ተስፋ እና ከመጠን የብዝሃ-የሂሳብ እገዳ እየተደረገ ሊመራ እንደሚችል ልብ ይበሉ. diff --git a/modules/i18n/messages/messages.ca b/modules/i18n/messages/messages.ca index 520c5f3092..5c82d848d2 100644 --- a/modules/i18n/messages/messages.ca +++ b/modules/i18n/messages/messages.ca @@ -280,8 +280,8 @@ puzzleFailed=Trencaclosques equivocat butYouCanKeepTrying=Podeu seguir intentant-ho. victory=Victòria! giveUp=Abandona -puzzleSolvedInXSeconds=Trencaclosques solucionat amb %s segons -wasThisPuzzleAnyGood=Ha estat bo aquest puzle? +puzzleSolvedInXSeconds=Trencaclosques solucionat en %s segons +wasThisPuzzleAnyGood=Ha valgut la pena aquest puzle? pleaseVotePuzzle=Ajudeu el lichess a millorar mitjançant el vostre vot amb les fletxes amunt o avall: thankYou=Gràcies! ratingX=Puntuació: %s @@ -321,7 +321,7 @@ reason=Raó whatIsIheMatter=Què succeeix? cheat=Trampós insult=Insult -troll=Trol +troll=Troll other=Altres reportDescriptionHelp=Enganxa el link del/s joc/s i explica què hi ha de malament en el comportament d'aquest usuari by=per %s @@ -337,7 +337,7 @@ pieceAnimation=Animació de les peces materialDifference=Diferència de material closeAccount=Tancar el compte closeYourAccount=Tanca el teu compte -changedMindDoNotCloseAccount=He canviat d'opinió, no vull tancar el meu compte +changedMindDoNotCloseAccount=He canviat d'opinió, no tanqueu el meu compte closeAccountExplanation=Estàs segur que vols tancar el teu compte? Tancar-lo és una decisió permanent. Ja no podràs iniciar la sessió, i la teva pàgina de perfil no serà accessible. thisAccountIsClosed=Aquest compte està tancat invalidUsernameOrPassword=Nom d'usuari o contrasenya incorrectes @@ -402,7 +402,7 @@ unauthorizedError=Accés no autoritzat noInternetConnection=Sense connexió a internet. Pots seguir jugar offline des del menú connectedToLichess=Ara estàs connectat a lichess.org signedOut=Has tancat la sessió -loginSuccessful=Ja estàs registrat +loginSuccessful=Ara ja estàs registrat playOnTheBoardOffline=Juga offline, sobre el tauler playOfflineComputer=Juga offline amb l'ordinador opponent=Rival @@ -453,7 +453,7 @@ noSimulFound=Simultània no trobada noSimulExplanation=Aquesta simultània no existeix. returnToSimulHomepage=Torna a la pàgina principal de simultànies aboutSimul=Les simultànies suposen enfrontar-se a més d'un jugador alhora. -aboutSimulImage=Contra 50 oponents, Fischer va obtenir 47 victòries, 2 taules i 1 derrota. +aboutSimulImage=Contra 50 contrincants, Fischer va obtenir 47 victòries, 2 taules i 1 derrota. aboutSimulRealLife=La idea està presa d'esdeveniments presencials. En aquests esdeveniments, l'amfitrió s'ha d'anar movent de taula en taula per fer cada moviment. aboutSimulRules=Quan comencen les simultànies, cada jugador comença la partida amb l'amfitrió, que té les blanques. L'exibició acaba quan s'han completat totes les partides. aboutSimulSettings=Les simultànies són sempre amistoses. Les opcions de revenja, desfer la jugada i donar més temps estan desactivades. @@ -489,7 +489,7 @@ youDoNotHaveAnEstablishedPerfTypeRating=No tens establerta una puntuació de %s. checkYourEmail=Comprova el teu email weHaveSentYouAnEmailClickTheLink=T'hem enviat un email. Clica l'enllaç de l'email per a activar el teu compte ifYouDoNotSeeTheEmailCheckOtherPlaces=Si no veus l'email, comprova altres llocs on pugui ser, com el correu brossa, spam, o altres carpetes -areYouSureYouEvenRegisteredYourEmailOnLichess=Estàs segur, fins i tot, d'haver registrat el teu email a lichess? +areYouSureYouEvenRegisteredYourEmailOnLichess=Estàs segur d'haver registrat en algun moment el teu email a lichess? itWasNotRequiredForYourRegistration=No es requeria per al seu registre weHaveSentYouAnEmailTo=Hem enviat un correu electrònic a %s. Cliqueu a l'enllaç per renovar la vostra contrasenya. byRegisteringYouAgreeToBeBoundByOur=En registrar-vos,doneu la vostra conformitat a estar legalment obligats per el nostre %s. @@ -519,6 +519,6 @@ correspondenceChess=Escacs per correspondència onlineAndOfflinePlay=Jugar en línia i desconectat correspondenceAndUnlimited=Per correspondència i sense límit de temps viewTheSolution=Veure la solució -followAndChallengeFriends=Seguir i desafiar amics +followAndChallengeFriends=Seguiu i desafieu amics availableInNbLanguages=Disponible en %s idiomes gameAnalysis=Anàlisi del joc diff --git a/modules/i18n/messages/messages.is b/modules/i18n/messages/messages.is index 2285fecdc6..a7eb0b3efe 100644 --- a/modules/i18n/messages/messages.is +++ b/modules/i18n/messages/messages.is @@ -512,9 +512,12 @@ asFreeAsLichess=Eins frítt og lichess builtForTheLoveOfChessNotMoney=Byggt af ást á skák, ekki skildingi everybodyGetsAllFeaturesForFree=Allir fá alla möguleika ókeypis zeroAdvertisement=Engar auglýsingar +fullFeatured=Fullbúið phoneAndTablet=Símar og spjaldtölvur +bulletBlitzClassical=Bullet, Blitz, klassík correspondenceChess=Bréfskák onlineAndOfflinePlay=Tengd og ótengd spilun +correspondenceAndUnlimited=Bréfaskipti og ótakmarkað viewTheSolution=Sjá lausn followAndChallengeFriends=Fylgdu og skoraðu á vini availableInNbLanguages=Til á %s tungumálum diff --git a/modules/i18n/messages/messages.tr b/modules/i18n/messages/messages.tr index 2bf99e9160..522d6829d8 100644 --- a/modules/i18n/messages/messages.tr +++ b/modules/i18n/messages/messages.tr @@ -389,8 +389,8 @@ timeline=Zaman çubuğu seeAllTournaments=Tüm turnuvalar gör starting=Başlangıç: allInformationIsPublicAndOptional=Tüm bilgiler herkese açık ve seçime bağlıdır. -yourCityRegionOrDepartment=İl, İlçe yada Bölge -biographyDescription=Kendinizden bahsedin, satrançta ne buluyorsunuz, favori açılışlarınız, oyunlarınız, hayranı olduğunuz satranç ustaları ... +yourCityRegionOrDepartment=İl, İlçe ya da Bölge +biographyDescription=Kendinizden bahsedin, satrançta ne buluyorsunuz, favori açılışlarınız, oyunlarınız, hayranı olduğunuz satranç ustaları... maximumNbCharacters=En fazla: %s karakter. blocks=%s kişiyi engelledi listBlockedPlayers=Engellenen oyuncular @@ -427,7 +427,7 @@ inCorrespondenceGames=Rövanş oyunlarında ifRatingIsPlusMinusX=Eğer derece +- %s onlyFriends=Sadece arkadaşlar menu=Menü -castling=Rok Yapma +castling=Rok Atma whiteCastlingKingside=Beyaz O-O whiteCastlingQueenside=Beyaz O-O-O blackCastlingKingside=Siyah O-O @@ -453,7 +453,7 @@ noSimulFound=Eş zamanlı bulunamadı noSimulExplanation=Eş zamanlı gösteri yok. returnToSimulHomepage=Eş zamanlı sayfasına geri dön aboutSimul=Eş zamanlılar tek oyuncunun aynı anda birkaç oyuncuyla mücadelesini içerir. -aboutSimulImage=Fischer 50 rakipten, 47 oyun kazandı, 2 pat ve 1'ini kaybetti. +aboutSimulImage=Fischer 50 rakibe karşı, 47 oyun kazandı, 2 beraberlik ve 1 yenilgi aldı. aboutSimulRealLife=Bu kavram gerçek etkinliklerden alınmıştır. Gerçek hayatta, eş zamanlının ev sahibi masadan masaya dolaşarak birer hamle yapar. aboutSimulRules=Eş zamanlı başlayınca, her oyuncu beyaz taşlarla oynayan ev sahibiyle oyuna başlar. Eş zamanlı, bütün oyunlar bittiğinde biter. aboutSimulSettings=Eş zamanlılar daima puansızdır. Yeniden oynama, hamleyi geri alma ve zaman ekleme devredışıdır. @@ -507,7 +507,7 @@ letOtherPlayersMessageYou=Diğer oyuncuların size mesaj yollamasına izin verin shareYourInsightsData=Anlayış verilerinizi paylaşın youHaveAlreadyRegisteredTheEmail=Zaten kaydedilmiş e-posta adresiniz var: %s kidMode=Çocuk modu -playChessEverywhere=Heryerde satranç oyna +playChessEverywhere=Her yerde satranç oyna asFreeAsLichess=lichess kadar özgür builtForTheLoveOfChessNotMoney=Para için değil satranç sevgisi için geliştirilmiştir everybodyGetsAllFeaturesForFree=Bütün özellikler ücretsizdir diff --git a/modules/i18n/messages/messages.uz b/modules/i18n/messages/messages.uz index f826d2eec6..4c62e91a6a 100644 --- a/modules/i18n/messages/messages.uz +++ b/modules/i18n/messages/messages.uz @@ -2,6 +2,7 @@ playWithAFriend=Do'stingiz bilan oynash playWithTheMachine=Kopmyuter bilan o'ynash toInviteSomeoneToPlayGiveThisUrl=Biror kim bilan o'ynash uchun, ushbu URL ni bering gameOver=O'yin tugadi +waitingForOpponent=Raqibni kuting waiting=Kutilmoqda yourTurn=Sizning yurish aiNameLevelAiLevel=%s %s bosqichda @@ -9,9 +10,128 @@ level=Bosqich toggleTheChat=Chatni yoqish/o'chirish toggleSound=Ovozni yoqish/ochirish chat=Chat +resign=Taslim bo'lish checkmate=Mot +stalemate=Pot white=Oqlar black=Qoralar +randomColor=Tasodifiy taraf createAGame=Yangi o'yin yaratish whiteIsVictorious=Oqlar yutdi blackIsVictorious=Qoralar yutdi +kingInTheCenter=Shoh markazda +threeChecks=Uch martalik shoh +raceFinished=Poyga tugadi +newOpponent=Yangi raqib +yourOpponentWantsToPlayANewGameWithYou=Sizning raqibingiz siz bilan yangi o'yin o'ynashni istaydi +joinTheGame=O'yinga qo'shilish +whitePlays=Yurish oqlardan +blackPlays=Yurish qoralardan +talkInChat=Iltimos, suhbatda yaxshi muomalada bo'ling! +whiteResigned=Oqlar taslim bo'ldi +blackResigned=Qoralar taslim bo'ldi +whiteLeftTheGame=Oqlar o'yinni tark etdi +blackLeftTheGame=Qoralar o'yinni tark etdi +viewTheComputerAnalysis=Compyuter analizini ko'rsatish +requestAComputerAnalysis=Compyuter analizini so'rash +computerAnalysis=Compyuter analizi +mistakes=Xatolar +inaccuracies=Noaniqliklar +threefoldRepetition=Uch maratalik qaytarilish +claimADraw=Durang da'vo qilish +offerDraw=Durang taklif qilmoq +draw=Durang +gamesBeingPlayedRightNow=Hozirgi o'yinlar +viewInFullSize=To'liq kattalikda ko'rish +logOut=Tizimdan chiqish +signIn=Tizimga kirish +signUp=Ro'yxatdan o'tish +games=O'yinlar +forum=Forum +players=O'yinchilar +minutesPerSide=Taraflarning har biriga ajratilgan vaqt +variant=Variant +variants=Variantlar +timeControl=Vaqt nazorati +oneDay=Bir kun +time=Vaqt +rating=Reyting +username=Foydalanuvchi nomi +usernameOrEmail=Foydalanuvchi nomi yoki elektron pochta +password=Parol +changePassword=Parolni o'zgartirish +email=Elektron pochta +passwordReset=Parolni qayta tiklash +forgotPassword=Parolni unutdingizmi? +gamesPlayed=O'ynalgan o'yinlar soni +declineInvitation=Taklifni rad etish +cancel=Bekor qilish +drawOfferSent=Durang taklifi yuborildi +drawOfferDeclined=Durang taklifi rad etildi +drawOfferAccepted=Duran taklifi qabul qilindi +drawOfferCanceled=Durang taklifi rad etildi +whiteOffersDraw=Oqlar durang taklif qildi +blackOffersDraw=Qoralar durang taklif qildi +whiteDeclinesDraw=Oqlar durang taklifini rad etdi +blackDeclinesDraw=Qoralar durang taklifini rad etdi +yourOpponentOffersADraw=Sizning raqibingiz durang taklif qildi +accept=Qabul qilish +decline=Rad etish +finished=Tamomlandi +abortGame=O'yinni bekor qilish +gameAborted=O'yin bekor qilindi +standard=Standard +unlimited=Cheksiz +rematch=Qayta o'ynash +rematchOfferSent=Qayta o'ynash taklifi yuborildi +rematchOfferAccepted=Qayta o'ynash taklifi qabul qilindi +rematchOfferCanceled=Qayta o'ynash taklifi bekor qilindi +rematchOfferDeclined=Qayta o'ynash taklifi rad etildi +cancelRematchOffer=Qayta o'ynash taklifi bekor qilish +play=O'ynamoq +chatRoom=Muloqot honasi +subject=Mavzu +send=Yubormoq +spectators=Tomoshabinlar: +opening=Ochilish +takeback=Qaytib olish +proposeATakeback=Qaytib olishni taklif qilish +takebackPropositionSent=Qaytib olishni taklif yuborildi +takebackPropositionDeclined=Qaytib olishni taklifi rad etildi +takebackPropositionAccepted=Qaytib olishni taklifi qabul qilindi +takebackPropositionCanceled=Qaytib olishni taklifi bekor qilindi +yourOpponentProposesATakeback=Sizning raqibingiz qaytib olish taklifini yubordi +search=Izlash +tournaments=Musobaqalar +backToGame=O'yinga qaytmoq +teams=Jamoalar +allTeams=Hamma jamoalar +newTeam=Yangi jamoa +myTeams=Mening jamoalarim +noTeamFound=Jamoa topilmadi +joinTeam=Jamoaga qo'shilish +quitTeam=Jamoani tark etish +anyoneCanJoin=Hamma uchun bepul +teamBestPlayers=Eng yaxshi o'yinchilar +location=Koylashgan joyi +settings=Sozlamalar +continueFromHere=Shu yerdan davom ettirish +retry=Qayta urinish +player=O'yinchi +list=Ro'yxat +join=Qo'shilish +withdraw=Qaytib olish +wins=G'alabalar soni +losses=Mag'lubiyatlar soni +no=Ha +yes=Yo'q +never=Hech qachon +fast=Tez +normal=O'rta +slow=Sekin +difficultyEasy=Oson +difficultyNormal=O'rta +difficultyHard=Qiyin +side=Taraf +clock=Soat +learn=O'rganmoq diff --git a/modules/i18n/messages/messages.zh b/modules/i18n/messages/messages.zh index 9a3c89575f..4658f94c70 100644 --- a/modules/i18n/messages/messages.zh +++ b/modules/i18n/messages/messages.zh @@ -6,7 +6,7 @@ waitingForOpponent=等待对手 waiting=稍等 yourTurn=你的回合 aiNameLevelAiLevel=%s级别%s -level=级别 +level=水平 toggleTheChat=聊天开关 toggleSound=声音开关 chat=聊天 diff --git a/modules/i18n/messages/messages.zu b/modules/i18n/messages/messages.zu index 4987ae2954..1697385edd 100644 --- a/modules/i18n/messages/messages.zu +++ b/modules/i18n/messages/messages.zu @@ -4,3 +4,4 @@ toInviteSomeoneToPlayGiveThisUrl=Ukuze umeme othile ukudlala, unike lolu kheli gameOver=Game Over waitingForOpponent=Elinde isitha waiting=Ukulinda +yourTurn=Ithuba lakho diff --git a/modules/lobby/src/main/Hook.scala b/modules/lobby/src/main/Hook.scala index c67fb44537..850fc14f5a 100644 --- a/modules/lobby/src/main/Hook.scala +++ b/modules/lobby/src/main/Hook.scala @@ -90,7 +90,7 @@ object Hook { sid: Option[String], ratingRange: RatingRange, blocking: Set[String]): Hook = new Hook( - id = Random nextStringUppercase idSize, + id = Random nextString idSize, uid = uid, variant = variant.id, clock = clock, diff --git a/modules/lobby/src/main/Seek.scala b/modules/lobby/src/main/Seek.scala index 8fbbbb4e45..1515ee6181 100644 --- a/modules/lobby/src/main/Seek.scala +++ b/modules/lobby/src/main/Seek.scala @@ -76,7 +76,7 @@ object Seek { user: User, ratingRange: RatingRange, blocking: Set[String]): Seek = new Seek( - _id = Random nextStringUppercase idSize, + _id = Random nextString idSize, variant = variant.id, daysPerTurn = daysPerTurn, mode = mode.id, @@ -86,7 +86,7 @@ object Seek { createdAt = DateTime.now) def renew(seek: Seek) = new Seek( - _id = Random nextStringUppercase idSize, + _id = Random nextString idSize, variant = seek.variant, daysPerTurn = seek.daysPerTurn, mode = seek.mode, diff --git a/modules/message/src/main/Post.scala b/modules/message/src/main/Post.scala index 0fee0b65b4..6dffd7ecce 100644 --- a/modules/message/src/main/Post.scala +++ b/modules/message/src/main/Post.scala @@ -24,7 +24,7 @@ object Post { def make( text: String, isByCreator: Boolean): Post = Post( - id = Random nextStringUppercase idSize, + id = Random nextString idSize, text = text, isByCreator = isByCreator, isRead = false, diff --git a/modules/message/src/main/Thread.scala b/modules/message/src/main/Thread.scala index 8d25761744..2cbacc40a6 100644 --- a/modules/message/src/main/Thread.scala +++ b/modules/message/src/main/Thread.scala @@ -78,7 +78,7 @@ object Thread { text: String, creatorId: String, invitedId: String): Thread = Thread( - _id = Random nextStringUppercase idSize, + _id = Random nextString idSize, name = name, createdAt = DateTime.now, updatedAt = DateTime.now, diff --git a/modules/mod/src/main/AssessApi.scala b/modules/mod/src/main/AssessApi.scala index d92245aa58..89e871d7af 100644 --- a/modules/mod/src/main/AssessApi.scala +++ b/modules/mod/src/main/AssessApi.scala @@ -10,8 +10,8 @@ import lila.game.{ Game, Player, GameRepo, Source, Pov } import lila.user.{ User, UserRepo } import org.joda.time.DateTime -import reactivemongo.bson._ import reactivemongo.api.ReadPreference +import reactivemongo.bson._ import scala.concurrent._ import scala.util.Random @@ -107,20 +107,27 @@ final class AssessApi( }) } - def assessUser(userId: String): Funit = + def assessUser(userId: String): Funit = { getPlayerAggregateAssessment(userId) flatMap { case Some(playerAggregateAssessment) => playerAggregateAssessment.action match { case AccountAction.Engine | AccountAction.EngineAndBan => - modApi.autoAdjust(userId) - case AccountAction.Report(reason) => + UserRepo.getTitle(userId).flatMap { + case None => modApi.autoAdjust(userId) + case Some(title) => fuccess { + val reason = s"Would mark as engine, but has a $title title" + reporter ! lila.hub.actorApi.report.Cheater(userId, playerAggregateAssessment.reportText(reason, 3)) + } + } + case AccountAction.Report(reason) => fuccess { reporter ! lila.hub.actorApi.report.Cheater(userId, playerAggregateAssessment.reportText(reason, 3)) - funit + } case AccountAction.Nothing => // reporter ! lila.hub.actorApi.report.Clean(userId) funit } case none => funit } + } private val assessableSources: Set[Source] = Set(Source.Lobby, Source.Tournament) diff --git a/modules/notify/src/main/Notification.scala b/modules/notify/src/main/Notification.scala index aba0350ab8..9055fe0477 100644 --- a/modules/notify/src/main/Notification.scala +++ b/modules/notify/src/main/Notification.scala @@ -28,7 +28,7 @@ object Notification { def make(notifies: Notification.Notifies, content: NotificationContent): Notification = { val idSize = 8 - val id = Random nextStringUppercase idSize + val id = Random nextString idSize new Notification(id, notifies, content, NotificationRead(false), DateTime.now) } } diff --git a/modules/plan/src/main/Charge.scala b/modules/plan/src/main/Charge.scala index 4a573a39f1..4727fdef49 100644 --- a/modules/plan/src/main/Charge.scala +++ b/modules/plan/src/main/Charge.scala @@ -29,7 +29,7 @@ object Charge { stripe: Option[Charge.Stripe] = none, payPal: Option[Charge.PayPal] = none, cents: Cents) = Charge( - _id = Random nextStringUppercase 8, + _id = Random nextString 8, userId = userId, stripe = stripe, payPal = payPal, diff --git a/modules/qa/src/main/model.scala b/modules/qa/src/main/model.scala index 1eb639395f..5691165681 100644 --- a/modules/qa/src/main/model.scala +++ b/modules/qa/src/main/model.scala @@ -82,5 +82,5 @@ case class Comment( object Comment { - def makeId = ornicar.scalalib.Random nextStringUppercase 8 + def makeId = ornicar.scalalib.Random nextString 8 } diff --git a/modules/quote/src/main/Quote.scala b/modules/quote/src/main/Quote.scala index 756191f8ff..2027c64ca7 100644 --- a/modules/quote/src/main/Quote.scala +++ b/modules/quote/src/main/Quote.scala @@ -567,6 +567,7 @@ object Quote { , new Quote("[...], even extremely intoxicated my chess strength and knowledge is still in my bones.", "Magnus Carlsen") , new Quote("I don't play unorthodox openings. I prefer to give mainstream openings my own spin.", "Magnus Carlsen") , new Quote("Playing long games online just takes too much time. It's fun to play blitz once in a while, where you can rely more on your intuition, your instincts rather than pure calculation and analysis.", "Magnus Carlsen") + , new Quote("Fortune favors the lucky!", "Robert Houdart (Houdini author)") ) implicit def quoteWriter: OWrites[Quote] = OWrites { q => diff --git a/modules/report/src/main/Report.scala b/modules/report/src/main/Report.scala index d1d2937319..882b04c7b2 100644 --- a/modules/report/src/main/Report.scala +++ b/modules/report/src/main/Report.scala @@ -61,7 +61,7 @@ object Report { reason: Reason, text: String, createdBy: User): Report = new Report( - _id = Random nextStringUppercase 8, + _id = Random nextString 8, user = user.id, reason = reason.name, text = text, diff --git a/modules/round/src/main/CorresAlarm.scala b/modules/round/src/main/CorresAlarm.scala index 962dbb63c6..7f173b5936 100644 --- a/modules/round/src/main/CorresAlarm.scala +++ b/modules/round/src/main/CorresAlarm.scala @@ -62,17 +62,18 @@ private final class CorresAlarm(coll: Coll) extends Actor { case move: lila.hub.actorApi.round.MoveEvent if move.alarmable => GameRepo game move.gameId flatMap { _ ?? { game => - game.playableCorrespondenceClock ?? { clock => - val remainingTime = clock remainingTime game.turnColor - // val ringsAt = DateTime.now.plusSeconds(remainingTime.toInt * 9 / 10) - val ringsAt = DateTime.now.plusSeconds(5) - coll.update( - $id(game.id), - Alarm( - _id = game.id, - ringsAt = ringsAt, - expiresAt = DateTime.now.plusSeconds(remainingTime.toInt * 2)), - upsert = true).void + game.bothPlayersHaveMoved ?? { + game.playableCorrespondenceClock ?? { clock => + val remainingTime = clock remainingTime game.turnColor + val ringsAt = DateTime.now.plusSeconds(remainingTime.toInt * 9 / 10) + coll.update( + $id(game.id), + Alarm( + _id = game.id, + ringsAt = ringsAt, + expiresAt = DateTime.now.plusSeconds(remainingTime.toInt * 2)), + upsert = true).void + } } } } diff --git a/modules/round/src/main/Env.scala b/modules/round/src/main/Env.scala index a08fd24a02..ae0c4d2ced 100644 --- a/modules/round/src/main/Env.scala +++ b/modules/round/src/main/Env.scala @@ -196,9 +196,9 @@ final class Env( Props(classOf[Titivate], roundMap, hub.actor.bookmark), name = "titivate") - system.lilaBus.subscribe(system.actorOf( - Props(classOf[CorresAlarm], db(CollectionAlarm)), - name = "corres-alarm"), 'moveEvent, 'finishGame) + // system.lilaBus.subscribe(system.actorOf( + // Props(classOf[CorresAlarm], db(CollectionAlarm)), + // name = "corres-alarm"), 'moveEvent, 'finishGame) lazy val takebacker = new Takebacker( messenger = messenger, diff --git a/modules/security/src/main/Api.scala b/modules/security/src/main/Api.scala index 2b2a5db3cf..e6c6346b13 100644 --- a/modules/security/src/main/Api.scala +++ b/modules/security/src/main/Api.scala @@ -51,7 +51,7 @@ final class Api( UserRepo mustConfirmEmail userId flatMap { case true => fufail(Api MustConfirmEmail userId) case false => - val sessionId = Random nextStringUppercase 12 + val sessionId = Random secureString 12 Store.save(sessionId, userId, req, apiVersion) inject sessionId } diff --git a/modules/security/src/main/DisposableEmailDomain.scala b/modules/security/src/main/DisposableEmailDomain.scala index 9980aa166a..2b55121345 100644 --- a/modules/security/src/main/DisposableEmailDomain.scala +++ b/modules/security/src/main/DisposableEmailDomain.scala @@ -33,7 +33,7 @@ final class DisposableEmailDomain( } private[security] def textToDomains(text: String): List[String] = - text.lines.map(_.trim).filter(_.nonEmpty).toList + text.lines.map(_.trim.toLowerCase).filter(_.nonEmpty).toList private var failed = false @@ -54,5 +54,58 @@ final class DisposableEmailDomain( (s: String) => matcher(s).matches } - def apply(domain: String) = matchers exists { _(domain) } + def isMainstream(domain: String) = + DisposableEmailDomain.mainstreamDomains contains domain.toLowerCase + + def apply(domain: String) = + if (isMainstream(domain)) false + else matchers exists { _(domain.toLowerCase) } +} + +object DisposableEmailDomain { + + val mainstreamDomains = Set( + /* Default domains included */ + "aol.com", "att.net", "comcast.net", "facebook.com", "gmail.com", "gmx.com", "googlemail.com", + "google.com", "hotmail.com", "hotmail.co.uk", "mac.com", "me.com", "mail.com", "msn.com", + "live.com", "sbcglobal.net", "verizon.net", "yahoo.com", "yahoo.co.uk", + + /* Other global domains */ + "email.com", "games.com" /* AOL */ , "gmx.net", "hush.com", "hushmail.com", "icloud.com", "inbox.com", + "lavabit.com", "love.com" /* AOL */ , "outlook.com", "pobox.com", "rocketmail.com" /* Yahoo */ , + "safe-mail.net", "wow.com" /* AOL */ , "ygm.com" /* AOL */ , "ymail.com" /* Yahoo */ , "zoho.com", "fastmail.fm", + "yandex.com", + + /* United States ISP domains */ + "bellsouth.net", "charter.net", "comcast.net", "cox.net", "earthlink.net", "juno.com", + + /* British ISP domains */ + "btinternet.com", "virginmedia.com", "blueyonder.co.uk", "freeserve.co.uk", "live.co.uk", + "ntlworld.com", "o2.co.uk", "orange.net", "sky.com", "talktalk.co.uk", "tiscali.co.uk", + "virgin.net", "wanadoo.co.uk", "bt.com", + + /* Domains used in Asia */ + "sina.com", "qq.com", "naver.com", "hanmail.net", "daum.net", "nate.com", "yahoo.co.jp", "yahoo.co.kr", "yahoo.co.id", "yahoo.co.in", "yahoo.com.sg", "yahoo.com.ph", + + /* French ISP domains */ + "hotmail.fr", "live.fr", "laposte.net", "yahoo.fr", "wanadoo.fr", "orange.fr", "gmx.fr", "sfr.fr", "neuf.fr", "free.fr", + + /* German ISP domains */ + "gmx.de", "hotmail.de", "live.de", "online.de", "t-online.de" /* T-Mobile */ , "web.de", "yahoo.de", + + /* Russian ISP domains */ + "mail.ru", "rambler.ru", "yandex.ru", "ya.ru", "list.ru", + + /* Belgian ISP domains */ + "hotmail.be", "live.be", "skynet.be", "voo.be", "tvcablenet.be", "telenet.be", + + /* Argentinian ISP domains */ + "hotmail.com.ar", "live.com.ar", "yahoo.com.ar", "fibertel.com.ar", "speedy.com.ar", "arnet.com.ar", + + /* Domains used in Mexico */ + "yahoo.com.mx", "live.com.mx", "hotmail.es", "hotmail.com.mx", "prodigy.net.mx", + + /* Domains used in Brazil */ + "yahoo.com.br", "hotmail.com.br", "outlook.com.br", "uol.com.br", "bol.com.br", "terra.com.br", "ig.com.br", "itelefonica.com.br", "r7.com", "zipmail.com.br", "globo.com", "globomail.com", "oi.com.br" + ) } diff --git a/modules/security/src/main/Firewall.scala b/modules/security/src/main/Firewall.scala index 04fd22dece..5e6c8ab365 100644 --- a/modules/security/src/main/Firewall.scala +++ b/modules/security/src/main/Firewall.scala @@ -44,7 +44,7 @@ final class Firewall( private def infectCookie(name: String)(implicit req: RequestHeader) = Action { logger.info("Infect cookie " + formatReq(req)) - val cookie = LilaCookie.cookie(name, Random nextStringUppercase 32) + val cookie = LilaCookie.cookie(name, Random secureString 32) Redirect("/") withCookies cookie } diff --git a/modules/security/src/test/DisposableEmailDomainTest.scala b/modules/security/src/test/DisposableEmailDomainTest.scala index 47d4de7ff1..6db3b37a79 100644 --- a/modules/security/src/test/DisposableEmailDomainTest.scala +++ b/modules/security/src/test/DisposableEmailDomainTest.scala @@ -14,6 +14,8 @@ class DisposableEmailDomainTest extends Specification { d("hotmail.com") must beFalse d("live.com") must beFalse d("docmail.cz") must beTrue + d("DoCmAiL.cz") must beTrue + d("chacuo.net") must beTrue } "suffix" in { d("foo.some.randomgoodemail.org") must beFalse diff --git a/modules/security/src/test/Fixtures.scala b/modules/security/src/test/Fixtures.scala index 8b4df09ae4..71c9a3b7b7 100644 --- a/modules/security/src/test/Fixtures.scala +++ b/modules/security/src/test/Fixtures.scala @@ -4,7 +4,7 @@ case object Fixtures { def text = """ leeching.net -chacuo.net +ChaCuo.net 027168.com 0-mail.com mail1a.de diff --git a/modules/setup/src/main/Config.scala b/modules/setup/src/main/Config.scala index 326a61d86c..516983a1d8 100644 --- a/modules/setup/src/main/Config.scala +++ b/modules/setup/src/main/Config.scala @@ -96,12 +96,20 @@ trait BaseConfig { chess.variant.Crazyhouse.id :+ chess.variant.KingOfTheHill.id :+ chess.variant.ThreeCheck.id :+ + chess.variant.Antichess.id :+ chess.variant.Atomic.id :+ chess.variant.Horde.id :+ chess.variant.RacingKings.id :+ chess.variant.FromPosition.id val variantsWithVariants = - variants :+ chess.variant.Crazyhouse.id :+ chess.variant.KingOfTheHill.id :+ chess.variant.ThreeCheck.id :+ chess.variant.Antichess.id :+ chess.variant.Atomic.id :+ chess.variant.Horde.id :+ chess.variant.RacingKings.id + variants :+ + chess.variant.Crazyhouse.id :+ + chess.variant.KingOfTheHill.id :+ + chess.variant.ThreeCheck.id :+ + chess.variant.Antichess.id :+ + chess.variant.Atomic.id :+ + chess.variant.Horde.id :+ + chess.variant.RacingKings.id val variantsWithFenAndVariants = variantsWithVariants :+ chess.variant.FromPosition.id @@ -109,7 +117,7 @@ trait BaseConfig { private val timeMin = 0 private val timeMax = 180 - private val acceptableFractions = Set(1/2d, 3/4d, 3/2d) + private val acceptableFractions = Set(1 / 2d, 3 / 4d, 3 / 2d) def validateTime(t: Double) = t >= timeMin && t <= timeMax && (t.isWhole || acceptableFractions(t)) diff --git a/modules/simul/src/main/Simul.scala b/modules/simul/src/main/Simul.scala index 91f6212398..6d61f0edf1 100644 --- a/modules/simul/src/main/Simul.scala +++ b/modules/simul/src/main/Simul.scala @@ -136,7 +136,7 @@ object Simul { clock: SimulClock, variants: List[Variant], color: String): Simul = Simul( - _id = Random nextStringUppercase 8, + _id = Random nextString 8, name = makeName(host), status = SimulStatus.Created, clock = clock, diff --git a/modules/site/src/main/ApiSocketHandler.scala b/modules/site/src/main/ApiSocketHandler.scala index 765c80c34c..49a59980b9 100644 --- a/modules/site/src/main/ApiSocketHandler.scala +++ b/modules/site/src/main/ApiSocketHandler.scala @@ -16,7 +16,7 @@ private[site] final class ApiSocketHandler( def apply: Fu[JsSocketHandler] = { - val uid = Random nextStringUppercase 8 + val uid = Random secureString 8 def controller(member: SocketMember): Handler.Controller = { case ("startWatching", o) => o str "d" foreach { ids => diff --git a/modules/study/src/main/SocketHandler.scala b/modules/study/src/main/SocketHandler.scala index 9ec2f665c4..d8d590a5cb 100644 --- a/modules/study/src/main/SocketHandler.scala +++ b/modules/study/src/main/SocketHandler.scala @@ -29,6 +29,12 @@ private[study] final class SocketHandler( import JsonView.shapeReader import lila.socket.tree.Node.openingWriter + private val InviteLimitPerUser = new lila.memo.RateLimit( + credits = 50, + duration = 24 hour, + name = "study invites per user", + key = "study_invite.user") + private def controller( socket: ActorRef, studyId: Study.ID, @@ -139,7 +145,9 @@ private[study] final class SocketHandler( case ("invite", o) if owner => for { byUserId <- member.userId username <- o str "d" - } api.invite(byUserId, studyId, username, socket) + } InviteLimitPerUser(byUserId, cost = 1) { + api.invite(byUserId, studyId, username, socket) + } case ("kick", o) if owner => o str "d" foreach { api.kick(studyId, _) } diff --git a/modules/study/src/main/Study.scala b/modules/study/src/main/Study.scala index 030b4382cb..774b40b42f 100644 --- a/modules/study/src/main/Study.scala +++ b/modules/study/src/main/Study.scala @@ -59,6 +59,8 @@ case class Study( createdAt = DateTime.now, updatedAt = DateTime.now) } + + def nbMembers = members.members.size } object Study { diff --git a/modules/study/src/main/StudyApi.scala b/modules/study/src/main/StudyApi.scala index d7e5b9e335..cd79fd7553 100644 --- a/modules/study/src/main/StudyApi.scala +++ b/modules/study/src/main/StudyApi.scala @@ -180,11 +180,11 @@ final class StudyApi( } def invite(byUserId: User.ID, studyId: Study.ID, username: String, socket: ActorRef) = sequenceStudy(studyId) { study => - (study isOwner byUserId) ?? { + (study.isOwner(byUserId) && study.nbMembers < 30) ?? { UserRepo.named(username).flatMap { _.filterNot(study.members.contains) ?? { user => studyRepo.addMember(study, StudyMember make user) >>- - notifier(study, user, socket) + notifier.invite(study, user, socket) } } >>- reloadMembers(study) >>- indexStudy(study) } diff --git a/modules/study/src/main/StudyNotifier.scala b/modules/study/src/main/StudyNotifier.scala index 8b585a99b4..f71f4e24e3 100644 --- a/modules/study/src/main/StudyNotifier.scala +++ b/modules/study/src/main/StudyNotifier.scala @@ -7,6 +7,7 @@ import lila.hub.actorApi.HasUserId import lila.notify.InvitedToStudy.InvitedBy import lila.notify.{ InvitedToStudy, NotifyApi, Notification } import lila.relation.RelationApi +import lila.user.{ User, UserRepo } import makeTimeout.short import org.joda.time.DateTime @@ -15,10 +16,9 @@ private final class StudyNotifier( notifyApi: NotifyApi, relationApi: RelationApi) { - def apply(study: Study, invited: lila.user.User, socket: ActorRef) = - relationApi.fetchBlocks(invited.id, study.ownerId).flatMap { - case true => funit - case false => + def invite(study: Study, invited: User, socket: ActorRef) = + canNotify(study.ownerId, invited) flatMap { + _ ?? { socket ? HasUserId(invited.id) mapTo manifest[Boolean] map { isPresent => study.owner.ifFalse(isPresent) foreach { owner => val notificationContent = InvitedToStudy(InvitedToStudy.InvitedBy(owner.id), InvitedToStudy.StudyName(study.name), InvitedToStudy.StudyId(study.id)) @@ -26,6 +26,13 @@ private final class StudyNotifier( notifyApi.addNotification(notification) } } + } + } + + private def canNotify(fromId: User.ID, to: User): Fu[Boolean] = + UserRepo.isTroll(fromId) flatMap { + case true => relationApi.fetchFollows(to.id, fromId) + case false => !relationApi.fetchBlocks(to.id, fromId) } private def studyUrl(study: Study) = s"$netBaseUrl/study/${study.id}" diff --git a/modules/team/src/main/Team.scala b/modules/team/src/main/Team.scala index 4edfbaf5c1..e3cd4489db 100644 --- a/modules/team/src/main/Team.scala +++ b/modules/team/src/main/Team.scala @@ -45,6 +45,6 @@ object Team { def nameToId(name: String) = (lila.common.String slugify name) |> { slug => // if most chars are not latin, go for random slug - (slug.size > (name.size / 2)).fold(slug, Random nextStringUppercase 8) + (slug.size > (name.size / 2)).fold(slug, Random nextString 8) } } diff --git a/modules/tournament/src/main/ApiActor.scala b/modules/tournament/src/main/ApiActor.scala index 1448f2bd28..8889565089 100644 --- a/modules/tournament/src/main/ApiActor.scala +++ b/modules/tournament/src/main/ApiActor.scala @@ -1,15 +1,11 @@ package lila.tournament -import scala.concurrent.duration._ import akka.actor._ import lila.game.actorApi.FinishGame private[tournament] final class ApiActor(api: TournamentApi) extends Actor { - override def preStart { - } - def receive = { case FinishGame(game, _, _) => api finishGame game diff --git a/modules/tournament/src/main/Schedule.scala b/modules/tournament/src/main/Schedule.scala index 6d2092ed9b..583a495917 100644 --- a/modules/tournament/src/main/Schedule.scala +++ b/modules/tournament/src/main/Schedule.scala @@ -202,9 +202,9 @@ object Schedule { import Freq._, Speed._ val nbRatedGame = (s.freq, s.speed) match { - case (Daily, HyperBullet | Bullet) => 20 - case (Daily, SuperBlitz | Blitz) => 15 - case (Daily, Classical) => 10 + case (Daily | Eastern, HyperBullet | Bullet) => 20 + case (Daily | Eastern, SuperBlitz | Blitz) => 15 + case (Daily | Eastern, Classical) => 10 case (Weekly | Monthly, HyperBullet | Bullet) => 30 case (Weekly | Monthly, SuperBlitz | Blitz) => 20 diff --git a/modules/tournament/src/main/Tournament.scala b/modules/tournament/src/main/Tournament.scala index 365afbe333..dd4c3a5713 100644 --- a/modules/tournament/src/main/Tournament.scala +++ b/modules/tournament/src/main/Tournament.scala @@ -127,7 +127,7 @@ object Tournament { `private`: Boolean, password: Option[String], waitMinutes: Int) = Tournament( - id = Random nextStringUppercase 8, + id = Random nextString 8, name = if (position.initial) GreatPlayer.randomName else position.shortName, status = Status.Created, system = system, @@ -146,7 +146,7 @@ object Tournament { startsAt = DateTime.now plusMinutes waitMinutes) def schedule(sched: Schedule, minutes: Int) = Tournament( - id = Random nextStringUppercase 8, + id = Random nextString 8, name = sched.name, status = Status.Created, system = System.default, diff --git a/modules/user/src/main/NoteApi.scala b/modules/user/src/main/NoteApi.scala index 3c8595bb27..705e7a666b 100644 --- a/modules/user/src/main/NoteApi.scala +++ b/modules/user/src/main/NoteApi.scala @@ -49,7 +49,7 @@ final class NoteApi( def write(to: User, text: String, from: User, modOnly: Boolean) = { val note = Note( - _id = ornicar.scalalib.Random nextStringUppercase 8, + _id = ornicar.scalalib.Random nextString 8, from = from.id, to = to.id, text = text, diff --git a/modules/user/src/main/Trophy.scala b/modules/user/src/main/Trophy.scala index bab01b9040..37a8eb6b8c 100644 --- a/modules/user/src/main/Trophy.scala +++ b/modules/user/src/main/Trophy.scala @@ -108,7 +108,7 @@ object Trophy { } def make(userId: String, kind: Trophy.Kind) = Trophy( - _id = ornicar.scalalib.Random nextStringUppercase 8, + _id = ornicar.scalalib.Random nextString 8, user = userId, kind = kind, date = DateTime.now) diff --git a/modules/user/src/main/UserRepo.scala b/modules/user/src/main/UserRepo.scala index 866d36cfdb..52979e4003 100644 --- a/modules/user/src/main/UserRepo.scala +++ b/modules/user/src/main/UserRepo.scala @@ -318,6 +318,8 @@ object UserRepo { def hasEmail(id: ID): Fu[Boolean] = email(id).map(_.isDefined) + def getTitle(id: ID): Fu[Option[String]] = coll.primitiveOne[String]($id(id), F.title) + def setPlan(user: User, plan: Plan): Funit = { implicit val pbw: BSONValueWriter[Plan] = Plan.planBSONHandler coll.updateField($id(user.id), "plan", plan).void @@ -379,7 +381,7 @@ object UserRepo { mobileApiVersion: Option[ApiVersion], mustConfirmEmail: Boolean) = { - val salt = ornicar.scalalib.Random nextStringUppercase 32 + val salt = ornicar.scalalib.Random secureString 32 implicit def countHandler = Count.countBSONHandler implicit def perfsHandler = Perfs.perfsBSONHandler import lila.db.BSON.BSONJodaDateTimeHandler diff --git a/project/Build.scala b/project/Build.scala index 3de7032679..988bd6f507 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -35,7 +35,8 @@ object ApplicationBuild extends Build { scalaz, scalalib, hasher, config, apache, jgit, findbugs, reactivemongo.driver, reactivemongo.iteratees, akka.actor, akka.slf4j, spray.caching, maxmind, prismic, - kamon.core, kamon.statsd, java8compat, semver, scrimage), + kamon.core, kamon.statsd, kamon.influxdb, + java8compat, semver, scrimage), TwirlKeys.templateImports ++= Seq( "lila.game.{ Game, Player, Pov }", "lila.tournament.Tournament", @@ -67,7 +68,7 @@ object ApplicationBuild extends Build { libraryDependencies ++= provided( play.api, hasher, config, apache, jgit, findbugs, reactivemongo.driver, reactivemongo.iteratees, - kamon.core, kamon.statsd) + kamon.core, kamon.statsd, kamon.influxdb) ) aggregate (moduleRefs: _*) lazy val puzzle = project("puzzle", Seq( diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 2ed9386721..ba461145b8 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -26,7 +26,7 @@ object Dependencies { } val scalaz = "org.scalaz" %% "scalaz-core" % "7.1.11" - val scalalib = "com.github.ornicar" %% "scalalib" % "5.6" + val scalalib = "com.github.ornicar" %% "scalalib" % "5.7" val config = "com.typesafe" % "config" % "1.3.0" val apache = "org.apache.commons" % "commons-lang3" % "3.4" val findbugs = "com.google.code.findbugs" % "jsr305" % "3.0.1" @@ -65,5 +65,6 @@ object Dependencies { val version = "0.6.3" val core = "io.kamon" %% "kamon-core" % version val statsd = "io.kamon" %% "kamon-statsd" % version + val influxdb = "io.kamon" %% "kamon-influxdb" % version } } diff --git a/public/javascripts/chart/advantage.js b/public/javascripts/chart/advantage.js index d299bb8fb9..cb5b3b838e 100644 --- a/public/javascripts/chart/advantage.js +++ b/public/javascripts/chart/advantage.js @@ -19,6 +19,7 @@ lichess.advantageChart = function(data) { if (node.eval.mate < 0) y = -y; } else if (node.san.indexOf('#') > 0) { y = 100 * (node.ply % 2 === 1 ? max : -max); + if (d.game.variant.key === 'antichess') y = -y; } var turn = Math.floor((node.ply - 1) / 2) + 1; var dots = node.ply % 2 === 1 ? '.' : '...'; diff --git a/public/javascripts/embed-analyse.js b/public/javascripts/embed-analyse.js index 30bfa856ad..40f4061d79 100644 --- a/public/javascripts/embed-analyse.js +++ b/public/javascripts/embed-analyse.js @@ -1,7 +1,7 @@ $(function() { - var studyRegex = /\.org\/study\/(?:embed\/)?(\w{8})\/(\w{8})\b/; - var gameRegex = /\.org\/(?:embed\/)?(\w{8})(?:(?:\/(white|black))|\w{4}|)(#\d+)?\b/; + var studyRegex = /lichess\.org\/study\/(?:embed\/)?(\w{8})\/(\w{8})\b/; + var gameRegex = /lichess\.org\/(?:embed\/)?(\w{8})(?:(?:\/(white|black))|\w{4}|)(#\d+)?\b/; var notGames = ['training', 'analysis']; var parseLink = function(a) { diff --git a/public/javascripts/insight-refresh.js b/public/javascripts/insight-refresh.js index cc297c8678..37c484d0b4 100644 --- a/public/javascripts/insight-refresh.js +++ b/public/javascripts/insight-refresh.js @@ -1,6 +1,6 @@ $(function() { lichess.refreshInsightForm = function() { - $('form.insight-refresh').submit(function() { + $('form.insight-refresh:not(.armed)').addClass('armed').submit(function() { $.modal($(this).find('.crunching')); $.post($(this).attr('action'), function() { lichess.reload(); diff --git a/public/javascripts/main.js b/public/javascripts/main.js index a1d48f804d..da81a2fafa 100644 --- a/public/javascripts/main.js +++ b/public/javascripts/main.js @@ -1159,7 +1159,7 @@ lichess.notifyApp = (function() { var url = '/@/' + name; var tvButton = user.playing ? '' : ''; - return '
' + icon + user.name + '' + tvButton + '
'; + return '
' + icon + user.name + '' + tvButton + '
'; } }; })()); @@ -1733,7 +1733,7 @@ lichess.notifyApp = (function() { }); }; $('#site_header .help a.more').click(function() { - $.modal($(this).parent().find('div.more').clone().removeClass('none')).addClass('card'); + $.modal($(this).parent().find('div.more')).addClass('card'); }); return; } diff --git a/public/javascripts/util.js b/public/javascripts/util.js index 88ef1cbb55..af2a2551b8 100644 --- a/public/javascripts/util.js +++ b/public/javascripts/util.js @@ -97,8 +97,7 @@ lichess.powertip = (function() { var userPowertip = function(el, pos) { if (!pos) { if (elementIdContains('site_header', el)) pos = 'e'; - else if (elementIdContains('friend_box', el)) pos = 'nw'; - else pos = 'w'; + else pos = el.getAttribute('data-pt-pos') || 'w'; } $(el).removeClass('ulpt').powerTip({ intentPollInterval: 200, @@ -141,8 +140,7 @@ lichess.powertip = (function() { }, manualGameIn: function(parent) { Array.prototype.forEach.call(parent.querySelectorAll('.glpt'), gamePowertip); - }, - manualUser: userPowertip + } }; })(); lichess.trans = function(i18n) { @@ -400,7 +398,7 @@ $.fn.scrollTo = function(target, offsetTop) { }; $.modal = function(html) { if (!html.clone) html = $('
' + html + '
'); - var $wrap = $('