merge + tweaks, post-log4j

deepcrayonfish
server 2021-12-11 14:38:35 -07:00
commit 07fe9c8817
245 changed files with 1704 additions and 856 deletions

2
.gitignore vendored
View File

@ -55,3 +55,5 @@ lost+found
nohup.out nohup.out
nohup.out.old nohup.out.old
ATTIC ATTIC
# IntelliJ auto-generated files
.idea

View File

@ -4,3 +4,4 @@ maxColumn = 110
spaces.inImportCurlyBraces = true spaces.inImportCurlyBraces = true
rewrite.rules = [SortImports, RedundantParens, SortModifiers] rewrite.rules = [SortImports, RedundantParens, SortModifiers]
rewrite.redundantBraces.stringInterpolation = true rewrite.redundantBraces.stringInterpolation = true
runner.dialect = scala213

View File

@ -117,16 +117,21 @@ final class Env(
text = "Team IDs that always get their tournaments visible on /tournament. Separated by commas.".some text = "Team IDs that always get their tournaments visible on /tournament. Separated by commas.".some
) )
lazy val prizeTournamentMakers = memo.settingStore[UserIds]( lazy val prizeTournamentMakers = memo.settingStore[UserIds](
"prizeTournamentMakers ", "prizeTournamentMakers",
default = UserIds(Nil), default = UserIds(Nil),
text = text =
"User IDs who can make prize tournaments (arena & swiss) without a warning. Separated by commas.".some "User IDs who can make prize tournaments (arena & swiss) without a warning. Separated by commas.".some
) )
lazy val apiExplorerGamesPerSecond = memo.settingStore[Int]( lazy val apiExplorerGamesPerSecond = memo.settingStore[Int](
"apiExplorerGamesPerSecond ", "apiExplorerGamesPerSecond",
default = 200, default = 300,
text = "Opening explorer games per second".some text = "Opening explorer games per second".some
) )
lazy val pieceImageExternal = memo.settingStore[Boolean](
"pieceImageExternal",
default = false,
text = "Use external piece images".some
)
lazy val preloader = wire[mashup.Preload] lazy val preloader = wire[mashup.Preload]
lazy val socialInfo = wire[mashup.UserInfo.SocialApi] lazy val socialInfo = wire[mashup.UserInfo.SocialApi]

View File

@ -10,6 +10,7 @@ import lila.api.{ Context, GameApiV2 }
import lila.app._ import lila.app._
import lila.common.config.{ MaxPerPage, MaxPerSecond } import lila.common.config.{ MaxPerPage, MaxPerSecond }
import lila.common.{ HTTPRequest, IpAddress } import lila.common.{ HTTPRequest, IpAddress }
import lila.common.LightUser
final class Api( final class Api(
env: Env, env: Env,
@ -74,17 +75,20 @@ final class Api(
def usersStatus = def usersStatus =
ApiRequest { req => ApiRequest { req =>
val ids = get("ids", req).??(_.split(',').take(100).toList map lila.user.User.normalize) val ids = get("ids", req).??(_.split(',').take(100).toList map lila.user.User.normalize)
env.user.lightUserApi asyncMany ids dmap (_.flatten) map { users => env.user.lightUserApi asyncMany ids dmap (_.flatten) flatMap { users =>
val streamingIds = env.streamer.liveStreamApi.userIds val streamingIds = env.streamer.liveStreamApi.userIds
toApiResult { def toJson(u: LightUser) =
users.map { u => lila.common.LightUser.lightUserWrites
lila.common.LightUser.lightUserWrites .writes(u)
.writes(u) .add("online" -> env.socket.isOnline(u.id))
.add("online" -> env.socket.isOnline(u.id)) .add("playing" -> env.round.playing(u.id))
.add("playing" -> env.round.playing(u.id)) .add("streaming" -> streamingIds(u.id))
.add("streaming" -> streamingIds(u.id)) if (getBool("withGameIds", req)) users.map { u =>
(env.round.playing(u.id) ?? env.game.cached.lastPlayedPlayingId(u.id)) map { gameId =>
toJson(u).add("playingId", gameId)
} }
} }.sequenceFu map toApiResult
else fuccess(toApiResult(users map toJson))
} }
} }

View File

@ -26,7 +26,9 @@ final class Dev(env: Env) extends LilaController(env) {
env.fishnet.openingBookDepth, env.fishnet.openingBookDepth,
env.noDelaySecretSetting, env.noDelaySecretSetting,
env.featuredTeamsSetting, env.featuredTeamsSetting,
env.prizeTournamentMakers env.prizeTournamentMakers,
env.pieceImageExternal,
env.evalCache.enable
) )
def settings = def settings =

View File

@ -7,7 +7,7 @@ final class Irwin(env: Env) extends LilaController(env) {
import lila.irwin.JSONHandlers.reportReader import lila.irwin.JSONHandlers.reportReader
def dashboard = def dashboard =
Secure(_.SeeReport) { implicit ctx => _ => Secure(_.MarkEngine) { implicit ctx => _ =>
env.irwin.api.dashboard map { d => env.irwin.api.dashboard map { d =>
Ok(views.html.irwin.dashboard(d)) Ok(views.html.irwin.dashboard(d))
} }

View File

@ -65,10 +65,11 @@ abstract private[controllers] class LilaController(val env: Env)
implicit def reqConfig(implicit req: RequestHeader) = ui.EmbedConfig(req) implicit def reqConfig(implicit req: RequestHeader) = ui.EmbedConfig(req)
def reqLang(implicit req: RequestHeader) = I18nLangPicker(req) def reqLang(implicit req: RequestHeader) = I18nLangPicker(req)
protected def EnableSharedArrayBuffer(res: Result): Result = protected def EnableSharedArrayBuffer(res: Result)(implicit req: RequestHeader): Result =
res.withHeaders( res.withHeaders(
"Cross-Origin-Opener-Policy" -> "same-origin", "Cross-Origin-Opener-Policy" -> "same-origin",
"Cross-Origin-Embedder-Policy" -> "require-corp" "Cross-Origin-Embedder-Policy" -> (if (HTTPRequest isChrome96OrMore req) "credentialless"
else "require-corp")
) )
protected def NoCache(res: Result): Result = protected def NoCache(res: Result): Result =

View File

@ -108,6 +108,7 @@ final class Mod(
suspect <- modApi.setTroll(me, prev, prev.user.marks.troll) suspect <- modApi.setTroll(me, prev, prev.user.marks.troll)
_ <- env.msg.api.systemPost(suspect.user.id, preset.text) _ <- env.msg.api.systemPost(suspect.user.id, preset.text)
_ <- env.mod.logApi.modMessage(me.id, suspect.user.id, preset.name) _ <- env.mod.logApi.modMessage(me.id, suspect.user.id, preset.name)
_ <- preset.isNameClose ?? env.irc.api.nameClosePreset(username)
} yield (inquiry, suspect).some } yield (inquiry, suspect).some
} }
} }

View File

@ -122,15 +122,22 @@ final class PlayApi(
def boardCommandGet(cmd: String) = def boardCommandGet(cmd: String) =
ScopedBody(_.Board.Play) { implicit req => me => ScopedBody(_.Board.Play) { implicit req => me =>
cmd.split('/') match { cmd.split('/') match {
case Array("game", id, "chat") => case Array("game", id, "chat") => WithPovAsBoard(id, me)(getChat)
WithPovAsBoard(id, me) { pov => case _ => notFoundJson("No such command")
env.chat.api.userChat.find(lila.chat.Chat.Id(pov.game.id)) map
lila.chat.JsonView.boardApi map JsonOk
}
case _ => notFoundJson("No such command")
} }
} }
def botCommandGet(cmd: String) =
ScopedBody(_.Bot.Play) { implicit req => me =>
cmd.split('/') match {
case Array("game", id, "chat") => WithPovAsBot(id, me)(getChat)
case _ => notFoundJson("No such command")
}
}
private def getChat(pov: Pov) =
env.chat.api.userChat.find(lila.chat.Chat.Id(pov.game.id)) map lila.chat.JsonView.boardApi map JsonOk
// utils // utils
private def toResult(f: Funit): Fu[Result] = catchClientError(f inject jsonOkResult) private def toResult(f: Funit): Fu[Result] = catchClientError(f inject jsonOkResult)

View File

@ -131,7 +131,7 @@ final class Round(
Open { implicit ctx => Open { implicit ctx =>
proxyPov(gameId, color) flatMap { proxyPov(gameId, color) flatMap {
case Some(pov) => case Some(pov) =>
get("pov") match { get("pov").map(UserModel.normalize) match {
case Some(requestedPov) => case Some(requestedPov) =>
(pov.player.userId, pov.opponent.userId) match { (pov.player.userId, pov.opponent.userId) match {
case (Some(_), Some(opponent)) if opponent == requestedPov => case (Some(_), Some(opponent)) if opponent == requestedPov =>

View File

@ -328,15 +328,17 @@ final class Study(
env.study.api.importPgns( env.study.api.importPgns(
StudyModel.Id(id), StudyModel.Id(id),
data.toChapterDatas, data.toChapterDatas,
sticky = data.sticky sticky = data.sticky,
ctx.pref.showRatings
)(Who(me.id, lila.socket.Socket.Sri(sri))) )(Who(me.id, lila.socket.Socket.Sri(sri)))
) )
} }
} }
def admin(id: String) = def admin(id: String) =
Secure(_.StudyAdmin) { _ => me => Secure(_.StudyAdmin) { ctx => me =>
env.study.api.adminInvite(id, me) inject Redirect(routes.Study.show(id)) env.study.api.adminInvite(id, me) inject (if (HTTPRequest isXhr ctx.req) NoContent
else Redirect(routes.Study.show(id)))
} }
def embed(id: String, chapterId: String) = def embed(id: String, chapterId: String) =

View File

@ -22,12 +22,11 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
def assetVersion = AssetVersion.current def assetVersion = AssetVersion.current
def assetUrl(path: String): String = s"$assetBaseUrl/assets/_$assetVersion/$path" def assetUrl(path: String): String = s"$assetBaseUrl/assets/_$assetVersion/$path"
def staticAssetUrl(path: String): String = s"$assetBaseUrl/assets/$path"
def cdnUrl(path: String) = s"$assetBaseUrl$path" def cdnUrl(path: String) = s"$assetBaseUrl$path"
def dbImageUrl(path: String) = s"$assetBaseUrl/image/$path"
def cssTag(name: String)(implicit ctx: Context): Frag = def cssTag(name: String)(implicit ctx: Context): Frag =
cssTagWithTheme(name, ctx.currentBg) cssTagWithTheme(name, ctx.currentBg)

View File

@ -51,6 +51,6 @@ object Environment
) )
val spinner: Frag = raw( val spinner: Frag = raw(
"""<div class="spinner"><svg viewBox="-2 -2 54 54"><g mask="url(#mask)" fill="none" stroke="#888" stroke-dasharray="1"><path id="a" pathLength="1" stroke-width="3.779" d="m21.78 12.64c-1.284 8.436 8.943 12.7 14.54 17.61 3 2.632 4.412 4.442 5.684 7.93"/><path id="b" pathLength="1" stroke-width="4.157" d="m43.19 36.32c2.817-1.203 6.659-5.482 5.441-7.623-2.251-3.957-8.883-14.69-11.89-19.73-0.4217-0.7079-0.2431-1.835 0.5931-3.3 1.358-2.38 1.956-5.628 1.956-5.628"/><path id="c" pathLength="1" stroke-width="4.535" d="m37.45 2.178s-3.946 0.6463-6.237 2.234c-0.5998 0.4156-2.696 0.7984-3.896 0.6388-17.64-2.345-29.61 14.08-25.23 27.34 4.377 13.26 22.54 25.36 39.74 8.666"/></g></svg></div>""" """<div class="spinner"><svg viewBox="-2 -2 54 54"><g mask="url(#mask)" fill="none"><path id="a" stroke-width="3.779" d="m21.78 12.64c-1.284 8.436 8.943 12.7 14.54 17.61 3 2.632 4.412 4.442 5.684 7.93"/><path id="b" stroke-width="4.157" d="m43.19 36.32c2.817-1.203 6.659-5.482 5.441-7.623-2.251-3.957-8.883-14.69-11.89-19.73-0.4217-0.7079-0.2431-1.835 0.5931-3.3 1.358-2.38 1.956-5.628 1.956-5.628"/><path id="c" stroke-width="4.535" d="m37.45 2.178s-3.946 0.6463-6.237 2.234c-0.5998 0.4156-2.696 0.7984-3.896 0.6388-17.64-2.345-29.61 14.08-25.23 27.34 4.377 13.26 22.54 25.36 39.74 8.666"/></g></svg></div>"""
) )
} }

View File

@ -99,15 +99,16 @@ trait ScalatagsPrefix {
// what to import in a pure scalatags template // what to import in a pure scalatags template
trait ScalatagsTemplate trait ScalatagsTemplate
extends Styles extends ScalatagsBundle
with ScalatagsBundle
with ScalatagsAttrs with ScalatagsAttrs
with ScalatagsExtensions with ScalatagsExtensions
with ScalatagsSnippets with ScalatagsSnippets
with ScalatagsPrefix { with ScalatagsPrefix {
val trans = lila.i18n.I18nKeys val trans = lila.i18n.I18nKeys
def main = scalatags.Text.tags2.main def main = scalatags.Text.tags2.main
def cssWidth = scalatags.Text.styles.width
def cssHeight = scalatags.Text.styles.height
/* Convert play URLs to scalatags attributes with toString */ /* Convert play URLs to scalatags attributes with toString */
implicit val playCallAttr = genericAttr[play.api.mvc.Call] implicit val playCallAttr = genericAttr[play.api.mvc.Call]

View File

@ -17,7 +17,7 @@ object bits {
div(cls := "personal-data__header")( div(cls := "personal-data__header")(
p("Here is all personal information Lichess has about ", userLink(u)), p("Here is all personal information Lichess has about ", userLink(u)),
a(cls := "button", href := s"${routes.Account.data}?user=${u.id}&text=1", downloadAttr)( a(cls := "button", href := s"${routes.Account.data}?user=${u.id}&text=1", downloadAttr)(
trans.downloadRaw() trans.download()
) )
) )
) )

View File

@ -34,23 +34,38 @@ object layout {
def pieceSprite(ps: lila.pref.PieceSet): Frag = def pieceSprite(ps: lila.pref.PieceSet): Frag =
link( link(
id := "piece-sprite", id := "piece-sprite",
href := assetUrl(s"piece-css/$ps.css"), href := assetUrl(s"piece-css/$ps.${env.pieceImageExternal.get() ?? "external."}css"),
rel := "stylesheet" rel := "stylesheet"
) )
} }
import bits._ import bits._
private val noTranslate = raw("""<meta name="google" content="notranslate">""") private val noTranslate = raw("""<meta name="google" content="notranslate">""")
private def fontPreload(implicit ctx: Context) =
raw { private def preload(href: String, as: String, crossorigin: Boolean, tpe: Option[String] = None) =
s"""<link rel="preload" href="${assetUrl( raw(s"""<link rel="preload" href="$href" as="$as" ${tpe.??(t =>
s"font/lichess.woff2" s"""type="$t" """
)}" as="font" type="font/woff2" crossorigin>""" + )}${crossorigin ?? "crossorigin"}>""")
!ctx.pref.pieceNotationIsLetter ??
s"""<link rel="preload" href="${assetUrl( private def fontPreload(implicit ctx: Context) = frag(
s"font/lichess.chess.woff2" preload(assetUrl(s"font/lichess.woff2"), "font", crossorigin = true, "font/woff2".some),
)}" as="font" type="font/woff2" crossorigin>""" !ctx.pref.pieceNotationIsLetter option
preload(assetUrl(s"font/lichess.chess.woff2"), "font", crossorigin = true, "font/woff2".some)
)
private def boardPreload(implicit ctx: Context) = frag(
preload(assetUrl(s"images/board/${ctx.currentTheme.file}"), "image", crossorigin = false),
ctx.pref.is3d option
preload(s"images/staunton/board/${ctx.currentTheme3d.file}", "image", crossorigin = false)
)
private def piecesPreload(implicit ctx: Context) =
env.pieceImageExternal.get() option raw {
(for {
c <- List('w', 'b')
p <- List('K', 'Q', 'R', 'B', 'N', 'P')
href = staticAssetUrl(s"piece/${ctx.currentPieceSet.name}/$c$p.svg")
} yield s"""<link rel="preload" href="$href" as="image">""").mkString
} }
private val manifests = raw( private val manifests = raw(
"""<link rel="manifest" href="/manifest.json"><meta name="qitter:site" content="@foo">""" """<link rel="manifest" href="/manifest.json"><meta name="qitter:site" content="@foo">"""
) )
@ -149,7 +164,7 @@ object layout {
def lichessJsObject(nonce: Nonce)(implicit lang: Lang) = def lichessJsObject(nonce: Nonce)(implicit lang: Lang) =
embedJsUnsafe( embedJsUnsafe(
s"""lichess={load:new Promise(r=>{window.onload=r}),quantity:${lila.i18n s"""lichess={load:new Promise(r=>{document.addEventListener("DOMContentLoaded",r)}),quantity:${lila.i18n
.JsQuantity(lang)}};$timeagoLocaleScript""", .JsQuantity(lang)}};$timeagoLocaleScript""",
nonce nonce
) )
@ -229,7 +244,7 @@ object layout {
content := openGraph.fold(trans.siteDescription.txt())(o => o.description), content := openGraph.fold(trans.siteDescription.txt())(o => o.description),
name := "description" name := "description"
), ),
link(rel := "mask-icon", href := assetUrl("logo/lichess.svg"), color := "black"), link(rel := "mask-icon", href := assetUrl("logo/lichess.svg"), attr("color") := "black"),
favicons, favicons,
!robots option raw("""<meta content="noindex, nofollow" name="robots">"""), !robots option raw("""<meta content="noindex, nofollow" name="robots">"""),
noTranslate, noTranslate,
@ -248,6 +263,8 @@ object layout {
) )
}, },
fontPreload, fontPreload,
boardPreload,
piecesPreload,
manifests, manifests,
jsLicense jsLicense
), ),

