Merge branch 'master' into puzzle2

* master: (67 commits)
  enable kamon influxdb backend
  add player users infos to UserGameApi - for #2397
  formatting
  fix unmoved rook persistence after takeback
  uz "oʻzbekcha" translation #16974. Author: VMN91.
  Add source to UserGameApi JSON
  upgrade scalachess to fix FEN tests
  upgrade scalachess
  upgrade scalachess
  ca "Català, valencià" translation #16972. Author: garciagil. (285/534): A preposition in the sentence about the time taken for quiz solution. I replaces "amb"(with) with "en" (in) xxx minutes. Plus a couple of minor changes on verbal tenses.
  tr "Türkçe" translation #16971. Author: katakamata.
  antiches san with # means loss
  fix deploy script
  play & analyse antichess
  upgrade scalachess
  update assets version
  complete unmoved rooks persistence - closes #2392
  tweak perf tests
  test and optimize unmoved rooks serializer performances
  update sf and enable ceval for antichess
  ...
puzzle2
Thibault Duplessis 2016-11-18 19:18:26 +01:00
commit 5476bd1b17
100 changed files with 710 additions and 297 deletions

3
.gitmodules vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,9 +73,6 @@ atom = atom.some) {
@if(game.isPgnImport) {
<a data-icon="x" class="text" rel="nofollow" href="@routes.Export.pgn(game.id)?as=imported">@trans.downloadImported()</a>
}
@if(false) {
<a data-icon="x" class="text" target="_blank" rel="nofollow" href="@cdnUrl(routes.Export.pdf(game.id).url)">@trans.printFriendlyPDF()</a>
}
@if(false && lila.game.Game.visualisableVariants(game.variant)) {
<a data-icon="=" class="text" target="_blank" href="@routes.Export.visualizer(game.id)">Generate images</a>
}

View File

@ -84,8 +84,6 @@ chessground = true) {
/
<a data-icon="x" rel="nofollow" href="@routes.Export.pgn(game.id)?as=imported"> @trans.downloadImported()</a>
}
/
<a data-icon="x" target="_blank" rel="nofollow" href="@cdnUrl(routes.Export.pdf(game.id).url)"> @trans.printFriendlyPDF()</a>
</p>
<div class="pgn">@Html(nl2br(escapeHtml(pgn)))</div>
</div>

View File

