Merge branch 'master' of github.com:ornicar/lila into prettier

* 'master' of github.com:ornicar/lila: (64 commits)
  clarify secrets in base.conf, move one to application.conf.default
  put reminder that twitch must come last in csp
  update stockfish-mv.wasm to 0.5.2 (fixes variant piece values)
  make sure that the requested chapter belongs to the study
  {master} don't log pgn import errors
  give up on twitch in studies, update csp accordingly (#6684)
  fix typo in route cloneAplly -> cloneApply (was working regardless)
  clean coachForm langInput
  update tagify to 3.22.1 (fixes xss)
  install tagify from npm
  Revert "{master} lazy fix xss in own studies topic manager"
  {master} lazy fix xss in own studies topic manager
  {master} fix colors in large team battles
  {master} add NoBot/OnlyBot challenge decline reasons - closes #7993
  fix analysis background
  New Crowdin updates (#7971)
  team battles up to 200 teams
  unlimited team battle POC
  send info about my team in large battles
  static team battle full leaderboard page
  ...
pull/7944/head
Panayiotis Lipiridis 2021-01-25 19:26:31 +02:00
commit 50f512a790
521 changed files with 3000 additions and 1934 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@ public/compiled
public/vendor/bar-rating
public/vendor/highcharts-4.2.5
public/vendor/hopscotch
public/vendor/tagify
public/vendor/stockfish.wasm
public/vendor/stockfish-mv.wasm
public/vendor/stockfish.js

View File

@ -5,11 +5,13 @@ import play.api.data.FormError
import play.api.libs.json._
import play.api.mvc._
import scala.annotation.nowarn
import scala.concurrent.duration._
import views._
import lila.api.Context
import lila.app._
import lila.common.{ EmailAddress, HTTPRequest }
import lila.memo.RateLimit
import lila.security.SecurityForm.{ MagicLink, PasswordReset }
import lila.security.{ FingerPrint, Signup }
import lila.user.User.ClearPassword
@ -425,17 +427,25 @@ final class Auth(
}
}
private lazy val magicLinkLoginRateLimitPerToken = new RateLimit[String](
credits = 3,
duration = 1 hour,
key = "login.magicLink.token"
)
def magicLinkLogin(token: String) =
Open { implicit ctx =>
env.security.magicLink confirm token flatMap {
case None =>
lila.mon.user.auth.magicLinkConfirm("token_fail").increment()
notFound
case Some(user) =>
authLog(user.username, "-", "Magic link")
authenticateUser(user) >>-
lila.mon.user.auth.magicLinkConfirm("success").increment().unit
}
magicLinkLoginRateLimitPerToken(token) {
env.security.magicLink confirm token flatMap {
case None =>
lila.mon.user.auth.magicLinkConfirm("token_fail").increment()
notFound
case Some(user) =>
authLog(user.username, "-", "Magic link")
authenticateUser(user) >>-
lila.mon.user.auth.magicLinkConfirm("success").increment().unit
}
}(rateLimitedFu)
}
private def loginTokenFor(me: UserModel) = JsonOk {

View File

@ -12,9 +12,9 @@ import lila.challenge.{ Challenge => ChallengeModel }
import lila.common.{ HTTPRequest, IpAddress }
import lila.game.{ AnonCookie, Pov }
import lila.oauth.{ AccessToken, OAuthScope }
import lila.setup.ApiConfig
import lila.socket.Socket.SocketVersion
import lila.user.{ User => UserModel }
import lila.setup.ApiConfig
final class Challenge(
env: Env
@ -136,21 +136,34 @@ final class Challenge(
}
def decline(id: String) =
Auth { implicit ctx => _ =>
AuthBody { implicit ctx => _ =>
OptionFuResult(api byId id) { c =>
if (isForMe(c)) api decline c
else notFound
implicit val req = ctx.body
isForMe(c) ??
api.decline(
c,
env.challenge.forms.decline
.bindFromRequest()
.fold(_ => ChallengeModel.DeclineReason.default, _.realReason)
)
}
}
def apiDecline(id: String) =
Scoped(_.Challenge.Write, _.Bot.Play, _.Board.Play) { _ => me =>
ScopedBody(_.Challenge.Write, _.Bot.Play, _.Board.Play) { implicit req => me =>
implicit val lang = reqLang
api.activeByIdFor(id, me) flatMap {
case None =>
env.bot.player.rematchDecline(id, me) flatMap {
case true => jsonOkResult.fuccess
case _ => notFoundJson()
}
case Some(c) => api.decline(c) inject jsonOkResult
case Some(c) =>
env.challenge.forms.decline
.bindFromRequest()
.fold(
newJsonFormError,
data => api.decline(c, data.realReason) inject jsonOkResult
)
}
}
@ -168,7 +181,7 @@ final class Challenge(
case Some(c) => api.cancel(c) inject jsonOkResult
case None =>
api.activeByIdFor(id, me) flatMap {
case Some(c) => api.decline(c) inject jsonOkResult
case Some(c) => api.decline(c, ChallengeModel.DeclineReason.default) inject jsonOkResult
case None =>
env.game.gameRepo game id dmap {
_ flatMap { Pov.ofUserId(_, me.id) }

View File

@ -1,19 +1,21 @@
package controllers
import scala.concurrent.duration._
import play.api.libs.json.Json
import play.api.mvc._
import scala.concurrent.duration._
import views._
import lila.app._
import lila.common.{ HTTPRequest, IpAddress }
import play.api.libs.json.Json
import views._
final class Importer(env: Env) extends LilaController(env) {
private val ImportRateLimitPerIP = new lila.memo.RateLimit[IpAddress](
credits = 200,
duration = 1.hour,
key = "import.game.ip"
private val ImportRateLimitPerIP = lila.memo.RateLimit.composite[IpAddress](
key = "import.game.ip",
enforce = env.net.rateLimit.value
)(
("fast", 4, 1.minute),
("slow", 150, 1.hour)
)
def importGame =

View File

@ -339,12 +339,11 @@ abstract private[controllers] class LilaController(val env: Env)
protected def NoPlaybanOrCurrent(a: => Fu[Result])(implicit ctx: Context): Fu[Result] =
NoPlayban(NoCurrentGame(a))
protected def JsonOk[A: Writes](fua: Fu[A]) =
protected def JsonOk[A: Writes](fua: Fu[A]): Fu[Result] =
fua map { a =>
Ok(Json toJson a) as JSON
}
protected def JsonOk[A: Writes](a: A): Result = Ok(Json toJson a) as JSON
protected def JsonFuOk[A: Writes](fua: Fu[A]): Fu[Result] = fua map { JsonOk(_) }
protected def JsonOk[A: Writes](a: A): Result = Ok(Json toJson a) as JSON
protected def JsonOptionOk[A: Writes](fua: Fu[Option[A]]) =
fua flatMap {

View File

@ -1,5 +1,6 @@
package controllers
import play.api.data.Form
import play.api.libs.json._
import scala.util.chaining._
import views._
@ -9,10 +10,9 @@ import lila.api.Context
import lila.app._
import lila.common.ApiVersion
import lila.common.config.MaxPerSecond
import lila.puzzle.PuzzleForm.RoundData
import lila.puzzle.PuzzleTheme
import lila.puzzle.{ Result, PuzzleRound, PuzzleDifficulty, PuzzleReplay, Puzzle => Puz }
import play.api.data.Form
import lila.puzzle.PuzzleForm.RoundData
final class Puzzle(
env: Env,
@ -262,6 +262,14 @@ final class Puzzle(
.fuccess
}
def apiDashboard(days: Int) =
Scoped(_.Puzzle.Read) { implicit req => me =>
implicit val lang = reqLang
JsonOptionOk {
env.puzzle.dashboard(me, days) map2 { env.puzzle.jsonView.dashboardJson(_, days) }
}
}
def dashboard(days: Int, path: String = "home") =
Auth { implicit ctx => me =>
get("u")

View File

@ -2,6 +2,7 @@ package controllers
import play.api.libs.json._
import play.api.mvc._
import scala.concurrent.duration._
import views._
import lila.api.Context
@ -350,4 +351,17 @@ final class Round(
html.game.mini(_)
)
}
def apiAddTime(anyId: String, seconds: Int) =
Scoped(_.Challenge.Write) { implicit req => me =>
import lila.round.actorApi.round.Moretime
if (seconds < 1 || seconds > 86400) BadRequest.fuccess
else
env.round.proxyRepo.game(lila.game.Game takeGameId anyId) map {
_.flatMap { Pov(_, me) }.?? { pov =>
env.round.tellRound(pov.gameId, Moretime(pov.typedPlayerId, seconds.seconds))
jsonOkResult
}
}
}
}

View File

@ -53,8 +53,9 @@ final class Swiss(
_ <- chat ?? { c =>
env.user.lightUserApi.preloadMany(c.chat.userIds)
}
streamers <- streamerCache get swiss.id
isLocalMod <- canChat ?? canModChat(swiss)
} yield Ok(html.swiss.show(swiss, verdicts, json, chat, isLocalMod))
} yield Ok(html.swiss.show(swiss, verdicts, json, chat, streamers, isLocalMod))
},
api = _ =>
swissOption.fold(notFoundJson("No such swiss tournament")) { swiss =>
@ -274,4 +275,15 @@ final class Swiss(
private def canModChat(swiss: SwissModel)(implicit ctx: Context): Fu[Boolean] =
if (isGranted(_.ChatTimeout)) fuTrue
else ctx.userId ?? { env.team.cached.isLeader(swiss.teamId, _) }
private val streamerCache =
env.memo.cacheApi[SwissModel.Id, List[lila.user.User.ID]](64, "swiss.streamers") {
_.refreshAfterWrite(15.seconds)
.maximumSize(64)
.buildAsyncFuture { id =>
env.streamer.liveStreamApi.all.flatMap { streams =>
env.swiss.api.filterPlaying(id, streams.streams.map(_.streamer.userId))
}
}
}
}

View File

@ -447,7 +447,7 @@ final class Team(
Action.async {
import env.team.jsonView._
import lila.common.paginator.PaginatorJson._
JsonFuOk {
JsonOk {
paginator popularTeams page flatMap { pager =>
env.user.lightUserApi.preloadMany(pager.currentPageResults.flatMap(_.leaders)) inject pager
}
@ -478,7 +478,7 @@ final class Team(
Action.async {
import env.team.jsonView._
import lila.common.paginator.PaginatorJson._
JsonFuOk {
JsonOk {
if (text.trim.isEmpty) paginator popularTeams page
else env.teamSearch(text, page)
}
@ -487,7 +487,7 @@ final class Team(
def apiTeamsOf(username: String) =
Action.async {
import env.team.jsonView._
JsonFuOk {
JsonOk {
api teamsOf username flatMap { teams =>
env.user.lightUserApi.preloadMany(teams.flatMap(_.leaders)) inject teams
}

View File

@ -484,6 +484,20 @@ final class Tournament(
}.fuccess
}
def battleTeams(id: String) =
Open { implicit ctx =>
repo byId id flatMap {
_ ?? { tour =>
tour.teamBattle ?? { battle =>
env.tournament.cached.battle.teamStanding.get(tour.id) map { standing =>
Ok(views.html.tournament.teamBattle.standing(tour, battle, standing))
}
}
}
}
}
private def WithEditableTournament(id: String, me: UserModel)(
f: Tour => Fu[Result]
)(implicit ctx: Context): Fu[Result] =

View File

@ -116,7 +116,7 @@ trait UserHelper { self: I18nHelper with StringHelper with NumberHelper =>
withTitle: Boolean = true,
truncate: Option[Int] = None,
params: String = ""
)(implicit lang: Lang): Frag =
)(implicit lang: Lang): Tag =
userIdNameLink(
userId = user.id,
username = user.name,
@ -150,7 +150,7 @@ trait UserHelper { self: I18nHelper with StringHelper with NumberHelper =>
title: Option[Title],
params: String,
modIcon: Boolean
)(implicit lang: Lang): Frag =
)(implicit lang: Lang): Tag =
a(
cls := userClass(userId, cssClass, withOnline),
href := userUrl(username, params = params)
@ -170,7 +170,7 @@ trait UserHelper { self: I18nHelper with StringHelper with NumberHelper =>
withPerfRating: Option[PerfType] = None,
name: Option[Frag] = None,
params: String = ""
)(implicit lang: Lang): Frag =
)(implicit lang: Lang): Tag =
a(
cls := userClass(user.id, cssClass, withOnline, withPowerTip),
href := userUrl(user.username, params)

View File

@ -91,7 +91,7 @@ object layout {
private def allNotifications(implicit ctx: Context) =
spaceless(s"""<div>
<a id="challenge-toggle" class="toggle link">
<span title="${trans.challenges
<span title="${trans.challenge.challenges
.txt()}" class="data-count" data-count="${ctx.nbChallenges}" data-icon="U"></span>
</a>
<div id="challenge-app" class="dropdown"></div>

View File

@ -25,11 +25,11 @@ object mine {
moreCss = cssTag("challenge.page")
) {
val challengeLink = s"$netBaseUrl${routes.Round.watcher(c.id, "white")}"
main(cls := "page-small challenge-page box box-pad")(
main(cls := s"page-small challenge-page box box-pad challenge--${c.status.name}")(
c.status match {
case Status.Created | Status.Offline =>
div(id := "ping-challenge")(
h1(if (c.isOpen) "Open challenge" else trans.challengeToPlay.txt()),
h1(if (c.isOpen) "Open challenge" else trans.challenge.challengeToPlay.txt()),
bits.details(c),
c.destUserId.map { destId =>
div(cls := "waiting")(
@ -93,13 +93,17 @@ object mine {
)
case Status.Declined =>
div(cls := "follow-up")(
h1(trans.challengeDeclined()),
h1(trans.challenge.challengeDeclined()),
blockquote(cls := "challenge-reason pull-quote")(
p(c.anyDeclineReason.trans()),
footer(userIdLink(c.destUserId))
),
bits.details(c),
a(cls := "button button-fat", href := routes.Lobby.home())(trans.newOpponent())
)
case Status.Accepted =>
div(cls := "follow-up")(
h1(trans.challengeAccepted()),
h1(trans.challenge.challengeAccepted()),
bits.details(c),
a(id := "challenge-redirect", href := routes.Round.watcher(c.id, "white"), cls := "button-fat")(
trans.joinTheGame()
@ -107,7 +111,7 @@ object mine {
)
case Status.Canceled =>
div(cls := "follow-up")(
h1(trans.challengeCanceled()),
h1(trans.challenge.challengeCanceled()),
bits.details(c),
a(cls := "button button-fat", href := routes.Lobby.home())(trans.newOpponent())
)

View File

@ -73,13 +73,13 @@ object theirs {
)
case Status.Declined =>
div(cls := "follow-up")(
h1(trans.challengeDeclined()),
h1(trans.challenge.challengeDeclined()),
bits.details(c),
a(cls := "button button-fat", href := routes.Lobby.home())(trans.newOpponent())
)
case Status.Accepted =>
div(cls := "follow-up")(
h1(trans.challengeAccepted()),
h1(trans.challenge.challengeAccepted()),
bits.details(c),
a(
id := "challenge-redirect",
@ -91,7 +91,7 @@ object theirs {
)
case Status.Canceled =>
div(cls := "follow-up")(
h1(trans.challengeCanceled()),
h1(trans.challenge.challengeCanceled()),
bits.details(c),
a(cls := "button button-fat", href := routes.Lobby.home())(trans.newOpponent())
)

View File

@ -107,7 +107,7 @@ object studentDashboard {
a(
dataIcon := "U",
cls := List("button button-empty text" -> true, "disabled" -> !online),
title := trans.challengeToPlay.txt(),
title := trans.challenge.challengeToPlay.txt(),
href := online option s"${routes.Lobby.home()}?user=${user.username}#friend"
)(trans.play())
)

View File

@ -6,7 +6,7 @@ import play.api.data.Form
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.event.EventForm
import lila.event.{ Event, EventForm }
object event {
@ -20,7 +20,7 @@ object event {
)
}
def edit(event: lila.event.Event, form: Form[_])(implicit ctx: Context) =
def edit(event: Event, form: Form[_])(implicit ctx: Context) =
layout(title = event.title, css = "mod.form") {
div(cls := "crud edit page-menu__content box box-pad")(
div(cls := "box__top")(
@ -37,7 +37,14 @@ object event {
)
}
def show(e: lila.event.Event)(implicit ctx: Context) =
def iconOf(e: Event) =
e.icon match {
case None => i(cls := "img", dataIcon := "")
case Some(c) if c == EventForm.icon.broadcast => i(cls := "img", dataIcon := "")
case Some(c) => img(cls := "img", src := assetUrl(s"images/$c"))
}
def show(e: Event)(implicit ctx: Context) =
views.html.base.layout(
title = e.title,
moreCss = cssTag("event"),
@ -45,9 +52,7 @@ object event {
) {
main(cls := "page-small event box box-pad")(
div(cls := "box__top")(
e.icon map { i =>
img(cls := "img", src := assetUrl(s"images/$i"))
} getOrElse i(cls := "img", dataIcon := ""),
iconOf(e),
div(
h1(e.title),
strong(cls := "headline")(e.headline)
@ -67,7 +72,7 @@ object event {
)
}
def manager(events: List[lila.event.Event])(implicit ctx: Context) = {
def manager(events: List[Event])(implicit ctx: Context) = {
val title = "Event manager"
layout(title = title) {
div(cls := "crud page-menu__content box")(
@ -134,7 +139,7 @@ object event {
frag("Icon"),
half = true,
help = frag("Displayed on the homepage button").some
)(form3.select(_, EventForm.iconChoices))
)(form3.select(_, EventForm.icon.choices))
),
form3.group(
form("headline"),

View File

@ -172,9 +172,7 @@ object bits {
"invert" -> e.isNowOrSoon
)
)(
e.icon map { i =>
img(cls := "img", src := assetUrl(s"images/$i"))
} getOrElse i(cls := "img", dataIcon := ""),
views.html.event.iconOf(e),
span(cls := "content")(
span(cls := "name")(e.title),
span(cls := "headline")(e.headline),

View File

@ -32,7 +32,7 @@ object msg {
private val i18nKeys = List(
trans.inbox,
trans.challengeToPlay,
trans.challenge.challengeToPlay,
trans.block,
trans.unblock,
trans.blocked,

View File

@ -57,21 +57,13 @@ object dashboard {
)
}
) { dash =>
frag(
dash.mostPlayed.size > 2 option
div(cls := s"${baseClass}__global")(
metricsOf(days, PuzzleTheme.mix.key, dash.global),
canvas(cls := s"${baseClass}__radar")
)
)
}
// data: {
// labels: ['Running', 'Swimming', 'Eating', 'Cycling'],
// datasets: [{
// data: [20, 10, 4, 2]
// }]
// }
def improvementAreas(user: User, dashOpt: Option[PuzzleDashboard], days: Int)(implicit ctx: Context) =
dashboardLayout(
user = user,
@ -83,7 +75,7 @@ object dashboard {
subtitle = "Train these to optimize your progress!",
dashOpt = dashOpt
) { dash =>
themeSelection(days, dash.weakThemes)
dash.weakThemes.nonEmpty option themeSelection(days, dash.weakThemes)
}
def strengths(user: User, dashOpt: Option[PuzzleDashboard], days: Int)(implicit ctx: Context) =
@ -97,7 +89,7 @@ object dashboard {
subtitle = "You perform the best in these themes",
dashOpt = dashOpt
) { dash =>
themeSelection(days, dash.strongThemes)
dash.strongThemes.nonEmpty option themeSelection(days, dash.strongThemes)
}
private def dashboardLayout(
@ -109,7 +101,7 @@ object dashboard {
dashOpt: Option[PuzzleDashboard],
moreJs: Frag = emptyFrag
)(
body: PuzzleDashboard => Frag
body: PuzzleDashboard => Option[Frag]
)(implicit ctx: Context) =
views.html.base.layout(
title = title,
@ -136,13 +128,10 @@ object dashboard {
}
)
),
dashOpt match {
case None =>
div(cls := s"${baseClass}__empty")(
a(href := routes.Puzzle.home())("Nothing to show, go play some puzzles first!")
)
case Some(dash) => body(dash)
}
dashOpt.flatMap(body) |
div(cls := s"${baseClass}__empty")(
a(href := routes.Puzzle.home())("Nothing to show, go play some puzzles first!")
)
)
)
)

View File

@ -22,7 +22,7 @@ object actions {
(myId != userId) ?? frag(
!blocked option frag(
a(
titleOrText(trans.challengeToPlay.txt()),
titleOrText(trans.challenge.challengeToPlay.txt()),
href := s"${routes.Lobby.home()}?user=$userId#friend",
cls := "btn-rack__btn",
dataIcon := "U"

View File

@ -56,7 +56,7 @@ object show {
),
chessground = false,
zoomable = true,
csp = defaultCsp.withWebAssembly.withTwitch.some,
csp = defaultCsp.withWebAssembly.some,
openGraph = lila.app.ui
.OpenGraph(
title = r.name,

View File

@ -94,7 +94,7 @@ object forms {
)(implicit ctx: Context) =
layout(
"friend",
(if (user.isDefined) trans.challengeToPlay else trans.playWithAFriend)(),
(if (user.isDefined) trans.challenge.challengeToPlay else trans.playWithAFriend)(),
routes.Setup.friend(user map (_.id)),
error.map(e => raw(e.replace("{{user}}", userIdLink(user.map(_.id)).toString)))
)(

View File

@ -1,9 +1,9 @@
package views
package html.site
import controllers.routes
import scala.util.chaining._
import controllers.routes
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
@ -149,39 +149,27 @@ object contact {
)
)
),
Branch(
Leaf(
"report",
wantReport(),
List(
"cheating" -> cheating(),
"sandbagging" -> sandbagging(),
"trolling" -> trolling(),
"insults" -> insults(),
"some other reason" -> otherReason()
).map { case (reason, name) =>
Leaf(
reason,
frag("Report a player for ", name),
frag(
p(
a(href := routes.Report.form())(toReportAPlayer(name)),
"."
),
p(
youCanAlsoReachReportPage(button(cls := "thin button button-empty", dataIcon := "!"))
),
p(
doNotMessageModerators(),
br,
doNotReportInForum(),
br,
doNotSendReportEmails(),
br,
onlyReports()
)
)
frag(
p(
a(href := routes.Report.form())(toReportAPlayerUseForm()),
"."
),
p(
youCanAlsoReachReportPage(button(cls := "thin button button-empty", dataIcon := "!"))
),
p(
doNotMessageModerators(),
br,
doNotReportInForum(),
br,
doNotSendReportEmails(),
br,
onlyReports()
)
}
)
),
Branch(
"bug",

View File

@ -73,7 +73,7 @@ object message {
def challengeDenied(msg: String)(implicit ctx: Context) =
apply(
title = trans.challengeToPlay.txt(),
title = trans.challenge.challengeToPlay.txt(),
back = routes.Lobby.home().url.some
)(msg)

View File

@ -29,7 +29,7 @@ object show {
image = s.streamer.picturePath.map(p => dbImageUrl(p.value))
)
.some,
csp = defaultCsp.withTwitch.some
csp = defaultCsp.finalizeWithTwitch.some
)(
main(cls := "page-menu streamer-show")(
st.aside(cls := "page-menu__menu")(

View File

@ -55,7 +55,7 @@ object show {
robots = s.isPublic,
chessground = false,
zoomable = true,
csp = defaultCsp.withWebAssembly.withTwitch.withPeer.some,
csp = defaultCsp.withWebAssembly.withPeer.some,
openGraph = lila.app.ui
.OpenGraph(
title = s.name.value,

View File

@ -17,6 +17,7 @@ object show {
verdicts: SwissCondition.All.WithVerdicts,
data: play.api.libs.json.JsObject,
chatOption: Option[lila.chat.UserChat.Mine],
streamers: List[lila.user.User.ID],
isLocalMod: Boolean
)(implicit ctx: Context): Frag = {
val isDirector = ctx.userId.has(s.createdBy)
@ -66,7 +67,7 @@ object show {
)(
main(cls := "swiss")(
st.aside(cls := "swiss__side")(
swiss.side(s, verdicts, chatOption.isDefined)
swiss.side(s, verdicts, streamers, chatOption.isDefined)
),
div(cls := "swiss__main")(div(cls := "box"))
)

View File

@ -13,7 +13,12 @@ object side {
private val separator = " • "
def apply(s: Swiss, verdicts: SwissCondition.All.WithVerdicts, chat: Boolean)(implicit
def apply(
s: Swiss,
verdicts: SwissCondition.All.WithVerdicts,
streamers: List[lila.user.User.ID],
chat: Boolean
)(implicit
ctx: Context
) =
frag(
@ -88,6 +93,9 @@ object side {
else br,
absClientDateTime(s.startsAt)
),
streamers.nonEmpty option div(cls := "context-streamers")(
streamers map views.html.streamer.bits.contextual
),
chat option views.html.chat.frag
)
}

View File

@ -1,15 +1,15 @@
package views.html
package tournament
import controllers.routes
import play.api.data.Form
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.tournament.TeamBattle
import lila.tournament.Tournament
import controllers.routes
object teamBattle {
def edit(tour: Tournament, form: Form[_])(implicit ctx: Context) =
@ -52,4 +52,37 @@ object teamBattle {
)
)
)
private val scoreTag = tag("score")
def standing(tour: Tournament, battle: TeamBattle, standing: List[TeamBattle.RankedTeam])(implicit
ctx: Context
) =
views.html.base.layout(
title = tour.name(),
moreCss = cssTag("tournament.show.team-battle")
)(
main(cls := "box")(
h1(a(href := routes.Tournament.show(tour.id))(tour.name())),
table(cls := "slist slist-pad tour__team-standing tour__team-standing--full")(
tbody(
standing.map { t =>
tr(
td(cls := "rank")(t.rank),
td(cls := "team")(teamIdToName(t.teamId)),
td(cls := "players")(
fragList(
t.leaders.map { l =>
scoreTag(dataHref := routes.User.show(l.userId), cls := "user-link ulpt")(l.score)
},
"+"
)
),
td(cls := "total")(t.score)
)
}
)
)
)
)
}

View File

@ -80,7 +80,7 @@ object bots {
a(
dataIcon := "U",
cls := List("button button-empty text" -> true),
st.title := trans.challengeToPlay.txt(),
st.title := trans.challenge.challengeToPlay.txt(),
href := s"${routes.Lobby.home()}?user=${u.username}#friend"
)(trans.play())
)

View File

@ -60,7 +60,7 @@ object mini {
a(
dataIcon := "U",
cls := "btn-rack__btn",
title := trans.challengeToPlay.txt(),
title := trans.challenge.challengeToPlay.txt(),
href := s"${routes.Lobby.home()}?user=${u.username}#friend"
)
),

View File

@ -20,16 +20,18 @@ object side {
perf.nonEmpty option showPerf(perf, perfType)
def showPerf(perf: lila.rating.Perf, perfType: PerfType) = {
val isGame = lila.rating.PerfType.isGame(perfType)
val isPuzzle = perfType == lila.rating.PerfType.Puzzle
a(
dataIcon := perfType.iconChar,
title := perfType.desc,
cls := List(
"empty" -> perf.isEmpty,
"game" -> isGame,
"active" -> active.has(perfType)
),
href := isGame option routes.User.perfStat(u.username, perfType.key).url,
href := {
if (isPuzzle) routes.Puzzle.dashboard(30, "home")
else routes.User.perfStat(u.username, perfType.key)
},
span(
h3(perfType.trans),
st.rating(
@ -53,7 +55,7 @@ object side {
)
}
),
isGame option iconTag("G")
!isPuzzle option iconTag("G")
)
}

View File

@ -71,7 +71,7 @@ def main(args):
def terser(js):
p = subprocess.Popen(["yarn", "run", "--silent", "terser", "--mangle", "--compress", "--safari10"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr)
p = subprocess.Popen(["yarn", "run", "--silent", "terser", "--mangle", "--compress"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr)
stdout, stderr = p.communicate(js.encode("utf-8"))
if p.returncode != 0:
sys.exit(p.returncode)

View File

@ -2,7 +2,7 @@ const fs = require('fs').promises;
const parseString = require('xml2js').parseString;
const baseDir = 'translation/source';
const dbs = 'site arena emails learn activity coordinates study clas contact patron coach broadcast streamer tfa settings preferences team perfStat search tourname faq lag swiss puzzle puzzleTheme'.split(' ');
const dbs = 'site arena emails learn activity coordinates study clas contact patron coach broadcast streamer tfa settings preferences team perfStat search tourname faq lag swiss puzzle puzzleTheme challenge'.split(' ');
function ucfirst(s) {
return s.charAt(0).toUpperCase() + s.slice(1);

View File

@ -60,7 +60,7 @@ lazy val i18n = smallModule("i18n",
MessageCompiler(
sourceDir = new File("translation/source"),
destDir = new File("translation/dest"),
dbs = "site arena emails learn activity coordinates study class contact patron coach broadcast streamer tfa settings preferences team perfStat search tourname faq lag swiss puzzle puzzleTheme".split(' ').toList,
dbs = "site arena emails learn activity coordinates study class contact patron coach broadcast streamer tfa settings preferences team perfStat search tourname faq lag swiss puzzle puzzleTheme challenge".split(' ').toList,
compileTo = (sourceManaged in Compile).value
)
}.taskValue

View File

@ -1,4 +1,7 @@
include "base"
include "version"
# change for additional encryption of password hashes
user.password.bpass.secret = "9qEYN0ThHer1KWLNekA76Q=="
# override values from base.conf here

View File

@ -30,7 +30,7 @@ play {
cookieName = "lila2"
maxAge = 3650 days
}
secret.key = "CiebwjgIM9cHQ;I?Xk:sfqDJ;BhIe:jsL?r=?IPF[saf>s^r0]?0grUq4>q?5mP^"
secret.key = "CiebwjgIM9cHQ;I?Xk:sfqDJ;BhIe:jsL?r=?IPF[saf>s^r0]?0grUq4>q?5mP^" # public (lila does not rely on cookie signing)
}
ws {
useragent = "lichess.org"
@ -339,7 +339,6 @@ gameSearch {
actor.name = game-search
}
round {
moretime = 15 seconds
collection {
note = game_note
forecast = forecast
@ -414,9 +413,6 @@ user {
trophyKind = trophyKind
ranking = ranking
}
password.bpass {
secret = "9qEYN0ThHer1KWLNekA76Q=="
}
}
fishnet {
redis = ${redis}

View File

@ -124,7 +124,7 @@ GET /study/$id<\w{8}>/$chapterId<\w{8}>.pgn controllers.Study.chapterPgn(id: S
GET /study/$id<\w{8}>/$chapterId<\w{8}>.gif controllers.Study.chapterGif(id: String, chapterId: String)
POST /study/$id<\w{8}>/delete controllers.Study.delete(id: String)
GET /study/$id<\w{8}>/clone controllers.Study.cloneStudy(id: String)
POST /study/$id<\w{8}>/cloneAplly controllers.Study.cloneApply(id: String)
POST /study/$id<\w{8}>/cloneApply controllers.Study.cloneApply(id: String)
GET /study/$id<\w{8}>/$chapterId<\w{8}> controllers.Study.chapter(id: String, chapterId: String)
GET /study/$id<\w{8}>/$chapterId<\w{8}>/meta controllers.Study.chapterMeta(id: String, chapterId: String)
GET /study/embed/$id<\w{8}>/$chapterId<\w{8}> controllers.Study.embed(id: String, chapterId: String)
@ -236,6 +236,7 @@ GET /tournament/$id<\w{8}>/team/:team controllers.Tournament.teamInfo(id
POST /tournament/$id<\w{8}>/terminate controllers.Tournament.terminate(id: String)
GET /tournament/$id<\w{8}>/edit controllers.Tournament.edit(id: String)
POST /tournament/$id<\w{8}>/edit controllers.Tournament.update(id: String)
GET /tournament/$id<\w{8}>/teams controllers.Tournament.battleTeams(id: String)
GET /tournament/help controllers.Tournament.help(system: Option[String] ?= None)
GET /tournament/leaderboard controllers.Tournament.leaderboard
GET /tournament/shields controllers.Tournament.shields
@ -564,6 +565,8 @@ GET /stat/rating/distribution/:perf controllers.Stat.ratingDistribution(perf:
GET /api controllers.Api.index
POST /api/users controllers.Api.usersByIds
GET /api/user/puzzle-activity controllers.Puzzle.activity
GET /api/puzzle/activity controllers.Puzzle.activity
GET /api/puzzle/dashboard/:days controllers.Puzzle.apiDashboard(days: Int)
GET /api/user/:name/tournament/created controllers.Api.tournamentsByOwner(name: String)
GET /api/user/:name controllers.Api.user(name: String)
GET /api/user/:name/activity controllers.Api.activity(name: String)
@ -601,7 +604,8 @@ POST /api/challenge/:user controllers.Challenge.apiCreate(user: Str
POST /api/challenge/$id<\w{8}>/accept controllers.Challenge.apiAccept(id: String)
POST /api/challenge/$id<\w{8}>/decline controllers.Challenge.apiDecline(id: String)
POST /api/challenge/$id<\w{8}>/cancel controllers.Challenge.apiCancel(id: String)
POST /api/challenge/$id<\w{8}>/start-clocks controllers.Challenge.apiStartClocks(id: String)
POST /api/challenge/$id<\w{8}>/start-clocks controllers.Challenge.apiStartClocks(id: String)
POST /api/round/$id<\w{8}>/add-time/:seconds controllers.Round.apiAddTime(id: String, seconds: Int)
GET /api/cloud-eval controllers.Api.cloudEval
GET /api/broadcast controllers.Relay.apiIndex
POST /api/import controllers.Importer.apiSendGame

View File

@ -66,7 +66,7 @@ final class PersonalDataExport(
List(
textTitle(s"${sessions.size} Connections"),
sessions.map { s =>
s"${s.ip} ${s.date.map(textDate)}\n${s.ua}"
s"${s.ip} ${s.date.??(textDate)}\n${s.ua}"
} mkString "\n\n"
)

View File

@ -7,6 +7,7 @@ import chess.variant.Variant
import lila.db.BSON
import lila.db.BSON.{ Reader, Writer }
import lila.db.dsl._
import scala.util.Success
private object BSONHandlers {
@ -71,6 +72,10 @@ private object BSONHandlers {
"s" -> a.secret
)
}
implicit val DeclineReasonBSONHandler = tryHandler[DeclineReason](
{ case BSONString(k) => Success(Challenge.DeclineReason(k)) },
r => BSONString(r.key)
)
implicit val ChallengerBSONHandler = new BSON[Challenger] {
def reads(r: Reader) =
if (r contains "id") RegisteredBSONHandler reads r

View File

@ -6,6 +6,7 @@ import chess.{ Color, Mode, Speed }
import org.joda.time.DateTime
import lila.game.{ Game, PerfPicker }
import lila.i18n.{ I18nKey, I18nKeys }
import lila.rating.PerfType
import lila.user.User
@ -24,7 +25,8 @@ case class Challenge(
createdAt: DateTime,
seenAt: Option[DateTime], // None for open challenges, so they don't sweep
expiresAt: DateTime,
open: Option[Boolean] = None
open: Option[Boolean] = None,
declineReason: Option[Challenge.DeclineReason] = None
) {
import Challenge._
@ -93,6 +95,13 @@ case class Challenge(
def isOpen = ~open
lazy val perfType = perfTypeOf(variant, timeControl)
def anyDeclineReason = declineReason | DeclineReason.default
def declineWith(reason: DeclineReason) = copy(
status = Status.Declined,
declineReason = reason.some
)
}
object Challenge {
@ -112,6 +121,29 @@ object Challenge {
def apply(id: Int): Option[Status] = all.find(_.id == id)
}
sealed abstract class DeclineReason(val trans: I18nKey) {
val key = toString.toLowerCase
}
object DeclineReason {
case object Generic extends DeclineReason(I18nKeys.challenge.declineGeneric)
case object Later extends DeclineReason(I18nKeys.challenge.declineLater)
case object TooFast extends DeclineReason(I18nKeys.challenge.declineTooFast)
case object TooSlow extends DeclineReason(I18nKeys.challenge.declineTooSlow)
case object TimeControl extends DeclineReason(I18nKeys.challenge.declineTimeControl)
case object Rated extends DeclineReason(I18nKeys.challenge.declineRated)
case object Casual extends DeclineReason(I18nKeys.challenge.declineCasual)
case object Standard extends DeclineReason(I18nKeys.challenge.declineStandard)
case object Variant extends DeclineReason(I18nKeys.challenge.declineVariant)
case object NoBot extends DeclineReason(I18nKeys.challenge.declineNoBot)
case object OnlyBot extends DeclineReason(I18nKeys.challenge.declineOnlyBot)
val default: DeclineReason = Generic
val all: List[DeclineReason] =
List(Generic, Later, TooFast, TooSlow, TimeControl, Rated, Casual, Standard, Variant, NoBot, OnlyBot)
def apply(key: String) = all.find { d => d.key == key.toLowerCase || d.trans.key == key } | Generic
}
case class Rating(int: Int, provisional: Boolean) {
def show = s"$int${if (provisional) "?" else ""}"
}

View File

@ -73,10 +73,10 @@ final class ChallengeApi(
case _ => fuccess(socketReload(id))
}
def decline(c: Challenge) =
repo.decline(c) >>- {
def decline(c: Challenge, reason: Challenge.DeclineReason) =
repo.decline(c, reason) >>- {
uncacheAndNotify(c)
Bus.publish(Event.Decline(c), "challenge")
Bus.publish(Event.Decline(c declineWith reason), "challenge")
}
private val acceptQueue = new lila.hub.DuctSequencer(maxSize = 64, timeout = 5 seconds, "challengeAccept")
@ -149,7 +149,7 @@ final class ChallengeApi(
}
private def socketReload(id: Challenge.ID): Unit =
socket foreach (_ reload id)
socket.foreach(_ reload id)
private def notify(userId: User.ID): Funit =
for {

View File

@ -0,0 +1,18 @@
package lila.challenge
import play.api.data._
import play.api.data.Forms._
final class ChallengeForm {
val decline = Form(
mapping(
"reason" -> optional(nonEmptyText)
)(DeclineData.apply _)(DeclineData.unapply _)
)
case class DeclineData(reason: Option[String]) {
def realReason = reason.fold(Challenge.DeclineReason.default)(Challenge.DeclineReason.apply)
}
}

View File

@ -2,7 +2,7 @@ package lila.challenge
import play.api.i18n.Lang
import lila.i18n.I18nKeys
import lila.i18n.I18nKeys.{ challenge => trans }
import lila.pref.Pref
import lila.rating.PerfType
import lila.relation.{ Block, Follow }
@ -26,13 +26,13 @@ object ChallengeDenied {
def translated(d: ChallengeDenied)(implicit lang: Lang): String =
d.reason match {
case Reason.YouAreAnon => I18nKeys.registerToSendChallenges.txt()
case Reason.YouAreBlocked => I18nKeys.youCannotChallengeX.txt(d.dest.titleUsername)
case Reason.TheyDontAcceptChallenges => I18nKeys.xDoesNotAcceptChallenges.txt(d.dest.titleUsername)
case Reason.YouAreAnon => trans.registerToSendChallenges.txt()
case Reason.YouAreBlocked => trans.youCannotChallengeX.txt(d.dest.titleUsername)
case Reason.TheyDontAcceptChallenges => trans.xDoesNotAcceptChallenges.txt(d.dest.titleUsername)
case Reason.RatingOutsideRange(perf) =>
I18nKeys.yourXRatingIsTooFarFromY.txt(perf.trans, d.dest.titleUsername)
case Reason.RatingIsProvisional(perf) => I18nKeys.cannotChallengeDueToProvisionalXRating.txt(perf.trans)
case Reason.FriendsOnly => I18nKeys.xOnlyAcceptsChallengesFromFriends.txt(d.dest.titleUsername)
trans.yourXRatingIsTooFarFromY.txt(perf.trans, d.dest.titleUsername)
case Reason.RatingIsProvisional(perf) => trans.cannotChallengeDueToProvisionalXRating.txt(perf.trans)
case Reason.FriendsOnly => trans.xOnlyAcceptsChallengesFromFriends.txt(d.dest.titleUsername)
case Reason.BotUltraBullet => "Bots cannot play UltraBullet. Choose a slower time control."
}
}

View File

@ -114,8 +114,12 @@ final private class ChallengeRepo(coll: Coll, maxPerUser: Max)(implicit
def offline(challenge: Challenge) = setStatus(challenge, Status.Offline, Some(_ plusHours 3))
def cancel(challenge: Challenge) = setStatus(challenge, Status.Canceled, Some(_ plusHours 3))
def decline(challenge: Challenge) = setStatus(challenge, Status.Declined, Some(_ plusHours 3))
def accept(challenge: Challenge) = setStatus(challenge, Status.Accepted, Some(_ plusHours 3))
def decline(challenge: Challenge, reason: Challenge.DeclineReason) =
setStatus(challenge, Status.Declined, Some(_ plusHours 3)) >> {
(reason != Challenge.DeclineReason.default) ??
coll.updateField($id(challenge.id), "declineReason", reason).void
}
def accept(challenge: Challenge) = setStatus(challenge, Status.Accepted, Some(_ plusHours 3))
def statusById(id: Challenge.ID) = coll.primitiveOne[Status]($id(id), "status")

View File

@ -50,6 +50,8 @@ final class Env(
lazy val jsonView = wire[JsonView]
val forms = new ChallengeForm
system.scheduler.scheduleWithFixedDelay(10 seconds, 3 seconds) { () =>
api.sweep.unit
}

View File

@ -35,7 +35,10 @@ final class JsonView(
Json.obj(
"in" -> a.in.map(apply(Direction.In.some)),
"out" -> a.out.map(apply(Direction.Out.some)),
"i18n" -> lila.i18n.JsDump.keysToObject(i18nKeys, lang)
"i18n" -> lila.i18n.JsDump.keysToObject(i18nKeys, lang),
"reasons" -> JsObject(Challenge.DeclineReason.all.map { r =>
r.key -> JsString(r.trans.txt())
})
)
def show(challenge: Challenge, socketVersion: SocketVersion, direction: Option[Direction])(implicit
@ -80,6 +83,7 @@ final class JsonView(
)
.add("direction" -> direction.map(_.name))
.add("initialFen" -> c.initialFen)
.add("declineReason" -> c.declineReason.map(_.trans.txt()))
private def iconChar(c: Challenge) =
if (c.variant == chess.variant.FromPosition) '*'

View File

@ -26,7 +26,7 @@ case class ContentSecurityPolicy(
frameSrc = "https://*.stripe.com" :: frameSrc
)
def withTwitch =
def finalizeWithTwitch =
copy(
defaultSrc = Nil,
connectSrc = "https://www.twitch.tv" :: "https://www-cdn.jtvnw.net" :: connectSrc,

View File

@ -46,48 +46,6 @@ final class Adapter[A: BSONDocumentReader](
.list(length)
}
/*
* because mongodb mapReduce doesn't support `skip`, slice requires two queries.
* The first one gets the IDs with `skip`.
* The second one runs the mapReduce on these IDs.
* This avoid running mapReduce on many unnecessary docs.
* NOTE: Requires string ID.
*/
final class MapReduceAdapter[A: BSONDocumentReader](
collection: Coll,
selector: Bdoc,
sort: Bdoc,
runCommand: RunCommand,
command: Bdoc,
readPreference: ReadPreference = ReadPreference.primary
)(implicit ec: ExecutionContext)
extends AdapterLike[A] {
def nbResults: Fu[Int] = collection.secondaryPreferred.countSel(selector)
def slice(offset: Int, length: Int): Fu[List[A]] =
collection
.find(selector, $id(true).some)
.sort(sort)
.skip(offset)
.cursor[Bdoc](readPreference)
.list(length)
.dmap { _ flatMap { _.getAsOpt[BSONString]("_id") } }
.flatMap { ids =>
runCommand(
$doc(
"mapreduce" -> collection.name,
"query" -> $inIds(ids),
"sort" -> sort,
"out" -> $doc("inline" -> 1)
) ++ command,
readPreference
) map { res =>
res.getAsOpt[List[Bdoc]]("results").??(_ flatMap implicitly[BSONDocumentReader[A]].readOpt)
}
}
}
final class StaticAdapter[A](results: Seq[A])(implicit ec: ExecutionContext) extends AdapterLike[A] {
def nbResults = fuccess(results.size)

View File

@ -12,13 +12,17 @@ import lila.user.User
object EventForm {
val iconChoices = List(
"" -> "Microphone",
"lichess.event.png" -> "Lichess",
"trophy.event.png" -> "Trophy",
"offerspill.logo.png" -> "Offerspill"
)
val imageDefault = ""
object icon {
val default = ""
val broadcast = "broadcast.icon"
val choices = List(
default -> "Microphone",
"lichess.event.png" -> "Lichess",
"trophy.event.png" -> "Trophy",
broadcast -> "Broadcast",
"offerspill.logo.png" -> "Offerspill"
)
}
val form = Form(
mapping(
@ -35,7 +39,7 @@ object EventForm {
lila.user.UserForm.historicalUsernameField
.transform[User.ID](_.toLowerCase, identity)
},
"icon" -> stringIn(iconChoices),
"icon" -> stringIn(icon.choices),
"countdown" -> boolean
)(Data.apply)(Data.unapply)
) fill Data(

View File

@ -9,6 +9,8 @@ case class Pov(game: Game, color: Color) {
def playerId = player.id
def typedPlayerId = Game.PlayerId(player.id)
def fullId = game fullIdOf color
def gameId = game.id

View File

@ -279,7 +279,6 @@ val `xStartedFollowingY` = new I18nKey("xStartedFollowingY")
val `more` = new I18nKey("more")
val `memberSince` = new I18nKey("memberSince")
val `lastSeenActive` = new I18nKey("lastSeenActive")
val `challengeToPlay` = new I18nKey("challengeToPlay")
val `player` = new I18nKey("player")
val `list` = new I18nKey("list")
val `graph` = new I18nKey("graph")
@ -604,7 +603,6 @@ val `error.max` = new I18nKey("error.max")
val `error.unknown` = new I18nKey("error.unknown")
val `custom` = new I18nKey("custom")
val `notifications` = new I18nKey("notifications")
val `challenges` = new I18nKey("challenges")
val `perfRatingX` = new I18nKey("perfRatingX")
val `practiceWithComputer` = new I18nKey("practiceWithComputer")
val `anotherWasX` = new I18nKey("anotherWasX")
@ -643,12 +641,6 @@ val `advantage` = new I18nKey("advantage")
val `opening` = new I18nKey("opening")
val `middlegame` = new I18nKey("middlegame")
val `endgame` = new I18nKey("endgame")
val `registerToSendChallenges` = new I18nKey("registerToSendChallenges")
val `youCannotChallengeX` = new I18nKey("youCannotChallengeX")
val `xDoesNotAcceptChallenges` = new I18nKey("xDoesNotAcceptChallenges")
val `yourXRatingIsTooFarFromY` = new I18nKey("yourXRatingIsTooFarFromY")
val `cannotChallengeDueToProvisionalXRating` = new I18nKey("cannotChallengeDueToProvisionalXRating")
val `xOnlyAcceptsChallengesFromFriends` = new I18nKey("xOnlyAcceptsChallengesFromFriends")
val `conditionalPremoves` = new I18nKey("conditionalPremoves")
val `addCurrentVariation` = new I18nKey("addCurrentVariation")
val `playVariationToCreateConditionalPremoves` = new I18nKey("playVariationToCreateConditionalPremoves")
@ -698,9 +690,6 @@ val `teamNamedX` = new I18nKey("teamNamedX")
val `youCannotPostYetPlaySomeGames` = new I18nKey("youCannotPostYetPlaySomeGames")
val `subscribe` = new I18nKey("subscribe")
val `unsubscribe` = new I18nKey("unsubscribe")
val `challengeDeclined` = new I18nKey("challengeDeclined")
val `challengeAccepted` = new I18nKey("challengeAccepted")
val `challengeCanceled` = new I18nKey("challengeCanceled")
val `opponentLeftCounter` = new I18nKey("opponentLeftCounter")
val `mateInXHalfMoves` = new I18nKey("mateInXHalfMoves")
val `nextCaptureOrPawnMoveInXHalfMoves` = new I18nKey("nextCaptureOrPawnMoveInXHalfMoves")
@ -1225,12 +1214,7 @@ val `orCloseAccount` = new I18nKey("contact:orCloseAccount")
val `wantClearHistory` = new I18nKey("contact:wantClearHistory")
val `cantClearHistory` = new I18nKey("contact:cantClearHistory")
val `wantReport` = new I18nKey("contact:wantReport")
val `cheating` = new I18nKey("contact:cheating")
val `sandbagging` = new I18nKey("contact:sandbagging")
val `trolling` = new I18nKey("contact:trolling")
val `insults` = new I18nKey("contact:insults")
val `otherReason` = new I18nKey("contact:otherReason")
val `toReportAPlayer` = new I18nKey("contact:toReportAPlayer")
val `toReportAPlayerUseForm` = new I18nKey("contact:toReportAPlayerUseForm")
val `youCanAlsoReachReportPage` = new I18nKey("contact:youCanAlsoReachReportPage")
val `doNotReportInForum` = new I18nKey("contact:doNotReportInForum")
val `doNotSendReportEmails` = new I18nKey("contact:doNotSendReportEmails")
@ -1844,6 +1828,10 @@ val `advancedPawn` = new I18nKey("puzzleTheme:advancedPawn")
val `advancedPawnDescription` = new I18nKey("puzzleTheme:advancedPawnDescription")
val `advantage` = new I18nKey("puzzleTheme:advantage")
val `advantageDescription` = new I18nKey("puzzleTheme:advantageDescription")
val `anastasiaMate` = new I18nKey("puzzleTheme:anastasiaMate")
val `anastasiaMateDescription` = new I18nKey("puzzleTheme:anastasiaMateDescription")
val `arabianMate` = new I18nKey("puzzleTheme:arabianMate")
val `arabianMateDescription` = new I18nKey("puzzleTheme:arabianMateDescription")
val `attackingF2F7` = new I18nKey("puzzleTheme:attackingF2F7")
val `attackingF2F7Description` = new I18nKey("puzzleTheme:attackingF2F7Description")
val `attraction` = new I18nKey("puzzleTheme:attraction")
@ -1852,12 +1840,18 @@ val `backRankMate` = new I18nKey("puzzleTheme:backRankMate")
val `backRankMateDescription` = new I18nKey("puzzleTheme:backRankMateDescription")
val `bishopEndgame` = new I18nKey("puzzleTheme:bishopEndgame")
val `bishopEndgameDescription` = new I18nKey("puzzleTheme:bishopEndgameDescription")
val `bodenMate` = new I18nKey("puzzleTheme:bodenMate")
val `bodenMateDescription` = new I18nKey("puzzleTheme:bodenMateDescription")
val `castling` = new I18nKey("puzzleTheme:castling")
val `castlingDescription` = new I18nKey("puzzleTheme:castlingDescription")
val `capturingDefender` = new I18nKey("puzzleTheme:capturingDefender")
val `capturingDefenderDescription` = new I18nKey("puzzleTheme:capturingDefenderDescription")
val `crushing` = new I18nKey("puzzleTheme:crushing")
val `crushingDescription` = new I18nKey("puzzleTheme:crushingDescription")
val `doubleBishopMate` = new I18nKey("puzzleTheme:doubleBishopMate")
val `doubleBishopMateDescription` = new I18nKey("puzzleTheme:doubleBishopMateDescription")
val `dovetailMate` = new I18nKey("puzzleTheme:dovetailMate")
val `dovetailMateDescription` = new I18nKey("puzzleTheme:dovetailMateDescription")
val `equality` = new I18nKey("puzzleTheme:equality")
val `equalityDescription` = new I18nKey("puzzleTheme:equalityDescription")
val `kingsideAttack` = new I18nKey("puzzleTheme:kingsideAttack")
@ -1881,6 +1875,8 @@ val `fork` = new I18nKey("puzzleTheme:fork")
val `forkDescription` = new I18nKey("puzzleTheme:forkDescription")
val `hangingPiece` = new I18nKey("puzzleTheme:hangingPiece")
val `hangingPieceDescription` = new I18nKey("puzzleTheme:hangingPieceDescription")
val `hookMate` = new I18nKey("puzzleTheme:hookMate")
val `hookMateDescription` = new I18nKey("puzzleTheme:hookMateDescription")
val `interference` = new I18nKey("puzzleTheme:interference")
val `interferenceDescription` = new I18nKey("puzzleTheme:interferenceDescription")
val `intermezzo` = new I18nKey("puzzleTheme:intermezzo")
@ -1951,4 +1947,29 @@ val `healthyMix` = new I18nKey("puzzleTheme:healthyMix")
val `healthyMixDescription` = new I18nKey("puzzleTheme:healthyMixDescription")
}
object challenge {
val `challenges` = new I18nKey("challenge:challenges")
val `challengeToPlay` = new I18nKey("challenge:challengeToPlay")
val `challengeDeclined` = new I18nKey("challenge:challengeDeclined")
val `challengeAccepted` = new I18nKey("challenge:challengeAccepted")
val `challengeCanceled` = new I18nKey("challenge:challengeCanceled")
val `registerToSendChallenges` = new I18nKey("challenge:registerToSendChallenges")
val `youCannotChallengeX` = new I18nKey("challenge:youCannotChallengeX")
val `xDoesNotAcceptChallenges` = new I18nKey("challenge:xDoesNotAcceptChallenges")
val `yourXRatingIsTooFarFromY` = new I18nKey("challenge:yourXRatingIsTooFarFromY")
val `cannotChallengeDueToProvisionalXRating` = new I18nKey("challenge:cannotChallengeDueToProvisionalXRating")
val `xOnlyAcceptsChallengesFromFriends` = new I18nKey("challenge:xOnlyAcceptsChallengesFromFriends")
val `declineGeneric` = new I18nKey("challenge:declineGeneric")
val `declineLater` = new I18nKey("challenge:declineLater")
val `declineTooFast` = new I18nKey("challenge:declineTooFast")
val `declineTooSlow` = new I18nKey("challenge:declineTooSlow")
val `declineTimeControl` = new I18nKey("challenge:declineTimeControl")
val `declineRated` = new I18nKey("challenge:declineRated")
val `declineCasual` = new I18nKey("challenge:declineCasual")
val `declineStandard` = new I18nKey("challenge:declineStandard")
val `declineVariant` = new I18nKey("challenge:declineVariant")
val `declineNoBot` = new I18nKey("challenge:declineNoBot")
val `declineOnlyBot` = new I18nKey("challenge:declineOnlyBot")
}
}

View File

@ -19,7 +19,19 @@ final class ImporterForm {
)(ImportData.apply)(ImportData.unapply)
)
def checkPgn(pgn: String): Validated[String, Preprocessed] = ImportData(pgn, none).preprocess(none)
def checkPgn(pgn: String): Validated[String, Preprocessed] = ImporterForm.catchOverflow { () =>
ImportData(pgn, none).preprocess(none)
}
}
object ImporterForm {
def catchOverflow(f: () => Validated[String, Preprocessed]): Validated[String, Preprocessed] = try {
f()
} catch {
case e: RuntimeException if e.getMessage contains "StackOverflowError" =>
Validated.Invalid("This PGN seems too long or too complex!")
}
}
private case class TagResult(status: Status, winner: Option[Color])
@ -42,7 +54,7 @@ case class ImportData(pgn: String, analyse: Option[String]) {
case Reader.Result.Incomplete(replay, _) => replay
}
def preprocess(user: Option[String]): Validated[String, Preprocessed] =
def preprocess(user: Option[String]): Validated[String, Preprocessed] = ImporterForm.catchOverflow { () =>
Parser.full(pgn) flatMap { parsed =>
Reader.fullWithSans(
pgn,
@ -114,4 +126,5 @@ case class ImportData(pgn: String, analyse: Option[String]) {
Preprocessed(NewGame(dbGame), replay.copy(state = game), initialFen, parsed)
}
}
}
}

View File

@ -17,7 +17,7 @@ final private class AggregationPipeline(store: Storage)(implicit ec: scala.concu
import question.{ dimension, filters, metric }
import lila.insight.{ Dimension => D, Metric => M }
import Entry.{ BSONFields => F }
import InsightEntry.{ BSONFields => F }
import Storage._
val sampleGames = Sample(10_000)

View File

@ -67,9 +67,9 @@ private object BSONHandlers {
implicit val PeriodBSONHandler = intIsoHandler(lila.common.Iso.int[Period](Period.apply, _.days))
implicit def MoveBSONHandler =
new BSON[Move] {
new BSON[InsightMove] {
def reads(r: BSON.Reader) =
Move(
InsightMove(
phase = r.get[Phase]("p"),
tenths = r.get[Int]("t"),
role = r.get[Role]("r"),
@ -82,7 +82,7 @@ private object BSONHandlers {
blur = r.boolD("b"),
timeCv = r.intO("v").map(v => v.toFloat / TimeVariance.intFactor)
)
def writes(w: BSON.Writer, b: Move) =
def writes(w: BSON.Writer, b: InsightMove) =
BSONDocument(
"p" -> b.phase,
"t" -> b.tenths,
@ -99,10 +99,10 @@ private object BSONHandlers {
}
implicit def EntryBSONHandler =
new BSON[Entry] {
import Entry.BSONFields._
new BSON[InsightEntry] {
import InsightEntry.BSONFields._
def reads(r: BSON.Reader) =
Entry(
InsightEntry(
id = r.str(id),
number = r.int(number),
userId = r.str(userId),
@ -113,7 +113,7 @@ private object BSONHandlers {
opponentRating = r.int(opponentRating),
opponentStrength = r.get[RelativeStrength](opponentStrength),
opponentCastling = r.get[Castling](opponentCastling),
moves = r.get[List[Move]](moves),
moves = r.get[List[InsightMove]](moves),
queenTrade = r.get[QueenTrade](queenTrade),
result = r.get[Result](result),
termination = r.get[Termination](termination),
@ -122,7 +122,7 @@ private object BSONHandlers {
provisional = r.boolD(provisional),
date = r.date(date)
)
def writes(w: BSON.Writer, e: Entry) =
def writes(w: BSON.Writer, e: InsightEntry) =
BSONDocument(
id -> e.id,
number -> e.number,

View File

@ -28,7 +28,7 @@ object Dimension {
import BSONHandlers._
import Position._
import Entry.{ BSONFields => F }
import InsightEntry.{ BSONFields => F }
import lila.rating.BSONHandlers.perfTypeIdHandler
case object Period

View File

@ -62,7 +62,7 @@ final class InsightApi(
Pov(g)
.map { pov =>
pov.player.userId ?? { userId =>
storage find Entry.povToId(pov) flatMap {
storage find InsightEntry.povToId(pov) flatMap {
_ ?? { old =>
indexer.update(g, userId, old)
}

View File

@ -7,7 +7,7 @@ import lila.rating.PerfType
import org.joda.time.DateTime
import cats.data.NonEmptyList
case class Entry(
case class InsightEntry(
id: String, // gameId + w/b
number: Int, // auto increment over userId
userId: String,
@ -18,7 +18,7 @@ case class Entry(
opponentRating: Int,
opponentStrength: RelativeStrength,
opponentCastling: Castling,
moves: List[Move],
moves: List[InsightMove],
queenTrade: QueenTrade,
result: Result,
termination: Termination,
@ -31,7 +31,7 @@ case class Entry(
def gameId = id take Game.gameIdSize
}
case object Entry {
case object InsightEntry {
def povToId(pov: Pov) = pov.gameId + pov.color.letter
@ -58,7 +58,7 @@ case object Entry {
}
}
case class Move(
case class InsightMove(
phase: Phase,
tenths: Int,
role: Role,

View File

@ -37,7 +37,7 @@ final private class InsightIndexer(
}
}
def update(game: Game, userId: String, previous: Entry): Funit =
def update(game: Game, userId: String, previous: InsightEntry): Funit =
povToEntry(game, userId, previous.provisional) flatMap {
case Right(e) => storage update e.copy(number = previous.number)
case _ => funit
@ -76,7 +76,7 @@ final private class InsightIndexer(
private def computeFrom(user: User, from: DateTime, fromNumber: Int): Funit =
storage nbByPerf user.id flatMap { nbs =>
var nbByPerf = nbs
def toEntry(game: Game): Fu[Option[Entry]] =
def toEntry(game: Game): Fu[Option[InsightEntry]] =
game.perfType ?? { pt =>
val nb = nbByPerf.getOrElse(pt, 0) + 1
nbByPerf = nbByPerf.updated(pt, nb)

View File

@ -28,7 +28,7 @@ object Metric {
import DataType._
import Position._
import Entry.{ BSONFields => F }
import InsightEntry.{ BSONFields => F }
case object MeanCpl
extends Metric(

View File

@ -27,7 +27,7 @@ final private class PovToEntry(
advices: Map[Ply, Advice]
)
def apply(game: Game, userId: String, provisional: Boolean): Fu[Either[Game, Entry]] =
def apply(game: Game, userId: String, provisional: Boolean): Fu[Either[Game, InsightEntry]] =
enrich(game, userId, provisional) map
(_ flatMap convert toRight game)
@ -84,7 +84,7 @@ final private class PovToEntry(
case _ => chess.Pawn
}
private def makeMoves(from: RichPov): List[Move] = {
private def makeMoves(from: RichPov): List[InsightMove] = {
val cpDiffs = ~from.moveAccuracy toVector
val prevInfos = from.analysis.?? { an =>
Accuracy.prevColorInfos(from.pov, an) pipe { is =>
@ -124,7 +124,7 @@ final private class PovToEntry(
}
case _ => none
}
Move(
InsightMove(
phase = Phase.of(from.division, ply),
tenths = movetime.roundTenths,
role = role,
@ -174,7 +174,7 @@ final private class PovToEntry(
}
}
private def convert(from: RichPov): Option[Entry] = {
private def convert(from: RichPov): Option[InsightEntry] = {
import from._
import pov.game
for {
@ -182,8 +182,8 @@ final private class PovToEntry(
myRating <- pov.player.rating
opRating <- pov.opponent.rating
perfType <- game.perfType
} yield Entry(
id = Entry povToId pov,
} yield InsightEntry(
id = InsightEntry povToId pov,
number = 0, // temporary :-/ the Indexer will set it
userId = myId,
color = pov.color,

View File

@ -11,33 +11,33 @@ final private class Storage(val coll: AsyncColl)(implicit ec: scala.concurrent.E
import Storage._
import BSONHandlers._
import Entry.{ BSONFields => F }
import InsightEntry.{ BSONFields => F }
def fetchFirst(userId: String): Fu[Option[Entry]] =
coll(_.find(selectUserId(userId)).sort(sortChronological).one[Entry])
def fetchFirst(userId: String): Fu[Option[InsightEntry]] =
coll(_.find(selectUserId(userId)).sort(sortChronological).one[InsightEntry])
def fetchLast(userId: String): Fu[Option[Entry]] =
coll(_.find(selectUserId(userId)).sort(sortAntiChronological).one[Entry])
def fetchLast(userId: String): Fu[Option[InsightEntry]] =
coll(_.find(selectUserId(userId)).sort(sortAntiChronological).one[InsightEntry])
def count(userId: String): Fu[Int] =
coll(_.countSel(selectUserId(userId)))
def insert(p: Entry) = coll(_.insert.one(p).void)
def insert(p: InsightEntry) = coll(_.insert.one(p).void)
def bulkInsert(ps: Seq[Entry]) =
def bulkInsert(ps: Seq[InsightEntry]) =
coll {
_.insert.many(
ps.flatMap(BSONHandlers.EntryBSONHandler.writeOpt)
)
}
def update(p: Entry) = coll(_.update.one(selectId(p.id), p, upsert = true).void)
def update(p: InsightEntry) = coll(_.update.one(selectId(p.id), p, upsert = true).void)
def remove(p: Entry) = coll(_.delete.one(selectId(p.id)).void)
def remove(p: InsightEntry) = coll(_.delete.one(selectId(p.id)).void)
def removeAll(userId: String) = coll(_.delete.one(selectUserId(userId)).void)
def find(id: String) = coll(_.one[Entry](selectId(id)))
def find(id: String) = coll(_.one[InsightEntry](selectId(id)))
def ecos(userId: String): Fu[Set[String]] =
coll {
@ -66,7 +66,7 @@ final private class Storage(val coll: AsyncColl)(implicit ec: scala.concurrent.E
private object Storage {
import Entry.{ BSONFields => F }
import InsightEntry.{ BSONFields => F }
def selectId(id: String) = BSONDocument(F.id -> id)
def selectUserId(id: String) = BSONDocument(F.userId -> id)

View File

@ -82,6 +82,25 @@ final class JsonView(
"is3d" -> p.is3d
)
def dashboardJson(dash: PuzzleDashboard, days: Int)(implicit lang: Lang) = Json.obj(
"days" -> days,
"global" -> dashboardResults(dash.global),
"themes" -> JsObject(dash.byTheme.toList.sortBy(-_._2.nb).map { case (key, res) =>
key.value -> Json.obj(
"theme" -> PuzzleTheme(key).name.txt(),
"results" -> dashboardResults(res)
)
})
)
private def dashboardResults(res: PuzzleDashboard.Results) = Json.obj(
"nb" -> res.nb,
"firstWins" -> res.wins,
"replayWins" -> res.fixed,
"puzzleRatingAvg" -> res.puzzleRatingAvg,
"performance" -> res.performance
)
private def puzzleJson(puzzle: Puzzle): JsObject = Json.obj(
"id" -> puzzle.id,
"rating" -> puzzle.glicko.intRating,

View File

@ -14,10 +14,13 @@ object PuzzleTheme {
val mix = PuzzleTheme(Key("mix"), i.healthyMix, i.healthyMixDescription)
val advancedPawn = PuzzleTheme(Key("advancedPawn"), i.advancedPawn, i.advancedPawnDescription)
val advantage = PuzzleTheme(Key("advantage"), i.advantage, i.advantageDescription)
val anastasiaMate = PuzzleTheme(Key("anastasiaMate"), i.anastasiaMate, i.anastasiaMateDescription)
val arabianMate = PuzzleTheme(Key("arabianMate"), i.arabianMate, i.arabianMateDescription)
val attackingF2F7 = PuzzleTheme(Key("attackingF2F7"), i.attackingF2F7, i.attackingF2F7Description)
val attraction = PuzzleTheme(Key("attraction"), i.attraction, i.attractionDescription)
val backRankMate = PuzzleTheme(Key("backRankMate"), i.backRankMate, i.backRankMateDescription)
val bishopEndgame = PuzzleTheme(Key("bishopEndgame"), i.bishopEndgame, i.bishopEndgameDescription)
val bodenMate = PuzzleTheme(Key("bodenMate"), i.bodenMate, i.bodenMateDescription)
val capturingDefender =
PuzzleTheme(Key("capturingDefender"), i.capturingDefender, i.capturingDefenderDescription)
val castling = PuzzleTheme(Key("castling"), i.castling, i.castlingDescription)
@ -27,13 +30,18 @@ object PuzzleTheme {
val deflection = PuzzleTheme(Key("deflection"), i.deflection, i.deflectionDescription)
val discoveredAttack =
PuzzleTheme(Key("discoveredAttack"), i.discoveredAttack, i.discoveredAttackDescription)
val doubleCheck = PuzzleTheme(Key("doubleCheck"), i.doubleCheck, i.doubleCheckDescription)
val doubleBishopMate =
PuzzleTheme(Key("doubleBishopMate"), i.doubleBishopMate, i.doubleBishopMateDescription)
val doubleCheck = PuzzleTheme(Key("doubleCheck"), i.doubleCheck, i.doubleCheckDescription)
val dovetailMate =
PuzzleTheme(Key("dovetailMate"), i.dovetailMate, i.dovetailMateDescription)
val equality = PuzzleTheme(Key("equality"), i.equality, i.equalityDescription)
val endgame = PuzzleTheme(Key("endgame"), i.endgame, i.endgameDescription)
val enPassant = PuzzleTheme(Key("enPassant"), new I18nKey("En passant"), i.enPassantDescription)
val exposedKing = PuzzleTheme(Key("exposedKing"), i.exposedKing, i.exposedKingDescription)
val fork = PuzzleTheme(Key("fork"), i.fork, i.forkDescription)
val hangingPiece = PuzzleTheme(Key("hangingPiece"), i.hangingPiece, i.hangingPieceDescription)
val hookMate = PuzzleTheme(Key("hookMate"), i.hookMate, i.hookMateDescription)
val interference = PuzzleTheme(Key("interference"), i.interference, i.interferenceDescription)
val intermezzo = PuzzleTheme(Key("intermezzo"), i.intermezzo, i.intermezzoDescription)
val kingsideAttack = PuzzleTheme(Key("kingsideAttack"), i.kingsideAttack, i.kingsideAttackDescription)
@ -118,8 +126,14 @@ object PuzzleTheme {
mateIn3,
mateIn4,
mateIn5,
smotheredMate,
backRankMate
anastasiaMate,
arabianMate,
backRankMate,
bodenMate,
doubleBishopMate,
dovetailMate,
hookMate,
smotheredMate
),
trans.puzzle.specialMoves -> List(
castling,

View File

@ -38,8 +38,6 @@ final private class PuzzleTrustApi(colls: PuzzleColls)(implicit ec: scala.concur
ratingBonus(user) +
titleBonus(user) +
patronBonus(user) +
nbGamesBonus(user) +
nbPuzzlesBonus(user) +
modBonus(user) +
lameBonus(user)
}.toInt
@ -63,19 +61,6 @@ final private class PuzzleTrustApi(colls: PuzzleColls)(implicit ec: scala.concur
private def patronBonus(user: User) = (~user.planMonths * 5) atMost 20
// 0 games = 0
// 200 games = 1
// 400 games = 2.41
// 2000 games = 3.16
private def nbGamesBonus(user: User) =
nbBonus(user.count.game)
private def nbPuzzlesBonus(user: User) =
nbBonus(user.perfs.puzzle.nb) / 2
private def nbBonus(nb: Int) =
math.sqrt(nb / 200) atMost 5
private def modBonus(user: User) =
if (user.roles.exists(_ contains "ROLE_PUZZLE_CURATOR")) 100
else if (user.isAdmin) 50

View File

@ -219,7 +219,6 @@ object PerfType {
Horde,
RacingKings
)
val nonGame: List[PerfType] = List(Puzzle)
val leaderboardable: List[PerfType] = List(
Bullet,
Blitz,
@ -239,8 +238,6 @@ object PerfType {
List(Crazyhouse, Chess960, KingOfTheHill, ThreeCheck, Antichess, Atomic, Horde, RacingKings)
val standard: List[PerfType] = List(Bullet, Blitz, Rapid, Classical, Correspondence)
def isGame(pt: PerfType) = !nonGame.contains(pt)
def variantOf(pt: PerfType): chess.variant.Variant =
pt match {
case Crazyhouse => chess.variant.Crazyhouse

View File

@ -19,8 +19,7 @@ import lila.user.User
private class RoundConfig(
@ConfigName("collection.note") val noteColl: CollName,
@ConfigName("collection.forecast") val forecastColl: CollName,
@ConfigName("collection.alarm") val alarmColl: CollName,
@ConfigName("moretime") val moretimeDuration: MoretimeDuration
@ConfigName("collection.alarm") val alarmColl: CollName
)
@Module
@ -59,7 +58,6 @@ final class Env(
scheduler: akka.actor.Scheduler
) {
implicit private val moretimeLoader = durationLoader(MoretimeDuration.apply)
implicit private val animationLoader = durationLoader(AnimationDuration.apply)
private val config = appConfig.get[RoundConfig]("round")(AutoConfig.loader)

View File

@ -21,14 +21,11 @@ final class JsonView(
moretimer: Moretimer,
divider: lila.game.Divider,
evalCache: lila.evalCache.EvalCacheApi,
isOfferingRematch: Pov => Boolean,
moretime: MoretimeDuration
isOfferingRematch: Pov => Boolean
)(implicit ec: scala.concurrent.ExecutionContext) {
import JsonView._
private val moretimeSeconds = moretime.value.toSeconds.toInt
private def checkCount(game: Game, color: Color) =
(game.variant == chess.variant.ThreeCheck) option game.history.checkCount(color)
@ -263,7 +260,7 @@ final class JsonView(
}
private def clockJson(clock: Clock): JsObject =
clockWriter.writes(clock) + ("moretime" -> JsNumber(moretimeSeconds))
clockWriter.writes(clock) + ("moretime" -> JsNumber(actorApi.round.Moretime.defaultDuration.toSeconds))
private def possibleMoves(pov: Pov, apiVersion: ApiVersion): Option[JsValue] =
(pov.game playableBy pov.player) option

View File

@ -4,15 +4,15 @@ import chess.Color
import lila.game.{ Event, Game, Pov, Progress }
import lila.pref.{ Pref, PrefApi }
import scala.concurrent.duration.FiniteDuration
final private class Moretimer(
messenger: Messenger,
prefApi: PrefApi,
duration: MoretimeDuration
prefApi: PrefApi
)(implicit ec: scala.concurrent.ExecutionContext) {
// pov of the player giving more time
def apply(pov: Pov): Fu[Option[Progress]] =
def apply(pov: Pov, duration: FiniteDuration): Fu[Option[Progress]] =
IfAllowed(pov.game) {
(pov.game moretimeable !pov.color) ?? {
if (pov.game.hasClock) give(pov.game, List(!pov.color), duration).some
@ -29,14 +29,14 @@ final private class Moretimer(
if (game.isMandatory) fuFalse
else isAllowedByPrefs(game)
private[round] def give(game: Game, colors: List[Color], duration: MoretimeDuration): Progress =
private[round] def give(game: Game, colors: List[Color], duration: FiniteDuration): Progress =
game.clock.fold(Progress(game)) { clock =>
val centis = duration.value.toCentis
val centis = duration.toCentis
val newClock = colors.foldLeft(clock) { case (c, color) =>
c.giveTime(color, centis)
}
colors.foreach { c =>
messenger.volatile(game, s"$c + ${duration.value.toSeconds} seconds")
messenger.volatile(game, s"$c + ${duration.toSeconds} seconds")
}
(game withClock newClock) ++ colors.map { Event.ClockInc(_, centis) }
}

View File

@ -366,9 +366,9 @@ final private[round] class RoundDuct(
}
}
case Moretime(playerId) =>
case Moretime(playerId, duration) =>
handle(playerId) { pov =>
moretimer(pov) flatMap {
moretimer(pov, duration) flatMap {
_ ?? { progress =>
proxy save progress inject progress.events
}
@ -404,7 +404,7 @@ final private[round] class RoundDuct(
handle { game =>
game.playable ?? {
messenger.system(game, "Lichess has been updated! Sorry for the inconvenience.")
val progress = moretimer.give(game, Color.all, MoretimeDuration(20 seconds))
val progress = moretimer.give(game, Color.all, 20 seconds)
proxy save progress inject progress.events
}
}

View File

@ -75,7 +75,7 @@ final class RoundSocket(
val duct = new RoundDuct(
dependencies = roundDependencies,
gameId = id,
socketSend = send
socketSend = sendForGameId(id)
)(ec, proxy)
terminationDelay schedule Game.Id(id)
duct.getGame dforeach {
@ -160,17 +160,19 @@ final class RoundSocket(
private def finishRound(gameId: Game.Id): Unit =
rounds.terminate(gameId.value, _ ! RoundDuct.Stop)
private lazy val send: String => Unit = remoteSocketApi.makeSender("r-out").apply _
private lazy val send: Sender = remoteSocketApi.makeSender("r-out", parallelism = 8)
private lazy val sendForGameId: Game.ID => String => Unit = gameId => msg => send.sticky(gameId, msg)
remoteSocketApi.subscribeRoundRobin("r-in", Protocol.In.reader, parallelism = 8)(
roundHandler orElse remoteSocketApi.baseHandler
) >>- send(P.Out.boot)
Bus.subscribeFun("tvSelect", "roundSocket", "tourStanding", "startGame", "finishGame") {
case TvSelect(gameId, speed, json) => send(Protocol.Out.tvSelect(gameId, speed, json))
case TvSelect(gameId, speed, json) => sendForGameId(gameId)(Protocol.Out.tvSelect(gameId, speed, json))
case Tell(gameId, e @ BotConnected(color, v)) =>
rounds.tell(gameId, e)
send(Protocol.Out.botConnected(gameId, color, v))
sendForGameId(gameId)(Protocol.Out.botConnected(gameId, color, v))
case Tell(gameId, msg) => rounds.tell(gameId, msg)
case TellIfExists(gameId, msg) => rounds.tellIfPresent(gameId, msg)
case TellMany(gameIds, msg) => rounds.tellIds(gameIds, msg)
@ -179,11 +181,11 @@ final class RoundSocket(
case TourStanding(tourId, json) => send(Protocol.Out.tourStanding(tourId, json))
case lila.game.actorApi.StartGame(game) if game.hasClock =>
game.userIds.some.filter(_.nonEmpty) foreach { usersPlaying =>
send(Protocol.Out.startGame(usersPlaying))
sendForGameId(game.id)(Protocol.Out.startGame(usersPlaying))
}
case lila.game.actorApi.FinishGame(game, _, _) if game.hasClock =>
game.userIds.some.filter(_.nonEmpty) foreach { usersPlaying =>
send(Protocol.Out.finishGame(game.id, game.winnerColor, usersPlaying))
sendForGameId(game.id)(Protocol.Out.finishGame(game.id, game.winnerColor, usersPlaying))
}
}

View File

@ -2,6 +2,7 @@ package lila.round
package actorApi
import scala.concurrent.Promise
import scala.concurrent.duration._
import chess.format.Uci
import chess.{ Color, MoveMetrics }
@ -48,7 +49,8 @@ package round {
case class DrawNo(playerId: PlayerId)
case class TakebackYes(playerId: PlayerId)
case class TakebackNo(playerId: PlayerId)
case class Moretime(playerId: PlayerId)
object Moretime { val defaultDuration = 15.seconds }
case class Moretime(playerId: PlayerId, seconds: FiniteDuration = Moretime.defaultDuration)
case object QuietFlag
case class ClientFlag(color: Color, fromPlayerId: Option[PlayerId])
case object Abandon

View File

@ -6,7 +6,6 @@ import lila.game.{ Game, Pov }
import lila.user.User
import play.api.libs.json.JsObject
private case class MoretimeDuration(value: FiniteDuration) extends AnyVal
private case class AnimationDuration(value: FiniteDuration) extends AnyVal
final class OnStart(f: Game.ID => Unit) extends (Game.ID => Unit) {

View File

@ -59,19 +59,19 @@ object MagicLink {
private lazy val rateLimitPerIP = new RateLimit[IpAddress](
credits = 5,
duration = 1 hour,
key = "email.confirms.ip"
key = "login.magicLink.ip"
)
private lazy val rateLimitPerUser = new RateLimit[String](
credits = 3,
duration = 1 hour,
key = "email.confirms.user"
key = "login.magicLink.user"
)
private lazy val rateLimitPerEmail = new RateLimit[String](
credits = 3,
duration = 1 hour,
key = "email.confirms.email"
key = "login.magicLink.email"
)
def rateLimit[A: Zero](user: User, email: EmailAddress, req: RequestHeader)(

View File

@ -16,7 +16,7 @@ final class Env(
private lazy val maxPlaying = appConfig.get[Max]("setup.max_playing")
lazy val forms = wire[FormFactory]
lazy val forms = wire[SetupForm]
lazy val processor = wire[Processor]
}

View File

@ -8,7 +8,7 @@ import play.api.data.Forms._
import lila.rating.RatingRange
import lila.user.{ User, UserContext }
final class FormFactory {
final class SetupForm {
import Mappings._

View File

@ -4,7 +4,7 @@ import akka.actor.{ ActorSystem, CoordinatedShutdown }
import cats.data.NonEmptyList
import chess.{ Centis, Color }
import io.lettuce.core._
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection
import io.lettuce.core.pubsub.{ StatefulRedisPubSubConnection => PubSub }
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.ConcurrentHashMap
import play.api.libs.json._
@ -116,12 +116,24 @@ final class RemoteSocket(
case UnFollow(u1, u2) => send(Out.unfollow(u1, u2))
}
final class StoppableSender(conn: StatefulRedisPubSubConnection[String, String], channel: Channel)
extends Sender {
def apply(msg: String): Unit = if (!stopping) conn.async.publish(channel, msg).unit
final class StoppableSender(conn: PubSub[String, String], channel: Channel) extends Sender {
def apply(msg: String): Unit = if (!stopping) conn.async.publish(channel, msg).unit
def sticky(_id: String, msg: String): Unit = apply(msg)
}
def makeSender(channel: Channel): Sender = new StoppableSender(redisClient.connectPubSub(), channel)
final class RoundRobinSender(conn: PubSub[String, String], channel: Channel, parallelism: Int)
extends Sender {
def apply(msg: String): Unit = publish(msg.hashCode.abs % parallelism, msg)
// use the ID to select the channel, not the entire message
def sticky(id: String, msg: String): Unit = publish(id.hashCode.abs % parallelism, msg)
private def publish(subChannel: Int, msg: String) =
if (!stopping) conn.async.publish(s"$channel:$subChannel", msg).unit
}
def makeSender(channel: Channel, parallelism: Int = 1): Sender =
if (parallelism > 1) new RoundRobinSender(redisClient.connectPubSub(), channel, parallelism)
else new StoppableSender(redisClient.connectPubSub(), channel)
private val send: Send = makeSender("site-out").apply _
@ -184,6 +196,7 @@ object RemoteSocket {
trait Sender {
def apply(msg: String): Unit
def sticky(_id: String, msg: String): Unit
}
object Protocol {

View File

@ -1,26 +1,27 @@
package lila.study
import com.github.blemale.scaffeine.AsyncLoadingCache
import play.api.libs.json._
import reactivemongo.api.bson._
import scala.concurrent.duration._
import BSONHandlers._
import chess.Color
import chess.format.pgn.Tags
import chess.format.{ FEN, Uci }
import BSONHandlers._
import com.github.blemale.scaffeine.AsyncLoadingCache
import JsonView._
import play.api.libs.json._
import reactivemongo.api.bson._
import reactivemongo.api.ReadPreference
import scala.concurrent.duration._
import lila.common.config.MaxPerPage
import lila.common.paginator.AdapterLike
import lila.common.paginator.{ Paginator, PaginatorJson }
import lila.db.dsl._
import lila.db.paginator.MapReduceAdapter
final class StudyMultiBoard(
runCommand: lila.db.RunCommand,
chapterRepo: ChapterRepo,
cacheApi: lila.memo.CacheApi
)(implicit ec: scala.concurrent.ExecutionContext) {
private val maxPerPage = MaxPerPage(9)
import StudyMultiBoard._
@ -37,57 +38,69 @@ final class StudyMultiBoard(
.expireAfterAccess(10 minutes)
.buildAsyncFuture[Study.Id, Paginator[ChapterPreview]] { fetch(_, 1, playing = false) }
private def fetch(studyId: Study.Id, page: Int, playing: Boolean): Fu[Paginator[ChapterPreview]] = {
private val playingSelector = $doc("tags" -> "Result:*", "root.n.0" $exists true)
val selector = $doc("studyId" -> studyId) ++ playing.??(playingSelector)
/* If players are found in the tags,
* return the last mainline node.
* Else, return the root node without its children.
*/
Paginator(
adapter = new MapReduceAdapter[ChapterPreview](
collection = chapterRepo.coll,
selector = selector,
sort = $sort asc "order",
runCommand = runCommand,
command = $doc(
"map" -> """var node = this.root, child, tagPrefixes = ['White','Black','Result'], result = {name:this.name,orientation:this.setup.orientation,tags:this.tags.filter(t => tagPrefixes.find(p => t.startsWith(p)))};
if (result.tags.length > 1) { while(child = node.n[0]) { node = child }; }
result.fen = node.f;
result.uci = node.u;
emit(this._id, result)""",
"reduce" -> """function() {}""",
"jsMode" -> true
)
)(previewBSONReader, ec),
def fetch(studyId: Study.Id, page: Int, playing: Boolean): Fu[Paginator[ChapterPreview]] =
Paginator[ChapterPreview](
new ChapterPreviewAdapter(studyId, playing),
currentPage = page,
maxPerPage = maxPerPage
)
final private class ChapterPreviewAdapter(studyId: Study.Id, playing: Boolean)
extends AdapterLike[ChapterPreview] {
private val selector = $doc("studyId" -> studyId) ++ playing.??(playingSelector)
def nbResults: Fu[Int] = chapterRepo.coll.secondaryPreferred.countSel(selector)
def slice(offset: Int, length: Int): Fu[Seq[ChapterPreview]] =
chapterRepo.coll
.aggregateList(length, readPreference = ReadPreference.secondaryPreferred) { framework =>
import framework._
Match(selector) -> List(
Sort(Ascending("order")),
Skip(offset),
Limit(length),
Project(
$doc(
"comp" -> $doc(
"$function" -> $doc(
"lang" -> "js",
"args" -> $arr("$root", "$tags"),
"body" -> """function(node, tags) { tags = tags.filter(t => t.startsWith('White') || t.startsWith('Black') || t.startsWith('Result')); if (tags.length) while(child = node.n[0]) { node = child }; return {node:{fen:node.f,uci:node.u},tags} }"""
)
),
"orientation" -> "$setup.orientation",
"name" -> true
)
)
)
}
.map { r =>
for {
doc <- r
id <- doc.getAsOpt[Chapter.Id]("_id")
name <- doc.getAsOpt[Chapter.Name]("name")
comp <- doc.getAsOpt[Bdoc]("comp")
node <- comp.getAsOpt[Bdoc]("node")
fen <- node.getAsOpt[FEN]("fen")
lastMove = node.getAsOpt[Uci]("uci")
tags = comp.getAsOpt[Tags]("tags")
} yield ChapterPreview(
id = id,
name = name,
players = tags flatMap ChapterPreview.players,
orientation = doc.getAsOpt[Color]("orientation") | Color.White,
fen = fen,
lastMove = lastMove,
playing = lastMove.isDefined && tags.flatMap(_(_.Result)).has("*")
)
}
}
private val playingSelector = $doc("tags" -> "Result:*", "root.n.0" $exists true)
private object handlers {
implicit val previewBSONReader = new BSONDocumentReader[ChapterPreview] {
def readDocument(result: BSONDocument) =
for {
value <- result.getAsTry[List[Bdoc]]("value")
doc <- value.headOption toTry "No mapReduce value?!"
tags = doc.getAsOpt[Tags]("tags")
lastMove = doc.getAsOpt[Uci]("uci")
} yield ChapterPreview(
id = result.getAsOpt[Chapter.Id]("_id") err "Preview missing id",
name = doc.getAsOpt[Chapter.Name]("name") err "Preview missing name",
players = tags flatMap ChapterPreview.players,
orientation = doc.getAsOpt[Color]("orientation") getOrElse Color.White,
fen = doc.getAsOpt[FEN]("fen") err "Preview missing FEN",
lastMove = lastMove,
playing = lastMove.isDefined && tags.flatMap(_(_.Result)).has("*")
)
}
implicit val previewPlayerWriter: Writes[ChapterPreview.Player] = Writes[ChapterPreview.Player] { p =>
Json
.obj("name" -> p.name)

View File

@ -28,7 +28,7 @@ final private class StudySequencer(
): Funit =
sequenceStudy(studyId) { study =>
chapterRepo.byId(chapterId) flatMap {
_ ?? { chapter =>
_.filter(_.studyId == studyId) ?? { chapter =>
f(Study.WithChapter(study, chapter))
}
}

View File

@ -608,6 +608,23 @@ final class SwissApi(
_.map { withdraw(_, user.id) }.sequenceFu.void
}
def isUnfinished(id: Swiss.Id): Fu[Boolean] =
colls.swiss.exists($id(id) ++ $doc("finishedAt" $exists false))
def filterPlaying(id: Swiss.Id, userIds: Seq[User.ID]): Fu[List[User.ID]] =
userIds.nonEmpty ??
colls.swiss.exists($id(id) ++ $doc("finishedAt" $exists false)) flatMap {
_ ?? SwissPlayer.fields { f =>
colls.player.distinctEasy[User.ID, List](
f.userId,
$doc(
f.id $in userIds.map(SwissPlayer.makeId(id, _)),
f.absent $ne true
)
)
}
}
def resultStream(swiss: Swiss, perSecond: MaxPerSecond, nb: Int): Source[(SwissPlayer, Long), _] =
SwissPlayer.fields { f =>
colls.player

View File

@ -39,7 +39,7 @@ final class Env(
lazy val paginator = wire[PaginatorBuilder]
lazy val cli = wire[Cli]
lazy val cli = wire[TeamCli]
lazy val cached: Cached = wire[Cached]

View File

@ -2,7 +2,7 @@ package lila.team
import lila.db.dsl._
final private[team] class Cli(
final private[team] class TeamCli(
teamRepo: TeamRepo,
api: TeamApi
)(implicit ec: scala.concurrent.ExecutionContext)

View File

@ -7,6 +7,7 @@ import lila.hub.LightTeam.TeamID
import lila.memo._
import lila.memo.CacheApi._
import lila.user.User
import play.api.libs.json.JsArray
final private[tournament] class Cached(
playerRepo: PlayerRepo,
@ -59,6 +60,19 @@ final private[tournament] class Cached(
.buildAsyncFuture(playerRepo.computeRanking)
}
object battle {
val teamStanding =
cacheApi[Tournament.ID, List[TeamBattle.RankedTeam]](8, "tournament.teamStanding") {
_.expireAfterWrite(1 second)
.buildAsyncFuture { id =>
tournamentRepo teamBattleOf id flatMap {
_ ?? { playerRepo.bestTeamIdsByTour(id, _) }
}
}
}
}
private[tournament] object sheet {
import arena.Sheet

View File

@ -82,6 +82,7 @@ final class JsonView(
battle.teams.intersect(teams.toSet).toList
}))
teamStanding <- getTeamStanding(tour)
myTeam <- myInfo.flatMap(_.teamId) ?? { getMyRankedTeam(tour, _) }
} yield Json
.obj(
"nbPlayers" -> tour.nbPlayers,
@ -101,6 +102,7 @@ final class JsonView(
.add("stats" -> stats)
.add("socketVersion" -> socketVersion.map(_.value))
.add("teamStanding" -> teamStanding)
.add("myTeam" -> myTeam)
.add("duelTeams" -> data.duelTeams) ++
full.?? {
Json
@ -148,7 +150,7 @@ final class JsonView(
_ ?? { player =>
fetchCurrentGameId(tour, me) flatMap { gameId =>
getOrGuessRank(tour, player) dmap { rank =>
MyInfo(rank + 1, player.withdraw, gameId).some
MyInfo(rank + 1, player.withdraw, gameId, player.team).some
}
}
}
@ -363,24 +365,18 @@ final class JsonView(
"p" -> Json.arr(u1, u2)
)
private val teamStandingCache = cacheApi[Tournament.ID, JsArray](4, "tournament.teamStanding") {
_.expireAfterWrite(1 second)
def getTeamStanding(tour: Tournament): Fu[Option[JsArray]] =
tour.isTeamBattle ?? { teamStandingJsonCache get tour.id dmap some }
private val teamStandingJsonCache = cacheApi[Tournament.ID, JsArray](4, "tournament.teamStanding") {
_.expireAfterWrite(500 millis)
.buildAsyncFuture { id =>
tournamentRepo.teamBattleOf(id) flatMap {
_.fold(fuccess(JsArray())) { battle =>
playerRepo.bestTeamIdsByTour(id, battle) map { ranked =>
JsArray(ranked map teamBattleRankedWrites.writes)
}
}
cached.battle.teamStanding.get(id) map { ranked =>
JsArray(ranked take TeamBattle.displayTeams map teamBattleRankedWrites.writes)
}
}
}
def getTeamStanding(tour: Tournament): Fu[Option[JsArray]] =
tour.isTeamBattle ?? {
teamStandingCache get tour.id dmap some
}
implicit private val teamBattleRankedWrites: Writes[TeamBattle.RankedTeam] = OWrites { rt =>
Json.obj(
"rank" -> rt.rank,
@ -395,6 +391,12 @@ final class JsonView(
)
}
private def getMyRankedTeam(tour: Tournament, teamId: TeamID): Fu[Option[TeamBattle.RankedTeam]] =
tour.teamBattle.exists(_.hasTooManyTeams) ??
cached.battle.teamStanding.get(tour.id) map {
_.find(_.teamId == teamId)
}
private val teamInfoCache =
cacheApi[(Tournament.ID, TeamID), Option[JsObject]](16, "tournament.teamInfo.json") {
_.expireAfterWrite(5 seconds)

View File

@ -55,7 +55,7 @@ final class PlayerRepo(coll: Coll)(implicit ec: scala.concurrent.ExecutionContex
): Fu[List[TeamBattle.RankedTeam]] = {
import TeamBattle.{ RankedTeam, TeamLeader }
coll
.aggregateList(maxDocs = 10) { framework =>
.aggregateList(maxDocs = TeamBattle.maxTeams) { framework =>
import framework._
Match(selectTour(tourId)) -> List(
Sort(Descending("m")),

View File

@ -11,10 +11,15 @@ case class TeamBattle(
) {
def hasEnoughTeams = teams.sizeIs > 1
lazy val sortedTeamIds = teams.toList.sorted
def hasTooManyTeams = teams.sizeIs > TeamBattle.displayTeams
}
object TeamBattle {
val maxTeams = 200
val displayTeams = 10
def init(teamId: TeamID) = TeamBattle(Set(teamId), 5)
case class TeamVs(teams: chess.Color.Map[TeamID])
@ -58,8 +63,8 @@ object TeamBattle {
)(Setup.apply)(Setup.unapply)
.verifying("We need at least 2 teams", s => s.potentialTeamIds.sizeIs > 1)
.verifying(
"In this version of team battles, no more than 10 teams can be allowed.",
s => s.potentialTeamIds.sizeIs <= 10
s"In this version of team battles, no more than $maxTeams teams can be allowed.",
s => s.potentialTeamIds.sizeIs <= maxTeams
)
def edit(teams: List[String], nbLeaders: Int) =

View File

@ -14,7 +14,7 @@ object CrudForm {
import TournamentForm._
import lila.common.Form.UTCDate._
val maxHomepageHours = 72
val maxHomepageHours = 24
lazy val apply = Form(
mapping(

View File

@ -2,6 +2,8 @@ package lila.tournament
import play.api.i18n.Lang
import lila.hub.LightTeam.TeamID
final class LeaderboardRepo(val coll: lila.db.dsl.Coll)
case class TournamentTop(value: List[Player]) extends AnyVal
@ -25,7 +27,7 @@ case class GameView(
def tourAndTeamVs = TourAndTeamVs(tour, teamVs)
}
case class MyInfo(rank: Int, withdraw: Boolean, gameId: Option[lila.game.Game.ID]) {
case class MyInfo(rank: Int, withdraw: Boolean, gameId: Option[lila.game.Game.ID], teamId: Option[TeamID]) {
def page = (rank + 9) / 10
}

View File

@ -98,8 +98,8 @@ final class Cached(
private def userIdsLikeFetch(text: String) =
userRepo.userIdsLikeFilter(text, $empty, 12)
private val userIdsLikeCache = cacheApi[String, List[User.ID]](64, "user.like") {
_.expireAfterWrite(3 minutes).buildAsyncFuture(userIdsLikeFetch)
private val userIdsLikeCache = cacheApi[String, List[User.ID]](1024, "user.like") {
_.expireAfterWrite(5 minutes).buildAsyncFuture(userIdsLikeFetch)
}
def userIdsLike(text: String): Fu[List[User.ID]] = {

View File

@ -8,7 +8,7 @@ object Dependencies {
val scalalib = "com.github.ornicar" %% "scalalib" % "7.0.2"
val hasher = "com.roundeights" %% "hasher" % "1.2.1"
val jodaTime = "joda-time" % "joda-time" % "2.10.9"
val chess = "org.lichess" %% "scalachess" % "10.1.6"
val chess = "org.lichess" %% "scalachess" % "10.1.7"
val compression = "org.lichess" %% "compression" % "1.6"
val maxmind = "com.sanoma.cda" %% "maxmind-geoip2-scala" % "1.3.1-THIB"
val prismic = "io.prismic" %% "scala-kit" % "1.2.19-THIB213"
@ -22,7 +22,7 @@ object Dependencies {
val autoconfig = "io.methvin.play" %% "autoconfig-macros" % "0.3.2" % "provided"
val scalatest = "org.scalatest" %% "scalatest" % "3.1.0" % Test
val uaparser = "org.uaparser" %% "uap-scala" % "0.11.0"
val specs2 = "org.specs2" %% "specs2-core" % "4.10.5" % Test
val specs2 = "org.specs2" %% "specs2-core" % "4.10.6" % Test
val apacheText = "org.apache.commons" % "commons-text" % "1.9"
object flexmark {

File diff suppressed because one or more lines are too long

View File

@ -7,8 +7,8 @@
<item quantity="other">Lichess.org\'u %1$s aylığına %2$s olaraq dəstəklədi</item>
</plurals>
<plurals name="practicedNbPositions">
<item quantity="one">%2$s mövzusunda %1$s mövqe məşq etdi</item>
<item quantity="other">%2$s mövzusunda %1$s mövqe məşq etdi</item>
<item quantity="one">%2$s mövzusunda %1$s pozisiya məşq etdi</item>
<item quantity="other">%2$s mövzusunda %1$s pozisiya məşq etdi</item>
</plurals>
<plurals name="solvedNbPuzzles">
<item quantity="one">%s taktiki tapmaca həll etdi</item>

View File

@ -62,7 +62,7 @@
<item quantity="one">Κατατάχθηκε #%1$s (κορυφαίος %2$s%%) με %3$s παιχνίδι σε %4$s</item>
<item quantity="other">Κατατάχθηκε #%1$s (κορυφαίος %2$s%%) με %3$s παιχνίδια σε %4$s</item>
</plurals>
<string name="signedUp">Εγγεγραμμένοι στο lichess</string>
<string name="signedUp">Έκανε εγγραφή στο lichess.org</string>
<plurals name="joinedNbTeams">
<item quantity="one">Έγινε μέλος %s ομάδας</item>
<item quantity="other">Έγινε μέλος %s ομαδών</item>

View File

@ -3,13 +3,13 @@
<string name="arenaTournaments">Arena turnirləri</string>
<string name="isItRated">Reytinq hesablanırmı?</string>
<string name="willBeNotified">Turnir başlayanda sizə xəbər veriləcəkdir, buna görə gözləyərkən başqa bir pəncərədə oynamaq təhlükəsizdir.</string>
<string name="isRated">Bu turnir reytinqlidir və reytinqinizə tesir edəcək.</string>
<string name="isNotRated">Bu turnir reytinqli deyil ve reytinqinizə təsir etməyəcək.</string>
<string name="someRated">Bəzi turnirlər reytinqlidir ve reytinqinizə təsir edəcək.</string>
<string name="isRated">Bu turnir reytinqlidir və reytinqinizə təsir edəcək.</string>
<string name="isNotRated">Bu turnir reytinqli deyil və reytinqinizə təsir etməyəcək.</string>
<string name="someRated">Bəzi turnirlər reytinqlidir və reytinqinizə təsir edəcək.</string>
<string name="howAreScoresCalculated">Xallar necə hesablanır?</string>
<string name="howAreScoresCalculatedAnswer">Qazanılan bir oyun təməldə 2 xal, bərabərlik 1 xal dəyərindədir. Uduzulan bir oyun isə heçbir xal qazandırmır. Üst- üstə iki oyun qazandığınız təqdirdə alov ikonu ile təmsil olunan ikiqat xal seriyasına girirsiniz.
Məğlub olana vəya bərabərə qalana qədər oynanılan oyunlardan iki qatı xal qazanırsınız. Yəni seriyada ikən bir qalibiyyət 4 xal, bərabərlik 2 xal qazandırarkən uduzulan oyundan heçbir xal qazanılmaz.
Məğlub olana vəya bərabərə qalana qədər oynanılan oyunlardan iki qatı xal qazanırsınız. Yəni seriyada ikən bir qalibiyyət 4 xal, bərabərlik 2 xal qazandırarkən uduzulan oyundan heç bir xal qazanılmaz.
Məsələn; üst-üstə qazanılan iki oyun və ardından alınan bir bərabərlik 6 xal dəyərindədir: 2 + 2 + (2 x 1)</string>
<string name="berserk">Çıın Modu</string>
@ -26,7 +26,7 @@ Berserk yalnız oyunda ən azı 7 gediş etsəniz əlavə bir xal verir.</string
İki və ya daha çox oyunçu eyni xala sahib olduqda, turnir taybrekə qalacaq.</string>
<string name="howDoesPairingWork">Qoşulma necə işləyir?</string>
<string name="howDoesPairingWorkAnswer">Turnirin əvvəlində oyunçular reytinqlərinə görə qarşılaşırlar.
Bir oyunu bitirən kimi turnir lobbisinə qayıdın: daha sonra reytinqinizə yaxın bir oyunçu ilə qarşılaşacaqsınız. Beləliklə gözləmə müddətini ən aza edirilir, lakin turnirdəki bütün digər oyunçularla qarşılaşa bilməzsiniz.
Bir oyunu bitirən kimi turnir lobbisinə qayıdın: daha sonra reytinqinizə yaxın bir oyunçu ilə qarşılaşacaqsınız. Beləliklə gözləmə müddətini ən aza endirilir, lakin turnirdəki bütün digər oyunçularla qarşılaşa bilməzsiniz.
Daha çox oyun oynamaq və daha çox xal qazanmaq üçün sürətli oynayın və lobbiyə qayıdın.</string>
<string name="howDoesItEnd">Necə başa çatır?</string>
<string name="howDoesItEndAnswer">Turnirin başa çatma vaxtı var. Vaxt başa çatdıqda turnir sıralaması dondurulur və qalib elan edilir. Davam edən oyunlar başa çatmalıdır, lakin bu oyunlar turnirə daxil edilməz.</string>

View File

@ -36,6 +36,6 @@ Jouez vite et retournez à la page d\'accueil du tournoi pour jouer plus de part
</plurals>
<string name="thisIsPrivate">Ce tournoi est privé</string>
<string name="shareUrl">Partagez cette URL pour permettre aux personnes de rejoindre : %s</string>
<string name="drawStreak">Séries de parties nulles : lorsqu\'un joueur fait des nulles consécutives dans une arène, seul la première nulle donnera un point, ou des nulles durant plus de %s coups. La série de nulles ne peut être interrompue que par une victoire, pas par une défaite ou une nulle.</string>
<string name="drawStreak">Séries de parties nulles : lorsqu\'un joueur fait des nulles consécutives dans une arène ou une bataille d\'équipes, seule la première nulle donnera un point, ou des nulles durant plus de %s coups. La série de nulles ne peut être interrompue que par une victoire, pas par une défaite ou une nulle.</string>
<string name="history">Historique des tournois d\'arène</string>
</resources>

View File

@ -13,7 +13,7 @@
<string name="sourceUrl">Mənbə URL</string>
<string name="sourceUrlHelp">Lichess, verdiyiniz URL ilə PGN-i yeniləyəcək. URL, internetdə hamı tərəfindən əldə edilə bilən olmalıdır.</string>
<string name="roundNumber">Tur sayı</string>
<string name="startDate">Öz vaxt qurşağınızdakı başlama tarixi</string>
<string name="startDate">Öz saat qurşağınızdakı başlama tarixi</string>
<string name="startDateHelp">İstəyə bağlı, tədbirin başlama vaxtını bilirsinizsə</string>
<string name="credits">Mənbəyə bax</string>
<string name="cloneBroadcast">Yayımı klonla</string>

View File

@ -9,6 +9,7 @@
<string name="eventName">Назив догађаја</string>
<string name="eventDescription">Кратак опис догађаја</string>
<string name="fullDescription">Цео опис догађаја</string>
<string name="sourceUrl">Изворни УРЛ</string>
<string name="roundNumber">Број рунде</string>
<string name="startDate">Датум почетка у твојој временској зони</string>
<string name="cloneBroadcast">Клонирај емитовање</string>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

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