View File

@ -32,8 +32,8 @@ object notFound {
iframe( iframe(
src := assetUrl(s"vendor/ChessPursuit/bin-release/index.html"), src := assetUrl(s"vendor/ChessPursuit/bin-release/index.html"),
st.frameborder := 0, st.frameborder := 0,
width := 400, widthA := 400,
height := 500 heightA := 500
), ),
p(cls := "credits")( p(cls := "credits")(
a(href := "https://github.com/Saturnyn/ChessPursuit")("ChessPursuit"), a(href := "https://github.com/Saturnyn/ChessPursuit")("ChessPursuit"),

View File

@ -46,8 +46,8 @@ object picture {
img( img(
widthA := Coach.imageSize, widthA := Coach.imageSize,
heightA := Coach.imageSize, heightA := Coach.imageSize,
width := cssSize, cssWidth := cssSize,
height := cssSize, cssHeight := cssSize,
cls := "picture", cls := "picture",
src := url(c.coach), src := url(c.coach),
alt := s"${c.user.titleUsername} Lichess coach picture" alt := s"${c.user.titleUsername} Lichess coach picture"

View File

@ -97,8 +97,8 @@ object show {
div(cls := "list")( div(cls := "list")(
profile.youtubeUrls.map { url => profile.youtubeUrls.map { url =>
iframe( iframe(
width := "256", widthA := "256",
height := "192", heightA := "192",
src := url.value, src := url.value,
attr("frameborder") := "0", attr("frameborder") := "0",
frame.allowfullscreen frame.allowfullscreen

View File

@ -47,16 +47,16 @@ object mobile {
), ),
div(cls := "right-side")( div(cls := "right-side")(
img( img(
cls := "nexus5-playing", widthA := "437",
width := "268", heightA := "883",
height := "513", cls := "mobile-playing",
src := assetUrl("images/mobile/nexus5-playing.png"), src := assetUrl("images/mobile/lichesstv-mobile.png"),
alt := "Lichess mobile on nexus 5" alt := "Lichess TV on mobile"
), ),
img( img(
cls := "qrcode", cls := "qrcode",
width := "200", widthA := "200",
height := "200", heightA := "200",
src := assetUrl("images/mobile/dynamic-qrcode.png"), src := assetUrl("images/mobile/dynamic-qrcode.png"),
alt := "Download QR code" alt := "Download QR code"
) )

View File

@ -30,7 +30,7 @@ object menu {
a(cls := active.active("tour"), href := routes.TournamentCrud.index(1))("Tournaments"), a(cls := active.active("tour"), href := routes.TournamentCrud.index(1))("Tournaments"),
isGranted(_.ManageEvent) option isGranted(_.ManageEvent) option
a(cls := active.active("event"), href := routes.Event.manager)("Events"), a(cls := active.active("event"), href := routes.Event.manager)("Events"),
isGranted(_.SeeReport) option isGranted(_.MarkEngine) option
a(cls := active.active("irwin"), href := routes.Irwin.dashboard)("Irwin dashboard"), a(cls := active.active("irwin"), href := routes.Irwin.dashboard)("Irwin dashboard"),
isGranted(_.Shadowban) option isGranted(_.Shadowban) option
a(cls := active.active("panic"), href := routes.Mod.chatPanic)( a(cls := active.active("panic"), href := routes.Mod.chatPanic)(

View File

@ -17,7 +17,8 @@ object authorize {
moreJs = embedJsUnsafe( moreJs = embedJsUnsafe(
// ensure maximum browser compatibility // ensure maximum browser compatibility
"""setTimeout(function(){var el=document.getElementById('oauth-authorize');el.removeAttribute('disabled');el.setAttribute('class','button')}, 2000);""" """setTimeout(function(){var el=document.getElementById('oauth-authorize');el.removeAttribute('disabled');el.setAttribute('class','button')}, 2000);"""
) ),
csp = defaultCsp.withLegacyCompatibility.some
) { ) {
main(cls := "oauth box box-pad")( main(cls := "oauth box box-pad")(
div(cls := "oauth__top")( div(cls := "oauth__top")(

View File

@ -167,14 +167,24 @@ object forms {
div(cls := "ratings")( div(cls := "ratings")(
form3.hidden("rating", "?"), form3.hidden("rating", "?"),
lila.rating.PerfType.nonPuzzle.map { perfType => lila.rating.PerfType.nonPuzzle.map { perfType =>
div(cls := perfType.key)( {
trans.perfRatingX( val rating = me
raw(s"""<strong data-icon="${perfType.iconChar}">${ctx.pref.showRatings ?? me .perfs(perfType.key)
.perfs(perfType.key) .map(_.intRating.toString)
.map(_.intRating.toString) .getOrElse("?")
.getOrElse("?")}</strong> ${perfType.trans}""") div(cls := perfType.key)(
if (ctx.pref.showRatings)
trans.perfRatingX(
raw(s"""<strong data-icon="${perfType.iconChar}">${rating}</strong> ${perfType.trans}""")
)
else
frag(
i(dataIcon := perfType.iconChar),
strong(cls := "none")(rating), // To calculate rating range in JS
perfType.trans
)
) )
) }
} }
) )
} }

View File

@ -25,8 +25,8 @@ object dailyPuzzleSlackApp {
)( )(
img( img(
alt := "Add to Slack", alt := "Add to Slack",
height := 40, heightA := 40,
width := 139, widthA := 139,
src := assetUrl("images/add-to-slack.png") src := assetUrl("images/add-to-slack.png")
) )
), ),

View File

@ -83,8 +83,7 @@ object show {
def round(s: Swiss, r: SwissRound.Number, pairings: Paginator[SwissPairing])(implicit ctx: Context) = def round(s: Swiss, r: SwissRound.Number, pairings: Paginator[SwissPairing])(implicit ctx: Context) =
views.html.base.layout( views.html.base.layout(
title = s"${fullName(s)} • Round $r/${s.round}", title = s"${fullName(s)} • Round $r/${s.round}",
moreCss = cssTag("swiss.show"), moreCss = cssTag("swiss.show")
moreJs = infiniteScrollTag
) { ) {
val pager = views.html.base.bits val pager = views.html.base.bits
.pagination(p => routes.Swiss.round(s.id.value, p).url, r.value, s.round.value, showPost = true) .pagination(p => routes.Swiss.round(s.id.value, p).url, r.value, s.round.value, showPost = true)
@ -95,7 +94,6 @@ object show {
), ),
pager(cls := "pagination--top"), pager(cls := "pagination--top"),
table(cls := "slist slist-pad")( table(cls := "slist slist-pad")(
tbody(cls := "infinite-scroll")(
pairings.currentPageResults map { p => pairings.currentPageResults map { p =>
tr(cls := "paginated")( tr(cls := "paginated")(
td(a(href := routes.Round.watcher(p.gameId, "white"), cls := "glpt")(s"#${p.gameId}")), td(a(href := routes.Round.watcher(p.gameId, "white"), cls := "glpt")(s"#${p.gameId}")),
@ -104,9 +102,7 @@ object show {
td(p strResultOf chess.Black), td(p strResultOf chess.Black),
td(userIdLink(p.black.some)) td(userIdLink(p.black.some))
) )
}, }
pagerNextTable(pairings, p => routes.Swiss.round(s.id.value, r.value).url)
)
), ),
pager(cls := "pagination--bottom") pager(cls := "pagination--bottom")
) )

View File

@ -73,7 +73,7 @@ object post {
post.lived map { live => post.lived map { live =>
span(cls := "ublog-post__meta__date")(semanticDate(live.at)) span(cls := "ublog-post__meta__date")(semanticDate(live.at))
}, },
likeButton(post, liked)(ctx)(cls := "ublog-post__like--mini button-link"), likeButton(post, liked, showText = false),
span(cls := "ublog-post__views")( span(cls := "ublog-post__views")(
trans.ublog.nbViews.plural(post.views.value, strong(post.views.value.localize)) trans.ublog.nbViews.plural(post.views.value, strong(post.views.value.localize))
), ),
@ -104,9 +104,7 @@ object post {
div(cls := "ublog-post__footer")( div(cls := "ublog-post__footer")(
(ctx.isAuth && !ctx.is(user)) option (ctx.isAuth && !ctx.is(user)) option
div(cls := "ublog-post__actions")( div(cls := "ublog-post__actions")(
likeButton(post, liked)(ctx)(cls := "ublog-post__like--big button button-big button-red")( likeButton(post, liked, showText = true),
span(cls := "button-label")("Like this post")
),
followButton(user, post, followed) followButton(user, post, followed)
), ),
h2(a(href := routes.Ublog.index(user.username))(trans.ublog.moreBlogPostsBy(user.username))), h2(a(href := routes.Ublog.index(user.username))(trans.ublog.moreBlogPostsBy(user.username))),
@ -122,15 +120,27 @@ object post {
dataIcon := "" dataIcon := ""
)(trans.edit()) )(trans.edit())
private def likeButton(post: UblogPost, liked: Boolean)(implicit ctx: Context) = button( private def likeButton(post: UblogPost, liked: Boolean, showText: Boolean)(implicit ctx: Context) = {
tpe := "button", val text = if (liked) trans.study.unlike.txt() else trans.study.like.txt()
cls := List( button(
"ublog-post__like is" -> true, tpe := "button",
"ublog-post__like--liked" -> liked cls := List(
), "ublog-post__like is" -> true,
dataRel := post.id.value, "ublog-post__like--liked" -> liked,
title := trans.study.like.txt() "ublog-post__like--big button button-big button-red" -> showText,
)(span(cls := "ublog-post__like__nb")(post.likes.value.localize)) "ublog-post__like--mini button-link" -> !showText
),
dataRel := post.id.value,
title := text
)(
span(cls := "ublog-post__like__nb")(post.likes.value.localize),
showText option span(
cls := "button-label",
attr("data-i18n-like") := trans.study.like.txt(),
attr("data-i18n-unlike") := trans.study.unlike.txt()
)(text)
)
}
private def followButton(user: User, post: UblogPost, followed: Boolean)(implicit ctx: Context) = private def followButton(user: User, post: UblogPost, followed: Boolean)(implicit ctx: Context) =
div( div(

View File

@ -37,9 +37,7 @@ object bots {
private def botTable(users: List[User])(implicit ctx: Context) = table(cls := "slist slist-pad")( private def botTable(users: List[User])(implicit ctx: Context) = table(cls := "slist slist-pad")(
tbody( tbody(
users.sortBy { u => users map { u =>
(if (u.isVerified) -1 else 1, -u.playTime.??(_.total))
} map { u =>
tr( tr(
td(userLink(u)), td(userLink(u)),
u.profile u.profile

View File

@ -62,7 +62,7 @@ object perfStat {
counter(stat.count), counter(stat.count),
highlow(stat), highlow(stat),
resultStreak(stat.resultStreak), resultStreak(stat.resultStreak),
result(stat), result(stat, user),
playStreakNb(stat.playStreak), playStreakNb(stat.playStreak),
playStreakTime(stat.playStreak) playStreakTime(stat.playStreak)
) )
@ -259,7 +259,9 @@ object perfStat {
resultStreakSide(streak.loss, losingStreak(), "red") resultStreakSide(streak.loss, losingStreak(), "red")
) )
private def resultTable(results: lila.perfStat.Results, title: Frag)(implicit lang: Lang): Frag = private def resultTable(results: lila.perfStat.Results, title: Frag, user: User)(implicit
lang: Lang
): Frag =
div( div(
table( table(
thead( thead(
@ -271,17 +273,21 @@ object perfStat {
results.results map { r => results.results map { r =>
tr( tr(
td(userIdLink(r.opId.value.some, withOnline = false), " (", r.opInt, ")"), td(userIdLink(r.opId.value.some, withOnline = false), " (", r.opInt, ")"),
td(a(cls := "glpt", href := routes.Round.watcher(r.gameId, "white"))(absClientDateTime(r.at))) td(
a(cls := "glpt", href := s"${routes.Round.watcher(r.gameId, "white")}?pov=${user.username}")(
absClientDateTime(r.at)
)
)
) )
} }
) )
) )
) )
private def result(stat: PerfStat)(implicit lang: Lang): Frag = private def result(stat: PerfStat, user: User)(implicit lang: Lang): Frag =
st.section(cls := "result split")( st.section(cls := "result split")(
resultTable(stat.bestWins, bestRated()), resultTable(stat.bestWins, bestRated(), user),
resultTable(stat.worstLosses, worstRated()) resultTable(stat.worstLosses, worstRated(), user)
) )
private def playStreakNbStreak(s: lila.perfStat.Streak, title: Frag => Frag)(implicit lang: Lang): Frag = private def playStreakNbStreak(s: lila.perfStat.Streak, title: Frag => Frag)(implicit lang: Lang): Frag =

View File

@ -46,7 +46,7 @@ object otherTrophies {
ariaTitle(t.kind.name), ariaTitle(t.kind.name),
style := "width: 65px; margin: 0 3px!important;" style := "width: 65px; margin: 0 3px!important;"
)( )(
img(src := assetUrl(s"images/trophy/${t.kind._id}.png"), width := 65, height := 80) img(src := assetUrl(s"images/trophy/${t.kind._id}.png"), cssWidth := 65, cssHeight := 80)
) )
}, },
info.trophies.filter(_.kind.klass.has("icon3d")).sorted.map { trophy => info.trophies.filter(_.kind.klass.has("icon3d")).sorted.map { trophy =>

View File

@ -108,9 +108,7 @@ pagerDuty {
serviceId = "" serviceId = ""
apiKey = "" apiKey = ""
} }
prismic { prismic.api_url = "https://lichess-clone.cdn.prismic.io/api"
api_url = "https://lichess.cdn.prismic.io/api"
}
blog { blog {
prismic = ${prismic} prismic = ${prismic}
collection = blog collection = blog

View File

@ -707,9 +707,10 @@ GET /api/user/:name/games controllers.Api.userGames(name: String)
# Bot API # Bot API
GET /api/bot/game/stream/:id controllers.PlayApi.botGameStream(id: String) GET /api/bot/game/stream/:id controllers.PlayApi.botGameStream(id: String)
POST /api/bot/game/:id/move/:uci controllers.PlayApi.botMove(id: String, uci: String, offeringDraw: Option[Boolean] ?= None) POST /api/bot/game/:id/move/:uci controllers.PlayApi.botMove(id: String, uci: String, offeringDraw: Option[Boolean] ?= None)
POST /api/bot/*cmd controllers.PlayApi.botCommand(cmd: String)
GET /player/bots controllers.PlayApi.botOnline
GET /api/bot/online controllers.PlayApi.botOnlineApi GET /api/bot/online controllers.PlayApi.botOnlineApi
POST /api/bot/*cmd controllers.PlayApi.botCommand(cmd: String)
GET /api/bot/*cmd controllers.PlayApi.botCommandGet(cmd: String)
GET /player/bots controllers.PlayApi.botOnline
# Board API # Board API
GET /api/board/game/stream/:id controllers.PlayApi.boardGameStream(id: String) GET /api/board/game/stream/:id controllers.PlayApi.boardGameStream(id: String)

View File

@ -6,7 +6,6 @@ import play.api.libs.json._
import lila.common.Iso import lila.common.Iso
import lila.common.Json._ import lila.common.Json._
import lila.game.JsonView.colorWrites
import lila.game.LightPov import lila.game.LightPov
import lila.rating.PerfType import lila.rating.PerfType
import lila.simul.Simul import lila.simul.Simul

View File

@ -8,7 +8,7 @@ import reactivemongo.api.ReadPreference
import lila.analyse.{ JsonView => analysisJson, Analysis } import lila.analyse.{ JsonView => analysisJson, Analysis }
import lila.common.config._ import lila.common.config._
import lila.common.Json.jodaWrites import lila.common.Json._
import lila.common.paginator.{ Paginator, PaginatorJson } import lila.common.paginator.{ Paginator, PaginatorJson }
import lila.db.dsl._ import lila.db.dsl._
import lila.db.paginator.Adapter import lila.db.paginator.Adapter

View File

@ -9,7 +9,7 @@ import scala.concurrent.duration._
import lila.analyse.{ JsonView => analysisJson, Analysis } import lila.analyse.{ JsonView => analysisJson, Analysis }
import lila.common.config.MaxPerSecond import lila.common.config.MaxPerSecond
import lila.common.Json.jodaWrites import lila.common.Json._
import lila.common.{ HTTPRequest, LightUser } import lila.common.{ HTTPRequest, LightUser }
import lila.db.dsl._ import lila.db.dsl._
import lila.game.JsonView._ import lila.game.JsonView._

View File

@ -6,6 +6,7 @@ import play.api.libs.ws.StandaloneWSClient
import lila.common.config.MaxPerPage import lila.common.config.MaxPerPage
import lila.common.paginator._ import lila.common.paginator._
import scala.util.Try
final class BlogApi( final class BlogApi(
config: BlogConfig config: BlogConfig
@ -20,7 +21,7 @@ final class BlogApi(
page: Int, page: Int,
maxPerPage: MaxPerPage, maxPerPage: MaxPerPage,
ref: Option[String] ref: Option[String]
): Fu[Option[Paginator[Document]]] = ): Fu[Option[Paginator[Document]]] = Try {
api api
.forms(collection) .forms(collection)
.ref(ref | api.master.ref) .ref(ref | api.master.ref)
@ -30,6 +31,9 @@ final class BlogApi(
.submit() .submit()
.fold(_ => none, some) .fold(_ => none, some)
.dmap2 { PrismicPaginator(_, page, maxPerPage) } .dmap2 { PrismicPaginator(_, page, maxPerPage) }
} recover { case _: NoSuchElementException =>
fuccess(none)
} get
def recent( def recent(
prismic: BlogApi.Context, prismic: BlogApi.Context,
@ -94,9 +98,7 @@ final class BlogApi(
} getOrElse reqRef } getOrElse reqRef
} }
private val prismicBuilder = new Prismic def prismicApi = (new Prismic).get(config.apiUrl)
def prismicApi = prismicBuilder.get(config.apiUrl)
} }
object BlogApi { object BlogApi {

View File

@ -3,6 +3,7 @@ package lila.bot
import play.api.i18n.Lang import play.api.i18n.Lang
import play.api.libs.json._ import play.api.libs.json._
import lila.common.Json._
import lila.common.Json.jodaWrites import lila.common.Json.jodaWrites
import lila.game.JsonView._ import lila.game.JsonView._
import lila.game.{ Game, GameRepo, Pov } import lila.game.{ Game, GameRepo, Pov }

View File

@ -1,8 +1,10 @@
package lila.challenge package lila.challenge
import play.api.libs.json._
import play.api.i18n.Lang import play.api.i18n.Lang
import play.api.libs.json._
import lila.common.Json._
import lila.game.JsonView._
import lila.i18n.{ I18nKeys => trans } import lila.i18n.{ I18nKeys => trans }
import lila.socket.Socket.SocketVersion import lila.socket.Socket.SocketVersion
import lila.socket.UserLagCache import lila.socket.UserLagCache
@ -13,7 +15,6 @@ final class JsonView(
isOnline: lila.socket.IsOnline isOnline: lila.socket.IsOnline
) { ) {
import lila.game.JsonView._
import Challenge._ import Challenge._
implicit private val RegisteredWrites = OWrites[Challenger.Registered] { r => implicit private val RegisteredWrites = OWrites[Challenger.Registered] { r =>

View File

@ -11,11 +11,9 @@ case class ContentSecurityPolicy(
baseUri: List[String] baseUri: List[String]
) { ) {
def withNonce(nonce: Nonce) = copy(scriptSrc = def withNonce(nonce: Nonce) = copy(scriptSrc = nonce.scriptSrc :: scriptSrc)
nonce.scriptSrc ::
"'unsafe-inline'" :: // ignored by browsers supporting nonce def withLegacyCompatibility = copy(scriptSrc = "'unsafe-inline'" :: scriptSrc)
scriptSrc
)
def withWebAssembly = def withWebAssembly =
copy( copy(

View File

@ -45,6 +45,7 @@ object HTTPRequest {
private def uaContains(req: RequestHeader, str: String) = userAgent(req).exists(_ contains str) private def uaContains(req: RequestHeader, str: String) = userAgent(req).exists(_ contains str)
def isChrome(req: RequestHeader) = uaContains(req, "Chrome/") def isChrome(req: RequestHeader) = uaContains(req, "Chrome/")
val isChrome96OrMore = UaMatcher("""Chrome/(?:\d{3,}|9[6-9])""")
def origin(req: RequestHeader): Option[String] = req.headers get HeaderNames.ORIGIN def origin(req: RequestHeader): Option[String] = req.headers get HeaderNames.ORIGIN
@ -58,7 +59,7 @@ object HTTPRequest {
def sid(req: RequestHeader): Option[String] = req.session get LilaCookie.sessionId def sid(req: RequestHeader): Option[String] = req.session get LilaCookie.sessionId
val isCrawler = UaMatcher { val isCrawler = UaMatcher {
"""(?i)googlebot|googlebot-mobile|googlebot-image|mediapartners-google|bingbot|slurp|java|wget|curl|commons-httpclient|python-urllib|libwww|httpunit|nutch|phpcrawl|msnbot|adidxbot|blekkobot|teoma|ia_archiver|gingercrawler|webmon|httrack|webcrawler|fast-webcrawler|fastenterprisecrawler|convera|biglotron|grub\.org|usinenouvellecrawler|antibot|netresearchserver|speedy|fluffy|jyxobot|bibnum\.bnf|findlink|exabot|gigabot|msrbot|seekbot|ngbot|panscient|yacybot|aisearchbot|ioi|ips-agent|tagoobot|mj12bot|dotbot|woriobot|yanga|buzzbot|mlbot|purebot|lingueebot|yandex\.com/bots|""" + """(?i)googlebot|googlebot-mobile|googlebot-image|mediapartners-google|bingbot|slurp|java|wget|curl|python-requests|commons-httpclient|python-urllib|libwww|httpunit|nutch|phpcrawl|msnbot|adidxbot|blekkobot|teoma|ia_archiver|gingercrawler|webmon|httrack|webcrawler|fast-webcrawler|fastenterprisecrawler|convera|biglotron|grub\.org|usinenouvellecrawler|antibot|netresearchserver|speedy|fluffy|jyxobot|bibnum\.bnf|findlink|exabot|gigabot|msrbot|seekbot|ngbot|panscient|yacybot|aisearchbot|ioi|ips-agent|tagoobot|mj12bot|dotbot|woriobot|yanga|buzzbot|mlbot|purebot|lingueebot|yandex\.com/bots|""" +
"""voyager|cyberpatrol|voilabot|baiduspider|citeseerxbot|spbot|twengabot|postrank|turnitinbot|scribdbot|page2rss|sitebot|linkdex|ezooms|dotbot|mail\.ru|discobot|zombie\.js|heritrix|findthatfile|europarchive\.org|nerdbynature\.bot|sistrixcrawler|ahrefsbot|aboundex|domaincrawler|wbsearchbot|summify|ccbot|edisterbot|seznambot|ec2linkfinder|gslfbot|aihitbot|intelium_bot|yeti|retrevopageanalyzer|lb-spider|sogou|lssbot|careerbot|wotbox|wocbot|ichiro|duckduckbot|lssrocketcrawler|drupact|webcompanycrawler|acoonbot|openindexspider|gnamgnamspider|web-archive-net\.com\.bot|backlinkcrawler|""" + """voyager|cyberpatrol|voilabot|baiduspider|citeseerxbot|spbot|twengabot|postrank|turnitinbot|scribdbot|page2rss|sitebot|linkdex|ezooms|dotbot|mail\.ru|discobot|zombie\.js|heritrix|findthatfile|europarchive\.org|nerdbynature\.bot|sistrixcrawler|ahrefsbot|aboundex|domaincrawler|wbsearchbot|summify|ccbot|edisterbot|seznambot|ec2linkfinder|gslfbot|aihitbot|intelium_bot|yeti|retrevopageanalyzer|lb-spider|sogou|lssbot|careerbot|wotbox|wocbot|ichiro|duckduckbot|lssrocketcrawler|drupact|webcompanycrawler|acoonbot|openindexspider|gnamgnamspider|web-archive-net\.com\.bot|backlinkcrawler|""" +
"""coccoc|integromedb|contentcrawlerspider|toplistbot|seokicks-robot|it2media-domain-crawler|ip-web-crawler\.com|siteexplorer\.info|elisabot|proximic|changedetection|blexbot|arabot|wesee:search|niki-bot|crystalsemanticsbot|rogerbot|360spider|psbot|interfaxscanbot|lipperheyseoservice|ccmetadatascaper|g00g1e\.net|grapeshotcrawler|urlappendbot|brainobot|fr-crawler|binlar|simplecrawler|simplecrawler|livelapbot|twitterbot|cxensebot|smtbot|facebookexternalhit|daumoa|sputnikimagebot|visionutils|yisouspider|parsijoobot|mediatoolkit\.com|semrushbot""" """coccoc|integromedb|contentcrawlerspider|toplistbot|seokicks-robot|it2media-domain-crawler|ip-web-crawler\.com|siteexplorer\.info|elisabot|proximic|changedetection|blexbot|arabot|wesee:search|niki-bot|crystalsemanticsbot|rogerbot|360spider|psbot|interfaxscanbot|lipperheyseoservice|ccmetadatascaper|g00g1e\.net|grapeshotcrawler|urlappendbot|brainobot|fr-crawler|binlar|simplecrawler|simplecrawler|livelapbot|twitterbot|cxensebot|smtbot|facebookexternalhit|daumoa|sputnikimagebot|visionutils|yisouspider|parsijoobot|mediatoolkit\.com|semrushbot"""
} }

View File

@ -41,9 +41,16 @@ object Json {
JsNumber(time.getMillis) JsNumber(time.getMillis)
} }
implicit val colorWrites: Writes[chess.Color] = Writes { c =>
JsString(c.name)
}
implicit val fenFormat: Format[FEN] = stringIsoFormat[FEN](Iso.fenIso) implicit val fenFormat: Format[FEN] = stringIsoFormat[FEN](Iso.fenIso)
implicit val uciReader: Reads[Uci] = Reads.of[String] flatMapResult { str => implicit val uciReads: Reads[Uci] = Reads.of[String] flatMapResult { str =>
JsResult.fromTry(Uci(str) toTry s"Invalid UCI: $str") JsResult.fromTry(Uci(str) toTry s"Invalid UCI: $str")
} }
implicit val uciWrites: Writes[Uci] = Writes { u =>
JsString(u.uci)
}
} }

View File

@ -60,11 +60,14 @@ final class Markdown(
private def mentionsToLinks(markdown: Text): Text = private def mentionsToLinks(markdown: Text): Text =
RawHtml.atUsernameRegex.replaceAllIn(markdown, "[$1](/@/$1)") RawHtml.atUsernameRegex.replaceAllIn(markdown, "[$1](/@/$1)")
private val tooManyUnderscoreRegex = """(_{4,})""".r
private def preventStackOverflow(text: String) = tooManyUnderscoreRegex.replaceAllIn(text, "_" * 3)
def apply(key: Key)(text: Text): Html = def apply(key: Key)(text: Text): Html =
Chronometer Chronometer
.sync { .sync {
try { try {
addLinkAttributes(renderer.render(parser.parse(mentionsToLinks(text)))) addLinkAttributes(renderer.render(parser.parse(mentionsToLinks(preventStackOverflow(text)))))
} catch { } catch {
case e: StackOverflowError => case e: StackOverflowError =>
logger.branch(key).error("StackOverflowError", e) logger.branch(key).error("StackOverflowError", e)

View File

@ -38,10 +38,17 @@ abstract class Random {
vec.nonEmpty ?? { vec.nonEmpty ?? {
vec lift nextInt(vec.size) vec lift nextInt(vec.size)
} }
// odds(1) = 100% true
// odds(2) = 50% true
// odds(3) = 33% true
def odds(n: Int): Boolean = nextInt(n) == 0
} }
object ThreadLocalRandom extends Random { object ThreadLocalRandom extends Random {
override def current = java.util.concurrent.ThreadLocalRandom.current override def current = java.util.concurrent.ThreadLocalRandom.current
def nextLong(n: Long): Long = current.nextLong(n)
} }
object SecureRandom extends Random { object SecureRandom extends Random {

View File

@ -24,8 +24,6 @@ final class PimpedOption[A](private val self: Option[A]) extends AnyVal {
def err(message: => String): A = self.getOrElse(sys.error(message)) def err(message: => String): A = self.getOrElse(sys.error(message))
def ifNone(n: => Unit): Unit = if (self.isEmpty) n
def has(a: A) = self contains a def has(a: A) = self contains a
} }

View File

@ -646,9 +646,15 @@ object mon {
def request(hit: Boolean) = counter("fishnet.http.acquire").withTag("hit", hit) def request(hit: Boolean) = counter("fishnet.http.acquire").withTag("hit", hit)
} }
def move(level: Int) = counter("fishnet.move.time").withTag("level", level) def move(level: Int) = counter("fishnet.move.time").withTag("level", level)
def openingBook(level: Int, variant: String, ply: Int, hit: Boolean) = def openingBook(level: Int, variant: String, ply: Int, hit: Boolean, success: Boolean) =
timer("fishnet.opening.hit").withTags( timer("fishnet.opening.hit").withTags(
Map("level" -> level, "variant" -> variant, "ply" -> ply, "hit" -> hit) Map(
"level" -> level.toLong,
"variant" -> variant,
"ply" -> ply.toLong,
"hit" -> hitTag(hit),
"success" -> successTag(success)
)
) )
} }
object study { object study {
@ -727,6 +733,7 @@ object mon {
) )
private def successTag(success: Boolean) = if (success) "success" else "failure" private def successTag(success: Boolean) = if (success) "success" else "failure"
private def hitTag(hit: Boolean) = if (hit) "hit" else "miss"
private def apiTag(api: Option[ApiVersion]) = api.fold("-")(_.toString) private def apiTag(api: Option[ApiVersion]) = api.fold("-")(_.toString)

View File

@ -63,6 +63,10 @@ trait dsl {
def $nor(expressions: Bdoc*): Bdoc = { def $nor(expressions: Bdoc*): Bdoc = {
$doc("$nor" -> expressions) $doc("$nor" -> expressions)
} }
def $not(expression: Bdoc): Bdoc = {
$doc("$not" -> expression)
}
// End of Top Level Logical Operators // End of Top Level Logical Operators
//**********************************************************************************************// //**********************************************************************************************//
@ -291,13 +295,6 @@ trait dsl {
} }
trait LogicalOperators { self: ElementBuilder =>
def $not(f: String => Expression[Bdoc]): SimpleExpression[Bdoc] = {
val expression = f(field)
SimpleExpression(field, $doc("$not" -> expression.value))
}
}
trait ElementOperators { self: ElementBuilder => trait ElementOperators { self: ElementBuilder =>
def $exists(v: Boolean): SimpleExpression[Bdoc] = { def $exists(v: Boolean): SimpleExpression[Bdoc] = {
SimpleExpression(field, $doc("$exists" -> v)) SimpleExpression(field, $doc("$exists" -> v))
@ -381,7 +378,6 @@ trait dsl {
with ComparisonOperators with ComparisonOperators
with ElementOperators with ElementOperators
with EvaluationOperators with EvaluationOperators
with LogicalOperators
with ArrayOperators with ArrayOperators
implicit def toBSONDocument[V: BSONWriter](expression: Expression[V]): Bdoc = implicit def toBSONDocument[V: BSONWriter](expression: Expression[V]): Bdoc =

View File

@ -16,6 +16,7 @@ final class Env(
userRepo: lila.user.UserRepo, userRepo: lila.user.UserRepo,
yoloDb: lila.db.AsyncDb @@ lila.db.YoloDb, yoloDb: lila.db.AsyncDb @@ lila.db.YoloDb,
cacheApi: lila.memo.CacheApi, cacheApi: lila.memo.CacheApi,
settingStore: lila.memo.SettingStore.Builder,
scheduler: akka.actor.Scheduler scheduler: akka.actor.Scheduler
)(implicit )(implicit
ec: scala.concurrent.ExecutionContext, ec: scala.concurrent.ExecutionContext,
@ -27,6 +28,12 @@ final class Env(
private lazy val truster = wire[EvalCacheTruster] private lazy val truster = wire[EvalCacheTruster]
lazy val enable = settingStore[Boolean](
"useCeval",
default = true,
text = "Enable cloud eval (disable in case of server trouble)".some
)
private lazy val upgrade = wire[EvalCacheUpgrade] private lazy val upgrade = wire[EvalCacheUpgrade]
lazy val api: EvalCacheApi = wire[EvalCacheApi] lazy val api: EvalCacheApi = wire[EvalCacheApi]

View File

@ -9,13 +9,16 @@ import scala.concurrent.duration._
import lila.db.AsyncCollFailingSilently import lila.db.AsyncCollFailingSilently
import lila.db.dsl._ import lila.db.dsl._
import lila.memo.CacheApi._ import lila.memo.CacheApi._
import lila.memo.SettingStore
import lila.socket.Socket import lila.socket.Socket
import lila.user.User
final class EvalCacheApi( final class EvalCacheApi(
coll: AsyncCollFailingSilently, coll: AsyncCollFailingSilently,
truster: EvalCacheTruster, truster: EvalCacheTruster,
upgrade: EvalCacheUpgrade, upgrade: EvalCacheUpgrade,
cacheApi: lila.memo.CacheApi cacheApi: lila.memo.CacheApi,
setting: SettingStore[Boolean]
)(implicit ec: scala.concurrent.ExecutionContext) { )(implicit ec: scala.concurrent.ExecutionContext) {
import EvalCacheEntry._ import EvalCacheEntry._
@ -36,7 +39,7 @@ final class EvalCacheApi(
def put(trustedUser: TrustedUser, candidate: Input.Candidate, sri: Socket.Sri): Funit = def put(trustedUser: TrustedUser, candidate: Input.Candidate, sri: Socket.Sri): Funit =
candidate.input ?? { put(trustedUser, _, sri) } candidate.input ?? { put(trustedUser, _, sri) }
def shouldPut = truster shouldPut _ def shouldPut(user: User) = setting.get() && truster.shouldPut(user)
def getSinglePvEval(variant: Variant, fen: FEN): Fu[Option[Eval]] = def getSinglePvEval(variant: Variant, fen: FEN): Fu[Option[Eval]] =
getEval( getEval(

View File

@ -9,38 +9,44 @@ import lila.socket.Socket
import lila.memo.ExpireCallbackMemo import lila.memo.ExpireCallbackMemo
import scala.collection.mutable import scala.collection.mutable
import lila.memo.SettingStore
/* Upgrades the user's eval when a better one becomes available, /* Upgrades the user's eval when a better one becomes available,
* by remembering the last evalGet of each socket member, * by remembering the last evalGet of each socket member,
* and listening to new evals stored. * and listening to new evals stored.
*/ */
final private class EvalCacheUpgrade(scheduler: akka.actor.Scheduler)(implicit final private class EvalCacheUpgrade(setting: SettingStore[Boolean], scheduler: akka.actor.Scheduler)(implicit
ec: scala.concurrent.ExecutionContext, ec: scala.concurrent.ExecutionContext,
mode: play.api.Mode mode: play.api.Mode
) { ) {
import EvalCacheUpgrade._ import EvalCacheUpgrade._
private val members = mutable.AnyRefMap.empty[SriString, WatchingMember] private val members = mutable.AnyRefMap.empty[SriString, WatchingMember]
private val evals = mutable.AnyRefMap.empty[SetupId, Set[SriString]] private val evals = mutable.AnyRefMap.empty[SetupId, EvalState]
private val expirableSris = new ExpireCallbackMemo(20 minutes, sri => unregister(Socket.Sri(sri))) private val expirableSris = new ExpireCallbackMemo(10 minutes, sri => unregister(Socket.Sri(sri)))
private val upgradeMon = lila.mon.evalCache.upgrade private val upgradeMon = lila.mon.evalCache.upgrade
def register(sri: Socket.Sri, variant: Variant, fen: FEN, multiPv: Int, path: String)(push: Push): Unit = { def register(sri: Socket.Sri, variant: Variant, fen: FEN, multiPv: Int, path: String)(push: Push): Unit =
members get sri.value foreach { wm => if (setting.get()) {
unregisterEval(wm.setupId, sri) members get sri.value foreach { wm =>
unregisterEval(wm.setupId, sri)
}
val setupId = makeSetupId(variant, fen, multiPv)
members += (sri.value -> WatchingMember(push, setupId, path))
evals += (setupId -> evals.get(setupId).fold(EvalState(Set(sri.value), 0))(_ addSri sri))
expirableSris put sri.value
} }
val setupId = makeSetupId(variant, fen, multiPv)
members += (sri.value -> WatchingMember(push, setupId, path))
evals += (setupId -> (~evals.get(setupId) + sri.value))
expirableSris put sri.value
}
def onEval(input: EvalCacheEntry.Input, sri: Socket.Sri): Unit = { def onEval(input: EvalCacheEntry.Input, sri: Socket.Sri): Unit = if (setting.get()) {
(1 to input.eval.multiPv) flatMap { multiPv => (1 to input.eval.multiPv) flatMap { multiPv =>
evals get makeSetupId(input.id.variant, input.fen, multiPv) val setupId = makeSetupId(input.id.variant, input.fen, multiPv)
} foreach { sris => evals get setupId map (setupId -> _)
val wms = sris.withFilter(sri.value !=) flatMap members.get } filter {
_._2.depth < input.eval.depth
} foreach { case (setupId, eval) =>
evals += (setupId -> eval.copy(depth = input.eval.depth))
val wms = eval.sris.withFilter(sri.value !=) flatMap members.get
if (wms.nonEmpty) { if (wms.nonEmpty) {
val json = JsonHandlers.writeEval(input.eval, input.fen) val json = JsonHandlers.writeEval(input.eval, input.fen)
wms foreach { wm => wms foreach { wm =>
@ -59,10 +65,10 @@ final private class EvalCacheUpgrade(scheduler: akka.actor.Scheduler)(implicit
} }
private def unregisterEval(setupId: SetupId, sri: Socket.Sri): Unit = private def unregisterEval(setupId: SetupId, sri: Socket.Sri): Unit =
evals get setupId foreach { sris => evals get setupId foreach { eval =>
val newSris = sris - sri.value val newSris = eval.sris - sri.value
if (newSris.isEmpty) evals -= setupId if (newSris.isEmpty) evals -= setupId
else evals += (setupId -> newSris) else evals += (setupId -> eval.copy(sris = newSris))
} }
scheduler.scheduleWithFixedDelay(1 minute, 1 minute) { () => scheduler.scheduleWithFixedDelay(1 minute, 1 minute) { () =>
@ -78,6 +84,10 @@ private object EvalCacheUpgrade {
private type SetupId = String private type SetupId = String
private type Push = JsObject => Unit private type Push = JsObject => Unit
private case class EvalState(sris: Set[SriString], depth: Int) {
def addSri(sri: Socket.Sri) = copy(sris = sris + sri.value)
}
private def makeSetupId(variant: Variant, fen: FEN, multiPv: Int): SetupId = private def makeSetupId(variant: Variant, fen: FEN, multiPv: Int): SetupId =
s"${variant.id}${EvalCacheEntry.SmallFen.make(variant, fen).value}^$multiPv" s"${variant.id}${EvalCacheEntry.SmallFen.make(variant, fen).value}^$multiPv"

View File

@ -71,21 +71,22 @@ final private class ExplorerIndexer(
case Correspondence | Classical => 1.00f case Correspondence | Classical => 1.00f
case Rapid if rating >= 2200 => 1.00f case Rapid if rating >= 2200 => 1.00f
case Rapid if rating >= 2000 => 0.50f case Rapid if rating >= 2000 => 0.83f
case Rapid if rating >= 1800 => 0.28f case Rapid if rating >= 1800 => 0.46f
case Rapid if rating >= 1600 => 0.24f case Rapid if rating >= 1600 => 0.39f
case Rapid => 0.02f case Rapid => 0.02f
case Blitz if rating >= 2500 => 1.00f case Blitz if rating >= 2500 => 1.00f
case Blitz if rating >= 2200 => 0.24f case Blitz if rating >= 2200 => 0.38f
case Blitz if rating >= 2000 => 0.11f case Blitz if rating >= 2000 => 0.18f
case Blitz if rating >= 1600 => 0.08f case Blitz if rating >= 1600 => 0.13f
case Blitz => 0.02f case Blitz => 0.02f
case Bullet if rating >= 2500 => 1.00f case Bullet if rating >= 2500 => 1.00f
case Bullet if rating >= 2200 => 0.30f case Bullet if rating >= 2200 => 0.48f
case Bullet if rating >= 2000 => 0.17f case Bullet if rating >= 2000 => 0.27f
case Bullet if rating >= 1600 => 0.11f case Bullet if rating >= 1800 => 0.19f
case Bullet if rating >= 1600 => 0.18f
case Bullet => 0.02f case Bullet => 0.02f
case UltraBullet => 1.00f case UltraBullet => 1.00f

View File

@ -18,7 +18,7 @@ final class Analyser(
system: akka.actor.ActorSystem system: akka.actor.ActorSystem
) { ) {
val maxPlies = 200 val maxPlies = 300
private val workQueue = private val workQueue =
new lila.hub.AsyncActorSequencer(maxSize = 256, timeout = 5 seconds, "fishnetAnalyser") new lila.hub.AsyncActorSequencer(maxSize = 256, timeout = 5 seconds, "fishnetAnalyser")

View File

@ -2,17 +2,19 @@ package lila.fishnet
import chess.format.Forsyth import chess.format.Forsyth
import chess.format.Uci import chess.format.Uci
import chess.Speed import chess.{ Color, Speed }
import com.softwaremill.tagging._ import com.softwaremill.tagging._
import play.api.libs.json._ import play.api.libs.json._
import play.api.libs.ws.JsonBodyReadables._ import play.api.libs.ws.JsonBodyReadables._
import play.api.libs.ws.StandaloneWSClient import play.api.libs.ws.StandaloneWSClient
import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext
import scala.concurrent.duration._
import lila.common.Json.uciReader import lila.common.Json._
import lila.common.ThreadLocalRandom import lila.common.ThreadLocalRandom
import lila.game.Game import lila.game.Game
import lila.memo.SettingStore import lila.memo.SettingStore
import scala.util.{ Failure, Success }
final private class FishnetOpeningBook( final private class FishnetOpeningBook(
ws: StandaloneWSClient, ws: StandaloneWSClient,
@ -21,32 +23,42 @@ final private class FishnetOpeningBook(
import FishnetOpeningBook._ import FishnetOpeningBook._
def apply(game: Game, level: Int): Fu[Option[Uci]] = (game.turns < depth.get()) ?? { private val outOfBook = new lila.memo.ExpireSetMemo(10 minutes)
ws.url(endpoint)
.withQueryStringParameters( def apply(game: Game, level: Int): Fu[Option[Uci]] =
"variant" -> game.variant.key, (game.turns < depth.get() && !outOfBook.get(game.id)) ?? {
"fen" -> Forsyth.>>(game.chess).value, ws.url(endpoint)
"topGames" -> "0", .withQueryStringParameters(
"recentGames" -> "0", "variant" -> game.variant.key,
"ratings" -> (~levelRatings.get(level)).mkString(","), "fen" -> Forsyth.>>(game.chess).value,
"speeds" -> (~openingSpeeds.get(game.speed)).map(_.key).mkString(",") "topGames" -> "0",
) "recentGames" -> "0",
.get() "ratings" -> (~levelRatings.get(level)).mkString(","),
.map { "speeds" -> (~openingSpeeds.get(game.speed)).map(_.key).mkString(",")
case res if res.status != 200 => )
logger.warn(s"opening book ${game.id} ${level} ${res.status} ${res.body}") .get()
none .map {
case res => case res if res.status != 200 =>
for { logger.warn(s"opening book ${game.id} ${level} ${res.status} ${res.body}")
data <- res.body[JsValue].validate[Response](responseReader).asOpt none
move <- data.randomPonderedMove case res =>
} yield move.uci for {
} data <- res.body[JsValue].validate[Response](responseReader).asOpt
.monValue(uci => _ = if (data.moves.isEmpty) outOfBook.put(game.id)
_.fishnet move <- data randomPonderedMove (game.turnColor, level)
.openingBook(level = level, variant = game.variant.key, ply = game.turns, hit = uci.isDefined) } yield move.uci
) }
} .monTry { res =>
_.fishnet
.openingBook(
level = level,
variant = game.variant.key,
ply = game.turns,
hit = res.toOption.exists(_.isDefined),
success = res.isSuccess
)
}
}
} }
object FishnetOpeningBook { object FishnetOpeningBook {
@ -54,20 +66,26 @@ object FishnetOpeningBook {
trait Depth trait Depth
case class Response(moves: List[Move]) { case class Response(moves: List[Move]) {
def randomPonderedMove: Option[Move] = {
val sum = moves.map(_.nb).sum def randomPonderedMove(turn: Color, level: Int): Option[Move] = {
val rng = ThreadLocalRandom nextInt sum val sum = moves.map(_.score(turn, level)).sum
val novelty = 5L * 14 // score of 5 winning games
val rng = ThreadLocalRandom.nextLong(sum + novelty)
moves moves
.foldLeft((none[Move], 0)) { case ((found, it), next) => .foldLeft((none[Move], 0L)) { case ((found, it), next) =>
val nextIt = it + next.nb val nextIt = it + next.score(turn, level)
(found orElse (nextIt > rng).option(next), nextIt) (found orElse (nextIt > rng).option(next), nextIt)
} }
._1 ._1
} }
} }
case class Move(uci: Uci, white: Int, draws: Int, black: Int) { case class Move(uci: Uci, white: Long, draws: Long, black: Long) {
def nb = white + draws + black def score(turn: Color, level: Int): Long =
// interpolate: real frequency at lvl 1, expectation value at lvl 8
14L * turn.fold(white, black) +
(15L - level) * draws +
(16L - 2 * level) * turn.fold(black, white)
} }
implicit val moveReader = Json.reads[Move] implicit val moveReader = Json.reads[Move]

View File

@ -1,15 +1,13 @@
package lila.fishnet package lila.fishnet
import chess.format.Uci
import chess.{ Black, Clock, White }
import scala.concurrent.duration._ import scala.concurrent.duration._
import chess.{ Black, Clock, White } import lila.common.{ Bus, Future, ThreadLocalRandom }
import lila.common.{ Future, ThreadLocalRandom }
import lila.game.{ Game, GameRepo, UciMemo } import lila.game.{ Game, GameRepo, UciMemo }
import lila.common.Bus
import lila.hub.actorApi.map.Tell import lila.hub.actorApi.map.Tell
import lila.hub.actorApi.round.FishnetPlay import lila.hub.actorApi.round.FishnetPlay
import chess.format.Uci
final class FishnetPlayer( final class FishnetPlayer(
redis: FishnetRedis, redis: FishnetRedis,
@ -28,7 +26,7 @@ final class FishnetPlayer(
openingBook(game, level) flatMap { openingBook(game, level) flatMap {
case Some(move) => case Some(move) =>
fuccess { fuccess {
Bus.publish(Tell(game.id, FishnetPlay(move, game.turns)), "roundSocket") Bus.publish(Tell(game.id, FishnetPlay(move, game.playedTurns)), "roundSocket")
} }
case None => makeWork(game, level) addEffect redis.request void case None => makeWork(game, level) addEffect redis.request void
} }

View File

@ -24,7 +24,7 @@ final class Cached(
private val lastPlayedPlayingIdCache: LoadingCache[User.ID, Fu[Option[Game.ID]]] = private val lastPlayedPlayingIdCache: LoadingCache[User.ID, Fu[Option[Game.ID]]] =
CacheApi.scaffeineNoScheduler CacheApi.scaffeineNoScheduler
.expireAfterWrite(5 seconds) .expireAfterWrite(11 seconds)
.build(gameRepo.lastPlayedPlayingId) .build(gameRepo.lastPlayedPlayingId)
lila.common.Bus.subscribeFun("startGame") { case lila.game.actorApi.StartGame(game) => lila.common.Bus.subscribeFun("startGame") { case lila.game.actorApi.StartGame(game) =>

View File

@ -47,21 +47,16 @@ final private class Captcher(gameRepo: GameRepo)(implicit ec: scala.concurrent.E
private val capacity = 256 private val capacity = 256
private var challenges = NonEmptyList.one(Captcha.default) private var challenges = NonEmptyList.one(Captcha.default)
private def add(c: Captcha): Unit = { private def add(c: Captcha): Unit =
find(c.gameId) ifNone { if (find(c.gameId).isEmpty) {
challenges = NonEmptyList(c, challenges.toList take capacity) challenges = NonEmptyList(c, challenges.toList take capacity)
} }
}
private def find(id: String): Option[Captcha] = private def find(id: String): Option[Captcha] =
challenges.find(_.gameId == id) challenges.find(_.gameId == id)
private def createFromDb: Fu[Option[Captcha]] = private def createFromDb: Fu[Option[Captcha]] =
findCheckmateInDb(10) flatMap { findCheckmateInDb(10) orElse findCheckmateInDb(1) flatMap { _ ?? fromGame }
_.fold(findCheckmateInDb(1))(g => fuccess(g.some))
} flatMap {
_ ?? fromGame
}
private def findCheckmateInDb(distribution: Int): Fu[Option[Game]] = private def findCheckmateInDb(distribution: Int): Fu[Option[Game]] =
gameRepo findRandomStandardCheckmate distribution gameRepo findRandomStandardCheckmate distribution

View File

@ -17,6 +17,7 @@ import chess.{
import JsonView._ import JsonView._
import lila.chat.{ PlayerLine, UserLine } import lila.chat.{ PlayerLine, UserLine }
import lila.common.ApiVersion import lila.common.ApiVersion
import lila.common.Json._
sealed trait Event { sealed trait Event {
def typ: String def typ: String

View File

@ -555,7 +555,7 @@ case class Game(
def abandoned = def abandoned =
(status <= Status.Started) && { (status <= Status.Started) && {
movedAt isBefore { movedAt isBefore {
if (hasAi && !hasCorrespondenceClock) Game.aiAbandonedDate if (hasAi && hasClock) Game.aiAbandonedDate
else Game.abandonedDate else Game.abandonedDate
} }
} }

View File

@ -1,5 +1,7 @@
package lila.game package lila.game
import scala.concurrent.duration._
import chess.format.{ FEN, Forsyth } import chess.format.{ FEN, Forsyth }
import chess.{ Color, Status } import chess.{ Color, Status }
import org.joda.time.DateTime import org.joda.time.DateTime
@ -19,6 +21,8 @@ final class GameRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
import Game.{ ID, BSONFields => F } import Game.{ ID, BSONFields => F }
import Player.holdAlertBSONHandler import Player.holdAlertBSONHandler
val fixedColorLobbyCache = new lila.memo.ExpireSetMemo(2 hours)
def game(gameId: ID): Fu[Option[Game]] = coll.byId[Game](gameId) def game(gameId: ID): Fu[Option[Game]] = coll.byId[Game](gameId)
def gameFromSecondary(gameId: ID): Fu[Option[Game]] = coll.secondaryPreferred.byId[Game](gameId) def gameFromSecondary(gameId: ID): Fu[Option[Game]] = coll.secondaryPreferred.byId[Game](gameId)

View File

@ -5,7 +5,7 @@ import play.api.libs.json._
import chess.format.{ FEN, Forsyth } import chess.format.{ FEN, Forsyth }
import chess.variant.Crazyhouse import chess.variant.Crazyhouse
import chess.{ Clock, Color } import chess.{ Clock, Color }
import lila.common.Json.jodaWrites import lila.common.Json._
final class JsonView(rematches: Rematches) { final class JsonView(rematches: Rematches) {
@ -143,12 +143,4 @@ object JsonView {
implicit val sourceWriter: Writes[Source] = Writes { s => implicit val sourceWriter: Writes[Source] = Writes { s =>
JsString(s.name) JsString(s.name)
} }
implicit val colorWrites: Writes[Color] = Writes { c =>
JsString(c.name)
}
implicit val fenWrites: Writes[FEN] = Writes { f =>
JsString(f.value)
}
} }

View File

@ -57,10 +57,12 @@ final class PgnDump(
private def gameLightUsers(game: Game): Fu[(Option[LightUser], Option[LightUser])] = private def gameLightUsers(game: Game): Fu[(Option[LightUser], Option[LightUser])] =
(game.whitePlayer.userId ?? lightUserApi.async) zip (game.blackPlayer.userId ?? lightUserApi.async) (game.whitePlayer.userId ?? lightUserApi.async) zip (game.blackPlayer.userId ?? lightUserApi.async)
private def rating(p: Player) = p.rating.fold("?")(_.toString) private def rating(p: Player) = p.rating.orElse(p.nameSplit.flatMap(_._2)).fold("?")(_.toString)
def player(p: Player, u: Option[LightUser]) = def player(p: Player, u: Option[LightUser]) =
p.aiLevel.fold(u.fold(p.name | lila.user.User.anonymous)(_.name))("lichess AI level " + _) p.aiLevel.fold(u.fold(p.nameSplit.map(_._1).orElse(p.name) | lila.user.User.anonymous)(_.name))(
"lichess AI level " + _
)
private val customStartPosition: Set[chess.variant.Variant] = private val customStartPosition: Set[chess.variant.Variant] =
Set(chess.variant.Chess960, chess.variant.FromPosition, chess.variant.Horde, chess.variant.RacingKings) Set(chess.variant.Chess960, chess.variant.FromPosition, chess.variant.Horde, chess.variant.RacingKings)

View File

@ -60,8 +60,8 @@ case class Player(
def nameSplit: Option[(String, Option[Int])] = def nameSplit: Option[(String, Option[Int])] =
name map { name map {
case Player.nameSplitRegex(n, r) => n -> r.toIntOption case Player.nameSplitRegex(n, r) => n.trim -> r.toIntOption
case n => n -> none case n => n -> none
} }
def before(other: Player) = def before(other: Player) =

View File

@ -9,20 +9,18 @@ final class Share(
relationApi: lila.relation.RelationApi relationApi: lila.relation.RelationApi
)(implicit ec: scala.concurrent.ExecutionContext) { )(implicit ec: scala.concurrent.ExecutionContext) {
def getPrefId(insighted: User) = prefApi.getPrefById(insighted.id) dmap (_.insightShare) def getPrefId(insighted: User) = prefApi.getPref(insighted.id, _.insightShare)
def grant(insighted: User, to: Option[User]): Fu[Boolean] = def grant(insighted: User, to: Option[User]): Fu[Boolean] =
if (to ?? Granter(_.SeeInsight)) fuTrue if (to ?? Granter(_.SeeInsight)) fuTrue
else else
prefApi.getPrefById(insighted.id) flatMap { pref => getPrefId(insighted) flatMap {
pref.insightShare match { case _ if to.contains(insighted) => fuTrue
case _ if to.contains(insighted) => fuTrue case Pref.InsightShare.EVERYBODY => fuTrue
case Pref.InsightShare.EVERYBODY => fuTrue case Pref.InsightShare.FRIENDS =>
case Pref.InsightShare.FRIENDS => to ?? { t =>
to ?? { t => relationApi.fetchAreFriends(insighted.id, t.id)
relationApi.fetchAreFriends(insighted.id, t.id) }
} case Pref.InsightShare.NOBODY => fuFalse
case Pref.InsightShare.NOBODY => fuFalse
}
} }
} }

View File

@ -38,7 +38,7 @@ final class IrcApi(
) )
case Some(note) => case Some(note) =>
zulip.sendAndGetLink(stream, "/" + user.username)( zulip.sendAndGetLink(stream, "/" + user.username)(
s"${markdown.modLink(mod.user.username)} :pepenote: **${markdown s"${markdown.modLink(mod.user)} :pepenote: **${markdown
.userLink(user.username)}** (${markdown.userNotesLink(user.username)}):\n" + .userLink(user.username)}** (${markdown.userNotesLink(user.username)}):\n" +
markdown.linkifyUsers(note.text take 2000) markdown.linkifyUsers(note.text take 2000)
) )
@ -70,7 +70,7 @@ final class IrcApi(
def commlog(mod: Holder, user: User, reportBy: Option[User.ID]): Funit = def commlog(mod: Holder, user: User, reportBy: Option[User.ID]): Funit =
zulip(_.mod.adminLog, "private comms checks")({ zulip(_.mod.adminLog, "private comms checks")({
val finalS = if (user.username endsWith "s") "" else "s" val finalS = if (user.username endsWith "s") "" else "s"
s"**${markdown modLink mod.user.username}** checked out **${markdown userLink user.username}**'$finalS communications " s"**${markdown modLink mod.user}** checked out **${markdown userLink user.username}**'$finalS communications "
} + reportBy.filter(mod.id !=).fold("spontaneously") { by => } + reportBy.filter(mod.id !=).fold("spontaneously") { by =>
s"while investigating a report created by ${markdown.userLink(by)}" s"while investigating a report created by ${markdown.userLink(by)}"
}) })
@ -96,10 +96,11 @@ final class IrcApi(
// def printBan(mod: Holder, print: String, userIds: List[User.ID]): Funit = // def printBan(mod: Holder, print: String, userIds: List[User.ID]): Funit =
// logMod(mod.id, "footprints", s"Ban print $print of ${userIds} users: ${userIds map linkifyUsers}") // logMod(mod.id, "footprints", s"Ban print $print of ${userIds} users: ${userIds map linkifyUsers}")
def chatPanic(mod: Holder, v: Boolean): Funit = def chatPanic(mod: Holder, v: Boolean): Funit = {
zulip(_.mod.log, "chat panic")( val msg =
s":stop: ${markdown.modLink(mod.user)} ${if (v) "enabled" else "disabled"} ${markdown.lichessLink("/mod/chat-panic", " Chat Panic")}" s":stop: ${markdown.modLink(mod.user)} ${if (v) "enabled" else "disabled"} ${markdown.lichessLink("/mod/chat-panic", " Chat Panic")}"
) zulip(_.mod.log, "chat panic")(msg) >> zulip(_.mod.commsPublic, "main")(msg)
}
def garbageCollector(msg: String): Funit = def garbageCollector(msg: String): Funit =
zulip(_.mod.adminLog, "garbage collector")(markdown linkifyUsers msg) zulip(_.mod.adminLog, "garbage collector")(markdown linkifyUsers msg)
@ -132,6 +133,9 @@ final class IrcApi(
} }
} }
def nameClosePreset(username: String): Funit =
zulip(_.mod.commsPublic, "/" + username)("@**remind** here in 48h to close this account")
def stop(): Funit = zulip(_.general, "lila")("Lichess is restarting.") def stop(): Funit = zulip(_.general, "lila")("Lichess is restarting.")
def publishEvent(event: Event): Funit = event match { def publishEvent(event: Event): Funit = event match {
@ -208,7 +212,7 @@ object IrcApi {
private object markdown { private object markdown {
def link(url: String, name: String) = s"[$name]($url)" def link(url: String, name: String) = s"[$name]($url)"
def lichessLink(path: String, name: String) = s"[$name](https://lichess.org$path)" def lichessLink(path: String, name: String) = s"[$name](https://lichess.org$path)"
def userLink(name: String): String = lichessLink(s"/@/$name?mod", name) def userLink(name: String): String = lichessLink(s"/@/$name?mod&notes", name)
def userLink(user: User): String = userLink(user.username) def userLink(user: User): String = userLink(user.username)
def modLink(name: String): String = lichessLink(s"/@/$name", name) def modLink(name: String): String = lichessLink(s"/@/$name", name)
def modLink(user: User): String = modLink(user.username) def modLink(user: User): String = modLink(user.username)

View File

@ -80,6 +80,7 @@ private object ZulipClient {
val log = "mod-log" val log = "mod-log"
val adminLog = "mod-admin-log" val adminLog = "mod-admin-log"
val adminGeneral = "mod-admin-general" val adminGeneral = "mod-admin-general"
val commsPublic = "mod-comms-public"
val commsPrivate = "mod-comms-private" val commsPrivate = "mod-comms-private"
val hunterCheat = "mod-hunter-cheat" val hunterCheat = "mod-hunter-cheat"
val adminAppeal = "mod-admin-appeal" val adminAppeal = "mod-admin-appeal"

View File

@ -4,6 +4,7 @@ import lila.game.{ Pov, Source }
final private class AbortListener( final private class AbortListener(
userRepo: lila.user.UserRepo, userRepo: lila.user.UserRepo,
gameRepo: lila.game.GameRepo,
seekApi: SeekApi, seekApi: SeekApi,
lobbyActor: LobbySyncActor lobbyActor: LobbySyncActor
)(implicit ec: scala.concurrent.ExecutionContext) { )(implicit ec: scala.concurrent.ExecutionContext) {
@ -14,7 +15,10 @@ final private class AbortListener(
lobbyActor.registerAbortedGame(pov.game) lobbyActor.registerAbortedGame(pov.game)
private def cancelColorIncrement(pov: Pov): Unit = private def cancelColorIncrement(pov: Pov): Unit =
if (pov.game.source.exists(s => s == Source.Lobby || s == Source.Pool)) pov.game.userIds match { if (
pov.game.source
.exists(s => s == Source.Lobby || s == Source.Pool) && !gameRepo.fixedColorLobbyCache.get(pov.game.id)
) pov.game.userIds match {
case List(u1, u2) => case List(u1, u2) =>
userRepo.incColor(u1, -1) userRepo.incColor(u1, -1)
userRepo.incColor(u2, 1) userRepo.incColor(u2, 1)

View File

@ -36,6 +36,7 @@ final private class Biter(
_ <- gameRepo insertDenormalized game _ <- gameRepo insertDenormalized game
} yield { } yield {
lila.mon.lobby.hook.join.increment() lila.mon.lobby.hook.join.increment()
rememberIfFixedColor(hook.realColor, game)
JoinHook(sri, hook, game, creatorColor) JoinHook(sri, hook, game, creatorColor)
} }
@ -50,7 +51,14 @@ final private class Biter(
blackUser = creatorColor.fold(user.some, owner.some) blackUser = creatorColor.fold(user.some, owner.some)
).withUniqueId ).withUniqueId
_ <- gameRepo insertDenormalized game _ <- gameRepo insertDenormalized game
} yield JoinSeek(user.id, seek, game, creatorColor) } yield {
rememberIfFixedColor(seek.realColor, game)
JoinSeek(user.id, seek, game, creatorColor)
}
private def rememberIfFixedColor(color: Color, game: Game) =
if (color != Color.Random)
gameRepo.fixedColorLobbyCache put game.id
private def assignCreatorColor( private def assignCreatorColor(
creatorUser: Option[User], creatorUser: Option[User],

View File

@ -51,11 +51,10 @@ final class ModQueueStats(
data <- doc.getAsOpt[List[Bdoc]]("data") data <- doc.getAsOpt[List[Bdoc]]("data")
} yield date -> { } yield date -> {
for { for {
entry <- data entry <- data
nb <- entry.int("nb") nb <- entry.int("nb")
roomStr <- entry.string("room") room <- entry.string("room")
room <- Room.byKey get roomStr score <- entry.int("score")
score <- entry.int("score")
} yield (room, score, nb) } yield (room, score, nb)
} }
} }
@ -66,20 +65,25 @@ final class ModQueueStats(
"common" -> Json.obj( "common" -> Json.obj(
"xaxis" -> days.map(_._1.getMillis) "xaxis" -> days.map(_._1.getMillis)
), ),
"rooms" -> Room.all.map { room => "rooms" -> Room.all
Json.obj( .map { room =>
"name" -> room.name, room.key -> room.name
"series" -> scores.collect { }
case score if score > 20 || room == Room.Boost => .appended { ("appeal", "Appeal") }
Json.obj( .map { case (roomKey, roomName) =>
"name" -> score, Json.obj(
"data" -> days.map(~_._2.collectFirst { "name" -> roomName,
case (r, s, nb) if r == room && s == score => nb "series" -> scores.collect {
}) case score if score > 20 || roomKey == Room.Boost.key =>
) Json.obj(
} "name" -> score,
) "data" -> days.map(~_._2.collectFirst {
} case (r, s, nb) if r == roomKey && s == score => nb
})
)
}
)
}
) )
) )
} }

View File

@ -321,7 +321,8 @@ final class ModlogApi(repo: ModlogRepo, userRepo: UserRepo, ircApi: IrcApi)(impl
case M.engine | M.unengine | M.booster | M.unbooster | M.reopenAccount | M.unalt => case M.engine | M.unengine | M.booster | M.unbooster | M.reopenAccount | M.unalt =>
Some(IrcApi.ModDomain.Hunt) Some(IrcApi.ModDomain.Hunt)
case M.troll | M.untroll | M.chatTimeout | M.closeTopic | M.openTopic | M.disableTeam | case M.troll | M.untroll | M.chatTimeout | M.closeTopic | M.openTopic | M.disableTeam |
M.enableTeam | M.setKidMode | M.deletePost | M.postAsAnonMod | M.editAsAnonMod => M.enableTeam | M.setKidMode | M.deletePost | M.postAsAnonMod | M.editAsAnonMod | M.blogTier |
M.blogPostEdit =>
Some(IrcApi.ModDomain.Comm) Some(IrcApi.ModDomain.Comm)
case _ => Some(IrcApi.ModDomain.Other) case _ => Some(IrcApi.ModDomain.Other)
} }

View File

@ -50,11 +50,15 @@ case class ModPresets(value: List[ModPreset]) {
value.find(_.text.filter(_.isLetter) == clean) value.find(_.text.filter(_.isLetter) == clean)
} }
} }
case class ModPreset(name: String, text: String, permissions: Set[Permission]) case class ModPreset(name: String, text: String, permissions: Set[Permission]) {
def isNameClose = name contains ModPresets.nameClosePresetName
}
object ModPresets { object ModPresets {
val groups = List("PM", "appeal") val groups = List("PM", "appeal")
val nameClosePresetName = "Account closure for name in 48h"
private[mod] object setting { private[mod] object setting {

View File

@ -5,6 +5,7 @@ import org.joda.time.format.ISODateTimeFormat
import play.api.i18n.Lang import play.api.i18n.Lang
import play.api.libs.json._ import play.api.libs.json._
import lila.common.Json.{ jodaWrites => _, _ }
import lila.common.LightUser import lila.common.LightUser
import lila.rating.{ Glicko, Perf, PerfType } import lila.rating.{ Glicko, Perf, PerfType }
import lila.user.User import lila.user.User

View File

@ -282,25 +282,24 @@ final class PlanApi(
.void >> setDbUserPlanOnCharge(user, levelUp = false) .void >> setDbUserPlanOnCharge(user, levelUp = false)
def gift(from: User, to: User, money: Money): Funit = def gift(from: User, to: User, money: Money): Funit =
!to.isPatron ?? { for {
for { toPatronOpt <- userPatron(to)
isLifetime <- pricingApi isLifetime money isLifetime <- fuccess(toPatronOpt.exists(_.isLifetime)) >>| (pricingApi isLifetime money)
_ <- patronColl.update _ <- patronColl.update
.one( .one(
$id(to.id), $id(to.id),
$set( $set(
"lastLevelUp" -> DateTime.now, "lastLevelUp" -> DateTime.now,
"lifetime" -> isLifetime, "lifetime" -> isLifetime,
"free" -> Patron.Free(DateTime.now, by = from.id.some), "free" -> Patron.Free(DateTime.now, by = from.id.some),
"expiresAt" -> (!isLifetime option DateTime.now.plusMonths(1)) "expiresAt" -> (!isLifetime option DateTime.now.plusMonths(1))
), ),
upsert = true upsert = true
) )
newTo = to.mapPlan(_.incMonths) newTo = to.mapPlan(p => if (toPatronOpt.exists(_.canLevelUp)) p.incMonths else p.enable)
_ <- setDbUserPlan(newTo) _ <- setDbUserPlan(newTo)
} yield { } yield {
notifier.onGift(from, newTo, isLifetime) notifier.onGift(from, newTo, isLifetime)
}
} }
def recentGiftFrom(from: User): Fu[Option[Patron]] = def recentGiftFrom(from: User): Fu[Option[Patron]] =

View File

@ -132,7 +132,7 @@ final class PlaybanApi(
Status.Resign.is(status) Status.Resign.is(status)
} }
.map { c => .map { c =>
(c.estimateTotalSeconds / 10) atLeast 15 atMost (3 * 60) (c.estimateTotalSeconds / 10) atLeast 30 atMost (3 * 60)
} }
.exists(_ < nowSeconds - game.movedAt.getSeconds) .exists(_ < nowSeconds - game.movedAt.getSeconds)
.option { .option {

View File

@ -1,5 +1,9 @@
package lila.pref package lila.pref
import org.joda.time.DateTime
import lila.user.User
case class Pref( case class Pref(
_id: String, // user id _id: String, // user id
bg: Int, bg: Int,
@ -413,7 +417,14 @@ object Pref {
object Zen extends BooleanPref {} object Zen extends BooleanPref {}
object Ratings extends BooleanPref {} object Ratings extends BooleanPref {}
def create(id: String) = default.copy(_id = id) val darkByDefaultSince = new DateTime(2021, 11, 7, 8, 0)
def create(id: User.ID) = default.copy(_id = id)
def create(user: User) = default.copy(
_id = user.id,
bg = if (user.createdAt isAfter darkByDefaultSince) Bg.DARK else Bg.LIGHT
)
lazy val default = Pref( lazy val default = Pref(
_id = "", _id = "",

View File

@ -37,13 +37,16 @@ final class PrefApi(
.void >>- { cache invalidate user.id } .void >>- { cache invalidate user.id }
} >>- { cache invalidate user.id } } >>- { cache invalidate user.id }
def getPrefById(id: User.ID): Fu[Pref] = cache get id dmap (_ getOrElse Pref.create(id)) def getPrefById(id: User.ID): Fu[Option[Pref]] = cache get id
val getPref = getPrefById _
def getPref(user: User): Fu[Pref] = getPref(user.id)
def getPref(user: Option[User]): Fu[Pref] = user.fold(fuccess(Pref.default))(getPref)
def getPref[A](user: User, pref: Pref => A): Fu[A] = getPref(user) dmap pref def getPref(user: User): Fu[Pref] = cache get user.id dmap {
def getPref[A](userId: User.ID, pref: Pref => A): Fu[A] = getPref(userId) dmap pref _ getOrElse Pref.create(user)
}
def getPref[A](user: User, pref: Pref => A): Fu[A] = getPref(user) dmap pref
def getPref[A](userId: User.ID, pref: Pref => A): Fu[A] =
getPrefById(userId).dmap(p => pref(p | Pref.default))
def getPref(user: User, req: RequestHeader): Fu[Pref] = def getPref(user: User, req: RequestHeader): Fu[Pref] =
getPref(user) dmap RequestPref.queryParamOverride(req) getPref(user) dmap RequestPref.queryParamOverride(req)
@ -81,9 +84,6 @@ final class PrefApi(
def setPref(user: User, change: Pref => Pref): Funit = def setPref(user: User, change: Pref => Pref): Funit =
getPref(user) map change flatMap setPref getPref(user) map change flatMap setPref
def setPref(userId: User.ID, change: Pref => Pref): Funit =
getPref(userId) map change flatMap setPref
def setPrefString(user: User, name: String, value: String): Funit = def setPrefString(user: User, name: String, value: String): Funit =
getPref(user) map { _.set(name, value) } orFail getPref(user) map { _.set(name, value) } orFail
s"Bad pref ${user.id} $name -> $value" flatMap setPref s"Bad pref ${user.id} $name -> $value" flatMap setPref

View File

@ -1,13 +1,10 @@
package lila.pref package lila.pref
sealed class Theme private[pref] (val name: String, val colors: Theme.HexColors) { sealed class Theme private[pref] (val name: String, val file: String) {
override def toString = name override def toString = name
def cssClass = name def cssClass = name
def light = colors._1
def dark = colors._2
} }
sealed trait ThemeObject { sealed trait ThemeObject {
@ -27,49 +24,33 @@ sealed trait ThemeObject {
object Theme extends ThemeObject { object Theme extends ThemeObject {
case class HexColor(value: String) extends AnyVal with StringValue
type HexColors = (HexColor, HexColor)
private[pref] val defaultHexColors = (HexColor("b0b0b0"), HexColor("909090"))
private val colors: Map[String, HexColors] = Map(
"blue" -> (HexColor("dee3e6") -> HexColor("8ca2ad")),
"brown" -> (HexColor("f0d9b5") -> HexColor("b58863")),
"green" -> (HexColor("ffffdd") -> HexColor("86a666")),
"purple" -> (HexColor("9f90b0") -> HexColor("7d4a8d")),
"ic" -> (HexColor("ececec") -> HexColor("c1c18e")),
"horsey" -> (HexColor("f1d9b6") -> HexColor("8e6547"))
)
val all = List( val all = List(
"blue", new Theme("blue", "svg/blue.svg"),
"blue2", new Theme("blue2", "blue2.jpg"),
"blue3", new Theme("blue3", "blue3.jpg"),
"blue-marble", new Theme("blue-marble", "blue-marble.jpg"),
"canvas", new Theme("canvas", "canvas2.jpg"),
"wood", new Theme("wood", "wood.jpg"),
"wood2", new Theme("wood2", "wood2.jpg"),
"wood3", new Theme("wood3", "wood3.jpg"),
"wood4", new Theme("wood4", "wood4.jpg"),
"maple", new Theme("maple", "maple.jpg"),
"maple2", new Theme("maple2", "maple2.jpg"),
"brown", new Theme("brown", "svg/brown.svg"),
"leather", new Theme("leather", "leather.jpg"),
"green", new Theme("green", "svg/green.svg"),
"marble", new Theme("marble", "marble.jpg"),
"green-plastic", new Theme("green-plastic", "green-plastic.png"),
"grey", new Theme("grey", "grey.jpg"),
"metal", new Theme("metal", "metal.jpg"),
"olive", new Theme("olive", "olive.jpg"),
"newspaper", new Theme("newspaper", "newspaper.png"),
"purple", new Theme("purple", "svg/purple.svg"),
"purple-diag", new Theme("purple-diag", "purple-diag.png"),
"pink", new Theme("pink", "pink-pyramid.png"),
"ic", new Theme("ic", "svg/ic.svg"),
"horsey" new Theme("horsey", "horsey.jpg")
) map { name => )
new Theme(name, colors.getOrElse(name, defaultHexColors))
}
lazy val default = allByName get "brown" err "Can't find default theme D:" lazy val default = allByName get "brown" err "Can't find default theme D:"
} }
@ -77,28 +58,26 @@ object Theme extends ThemeObject {
object Theme3d extends ThemeObject { object Theme3d extends ThemeObject {
val all = List( val all = List(
"Black-White-Aluminium", new Theme("Black-White-Aluminium", "Black-White-Aluminium.png"),
"Brushed-Aluminium", new Theme("Brushed-Aluminium", "Brushed-Aluminium.png"),
"China-Blue", new Theme("China-Blue", "China-Blue.png"),
"China-Green", new Theme("China-Green", "China-Green.png"),
"China-Grey", new Theme("China-Grey", "China-Grey.png"),
"China-Scarlet", new Theme("China-Scarlet", "China-Scarlet.png"),
"China-Yellow", new Theme("China-Yellow", "China-Yellow.png"),
"Classic-Blue", new Theme("Classic-Blue", "Classic-Blue.png"),
"Gold-Silver", new Theme("Gold-Silver", "Gold-Silver.png"),
"Green-Glass", new Theme("Green-Glass", "Green-Glass.png"),
"Light-Wood", new Theme("Light-Wood", "Light-Wood.png"),
"Power-Coated", new Theme("Power-Coated", "Power-Coated.png"),
"Purple-Black", new Theme("Purple-Black", "Purple-Black.png"),
"Rosewood", new Theme("Rosewood", "Rosewood.png"),
"Wood-Glass", new Theme("Wood-Glass", "Wood-Glass.png"),
"Marble", new Theme("Marble", "Marble.png"),
"Wax", new Theme("Wax", "Wax.png"),
"Jade", new Theme("Jade", "Jade.png"),
"Woodi" new Theme("Woodi", "Woodi.png")
) map { name => )
new Theme(name, Theme.defaultHexColors)
}
lazy val default = allByName get "Woodi" err "Can't find default theme D:" lazy val default = allByName get "Woodi" err "Can't find default theme D:"
} }

View File

@ -7,6 +7,7 @@ import scala.concurrent.duration._
import lila.db.dsl._ import lila.db.dsl._
import lila.memo.CacheApi._ import lila.memo.CacheApi._
import lila.common.ThreadLocalRandom
final private[puzzle] class DailyPuzzle( final private[puzzle] class DailyPuzzle(
colls: PuzzleColls, colls: PuzzleColls,
@ -26,7 +27,7 @@ final private[puzzle] class DailyPuzzle(
def get: Fu[Option[DailyPuzzle.WithHtml]] = cache.getUnit def get: Fu[Option[DailyPuzzle.WithHtml]] = cache.getUnit
private def find: Fu[Option[DailyPuzzle.WithHtml]] = private def find: Fu[Option[DailyPuzzle.WithHtml]] =
(findCurrent orElse findNew) recover { case e: Exception => (findCurrent orElse findNewBiased()) recover { case e: Exception =>
logger.error("find daily", e) logger.error("find daily", e)
none none
} flatMap { _ ?? makeDaily } } flatMap { _ ?? makeDaily }
@ -48,6 +49,18 @@ final private[puzzle] class DailyPuzzle(
.one[Puzzle] .one[Puzzle]
} }
private def findNewBiased(tries: Int = 0): Fu[Option[Puzzle]] = {
def tryAgainMaybe = (tries < 5) ?? findNewBiased(tries + 1)
import lila.common.ThreadLocalRandom.odds
import PuzzleTheme._
findNew flatMap {
case None => tryAgainMaybe
case Some(p) if p.hasTheme(anastasiaMate) && !odds(3) => tryAgainMaybe dmap (_ orElse p.some)
case Some(p) if p.hasTheme(arabianMate) && odds(2) => tryAgainMaybe dmap (_ orElse p.some)
case p => fuccess(p)
}
}
private def findNew: Fu[Option[Puzzle]] = private def findNew: Fu[Option[Puzzle]] =
colls colls
.path { .path {

View File

@ -29,6 +29,8 @@ case class Puzzle(
} err s"Can't apply puzzle $id first move" } err s"Can't apply puzzle $id first move"
def color = fen.color.fold[chess.Color](chess.White)(!_) def color = fen.color.fold[chess.Color](chess.White)(!_)
def hasTheme(theme: PuzzleTheme) = themes(theme.key)
} }
object Puzzle { object Puzzle {

View File

@ -155,7 +155,8 @@ final private class RelaySync(
(tour.official && chapter.root.mainline.sizeIs > 10) ?? studyApi.analysisRequest( (tour.official && chapter.root.mainline.sizeIs > 10) ?? studyApi.analysisRequest(
studyId = study.id, studyId = study.id,
chapterId = chapter.id, chapterId = chapter.id,
userId = study.ownerId userId = study.ownerId,
unlimited = true
) )
} >>- { } >>- {
multiboard.invalidate(study.id) multiboard.invalidate(study.id)

View File

@ -14,7 +14,10 @@ case class SyncLog(events: Vector[SyncLog.Event]) extends AnyVal {
def add(event: SyncLog.Event) = def add(event: SyncLog.Event) =
copy( copy(
events = events.take(SyncLog.historySize - 1) :+ event events = {
if (events.sizeIs > SyncLog.historySize) events drop 1
else events
} :+ event
) )
} }

View File

@ -49,10 +49,10 @@ object Reason {
def isGrantedFor(mod: Holder)(reason: Reason) = { def isGrantedFor(mod: Holder)(reason: Reason) = {
import lila.security.Granter import lila.security.Granter
reason match { reason match {
case Cheat => Granter.is(_.MarkEngine)(mod) case Cheat => Granter.is(_.MarkEngine)(mod)
case AltPrint | CheatPrint => Granter.is(_.Admin)(mod) case AltPrint | CheatPrint | Playbans | Other => Granter.is(_.Admin)(mod)
case Comm => Granter.is(_.Shadowban)(mod) case Comm => Granter.is(_.Shadowban)(mod)
case Boost | Playbans | Other => Granter.is(_.MarkBooster)(mod) case Boost => Granter.is(_.MarkBooster)(mod)
} }
} }
} }

View File

@ -22,9 +22,9 @@ final private[round] class Drawer(
import Pref.PrefZero import Pref.PrefZero
if (game.playerHasOfferedDrawRecently(pov.color)) fuccess(pov.some) if (game.playerHasOfferedDrawRecently(pov.color)) fuccess(pov.some)
else else
pov.player.userId ?? prefApi.getPref map { pref => pov.player.userId ?? { uid => prefApi.getPref(uid, _.autoThreefold) } map { autoThreefold =>
pref.autoThreefold == Pref.AutoThreefold.ALWAYS || { autoThreefold == Pref.AutoThreefold.ALWAYS || {
pref.autoThreefold == Pref.AutoThreefold.TIME && autoThreefold == Pref.AutoThreefold.TIME &&
game.clock ?? { _.remainingTime(pov.color) < Centis.ofSeconds(30) } game.clock ?? { _.remainingTime(pov.color) < Centis.ofSeconds(30) }
} || pov.player.userId.exists(isBotSync) } || pov.player.userId.exists(isBotSync)
} map (_ option pov) } map (_ option pov)
@ -37,7 +37,7 @@ final private[round] class Drawer(
case pov if pov.game.history.threefoldRepetition => case pov if pov.game.history.threefoldRepetition =>
finisher.other(pov.game, _.Draw, None) finisher.other(pov.game, _.Draw, None)
case pov if pov.opponent.isOfferingDraw => case pov if pov.opponent.isOfferingDraw =>
finisher.other(pov.game, _.Draw, None, Some(trans.drawOfferAccepted.txt())) finisher.other(pov.game, _.Draw, None, Messenger.Persistent(trans.drawOfferAccepted.txt()).some)
case Pov(g, color) if g playerCanOfferDraw color => case Pov(g, color) if g playerCanOfferDraw color =>
val progress = Progress(g) map { _ offerDraw color } val progress = Progress(g) map { _ offerDraw color }
messenger.system(g, color.fold(trans.whiteOffersDraw, trans.blackOffersDraw).txt()) messenger.system(g, color.fold(trans.whiteOffersDraw, trans.blackOffersDraw).txt())

View File

@ -48,7 +48,7 @@ final private class Finisher(
other(game, _.Aborted, none) other(game, _.Aborted, none)
} else if (game.player(!game.player.color).isOfferingDraw) { } else if (game.player(!game.player.color).isOfferingDraw) {
apply(game, _.Draw, None, Some(trans.drawOfferAccepted.txt())) apply(game, _.Draw, None, Messenger.Persistent(trans.drawOfferAccepted.txt()).some)
} else { } else {
val winner = Some(!game.player.color) ifFalse game.situation.opponentHasInsufficientMaterial val winner = Some(!game.player.color) ifFalse game.situation.opponentHasInsufficientMaterial
apply(game, _.Outoftime, winner) >>- apply(game, _.Outoftime, winner) >>-
@ -62,14 +62,14 @@ final private class Finisher(
lila.mon.round.expiration.count.increment() lila.mon.round.expiration.count.increment()
playban.noStart(Pov(game, culprit)) playban.noStart(Pov(game, culprit))
if (game.isMandatory) apply(game, _.NoStart, Some(!culprit.color)) if (game.isMandatory) apply(game, _.NoStart, Some(!culprit.color))
else apply(game, _.Aborted, None, Some("Game aborted by server")) else apply(game, _.Aborted, None, Messenger.Persistent("Game aborted by server").some)
} }
def other( def other(
game: Game, game: Game,
status: Status.type => Status, status: Status.type => Status,
winner: Option[Color], winner: Option[Color],
message: Option[String] = None message: Option[Messenger.SystemMessage] = None
)(implicit proxy: GameProxy): Fu[Events] = )(implicit proxy: GameProxy): Fu[Events] =
apply(game, status, winner, message) >>- playban.other(game, status, winner).unit apply(game, status, winner, message) >>- playban.other(game, status, winner).unit
@ -108,7 +108,7 @@ final private class Finisher(
game: Game, game: Game,
makeStatus: Status.type => Status, makeStatus: Status.type => Status,
winnerC: Option[Color], winnerC: Option[Color],
message: Option[String] = None message: Option[Messenger.SystemMessage] = None
)(implicit proxy: GameProxy): Fu[Events] = { )(implicit proxy: GameProxy): Fu[Events] = {
val status = makeStatus(Status) val status = makeStatus(Status)
val prog = game.finish(status, winnerC) val prog = game.finish(status, winnerC)
@ -139,7 +139,7 @@ final private class Finisher(
.flatMap { case (whiteO, blackO) => .flatMap { case (whiteO, blackO) =>
val finish = FinishGame(g, whiteO, blackO) val finish = FinishGame(g, whiteO, blackO)
updateCountAndPerfs(finish) map { ratingDiffs => updateCountAndPerfs(finish) map { ratingDiffs =>
message foreach { messenger.system(g, _) } message foreach { messenger(g, _) }
gameRepo game g.id foreach { newGame => gameRepo game g.id foreach { newGame =>
newGame foreach proxy.setFinishedGame newGame foreach proxy.setFinishedGame
val newFinish = finish.copy(game = newGame | g) val newFinish = finish.copy(game = newGame | g)

View File

@ -7,6 +7,7 @@ import play.api.libs.json._
import scala.math import scala.math
import lila.common.ApiVersion import lila.common.ApiVersion
import lila.common.Json._
import lila.game.JsonView._ import lila.game.JsonView._
import lila.game.{ Pov, Game, Player => GamePlayer } import lila.game.{ Pov, Game, Player => GamePlayer }
import lila.pref.Pref import lila.pref.Pref

View File

@ -13,6 +13,11 @@ final class Messenger(api: ChatApi) {
def volatile(game: Game, message: String): Unit = def volatile(game: Game, message: String): Unit =
system(persistent = false)(game, message) system(persistent = false)(game, message)
def apply(game: Game, message: Messenger.SystemMessage): Unit = message match {
case Messenger.Persistent(msg) => system(persistent = true)(game, msg)
case Messenger.Volatile(msg) => system(persistent = false)(game, msg)
}
def system(persistent: Boolean)(game: Game, message: String): Unit = if (game.nonAi) { def system(persistent: Boolean)(game: Game, message: String): Unit = if (game.nonAi) {
api.userChat.volatile(watcherId(Chat.Id(game.id)), message, _.Round) api.userChat.volatile(watcherId(Chat.Id(game.id)), message, _.Round)
if (persistent) api.userChat.system(Chat.Id(game.id), message, _.Round) if (persistent) api.userChat.system(Chat.Id(game.id), message, _.Round)
@ -58,3 +63,10 @@ final class Messenger(api: ChatApi) {
private def watcherId(chatId: Chat.Id) = Chat.Id(s"$chatId/w") private def watcherId(chatId: Chat.Id) = Chat.Id(s"$chatId/w")
private def watcherId(gameId: Game.Id) = Chat.Id(s"$gameId/w") private def watcherId(gameId: Game.Id) = Chat.Id(s"$gameId/w")
} }
private object Messenger {
sealed trait SystemMessage { val msg: String }
case class Persistent(msg: String) extends SystemMessage
case class Volatile(msg: String) extends SystemMessage
}

View File

@ -56,10 +56,10 @@ final private class Rematcher(
} }
def no(pov: Pov): Fu[Events] = { def no(pov: Pov): Fu[Events] = {
if (isOffering(pov)) messenger.system(pov.game, trans.rematchOfferCanceled.txt()) if (isOffering(pov)) messenger.volatile(pov.game, trans.rematchOfferCanceled.txt())
else if (isOffering(!pov)) { else if (isOffering(!pov)) {
declined put pov.fullId declined put pov.fullId
messenger.system(pov.game, trans.rematchOfferDeclined.txt()) messenger.volatile(pov.game, trans.rematchOfferDeclined.txt())
} }
offers invalidate pov.game.id offers invalidate pov.game.id
fuccess(List(Event.RematchOffer(by = none))) fuccess(List(Event.RematchOffer(by = none)))
@ -80,7 +80,7 @@ final private class Rematcher(
_ = if (pov.game.variant == Chess960 && !chess960.get(pov.gameId)) chess960.put(nextGame.id) _ = if (pov.game.variant == Chess960 && !chess960.get(pov.gameId)) chess960.put(nextGame.id)
_ <- gameRepo insertDenormalized nextGame _ <- gameRepo insertDenormalized nextGame
} yield { } yield {
messenger.system(pov.game, trans.rematchOfferAccepted.txt()) messenger.volatile(pov.game, trans.rematchOfferAccepted.txt())
onStart(nextGame.id) onStart(nextGame.id)
redirectEvents(nextGame) redirectEvents(nextGame)
} }
@ -88,7 +88,7 @@ final private class Rematcher(
} }
private def rematchCreate(pov: Pov): Events = { private def rematchCreate(pov: Pov): Events = {
messenger.system(pov.game, trans.rematchOfferSent.txt()) messenger.volatile(pov.game, trans.rematchOfferSent.txt())
pov.opponent.userId foreach { forId => pov.opponent.userId foreach { forId =>
Bus.publish(lila.hub.actorApi.round.RematchOffer(pov.gameId), s"rematchFor:$forId") Bus.publish(lila.hub.actorApi.round.RematchOffer(pov.gameId), s"rematchFor:$forId")
} }

View File

@ -403,7 +403,7 @@ final private[round] class RoundAsyncActor(
case WsBoot => case WsBoot =>
handle { game => handle { game =>
game.playable ?? { game.playable ?? {
messenger.system(game, "Lichess has been updated! Sorry for the inconvenience.") messenger.volatile(game, "Lichess has been updated! Sorry for the inconvenience.")
val progress = moretimer.give(game, Color.all, 20 seconds) val progress = moretimer.give(game, Color.all, 20 seconds)
proxy save progress inject progress.events proxy save progress inject progress.events
} }

View File

@ -35,7 +35,7 @@ final private class Takebacker(
double(game) >>- publishTakeback(pov) dmap (_ -> situation) double(game) >>- publishTakeback(pov) dmap (_ -> situation)
case Pov(game, color) if (game playerCanProposeTakeback color) && situation.offerable => case Pov(game, color) if (game playerCanProposeTakeback color) && situation.offerable =>
{ {
messenger.system(game, trans.takebackPropositionSent.txt()) messenger.volatile(game, trans.takebackPropositionSent.txt())
val progress = Progress(game) map { g => val progress = Progress(game) map { g =>
g.updatePlayer(color, _ proposeTakeback g.turns) g.updatePlayer(color, _ proposeTakeback g.turns)
} }
@ -50,7 +50,7 @@ final private class Takebacker(
def no(situation: TakebackSituation)(pov: Pov)(implicit proxy: GameProxy): Fu[(Events, TakebackSituation)] = def no(situation: TakebackSituation)(pov: Pov)(implicit proxy: GameProxy): Fu[(Events, TakebackSituation)] =
pov match { pov match {
case Pov(game, color) if pov.player.isProposingTakeback => case Pov(game, color) if pov.player.isProposingTakeback =>
messenger.system(game, trans.takebackPropositionCanceled.txt()) messenger.volatile(game, trans.takebackPropositionCanceled.txt())
val progress = Progress(game) map { g => val progress = Progress(game) map { g =>
g.updatePlayer(color, _.removeTakebackProposition) g.updatePlayer(color, _.removeTakebackProposition)
} }
@ -58,7 +58,7 @@ final private class Takebacker(
publishTakebackOffer(progress.game) inject publishTakebackOffer(progress.game) inject
List(Event.TakebackOffers(white = false, black = false)) -> situation.decline List(Event.TakebackOffers(white = false, black = false)) -> situation.decline
case Pov(game, color) if pov.opponent.isProposingTakeback => case Pov(game, color) if pov.opponent.isProposingTakeback =>
messenger.system(game, trans.takebackPropositionDeclined.txt()) messenger.volatile(game, trans.takebackPropositionDeclined.txt())
val progress = Progress(game) map { g => val progress = Progress(game) map { g =>
g.updatePlayer(!color, _.removeTakebackProposition) g.updatePlayer(!color, _.removeTakebackProposition)
} }

View File

@ -97,25 +97,31 @@ final class GarbageCollector(
private def isBadAccount(user: User) = user.lameOrTrollOrAlt private def isBadAccount(user: User) = user.lameOrTrollOrAlt
private def collect(user: User, email: EmailAddress, msg: => String): Funit = private def collect(user: User, email: EmailAddress, msg: => String): Funit = justOnce(user.id) ?? {
justOnce(user.id) ?? { hasBeenCollectedBefore(user) flatMap {
val armed = isArmed() case true => funit
val wait = (30 + ThreadLocalRandom.nextInt(300)).seconds case _ =>
val message = val armed = isArmed()
s"Will dispose of @${user.username} in $wait. Email: ${email.value}. $msg${!armed ?? " [SIMULATION]"}" val wait = (30 + ThreadLocalRandom.nextInt(300)).seconds
logger.info(message) val message =
noteApi.lichessWrite(user, s"Garbage collected because of $msg") s"Will dispose of @${user.username} in $wait. Email: ${email.value}. $msg${!armed ?? " [SIMULATION]"}"
irc.garbageCollector(message) >>- { logger.info(message)
if (armed) { noteApi.lichessWrite(user, s"Garbage collected because of $msg")
doInitialSb(user) irc.garbageCollector(message) >>- {
system.scheduler if (armed) {
.scheduleOnce(wait) { doInitialSb(user)
doCollect(user) system.scheduler
} .scheduleOnce(wait) {
.unit doCollect(user)
}
.unit
}
} }
}
} }
}
private def hasBeenCollectedBefore(user: User): Fu[Boolean] =
noteApi.byUserForMod(user.id).map(_.exists(_.text startsWith "Garbage collected"))
private def doInitialSb(user: User): Unit = private def doInitialSb(user: User): Unit =
Bus.publish( Bus.publish(

View File

@ -3,6 +3,7 @@ package lila.simul
import play.api.libs.json._ import play.api.libs.json._
import lila.common.LightUser import lila.common.LightUser
import lila.common.Json._
import lila.game.{ Game, GameRepo } import lila.game.{ Game, GameRepo }
import lila.user.User import lila.user.User
@ -12,10 +13,6 @@ final class JsonView(
proxyRepo: lila.round.GameProxyRepo proxyRepo: lila.round.GameProxyRepo
)(implicit ec: scala.concurrent.ExecutionContext) { )(implicit ec: scala.concurrent.ExecutionContext) {
implicit private val colorWriter: Writes[chess.Color] = Writes { c =>
JsString(c.name)
}
implicit private val simulTeamWriter = Json.writes[SimulTeam] implicit private val simulTeamWriter = Json.writes[SimulTeam]
private def fetchGames(simul: Simul) = private def fetchGames(simul: Simul) =

View File

@ -19,14 +19,14 @@ final private class ChapterMaker(
import ChapterMaker._ import ChapterMaker._
def apply(study: Study, data: Data, order: Int, userId: User.ID): Fu[Chapter] = def apply(study: Study, data: Data, order: Int, userId: User.ID, withRatings: Boolean): Fu[Chapter] =
data.game.??(parseGame) flatMap { data.game.??(parseGame) flatMap {
case None => case None =>
data.game ?? pgnFetch.fromUrl flatMap { data.game ?? pgnFetch.fromUrl flatMap {
case Some(pgn) => fromFenOrPgnOrBlank(study, data.copy(pgn = pgn.some), order, userId) case Some(pgn) => fromFenOrPgnOrBlank(study, data.copy(pgn = pgn.some), order, userId)
case _ => fromFenOrPgnOrBlank(study, data, order, userId) case _ => fromFenOrPgnOrBlank(study, data, order, userId)
} }
case Some(game) => fromGame(study, game, data, order, userId) case Some(game) => fromGame(study, game, data, order, userId, withRatings)
} map { (c: Chapter) => } map { (c: Chapter) =>
if (c.name.value.isEmpty) c.copy(name = Chapter defaultName order) else c if (c.name.value.isEmpty) c.copy(name = Chapter defaultName order) else c
} }
@ -125,14 +125,15 @@ final private class ChapterMaker(
data: Data, data: Data,
order: Int, order: Int,
userId: User.ID, userId: User.ID,
withRatings: Boolean,
initialFen: Option[FEN] = None initialFen: Option[FEN] = None
): Fu[Chapter] = ): Fu[Chapter] =
for { for {
root <- game2root(game, initialFen) root <- game2root(game, initialFen)
tags <- pgnDump.tags(game, initialFen, none, withOpening = true, withRating = true) tags <- pgnDump.tags(game, initialFen, none, withOpening = true, withRatings)
name <- { name <- {
if (data.isDefaultName) if (data.isDefaultName)
Namer.gameVsText(game, withRatings = false)(lightUser.async) dmap Chapter.Name.apply Namer.gameVsText(game, withRatings)(lightUser.async) dmap Chapter.Name.apply
else fuccess(data.name) else fuccess(data.name)
} }
_ = notifyChat(study, game, userId) _ = notifyChat(study, game, userId)

View File

@ -127,21 +127,12 @@ object JsonView {
implicit val chapterIdWrites: Writes[Chapter.Id] = stringIsoWriter(Chapter.idIso) implicit val chapterIdWrites: Writes[Chapter.Id] = stringIsoWriter(Chapter.idIso)
implicit val chapterNameWrites: Writes[Chapter.Name] = stringIsoWriter(Chapter.nameIso) implicit val chapterNameWrites: Writes[Chapter.Name] = stringIsoWriter(Chapter.nameIso)
implicit private[study] val uciWrites: Writes[Uci] = Writes[Uci] { u =>
JsString(u.uci)
}
implicit private val posReader: Reads[Pos] = Reads[Pos] { v => implicit private val posReader: Reads[Pos] = Reads[Pos] { v =>
(v.asOpt[String] flatMap Pos.fromKey).fold[JsResult[Pos]](JsError(Nil))(JsSuccess(_)) (v.asOpt[String] flatMap Pos.fromKey).fold[JsResult[Pos]](JsError(Nil))(JsSuccess(_))
} }
implicit private[study] val pathWrites: Writes[Path] = Writes[Path] { p => implicit private[study] val pathWrites: Writes[Path] = Writes[Path] { p =>
JsString(p.toString) JsString(p.toString)
} }
implicit private[study] val colorWriter: Writes[chess.Color] = Writes[chess.Color] { c =>
JsString(c.name)
}
implicit private[study] val fenWriter: Writes[FEN] = Writes[FEN] { f =>
JsString(f.value)
}
implicit private[study] val sriWriter: Writes[Sri] = Writes[Sri] { sri => implicit private[study] val sriWriter: Writes[Sri] = Writes[Sri] { sri =>
JsString(sri.value) JsString(sri.value)
} }

View File

@ -22,14 +22,15 @@ object ServerEval {
private val onceEvery = lila.memo.OnceEvery(5 minutes) private val onceEvery = lila.memo.OnceEvery(5 minutes)
def apply(study: Study, chapter: Chapter, userId: User.ID): Funit = def apply(study: Study, chapter: Chapter, userId: User.ID, unlimited: Boolean = false): Funit =
chapter.serverEval.fold(true) { eval => chapter.serverEval.fold(true) { eval =>
!eval.done && onceEvery(chapter.id.value) !eval.done && onceEvery(chapter.id.value)
} ?? { } ?? {
val unlimitedFu = val unlimitedFu =
fuccess(userId == User.lichessId) >>| userRepo fuccess(unlimited) >>|
.byId(userId) fuccess(userId == User.lichessId) >>| userRepo
.map(_.exists(Granter(_.Relay))) .byId(userId)
.map(_.exists(Granter(_.Relay)))
unlimitedFu flatMap { unlimited => unlimitedFu flatMap { unlimited =>
chapterRepo.startServerEval(chapter) >>- { chapterRepo.startServerEval(chapter) >>- {
fishnet ! StudyChapterRequest( fishnet ! StudyChapterRequest(

View File

@ -132,7 +132,8 @@ final class StudyApi(
addChapter( addChapter(
studyId = study.id, studyId = study.id,
data = data.form.toChapterData, data = data.form.toChapterData,
sticky = study.settings.sticky sticky = study.settings.sticky,
withRatings
)(Who(user.id, Sri(""))) >> byIdWithLastChapter(studyId) )(Who(user.id, Sri(""))) >> byIdWithLastChapter(studyId)
case _ => fuccess(none) case _ => fuccess(none)
} orElse importGame(data.copy(form = data.form.copy(asStr = none)), user, withRatings) } orElse importGame(data.copy(form = data.form.copy(asStr = none)), user, withRatings)
@ -587,11 +588,13 @@ final class StudyApi(
} }
} }
def addChapter(studyId: Study.Id, data: ChapterMaker.Data, sticky: Boolean)(who: Who): Funit = def addChapter(studyId: Study.Id, data: ChapterMaker.Data, sticky: Boolean, withRatings: Boolean)(
who: Who
): Funit =
data.manyGames match { data.manyGames match {
case Some(datas) => case Some(datas) =>
lila.common.Future.applySequentially(datas) { data => lila.common.Future.applySequentially(datas) { data =>
addChapter(studyId, data, sticky)(who) addChapter(studyId, data, sticky, withRatings)(who)
} }
case _ => case _ =>
sequenceStudy(studyId) { study => sequenceStudy(studyId) { study =>
@ -605,7 +608,7 @@ final class StudyApi(
} }
} >> } >>
chapterRepo.nextOrderByStudy(study.id) flatMap { order => chapterRepo.nextOrderByStudy(study.id) flatMap { order =>
chapterMaker(study, data, order, who.u) flatMap { chapter => chapterMaker(study, data, order, who.u, withRatings) flatMap { chapter =>
doAddChapter(study, chapter, sticky, who) doAddChapter(study, chapter, sticky, who)
} addFailureEffect { } addFailureEffect {
case ChapterMaker.ValidationException(error) => case ChapterMaker.ValidationException(error) =>
@ -624,9 +627,11 @@ final class StudyApi(
studyRepo.updateSomeFields(study) >>- indexStudy(study) studyRepo.updateSomeFields(study) >>- indexStudy(study)
} }
def importPgns(studyId: Study.Id, datas: List[ChapterMaker.Data], sticky: Boolean)(who: Who) = def importPgns(studyId: Study.Id, datas: List[ChapterMaker.Data], sticky: Boolean, withRatings: Boolean)(
who: Who
) =
lila.common.Future.applySequentially(datas) { data => lila.common.Future.applySequentially(datas) { data =>
addChapter(studyId, data, sticky)(who) addChapter(studyId, data, sticky, withRatings)(who)
} }
def doAddChapter(study: Study, chapter: Chapter, sticky: Boolean, who: Who) = def doAddChapter(study: Study, chapter: Chapter, sticky: Boolean, who: Who) =
@ -731,7 +736,13 @@ final class StudyApi(
chapterRepo.orderedMetadataByStudy(studyId).flatMap { chaps => chapterRepo.orderedMetadataByStudy(studyId).flatMap { chaps =>
// deleting the only chapter? Automatically create an empty one // deleting the only chapter? Automatically create an empty one
if (chaps.sizeIs < 2) { if (chaps.sizeIs < 2) {
chapterMaker(study, ChapterMaker.Data(Chapter.Name("Chapter 1")), 1, who.u) flatMap { c => chapterMaker(
study,
ChapterMaker.Data(Chapter.Name("Chapter 1")),
1,
who.u,
withRatings = true
) flatMap { c =>
doAddChapter(study, c, sticky = true, who) >> doSetChapter(study, c.id, who) doAddChapter(study, c, sticky = true, who) >> doSetChapter(study, c.id, who)
} }
} // deleting the current chapter? Automatically move to another one } // deleting the current chapter? Automatically move to another one
@ -841,10 +852,15 @@ final class StudyApi(
} }
} }
def analysisRequest(studyId: Study.Id, chapterId: Chapter.Id, userId: User.ID): Funit = def analysisRequest(
studyId: Study.Id,
chapterId: Chapter.Id,
userId: User.ID,
unlimited: Boolean = false
): Funit =
sequenceStudyWithChapter(studyId, chapterId) { case Study.WithChapter(study, chapter) => sequenceStudyWithChapter(studyId, chapterId) { case Study.WithChapter(study, chapter) =>
Contribute(userId, study) { Contribute(userId, study) {
serverEvalRequester(study, chapter, userId) serverEvalRequester(study, chapter, userId, unlimited)
} }
} }

View File

@ -40,7 +40,9 @@ final private class StudyInvite(
invited <- invited <-
userRepo userRepo
.named(invitedUsername) .named(invitedUsername)
.map(_.filterNot(_.id == User.lichessId)) orFail "No such invited" .map(
_.filterNot(_.id == User.lichessId && !Granter(_.StudyAdmin)(inviter))
) orFail "No such invited"
_ <- study.members.contains(invited) ?? fufail[Unit]("Already a member") _ <- study.members.contains(invited) ?? fufail[Unit]("Already a member")
relation <- relationApi.fetchRelation(invited.id, byUserId) relation <- relationApi.fetchRelation(invited.id, byUserId)
_ <- relation.has(Block) ?? fufail[Unit]("This user does not want to join") _ <- relation.has(Block) ?? fufail[Unit]("This user does not want to join")
@ -62,7 +64,6 @@ final private class StudyInvite(
else if (inviter.roles has "ROLE_COACH") 20 else if (inviter.roles has "ROLE_COACH") 20
else if (inviter.hasTitle) 20 else if (inviter.hasTitle) 20
else if (inviter.perfs.bestRating >= 2000) 50 else if (inviter.perfs.bestRating >= 2000) 50
else if (invited.hasTitle) 200
else 100 else 100
_ <- shouldNotify ?? notifyRateLimit(inviter.id, rateLimitCost) { _ <- shouldNotify ?? notifyRateLimit(inviter.id, rateLimitCost) {
val notificationContent = InvitedToStudy( val notificationContent = InvitedToStudy(

View File

@ -106,6 +106,8 @@ final class StudyMultiBoard(
private object handlers { private object handlers {
import lila.common.Json._
implicit val previewPlayerWriter: Writes[ChapterPreview.Player] = Writes[ChapterPreview.Player] { p => implicit val previewPlayerWriter: Writes[ChapterPreview.Player] = Writes[ChapterPreview.Player] { p =>
Json Json
.obj("name" -> p.name) .obj("name" -> p.name)

View File

@ -124,7 +124,7 @@ final private class StudySocket(
case "addChapter" => case "addChapter" =>
reading[ChapterMaker.Data](o) { data => reading[ChapterMaker.Data](o) { data =>
val sticky = o.obj("d").flatMap(_.boolean("sticky")) | true val sticky = o.obj("d").flatMap(_.boolean("sticky")) | true
who foreach api.addChapter(studyId, data, sticky = sticky) who foreach api.addChapter(studyId, data, sticky = sticky, withRatings = true)
} }
case "setChapter" => case "setChapter" =>
o.get[Chapter.Id]("d") foreach { chapterId => o.get[Chapter.Id]("d") foreach { chapterId =>
@ -304,7 +304,8 @@ final private class StudySocket(
"w" -> who "w" -> who
) )
) )
def setLiking(liking: Study.Liking, who: Who) = notify("liking", Json.obj("l" -> liking, "w" -> who)) def setLiking(liking: Study.Liking, who: Who) =
notifySri(who.sri, "liking", Json.obj("l" -> liking, "w" -> who))
def setShapes(pos: Position.Ref, shapes: Shapes, who: Who) = def setShapes(pos: Position.Ref, shapes: Shapes, who: Who) =
version( version(
"shapes", "shapes",

View File

@ -288,6 +288,7 @@ final class TeamApi(
lila.security.Granter(_.ManageTeam)(by) || team.createdBy == by.id || lila.security.Granter(_.ManageTeam)(by) || team.createdBy == by.id ||
(team.leaders(by.id) && !team.leaders(team.createdBy)) (team.leaders(by.id) && !team.leaders(team.createdBy))
) { ) {
logger.info(s"toggleEnabled ${team.id}: ${!team.enabled} by @${by.id}")
if (team.enabled) if (team.enabled)
teamRepo.disable(team).void >> teamRepo.disable(team).void >>
memberRepo.userIdsByTeam(team.id).map { _ foreach cached.invalidateTeamIds } >> memberRepo.userIdsByTeam(team.id).map { _ foreach cached.invalidateTeamIds } >>

View File

@ -173,7 +173,8 @@ final class UserRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
coll coll
.update(ordered = false, WriteConcern.Unacknowledged) .update(ordered = false, WriteConcern.Unacknowledged)
.one( .one(
$id(userId) ++ (value < 0).??($doc(F.colorIt $gt -3)), // limit to -3 <= colorIt <= 5 but set when undefined
$id(userId) ++ $doc(F.colorIt -> $not(if (value < 0) $lte(-3) else $gte(5))),
$inc(F.colorIt -> value) $inc(F.colorIt -> value)
) )
.unit .unit

View File

@ -13,7 +13,7 @@ object Dependencies {
val maxmind = "com.sanoma.cda" %% "maxmind-geoip2-scala" % "1.3.1-THIB" val maxmind = "com.sanoma.cda" %% "maxmind-geoip2-scala" % "1.3.1-THIB"
val prismic = "io.prismic" %% "scala-kit" % "1.2.19-THIB213" val prismic = "io.prismic" %% "scala-kit" % "1.2.19-THIB213"
val scaffeine = "com.github.blemale" %% "scaffeine" % "5.1.1" % "compile" val scaffeine = "com.github.blemale" %% "scaffeine" % "5.1.1" % "compile"
val googleOAuth = "com.google.auth" % "google-auth-library-oauth2-http" % "1.2.2" val googleOAuth = "com.google.auth" % "google-auth-library-oauth2-http" % "1.3.0"
val scalaUri = "io.lemonlabs" %% "scala-uri" % "3.6.0" val scalaUri = "io.lemonlabs" %% "scala-uri" % "3.6.0"
val scalatags = "com.lihaoyi" %% "scalatags" % "0.10.0" val scalatags = "com.lihaoyi" %% "scalatags" % "0.10.0"
val lettuce = "io.lettuce" % "lettuce-core" % "6.1.5.RELEASE" val lettuce = "io.lettuce" % "lettuce-core" % "6.1.5.RELEASE"
@ -21,7 +21,7 @@ object Dependencies {
val autoconfig = "io.methvin.play" %% "autoconfig-macros" % "0.3.2" % "provided" val autoconfig = "io.methvin.play" %% "autoconfig-macros" % "0.3.2" % "provided"
val scalatest = "org.scalatest" %% "scalatest" % "3.1.0" % Test val scalatest = "org.scalatest" %% "scalatest" % "3.1.0" % Test
val uaparser = "org.uaparser" %% "uap-scala" % "0.13.0" val uaparser = "org.uaparser" %% "uap-scala" % "0.13.0"
val specs2 = "org.specs2" %% "specs2-core" % "4.13.0" % Test val specs2 = "org.specs2" %% "specs2-core" % "4.13.1" % Test
val apacheText = "org.apache.commons" % "commons-text" % "1.9" val apacheText = "org.apache.commons" % "commons-text" % "1.9"
val bloomFilter = "com.github.alexandrnikitin" %% "bloom-filter" % "0.13.1" val bloomFilter = "com.github.alexandrnikitin" %% "bloom-filter" % "0.13.1"
@ -38,17 +38,17 @@ object Dependencies {
val version = "2.4.2" val version = "2.4.2"
val macros = "com.softwaremill.macwire" %% "macros" % version % "provided" val macros = "com.softwaremill.macwire" %% "macros" % version % "provided"
val util = "com.softwaremill.macwire" %% "util" % version % "provided" val util = "com.softwaremill.macwire" %% "util" % version % "provided"
val tagging = "com.softwaremill.common" %% "tagging" % "2.3.1" val tagging = "com.softwaremill.common" %% "tagging" % "2.3.2"
def bundle = Seq(macros, util, tagging) def bundle = Seq(macros, util, tagging)
} }
object reactivemongo { object reactivemongo {
val version = "1.0.7" val version = "1.0.8"
val driver = "org.reactivemongo" %% "reactivemongo" % version val driver = "org.reactivemongo" %% "reactivemongo" % version
val stream = "org.reactivemongo" %% "reactivemongo-akkastream" % version val stream = "org.reactivemongo" %% "reactivemongo-akkastream" % version
val epoll = "org.reactivemongo" % "reactivemongo-shaded-native" % s"$version-linux-x86-64" val epoll = "org.reactivemongo" % "reactivemongo-shaded-native" % s"$version-linux-x86-64"
val kamon = "org.reactivemongo" %% "reactivemongo-kamon" % "1.0.7" val kamon = "org.reactivemongo" %% "reactivemongo-kamon" % "1.0.8"
def bundle = Seq(driver, stream) def bundle = Seq(driver, stream)
} }

View File

@ -1 +1 @@
sbt.version=1.5.5 sbt.version=1.5.6

View File

@ -3,5 +3,5 @@ resolvers += Resolver.url(
url("https://raw.githubusercontent.com/ornicar/lila-maven/master") url("https://raw.githubusercontent.com/ornicar/lila-maven/master")
)(Resolver.ivyStylePatterns) )(Resolver.ivyStylePatterns)
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8-lila_1.8") addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8-lila_1.8")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.5")
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.11") addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.11")

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