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
commit
50f512a790
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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] =
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -32,7 +32,7 @@ object msg {
|
|||
|
||||
private val i18nKeys = List(
|
||||
trans.inbox,
|
||||
trans.challengeToPlay,
|
||||
trans.challenge.challengeToPlay,
|
||||
trans.block,
|
||||
trans.unblock,
|
||||
trans.blocked,
|
||||
|
|
|
@ -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!")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)))
|
||||
)(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"))
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
),
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""}"
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) '*'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
|
@ -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)
|
||||
|
|
|
@ -28,7 +28,7 @@ object Metric {
|
|||
|
||||
import DataType._
|
||||
import Position._
|
||||
import Entry.{ BSONFields => F }
|
||||
import InsightEntry.{ BSONFields => F }
|
||||
|
||||
case object MeanCpl
|
||||
extends Metric(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)(
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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._
|
||||
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")),
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -14,7 +14,7 @@ object CrudForm {
|
|||
import TournamentForm._
|
||||
import lila.common.Form.UTCDate._
|
||||
|
||||
val maxHomepageHours = 72
|
||||
val maxHomepageHours = 24
|
||||
|
||||
lazy val apply = Form(
|
||||
mapping(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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]] = {
|
||||
|
|
|
@ -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
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">Çılğı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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
|
@ -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
Loading…
Reference in New Issue