merge + tweaks, post-log4j
commit
07fe9c8817
|
@ -55,3 +55,5 @@ lost+found
|
|||
nohup.out
|
||||
nohup.out.old
|
||||
ATTIC
|
||||
# IntelliJ auto-generated files
|
||||
.idea
|
||||
|
|
|
@ -4,3 +4,4 @@ maxColumn = 110
|
|||
spaces.inImportCurlyBraces = true
|
||||
rewrite.rules = [SortImports, RedundantParens, SortModifiers]
|
||||
rewrite.redundantBraces.stringInterpolation = true
|
||||
runner.dialect = scala213
|
||||
|
|
|
@ -117,16 +117,21 @@ final class Env(
|
|||
text = "Team IDs that always get their tournaments visible on /tournament. Separated by commas.".some
|
||||
)
|
||||
lazy val prizeTournamentMakers = memo.settingStore[UserIds](
|
||||
"prizeTournamentMakers ",
|
||||
"prizeTournamentMakers",
|
||||
default = UserIds(Nil),
|
||||
text =
|
||||
"User IDs who can make prize tournaments (arena & swiss) without a warning. Separated by commas.".some
|
||||
)
|
||||
lazy val apiExplorerGamesPerSecond = memo.settingStore[Int](
|
||||
"apiExplorerGamesPerSecond ",
|
||||
default = 200,
|
||||
"apiExplorerGamesPerSecond",
|
||||
default = 300,
|
||||
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 socialInfo = wire[mashup.UserInfo.SocialApi]
|
||||
|
|
|
@ -10,6 +10,7 @@ import lila.api.{ Context, GameApiV2 }
|
|||
import lila.app._
|
||||
import lila.common.config.{ MaxPerPage, MaxPerSecond }
|
||||
import lila.common.{ HTTPRequest, IpAddress }
|
||||
import lila.common.LightUser
|
||||
|
||||
final class Api(
|
||||
env: Env,
|
||||
|
@ -74,17 +75,20 @@ final class Api(
|
|||
def usersStatus =
|
||||
ApiRequest { req =>
|
||||
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
|
||||
toApiResult {
|
||||
users.map { u =>
|
||||
lila.common.LightUser.lightUserWrites
|
||||
.writes(u)
|
||||
.add("online" -> env.socket.isOnline(u.id))
|
||||
.add("playing" -> env.round.playing(u.id))
|
||||
.add("streaming" -> streamingIds(u.id))
|
||||
def toJson(u: LightUser) =
|
||||
lila.common.LightUser.lightUserWrites
|
||||
.writes(u)
|
||||
.add("online" -> env.socket.isOnline(u.id))
|
||||
.add("playing" -> env.round.playing(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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,9 @@ final class Dev(env: Env) extends LilaController(env) {
|
|||
env.fishnet.openingBookDepth,
|
||||
env.noDelaySecretSetting,
|
||||
env.featuredTeamsSetting,
|
||||
env.prizeTournamentMakers
|
||||
env.prizeTournamentMakers,
|
||||
env.pieceImageExternal,
|
||||
env.evalCache.enable
|
||||
)
|
||||
|
||||
def settings =
|
||||
|
|
|
@ -7,7 +7,7 @@ final class Irwin(env: Env) extends LilaController(env) {
|
|||
import lila.irwin.JSONHandlers.reportReader
|
||||
|
||||
def dashboard =
|
||||
Secure(_.SeeReport) { implicit ctx => _ =>
|
||||
Secure(_.MarkEngine) { implicit ctx => _ =>
|
||||
env.irwin.api.dashboard map { d =>
|
||||
Ok(views.html.irwin.dashboard(d))
|
||||
}
|
||||
|
|
|
@ -65,10 +65,11 @@ abstract private[controllers] class LilaController(val env: Env)
|
|||
implicit def reqConfig(implicit req: RequestHeader) = ui.EmbedConfig(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(
|
||||
"Cross-Origin-Opener-Policy" -> "same-origin",
|
||||
"Cross-Origin-Embedder-Policy" -> "require-corp"
|
||||
"Cross-Origin-Opener-Policy" -> "same-origin",
|
||||
"Cross-Origin-Embedder-Policy" -> (if (HTTPRequest isChrome96OrMore req) "credentialless"
|
||||
else "require-corp")
|
||||
)
|
||||
|
||||
protected def NoCache(res: Result): Result =
|
||||
|
|
|
@ -108,6 +108,7 @@ final class Mod(
|
|||
suspect <- modApi.setTroll(me, prev, prev.user.marks.troll)
|
||||
_ <- env.msg.api.systemPost(suspect.user.id, preset.text)
|
||||
_ <- env.mod.logApi.modMessage(me.id, suspect.user.id, preset.name)
|
||||
_ <- preset.isNameClose ?? env.irc.api.nameClosePreset(username)
|
||||
} yield (inquiry, suspect).some
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,15 +122,22 @@ final class PlayApi(
|
|||
def boardCommandGet(cmd: String) =
|
||||
ScopedBody(_.Board.Play) { implicit req => me =>
|
||||
cmd.split('/') match {
|
||||
case Array("game", id, "chat") =>
|
||||
WithPovAsBoard(id, me) { pov =>
|
||||
env.chat.api.userChat.find(lila.chat.Chat.Id(pov.game.id)) map
|
||||
lila.chat.JsonView.boardApi map JsonOk
|
||||
}
|
||||
case _ => notFoundJson("No such command")
|
||||
case Array("game", id, "chat") => WithPovAsBoard(id, me)(getChat)
|
||||
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
|
||||
|
||||
private def toResult(f: Funit): Fu[Result] = catchClientError(f inject jsonOkResult)
|
||||
|
|
|
@ -131,7 +131,7 @@ final class Round(
|
|||
Open { implicit ctx =>
|
||||
proxyPov(gameId, color) flatMap {
|
||||
case Some(pov) =>
|
||||
get("pov") match {
|
||||
get("pov").map(UserModel.normalize) match {
|
||||
case Some(requestedPov) =>
|
||||
(pov.player.userId, pov.opponent.userId) match {
|
||||
case (Some(_), Some(opponent)) if opponent == requestedPov =>
|
||||
|
|
|
@ -328,15 +328,17 @@ final class Study(
|
|||
env.study.api.importPgns(
|
||||
StudyModel.Id(id),
|
||||
data.toChapterDatas,
|
||||
sticky = data.sticky
|
||||
sticky = data.sticky,
|
||||
ctx.pref.showRatings
|
||||
)(Who(me.id, lila.socket.Socket.Sri(sri)))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def admin(id: String) =
|
||||
Secure(_.StudyAdmin) { _ => me =>
|
||||
env.study.api.adminInvite(id, me) inject Redirect(routes.Study.show(id))
|
||||
Secure(_.StudyAdmin) { ctx => me =>
|
||||
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) =
|
||||
|
|
|
@ -22,12 +22,11 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
|
|||
|
||||
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 dbImageUrl(path: String) = s"$assetBaseUrl/image/$path"
|
||||
|
||||
def cssTag(name: String)(implicit ctx: Context): Frag =
|
||||
cssTagWithTheme(name, ctx.currentBg)
|
||||
|
||||
|
|
|
@ -51,6 +51,6 @@ object Environment
|
|||
)
|
||||
|
||||
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>"""
|
||||
)
|
||||
}
|
||||
|
|
|
@ -99,15 +99,16 @@ trait ScalatagsPrefix {
|
|||
|
||||
// what to import in a pure scalatags template
|
||||
trait ScalatagsTemplate
|
||||
extends Styles
|
||||
with ScalatagsBundle
|
||||
extends ScalatagsBundle
|
||||
with ScalatagsAttrs
|
||||
with ScalatagsExtensions
|
||||
with ScalatagsSnippets
|
||||
with ScalatagsPrefix {
|
||||
|
||||
val trans = lila.i18n.I18nKeys
|
||||
def main = scalatags.Text.tags2.main
|
||||
val trans = lila.i18n.I18nKeys
|
||||
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 */
|
||||
implicit val playCallAttr = genericAttr[play.api.mvc.Call]
|
||||
|
|
|
@ -17,7 +17,7 @@ object bits {
|
|||
div(cls := "personal-data__header")(
|
||||
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)(
|
||||
trans.downloadRaw()
|
||||
trans.download()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -34,23 +34,38 @@ object layout {
|
|||
def pieceSprite(ps: lila.pref.PieceSet): Frag =
|
||||
link(
|
||||
id := "piece-sprite",
|
||||
href := assetUrl(s"piece-css/$ps.css"),
|
||||
href := assetUrl(s"piece-css/$ps.${env.pieceImageExternal.get() ?? "external."}css"),
|
||||
rel := "stylesheet"
|
||||
)
|
||||
}
|
||||
import bits._
|
||||
|
||||
private val noTranslate = raw("""<meta name="google" content="notranslate">""")
|
||||
private def fontPreload(implicit ctx: Context) =
|
||||
raw {
|
||||
s"""<link rel="preload" href="${assetUrl(
|
||||
s"font/lichess.woff2"
|
||||
)}" as="font" type="font/woff2" crossorigin>""" +
|
||||
!ctx.pref.pieceNotationIsLetter ??
|
||||
s"""<link rel="preload" href="${assetUrl(
|
||||
s"font/lichess.chess.woff2"
|
||||
)}" as="font" type="font/woff2" crossorigin>"""
|
||||
|
||||
private def preload(href: String, as: String, crossorigin: Boolean, tpe: Option[String] = None) =
|
||||
raw(s"""<link rel="preload" href="$href" as="$as" ${tpe.??(t =>
|
||||
s"""type="$t" """
|
||||
)}${crossorigin ?? "crossorigin"}>""")
|
||||
|
||||
private def fontPreload(implicit ctx: Context) = frag(
|
||||
preload(assetUrl(s"font/lichess.woff2"), "font", crossorigin = true, "font/woff2".some),
|
||||
!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(
|
||||
"""<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) =
|
||||
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""",
|
||||
nonce
|
||||
)
|
||||
|
@ -229,7 +244,7 @@ object layout {
|
|||
content := openGraph.fold(trans.siteDescription.txt())(o => o.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,
|
||||
!robots option raw("""<meta content="noindex, nofollow" name="robots">"""),
|
||||
noTranslate,
|
||||
|
@ -248,6 +263,8 @@ object layout {
|
|||
)
|
||||
},
|
||||
fontPreload,
|
||||
boardPreload,
|
||||
piecesPreload,
|
||||
manifests,
|
||||
jsLicense
|
||||
),
|
||||
|
|
|
@ -32,8 +32,8 @@ object notFound {
|
|||
iframe(
|
||||
src := assetUrl(s"vendor/ChessPursuit/bin-release/index.html"),
|
||||
st.frameborder := 0,
|
||||
width := 400,
|
||||
height := 500
|
||||
widthA := 400,
|
||||
heightA := 500
|
||||
),
|
||||
p(cls := "credits")(
|
||||
a(href := "https://github.com/Saturnyn/ChessPursuit")("ChessPursuit"),
|
||||
|
|
|
@ -46,8 +46,8 @@ object picture {
|
|||
img(
|
||||
widthA := Coach.imageSize,
|
||||
heightA := Coach.imageSize,
|
||||
width := cssSize,
|
||||
height := cssSize,
|
||||
cssWidth := cssSize,
|
||||
cssHeight := cssSize,
|
||||
cls := "picture",
|
||||
src := url(c.coach),
|
||||
alt := s"${c.user.titleUsername} Lichess coach picture"
|
||||
|
|
|
@ -97,8 +97,8 @@ object show {
|
|||
div(cls := "list")(
|
||||
profile.youtubeUrls.map { url =>
|
||||
iframe(
|
||||
width := "256",
|
||||
height := "192",
|
||||
widthA := "256",
|
||||
heightA := "192",
|
||||
src := url.value,
|
||||
attr("frameborder") := "0",
|
||||
frame.allowfullscreen
|
||||
|
|
|
@ -47,16 +47,16 @@ object mobile {
|
|||
),
|
||||
div(cls := "right-side")(
|
||||
img(
|
||||
cls := "nexus5-playing",
|
||||
width := "268",
|
||||
height := "513",
|
||||
src := assetUrl("images/mobile/nexus5-playing.png"),
|
||||
alt := "Lichess mobile on nexus 5"
|
||||
widthA := "437",
|
||||
heightA := "883",
|
||||
cls := "mobile-playing",
|
||||
src := assetUrl("images/mobile/lichesstv-mobile.png"),
|
||||
alt := "Lichess TV on mobile"
|
||||
),
|
||||
img(
|
||||
cls := "qrcode",
|
||||
width := "200",
|
||||
height := "200",
|
||||
widthA := "200",
|
||||
heightA := "200",
|
||||
src := assetUrl("images/mobile/dynamic-qrcode.png"),
|
||||
alt := "Download QR code"
|
||||
)
|
||||
|
|
|
@ -30,7 +30,7 @@ object menu {
|
|||
a(cls := active.active("tour"), href := routes.TournamentCrud.index(1))("Tournaments"),
|
||||
isGranted(_.ManageEvent) option
|
||||
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"),
|
||||
isGranted(_.Shadowban) option
|
||||
a(cls := active.active("panic"), href := routes.Mod.chatPanic)(
|
||||
|
|
|
@ -17,7 +17,8 @@ object authorize {
|
|||
moreJs = embedJsUnsafe(
|
||||
// ensure maximum browser compatibility
|
||||
"""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")(
|
||||
div(cls := "oauth__top")(
|
||||
|
|
|
@ -167,14 +167,24 @@ object forms {
|
|||
div(cls := "ratings")(
|
||||
form3.hidden("rating", "?"),
|
||||
lila.rating.PerfType.nonPuzzle.map { perfType =>
|
||||
div(cls := perfType.key)(
|
||||
trans.perfRatingX(
|
||||
raw(s"""<strong data-icon="${perfType.iconChar}">${ctx.pref.showRatings ?? me
|
||||
.perfs(perfType.key)
|
||||
.map(_.intRating.toString)
|
||||
.getOrElse("?")}</strong> ${perfType.trans}""")
|
||||
{
|
||||
val rating = me
|
||||
.perfs(perfType.key)
|
||||
.map(_.intRating.toString)
|
||||
.getOrElse("?")
|
||||
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
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ object dailyPuzzleSlackApp {
|
|||
)(
|
||||
img(
|
||||
alt := "Add to Slack",
|
||||
height := 40,
|
||||
width := 139,
|
||||
heightA := 40,
|
||||
widthA := 139,
|
||||
src := assetUrl("images/add-to-slack.png")
|
||||
)
|
||||
),
|
||||
|
|
|
@ -83,8 +83,7 @@ object show {
|
|||
def round(s: Swiss, r: SwissRound.Number, pairings: Paginator[SwissPairing])(implicit ctx: Context) =
|
||||
views.html.base.layout(
|
||||
title = s"${fullName(s)} • Round $r/${s.round}",
|
||||
moreCss = cssTag("swiss.show"),
|
||||
moreJs = infiniteScrollTag
|
||||
moreCss = cssTag("swiss.show")
|
||||
) {
|
||||
val pager = views.html.base.bits
|
||||
.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"),
|
||||
table(cls := "slist slist-pad")(
|
||||
tbody(cls := "infinite-scroll")(
|
||||
pairings.currentPageResults map { p =>
|
||||
tr(cls := "paginated")(
|
||||
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(userIdLink(p.black.some))
|
||||
)
|
||||
},
|
||||
pagerNextTable(pairings, p => routes.Swiss.round(s.id.value, r.value).url)
|
||||
)
|
||||
}
|
||||
),
|
||||
pager(cls := "pagination--bottom")
|
||||
)
|
||||
|
|
|
@ -73,7 +73,7 @@ object post {
|
|||
post.lived map { live =>
|
||||
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")(
|
||||
trans.ublog.nbViews.plural(post.views.value, strong(post.views.value.localize))
|
||||
),
|
||||
|
@ -104,9 +104,7 @@ object post {
|
|||
div(cls := "ublog-post__footer")(
|
||||
(ctx.isAuth && !ctx.is(user)) option
|
||||
div(cls := "ublog-post__actions")(
|
||||
likeButton(post, liked)(ctx)(cls := "ublog-post__like--big button button-big button-red")(
|
||||
span(cls := "button-label")("Like this post")
|
||||
),
|
||||
likeButton(post, liked, showText = true),
|
||||
followButton(user, post, followed)
|
||||
),
|
||||
h2(a(href := routes.Ublog.index(user.username))(trans.ublog.moreBlogPostsBy(user.username))),
|
||||
|
@ -122,15 +120,27 @@ object post {
|
|||
dataIcon := ""
|
||||
)(trans.edit())
|
||||
|
||||
private def likeButton(post: UblogPost, liked: Boolean)(implicit ctx: Context) = button(
|
||||
tpe := "button",
|
||||
cls := List(
|
||||
"ublog-post__like is" -> true,
|
||||
"ublog-post__like--liked" -> liked
|
||||
),
|
||||
dataRel := post.id.value,
|
||||
title := trans.study.like.txt()
|
||||
)(span(cls := "ublog-post__like__nb")(post.likes.value.localize))
|
||||
private def likeButton(post: UblogPost, liked: Boolean, showText: Boolean)(implicit ctx: Context) = {
|
||||
val text = if (liked) trans.study.unlike.txt() else trans.study.like.txt()
|
||||
button(
|
||||
tpe := "button",
|
||||
cls := List(
|
||||
"ublog-post__like is" -> true,
|
||||
"ublog-post__like--liked" -> liked,
|
||||
"ublog-post__like--big button button-big button-red" -> showText,
|
||||
"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) =
|
||||
div(
|
||||
|
|
|
@ -37,9 +37,7 @@ object bots {
|
|||
|
||||
private def botTable(users: List[User])(implicit ctx: Context) = table(cls := "slist slist-pad")(
|
||||
tbody(
|
||||
users.sortBy { u =>
|
||||
(if (u.isVerified) -1 else 1, -u.playTime.??(_.total))
|
||||
} map { u =>
|
||||
users map { u =>
|
||||
tr(
|
||||
td(userLink(u)),
|
||||
u.profile
|
||||
|
|
|
@ -62,7 +62,7 @@ object perfStat {
|
|||
counter(stat.count),
|
||||
highlow(stat),
|
||||
resultStreak(stat.resultStreak),
|
||||
result(stat),
|
||||
result(stat, user),
|
||||
playStreakNb(stat.playStreak),
|
||||
playStreakTime(stat.playStreak)
|
||||
)
|
||||
|
@ -259,7 +259,9 @@ object perfStat {
|
|||
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(
|
||||
table(
|
||||
thead(
|
||||
|
@ -271,17 +273,21 @@ object perfStat {
|
|||
results.results map { r =>
|
||||
tr(
|
||||
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")(
|
||||
resultTable(stat.bestWins, bestRated()),
|
||||
resultTable(stat.worstLosses, worstRated())
|
||||
resultTable(stat.bestWins, bestRated(), user),
|
||||
resultTable(stat.worstLosses, worstRated(), user)
|
||||
)
|
||||
|
||||
private def playStreakNbStreak(s: lila.perfStat.Streak, title: Frag => Frag)(implicit lang: Lang): Frag =
|
||||
|
|
|
@ -46,7 +46,7 @@ object otherTrophies {
|
|||
ariaTitle(t.kind.name),
|
||||
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 =>
|
||||
|
|
|
@ -108,9 +108,7 @@ pagerDuty {
|
|||
serviceId = ""
|
||||
apiKey = ""
|
||||
}
|
||||
prismic {
|
||||
api_url = "https://lichess.cdn.prismic.io/api"
|
||||
}
|
||||
prismic.api_url = "https://lichess-clone.cdn.prismic.io/api"
|
||||
blog {
|
||||
prismic = ${prismic}
|
||||
collection = blog
|
||||
|
|
|
@ -707,9 +707,10 @@ GET /api/user/:name/games controllers.Api.userGames(name: String)
|
|||
# Bot API
|
||||
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/*cmd controllers.PlayApi.botCommand(cmd: String)
|
||||
GET /player/bots controllers.PlayApi.botOnline
|
||||
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
|
||||
GET /api/board/game/stream/:id controllers.PlayApi.boardGameStream(id: String)
|
||||
|
|
|
@ -6,7 +6,6 @@ import play.api.libs.json._
|
|||
|
||||
import lila.common.Iso
|
||||
import lila.common.Json._
|
||||
import lila.game.JsonView.colorWrites
|
||||
import lila.game.LightPov
|
||||
import lila.rating.PerfType
|
||||
import lila.simul.Simul
|
||||
|
|
|
@ -8,7 +8,7 @@ import reactivemongo.api.ReadPreference
|
|||
|
||||
import lila.analyse.{ JsonView => analysisJson, Analysis }
|
||||
import lila.common.config._
|
||||
import lila.common.Json.jodaWrites
|
||||
import lila.common.Json._
|
||||
import lila.common.paginator.{ Paginator, PaginatorJson }
|
||||
import lila.db.dsl._
|
||||
import lila.db.paginator.Adapter
|
||||
|
|
|
@ -9,7 +9,7 @@ import scala.concurrent.duration._
|
|||
|
||||
import lila.analyse.{ JsonView => analysisJson, Analysis }
|
||||
import lila.common.config.MaxPerSecond
|
||||
import lila.common.Json.jodaWrites
|
||||
import lila.common.Json._
|
||||
import lila.common.{ HTTPRequest, LightUser }
|
||||
import lila.db.dsl._
|
||||
import lila.game.JsonView._
|
||||
|
|
|
@ -6,6 +6,7 @@ import play.api.libs.ws.StandaloneWSClient
|
|||
|
||||
import lila.common.config.MaxPerPage
|
||||
import lila.common.paginator._
|
||||
import scala.util.Try
|
||||
|
||||
final class BlogApi(
|
||||
config: BlogConfig
|
||||
|
@ -20,7 +21,7 @@ final class BlogApi(
|
|||
page: Int,
|
||||
maxPerPage: MaxPerPage,
|
||||
ref: Option[String]
|
||||
): Fu[Option[Paginator[Document]]] =
|
||||
): Fu[Option[Paginator[Document]]] = Try {
|
||||
api
|
||||
.forms(collection)
|
||||
.ref(ref | api.master.ref)
|
||||
|
@ -30,6 +31,9 @@ final class BlogApi(
|
|||
.submit()
|
||||
.fold(_ => none, some)
|
||||
.dmap2 { PrismicPaginator(_, page, maxPerPage) }
|
||||
} recover { case _: NoSuchElementException =>
|
||||
fuccess(none)
|
||||
} get
|
||||
|
||||
def recent(
|
||||
prismic: BlogApi.Context,
|
||||
|
@ -94,9 +98,7 @@ final class BlogApi(
|
|||
} getOrElse reqRef
|
||||
}
|
||||
|
||||
private val prismicBuilder = new Prismic
|
||||
|
||||
def prismicApi = prismicBuilder.get(config.apiUrl)
|
||||
def prismicApi = (new Prismic).get(config.apiUrl)
|
||||
}
|
||||
|
||||
object BlogApi {
|
||||
|
|
|
@ -3,6 +3,7 @@ package lila.bot
|
|||
import play.api.i18n.Lang
|
||||
import play.api.libs.json._
|
||||
|
||||
import lila.common.Json._
|
||||
import lila.common.Json.jodaWrites
|
||||
import lila.game.JsonView._
|
||||
import lila.game.{ Game, GameRepo, Pov }
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package lila.challenge
|
||||
|
||||
import play.api.libs.json._
|
||||
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.socket.Socket.SocketVersion
|
||||
import lila.socket.UserLagCache
|
||||
|
@ -13,7 +15,6 @@ final class JsonView(
|
|||
isOnline: lila.socket.IsOnline
|
||||
) {
|
||||
|
||||
import lila.game.JsonView._
|
||||
import Challenge._
|
||||
|
||||
implicit private val RegisteredWrites = OWrites[Challenger.Registered] { r =>
|
||||
|
|
|
@ -11,11 +11,9 @@ case class ContentSecurityPolicy(
|
|||
baseUri: List[String]
|
||||
) {
|
||||
|
||||
def withNonce(nonce: Nonce) = copy(scriptSrc =
|
||||
nonce.scriptSrc ::
|
||||
"'unsafe-inline'" :: // ignored by browsers supporting nonce
|
||||
scriptSrc
|
||||
)
|
||||
def withNonce(nonce: Nonce) = copy(scriptSrc = nonce.scriptSrc :: scriptSrc)
|
||||
|
||||
def withLegacyCompatibility = copy(scriptSrc = "'unsafe-inline'" :: scriptSrc)
|
||||
|
||||
def withWebAssembly =
|
||||
copy(
|
||||
|
|
|
@ -45,6 +45,7 @@ object HTTPRequest {
|
|||
|
||||
private def uaContains(req: RequestHeader, str: String) = userAgent(req).exists(_ contains str)
|
||||
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
|
||||
|
||||
|
@ -58,7 +59,7 @@ object HTTPRequest {
|
|||
def sid(req: RequestHeader): Option[String] = req.session get LilaCookie.sessionId
|
||||
|
||||
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|""" +
|
||||
"""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"""
|
||||
}
|
||||
|
|
|
@ -41,9 +41,16 @@ object Json {
|
|||
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 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")
|
||||
}
|
||||
implicit val uciWrites: Writes[Uci] = Writes { u =>
|
||||
JsString(u.uci)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,11 +60,14 @@ final class Markdown(
|
|||
private def mentionsToLinks(markdown: Text): Text =
|
||||
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 =
|
||||
Chronometer
|
||||
.sync {
|
||||
try {
|
||||
addLinkAttributes(renderer.render(parser.parse(mentionsToLinks(text))))
|
||||
addLinkAttributes(renderer.render(parser.parse(mentionsToLinks(preventStackOverflow(text)))))
|
||||
} catch {
|
||||
case e: StackOverflowError =>
|
||||
logger.branch(key).error("StackOverflowError", e)
|
||||
|
|
|
@ -38,10 +38,17 @@ abstract class Random {
|
|||
vec.nonEmpty ?? {
|
||||
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 {
|
||||
override def current = java.util.concurrent.ThreadLocalRandom.current
|
||||
|
||||
def nextLong(n: Long): Long = current.nextLong(n)
|
||||
}
|
||||
|
||||
object SecureRandom extends Random {
|
||||
|
|
|
@ -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 ifNone(n: => Unit): Unit = if (self.isEmpty) n
|
||||
|
||||
def has(a: A) = self contains a
|
||||
}
|
||||
|
||||
|
|
|
@ -646,9 +646,15 @@ object mon {
|
|||
def request(hit: Boolean) = counter("fishnet.http.acquire").withTag("hit", hit)
|
||||
}
|
||||
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(
|
||||
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 {
|
||||
|
@ -727,6 +733,7 @@ object mon {
|
|||
)
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
@ -63,6 +63,10 @@ trait dsl {
|
|||
def $nor(expressions: Bdoc*): Bdoc = {
|
||||
$doc("$nor" -> expressions)
|
||||
}
|
||||
|
||||
def $not(expression: Bdoc): Bdoc = {
|
||||
$doc("$not" -> expression)
|
||||
}
|
||||
// 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 =>
|
||||
def $exists(v: Boolean): SimpleExpression[Bdoc] = {
|
||||
SimpleExpression(field, $doc("$exists" -> v))
|
||||
|
@ -381,7 +378,6 @@ trait dsl {
|
|||
with ComparisonOperators
|
||||
with ElementOperators
|
||||
with EvaluationOperators
|
||||
with LogicalOperators
|
||||
with ArrayOperators
|
||||
|
||||
implicit def toBSONDocument[V: BSONWriter](expression: Expression[V]): Bdoc =
|
||||
|
|
|
@ -16,6 +16,7 @@ final class Env(
|
|||
userRepo: lila.user.UserRepo,
|
||||
yoloDb: lila.db.AsyncDb @@ lila.db.YoloDb,
|
||||
cacheApi: lila.memo.CacheApi,
|
||||
settingStore: lila.memo.SettingStore.Builder,
|
||||
scheduler: akka.actor.Scheduler
|
||||
)(implicit
|
||||
ec: scala.concurrent.ExecutionContext,
|
||||
|
@ -27,6 +28,12 @@ final class Env(
|
|||
|
||||
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]
|
||||
|
||||
lazy val api: EvalCacheApi = wire[EvalCacheApi]
|
||||
|
|
|
@ -9,13 +9,16 @@ import scala.concurrent.duration._
|
|||
import lila.db.AsyncCollFailingSilently
|
||||
import lila.db.dsl._
|
||||
import lila.memo.CacheApi._
|
||||
import lila.memo.SettingStore
|
||||
import lila.socket.Socket
|
||||
import lila.user.User
|
||||
|
||||
final class EvalCacheApi(
|
||||
coll: AsyncCollFailingSilently,
|
||||
truster: EvalCacheTruster,
|
||||
upgrade: EvalCacheUpgrade,
|
||||
cacheApi: lila.memo.CacheApi
|
||||
cacheApi: lila.memo.CacheApi,
|
||||
setting: SettingStore[Boolean]
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
import EvalCacheEntry._
|
||||
|
@ -36,7 +39,7 @@ final class EvalCacheApi(
|
|||
def put(trustedUser: TrustedUser, candidate: Input.Candidate, sri: Socket.Sri): Funit =
|
||||
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]] =
|
||||
getEval(
|
||||
|
|
|
@ -9,38 +9,44 @@ import lila.socket.Socket
|
|||
import lila.memo.ExpireCallbackMemo
|
||||
|
||||
import scala.collection.mutable
|
||||
import lila.memo.SettingStore
|
||||
|
||||
/* Upgrades the user's eval when a better one becomes available,
|
||||
* by remembering the last evalGet of each socket member,
|
||||
* 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,
|
||||
mode: play.api.Mode
|
||||
) {
|
||||
import EvalCacheUpgrade._
|
||||
|
||||
private val members = mutable.AnyRefMap.empty[SriString, WatchingMember]
|
||||
private val evals = mutable.AnyRefMap.empty[SetupId, Set[SriString]]
|
||||
private val expirableSris = new ExpireCallbackMemo(20 minutes, sri => unregister(Socket.Sri(sri)))
|
||||
private val evals = mutable.AnyRefMap.empty[SetupId, EvalState]
|
||||
private val expirableSris = new ExpireCallbackMemo(10 minutes, sri => unregister(Socket.Sri(sri)))
|
||||
|
||||
private val upgradeMon = lila.mon.evalCache.upgrade
|
||||
|
||||
def register(sri: Socket.Sri, variant: Variant, fen: FEN, multiPv: Int, path: String)(push: Push): Unit = {
|
||||
members get sri.value foreach { wm =>
|
||||
unregisterEval(wm.setupId, sri)
|
||||
def register(sri: Socket.Sri, variant: Variant, fen: FEN, multiPv: Int, path: String)(push: Push): Unit =
|
||||
if (setting.get()) {
|
||||
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 =>
|
||||
evals get makeSetupId(input.id.variant, input.fen, multiPv)
|
||||
} foreach { sris =>
|
||||
val wms = sris.withFilter(sri.value !=) flatMap members.get
|
||||
val setupId = makeSetupId(input.id.variant, input.fen, multiPv)
|
||||
evals get setupId map (setupId -> _)
|
||||
} 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) {
|
||||
val json = JsonHandlers.writeEval(input.eval, input.fen)
|
||||
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 =
|
||||
evals get setupId foreach { sris =>
|
||||
val newSris = sris - sri.value
|
||||
evals get setupId foreach { eval =>
|
||||
val newSris = eval.sris - sri.value
|
||||
if (newSris.isEmpty) evals -= setupId
|
||||
else evals += (setupId -> newSris)
|
||||
else evals += (setupId -> eval.copy(sris = newSris))
|
||||
}
|
||||
|
||||
scheduler.scheduleWithFixedDelay(1 minute, 1 minute) { () =>
|
||||
|
@ -78,6 +84,10 @@ private object EvalCacheUpgrade {
|
|||
private type SetupId = String
|
||||
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 =
|
||||
s"${variant.id}${EvalCacheEntry.SmallFen.make(variant, fen).value}^$multiPv"
|
||||
|
||||
|
|
|
@ -71,21 +71,22 @@ final private class ExplorerIndexer(
|
|||
case Correspondence | Classical => 1.00f
|
||||
|
||||
case Rapid if rating >= 2200 => 1.00f
|
||||
case Rapid if rating >= 2000 => 0.50f
|
||||
case Rapid if rating >= 1800 => 0.28f
|
||||
case Rapid if rating >= 1600 => 0.24f
|
||||
case Rapid if rating >= 2000 => 0.83f
|
||||
case Rapid if rating >= 1800 => 0.46f
|
||||
case Rapid if rating >= 1600 => 0.39f
|
||||
case Rapid => 0.02f
|
||||
|
||||
case Blitz if rating >= 2500 => 1.00f
|
||||
case Blitz if rating >= 2200 => 0.24f
|
||||
case Blitz if rating >= 2000 => 0.11f
|
||||
case Blitz if rating >= 1600 => 0.08f
|
||||
case Blitz if rating >= 2200 => 0.38f
|
||||
case Blitz if rating >= 2000 => 0.18f
|
||||
case Blitz if rating >= 1600 => 0.13f
|
||||
case Blitz => 0.02f
|
||||
|
||||
case Bullet if rating >= 2500 => 1.00f
|
||||
case Bullet if rating >= 2200 => 0.30f
|
||||
case Bullet if rating >= 2000 => 0.17f
|
||||
case Bullet if rating >= 1600 => 0.11f
|
||||
case Bullet if rating >= 2200 => 0.48f
|
||||
case Bullet if rating >= 2000 => 0.27f
|
||||
case Bullet if rating >= 1800 => 0.19f
|
||||
case Bullet if rating >= 1600 => 0.18f
|
||||
case Bullet => 0.02f
|
||||
|
||||
case UltraBullet => 1.00f
|
||||
|
|
|
@ -18,7 +18,7 @@ final class Analyser(
|
|||
system: akka.actor.ActorSystem
|
||||
) {
|
||||
|
||||
val maxPlies = 200
|
||||
val maxPlies = 300
|
||||
|
||||
private val workQueue =
|
||||
new lila.hub.AsyncActorSequencer(maxSize = 256, timeout = 5 seconds, "fishnetAnalyser")
|
||||
|
|
|
@ -2,17 +2,19 @@ package lila.fishnet
|
|||
|
||||
import chess.format.Forsyth
|
||||
import chess.format.Uci
|
||||
import chess.Speed
|
||||
import chess.{ Color, Speed }
|
||||
import com.softwaremill.tagging._
|
||||
import play.api.libs.json._
|
||||
import play.api.libs.ws.JsonBodyReadables._
|
||||
import play.api.libs.ws.StandaloneWSClient
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.Json.uciReader
|
||||
import lila.common.Json._
|
||||
import lila.common.ThreadLocalRandom
|
||||
import lila.game.Game
|
||||
import lila.memo.SettingStore
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
final private class FishnetOpeningBook(
|
||||
ws: StandaloneWSClient,
|
||||
|
@ -21,32 +23,42 @@ final private class FishnetOpeningBook(
|
|||
|
||||
import FishnetOpeningBook._
|
||||
|
||||
def apply(game: Game, level: Int): Fu[Option[Uci]] = (game.turns < depth.get()) ?? {
|
||||
ws.url(endpoint)
|
||||
.withQueryStringParameters(
|
||||
"variant" -> game.variant.key,
|
||||
"fen" -> Forsyth.>>(game.chess).value,
|
||||
"topGames" -> "0",
|
||||
"recentGames" -> "0",
|
||||
"ratings" -> (~levelRatings.get(level)).mkString(","),
|
||||
"speeds" -> (~openingSpeeds.get(game.speed)).map(_.key).mkString(",")
|
||||
)
|
||||
.get()
|
||||
.map {
|
||||
case res if res.status != 200 =>
|
||||
logger.warn(s"opening book ${game.id} ${level} ${res.status} ${res.body}")
|
||||
none
|
||||
case res =>
|
||||
for {
|
||||
data <- res.body[JsValue].validate[Response](responseReader).asOpt
|
||||
move <- data.randomPonderedMove
|
||||
} yield move.uci
|
||||
}
|
||||
.monValue(uci =>
|
||||
_.fishnet
|
||||
.openingBook(level = level, variant = game.variant.key, ply = game.turns, hit = uci.isDefined)
|
||||
)
|
||||
}
|
||||
private val outOfBook = new lila.memo.ExpireSetMemo(10 minutes)
|
||||
|
||||
def apply(game: Game, level: Int): Fu[Option[Uci]] =
|
||||
(game.turns < depth.get() && !outOfBook.get(game.id)) ?? {
|
||||
ws.url(endpoint)
|
||||
.withQueryStringParameters(
|
||||
"variant" -> game.variant.key,
|
||||
"fen" -> Forsyth.>>(game.chess).value,
|
||||
"topGames" -> "0",
|
||||
"recentGames" -> "0",
|
||||
"ratings" -> (~levelRatings.get(level)).mkString(","),
|
||||
"speeds" -> (~openingSpeeds.get(game.speed)).map(_.key).mkString(",")
|
||||
)
|
||||
.get()
|
||||
.map {
|
||||
case res if res.status != 200 =>
|
||||
logger.warn(s"opening book ${game.id} ${level} ${res.status} ${res.body}")
|
||||
none
|
||||
case res =>
|
||||
for {
|
||||
data <- res.body[JsValue].validate[Response](responseReader).asOpt
|
||||
_ = if (data.moves.isEmpty) outOfBook.put(game.id)
|
||||
move <- data randomPonderedMove (game.turnColor, level)
|
||||
} 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 {
|
||||
|
@ -54,20 +66,26 @@ object FishnetOpeningBook {
|
|||
trait Depth
|
||||
|
||||
case class Response(moves: List[Move]) {
|
||||
def randomPonderedMove: Option[Move] = {
|
||||
val sum = moves.map(_.nb).sum
|
||||
val rng = ThreadLocalRandom nextInt sum
|
||||
|
||||
def randomPonderedMove(turn: Color, level: Int): Option[Move] = {
|
||||
val sum = moves.map(_.score(turn, level)).sum
|
||||
val novelty = 5L * 14 // score of 5 winning games
|
||||
val rng = ThreadLocalRandom.nextLong(sum + novelty)
|
||||
moves
|
||||
.foldLeft((none[Move], 0)) { case ((found, it), next) =>
|
||||
val nextIt = it + next.nb
|
||||
.foldLeft((none[Move], 0L)) { case ((found, it), next) =>
|
||||
val nextIt = it + next.score(turn, level)
|
||||
(found orElse (nextIt > rng).option(next), nextIt)
|
||||
}
|
||||
._1
|
||||
}
|
||||
}
|
||||
|
||||
case class Move(uci: Uci, white: Int, draws: Int, black: Int) {
|
||||
def nb = white + draws + black
|
||||
case class Move(uci: Uci, white: Long, draws: Long, black: Long) {
|
||||
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]
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
package lila.fishnet
|
||||
|
||||
import chess.format.Uci
|
||||
import chess.{ Black, Clock, White }
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import chess.{ Black, Clock, White }
|
||||
|
||||
import lila.common.{ Future, ThreadLocalRandom }
|
||||
import lila.common.{ Bus, Future, ThreadLocalRandom }
|
||||
import lila.game.{ Game, GameRepo, UciMemo }
|
||||
import lila.common.Bus
|
||||
import lila.hub.actorApi.map.Tell
|
||||
import lila.hub.actorApi.round.FishnetPlay
|
||||
import chess.format.Uci
|
||||
|
||||
final class FishnetPlayer(
|
||||
redis: FishnetRedis,
|
||||
|
@ -28,7 +26,7 @@ final class FishnetPlayer(
|
|||
openingBook(game, level) flatMap {
|
||||
case Some(move) =>
|
||||
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
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ final class Cached(
|
|||
|
||||
private val lastPlayedPlayingIdCache: LoadingCache[User.ID, Fu[Option[Game.ID]]] =
|
||||
CacheApi.scaffeineNoScheduler
|
||||
.expireAfterWrite(5 seconds)
|
||||
.expireAfterWrite(11 seconds)
|
||||
.build(gameRepo.lastPlayedPlayingId)
|
||||
|
||||
lila.common.Bus.subscribeFun("startGame") { case lila.game.actorApi.StartGame(game) =>
|
||||
|
|
|
@ -47,21 +47,16 @@ final private class Captcher(gameRepo: GameRepo)(implicit ec: scala.concurrent.E
|
|||
private val capacity = 256
|
||||
private var challenges = NonEmptyList.one(Captcha.default)
|
||||
|
||||
private def add(c: Captcha): Unit = {
|
||||
find(c.gameId) ifNone {
|
||||
private def add(c: Captcha): Unit =
|
||||
if (find(c.gameId).isEmpty) {
|
||||
challenges = NonEmptyList(c, challenges.toList take capacity)
|
||||
}
|
||||
}
|
||||
|
||||
private def find(id: String): Option[Captcha] =
|
||||
challenges.find(_.gameId == id)
|
||||
|
||||
private def createFromDb: Fu[Option[Captcha]] =
|
||||
findCheckmateInDb(10) flatMap {
|
||||
_.fold(findCheckmateInDb(1))(g => fuccess(g.some))
|
||||
} flatMap {
|
||||
_ ?? fromGame
|
||||
}
|
||||
findCheckmateInDb(10) orElse findCheckmateInDb(1) flatMap { _ ?? fromGame }
|
||||
|
||||
private def findCheckmateInDb(distribution: Int): Fu[Option[Game]] =
|
||||
gameRepo findRandomStandardCheckmate distribution
|
||||
|
|
|
@ -17,6 +17,7 @@ import chess.{
|
|||
import JsonView._
|
||||
import lila.chat.{ PlayerLine, UserLine }
|
||||
import lila.common.ApiVersion
|
||||
import lila.common.Json._
|
||||
|
||||
sealed trait Event {
|
||||
def typ: String
|
||||
|
|
|
@ -555,7 +555,7 @@ case class Game(
|
|||
def abandoned =
|
||||
(status <= Status.Started) && {
|
||||
movedAt isBefore {
|
||||
if (hasAi && !hasCorrespondenceClock) Game.aiAbandonedDate
|
||||
if (hasAi && hasClock) Game.aiAbandonedDate
|
||||
else Game.abandonedDate
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package lila.game
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import chess.format.{ FEN, Forsyth }
|
||||
import chess.{ Color, Status }
|
||||
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 Player.holdAlertBSONHandler
|
||||
|
||||
val fixedColorLobbyCache = new lila.memo.ExpireSetMemo(2 hours)
|
||||
|
||||
def game(gameId: ID): Fu[Option[Game]] = coll.byId[Game](gameId)
|
||||
def gameFromSecondary(gameId: ID): Fu[Option[Game]] = coll.secondaryPreferred.byId[Game](gameId)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import play.api.libs.json._
|
|||
import chess.format.{ FEN, Forsyth }
|
||||
import chess.variant.Crazyhouse
|
||||
import chess.{ Clock, Color }
|
||||
import lila.common.Json.jodaWrites
|
||||
import lila.common.Json._
|
||||
|
||||
final class JsonView(rematches: Rematches) {
|
||||
|
||||
|
@ -143,12 +143,4 @@ object JsonView {
|
|||
implicit val sourceWriter: Writes[Source] = Writes { s =>
|
||||
JsString(s.name)
|
||||
}
|
||||
|
||||
implicit val colorWrites: Writes[Color] = Writes { c =>
|
||||
JsString(c.name)
|
||||
}
|
||||
|
||||
implicit val fenWrites: Writes[FEN] = Writes { f =>
|
||||
JsString(f.value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,10 +57,12 @@ final class PgnDump(
|
|||
private def gameLightUsers(game: Game): Fu[(Option[LightUser], Option[LightUser])] =
|
||||
(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]) =
|
||||
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] =
|
||||
Set(chess.variant.Chess960, chess.variant.FromPosition, chess.variant.Horde, chess.variant.RacingKings)
|
||||
|
|
|
@ -60,8 +60,8 @@ case class Player(
|
|||
|
||||
def nameSplit: Option[(String, Option[Int])] =
|
||||
name map {
|
||||
case Player.nameSplitRegex(n, r) => n -> r.toIntOption
|
||||
case n => n -> none
|
||||
case Player.nameSplitRegex(n, r) => n.trim -> r.toIntOption
|
||||
case n => n -> none
|
||||
}
|
||||
|
||||
def before(other: Player) =
|
||||
|
|
|
@ -9,20 +9,18 @@ final class Share(
|
|||
relationApi: lila.relation.RelationApi
|
||||
)(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] =
|
||||
if (to ?? Granter(_.SeeInsight)) fuTrue
|
||||
else
|
||||
prefApi.getPrefById(insighted.id) flatMap { pref =>
|
||||
pref.insightShare match {
|
||||
case _ if to.contains(insighted) => fuTrue
|
||||
case Pref.InsightShare.EVERYBODY => fuTrue
|
||||
case Pref.InsightShare.FRIENDS =>
|
||||
to ?? { t =>
|
||||
relationApi.fetchAreFriends(insighted.id, t.id)
|
||||
}
|
||||
case Pref.InsightShare.NOBODY => fuFalse
|
||||
}
|
||||
getPrefId(insighted) flatMap {
|
||||
case _ if to.contains(insighted) => fuTrue
|
||||
case Pref.InsightShare.EVERYBODY => fuTrue
|
||||
case Pref.InsightShare.FRIENDS =>
|
||||
to ?? { t =>
|
||||
relationApi.fetchAreFriends(insighted.id, t.id)
|
||||
}
|
||||
case Pref.InsightShare.NOBODY => fuFalse
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ final class IrcApi(
|
|||
)
|
||||
case Some(note) =>
|
||||
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" +
|
||||
markdown.linkifyUsers(note.text take 2000)
|
||||
)
|
||||
|
@ -70,7 +70,7 @@ final class IrcApi(
|
|||
def commlog(mod: Holder, user: User, reportBy: Option[User.ID]): Funit =
|
||||
zulip(_.mod.adminLog, "private comms checks")({
|
||||
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 =>
|
||||
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 =
|
||||
// logMod(mod.id, "footprints", s"Ban print $print of ${userIds} users: ${userIds map linkifyUsers}")
|
||||
|
||||
def chatPanic(mod: Holder, v: Boolean): Funit =
|
||||
zulip(_.mod.log, "chat panic")(
|
||||
def chatPanic(mod: Holder, v: Boolean): Funit = {
|
||||
val msg =
|
||||
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 =
|
||||
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 publishEvent(event: Event): Funit = event match {
|
||||
|
@ -208,7 +212,7 @@ object IrcApi {
|
|||
private object markdown {
|
||||
def link(url: String, name: String) = s"[$name]($url)"
|
||||
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¬es", name)
|
||||
def userLink(user: User): String = userLink(user.username)
|
||||
def modLink(name: String): String = lichessLink(s"/@/$name", name)
|
||||
def modLink(user: User): String = modLink(user.username)
|
||||
|
|
|
@ -80,6 +80,7 @@ private object ZulipClient {
|
|||
val log = "mod-log"
|
||||
val adminLog = "mod-admin-log"
|
||||
val adminGeneral = "mod-admin-general"
|
||||
val commsPublic = "mod-comms-public"
|
||||
val commsPrivate = "mod-comms-private"
|
||||
val hunterCheat = "mod-hunter-cheat"
|
||||
val adminAppeal = "mod-admin-appeal"
|
||||
|
|
|
@ -4,6 +4,7 @@ import lila.game.{ Pov, Source }
|
|||
|
||||
final private class AbortListener(
|
||||
userRepo: lila.user.UserRepo,
|
||||
gameRepo: lila.game.GameRepo,
|
||||
seekApi: SeekApi,
|
||||
lobbyActor: LobbySyncActor
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
@ -14,7 +15,10 @@ final private class AbortListener(
|
|||
lobbyActor.registerAbortedGame(pov.game)
|
||||
|
||||
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) =>
|
||||
userRepo.incColor(u1, -1)
|
||||
userRepo.incColor(u2, 1)
|
||||
|
|
|
@ -36,6 +36,7 @@ final private class Biter(
|
|||
_ <- gameRepo insertDenormalized game
|
||||
} yield {
|
||||
lila.mon.lobby.hook.join.increment()
|
||||
rememberIfFixedColor(hook.realColor, game)
|
||||
JoinHook(sri, hook, game, creatorColor)
|
||||
}
|
||||
|
||||
|
@ -50,7 +51,14 @@ final private class Biter(
|
|||
blackUser = creatorColor.fold(user.some, owner.some)
|
||||
).withUniqueId
|
||||
_ <- 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(
|
||||
creatorUser: Option[User],
|
||||
|
|
|
@ -51,11 +51,10 @@ final class ModQueueStats(
|
|||
data <- doc.getAsOpt[List[Bdoc]]("data")
|
||||
} yield date -> {
|
||||
for {
|
||||
entry <- data
|
||||
nb <- entry.int("nb")
|
||||
roomStr <- entry.string("room")
|
||||
room <- Room.byKey get roomStr
|
||||
score <- entry.int("score")
|
||||
entry <- data
|
||||
nb <- entry.int("nb")
|
||||
room <- entry.string("room")
|
||||
score <- entry.int("score")
|
||||
} yield (room, score, nb)
|
||||
}
|
||||
}
|
||||
|
@ -66,20 +65,25 @@ final class ModQueueStats(
|
|||
"common" -> Json.obj(
|
||||
"xaxis" -> days.map(_._1.getMillis)
|
||||
),
|
||||
"rooms" -> Room.all.map { room =>
|
||||
Json.obj(
|
||||
"name" -> room.name,
|
||||
"series" -> scores.collect {
|
||||
case score if score > 20 || room == Room.Boost =>
|
||||
Json.obj(
|
||||
"name" -> score,
|
||||
"data" -> days.map(~_._2.collectFirst {
|
||||
case (r, s, nb) if r == room && s == score => nb
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
"rooms" -> Room.all
|
||||
.map { room =>
|
||||
room.key -> room.name
|
||||
}
|
||||
.appended { ("appeal", "Appeal") }
|
||||
.map { case (roomKey, roomName) =>
|
||||
Json.obj(
|
||||
"name" -> roomName,
|
||||
"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
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 =>
|
||||
Some(IrcApi.ModDomain.Hunt)
|
||||
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)
|
||||
case _ => Some(IrcApi.ModDomain.Other)
|
||||
}
|
||||
|
|
|
@ -50,11 +50,15 @@ case class ModPresets(value: List[ModPreset]) {
|
|||
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 {
|
||||
|
||||
val groups = List("PM", "appeal")
|
||||
val groups = List("PM", "appeal")
|
||||
val nameClosePresetName = "Account closure for name in 48h"
|
||||
|
||||
private[mod] object setting {
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import org.joda.time.format.ISODateTimeFormat
|
|||
import play.api.i18n.Lang
|
||||
import play.api.libs.json._
|
||||
|
||||
import lila.common.Json.{ jodaWrites => _, _ }
|
||||
import lila.common.LightUser
|
||||
import lila.rating.{ Glicko, Perf, PerfType }
|
||||
import lila.user.User
|
||||
|
|
|
@ -282,25 +282,24 @@ final class PlanApi(
|
|||
.void >> setDbUserPlanOnCharge(user, levelUp = false)
|
||||
|
||||
def gift(from: User, to: User, money: Money): Funit =
|
||||
!to.isPatron ?? {
|
||||
for {
|
||||
isLifetime <- pricingApi isLifetime money
|
||||
_ <- patronColl.update
|
||||
.one(
|
||||
$id(to.id),
|
||||
$set(
|
||||
"lastLevelUp" -> DateTime.now,
|
||||
"lifetime" -> isLifetime,
|
||||
"free" -> Patron.Free(DateTime.now, by = from.id.some),
|
||||
"expiresAt" -> (!isLifetime option DateTime.now.plusMonths(1))
|
||||
),
|
||||
upsert = true
|
||||
)
|
||||
newTo = to.mapPlan(_.incMonths)
|
||||
_ <- setDbUserPlan(newTo)
|
||||
} yield {
|
||||
notifier.onGift(from, newTo, isLifetime)
|
||||
}
|
||||
for {
|
||||
toPatronOpt <- userPatron(to)
|
||||
isLifetime <- fuccess(toPatronOpt.exists(_.isLifetime)) >>| (pricingApi isLifetime money)
|
||||
_ <- patronColl.update
|
||||
.one(
|
||||
$id(to.id),
|
||||
$set(
|
||||
"lastLevelUp" -> DateTime.now,
|
||||
"lifetime" -> isLifetime,
|
||||
"free" -> Patron.Free(DateTime.now, by = from.id.some),
|
||||
"expiresAt" -> (!isLifetime option DateTime.now.plusMonths(1))
|
||||
),
|
||||
upsert = true
|
||||
)
|
||||
newTo = to.mapPlan(p => if (toPatronOpt.exists(_.canLevelUp)) p.incMonths else p.enable)
|
||||
_ <- setDbUserPlan(newTo)
|
||||
} yield {
|
||||
notifier.onGift(from, newTo, isLifetime)
|
||||
}
|
||||
|
||||
def recentGiftFrom(from: User): Fu[Option[Patron]] =
|
||||
|
|
|
@ -132,7 +132,7 @@ final class PlaybanApi(
|
|||
Status.Resign.is(status)
|
||||
}
|
||||
.map { c =>
|
||||
(c.estimateTotalSeconds / 10) atLeast 15 atMost (3 * 60)
|
||||
(c.estimateTotalSeconds / 10) atLeast 30 atMost (3 * 60)
|
||||
}
|
||||
.exists(_ < nowSeconds - game.movedAt.getSeconds)
|
||||
.option {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package lila.pref
|
||||
|
||||
import org.joda.time.DateTime
|
||||
|
||||
import lila.user.User
|
||||
|
||||
case class Pref(
|
||||
_id: String, // user id
|
||||
bg: Int,
|
||||
|
@ -413,7 +417,14 @@ object Pref {
|
|||
object Zen 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(
|
||||
_id = "",
|
||||
|
|
|
@ -37,13 +37,16 @@ final class PrefApi(
|
|||
.void >>- { cache invalidate user.id }
|
||||
} >>- { cache invalidate user.id }
|
||||
|
||||
def getPrefById(id: User.ID): Fu[Pref] = cache get id dmap (_ getOrElse Pref.create(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 getPrefById(id: User.ID): Fu[Option[Pref]] = cache get id
|
||||
|
||||
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] = getPref(userId) dmap pref
|
||||
def getPref(user: User): Fu[Pref] = cache get user.id dmap {
|
||||
_ 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] =
|
||||
getPref(user) dmap RequestPref.queryParamOverride(req)
|
||||
|
@ -81,9 +84,6 @@ final class PrefApi(
|
|||
def setPref(user: User, change: Pref => Pref): Funit =
|
||||
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 =
|
||||
getPref(user) map { _.set(name, value) } orFail
|
||||
s"Bad pref ${user.id} $name -> $value" flatMap setPref
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
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
|
||||
|
||||
def cssClass = name
|
||||
|
||||
def light = colors._1
|
||||
def dark = colors._2
|
||||
}
|
||||
|
||||
sealed trait ThemeObject {
|
||||
|
@ -27,49 +24,33 @@ sealed trait 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(
|
||||
"blue",
|
||||
"blue2",
|
||||
"blue3",
|
||||
"blue-marble",
|
||||
"canvas",
|
||||
"wood",
|
||||
"wood2",
|
||||
"wood3",
|
||||
"wood4",
|
||||
"maple",
|
||||
"maple2",
|
||||
"brown",
|
||||
"leather",
|
||||
"green",
|
||||
"marble",
|
||||
"green-plastic",
|
||||
"grey",
|
||||
"metal",
|
||||
"olive",
|
||||
"newspaper",
|
||||
"purple",
|
||||
"purple-diag",
|
||||
"pink",
|
||||
"ic",
|
||||
"horsey"
|
||||
) map { name =>
|
||||
new Theme(name, colors.getOrElse(name, defaultHexColors))
|
||||
}
|
||||
new Theme("blue", "svg/blue.svg"),
|
||||
new Theme("blue2", "blue2.jpg"),
|
||||
new Theme("blue3", "blue3.jpg"),
|
||||
new Theme("blue-marble", "blue-marble.jpg"),
|
||||
new Theme("canvas", "canvas2.jpg"),
|
||||
new Theme("wood", "wood.jpg"),
|
||||
new Theme("wood2", "wood2.jpg"),
|
||||
new Theme("wood3", "wood3.jpg"),
|
||||
new Theme("wood4", "wood4.jpg"),
|
||||
new Theme("maple", "maple.jpg"),
|
||||
new Theme("maple2", "maple2.jpg"),
|
||||
new Theme("brown", "svg/brown.svg"),
|
||||
new Theme("leather", "leather.jpg"),
|
||||
new Theme("green", "svg/green.svg"),
|
||||
new Theme("marble", "marble.jpg"),
|
||||
new Theme("green-plastic", "green-plastic.png"),
|
||||
new Theme("grey", "grey.jpg"),
|
||||
new Theme("metal", "metal.jpg"),
|
||||
new Theme("olive", "olive.jpg"),
|
||||
new Theme("newspaper", "newspaper.png"),
|
||||
new Theme("purple", "svg/purple.svg"),
|
||||
new Theme("purple-diag", "purple-diag.png"),
|
||||
new Theme("pink", "pink-pyramid.png"),
|
||||
new Theme("ic", "svg/ic.svg"),
|
||||
new Theme("horsey", "horsey.jpg")
|
||||
)
|
||||
|
||||
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 {
|
||||
|
||||
val all = List(
|
||||
"Black-White-Aluminium",
|
||||
"Brushed-Aluminium",
|
||||
"China-Blue",
|
||||
"China-Green",
|
||||
"China-Grey",
|
||||
"China-Scarlet",
|
||||
"China-Yellow",
|
||||
"Classic-Blue",
|
||||
"Gold-Silver",
|
||||
"Green-Glass",
|
||||
"Light-Wood",
|
||||
"Power-Coated",
|
||||
"Purple-Black",
|
||||
"Rosewood",
|
||||
"Wood-Glass",
|
||||
"Marble",
|
||||
"Wax",
|
||||
"Jade",
|
||||
"Woodi"
|
||||
) map { name =>
|
||||
new Theme(name, Theme.defaultHexColors)
|
||||
}
|
||||
new Theme("Black-White-Aluminium", "Black-White-Aluminium.png"),
|
||||
new Theme("Brushed-Aluminium", "Brushed-Aluminium.png"),
|
||||
new Theme("China-Blue", "China-Blue.png"),
|
||||
new Theme("China-Green", "China-Green.png"),
|
||||
new Theme("China-Grey", "China-Grey.png"),
|
||||
new Theme("China-Scarlet", "China-Scarlet.png"),
|
||||
new Theme("China-Yellow", "China-Yellow.png"),
|
||||
new Theme("Classic-Blue", "Classic-Blue.png"),
|
||||
new Theme("Gold-Silver", "Gold-Silver.png"),
|
||||
new Theme("Green-Glass", "Green-Glass.png"),
|
||||
new Theme("Light-Wood", "Light-Wood.png"),
|
||||
new Theme("Power-Coated", "Power-Coated.png"),
|
||||
new Theme("Purple-Black", "Purple-Black.png"),
|
||||
new Theme("Rosewood", "Rosewood.png"),
|
||||
new Theme("Wood-Glass", "Wood-Glass.png"),
|
||||
new Theme("Marble", "Marble.png"),
|
||||
new Theme("Wax", "Wax.png"),
|
||||
new Theme("Jade", "Jade.png"),
|
||||
new Theme("Woodi", "Woodi.png")
|
||||
)
|
||||
|
||||
lazy val default = allByName get "Woodi" err "Can't find default theme D:"
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import scala.concurrent.duration._
|
|||
|
||||
import lila.db.dsl._
|
||||
import lila.memo.CacheApi._
|
||||
import lila.common.ThreadLocalRandom
|
||||
|
||||
final private[puzzle] class DailyPuzzle(
|
||||
colls: PuzzleColls,
|
||||
|
@ -26,7 +27,7 @@ final private[puzzle] class DailyPuzzle(
|
|||
def get: Fu[Option[DailyPuzzle.WithHtml]] = cache.getUnit
|
||||
|
||||
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)
|
||||
none
|
||||
} flatMap { _ ?? makeDaily }
|
||||
|
@ -48,6 +49,18 @@ final private[puzzle] class DailyPuzzle(
|
|||
.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]] =
|
||||
colls
|
||||
.path {
|
||||
|
|
|
@ -29,6 +29,8 @@ case class Puzzle(
|
|||
} err s"Can't apply puzzle $id first move"
|
||||
|
||||
def color = fen.color.fold[chess.Color](chess.White)(!_)
|
||||
|
||||
def hasTheme(theme: PuzzleTheme) = themes(theme.key)
|
||||
}
|
||||
|
||||
object Puzzle {
|
||||
|
|
|
@ -155,7 +155,8 @@ final private class RelaySync(
|
|||
(tour.official && chapter.root.mainline.sizeIs > 10) ?? studyApi.analysisRequest(
|
||||
studyId = study.id,
|
||||
chapterId = chapter.id,
|
||||
userId = study.ownerId
|
||||
userId = study.ownerId,
|
||||
unlimited = true
|
||||
)
|
||||
} >>- {
|
||||
multiboard.invalidate(study.id)
|
||||
|
|
|
@ -14,7 +14,10 @@ case class SyncLog(events: Vector[SyncLog.Event]) extends AnyVal {
|
|||
|
||||
def add(event: SyncLog.Event) =
|
||||
copy(
|
||||
events = events.take(SyncLog.historySize - 1) :+ event
|
||||
events = {
|
||||
if (events.sizeIs > SyncLog.historySize) events drop 1
|
||||
else events
|
||||
} :+ event
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -49,10 +49,10 @@ object Reason {
|
|||
def isGrantedFor(mod: Holder)(reason: Reason) = {
|
||||
import lila.security.Granter
|
||||
reason match {
|
||||
case Cheat => Granter.is(_.MarkEngine)(mod)
|
||||
case AltPrint | CheatPrint => Granter.is(_.Admin)(mod)
|
||||
case Comm => Granter.is(_.Shadowban)(mod)
|
||||
case Boost | Playbans | Other => Granter.is(_.MarkBooster)(mod)
|
||||
case Cheat => Granter.is(_.MarkEngine)(mod)
|
||||
case AltPrint | CheatPrint | Playbans | Other => Granter.is(_.Admin)(mod)
|
||||
case Comm => Granter.is(_.Shadowban)(mod)
|
||||
case Boost => Granter.is(_.MarkBooster)(mod)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,9 +22,9 @@ final private[round] class Drawer(
|
|||
import Pref.PrefZero
|
||||
if (game.playerHasOfferedDrawRecently(pov.color)) fuccess(pov.some)
|
||||
else
|
||||
pov.player.userId ?? prefApi.getPref map { pref =>
|
||||
pref.autoThreefold == Pref.AutoThreefold.ALWAYS || {
|
||||
pref.autoThreefold == Pref.AutoThreefold.TIME &&
|
||||
pov.player.userId ?? { uid => prefApi.getPref(uid, _.autoThreefold) } map { autoThreefold =>
|
||||
autoThreefold == Pref.AutoThreefold.ALWAYS || {
|
||||
autoThreefold == Pref.AutoThreefold.TIME &&
|
||||
game.clock ?? { _.remainingTime(pov.color) < Centis.ofSeconds(30) }
|
||||
} || pov.player.userId.exists(isBotSync)
|
||||
} map (_ option pov)
|
||||
|
@ -37,7 +37,7 @@ final private[round] class Drawer(
|
|||
case pov if pov.game.history.threefoldRepetition =>
|
||||
finisher.other(pov.game, _.Draw, None)
|
||||
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 =>
|
||||
val progress = Progress(g) map { _ offerDraw color }
|
||||
messenger.system(g, color.fold(trans.whiteOffersDraw, trans.blackOffersDraw).txt())
|
||||
|
|
|
@ -48,7 +48,7 @@ final private class Finisher(
|
|||
other(game, _.Aborted, none)
|
||||
|
||||
} 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 {
|
||||
val winner = Some(!game.player.color) ifFalse game.situation.opponentHasInsufficientMaterial
|
||||
apply(game, _.Outoftime, winner) >>-
|
||||
|
@ -62,14 +62,14 @@ final private class Finisher(
|
|||
lila.mon.round.expiration.count.increment()
|
||||
playban.noStart(Pov(game, culprit))
|
||||
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(
|
||||
game: Game,
|
||||
status: Status.type => Status,
|
||||
winner: Option[Color],
|
||||
message: Option[String] = None
|
||||
message: Option[Messenger.SystemMessage] = None
|
||||
)(implicit proxy: GameProxy): Fu[Events] =
|
||||
apply(game, status, winner, message) >>- playban.other(game, status, winner).unit
|
||||
|
||||
|
@ -108,7 +108,7 @@ final private class Finisher(
|
|||
game: Game,
|
||||
makeStatus: Status.type => Status,
|
||||
winnerC: Option[Color],
|
||||
message: Option[String] = None
|
||||
message: Option[Messenger.SystemMessage] = None
|
||||
)(implicit proxy: GameProxy): Fu[Events] = {
|
||||
val status = makeStatus(Status)
|
||||
val prog = game.finish(status, winnerC)
|
||||
|
@ -139,7 +139,7 @@ final private class Finisher(
|
|||
.flatMap { case (whiteO, blackO) =>
|
||||
val finish = FinishGame(g, whiteO, blackO)
|
||||
updateCountAndPerfs(finish) map { ratingDiffs =>
|
||||
message foreach { messenger.system(g, _) }
|
||||
message foreach { messenger(g, _) }
|
||||
gameRepo game g.id foreach { newGame =>
|
||||
newGame foreach proxy.setFinishedGame
|
||||
val newFinish = finish.copy(game = newGame | g)
|
||||
|
|
|
@ -7,6 +7,7 @@ import play.api.libs.json._
|
|||
import scala.math
|
||||
|
||||
import lila.common.ApiVersion
|
||||
import lila.common.Json._
|
||||
import lila.game.JsonView._
|
||||
import lila.game.{ Pov, Game, Player => GamePlayer }
|
||||
import lila.pref.Pref
|
||||
|
|
|
@ -13,6 +13,11 @@ final class Messenger(api: ChatApi) {
|
|||
def volatile(game: Game, message: String): Unit =
|
||||
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) {
|
||||
api.userChat.volatile(watcherId(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(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
|
||||
}
|
||||
|
|
|
@ -56,10 +56,10 @@ final private class Rematcher(
|
|||
}
|
||||
|
||||
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)) {
|
||||
declined put pov.fullId
|
||||
messenger.system(pov.game, trans.rematchOfferDeclined.txt())
|
||||
messenger.volatile(pov.game, trans.rematchOfferDeclined.txt())
|
||||
}
|
||||
offers invalidate pov.game.id
|
||||
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)
|
||||
_ <- gameRepo insertDenormalized nextGame
|
||||
} yield {
|
||||
messenger.system(pov.game, trans.rematchOfferAccepted.txt())
|
||||
messenger.volatile(pov.game, trans.rematchOfferAccepted.txt())
|
||||
onStart(nextGame.id)
|
||||
redirectEvents(nextGame)
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ final private class Rematcher(
|
|||
}
|
||||
|
||||
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 =>
|
||||
Bus.publish(lila.hub.actorApi.round.RematchOffer(pov.gameId), s"rematchFor:$forId")
|
||||
}
|
||||
|
|
|
@ -403,7 +403,7 @@ final private[round] class RoundAsyncActor(
|
|||
case WsBoot =>
|
||||
handle { game =>
|
||||
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)
|
||||
proxy save progress inject progress.events
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ final private class Takebacker(
|
|||
double(game) >>- publishTakeback(pov) dmap (_ -> situation)
|
||||
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 =>
|
||||
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)] =
|
||||
pov match {
|
||||
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 =>
|
||||
g.updatePlayer(color, _.removeTakebackProposition)
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ final private class Takebacker(
|
|||
publishTakebackOffer(progress.game) inject
|
||||
List(Event.TakebackOffers(white = false, black = false)) -> situation.decline
|
||||
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 =>
|
||||
g.updatePlayer(!color, _.removeTakebackProposition)
|
||||
}
|
||||
|
|
|
@ -97,25 +97,31 @@ final class GarbageCollector(
|
|||
|
||||
private def isBadAccount(user: User) = user.lameOrTrollOrAlt
|
||||
|
||||
private def collect(user: User, email: EmailAddress, msg: => String): Funit =
|
||||
justOnce(user.id) ?? {
|
||||
val armed = isArmed()
|
||||
val wait = (30 + ThreadLocalRandom.nextInt(300)).seconds
|
||||
val message =
|
||||
s"Will dispose of @${user.username} in $wait. Email: ${email.value}. $msg${!armed ?? " [SIMULATION]"}"
|
||||
logger.info(message)
|
||||
noteApi.lichessWrite(user, s"Garbage collected because of $msg")
|
||||
irc.garbageCollector(message) >>- {
|
||||
if (armed) {
|
||||
doInitialSb(user)
|
||||
system.scheduler
|
||||
.scheduleOnce(wait) {
|
||||
doCollect(user)
|
||||
}
|
||||
.unit
|
||||
private def collect(user: User, email: EmailAddress, msg: => String): Funit = justOnce(user.id) ?? {
|
||||
hasBeenCollectedBefore(user) flatMap {
|
||||
case true => funit
|
||||
case _ =>
|
||||
val armed = isArmed()
|
||||
val wait = (30 + ThreadLocalRandom.nextInt(300)).seconds
|
||||
val message =
|
||||
s"Will dispose of @${user.username} in $wait. Email: ${email.value}. $msg${!armed ?? " [SIMULATION]"}"
|
||||
logger.info(message)
|
||||
noteApi.lichessWrite(user, s"Garbage collected because of $msg")
|
||||
irc.garbageCollector(message) >>- {
|
||||
if (armed) {
|
||||
doInitialSb(user)
|
||||
system.scheduler
|
||||
.scheduleOnce(wait) {
|
||||
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 =
|
||||
Bus.publish(
|
||||
|
|
|
@ -3,6 +3,7 @@ package lila.simul
|
|||
import play.api.libs.json._
|
||||
|
||||
import lila.common.LightUser
|
||||
import lila.common.Json._
|
||||
import lila.game.{ Game, GameRepo }
|
||||
import lila.user.User
|
||||
|
||||
|
@ -12,10 +13,6 @@ final class JsonView(
|
|||
proxyRepo: lila.round.GameProxyRepo
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
implicit private val colorWriter: Writes[chess.Color] = Writes { c =>
|
||||
JsString(c.name)
|
||||
}
|
||||
|
||||
implicit private val simulTeamWriter = Json.writes[SimulTeam]
|
||||
|
||||
private def fetchGames(simul: Simul) =
|
||||
|
|
|
@ -19,14 +19,14 @@ final private class 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 {
|
||||
case None =>
|
||||
data.game ?? pgnFetch.fromUrl flatMap {
|
||||
case Some(pgn) => fromFenOrPgnOrBlank(study, data.copy(pgn = pgn.some), 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) =>
|
||||
if (c.name.value.isEmpty) c.copy(name = Chapter defaultName order) else c
|
||||
}
|
||||
|
@ -125,14 +125,15 @@ final private class ChapterMaker(
|
|||
data: Data,
|
||||
order: Int,
|
||||
userId: User.ID,
|
||||
withRatings: Boolean,
|
||||
initialFen: Option[FEN] = None
|
||||
): Fu[Chapter] =
|
||||
for {
|
||||
root <- game2root(game, initialFen)
|
||||
tags <- pgnDump.tags(game, initialFen, none, withOpening = true, withRating = true)
|
||||
tags <- pgnDump.tags(game, initialFen, none, withOpening = true, withRatings)
|
||||
name <- {
|
||||
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)
|
||||
}
|
||||
_ = notifyChat(study, game, userId)
|
||||
|
|
|
@ -127,21 +127,12 @@ object JsonView {
|
|||
implicit val chapterIdWrites: Writes[Chapter.Id] = stringIsoWriter(Chapter.idIso)
|
||||
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 =>
|
||||
(v.asOpt[String] flatMap Pos.fromKey).fold[JsResult[Pos]](JsError(Nil))(JsSuccess(_))
|
||||
}
|
||||
implicit private[study] val pathWrites: Writes[Path] = Writes[Path] { p =>
|
||||
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 =>
|
||||
JsString(sri.value)
|
||||
}
|
||||
|
|
|
@ -22,14 +22,15 @@ object ServerEval {
|
|||
|
||||
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 =>
|
||||
!eval.done && onceEvery(chapter.id.value)
|
||||
} ?? {
|
||||
val unlimitedFu =
|
||||
fuccess(userId == User.lichessId) >>| userRepo
|
||||
.byId(userId)
|
||||
.map(_.exists(Granter(_.Relay)))
|
||||
fuccess(unlimited) >>|
|
||||
fuccess(userId == User.lichessId) >>| userRepo
|
||||
.byId(userId)
|
||||
.map(_.exists(Granter(_.Relay)))
|
||||
unlimitedFu flatMap { unlimited =>
|
||||
chapterRepo.startServerEval(chapter) >>- {
|
||||
fishnet ! StudyChapterRequest(
|
||||
|
|
|
@ -132,7 +132,8 @@ final class StudyApi(
|
|||
addChapter(
|
||||
studyId = study.id,
|
||||
data = data.form.toChapterData,
|
||||
sticky = study.settings.sticky
|
||||
sticky = study.settings.sticky,
|
||||
withRatings
|
||||
)(Who(user.id, Sri(""))) >> byIdWithLastChapter(studyId)
|
||||
case _ => fuccess(none)
|
||||
} 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 {
|
||||
case Some(datas) =>
|
||||
lila.common.Future.applySequentially(datas) { data =>
|
||||
addChapter(studyId, data, sticky)(who)
|
||||
addChapter(studyId, data, sticky, withRatings)(who)
|
||||
}
|
||||
case _ =>
|
||||
sequenceStudy(studyId) { study =>
|
||||
|
@ -605,7 +608,7 @@ final class StudyApi(
|
|||
}
|
||||
} >>
|
||||
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)
|
||||
} addFailureEffect {
|
||||
case ChapterMaker.ValidationException(error) =>
|
||||
|
@ -624,9 +627,11 @@ final class StudyApi(
|
|||
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 =>
|
||||
addChapter(studyId, data, sticky)(who)
|
||||
addChapter(studyId, data, sticky, withRatings)(who)
|
||||
}
|
||||
|
||||
def doAddChapter(study: Study, chapter: Chapter, sticky: Boolean, who: Who) =
|
||||
|
@ -731,7 +736,13 @@ final class StudyApi(
|
|||
chapterRepo.orderedMetadataByStudy(studyId).flatMap { chaps =>
|
||||
// deleting the only chapter? Automatically create an empty one
|
||||
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)
|
||||
}
|
||||
} // 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) =>
|
||||
Contribute(userId, study) {
|
||||
serverEvalRequester(study, chapter, userId)
|
||||
serverEvalRequester(study, chapter, userId, unlimited)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,9 @@ final private class StudyInvite(
|
|||
invited <-
|
||||
userRepo
|
||||
.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")
|
||||
relation <- relationApi.fetchRelation(invited.id, byUserId)
|
||||
_ <- 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.hasTitle) 20
|
||||
else if (inviter.perfs.bestRating >= 2000) 50
|
||||
else if (invited.hasTitle) 200
|
||||
else 100
|
||||
_ <- shouldNotify ?? notifyRateLimit(inviter.id, rateLimitCost) {
|
||||
val notificationContent = InvitedToStudy(
|
||||
|
|
|
@ -106,6 +106,8 @@ final class StudyMultiBoard(
|
|||
|
||||
private object handlers {
|
||||
|
||||
import lila.common.Json._
|
||||
|
||||
implicit val previewPlayerWriter: Writes[ChapterPreview.Player] = Writes[ChapterPreview.Player] { p =>
|
||||
Json
|
||||
.obj("name" -> p.name)
|
||||
|
|
|
@ -124,7 +124,7 @@ final private class StudySocket(
|
|||
case "addChapter" =>
|
||||
reading[ChapterMaker.Data](o) { data =>
|
||||
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" =>
|
||||
o.get[Chapter.Id]("d") foreach { chapterId =>
|
||||
|
@ -304,7 +304,8 @@ final private class StudySocket(
|
|||
"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) =
|
||||
version(
|
||||
"shapes",
|
||||
|
|
|
@ -288,6 +288,7 @@ final class TeamApi(
|
|||
lila.security.Granter(_.ManageTeam)(by) || team.createdBy == by.id ||
|
||||
(team.leaders(by.id) && !team.leaders(team.createdBy))
|
||||
) {
|
||||
logger.info(s"toggleEnabled ${team.id}: ${!team.enabled} by @${by.id}")
|
||||
if (team.enabled)
|
||||
teamRepo.disable(team).void >>
|
||||
memberRepo.userIdsByTeam(team.id).map { _ foreach cached.invalidateTeamIds } >>
|
||||
|
|
|
@ -173,7 +173,8 @@ final class UserRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
|
|||
coll
|
||||
.update(ordered = false, WriteConcern.Unacknowledged)
|
||||
.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)
|
||||
)
|
||||
.unit
|
||||
|
|
|
@ -13,7 +13,7 @@ object Dependencies {
|
|||
val maxmind = "com.sanoma.cda" %% "maxmind-geoip2-scala" % "1.3.1-THIB"
|
||||
val prismic = "io.prismic" %% "scala-kit" % "1.2.19-THIB213"
|
||||
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 scalatags = "com.lihaoyi" %% "scalatags" % "0.10.0"
|
||||
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 scalatest = "org.scalatest" %% "scalatest" % "3.1.0" % Test
|
||||
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 bloomFilter = "com.github.alexandrnikitin" %% "bloom-filter" % "0.13.1"
|
||||
|
||||
|
@ -38,17 +38,17 @@ object Dependencies {
|
|||
val version = "2.4.2"
|
||||
val macros = "com.softwaremill.macwire" %% "macros" % 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)
|
||||
}
|
||||
|
||||
object reactivemongo {
|
||||
val version = "1.0.7"
|
||||
val version = "1.0.8"
|
||||
|
||||
val driver = "org.reactivemongo" %% "reactivemongo" % version
|
||||
val stream = "org.reactivemongo" %% "reactivemongo-akkastream" % version
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
sbt.version=1.5.5
|
||||
sbt.version=1.5.6
|
||||
|
|
|
@ -3,5 +3,5 @@ resolvers += Resolver.url(
|
|||
url("https://raw.githubusercontent.com/ornicar/lila-maven/master")
|
||||
)(Resolver.ivyStylePatterns)
|
||||
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")
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue