This commit is contained in:
clarkerubber 2016-09-18 23:47:48 +10:00
commit f7e119c9e6
153 changed files with 2839 additions and 1762 deletions

View file

@ -44,7 +44,7 @@ Installation
Feel free to use lichess API in your applications and websites.
If the resource you need is not available yet,
drop me an email at lichess.contact@gmail.com and we'll discuss it.
drop us an email at contact@lichess.org and we'll discuss it.
### API Limits

View file

@ -149,10 +149,10 @@ object Auth extends LilaController {
def signupConfirmEmail(token: String) = Open { implicit ctx =>
Env.security.emailConfirm.confirm(token) flatMap {
case None =>
lila.mon.user.register.confirmEmailResult(false)
lila.mon.user.register.confirmEmailResult(false)()
notFound
case Some(user) =>
lila.mon.user.register.confirmEmailResult(true)
lila.mon.user.register.confirmEmailResult(true)()
redirectNewUser(user)
}
}

View file

@ -14,22 +14,17 @@ object Coach extends LilaController {
def allDefault(page: Int) = all(CoachPager.Order.Login.key, page)
private val canViewCoaches = (u: UserModel) =>
isGranted(_.Admin, u) || isGranted(_.Coach, u) || isGranted(_.PreviewCoach, u)
def all(o: String, page: Int) = SecureF(canViewCoaches) { implicit ctx =>
me =>
val order = CoachPager.Order(o)
Env.coach.pager(order, page) map { pager =>
Ok(html.coach.index(pager, order))
}
def all(o: String, page: Int) = Open { implicit ctx =>
val order = CoachPager.Order(o)
Env.coach.pager(order, page) map { pager =>
Ok(html.coach.index(pager, order))
}
}
def show(username: String) = SecureF(canViewCoaches) { implicit ctx =>
me =>
def show(username: String) = Open { implicit ctx =>
OptionFuResult(api find username) { c =>
WithVisibleCoach(c) {
Env.study.api.byIds {
Env.study.api.publicByIds {
c.coach.profile.studyIds.map(_.value)
} flatMap Env.study.pager.withChaptersAndLiking(ctx.me) flatMap { studies =>
api.reviews.approvedByCoach(c.coach) flatMap { reviews =>
@ -68,7 +63,7 @@ object Coach extends LilaController {
}
private def WithVisibleCoach(c: CoachModel.WithUser)(f: Fu[Result])(implicit ctx: Context) =
if ((c.coach.isListed || ctx.me.??(c.coach.is) || isGranted(_.Admin)) && ctx.me.??(canViewCoaches)) f
if (c.coach.isListed || ctx.me.??(c.coach.is) || isGranted(_.Admin)) f
else notFound
def edit = Secure(_.Coach) { implicit ctx =>

View file

@ -1,7 +1,6 @@
package controllers
import scala.concurrent.duration._
import lila.common.HTTPRequest
import lila.app._
import views._
@ -53,6 +52,18 @@ object ForumPost extends LilaController with ForumController {
}
}
def edit(postId: String) = AuthBody { implicit ctx =>
me =>
implicit val req = ctx.body
forms.postEdit.bindFromRequest.fold(err => Redirect(routes.ForumPost.redirect(postId)).fuccess,
data =>
postApi.editPost(postId, data.changes, me).map { post =>
Redirect(routes.ForumPost.redirect(post.id))
}
)
}
def delete(categSlug: String, id: String) = Auth { implicit ctx =>
me =>
CategGrantMod(categSlug) {

View file

@ -50,14 +50,18 @@ private[controllers] trait LilaController
)
protected def Socket[A: FrameFormatter](f: Context => Fu[(Iteratee[A, _], Enumerator[A])]) =
WebSocket.tryAccept[A] { req => reqToCtx(req) flatMap f map scala.util.Right.apply }
WebSocket.tryAccept[A] { req =>
reqToCtx(req, sameOriginAuth = true) flatMap f map scala.util.Right.apply
}
protected def SocketEither[A: FrameFormatter](f: Context => Fu[Either[Result, (Iteratee[A, _], Enumerator[A])]]) =
WebSocket.tryAccept[A] { req => reqToCtx(req) flatMap f }
WebSocket.tryAccept[A] { req =>
reqToCtx(req, sameOriginAuth = true) flatMap f
}
protected def SocketOption[A: FrameFormatter](f: Context => Fu[Option[(Iteratee[A, _], Enumerator[A])]]) =
WebSocket.tryAccept[A] { req =>
reqToCtx(req) flatMap f map {
reqToCtx(req, sameOriginAuth = true) flatMap f map {
case None => Left(NotFound(jsonError("socket resource not found")))
case Some(pair) => Right(pair)
}
@ -76,8 +80,10 @@ private[controllers] trait LilaController
protected def Open[A](p: BodyParser[A])(f: Context => Fu[Result]): Action[A] =
Action.async(p) { req =>
reqToCtx(req) flatMap { ctx =>
Env.i18n.requestHandler.forUser(req, ctx.me).fold(f(ctx))(fuccess)
CSRF(req) {
reqToCtx(req) flatMap { ctx =>
Env.i18n.requestHandler.forUser(req, ctx.me).fold(f(ctx))(fuccess)
}
}
}
@ -85,19 +91,22 @@ private[controllers] trait LilaController
OpenBody(BodyParsers.parse.anyContent)(f)
protected def OpenBody[A](p: BodyParser[A])(f: BodyContext[A] => Fu[Result]): Action[A] =
Action.async(p)(req => reqToCtx(req) flatMap f)
protected def OpenNoCtx(f: RequestHeader => Fu[Result]): Action[AnyContent] =
Action.async(f)
Action.async(p) { req =>
CSRF(req) {
reqToCtx(req) flatMap f
}
}
protected def Auth(f: Context => UserModel => Fu[Result]): Action[Unit] =
Auth(BodyParsers.parse.empty)(f)
protected def Auth[A](p: BodyParser[A])(f: Context => UserModel => Fu[Result]): Action[A] =
Action.async(p) { req =>
reqToCtx(req) flatMap { implicit ctx =>
ctx.me.fold(authenticationFailed) { me =>
Env.i18n.requestHandler.forUser(req, ctx.me).fold(f(ctx)(me))(fuccess)
CSRF(req) {
reqToCtx(req) flatMap { implicit ctx =>
ctx.me.fold(authenticationFailed) { me =>
Env.i18n.requestHandler.forUser(req, ctx.me).fold(f(ctx)(me))(fuccess)
}
}
}
}
@ -107,8 +116,10 @@ private[controllers] trait LilaController
protected def AuthBody[A](p: BodyParser[A])(f: BodyContext[A] => UserModel => Fu[Result]): Action[A] =
Action.async(p) { req =>
reqToCtx(req) flatMap { implicit ctx =>
ctx.me.fold(authenticationFailed)(me => f(ctx)(me))
CSRF(req) {
reqToCtx(req) flatMap { implicit ctx =>
ctx.me.fold(authenticationFailed)(me => f(ctx)(me))
}
}
}
@ -289,16 +300,18 @@ private[controllers] trait LilaController
case _ => html
}) map (_.withHeaders("Vary" -> "Accept"))
protected def reqToCtx(req: RequestHeader): Fu[HeaderContext] =
restoreUser(req) flatMap { d =>
val ctx = UserContext(req, d.map(_.user))
pageDataBuilder(ctx, d.??(_.hasFingerprint)) map { Context(ctx, _) }
}
protected def reqToCtx(req: RequestHeader, sameOriginAuth: Boolean = false): Fu[HeaderContext] = {
if (sameOriginAuth && !Env.security.csrfRequestHandler.check(req)) fuccess(none)
else restoreUser(req)
} flatMap { d =>
val ctx = UserContext(req, d.map(_.user))
pageDataBuilder(ctx, d.exists(_.hasFingerprint)) map { Context(ctx, _) }
}
protected def reqToCtx[A](req: Request[A]): Fu[BodyContext[A]] =
restoreUser(req) flatMap { d =>
val ctx = UserContext(req, d.map(_.user))
pageDataBuilder(ctx, d.??(_.hasFingerprint)) map { Context(ctx, _) }
pageDataBuilder(ctx, d.exists(_.hasFingerprint)) map { Context(ctx, _) }
}
import lila.hub.actorApi.relation._
@ -341,6 +354,10 @@ private[controllers] trait LilaController
}
}
private def CSRF(req: RequestHeader)(f: => Fu[Result]): Fu[Result] =
if (Env.security.csrfRequestHandler.check(req)) f
else Forbidden("Cross origin request forbidden").fuccess
protected def XhrOnly(res: => Fu[Result])(implicit ctx: Context) =
if (HTTPRequest isXhr ctx.req) res else notFound

View file

@ -18,7 +18,7 @@ trait LilaSocket { self: LilaController =>
def rateLimitedSocket[A: FrameFormatter](consumer: TokenBucket.Consumer, name: String)(f: AcceptType[A]): WebSocket[A, A] =
WebSocket[A, A] { req =>
reqToCtx(req) flatMap { ctx =>
reqToCtx(req, sameOriginAuth = true) flatMap { ctx =>
val ip = HTTPRequest lastRemoteAddress req
def userInfo = {
val sri = get("sri", req) | "none"

View file

@ -126,7 +126,7 @@ object Mod extends LilaController {
lila.memo.AsyncCache[String, Int](ip => {
import play.api.libs.ws.WS
import play.api.Play.current
val email = "lichess.contact@gmail.com"
val email = Env.api.Net.Email
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 =>

View file

@ -0,0 +1,37 @@
package controllers
import scala.concurrent.duration._
import play.api.libs.ws.WS
import play.api.mvc._, Results._
import play.api.Play.current
import lila.app._
import views._
object Monitor extends LilaController {
private val url = "http://api.monitor.lichess.org/render"
private object path {
val coachPageView = "servers.lichess.statsite.counts.main.counter.coach.page_view.profile"
}
private val coachPageViewCache = lila.memo.AsyncCache[lila.user.User.ID, Result](
f = userId => WS.url(url).withQueryString(
"format" -> "json",
"target" -> s"""summarize(${path.coachPageView}.$userId,"1d","sum",false)""",
// "target" -> s"""summarize(servers.lichess.statsite.counts.main.counter.insight.request,'1d','sum',false)""",
"from" -> "-7d",
"until" -> "now"
).get() map {
case res if res.status == 200 => Ok(res.body)
case res =>
lila.log("monitor").warn(s"coach ${res.status} ${res.body}")
NotFound
},
timeToLive = 10 seconds
)
def coachPageView = Secure(_.Coach) { ctx =>
me =>
coachPageViewCache(me.id)
}
}

View file

@ -44,9 +44,9 @@ trait AssetHelper { self: I18nHelper =>
local = staticUrl("vendor/highcharts4/highcharts.js"))
val highchartsLatestTag = cdnOrLocal(
cdn = "//code.highcharts.com/4.1/highcharts.js",
cdn = "//code.highcharts.com/4.2/highcharts.js",
test = "window.Highcharts",
local = staticUrl("vendor/highcharts4/highcharts-4.1.9.js"))
local = staticUrl("vendor/highcharts4/highcharts-4.2.5.js"))
val highchartsMoreTag = Html {
"""<script src="//code.highcharts.com/4.1.4/highcharts-more.js"></script>"""

View file

@ -55,6 +55,10 @@ object Environment
def tablebaseEndpoint = apiEnv.TablebaseEndpoint
def contactEmail = apiEnv.Net.Email
def contactEmailLink = Html(s"""<a href="mailto:$contactEmail">$contactEmail</a>""")
def globalCasualOnlyMessage = Env.setup.CasualOnly option {
"Due to temporary maintenance on the servers, only casual games are available."
}

View file

@ -34,8 +34,10 @@ evenMoreCss =cssTag("material.form.css")) {
</div>
}
<div>
@base.form.group(form("fideRating"), Html("FIDE rating"), klass = "half") {
@base.form.input(form("fideRating"), typ="number")
@List("fide", "uscf", "ecf").map { rn =>
@base.form.group(form(s"${rn}Rating"), Html(s"${rn.toUpperCase} rating"), help=Html("If none, leave empty").some, klass = "third") {
@base.form.input(form(s"${rn}Rating"), typ="number")
}
}
</div>
@errMsg(form)

View file

@ -8,6 +8,7 @@ zen = true) {
<h1 class="lichess_title is-green text" data-icon="E">@trans.checkYourEmail()</h1>
<p>@trans.weHaveSentYouAnEmailClickTheLink()</p>
<p>@trans.ifYouDoNotSeeTheEmailCheckOtherPlaces()</p>
<p>Not receiving it? Ask @contactEmailLink and we'll confirm your email for you.</p>
</div>
</div>
}

View file

@ -8,7 +8,7 @@ title = "Internal server error") {
<br />
<br />
<p>If the problem persists, please report it in the <a href="@routes.ForumCateg.show("lichess-feedback", 1)">forum</a>.</p>
<p>Or send me an email at lichess.contact&#64;gmail.com</p>
<p>Or send me us email at contact&#64;lichess.org</p>
<br />
<br />
<code>@ex.getMessage</code>

View file

@ -16,9 +16,7 @@
<a href="@routes.Puzzle.home">@trans.training()</a>
<a href="@routes.Coordinate.home">@trans.coordinates()</a>
<a href="@routes.Study.allDefault(1)">Study</a>
@if(isGranted(_.Coach)) {
<a href="@routes.Coach.allDefault(1)">Chess coaches</a>
}
</div>
</section>
<section>

View file

@ -16,22 +16,24 @@
@moreJs = {
<script src="//oss.maxcdn.com/jquery.form/3.50/jquery.form.min.js"></script>
@jsAt("vendor/bar-rating/dist/jquery.barrating.min.js")
@jsTag("chart/coachPageView.js")
@jsTag("coach.form.js")
}
@side = {
<a href="@routes.Coach.show(c.user.username)" class="button text" data-icon="v">Preview coach page</a>
<a href="@routes.Coach.show(c.user.username)" class="preview button text" data-icon="v">Preview coach page</a>
}
@layout(title = s"${c.user.titleUsername} coach page",
moreCss = moreCss,
moreJs = moreJs) {
moreJs = moreJs,
side = side.some) {
<div class="coach_edit content_box no_padding">
<div class="top">
<div class="picture_wrap">
@if(c.coach.hasPicture) {
<a class="upload_picture" href="@routes.Coach.picture" title="Change/delete your profile picture">
@pic(c, 200)
@pic(c, 250)
</a>
} else {
<div class="upload_picture">
@ -43,12 +45,12 @@ moreJs = moreJs) {
<h1>
@c.user.title.map { t => @t }@c.user.profileOrDefault.nonEmptyRealName.getOrElse(c.user.username)
</h1>
<div class="todo" data-profile="c.user.profileOrDefault.isComplete">
<div class="todo" data-profile="@c.user.profileOrDefault.isComplete">
<h3>TODO list before publishing your coach profile</h3>
<ul></ul>
</div>
<div class="analytics">
Soon here: Coach page analytics
<div class="pageview_chart">@base.spinner()</div>
</div>
</div>
</div>
@ -92,11 +94,11 @@ moreJs = moreJs) {
@textarea(form("profile.methodology"), Html("Teaching methodology"), help = Html("How you prepare and run lessons. How you follow up with students.").some)
</div>
<div class="panel contents">
@textarea(form("profile.youtubeVideos"), Html("Featured youtube videos"), help = Html("Up to 6 Youtube video URLs, one per line").some)
@textarea(form("profile.publicStudies"), Html("Featured public lichess studies"), help = Html("Up to 6 lichess study URLs, one per line").some)
@base.form.group(form("profile.youtubeChannel"), Html("URL of your Youtube channel")) {
@base.form.input(form("profile.youtubeChannel"))
}
@textarea(form("profile.publicStudies"), Html("Featured public lichess studies"), help = Html("Up to 6 lichess study URLs, one per line").some)
@textarea(form("profile.youtubeVideos"), Html("Featured youtube videos"), help = Html("Up to 6 Youtube video URLs, one per line").some)
</div>
<div class="panel reviews">
<p class="help text" data-icon="">Reviews are visible only after you approve them.</p>

View file

@ -1,7 +1,7 @@
@(pager: Paginator[lila.coach.Coach.WithUser], order: lila.coach.CoachPager.Order)(implicit ctx: Context)
@side = {
<div class="coach-intro">
<div class="coach-intro coach-side">
<img src="@staticUrl("images/icons/certification.svg")" class="certification" />
<h2>Certified coaches</h2>
<p>
@ -16,13 +16,11 @@
make your choice and enjoy learning chess!
</p>
</div>
<div class="coach-add">
<div class="coach-add coach-side">
<p>
Are you a great chess coach?<br />
Do you want to be part of this list?<br />
@defining("lichess.contact@gmail.com") { email =>
Send us an email at <a href="mailto:@email">@email</a><br />
}
Send us an email at @contactEmailLink<br />
and we will review your application.
</p>
</div>
@ -60,7 +58,7 @@ side = side.some) {
}
@pager.nextPage.map { np =>
<div class="pager none">
<a href="@addQueryParameter(routes.Coach.all(order.key).toString, "page", np)">Next</a>
<a rel="next" href="@addQueryParameter(routes.Coach.all(order.key).toString, "page", np)">Next</a>
</div>
}
</div>

View file

@ -5,7 +5,9 @@
@cssTag("coach.form.css")
}
@layout(title = s"${c.user.titleUsername} coach picture", moreCss = moreCss) {
@layout(title = s"${c.user.titleUsername} coach picture",
moreJs = jsTag("coach.form.js"),
moreCss = moreCss) {
<div class="coach_picture content_box small_box no_padding">
<h1 class="lichess_title">
@userLink(c.user) coach picture
@ -28,7 +30,7 @@
}
}
<div class="cancel">
<a href="@routes.Coach.edit" class="text" data-icon="I">Cancel and return to coach page form</a>
<a href="@routes.Coach.edit" class="text" data-icon="I">Return to coach page form</a>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
@(c: lila.coach.Coach.WithUser, approval: Boolean)(implicit ctx: Context)
<div class="review-form">
<div class="review-form coach-side">
@if(approval) {
<div class="approval">
<p>Thank you for the review!</p>

View file

@ -1,7 +1,7 @@
@(c: lila.coach.Coach.WithUser, reviews: lila.coach.CoachReview.Reviews)(implicit ctx: Context)
@if(reviews.list.nonEmpty) {
<div class="reviews">
<div class="reviews coach-side">
<h2>@pluralize("Player review", reviews.list.size)</h2>
@reviews.list.map { r =>
<div class="review">

View file

@ -8,6 +8,7 @@
@moreJs = {
@jsAt("vendor/bar-rating/dist/jquery.barrating.min.js")
@if(ctx.isAuth) {
@embedJs {
$(function() {
$(".bar-rating").barrating();
$('.review-form .toggle').click(function() {
@ -17,6 +18,7 @@ $('.review-form form').slideDown(500);
});
}
}
}
@side = {
@reviewForm(c, reviewApproval)
@ -71,8 +73,8 @@ moreCss = moreCss) {
<tr class="rating">
<th>Rating</th>
<td>
@profile.fideRating.map { r =>
FIDE: @r,
@profile.officialRating.map { r =>
@r.name.toUpperCase: @r.rating,
}
<a href="@routes.User.show(c.user.username)">
@c.user.best8Perfs.take(6).filter(c.user.hasEstablishedRating).map { pt =>
@ -133,6 +135,18 @@ moreCss = moreCss) {
@section("Best skills", profile.skills)
@section("Teaching methodology", profile.methodology)
</div>
@if(studies.nonEmpty) {
<section class="studies">
<h1>Public studies</h1>
<div class="list">
@studies.map { s =>
<div class="study">
@study.widget(s)
</div>
}
</div>
</section>
}
@if(profile.youtubeUrls.nonEmpty) {
<section class="youtube">
<h1>
@ -148,18 +162,6 @@ moreCss = moreCss) {
</div>
</section>
}
@if(studies.nonEmpty) {
<section class="studies">
<h1>Public studies</h1>
<div class="list">
@studies.map { s =>
<div class="study">
@study.widget(s)
</div>
}
</div>
</section>
}
}
</div>
}

View file

@ -20,7 +20,7 @@ moreCss = cssTag("event.css")) {
}
</td>
<td>
<span class="date">@showDateTime(e.startsAt)</span>
<span class="date">@momentFormat(e.startsAt)</span>
@momentFromNow(e.startsAt)
</td>
</tr>

View file

@ -23,7 +23,7 @@
}
}
@if(pager.hasNextPage) {
<a href="@url(pager.nextPage.get)" data-icon="H"></a>
<a rel="next" href="@url(pager.nextPage.get)" data-icon="H"></a>
} else {
<span class="disabled" data-icon="H"></span>
}

View file

@ -17,7 +17,7 @@ searchText = text
<tbody class="infinitescroll">
@views.nextPage.map { n =>
<tr><th class="pager none">
<a href="@routes.ForumPost.search(text, n)">Next</a>
<a rel="next" href="@routes.ForumPost.search(text, n)">Next</a>
</th></tr>
<tr></tr>
}

View file

@ -8,6 +8,7 @@
@forum.layout(
title = s"${categ.name} / ${topic.name} page ${posts.currentPage}",
menu = modMenu.some.ifTrue(categ.isStaff),
moreJs = jsTag("forum-post.js"),
openGraph = lila.app.ui.OpenGraph(
title = s"Forum: ${categ.name} / ${topic.name} page ${posts.currentPage}",
url = s"$netBaseUrl${routes.ForumTopic.show(categ.slug, topic.slug).url}",
@ -36,16 +37,36 @@ description = shorten(posts.currentPageResults.headOption.??(_.text).replace("\n
<div class="post" id="@post.number">
<div class="metas clearfix">
@authorLink(post, "author".some)
@momentFromNow(post.createdAt)
@if(post.hasEdits) {
<span class="post-edited">edited</span>
@momentFromNow(post.updatedAt)
} else {
@momentFromNow(post.createdAt)
}
<a class="anchor" href="@routes.ForumTopic.show(categ.slug, topic.slug, posts.currentPage)#@post.number">#@post.number</a>
@if(isGranted(_.IpBan)) {
<span class="mod postip">@post.ip</span>
}
@if(ctx.userId.fold(false)(post.shouldShowEditForm(_))) {
<a class="mod thin edit button" data-icon="m"> Edit</a>
}
@if(isGrantedMod(categ.slug)) {
<a class="mod thin delete button" href="@routes.ForumPost.delete(categ.slug, post.id)" data-icon="q"> Delete</a>
}
</div>
<p class="message">@autoLink(post.text)</p>
@if(ctx.userId.fold(false)(post.shouldShowEditForm(_))) {
<form class="edit-post-form" method="post" action="@routes.ForumPost.edit(post.id)">
<textarea name="changes" class="edit-post-box" minlength="3" required>@post.text</textarea>
<div class="edit-buttons">
<a class="edit-post-cancel" href="@routes.ForumPost.redirect(post.id)" style="margin-left:20px">@trans.cancel()</a>
<button type="submit" class="submit button">@trans.apply()</button>
</div>
</form>
}
</div>
}
</div>

View file

@ -34,7 +34,7 @@
}
</span>
@game.pgnImport.flatMap(_.date).getOrElse(
game.isBeingPlayed.fold(trans.playingRightNow(), Html(s"<span title='${showDateTime(game.createdAt)}'>${momentFormat(game.createdAt)}</span>"))
game.isBeingPlayed.fold(trans.playingRightNow(), momentFormat(game.createdAt))
)
</div>
@game.pgnImport.flatMap(_.date).map { date =>

View file

@ -26,7 +26,7 @@ title = trans.inbox.str()) {
<tbody class="infinitescroll">
@if(threads.hasToPaginate) {
<tr><th class="pager none">
<a href="@routes.Message.inbox(threads.nextPage | 1)">Next</a>
<a rel="next" href="@routes.Message.inbox(threads.nextPage | 1)">Next</a>
</th></tr>
}
@threads.currentPageResults.map { thread =>

View file

@ -51,7 +51,7 @@ side = side.some) {
@chat.lines.map { line =>
<div class="line @if(line.author.toLowerCase == u.id) { author }">
@userIdLink(line.author.toLowerCase.some, withOnline = false, withTitle = false)
@line.text
@autoLink(line.text)
</div>
}
</div>

View file

@ -20,7 +20,7 @@ moreJs = jsTag("public-chat.js")) {
@chat.lines.filter(_.isVisible).map { line =>
<div class="line">
@userIdLink(line.author.toLowerCase.some, withOnline = false, withTitle = false)
@line.text
@autoLink(line.text)
</div>
}
</div>
@ -42,12 +42,11 @@ moreJs = jsTag("public-chat.js")) {
@chat.lines.filter(_.isVisible).map { line =>
<div class="line">
@userIdLink(line.author.toLowerCase.some, withOnline = false, withTitle = false)
@line.text
@autoLink(line.text)
</div>
}
</div>
</div>
</div>
}
}
</div>

View file

@ -8,7 +8,7 @@ moreCss = cssTag("plan.css")) {
<h1>@userLink(me) • lichess Patron for @pluralize("month", me.plan.months)</h1>
<div class="paypal_patron">
<p>You are supporting lichess with paypal.</p>
<p>If you wish to cancel your support, please contact us at lichess.contact@@gmail.com.</p>
<p>If you wish to cancel your support, please contact us at contact@@lichess.org.</p>
</div>
</div>
}

View file

@ -28,7 +28,7 @@ description = "Community knowledge and frequently asked questions about lichess.
}
@questions.nextPage.map { next =>
<tr><th class="pager none">
<a href="@routes.QaQuestion.index(next.some)">Next</a>
<a rel="next" href="@routes.QaQuestion.index(next.some)">Next</a>
</th></tr>
}
</tbody>

View file

@ -207,7 +207,7 @@ description = s"Search in ${nbGames.localize} chess games using advanced criteri
</div>
<div class="search_infinitescroll">
@pager.nextPage.map { n =>
<div class="pager none"><a href="@routes.Search.index(n)">Next</a></div>
<div class="pager none"><a rel="next" href="@routes.Search.index(n)">Next</a></div>
}.getOrElse {
<div class="none"></div>
}

View file

@ -13,7 +13,7 @@
}
@pager.nextPage.map { np =>
<div class="pager none">
<a href="@addQueryParameter(url.toString, "page", np)">Next</a>
<a rel="next" href="@addQueryParameter(url.toString, "page", np)">Next</a>
</div>
}
</div>

View file

@ -15,7 +15,7 @@ currentTab = tab.some) {
<tbody class="infinitescroll">
@next.map { n =>
<tr><th class="pager none">
<a href="@n">Next</a>
<a rel="next" href="@n">Next</a>
</th></tr>
<tr></tr>
}

View file

@ -14,7 +14,7 @@
<h2>@trans.teamRecentMembers()</h2>
<div class="userlist infinitescroll">
@members.nextPage.map { np =>
<div class="pager none"><a href="@routes.Team.show(t.id, np)">Next</a></div>
<div class="pager none"><a rel="next" href="@routes.Team.show(t.id, np)">Next</a></div>
}
@members.currentPageResults.map { member =>
<div class="paginated_element">@userLink(member.user)</div>

View file

@ -60,5 +60,13 @@
@base.form.select(form("conditions.maxRating.perf"), Condition.DataForm.perfChoices)
}
</div>
<div>
@base.form.group(form("conditions.minRating.rating"), Html("Minimum top rating"), half = true) {
@base.form.select(form("conditions.minRating.rating"), Condition.DataForm.minRatingChoices)
}
@base.form.group(form("conditions.minRating.perf"), Html("In variant"), half = true) {
@base.form.select(form("conditions.minRating.perf"), Condition.DataForm.perfChoices)
}
</div>
@base.form.submit()

View file

@ -21,10 +21,5 @@
}
<td>@tour.durationString</td>
<td data-icon="r" class="text">@tour.nbPlayers</td>
<td>
<form action="@routes.Tournament.join(tour.id)" method="POST">
<button type="submit" class="submit button" data-icon="G"></button>
</form>
</td>
</tr>
}

View file

@ -2,7 +2,7 @@
<tbody class="infinitescroll">
@finished.nextPage.map { np =>
<tr><th class="pager none">
<a href="@routes.Tournament.home(np)">Next</a>
<a rel="next" href="@routes.Tournament.home(np)">Next</a>
</th></tr>
}
@finished.currentPageResults.map { t =>

View file

@ -53,7 +53,7 @@
} else {
@trans.by(usernameOrId(tour.createdBy))
}
<span title="@showDateTime(tour.startsAt)">@momentFormat(tour.startsAt)</span>
• @momentFormat(tour.startsAt)
@if(!tour.position.initial) {
<br /><br />
<a target="_blank" href="@tour.position.url">

View file

@ -1,7 +1,5 @@
@(u: User)(implicit ctx: Context)
@defining("lichess.contact@gmail.com") { email =>
<div class="claim_title_zone">
<h2 data-icon="C"> Congratulations for breaking the 2400 rating threshold!</h2>
@ -18,8 +16,7 @@
</p>
<p>
If you need help or have any question, feel free to contact us by email at
<a href="mailto:@email" title="About title verification">@email</a>.
If you need help or have any question, feel free to contact us by email at @contactEmailLink.
</p>
<p class="actions">
@ -27,4 +24,3 @@
<a class="button" href="@routes.Pref.saveTag(lila.pref.Pref.Tag.verifyTitle, "0")">I don't have an official title</a>
</p>
</div>
}

View file

@ -9,7 +9,7 @@
</div>
<div class="search_infinitescroll">
@pager.nextPage.map { n =>
<div class="pager none"><a href="@routes.User.showFilter(u.username, filterName, n)">Next</a></div>
<div class="pager none"><a rel="next" href="@routes.User.showFilter(u.username, filterName, n)">Next</a></div>
}.getOrElse {
<div class="none"></div>
}
@ -25,7 +25,7 @@
} else {
<div class="games infinitescroll @if(filterName == "playing" && pager.nbResults > 2) {game_list playing center}">
@pager.nextPage.map { np =>
<div class="pager none"><a href="@routes.User.showFilter(u.username, filterName, np)">Next</a></div>
<div class="pager none"><a rel="next" href="@routes.User.showFilter(u.username, filterName, np)">Next</a></div>
}
@if(filterName == "playing" && pager.nbResults > 2) {
@pager.currentPageResults.flatMap{ Pov(_, u) }.map { p =>

View file

@ -262,13 +262,10 @@
<br />
}
</div>
@if(spy.otherUsers.size == 1) {
<strong data-icon="f"> No similar user found</strong>
} else {
<table class="others slist">
<thead>
<tr>
<th>@spy.otherUsers.size similar user(s)</th>
<th>@spy.otherUsers.size user(s) on these IPs</th>
<th>Same</th>
<th>Games</th>
<th>Marks</th>
@ -306,7 +303,6 @@
}
</tbody>
</table>
}
<div class="listings clearfix">
<div class="spy_ips">
<strong>@spy.ips.size IP addresses</strong> <ul>@spy.ipsByLocations.map {

View file

@ -167,8 +167,8 @@ description = describeUser(u)).some) {
}
}
<div class="stats">
@profile.fideRating.map { rating =>
<div class="fide_rating">FIDE rating: <strong>@rating</strong></div>
@profile.officialRating.map { r =>
<div>@r.name.toUpperCase rating: <strong>@r.rating</strong></div>
}
@NotForKids {
@profile.nonEmptyLocation.map { l =>

View file

@ -5,7 +5,7 @@
<tbody class="infinitescroll">
@pager.nextPage.map { np =>
<tr><th class="pager none">
<a href="@call.url?page=@np">Next</a>
<a rel="next" href="@call.url?page=@np">Next</a>
</th></tr>
}
@pager.currentPageResults.map { r =>

View file

@ -18,7 +18,7 @@
<tbody class="infinitescroll">
@pager.nextPage.map { np =>
<tr><th class="pager none">
<a href="@routes.UserTournament.path(u.username, path, np)">Next</a>
<a rel="next" href="@routes.UserTournament.path(u.username, path, np)">Next</a>
</th></tr>
}
@pager.currentPageResults.map { e =>
@ -36,7 +36,7 @@
} else {
@e.tour.perfType.map(_.name)
} •
@showDateTime(e.tour.startsAt)
@momentFormat(e.tour.startsAt)
</span>
</a>
</td>

View file

@ -16,7 +16,7 @@ control = control) {
}
@videos.nextPage.map { next =>
<div class="pager none">
<a href="@routes.Video.author(author)?@control.queryString&page=@next">Next</a>
<a rel="next" href="@routes.Video.author(author)?@control.queryString&page=@next">Next</a>
</div>
}
</div>

View file

@ -48,7 +48,7 @@ control = control) {
}
@videos.nextPage.map { next =>
<div class="pager none">
<a href="@routes.Video.index?@control.queryString&page=@next">Next</a>
<a rel="next" href="@routes.Video.index?@control.queryString&page=@next">Next</a>
</div>
}
</div>

View file

@ -29,7 +29,7 @@ control = control) {
}
@videos.nextPage.map { next =>
<div class="pager none">
<a href="@routes.Video.index?@control.queryString&page=@next">Next</a>
<a rel="next" href="@routes.Video.index?@control.queryString&page=@next">Next</a>
</div>
}
</div>

View file

@ -18,7 +18,9 @@ fi
lilalog "Deploy assets to $mode server $REMOTE:$REMOTE_DIR"
bin/prod/compile-client
# if [ $2 != "skip" ]; then
bin/prod/compile-client
# fi
lilalog "Rsync scripts"
rsync --archive --no-o --no-g --progress public $REMOTE:$REMOTE_DIR

View file

@ -10,8 +10,9 @@ net {
ip = "5.196.91.160"
asset {
domain = ${net.domain}
version = 1098
version = 1118
}
email = "contact@lichess.org"
}
forcedev = false
play {
@ -28,7 +29,7 @@ play {
}
}
i18n {
langs=[en,fr,ru,de,tr,sr,lv,bs,da,es,ro,it,fi,uk,pt,pl,nl,vi,sv,cs,sk,hu,ca,sl,az,nn,eo,tp,el,fp,lt,nb,et,hy,af,hi,ar,zh,gl,hr,mk,id,ja,bg,th,fa,he,mr,mn,cy,gd,ga,sq,be,ka,sw,ps,is,kk,io,gu,fo,eu,bn,id,la,jv,ky,pi,as,le,ta,sa,ml,kn,ko,mg,kb,zu,ur,yo,tl,fy,jb,tg]
langs=[en,fr,ru,de,tr,sr,lv,bs,da,es,ro,it,fi,uk,pt,pl,nl,vi,sv,cs,sk,hu,ca,sl,az,nn,eo,tp,el,fp,lt,nb,et,hy,af,hi,ar,zh,gl,hr,mk,id,ja,bg,th,fa,he,mr,mn,cy,gd,ga,sq,be,ka,sw,ps,is,kk,io,gu,fo,eu,bn,id,la,jv,ky,pi,as,le,ta,sa,ml,kn,ko,mg,kb,zu,ur,yo,tl,fy,jb,tg,cv]
}
http {
session {
@ -92,7 +93,7 @@ blog {
}
last_post_cache.ttl = 5 minutes
rss {
email = "lichess.contact@gmail.com"
email = ${net.email}
}
}
qa {
@ -255,13 +256,14 @@ security {
refresh_delay = 1 hour
}
disposable_email {
provider_url = "https://raw.githubusercontent.com/ornicar/disposable-email-domains/master/index.json"
provider_url = "https://raw.githubusercontent.com/ornicar/disposable-email-domains/master/list"
refresh_delay = 10 minutes
}
recaptcha = ${recaptcha}
whois {
key = "matewithknightandbishop"
}
net.domain = ${net.domain}
}
recaptcha {
endpoint = "https://www.google.com/recaptcha/api/siteverify"
@ -335,7 +337,7 @@ mailgun {
url = "???"
key = "???"
}
sender = "lichess.org <noreply@mail.lichess.org>"
sender = "lichess.org <noreply@lichess.org>"
base_url = ${net.base_url}
}
lobby {
@ -396,6 +398,7 @@ tv {
google.api_key = ""
keyword = "lichess.org"
hitbox.url = "http://api.hitbox.tv/media/live/"
twitch.client_id = ""
}
}
explorer {

View file

@ -56,6 +56,12 @@
<encoder><pattern>%date [%level] %message%n%xException</pattern></encoder>
</appender>
</logger>
<logger name="csrf" level="DEBUG">
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${application.home}/logs/csrf.log</file>
<encoder><pattern>%date [%level] %message%n%xException</pattern></encoder>
</appender>
</logger>
<!-- <logger name="java.io.IOException" level="OFF" /> -->
<!-- <logger name="java.nio.channels.ClosedChannelException" level="OFF" /> -->

View file

@ -125,6 +125,16 @@
</rollingPolicy>
</appender>
</logger>
<logger name="csrf" level="DEBUG">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/csrf.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/lichess/csrf-log-%d{yyyy-MM-dd}.gz</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
</appender>
</logger>
<!-- Set a specific actor to DEBUG -->
<!-- <logger name="actors.MyActor" level="DEBUG" /> -->

View file

@ -362,6 +362,7 @@ POST /forum/:categSlug/:slug/close controllers.ForumTopic.close(categSlug: S
POST /forum/:categSlug/:slug/hide controllers.ForumTopic.hide(categSlug: String, slug: String)
POST /forum/:categSlug/:slug/new controllers.ForumPost.create(categSlug: String, slug: String, page: Int ?= 1)
POST /forum/:categSlug/delete/:id controllers.ForumPost.delete(categSlug: String, id: String)
POST /forum/post/:id controllers.ForumPost.edit(id: String)
GET /forum/redirect/post/:id controllers.ForumPost.redirect(id: String)
# Message
@ -481,6 +482,9 @@ GET /help/contribute controllers.Page.contribute
GET /help/master controllers.Page.master
GET /help/stream-on-lichess controllers.Page.streamHowTo
# Graphite
GET /monitor/coach/pageview controllers.Monitor.coachPageView
POST /jslog/$id<\w{12}> controllers.Main.jslog(id)
# Assets

View file

@ -43,6 +43,7 @@ final class Env(
val Port = config getInt "http.port"
val AssetDomain = config getString "net.asset.domain"
val AssetVersion = config getInt "net.asset.version"
val Email = config getString "net.email"
}
val PrismicApiUrl = config getString "prismic.api_url"
val EditorAnimationDuration = config duration "editor.animation.duration"

View file

@ -11,7 +11,7 @@ object UrlList {
def apply(text: String): List[Url] =
text.lines.toList.map(_.trim).filter(_.nonEmpty) flatMap toUrl take max
private val UrlRegex = """.*(?:youtube\.com|youtu\.be)/(?:watch)?(?:\?v=)(.+)$""".r
private val UrlRegex = """.*(?:youtube\.com|youtu\.be)/(?:watch)?(?:\?v=)([^"&?\/ ]{11}).*""".r
/*
* https://www.youtube.com/watch?v=wEwoyYp_iw8

View file

@ -9,11 +9,12 @@ object HTTPRequest {
(req.headers get "X-Requested-With") contains "XMLHttpRequest"
def isSocket(req: RequestHeader): Boolean =
(req.headers get HeaderNames.UPGRADE) ?? (_.toLowerCase == "websocket")
(req.headers get HeaderNames.UPGRADE).exists(_.toLowerCase == "websocket")
def isSynchronousHttp(req: RequestHeader) = !isXhr(req) && !isSocket(req)
def isSafe(req: RequestHeader) = req.method == "GET"
def isSafe(req: RequestHeader) = req.method == "GET" || req.method == "HEAD" || req.method == "OPTIONS"
def isUnsafe(req: RequestHeader) = !isSafe(req)
def isRedirectable(req: RequestHeader) = isSynchronousHttp(req) && isSafe(req)
@ -28,6 +29,8 @@ object HTTPRequest {
def isChrome(req: RequestHeader) = uaContains(req, "Chrome/")
def isSafari(req: RequestHeader) = uaContains(req, "Safari/") && !isChrome(req)
def origin(req: RequestHeader): Option[String] = req.headers get HeaderNames.ORIGIN
def referer(req: RequestHeader): Option[String] = req.headers get HeaderNames.REFERER
def lastRemoteAddress(req: RequestHeader): String =
@ -56,4 +59,7 @@ object HTTPRequest {
def hasFileExtension(req: RequestHeader) =
fileExtensionPattern.matcher(req.path).matches
def print(req: RequestHeader) =
s"${req.method} ${req.domain}${req.uri} ${lastRemoteAddress(req)} origin:${~origin(req)} referer:${~referer(req)} ua:${~userAgent(req)}"
}

View file

@ -45,7 +45,12 @@ object mon {
val timeout = inc("http.mailgun.timeout")
}
object userGames {
def cost = incX(s"http.user-games.cost")
def cost = incX("http.user-games.cost")
}
object csrf {
val missingOrigin = inc("http.csrf.missing_origin")
val forbidden = inc("http.csrf.forbidden")
val websocket = inc("http.csrf.websocket")
}
}
object lobby {
@ -228,6 +233,12 @@ object mon {
val created = rec("tournament.created")
val started = rec("tournament.started")
val player = rec("tournament.player")
object startedOrganizer {
val tickTime = rec("tournament.started_organizer.tick_time")
}
object createdOrganizer {
val tickTime = rec("tournament.created_organizer.tick_time")
}
}
object donation {
val goal = rec("donation.goal")
@ -337,12 +348,12 @@ object mon {
object work {
def acquired(skill: String) = rec(s"fishnet.work.$skill.acquired")
def queued(skill: String) = rec(s"fishnet.work.$skill.queued")
def moveDbSize = rec(s"fishnet.work.move.db_size")
val moveDbSize = rec("fishnet.work.move.db_size")
}
object move {
def time(client: String) = rec(s"fishnet.move.time.$client")
def post = rec(s"fishnet.move.post")
def dbDrop = inc(s"fishnet.move.db_drop")
val post = rec("fishnet.move.post")
val dbDrop = inc("fishnet.move.db_drop")
}
object analysis {
def by(client: String) = new {
@ -357,18 +368,18 @@ object mon {
def totalSecond = incX(s"fishnet.analysis.total.second.$client")
def totalPosition = incX(s"fishnet.analysis.total.position.$client")
}
def post = rec(s"fishnet.analysis.post")
val post = rec("fishnet.analysis.post")
}
}
object api {
object teamUsers {
def cost = incX(s"api.team-users.cost")
val cost = incX("api.team-users.cost")
}
object userGames {
def cost = incX(s"api.user-games.cost")
val cost = incX("api.user-games.cost")
}
object game {
def cost = incX(s"api.game.cost")
val cost = incX("api.game.cost")
}
}
object export {

View file

@ -7,7 +7,10 @@ import reactivemongo.bson._
private object BSONHandlers {
implicit val CategBSONHandler = Macros.handler[Categ]
implicit val PostEditBSONHandler = Macros.handler[OldVersion]
implicit val PostBSONHandler = Macros.handler[Post]
private val topicHandler: BSONDocumentReader[Topic] with BSONDocumentWriter[Topic] with BSONHandler[Bdoc, Topic] = Macros.handler[Topic]
implicit val TopicBSONHandler: BSONDocumentReader[Topic] with BSONDocumentWriter[Topic] with BSONHandler[Bdoc, Topic] = LoggingHandler(logger)(topicHandler)
}

View file

@ -17,6 +17,8 @@ private[forum] final class DataForm(val captcher: akka.actor.ActorSelection) ext
val post = Form(postMapping)
val postEdit = Form(mapping("changes" -> text(minLength=3))(PostEdit.apply)(PostEdit.unapply))
def postWithCaptcha = withCaptcha(post)
val topic = Form(mapping(
@ -36,4 +38,6 @@ object DataForm {
case class TopicData(
name: String,
post: PostData)
case class PostEdit(changes: String)
}

View file

@ -2,8 +2,10 @@ package lila.forum
import org.joda.time.DateTime
import ornicar.scalalib.Random
import lila.user.User
import scala.concurrent.duration._
case class OldVersion(text: String, createdAt: DateTime)
case class Post(
_id: String,
@ -17,7 +19,12 @@ case class Post(
troll: Boolean,
hidden: Boolean,
lang: Option[String],
createdAt: DateTime) {
editHistory: List[OldVersion],
createdAt: DateTime,
updatedAt: DateTime) {
private val permitEditsFor = 4 hours
private val showEditFormFor = 3 hours
def id = _id
@ -28,6 +35,24 @@ case class Post(
def isTeam = categId startsWith teamSlug("")
def isStaff = categId == "staff"
def canStillBeEdited() = {
updatedAt.plus(permitEditsFor.toMillis).isAfter(DateTime.now)
}
def canBeEditedBy(editingId: String): Boolean = userId.fold(false)(editingId == _)
def shouldShowEditForm(editingId: String) =
canBeEditedBy(editingId) && updatedAt.plus(showEditFormFor.toMillis).isAfter(DateTime.now)
def editPost(updated: DateTime, newText: String) : Post = {
val oldVersion = new OldVersion(text, updatedAt)
val history = oldVersion :: editHistory
this.copy(editHistory = history, text = newText, updatedAt = updated)
}
def hasEdits = editHistory.nonEmpty
}
object Post {
@ -44,17 +69,24 @@ object Post {
number: Int,
lang: Option[String],
troll: Boolean,
hidden: Boolean): Post = Post(
_id = Random nextStringUppercase idSize,
topicId = topicId,
author = author,
userId = userId,
ip = ip,
text = text,
number = number,
lang = lang,
troll = troll,
hidden = hidden,
createdAt = DateTime.now,
categId = categId)
hidden: Boolean): Post = {
val now = DateTime.now
Post(
_id = Random nextStringUppercase idSize,
topicId = topicId,
author = author,
userId = userId,
ip = ip,
text = text,
number = number,
lang = lang,
editHistory = List.empty,
troll = troll,
hidden = hidden,
createdAt = now,
updatedAt = now,
categId = categId)
}
}

View file

@ -3,14 +3,13 @@ package lila.forum
import actorApi._
import akka.actor.ActorSelection
import org.joda.time.DateTime
import lila.common.paginator._
import lila.db.dsl._
import lila.db.paginator._
import lila.hub.actorApi.timeline.{ Propagate, ForumPost }
import lila.hub.actorApi.timeline.{ForumPost, Propagate}
import lila.mod.ModlogApi
import lila.security.{ Granter => MasterGranter }
import lila.user.{ User, UserContext }
import lila.security.{Granter => MasterGranter}
import lila.user.{User, UserContext}
final class PostApi(
env: Env,
@ -69,6 +68,25 @@ final class PostApi(
}
}
def editPost(postId: String, newText: String, user: User) : Fu[Post] = {
get(postId) flatMap { post =>
val now = DateTime.now
post match {
case Some((_, post)) if !post.canBeEditedBy(user.id) =>
fufail("You are not authorized to modify this post.")
case Some((_, post)) if !post.canStillBeEdited() =>
fufail("Post can no longer be edited")
case Some((_,post)) =>
val spamEscapedTest = lila.security.Spam.replace(newText)
val newPost = post.editPost(now, spamEscapedTest)
env.postColl.update($id(post.id), newPost) inject newPost
case None => fufail("Post no longer exists.")
}
}
}
private val quickHideCategs = Set("lichess-feedback", "off-topic-discussion")
private def shouldHideOnPost(topic: Topic) =

View file

@ -62,7 +62,7 @@ nbPopularGames=%s Известни игри
nbAnalysedGames=%s Проучени игри
bookmarkedByNbPlayers=Отбелязана от %s играчи
viewInFullSize=Гледай на пълен екран
logOut=Отписване
logOut=Излез
signIn=Влез
newToLichess=Нов потребител?
youNeedAnAccountToDoThat=Трябва ти регистрация, за да направиш това
@ -146,7 +146,7 @@ nbWins=%s победи
nbLosses=%s загуби
nbDraws=%s равенства
exportGames=Изтегли партиите
ratingRange=Рейтинг на противника
ratingRange=Рейтинг на съперника
giveNbSeconds=Дай %s sec
premoveEnabledClickAnywhereToCancel=Предварителен ход е зададен - Натиснете където и да е за отмяна
thisPlayerUsesChessComputerAssistance=Този играч използва компютърна помощ
@ -255,8 +255,8 @@ reportXToModerators=Докладвай %s на съдийте
profile=Профил
editProfile=Редактирай профила
firstName=Име
lastName=Фамилия
biography=Биография
lastName=Презиме
biography=Животопис
country=Страна
preferences=Предпочитания
watchLichessTV=Гледай Lichess TV
@ -266,7 +266,7 @@ activeToday=Активни днес
activePlayers=Активни играчи
bewareTheGameIsRatedButHasNoClock=Внимание, играта е с рейтинг, но без часовник!
training=Задачи
yourPuzzleRatingX=Вашият рейтинг на пъзели: %s
yourPuzzleRatingX=Вашият рейтинг по задачите: %s
findTheBestMoveForWhite=Открий най-добрия ход за белите.
findTheBestMoveForBlack=Открий най-добрия ход за черните.
toTrackYourProgress=За да следите напредъка си:
@ -433,7 +433,7 @@ error.email=Адресът на ел. поща е невалиден
error.email_acceptable=Адресът на ел. поща е недопустим
error.email_unique=Тази ел. поща е вече заета
blindfoldChess=Блинд (невидими фигури)
moveConfirmation=Потвърждение при местене
moveConfirmation=Потвърждение на хода
inCorrespondenceGames=Кореспондентски игри
ifRatingIsPlusMinusX=Ако рейтингът е ± %s
onlyFriends=Само приятели

View file

@ -0,0 +1,335 @@
playWithAFriend=Туспа выља
playWithTheMachine=Машинпа выља
gameOver=Вӑйӑ вӗҫленчӗ
waitingForOpponent=Ӑмӑртуҫшӑн кӗтсе тӑни
waiting=Кӗтсе тӑни
yourTurn=Санӑн ҫӳремелле
aiNameLevelAiLevel=%s шайӗ: %s
level=Шай
toggleTheChat=Сӳнтер / Пултар
toggleSound=Сӳнтер / Пултар
chat=Калаҫу
resign=Парӑн
checkmate=Шах та мат
white=Шуррисем
black=Хурисем
randomColor=Ӑнсӑрт йен
createAGame=Вӑйӑ йӗркелесси
whiteIsVictorious=Шуррисем ҫӗнтерчӗҫ
blackIsVictorious=Хурисем ҫӗнтерчӗҫ
kingInTheCenter=Ту патши
threeChecks=Виҫӗ шах пани
raceFinished=Чупу вӗҫленчӗ
newOpponent=Ҫӗнӗ ӑмӑртуҫӑ
joinTheGame=Вӑййине хутшӑн
whitePlays=Шуррисен ҫӳремелле
blackPlays=Хурисен ҫӳремелле
makeYourOpponentResign=Ӑмӑртуҫуна парӑнтаттар
talkInChat=Калаҫура сӑпайлӑ пул!
whiteResigned=Шуррисем парӑнчӗҫ
blackResigned=Хурисем парӑнчӗҫ
whiteLeftTheGame=Шуррисем вӑййинчен тухрӗҫ
blackLeftTheGame=Хурисем вӑййинчен тухрӗҫ
computerAnalysis=Компйутӑр тишкерӗвӗ
analysis=Тишкерӳ хӑми
blunders=Тӳрккес йӑнӑшсем
mistakes=Йӑнӑшсем
inaccuracies=Кӳрӗнмелле йӑнӑшсем
flipBoard=Хӑмана валтӑрт
claimADraw=Парма-паршӑн пӗлтер
offerDraw=Парма-пар сӗн
draw=Парма-пар
nbConnectedPlayers=%s вӑйӑҫӑ
gamesBeingPlayedRightNow=Халь пыракан вӑйӑсем
viewAllNbGames=%s Вӑйӑ
viewNbCheckmates=%s Мат туни
nbBookmarks=%s Картса хуни
nbPopularGames=%s Йуратнӑ вӑйӑ
nbAnalysedGames=%s Тишкернӗ вӑйӑ
bookmarkedByNbPlayers=%s Вӑйӑҫӑ картса хунӑ
logOut=Тух
signIn=Кӗ¨р
youNeedAnAccountToDoThat=Куна унӑ тумашкӑн аккаунт кирлӗ
signUp=Регистратсилен
games=Вӑйӑсем
forum=Форӑм
xPostedInForumY=%s усӑҫ %s темӑра ҫырнӑ
players=Вӑйӑҫсем
minutesPerSide=Минут хисепӗ
variant=Вӑйӑ тӗсӗ
variants=Вӑйӑ тӗсӗ
timeControl=Вӑхӑт ӗнерни
realTime=Халех
correspondence=Вӑрах
oneDay=Пӗр кун
nbDays=%s кун
nbHours=%s сехет
time=Вӑхӑт
rating=Рейтинг
ratingStats=Рейтинг графӗкӗсем
username=Усӑҫ йачӗ
usernameOrEmail=Усӑҫ йачӗ йе ел. пушти
password=Кӗме сӑмах
haveAnAccount=Аккаунт пур-и?
changePassword=Кӗме сӑмах улӑштар
changeEmail=Ел. пуштӑ улӑштар
email=Ел. пуштӑ
forgotPassword=Кӗме сӑмаха мансан
rank=Хаклав
cancel=Пӑрахӑҫла
timeOut=Вӑхӑтран пӗтни
whiteOffersDraw=Шуррисем парма-паршӑн
blackOffersDraw=Хурисем парма-паршӑн
whiteDeclinesDraw=Шуррисем парма-паршӑн мар
blackDeclinesDraw=Хурисем парма-паршӑн мар
yourOpponentOffersADraw=Сан ӑмӑртуҫӑ парма-паршӑн
accept=Йышӑн
decline=Сир
playingRightNow=Халлех пыракан
finished=Вӗҫленнисем
abortGame=Вӑййине пӑрахӑҫла
gameAborted=Вӑййине пӑрахӑҫланӑ
standard=Стандартла
unlimited=Чикӗсӗр
mode=Хаклани
casual=Ахаль
rated=Хаклавшӑн
rematch=Реванш ыйт
rematchOfferSent=Реванш ыйтни йанӑ
rematchOfferAccepted=Реванш ыйтнине йышӑннӑ
rematchOfferCanceled=Реванш ыйтнине пӑрахӑҫланӑ
rematchOfferDeclined=Реванш ыйтнине сирнӗ
cancelRematchOffer=Реванш ыйтнине пӑрахӑҫла
play=Выљасси
inbox=Ҫырусем
chatRoom=Калаҫу ани
spectatorRoom=Сӑнавҫӑ ани
composeMessage=Ҫыру ҫыр
noNewMessages=Ҫӗнӗ ҫырусӑр
recipient=Илекенни
send=Йар
incrementInSeconds=Ҫеккунт инкременчӗ
spectators=Сӑнавҫӑсем:
nbWins=%s ҫӗнтерӳ
nbLosses=%s парӑну
nbDraws=%s парма-пар
exportGames=Вӑйӑ еспортласси
ratingRange=Рейтинг хушши
giveNbSeconds=%s ҫеккунт пар
takeback=Тавӑрасси
proposeATakeback=Тавӑрасси ыйт
takebackPropositionSent=Тавӑрасси ыйтнине йанӑ
takebackPropositionDeclined=Тавӑрасси ыйтнине сирнӗ
takebackPropositionAccepted=Тавӑрасси ыйтнине йышӑннӑ
takebackPropositionCanceled=Тавӑрасси ыйтнине пӑрахӑҫланӑ
yourOpponentProposesATakeback=Ӑмӑртуҫу тавӑрассишӗн тӑрать
bookmarkThisGame=Вӑййине картса хур
search=Шыра
advancedSearch=Анлӑ шырасси
tournament=Турнир
tournaments=Турнирсем
tournamentPoints=Турнирти пӑнчӑсем
teams=Ушкӑнсем
nbMembers=%s пайташ
allTeams=Мӗн пур ушкӑн
newTeam=Ҫӗнӗ ушкӑн
myTeams=Манӑн ушкӑнсем
joinTeam=Ушкӑна хутшӑн
quitTeam=Ушкӑнтан тух
joiningPolicy=Хутшӑнасси шыв-йӗрки
teamLeader=Ушкӑн пуҫӗ
teamBestPlayers=Чи вӑйлӑ вӑйӑҫӑ
xCreatedTeamY=%s усӑҫ %s ушкӑн йӗркеленӗ
location=Вырӑн
filterGames=Вӑйӑ али
apply=Ҫирӗплет
leaderboard=Лидӗрсен йышӗ
continueFromHere=Кунтан малалла тӑс
importGame=Вӑйӑ импортласси
nbImportedGames=%s Импортланӑ вӑйӑ
thisIsAChessCaptcha=Ку вӑл CAPTCHA шахматла.
notACheckmate=Мат туни мар
colorPlaysCheckmateInOne=%s ҫӳреҫҫӗ; пӗрре ҫӳресе мат ту
retry=Тепӗр хут
onlineFriends=Тетелти тус
noFriendsOnline=Тетелти тус ҫук
findFriends=Тус тупар
follow=Ҫырӑн
following=Ҫырӑнса тӑни
unfollow=Ҫырӑнма пӑрах
block=Чарса ларт
blocked=Чарса лартнӑ
unblock=Чарӑва сир
followsYou=Сан ҫине ҫырӑнса тӑрать
xStartedFollowingY=%s -> %s ҫине ҫырӑннӑ
nbFollowers=%s ҫырӑнни
more=Тата
player=Вӑйӑҫӑ
list=Тапӑл
graph=Графӗк
textIsTooShort=Ытлӑ та кӗске текст.
textIsTooLong=Ытлӑ та вӑрӑм текст.
openTournaments=Уҫӑ турнир
duration=Вӑрӑмӑш
winner=Ҫӗнтерӳҫӗ
createANewTournament=Ҫӗнӗ турнир йӗркеле
join=Хутшӑнас
points=Пӑнчӑ шучӗ
wins=Ҫӗнтерӳ шучӗ
losses=Парӑну шучӗ
winStreak=Ҫӗнтерӳ йарӑмӗ
boardEditor=Хӑма хайласси
startPosition=Пуҫламӑш поҫитси
clearBoard=Хӑмана тасат
savePosition=Поҫитсине упра
loadPosition=Поҫитсине ларт
isPrivate=Харкам
reportXToModerators=Модератӑра %s ҫинчен пӗлтер
profile=Профӗлӳ
editProfile=Профӗлне тӳрлет
firstName=Йат
lastName=Хушамат
biography=Биографи
country=Ҫӗр-шыв
preferences=Ӗнерлев
watchLichessTV=Lichess TV кур
onlinePlayers=Тетелти вӑйӑҫӑ
activeToday=Пайан тимлисем
activePlayers=Тимлӗ вӑйӑҫӑ
training=Хӑнӑхтару
yourPuzzleRatingX=Санӑн лартӑм рейтингӗ: %s
findTheBestMoveForWhite=Шуррисемшӗн чи ҫӳрени туп.
findTheBestMoveForBlack=Хурисемшӗн чи ҫӳрени туп.
recentlyPlayedPuzzles=Кайранхи выљанӑ лартӑм
puzzleId=%s. лартӑм
puzzleOfTheDay=Пайанхи лартӑм
clickToSolve=Шутлас тесен чӑкӑл ту
goodMove=Лайӑх ҫӳрени
bestMove=Чи аван ҫӳрени!
victory=Ҫӗнтерӳ!
giveUp=Парӑн
thankYou=Тавах!
ratingX=Рейтинг: %s
playedXTimes=%s хут выљанӑ
fromGameLink=%s вӑйӑран
startTraining=Вӗренев пуҫла
retryThisPuzzle=Лартӑм
thisPuzzleIsCorrect=Ку лартӑм тӗрӗс тата кӑсӑклӑ
thisPuzzleIsWrong=Ку лартӑм тӗрӗс мар йе йӑлӑхтармӑш
nbGamesInPlay=%s вӑйӑ пырать
puzzles=Лартӑмсем
coordinates=Координатсем
openings=Пуҫламӑшсем
tournamentWinners=Турнирти ҫӗнтерӳҫсем
name=Йат
no=Ҫук
yes=Ара
help=Пулӑшу:
createANewTopic=Ҫӗнӗ темӑ пуҫлӑ
topics=Темӑсем
posts=Ҫыравсем
lastPost=Кайранхи ҫырав
replies=Хурав шучӗ
replyToThisTopic=Темине хуравлани
reply=Хуравлас
message=Ҫыру
createTheTopic=Темӑ пуҫла
reportAUser=Усӑҫ ҫине елекле
user=Усӑҫ
reason=Сӑлтав
whatIsIheMatter=Сӑлтава кӑтарт
cheat=Ултав
insult=Кӳрентерӳ
troll=Троллев
other=Ытти
by=%s тунӑ
thisTopicIsNowClosed=Ҫак темӑ хупӑннӑ.
theming=Калӑплани
donate=Укҫапа пулӑшни
blog=Блог
questionsAndAnswers=Ыйту-Хурав
notes=Сӑнӑм
typePrivateNotesHere=Кунта харпӑр сӑнӑм ҫыр
gameDisplay=Вӑйӑ курӑнни
materialDifference=Материаллӑ уйрӑмлӑх
closeAccount=Аккаунта хуп
closeYourAccount=Хӑвӑн аккаунтна хупни
thisAccountIsClosed=Ку аккаунт хупӑ.
invalidUsernameOrPassword=Тӗрӗс мар йатлӑх йе кӗме сӑмах
emailMeALink=Пуштӑм ҫине каҫӑ йартар
currentPassword=Хальхи кӗме сӑмах
newPassword=Ҫӗнӗ кӗме сӑмах
newPasswordAgain=Ҫӗнӗ кӗме сӑмах (тепӗр хут)
boardHighlights=Утӑм ҫути (кайранхи утӑм тата шах туни)
pieceDestinations=Кӗлетке сукмакӗ (тӗрӗс утни тата умӗн утни)
boardCoordinates=Хӑма координачӗсем (A-H, 1-8)
chessClock=Шахмат сехечӗ
tenthsOfSeconds=Ҫеккунтӑн вуннӑмӗшӗсем
never=Нихӑҫан
whenTimeRemainingLessThanTenSeconds=Вӑхӑт < 10 ҫеккунтран йулсан
horizontalGreenProgressBars=Горисонталлӑ симӗс йӗрсем
soundWhenTimeGetsCritical=Вӑхӑт пӗтсех пырсан сасӑпа пӗлтер
privacy=Вӑрттӑнлӑх
sound=Сасӑ
none=Унсӑр
fast=Хӑвӑрт
normal=Виҫеллӗ
slow=Вӑрах
insideTheBoard=Хӑма ӑшӗнче
outsideTheBoard=Хӑма тулӗнче
onSlowGames=Вӑрах вӑйӑра
always=Йалан
difficultyEasy=Ансат
difficultyNormal=Виҫеллӗ
difficultyHard=Кӑткӑс
xCompetesInY=%s усӑҫ %s хутшӑнать
xAskedY=%s усӑҫ %s ыйтнӑ
xAnsweredY=%s усӑҫ %s хуравланӑ
xCommentedY=%s усӑҫ %s шухӑш хушнӑ
seeAllTournaments=Пур турнира пӑх
yourCityRegionOrDepartment=Санӑн хула, тӑрӑх йе регион.
maximumNbCharacters=Чи нумай: %s паллӑ.
blocks=%s чарса лартни
human=Етем
computer=Компйутӑр
side=Тӗс
clock=Сехет
playOfflineComputer=Компйутӑр
opponent=Ӑмӑртуҫӑ
learn=Вӗренӳ
community=Пӗрлешӳ
tools=Мехел
increment=Инкремент
board=Хӑма
sharePGN=PGN пайлаш
shareGameURL=Вӑйӑ каҫҫине пайлаш
error.required=Тивӗҫлӗ ани
onlyFriends=Туссем кӑна
menu=Менӳ
castling=Роклани
whiteCastlingKingside=Шурӑ O-O
whiteCastlingQueenside=Шурӑ O-O-O
blackCastlingKingside=Хура O-O
blackCastlingQueenside=Хура O-O-O
nbForumPosts=%s Форӑмра Ҫырни
watchGames=Вӑййисене тишкер
watch=Тишкерӳ
internationalEvents=Халӑх хушшинчи пулӑмсем
videoLibrary=Курма пуххи
mobileApp=Мобиллӗ хушӑм
webmasters=Веб маҫтӑрсем
contribute=Пулӑшасси
contact=Ҫыхӑну
termsOfService=Шыв-йӗрки
simultaneousExhibitions=Пӗр вӑхӑтри вӑйӑсем
host=Хост
create=Йӗркелес
lichessTournaments=Lichess турнирӗсем
newTournament=Ҫӗнӗ турнир
tournamentNotFound=Турнир тупӑнман
checkYourEmail=Ел. пуштуна тӗрӗсле
kidMode=Ача решимӗ
phoneAndTablet=Телефон тата планшет
bulletBlitzClassical=Bullet, blitz, classical
correspondenceAndUnlimited=Вӑрах тата чикӗсӗр
viewTheSolution=Тупсӑмне тишкер

View file

@ -5,7 +5,7 @@ gameOver=Τέλος παιχνιδιού
waitingForOpponent=Αναμονή για αντίπαλο
waiting=Αναμονή
yourTurn=Η σειρά σας
aiNameLevelAiLevel=%s %sου επιπέδου
aiNameLevelAiLevel=%s επιπέδου %s
level=Επίπεδο
toggleTheChat=Εναλλαγή προβολής της συνομιλίας
toggleSound=Εναλλαγή ήχου
@ -33,15 +33,15 @@ makeYourOpponentResign=Αναγκάστε τον αντίπαλο σας να π
forceResignation=Ισχυριστείτε νίκη
forceDraw=Δηλώστε ισοπαλία
talkInChat=Παρακαλούμε να είστε ευγενικοί στη συνομιλία!
theFirstPersonToComeOnThisUrlWillPlayWithYou=Ο πρώτος που επισκεφθεί αυτήν τη διεύθυνση θα παίξει μαζί σας.
theFirstPersonToComeOnThisUrlWillPlayWithYou=Ο πρώτος που θα επισκεφθεί αυτήν τη διεύθυνση θα παίξει μαζί σας.
whiteResigned=Τα λευκά παραιτήθηκαν
blackResigned=Τα μαύρα παραιτήθηκαν
whiteLeftTheGame=Τα λευκά έφυγαν από το παιχνίδι
blackLeftTheGame=Τα μαύρα έφυγαν από το παιχνίδι
shareThisUrlToLetSpectatorsSeeTheGame=Στείλτε αυτήν τη διεύθυνση και επιτρέψτε σε θεατές να δουν το παιχνίδι
theComputerAnalysisHasFailed=Η υπολογιστική ανάλυση απέτυχε
theComputerAnalysisHasFailed=Η ανάλυση του υπολογιστή απέτυχε
viewTheComputerAnalysis=Δείτε την ανάλυση του υπολογιστή
requestAComputerAnalysis=Ζητήστε υπολογιστική ανάλυση
requestAComputerAnalysis=Ζητήστε την ανάλυση του υπολογιστή
computerAnalysis=Ανάλυση υπολογιστή
analysis=Πίνακας ανάλυσης
blunders=Σοβαρά λάθη

View file

@ -169,7 +169,7 @@ tournamentPoints=Puntos de torneo
viewTournament=Ver torneo
backToTournament=Regresar al torneo
backToGame=Volver a la partida
freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents=Ajedrez en línea gratis. Juega ajedrez con una interfaz limpia. Sin registrarse, sin publicidad, sin necesidad de plugins. Juega ajedrez contra el ordenador, amigos u oponentes aleatorios.
freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents=Ajedrez en línea gratis. Juega ajedrez en una interfaz limpia. Sin registrarse, sin publicidad, sin plugins. Juega ajedrez contra el ordenador, amigos u oponentes aleatorios.
teams=Equipos
nbMembers=%s miembros
allTeams=Todos los equipos
@ -531,5 +531,5 @@ onlineAndOfflinePlay=Jugar en línea o fuera de línea
correspondenceAndUnlimited=Correspondencia y sin límite de tiempo
viewTheSolution=Ver la solución
followAndChallengeFriends=Seguir y retar a amigos
availableInNbLanguages=Disponible en %s lenguajes!
availableInNbLanguages=¡Disponible en %s lenguajes!
gameAnalysis=Análisis de juego

View file

@ -489,7 +489,7 @@ newTournament=Nouveau tournoi
tournamentHomeTitle=Tournoi réunissant plusieurs variantes et cadences
tournamentHomeDescription=Jouez des tournois d'échecs palpitants ! Rejoignez un tournoi officiel programmé ou créez le vôtre. Bullet, Blitz, Classique, Chess960, King of the Hill, Threecheck et d'autres options sont disponibles pour s'amuser sans fin.
tournamentNotFound=Tournoi inexistant
tournamentDoesNotExist=Ce tournoi n'existe pas
tournamentDoesNotExist=Ce tournoi n'existe pas.
tournamentMayHaveBeenCanceled=Il a peut-être été annulé, si tous les joueurs l'ont quitté avant le début.
returnToTournamentsHomepage=Retour à la page des tournois
weeklyPerfTypeRatingDistribution=Distribution des classements mensuels en %s
@ -497,10 +497,10 @@ nbPerfTypePlayersThisWeek=%s joueurs de %s ce mois-ci.
yourPerfTypeRatingisRating=Votre classement en %s est de %s.
youAreBetterThanPercentOfPerfTypePlayers=Vous êtes meilleur que %s des joueurs de %s.
youDoNotHaveAnEstablishedPerfTypeRating=Vous n'avez pas de classement établi en %s.
checkYourEmail=Vérifiez votre courriel
checkYourEmail=Vérifiez vos courriels
weHaveSentYouAnEmailClickTheLink=Nous vous avons envoyé un courriel. Visitez le lien qui s'y trouve pour activer votre compte.
ifYouDoNotSeeTheEmailCheckOtherPlaces=Si vous ne trouvez pas le courriel dans votre boîte de réception, vérifiez dans vos courriels indésirables ou autres dossiers.
areYouSureYouEvenRegisteredYourEmailOnLichess=Êtes vous certain d'avoir enregistré votre adresse électronique sur lichess?
areYouSureYouEvenRegisteredYourEmailOnLichess=Êtes vous certain d'avoir enregistré votre adresse électronique sur lichess ?
itWasNotRequiredForYourRegistration=Elle n'était pas nécessaire pour vous enregistrer initialement.
weHaveSentYouAnEmailTo=Nous avons envoyé un courriel à %s. Cliquez le lien qui s'y trouve pour réinitialiser votre mot de passe.
byRegisteringYouAgreeToBeBoundByOur=En vous inscrivant, vous acceptez d'être lié à notre %s.
@ -516,7 +516,7 @@ pressShiftPlusClickOrRightClickToDrawCirclesAndArrowsOnTheBoard=Maj+clic ou clic
confirmResignation=Confirmer l'abandon
letOtherPlayersMessageYou=Permettre à d'autres joueurs de vous envoyer des messages
shareYourInsightsData=Partager les statistiques générées par lichess de vos parties
youHaveAlreadyRegisteredTheEmail=Cet email est deja associé à un compte existant: %s
youHaveAlreadyRegisteredTheEmail=Cet email est deja associé à un compte existant : %s
kidMode=Mode enfant
playChessEverywhere=Jouer aux échecs partout
asFreeAsLichess=Aussi libre que lichess

View file

@ -498,3 +498,9 @@ yourPerfTypeRatingisRating=आपकी %s रेटिंग %s है
youAreBetterThanPercentOfPerfTypePlayers=आप %s खेलने वालो से बेहतर हो, %s मे
youDoNotHaveAnEstablishedPerfTypeRating=आपके पास स्थापित %s रेटिंग नहीं है
checkYourEmail=अपना ई-मेल देखें
onlineAndOfflinePlay=ऑनलाइन और ऑफ़लाइन खेलने के लिए
correspondenceAndUnlimited=पत्राचार और असीमित
viewTheSolution=समाधान देखें
followAndChallengeFriends=अपने दोस्तों को चुनौती
availableInNbLanguages=%s भाषाओं में उपलब्ध है !
gameAnalysis=खेल अध्ययन

View file

@ -241,6 +241,7 @@ withdraw=ti'ekla
points=lo kelnemka'u
wins=lo nu jinga
losses=lo nu cirko
winStreak=lo se pormei be lo te lamji be lo nunji'a
createdBy=fi'e la'oi
tournamentIsStarting=le torneio ca'o cfari
membersOnly=lo cmima be la'o gy. lichess .gy cu selte'i
@ -350,4 +351,8 @@ closeYourAccount=co'upli lo do jaspu
changedMindDoNotCloseAccount=mi seljdicne .i na ku gau ko co'upli lo mi jaspu
closeAccountExplanation=xu do birti lo ka djica lo nu co'upli lo do jaspu .i vitno nu jdice .i ba ki ri ze'e na pilno .i lo do jaspu velski cu na jai zgana se zifre
thisAccountIsClosed=co'upli ti poi jaspu
invalidUsernameOrPassword=.i lo cmene .a lo japyfla cu na drani
currentPassword=lo cabna japyfla
newPassword=lo cfari japyfla
newPasswordAgain=lo ke'u cfari japyfla
side=le se skari

View file

@ -28,3 +28,23 @@ whiteResigned=Ак түстөгү оюнчу утулду
blackResigned=Кара түстөгү оюнчу утулду
whiteLeftTheGame=Ак түстөгү оюнчу оюндан чыгып кетти
blackLeftTheGame=Кара түстөгү оюнчу оюндан чыгып кетти
viewTheComputerAnalysis=Компьютердик анализди көрүү
requestAComputerAnalysis=Компьютердик анализ суроо
computerAnalysis=Компьютердик анализ
analysis=Досканы анализдөө
blunders=Одоно ката
mistakes=Жаңылыштык
inaccuracies=Так эместик
nbConnectedPlayers=%s оюнчу
viewAllNbGames=%s Оюн
viewNbCheckmates=%s Мат
nbPopularGames=%s Популярдуу оюн
nbAnalysedGames=%s Анализден өткөн оюн
playOfflineComputer=Компьютер
opponent=Өнөктөш
learn=Үйрөнүү
community=Коом
tools=Инструменттер
board=Доска
playOnline=Онлайн оноо
playOffline=Оффлайн ойноо

View file

@ -497,3 +497,39 @@ nbPerfTypePlayersThisWeek=%s %s pL@y3r5 7h15 w33k.
yourPerfTypeRatingisRating=Y0uR %s r4T1ng i$ %s
youAreBetterThanPercentOfPerfTypePlayers=Y0u @r3 b3773R 7h4n %s 0f %s pL4y3r5.
youDoNotHaveAnEstablishedPerfTypeRating=Y0u d0 n07 h@v3 @n 3s7@bL15h3d %s r@71nG.
checkYourEmail=Ch3Ck ur 3m@iL
weHaveSentYouAnEmailClickTheLink=W3'v3 $3n7 U @n 3m@iL
ifYouDoNotSeeTheEmailCheckOtherPlaces=iF U d0N'7 C th3 3m@iL, cH3ck 07H3r pL@c3$ i7 miGHt B, LIk3 ur JuNk, $p@M, $0cI@L, 0R 07h3R f0LD3r$.
areYouSureYouEvenRegisteredYourEmailOnLichess=R u $ur3 u 3V3N r3GI$73r3d ur 3m@iL 0N LiCHe$5?
itWasNotRequiredForYourRegistration=i7 w@$ n07 r3QuIr3d 4 ur r3GI$7r@7I0n.
weHaveSentYouAnEmailTo=W3'v3 s3n7 @n 3m@iL 2 %s. cLiK th3 LInK iN th3 3m@iL 2 r3$37 ur p@$$w0rd.
byRegisteringYouAgreeToBeBoundByOur=BY r3GI$73rIng, u @gr33 2 B b0Und bY 0Ur %s.
networkLagBetweenYouAndLichess=N37w0rk l@g b37w33n U @nd LiCHe$5
timeToProcessAMoveOnLichessServer=Tim3 2 pr0c3$$ a m0v3 0n LiCHe$5 $3rv3r
downloadAnnotated=D0wnL0@d @nN07@73d
downloadRaw=D0wnL0@d r@w
downloadImported=D0wnL0@d iMp0r73d
printFriendlyPDF=PriN7-frI3NdLy PDF
crosstable=Cr0$$7@bL3
youCanAlsoScrollOverTheBoardToMoveInTheGame=U c@n @ls0 sCr0LL 0v3r th3 b0@rd 2 m0v3 iN th3 g@m3.
pressShiftPlusClickOrRightClickToDrawCirclesAndArrowsOnTheBoard=Pr3$$ $hIf7+cLiK 0r rIGh7-cLiK 2 dr@w cIrcL3s @nd @rr0ws 0n th3 b0@rd.
confirmResignation=C0nfIrm r3$Ign@7I0n
letOtherPlayersMessageYou=L37 07h3r pL@y3rs m3$$@g3 U
shareYourInsightsData=$h@r3 ur in$IGh7s d@7@
youHaveAlreadyRegisteredTheEmail=U h@v3 @Lr3@dy r3gI$73r3d th3 3m@iL: %s
kidMode=KiD m0d3
playChessEverywhere=PL@y ch3$$ 3v3rywH3r3
asFreeAsLichess=@$ fr33 @$ LiCHe$5
builtForTheLoveOfChessNotMoney=BuiL7 4 th3 L0v3 0f ch3$$, n07 m0n3y
everybodyGetsAllFeaturesForFree=3v3ryb0dy g3t$ @LL f3@7ur3s 4 fr33
zeroAdvertisement=Z3r0 @dv3rtI$3m3n7
fullFeatured=FuLL f3@7ur3d
phoneAndTablet=Ph0n3 @nd 7@bL37
bulletBlitzClassical=BuLL37, bLi7z, cL@$$ic@L
correspondenceChess=C0rr3$p0nd3nc3 ch3$$
onlineAndOfflinePlay=0nLin3 @nd 0ffLin3 pL@y
correspondenceAndUnlimited=C0rr3$p0nd3nc3 @nd unLimi73d
viewTheSolution=Vi3w th3 $0Lu7i0n
followAndChallengeFriends=F0LL0w @nd ch@LL3ng3 fri3nd$
availableInNbLanguages=@v@iL@bL3 iN %s L@ngu@g3$!
gameAnalysis=G@m3 @n@Ly$i$

View file

@ -531,5 +531,5 @@ onlineAndOfflinePlay=Igranje s povezavo ali brez povezave
correspondenceAndUnlimited=korespondenčno in neomejeno
viewTheSolution=Poglej rešitev
followAndChallengeFriends=Sledi in izzovi prijatelje
availableInNbLanguages=Razpoložljivo v %s jezikih!
availableInNbLanguages=Na voljo v %s jezikih!
gameAnalysis=Analiza igre

View file

@ -19,7 +19,9 @@ randomColor=ஏதாவது ஒரு நிறம்
createAGame=ஆட்டமொன்றை உருவாக்கு
whiteIsVictorious=வெள்ளை வென்றது
blackIsVictorious=கருப்பு வென்றது
kingInTheCenter=மையத்தில் கிங்
threeChecks=மூன்று காசோலைகளை
raceFinished=ரேஸ் முடிந்ததும்
newOpponent=புது எதிராளி
yourOpponentWantsToPlayANewGameWithYou=நும் எதிராளி நும்மோடு புது ஆட்டமாட விரும்புகிறார்
joinTheGame=ஆட்டத்திலே சேர்
@ -312,6 +314,7 @@ typePrivateNotesHere=இரகசிய குறிப்புகள் இங
closeAccount=கணக்கு மூடவும்
closeYourAccount=உங்கள் கணக்கு மூடவும்
changedMindDoNotCloseAccount=என் மனதை மாற்றிநேன் என் கணக்கு மூட வேண்டாம்
chessClock=செஸ் கடிகாரம்
never=ஒருபோதும்
sound=சப்தம்
fast=விரைவாக

View file

@ -86,7 +86,8 @@ private[i18n] object Contributors {
"tl" -> List("Curlaub"),
"fy" -> List("FishingCat"),
"jb" -> List("username05"),
"tg" -> List("mondayguy"))
"tg" -> List("mondayguy"),
"cv" -> List("pentille"))
def apply(code: String): List[String] = ~(all get code)
}

View file

@ -12,14 +12,13 @@ final class PublicChat(
def tournamentChats: Fu[List[(Tournament, UserChat)]] =
tournamentApi.fetchVisibleTournaments.flatMap {
visibleTournaments =>
val tournamentList = sortTournamentsByRelevance(visibleTournaments.all)
val ids = tournamentList.map(_.id)
val ids = visibleTournaments.all.map(_.id)
chatApi.userChat.findAll(ids).map {
chats =>
chats.map { chat =>
tournamentList.find(_.id === chat.id).map(tour => (tour, chat))
visibleTournaments.all.find(_.id === chat.id).map(tour => (tour, chat))
}.flatten
}
} map sortTournamentsByRelevance
}
def simulChats: Fu[List[(Simul, UserChat)]] =
@ -37,7 +36,7 @@ final class PublicChat(
private def fetchVisibleSimuls: Fu[List[Simul]] = {
simulEnv.allCreated(true) zip
simulEnv.repo.allStarted zip
simulEnv.repo.allFinished(5) map {
simulEnv.repo.allFinished(3) map {
case ((created, started), finished) =>
created ::: started ::: finished
}
@ -46,6 +45,6 @@ final class PublicChat(
/**
* Sort the tournaments by the tournaments most likely to require moderation attention
*/
private def sortTournamentsByRelevance(tournaments: List[Tournament]): List[Tournament] =
tournaments.sortBy(-_.nbPlayers)
private def sortTournamentsByRelevance(tournaments: List[(Tournament, UserChat)]) =
tournaments.sortBy(-_._1.nbPlayers)
}

View file

@ -35,11 +35,12 @@ final class PlaybanApi(
private def IfBlameable[A: ornicar.scalalib.Zero](game: Game)(f: => Fu[A]): Fu[A] =
blameable(game) flatMap { _ ?? f }
def abort(pov: Pov): Funit = IfBlameable(pov.game) {
def abort(pov: Pov, isOnGame: Set[Color]): Funit = IfBlameable(pov.game) {
{
if (pov.game olderThan 45) pov.game.playerWhoDidNotMove map { Blame(_, Outcome.NoPlay) }
if (pov.game olderThan 30) pov.game.playerWhoDidNotMove map { Blame(_, Outcome.NoPlay) }
else if (pov.game olderThan 15) none
else pov.player.some map { Blame(_, Outcome.Abort) }
else if (isOnGame(pov.opponent.color)) pov.player.some map { Blame(_, Outcome.Abort) }
else none
} ?? {
case Blame(player, outcome) => player.userId.??(save(outcome))
}

View file

@ -140,7 +140,8 @@ final class Env(
notifier = notifier,
playban = playban,
bus = system.lilaBus,
casualOnly = CasualOnly)
casualOnly = CasualOnly,
getSocketStatus = getSocketStatus)
private lazy val rematcher = new Rematcher(
messenger = messenger,

View file

@ -18,10 +18,13 @@ private[round] final class Finisher(
notifier: RoundNotifier,
crosstableApi: lila.game.CrosstableApi,
bus: lila.common.Bus,
casualOnly: Boolean) {
casualOnly: Boolean,
getSocketStatus: Game.ID => Fu[actorApi.SocketStatus]) {
def abort(pov: Pov)(implicit proxy: GameProxy): Fu[Events] = apply(pov.game, _.Aborted) >>- {
playban.abort(pov)
getSocketStatus(pov.gameId) foreach { ss =>
playban.abort(pov, ss.colorsOnGame)
}
bus.publish(AbortedBy(pov), 'abortGame)
}

View file

@ -91,6 +91,7 @@ case class SocketStatus(
blackIsGone: Boolean) {
def onGame(color: Color) = color.fold(whiteOnGame, blackOnGame)
def isGone(color: Color) = color.fold(whiteIsGone, blackIsGone)
def colorsOnGame: Set[Color] = Color.all.filter(onGame).toSet
}
case class SetGame(game: Option[lila.game.Game])

View file

@ -0,0 +1,51 @@
package lila.security
import play.api.mvc.Results.Forbidden
import play.api.mvc.{ Action, RequestHeader, Result }
import lila.common.HTTPRequest._
final class CSRFRequestHandler(domain: String) {
private def logger = lila.log("csrf")
def check(req: RequestHeader): Boolean = {
if (isXhr(req) || (isSafe(req) && !isSocket(req))) true
else origin(req).orElse(referer(req) flatMap refererToOrigin) match {
case None =>
lila.mon.http.csrf.missingOrigin()
logger.debug(print(req))
true
case Some("file://") =>
true
case Some(o) if isSubdomain(o) =>
true
case Some(_) =>
if (isSocket(req)) {
lila.mon.http.csrf.websocket()
logger.info(s"WS ${print(req)}")
}
else {
lila.mon.http.csrf.forbidden()
logger.info(print(req))
}
false
}
}
private val topDomain = s"://$domain"
private val subDomain = s".$domain"
// origin = "https://en.lichess.org"
// domain = "lichess.org"
private def isSubdomain(origin: String) =
origin.endsWith(subDomain) || origin.endsWith(topDomain)
// input = "https://en.lichess.org/some/path?a=b&c=d"
// output = "https://en.lichess.org"
private val RefererToOriginRegex = """^([^:]+://[^/]+).*""".r // a.k.a. pokemon face regex
private def refererToOrigin(r: String): Option[String] = r match {
case RefererToOriginRegex(origin) => origin.some
case _ => none
}
}

View file

@ -1,6 +1,5 @@
package lila.security
import play.api.libs.json._
import play.api.libs.ws.WS
import play.api.Play.current
@ -14,7 +13,7 @@ final class DisposableEmailDomain(
private[security] def refresh {
WS.url(providerUrl).get() map { res =>
setDomains(res.json)
setDomains(textToDomains(res.body))
lila.mon.email.disposableDomain(matchers.size)
} recover {
case _: java.net.ConnectException => // ignore network errors
@ -22,9 +21,8 @@ final class DisposableEmailDomain(
}
}
private[security] def setDomains(json: JsValue): Unit = try {
val ds = json.as[List[String]]
matchers = ds.map { d =>
private[security] def setDomains(domains: List[String]): Unit = try {
matchers = domains.map { d =>
val regex = s"""(.+\\.|)${d.replace(".", "\\.")}"""
makeMatcher(regex)
}
@ -34,6 +32,9 @@ final class DisposableEmailDomain(
case e: Exception => onError(e)
}
private[security] def textToDomains(text: String): List[String] =
text.lines.map(_.trim).filter(_.nonEmpty).toList
private var failed = false
private def onError(e: Exception) {

View file

@ -36,11 +36,18 @@ final class EmailAddress(disposable: DisposableEmailDomain) {
def isValid(email: String) = validate(email).isDefined
private def isTakenBy(email: String, forUser: Option[User]): Option[String] = validate(email) ?? { e =>
/**
* Returns true if an E-mail address is taken by another user.
* @param email The E-mail address to be checked
* @param forUser Optionally, the user the E-mail address field is to be assigned to.
* If they already have it assigned, returns false.
* @return
*/
private def isTakenBySomeoneElse(email: String, forUser: Option[User]): Boolean = validate(email) ?? { e =>
(lila.user.UserRepo.idByEmail(e) awaitSeconds 2, forUser) match {
case (None, _) => none
case (Some(userId), Some(user)) => userId != user.id option userId
case (someUserId, _) => someUserId
case (None, _) => false
case (Some(userId), Some(user)) => userId != user.id
case (_, _) => true
}
}
@ -50,10 +57,9 @@ final class EmailAddress(disposable: DisposableEmailDomain) {
}
def uniqueConstraint(forUser: Option[User]) = Constraint[String]("constraint.email_unique") { e =>
isTakenBy(e, forUser) match {
case Some(userId) => Invalid(ValidationError(s"Email already in use by $userId"))
case None => Valid
}
if (isTakenBySomeoneElse(e, forUser))
Invalid(ValidationError(s"Email address is already in use by another account"))
else Valid
}
private val gmailDomains = Set("gmail.com", "googlemail.com")

View file

@ -45,6 +45,7 @@ final class Env(
val RecaptchaPrivateKey = config getString "recaptcha.private_key"
val RecaptchaEndpoint = config getString "recaptcha.endpoint"
val RecaptchaEnabled = config getBoolean "recaptcha.enabled"
val NetDomain = config getString "net.domain"
}
import settings._
@ -110,6 +111,8 @@ final class Env(
lazy val api = new Api(storeColl, firewall, geoIP, emailAddress)
lazy val csrfRequestHandler = new CSRFRequestHandler(NetDomain)
def cli = new Cli
private[security] lazy val storeColl = db(CollectionSecurity)

View file

@ -6,7 +6,7 @@ import play.api.libs.json._
class DisposableEmailDomainTest extends Specification {
val d = new DisposableEmailDomain("", None)
d.setDomains(Json.parse(Fixtures.json))
d.setDomains(d.textToDomains(Fixtures.text))
"disposable email domain" should {
"simple" in {

File diff suppressed because it is too large Load diff

View file

@ -17,16 +17,19 @@ private final class ChapterMaker(
def apply(study: Study, data: Data, order: Int, userId: User.ID): Fu[Option[Chapter]] = {
data.game.??(parsePov) flatMap {
case None => data.pgn.filter(_.trim.nonEmpty) match {
case Some(pgn) => fromPgn(study, pgn, data, order, userId)
case None => fuccess(fromFenOrBlank(study, data, order, userId).some)
}
case None => fuccess(fromFenOrPgnOrBlank(study, data, order, userId).some)
case Some(pov) => fromPov(study, pov, data, order, userId)
}
}
private def fromPgn(study: Study, pgn: String, data: Data, order: Int, userId: User.ID): Fu[Option[Chapter]] =
PgnImport(pgn).future map { res =>
def fromFenOrPgnOrBlank(study: Study, data: Data, order: Int, userId: User.ID): Chapter =
data.pgn.filter(_.trim.nonEmpty) match {
case Some(pgn) => fromPgn(study, pgn, data, order, userId)
case None => fromFenOrBlank(study, data, order, userId)
}
private def fromPgn(study: Study, pgn: String, data: Data, order: Int, userId: User.ID): Chapter =
PgnImport(pgn).toOption.fold(fromFenOrBlank(study, data, order, userId)) { res =>
Chapter.make(
studyId = study.id,
name = (for {
@ -42,10 +45,10 @@ private final class ChapterMaker(
root = res.root,
order = order,
ownerId = userId,
conceal = data.conceal option Chapter.Ply(res.root.ply)).some
conceal = data.conceal option Chapter.Ply(res.root.ply))
}
def fromFenOrBlank(study: Study, data: Data, order: Int, userId: User.ID): Chapter = {
private def fromFenOrBlank(study: Study, data: Data, order: Int, userId: User.ID): Chapter = {
val variant = data.variant.flatMap(Variant.apply) | Variant.default
(data.fen.map(_.trim).filter(_.nonEmpty).flatMap { fenStr =>
Forsyth.<<<@(variant, fenStr)

View file

@ -9,6 +9,7 @@ object DataForm {
"gameId" -> optional(nonEmptyText),
"orientation" -> optional(nonEmptyText),
"fen" -> optional(nonEmptyText),
"pgn" -> optional(nonEmptyText),
"variant" -> optional(nonEmptyText)
)(Data.apply)(Data.unapply))
@ -16,6 +17,7 @@ object DataForm {
gameId: Option[String] = None,
orientationStr: Option[String] = None,
fenStr: Option[String] = None,
pgnStr: Option[String] = None,
variantStr: Option[String] = None) {
def orientation = orientationStr.flatMap(chess.Color.apply) | chess.White

View file

@ -31,6 +31,8 @@ final class StudyApi(
def byIds = studyRepo byOrderedIds _
def publicByIds(ids: Seq[String]) = byIds(ids) map { _.filter(_.isPublic) }
def byIdWithChapter(id: Study.ID): Fu[Option[Study.WithChapter]] = byId(id) flatMap {
_ ?? { study =>
chapterRepo.byId(study.position.chapterId) flatMap {

View file

@ -16,12 +16,12 @@ private final class StudyMaker(
private def createFromScratch(data: DataForm.Data, user: User): Fu[Study.WithChapter] = fuccess {
val study = Study.make(user, Study.From.Scratch)
val chapter = chapterMaker.fromFenOrBlank(study, ChapterMaker.Data(
val chapter = chapterMaker.fromFenOrPgnOrBlank(study, ChapterMaker.Data(
game = none,
name = "Chapter 1",
variant = data.variantStr,
fen = data.fenStr,
pgn = none,
pgn = data.pgnStr,
orientation = data.orientation.name,
conceal = false,
initial = true),

View file

@ -5,18 +5,20 @@ import lila.user.User
sealed trait Condition {
def apply(getMaxRating: Condition.GetMaxRating)(user: User): Fu[Condition.Verdict]
def name: String
def withVerdict(getMaxRating: Condition.GetMaxRating)(user: User): Fu[Condition.WithVerdict] =
apply(getMaxRating)(user) map { Condition.WithVerdict(this, _) }
def withVerdict(verdict: Condition.Verdict) = Condition.WithVerdict(this, verdict)
override def toString = name
}
object Condition {
trait FlatCond {
def apply(user: User): Condition.Verdict
}
type GetMaxRating = PerfType => Fu[Int]
sealed abstract class Verdict(val accepted: Boolean)
@ -25,16 +27,15 @@ object Condition {
case class WithVerdict(condition: Condition, verdict: Verdict)
case class NbRatedGame(perf: Option[PerfType], nb: Int) extends Condition {
case class NbRatedGame(perf: Option[PerfType], nb: Int) extends Condition with FlatCond {
def apply(getMaxRating: GetMaxRating)(user: User) = fuccess {
def apply(user: User) =
perf match {
case Some(p) if user.perfs(p).nb >= nb => Accepted
case Some(p) => Refused(s"Only ${user.perfs(p).nb} of $nb rated ${p.name} games played")
case None if user.count.rated >= nb => Accepted
case None => Refused(s"Only ${user.count.rated} of $nb rated games played")
}
}
def name = perf match {
case None => s"$nb rated games"
@ -48,34 +49,47 @@ object Condition {
if (user.perfs(perf).provisional) fuccess(Refused(s"Provisional ${perf.name} rating"))
else getMaxRating(perf) map {
case r if r <= rating => Accepted
case r => Refused(s"Top monthly ${perf.name} rating ($r) is too high.")
case r => Refused(s"Top monthly ${perf.name} rating ($r) is too high")
}
def name = s"Rated ≤ $rating in ${perf.name}"
}
case class MinRating(perf: PerfType, rating: Int) extends Condition with FlatCond {
def apply(user: User) =
if (user.perfs(perf).provisional) Refused(s"Provisional ${perf.name} rating")
else if (user.perfs(perf).intRating < rating) Refused(s"Current ${perf.name} rating is too low")
else Accepted
def name = s"Rated ≥ $rating in ${perf.name}"
}
case class All(
nbRatedGame: Option[NbRatedGame],
maxRating: Option[MaxRating]) {
maxRating: Option[MaxRating],
minRating: Option[MinRating]) {
def relevant = list.nonEmpty
def list: List[Condition] = List(nbRatedGame, maxRating).flatten
def list: List[Condition] = List(nbRatedGame, maxRating, minRating).flatten
def ifNonEmpty = list.nonEmpty option this
def withVerdicts(getMaxRating: GetMaxRating)(user: User): Fu[All.WithVerdicts] =
list.map { cond =>
cond.withVerdict(getMaxRating)(user)
list.map {
case c: MaxRating => c(getMaxRating)(user) map c.withVerdict
case c: FlatCond => fuccess(c withVerdict c(user))
}.sequenceFu map All.WithVerdicts.apply
def accepted = All.WithVerdicts(list.map { WithVerdict(_, Accepted) })
def sameMaxRating(other: All) = maxRating.map(_.rating) == other.maxRating.map(_.rating)
def sameMinRating(other: All) = minRating.map(_.rating) == other.minRating.map(_.rating)
}
object All {
val empty = All(nbRatedGame = none, maxRating = none)
val empty = All(nbRatedGame = none, maxRating = none, minRating = none)
case class WithVerdicts(list: List[WithVerdict]) {
def relevant = list.nonEmpty
@ -102,6 +116,7 @@ object Condition {
}
private implicit val NbRatedGameHandler = Macros.handler[NbRatedGame]
private implicit val MaxRatingHandler = Macros.handler[MaxRating]
private implicit val MinRatingHandler = Macros.handler[MinRating]
implicit val AllBSONHandler = Macros.handler[All]
}
@ -165,18 +180,44 @@ object Condition {
val default = MaxRatingSetup(PerfType.Blitz.key, 9999)
def apply(x: MaxRating): MaxRatingSetup = MaxRatingSetup(x.perf.key, x.rating)
}
val minRatings = List(0, 1600, 1800, 1900, 2000, 2100, 2200, 2300, 2400, 2500, 2600)
val minRatingChoices = options(minRatings, "Min rating of %d") map {
case (0, name) => (0, "No restriction")
case x => x
}
val minRating = mapping(
"perf" -> text.verifying(perfChoices.toMap.contains _),
"rating" -> numberIn(minRatingChoices)
)(MinRatingSetup.apply)(MinRatingSetup.unapply)
case class MinRatingSetup(perf: String, rating: Int) {
def isDefined = rating > 0
def convert = isDefined option MinRating(PerfType(perf) err s"perf $perf", rating)
}
object MinRatingSetup {
val default = MinRatingSetup(PerfType.Blitz.key, 0)
def apply(x: MinRating): MinRatingSetup = MinRatingSetup(x.perf.key, x.rating)
}
val all = mapping(
"nbRatedGame" -> nbRatedGame,
"maxRating" -> maxRating
"maxRating" -> maxRating,
"minRating" -> minRating
)(AllSetup.apply)(AllSetup.unapply)
case class AllSetup(nbRatedGame: NbRatedGameSetup, maxRating: MaxRatingSetup) {
def convert = All(nbRatedGame.convert, maxRating.convert)
case class AllSetup(
nbRatedGame: NbRatedGameSetup,
maxRating: MaxRatingSetup,
minRating: MinRatingSetup) {
def convert = All(nbRatedGame.convert, maxRating.convert, minRating.convert)
}
object AllSetup {
val default = AllSetup(nbRatedGame = NbRatedGameSetup.default, maxRating = MaxRatingSetup.default)
val default = AllSetup(
nbRatedGame = NbRatedGameSetup.default,
maxRating = MaxRatingSetup.default,
minRating = MinRatingSetup.default)
def apply(all: All): AllSetup = AllSetup(
nbRatedGame = all.nbRatedGame.fold(NbRatedGameSetup.default)(NbRatedGameSetup.apply),
maxRating = all.maxRating.fold(MaxRatingSetup.default)(MaxRatingSetup.apply))
maxRating = all.maxRating.fold(MaxRatingSetup.default)(MaxRatingSetup.apply),
minRating = all.minRating.fold(MinRatingSetup.default)(MinRatingSetup.apply))
}
}
}

View file

@ -45,7 +45,10 @@ private[tournament] final class CreatedOrganizer(
}
}
lila.mon.tournament.created(tours.size)
} andThenAnyway scheduleNext
}.chronometer
.mon(_.tournament.createdOrganizer.tickTime)
.logIfSlow(500, logger)(_ => "CreatedOrganizer.Tick")
.result andThenAnyway scheduleNext
}
private def ejectLeavers(tour: Tournament) =

View file

@ -67,13 +67,14 @@ final class JsonView(
"isStarted" -> tour.isStarted,
"isFinished" -> tour.isFinished,
"isRecentlyFinished" -> tour.isRecentlyFinished.option(true),
"pairingsClosed" -> tour.pairingsClosed,
"schedule" -> tour.schedule.map(scheduleJson),
"secondsToFinish" -> tour.isStarted.option(tour.secondsToFinish),
"secondsToStart" -> tour.isCreated.option(tour.secondsToStart),
"startsAt" -> formatDate(tour.startsAt),
"pairings" -> data.pairings,
"standing" -> stand,
"me" -> myInfo.map(myInfoJson),
"me" -> myInfo.map(myInfoJson(me)),
"featured" -> data.featured,
"podium" -> data.podium,
"playerInfo" -> playerInfoJson,
@ -223,9 +224,10 @@ final class JsonView(
"black" -> ofPlayer(featured.black, game player chess.Black))
}
private def myInfoJson(i: PlayerInfo) = Json.obj(
private def myInfoJson(u: Option[User])(i: PlayerInfo) = Json.obj(
"rank" -> i.rank,
"withdraw" -> i.withdraw)
"withdraw" -> i.withdraw,
"username" -> u.map(_.titleUsername))
private def gameUserJson(player: lila.game.Player): JsObject =
gameUserJson(player.userId, player.rating)

View file

@ -17,8 +17,10 @@ case class Schedule(
def name = freq match {
case m@Schedule.Freq.ExperimentalMarathon => m.name
case _ if variant.standard && position.initial =>
conditions.maxRating.fold(s"${freq.toString} ${speed.toString}") {
case Condition.MaxRating(_, rating) => s"U$rating ${speed.toString}"
(conditions.minRating, conditions.maxRating) match {
case (None, None) => s"${freq.toString} ${speed.toString}"
case (Some(min), _) => s"Elite ${speed.toString}"
case (_, Some(max)) => s"U${max.rating} ${speed.toString}"
}
case _ if variant.standard => s"${position.shortName} ${speed.toString}"
case _ => s"${freq.toString} ${variant.name}"
@ -67,15 +69,17 @@ object Schedule {
case object Daily extends Freq(20, 20)
case object Eastern extends Freq(30, 15)
case object Weekly extends Freq(40, 40)
case object Weekend extends Freq(41, 41)
case object Monthly extends Freq(50, 50)
case object Marathon extends Freq(60, 60)
case object ExperimentalMarathon extends Freq(61, 55) { // for DB BC
override val name = "Experimental Marathon"
}
case object Yearly extends Freq(70, 70)
case object Unique extends Freq(90, 59)
val all: List[Freq] = List(Hourly, Daily, Eastern, Weekly, Monthly, Marathon, ExperimentalMarathon, Unique)
def apply(name: String) = all find (_.name == name)
def byId(id: Int) = all find (_.id == id)
val all: List[Freq] = List(Hourly, Daily, Eastern, Weekly, Weekend, Monthly, Marathon, ExperimentalMarathon, Yearly, Unique)
def apply(name: String) = all.find(_.name == name)
def byId(id: Int) = all.find(_.id == id)
}
sealed abstract class Speed(val id: Int) {
@ -143,11 +147,21 @@ object Schedule {
case (Weekly, Blitz, _) => 60 * 3
case (Weekly, Classical, _) => 60 * 4
case (Weekend, HyperBullet | Bullet, _) => 90
case (Weekend, SuperBlitz, _) => 60 * 2
case (Weekend, Blitz, _) => 60 * 3
case (Weekend, Classical, _) => 60 * 4
case (Monthly, HyperBullet | Bullet, _) => 60 * 3
case (Monthly, SuperBlitz, _) => 60 * 3 + 30
case (Monthly, Blitz, _) => 60 * 4
case (Monthly, Classical, _) => 60 * 5
case (Yearly, HyperBullet | Bullet, _) => 60 * 4
case (Yearly, SuperBlitz, _) => 60 * 5
case (Yearly, Blitz, _) => 60 * 6
case (Yearly, Classical, _) => 60 * 8
case (Marathon, _, _) => 60 * 24 // lol
case (ExperimentalMarathon, _, _) => 60 * 4

View file

@ -35,17 +35,16 @@ object Spotlight {
private def automatically(tour: Tournament, user: User): Boolean = tour.perfType ?? { pt =>
tour.schedule ?? { sched =>
val perf = user.perfs(pt)
def playedSinceWeeks(weeks: Int) = perf.latest ?? { l =>
def playedSinceWeeks(weeks: Int) = user.perfs(pt).latest ?? { l =>
l.plusWeeks(weeks) isAfter DateTime.now
}
sched.freq match {
case Hourly => false
case Daily | Eastern => playedSinceWeeks(2)
case Weekly => playedSinceWeeks(4)
case Unique => playedSinceWeeks(4)
case Monthly | Marathon => true
case ExperimentalMarathon => false
case Hourly => false
case Daily | Eastern => playedSinceWeeks(2)
case Weekly | Weekend => playedSinceWeeks(4)
case Unique => playedSinceWeeks(4)
case Monthly | Marathon | Yearly => true
case ExperimentalMarathon => false
}
}
}

View file

@ -42,7 +42,7 @@ private[tournament] final class StartedOrganizer(
val result: Funit =
if (tour.secondsToFinish == 0) fuccess(api finish tour)
else if (!tour.isScheduled && nb < 2) fuccess(api finish tour)
else if (!tour.isAlmostFinished) startPairing(tour, activeUserIds, startAt)
else if (!tour.pairingsClosed) startPairing(tour, activeUserIds, startAt)
else funit
result >>- {
reminder ! RemindTournament(tour, activeUserIds)
@ -52,7 +52,10 @@ private[tournament] final class StartedOrganizer(
lila.mon.tournament.player(playerCounts.sum)
lila.mon.tournament.started(started.size)
}
} andThenAnyway scheduleNext
}.chronometer
.mon(_.tournament.startedOrganizer.tickTime)
.logIfSlow(500, logger)(_ => "StartedOrganizer.Tick")
.result andThenAnyway scheduleNext
}
private def startPairing(tour: Tournament, activeUserIds: List[String], startAt: Long): Funit =

View file

@ -59,7 +59,7 @@ case class Tournament(
def secondsToFinish = (finishesAt.getSeconds - nowSeconds).toInt max 0
def isAlmostFinished = secondsToFinish < math.max(30, math.min(clock.limit / 2, 120))
def pairingsClosed = secondsToFinish < math.max(30, math.min(clock.limit / 2, 120))
def isStillWorthEntering = isMarathonOrUnique || {
secondsToFinish > (minutes * 60 / 3).atMost(20 * 60)

View file

@ -179,7 +179,7 @@ final class TournamentApi(
verdicts(tour, me.some) flatMap {
_.accepted ?? {
PlayerRepo.join(tour.id, me, tour.perfLens) >> updateNbPlayers(tour.id) >>- {
withdrawAllNonMarathonOrUniqueBut(tour.id, me.id)
withdrawOtherTournaments(tour.id, me.id)
socketReload(tour.id)
publish()
if (!tour.`private`) timeline ! {
@ -195,7 +195,7 @@ final class TournamentApi(
private def updateNbPlayers(tourId: String) =
PlayerRepo count tourId flatMap { TournamentRepo.setNbPlayers(tourId, _) }
private def withdrawAllNonMarathonOrUniqueBut(tourId: String, userId: String) {
private def withdrawOtherTournaments(tourId: String, userId: String) {
TournamentRepo toursToWithdrawWhenEntering tourId foreach {
_ foreach { other =>
PlayerRepo.exists(other.id, userId) foreach {

View file

@ -2,6 +2,7 @@ package lila.tournament
import chess.variant.Variant
import org.joda.time.DateTime
import reactivemongo.api.ReadPreference
import BSONHandlers._
import lila.common.paginator.Paginator
@ -13,9 +14,7 @@ object TournamentRepo {
private lazy val coll = Env.current.tournamentColl
private val enterableSelect = $doc(
"status" $in List(Status.Created.id, Status.Started.id))
private val enterableSelect = $doc("status" $lt Status.Finished.id)
private val createdSelect = $doc("status" -> Status.Created.id)
private val startedSelect = $doc("status" -> Status.Started.id)
private[tournament] val finishedSelect = $doc("status" -> Status.Finished.id)
@ -151,13 +150,13 @@ object TournamentRepo {
}
private def isPromotable(tour: Tournament) = tour.startsAt isBefore DateTime.now.plusMinutes {
import Schedule.Freq._
tour.schedule.map(_.freq) map {
case Schedule.Freq.Marathon => 24 * 60
case Schedule.Freq.Unique => 24 * 60
case Schedule.Freq.Monthly => 6 * 60
case Schedule.Freq.Weekly => 3 * 60
case Schedule.Freq.Daily => 1 * 60
case _ => 30
case Unique | Yearly | Marathon => 24 * 60
case Monthly => 6 * 60
case Weekly | Weekend => 3 * 60
case Daily => 1 * 60
case _ => 30
} getOrElse 30
}
@ -219,12 +218,14 @@ object TournamentRepo {
def exists(id: String) = coll exists $id(id)
def toursToWithdrawWhenEntering(tourId: String): Fu[List[Tournament]] =
coll.find(enterableSelect ++ $doc(
"_id" $ne tourId,
"schedule.freq" $nin List(
Schedule.Freq.Marathon.name,
Schedule.Freq.Unique.name
)
) ++ nonEmptySelect).cursor[Tournament]().gather[List]()
def toursToWithdrawWhenEntering(tourId: String): Fu[List[Tournament]] = {
import Schedule.Freq._
coll.find(
enterableSelect ++
nonEmptySelect ++
$doc(
"_id" $ne tourId,
"startsAt" $lt DateTime.now)
).cursor[Tournament](readPreference = ReadPreference.secondaryPreferred).gather[List]()
}
}

View file

@ -3,6 +3,7 @@ package lila.tournament
import akka.actor._
import akka.pattern.pipe
import org.joda.time.DateTime
import org.joda.time.DateTimeConstants._
import scala.concurrent.duration._
import actorApi._
@ -41,8 +42,11 @@ private final class TournamentScheduler private (api: TournamentApi) extends Act
val rightNow = DateTime.now
val today = rightNow.withTimeAtStartOfDay
val tomorrow = rightNow plusDays 1
val startOfYear = today.dayOfYear.withMinimumValue
val lastDayOfMonth = today.dayOfMonth.withMaximumValue
val lastMonday = lastDayOfMonth.minusDays((lastDayOfMonth.getDayOfWeek - 1) % 7)
val lastWeekOfMonth = lastDayOfMonth.minusDays((lastDayOfMonth.getDayOfWeek - 1) % 7)
def nextDayOfWeek(number: Int) = today.plusDays((number + 7 - today.getDayOfWeek) % 7)
val nextMonday = nextDayOfWeek(1)
@ -53,23 +57,52 @@ private final class TournamentScheduler private (api: TournamentApi) extends Act
val nextSaturday = nextDayOfWeek(6)
val nextSunday = nextDayOfWeek(7)
def secondWeekOf(month: Int) = {
val start = orNextYear(startOfYear.withMonthOfYear(month))
start.plusDays(15 - start.getDayOfWeek)
}
def orTomorrow(date: DateTime) = if (date isBefore rightNow) date plusDays 1 else date
def orNextWeek(date: DateTime) = if (date isBefore rightNow) date plusWeeks 1 else date
def orNextYear(date: DateTime) = if (date isBefore rightNow) date plusYears 1 else date
val isHalloween = today.getMonthOfYear == 10 && today.getDayOfMonth == 31
val isHalloween = today.getDayOfMonth == 31 && today.getMonthOfYear == OCTOBER
val std = StartingPosition.initial
val opening1 = isHalloween ? StartingPosition.presets.halloween | StartingPosition.randomFeaturable
val opening2 = isHalloween ? StartingPosition.presets.frankenstein | StartingPosition.randomFeaturable
val farFuture = today plusMonths 5
// all dates UTC
val nextSchedules: List[Schedule] = List(
List( // yearly tournaments!
secondWeekOf(JANUARY).withDayOfWeek(MONDAY) -> Bullet -> Standard,
secondWeekOf(FEBRUARY).withDayOfWeek(TUESDAY) -> SuperBlitz -> Standard,
secondWeekOf(MARCH).withDayOfWeek(WEDNESDAY) -> Blitz -> Standard,
secondWeekOf(APRIL).withDayOfWeek(THURSDAY) -> Classical -> Standard,
secondWeekOf(MAY).withDayOfWeek(FRIDAY) -> HyperBullet -> Standard,
secondWeekOf(JUNE).withDayOfWeek(SATURDAY) -> SuperBlitz -> Crazyhouse,
secondWeekOf(JULY).withDayOfWeek(MONDAY) -> Bullet -> Standard,
secondWeekOf(AUGUST).withDayOfWeek(TUESDAY) -> SuperBlitz -> Standard,
secondWeekOf(SEPTEMBER).withDayOfWeek(WEDNESDAY) -> Blitz -> Standard,
secondWeekOf(OCTOBER).withDayOfWeek(THURSDAY) -> Classical -> Standard,
secondWeekOf(NOVEMBER).withDayOfWeek(FRIDAY) -> HyperBullet -> Standard,
secondWeekOf(DECEMBER).withDayOfWeek(SATURDAY) -> SuperBlitz -> Crazyhouse
).flatMap {
case ((day, speed), variant) =>
at(day, 17) filter farFuture.isAfter map { date =>
Schedule(Yearly, speed, variant, std, date)
}
},
List( // monthly standard tournaments!
lastMonday -> Bullet,
lastMonday.plusDays(1) -> SuperBlitz,
lastMonday.plusDays(2) -> Blitz,
lastMonday.plusDays(3) -> Classical
lastWeekOfMonth.withDayOfWeek(MONDAY) -> Bullet,
lastWeekOfMonth.withDayOfWeek(TUESDAY) -> SuperBlitz,
lastWeekOfMonth.withDayOfWeek(WEDNESDAY) -> Blitz,
lastWeekOfMonth.withDayOfWeek(THURSDAY) -> Classical
).flatMap {
case (day, speed) => at(day, 17) map { date =>
Schedule(Monthly, speed, Standard, std, date)
@ -77,13 +110,13 @@ private final class TournamentScheduler private (api: TournamentApi) extends Act
},
List( // monthly variant tournaments!
lastMonday -> Chess960,
lastMonday.plusDays(1) -> Crazyhouse,
lastMonday.plusDays(2) -> KingOfTheHill,
lastMonday.plusDays(3) -> ThreeCheck,
lastMonday.plusDays(4) -> Antichess,
lastMonday.plusDays(5) -> Atomic,
lastMonday.plusDays(6) -> Horde
lastWeekOfMonth.withDayOfWeek(MONDAY) -> Chess960,
lastWeekOfMonth.withDayOfWeek(TUESDAY) -> Crazyhouse,
lastWeekOfMonth.withDayOfWeek(WEDNESDAY) -> KingOfTheHill,
lastWeekOfMonth.withDayOfWeek(THURSDAY) -> ThreeCheck,
lastWeekOfMonth.withDayOfWeek(FRIDAY) -> Antichess,
lastWeekOfMonth.withDayOfWeek(SATURDAY) -> Atomic,
lastWeekOfMonth.withDayOfWeek(SUNDAY) -> Horde
).flatMap {
case (day, variant) => at(day, 19) map { date =>
Schedule(Monthly, Blitz, variant, std, date)
@ -116,6 +149,20 @@ private final class TournamentScheduler private (api: TournamentApi) extends Act
}
},
List( // week-end elite tournaments!
nextSaturday -> Bullet -> 2100,
nextSunday -> SuperBlitz -> 2000
).flatMap {
case ((day, speed), minRating) => at(day, 17) map { date =>
val perf = Schedule.Speed toPerfType speed
Schedule(Weekend, speed, Standard, std, date |> orNextWeek,
conditions = Condition.All(
nbRatedGame = Condition.NbRatedGame(perf.some, 30).some,
maxRating = none,
minRating = Condition.MinRating(perf, minRating).some))
}
},
List( // daily tournaments!
at(today, 16) map { date => Schedule(Daily, Bullet, Standard, std, date |> orTomorrow) },
at(today, 17) map { date => Schedule(Daily, SuperBlitz, Standard, std, date |> orTomorrow) },
@ -192,8 +239,9 @@ private final class TournamentScheduler private (api: TournamentApi) extends Act
}
val perf = Schedule.Speed toPerfType speed
val conditions = Condition.All(
Condition.NbRatedGame(perf.some, 20).some,
Condition.MaxRating(perf, rating).some)
nbRatedGame = Condition.NbRatedGame(perf.some, 30).some,
maxRating = Condition.MaxRating(perf, rating).some,
minRating = none)
at(date, hour) map { date =>
Schedule(Hourly, speed, Standard, std, date, conditions)
}
@ -214,10 +262,7 @@ private final class TournamentScheduler private (api: TournamentApi) extends Act
).flatten
}
).flatten filter { s =>
// prevent duplicate september 2016 monthly - REMOVE ME
s.freq != Monthly || s.at.isAfter(new DateTime(2016, 10, 15, 0, 0))
}
).flatten
nextSchedules.foldLeft(List[Schedule]()) {
case (scheds, sched) if sched.at.isBeforeNow => scheds

View file

@ -9,16 +9,16 @@ private[tournament] case class WaitingUsers(
clock: Option[chess.Clock],
date: DateTime) {
// hyperbullet -> 9
// 1+0 -> 10 -> 12
// 3+0 -> 18 -> 18
// 5+0 -> 26 -> 26
// 10+0 -> 46 -> 35
// hyperbullet -> 10
// 1+0 -> 11 -> 14
// 3+0 -> 21 -> 21
// 5+0 -> 31 -> 31
// 10+0 -> 56 -> 40
private val waitSeconds: Int = clock.fold(30) { c =>
if (c.estimateTotalTime < 60) 9
if (c.estimateTotalTime < 60) 10
else {
c.estimateTotalTime / 15 + 6
} min 35 max 12
c.estimateTotalTime / 12 + 6
} atMost 40 atLeast 14
}
lazy val all = hash.keys.toList

View file

@ -44,7 +44,7 @@ private[tournament] object PairingSystem extends AbstractPairingSystem {
case x => fuccess(x)
}
val pairingGroupSize = 40
val pairingGroupSize = 42
private def makePreps(data: Data, users: List[String]): Fu[List[Pairing.Prep]] = {
import data._

Some files were not shown because too many files have changed in this diff Show more