@ -53,16 +53,12 @@
<h2>@trans.learn()</h2>
<a href="@routes.Puzzle.home">@trans.training()</a>
<a href="@routes.Coordinate.home">@trans.coordinates()</a>
@ctx.me.map { me =>
<a href="@routes.Study.byOwnerDefault(me.username)">Study</a>
}
<a href="@routes.Video.index">@trans.videoLibrary()</a>
<a href="@routes.Study.allDefault(1)">Study</a>
<a href="@routes.Video.index">@trans.videoLibrary()</a>
</section>
<section>
<h2>@trans.community()</h2>
<a href="@routes.User.list">@trans.players()</a>
<a href="@routes.Stat.ratingDistribution("blitz")">@trans.ratingStats()</a>
@NotForKids {
<a href="@routes.Team.home()">@trans.teams()</a>
<a href="@routes.ForumCateg.index">@trans.forum()</a>

View File

@ -8,11 +8,5 @@
<strong>Now crunching data just for you!</strong>
<br />
<br />
<p>Would you like to watch<br />a live game while you wait?</p>
<br />
<script src="//en.lichess.org/tv/embed?theme=brown&bg=light"></script>
<br />
<br />
<p>This can take a while,<br />maybe reload this page later!</p>
</div>
</form>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -1 +1 @@
Subproject commit ed23fd150d9d119f2089afbe3b2412973c58a152
Subproject commit ad672e9ae20869a11e1d0db6f9913903bbbd8b53

View File

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

View File

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

View File

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

View File

@ -77,7 +77,7 @@ object Post {
hidden: Boolean): Post = {
Post(
_id = Random nextStringUppercase idSize,
_id = Random nextString idSize,
topicId = topicId,
author = author,
userId = userId,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
playWithAFriend=ከጓደኛ ጋር ይጫወቱ
playWithTheMachine=ማሽኑ ጋር ይጫወቱ m
toInviteSomeoneToPlayGiveThisUrl=ለማጫወት አንድ ሰው ለመጋበዝ, ይህን ዩአርኤል መስጠት
computersAreNotAllowedToPlay=ኮምፒዩተሮች እና ኮምፒውተር-ድጋፍ ተጫዋቾች መጫወት አይፈቀድላቸውም. በማጫወት ላይ ሳለ, ወይም ከሌሎች ተጫዋቾች ከ የቼዝ ፕሮግራሞች, ጎታዎች እርዳታ ማግኘት አይስጡ. በተጨማሪም በርካታ መለያዎች ማድረግ በጥብቅ ተስፋ እና ከመጠን የብዝሃ-የሂሳብ እገዳ እየተደረገ ሊመራ እንደሚችል ልብ ይበሉ.

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ waitingForOpponent=等待对手
waiting=稍等
yourTurn=你的回合
aiNameLevelAiLevel=%s级别%s
level=级别
level=水平
toggleTheChat=聊天开关
toggleSound=声音开关
chat=聊天

View File

@ -4,3 +4,4 @@ toInviteSomeoneToPlayGiveThisUrl=Ukuze umeme othile ukudlala, unike lolu kheli
gameOver=Game Over
waitingForOpponent=Elinde isitha
waiting=Ukulinda
yourTurn=Ithuba lakho

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -82,5 +82,5 @@ case class Comment(
object Comment {
def makeId = ornicar.scalalib.Random nextStringUppercase 8
def makeId = ornicar.scalalib.Random nextString 8
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ case object Fixtures {
def text = """
leeching.net
chacuo.net
ChaCuo.net
027168.com
0-mail.com
mail1a.de

View File

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

View File

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

View File

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

View File

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

View File

@ -59,6 +59,8 @@ case class Study(
createdAt = DateTime.now,
updatedAt = DateTime.now)
}
def nbMembers = members.members.size
}
object Study {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ? '.' : '...';

View File

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

View File

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

View File

@ -1159,7 +1159,7 @@ lichess.notifyApp = (function() {
var url = '/@/' + name;
var tvButton = user.playing ? '<a data-icon="1" class="tv is-green ulpt" href="' + url + '/tv" data-href="' + url + '"></a>' : '';
return '<div><a class="user_link ulpt" href="' + url + '">' + icon + user.name + '</a>' + tvButton + '</div>';
return '<div><a class="user_link ulpt" data-pt-pos="nw" href="' + url + '">' + icon + user.name + '</a>' + tvButton + '</div>';
}
};
})());
@ -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;
}

View File

@ -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 = $('<div>' + html + '</div>');
var $wrap = $('<div id="modal-wrap">').html(html.clone().show()).prepend('<span class="close" data-icon="L"></span>');
var $wrap = $('<div id="modal-wrap">').html(html.clone().removeClass('none').show()).prepend('<span class="close" data-icon="L"></span>');
var $overlay = $('<div id="modal-overlay">').html($wrap);
$overlay.add($wrap.find('.close')).one('click', $.modal.close);
$wrap.click(function(e) {

View File

@ -650,36 +650,33 @@ div.table .spinner {
width: 30px;
height: 30px;
}
div.table div.username {
div.table .username {
white-space: nowrap;
display: flex;
margin-left: 4px;
}
div.table div.username:first-child {
margin-bottom: 6px;
}
div.table div.username:last-child {
padding-top: 6px;
}
div.table div.username .user_link {
display: flex;
width: 100%;
margin-left: 3px;
font-size: 1.25em;
}
div.table div.username name {
div.table .username:first-child {
margin-bottom: 6px;
}
div.table .username:last-child {
padding-top: 6px;
}
div.table .username a {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
}
div.table div.username .user_link.long name {
div.table .username.long a {
letter-spacing: -0.5px;
}
div.table div.username rating {
div.table .username rating {
flex: 0 0 auto;
margin: 0 0.2em 0 0.3em;
margin: 0 0.25em 0 0.3em;
opacity: 0.6;
letter-spacing: -0.5px;
}
div.table div.username .line {
div.table .username .line {
display: flex;
justify-content: center;
align-items: center;
@ -689,10 +686,10 @@ div.table div.username .line {
50% { opacity: 0.3; }
100% { opacity: 0.1; }
}
div.table a.connecting .line {
div.table .connecting .line {
animation: connecting 0.9s ease-in-out infinite;
}
div.table div.username .rp {
div.table .username .rp {
margin-right: 0.2em;
}
div.table .message {

View File

@ -1508,13 +1508,13 @@ body.fpmenu #friend_box {
.user_link {
white-space: nowrap;
}
a.user_link span.rp:before {
.user_link span.rp:before {
content: " ";
}
a.user_link span.rp.up {
.user_link span.rp.up {
color: #759900;
}
a.user_link span.rp.down {
.user_link span.rp.down {
color: #ac524f;
}
.user_link span.title {

View File

@ -512,7 +512,7 @@ div.side_box .refused {
#tournament_side .player .rank {
display: none;
}
#tournament_side .player .name {
#tournament_side .user_link .name {
padding-right: 5px;
}
#tournament_side .player close {

View File

@ -70,6 +70,7 @@ body.transp table.translations tbody tr,
body.transp form.translation_form div.message,
body.transp #video_side .tag_list a.checked,
body.transp #powerTip,
body.transp .study_comment_form .material.form .form-group textarea,
body.transp div.vstext,
body.transp .action_menu {
background: rgba(0, 0, 0, 0.6);

@ -1 +1 @@
Subproject commit fecfc8d511c6aa4874aab497d6d9995bad197ac6
Subproject commit c970bfb07955d508a0bee38a5fcee5c32f381c89

@ -1 +1 @@
Subproject commit 4424493424e0119e59bd3094001cbaf90f4c46e1
Subproject commit b9e40a124197e33f870dbf3cb4033599e0aa6274

@ -1 +0,0 @@
Subproject commit e8de1dddd5ba0371cd6310d22f9c80a7574ec161

View File

@ -20,8 +20,8 @@ module.exports = function(opts) {
var hoveringUci = m.prop(null);
var pool = makePool(stockfishProtocol, {
asmjs: '/assets/vendor/stockfish.js/stockfish.js?v=4',
pnacl: pnaclSupported && '/assets/vendor/stockfish.pexe/nacl/stockfish.nmf?v=4',
asmjs: '/assets/vendor/stockfish.js/stockfish.js?v=5',
pnacl: pnaclSupported && '/assets/vendor/stockfish.pexe/nacl/stockfish.nmf?v=5',
onCrash: opts.onCrash
}, {
minDepth: minDepth,

View File

@ -397,7 +397,7 @@ module.exports = function(opts) {
variant: this.data.game.variant,
possible: !this.embed && (
util.synthetic(this.data) || !game.playable(this.data)
) && this.data.game.variant.key !== 'antichess',
),
emit: function(res) {
this.tree.updateAt(res.work.path, function(node) {
if (res.work.threatMode) {

View File

@ -9,8 +9,7 @@ var replayable = require('game').game.replayable;
function tablebaseRelevant(fen) {
var parts = fen.split(/\s/);
var pieceCount = parts[0].split(/[nbrqkp]/i).length - 1;
var castling = parts[2];
return pieceCount <= 6 && castling === '-';
return pieceCount <= 7;
}
module.exports = function(root, opts, allow) {
@ -58,7 +57,7 @@ module.exports = function(root, opts, allow) {
}, false);
var fetchTablebase = throttle(250, function(fen) {
xhr.tablebase(opts.tablebaseEndpoint, root.vm.node.fen).then(function(res) {
xhr.tablebase(opts.tablebaseEndpoint, effectiveVariant, root.vm.node.fen).then(function(res) {
res.nbMoves = res.moves.length;
res.tablebase = true;
res.fen = fen;

View File

@ -204,19 +204,22 @@ function show(ctrl) {
var moves = data.moves;
if (moves.length) lastShow = m('div.data', [
showTablebase(ctrl, 'Winning', moves.filter(function(move) {
return move.real_wdl === -2;
return move.wdl === -2;
}), data.fen),
showTablebase(ctrl, 'Unknown', moves.filter(function(move) {
return move.wdl === null;
}), data.fen),
showTablebase(ctrl, 'Win prevented by 50-move rule', moves.filter(function(move) {
return move.real_wdl === -1;
return move.wdl === -1;
}), data.fen),
showTablebase(ctrl, 'Drawn', moves.filter(function(move) {
return move.real_wdl === 0;
return move.wdl === 0;
}), data.fen),
showTablebase(ctrl, 'Loss saved by 50-move rule', moves.filter(function(move) {
return move.real_wdl === 1;
return move.wdl === 1;
}), data.fen),
showTablebase(ctrl, 'Losing', moves.filter(function(move) {
return move.real_wdl === 2;
return move.wdl === 2;
}), data.fen)
])
else if (data.checkmate) lastShow = showGameEnd(ctrl, 'Checkmate')

View File

@ -25,11 +25,11 @@ module.exports = {
data: params
});
},
tablebase: function(endpoint, fen) {
tablebase: function(endpoint, variant, fen) {
return m.request({
background: true,
method: 'GET',
url: endpoint,
url: endpoint + '/' + variant,
data: {
fen: fen
}

View File

@ -22,6 +22,7 @@ module.exports = {
var active = {}; // recently active contributors
var online = {}; // userId -> bool
var spectatorIds = [];
var max = 30;
var owner = function() {
return dict()[ownerId];
@ -91,6 +92,7 @@ module.exports = {
myMember: myMember,
isOwner: isOwner,
canContribute: canContribute,
max: max,
setRole: function(id, role) {
setActive(id);
send("setRole", {
@ -214,6 +216,8 @@ module.exports = {
]);
};
var ordered = ctrl.members.ordered();
return [
m('div', {
key: 'members',
@ -222,7 +226,7 @@ module.exports = {
lichess.pubsub.emit('content_loaded')();
}
}, [
ctrl.members.ordered().map(function(member) {
ordered.map(function(member) {
var confing = ctrl.members.confing() === member.user.id;
return [
m('div', {
@ -238,7 +242,7 @@ module.exports = {
confing ? memberConfig(member) : null
];
}),
isOwner ? m('div', {
(isOwner && ordered.length < ctrl.members.max) ? m('div', {
key: 'invite-someone',
class: 'elem member add',
config: util.bindOnce('click', ctrl.members.inviteForm.toggle)

View File

@ -35,15 +35,11 @@ function compact(x) {
}
function renderPlayer(ctrl, player) {
return player.ai ? m('div.username',
m('span.user_link.online', [
return player.ai ? m('div.username.user_link.online', [
m('i.line'),
m('name', renderUser.aiName(ctrl, player))
])) : m('div', {
class: 'username ' + player.color
},
renderUser.userHtml(ctrl, player)
);
]) :
renderUser.userHtml(ctrl, player);
}
function isSpinning(ctrl) {

View File

@ -34,25 +34,22 @@ module.exports = {
var fullName = (user.title ? user.title + ' ' : '') + user.username;
var connecting = !player.onGame && ctrl.vm.firstSeconds && user.online;
var isMe = ctrl.userId === user.id;
return m('a', {
class: 'text user_link ' +
return m('div', {
class: 'username user_link ' + player.color + ' ' +
(player.onGame ? 'online' : 'offline') +
(fullName.length > 20 ? ' long' : '') +
(connecting ? ' connecting' : '') +
(isMe ? '' : ' ulpt'),
href: '/@/' + user.username,
target: game.isPlayerPlaying(d) ? '_blank' : '_self',
config: function(el, isUpdate) {
if (!isUpdate && !isMe) raf(function() {
lichess.powertip.manualUser(el, 's');
});
}
(connecting ? ' connecting' : '')
}, [
m('i', {
class: 'line' + (user.patron ? ' patron' : ''),
'title': connecting ? 'Connecting to the game' : (player.onGame ? 'Joined the game' : 'Left the game')
}),
m('name', fullName),
m('a', {
class: 'text ulpt',
'data-pt-pos': 's',
href: '/@/' + user.username,
target: game.isPlayerPlaying(d) ? '_blank' : '_self',
}, fullName),
rating ? m('rating', rating + (player.provisional ? '?' : '')) : null,
ratingDiff(player),
player.engine ? m('span[data-icon=j]', {
@ -61,8 +58,8 @@ module.exports = {
]);
}
var connecting = !player.onGame && ctrl.vm.firstSeconds;
return m('span', {
class: 'user_link ' +
return m('div', {
class: 'username user_link ' +
(player.onGame ? 'online' : 'offline') +
(connecting ? ' connecting' : ''),
}, [