Merge branch 'master' of https://github.com/ornicar/lila
This commit is contained in:
commit
f7e119c9e6
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 =>
|
||||
|
|
37
app/controllers/Monitor.scala
Normal file
37
app/controllers/Monitor.scala
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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>"""
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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@gmail.com</p>
|
||||
<p>Or send me us email at contact@lichess.org</p>
|
||||
<br />
|
||||
<br />
|
||||
<code>@ex.getMessage</code>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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" /> -->
|
||||
|
|
|
@ -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" /> -->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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)}"
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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=Само приятели
|
||||
|
|
335
modules/i18n/messages/messages.cv
Normal file
335
modules/i18n/messages/messages.cv
Normal 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=Тупсӑмне тишкер
|
|
@ -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=Σοβαρά λάθη
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -498,3 +498,9 @@ yourPerfTypeRatingisRating=आपकी %s रेटिंग %s है
|
|||
youAreBetterThanPercentOfPerfTypePlayers=आप %s खेलने वालो से बेहतर हो, %s मे
|
||||
youDoNotHaveAnEstablishedPerfTypeRating=आपके पास स्थापित %s रेटिंग नहीं है
|
||||
checkYourEmail=अपना ई-मेल देखें
|
||||
onlineAndOfflinePlay=ऑनलाइन और ऑफ़लाइन खेलने के लिए
|
||||
correspondenceAndUnlimited=पत्राचार और असीमित
|
||||
viewTheSolution=समाधान देखें
|
||||
followAndChallengeFriends=अपने दोस्तों को चुनौती
|
||||
availableInNbLanguages=%s भाषाओं में उपलब्ध है !
|
||||
gameAnalysis=खेल अध्ययन
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=Оффлайн ойноо
|
||||
|
|
|
@ -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$
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=விரைவாக
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
51
modules/security/src/main/CSRFRequestHandler.scala
Normal file
51
modules/security/src/main/CSRFRequestHandler.scala
Normal 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
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue