merge + tweaks, post-log4j

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

2
.gitignore vendored
View File

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

View File

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

View File

@ -117,16 +117,21 @@ final class Env(
text = "Team IDs that always get their tournaments visible on /tournament. Separated by commas.".some
)
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]

View File

@ -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))
}
}

View File

@ -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 =

View File

@ -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))
}

View File

@ -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 =

View File

@ -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
}
}

View File

@ -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)

View File

@ -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 =>

View File

@ -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) =

View File

@ -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)

View File

@ -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>"""
)
}

View File

@ -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]

View File

@ -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()
)
)
)

View File

@ -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
),

View File

@ -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"),

View File

@ -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"

View File

@ -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

View File

@ -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"
)

View File

@ -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)(

View File

@ -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")(

View File

@ -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
)
)
)
}
}
)
}

View File

@ -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")
)
),

View File

@ -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")
)

View File

@ -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(

View File

@ -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

View File

@ -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 =

View File

@ -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 =>

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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._

View File

@ -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 {

View File

@ -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 }

View File

@ -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 =>

View File

@ -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(

View File

@ -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"""
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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 {

View File

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

View File

@ -646,9 +646,15 @@ object mon {
def request(hit: Boolean) = counter("fishnet.http.acquire").withTag("hit", hit)
}
def 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)

View File

@ -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 =

View File

@ -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]

View File

@ -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(

View File

@ -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"

View File

@ -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

View File

@ -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")

View File

@ -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]

View File

@ -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
}

View File

@ -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) =>

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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) =

View File

@ -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
}
}

View File

@ -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&notes", 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)

View File

@ -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"

View File

@ -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)

View File

@ -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],

View File

@ -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
})
)
}
)
}
)
)
}

View File

@ -321,7 +321,8 @@ final class ModlogApi(repo: ModlogRepo, userRepo: UserRepo, ircApi: IrcApi)(impl
case M.engine | M.unengine | M.booster | M.unbooster | M.reopenAccount | M.unalt =>
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)
}

View File

@ -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 {

View File

@ -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

View File

@ -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]] =

View File

@ -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 {

View File

@ -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 = "",

View File

@ -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

View File

@ -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:"
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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
)
}

View File

@ -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)
}
}
}

View File

@ -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())

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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(

View File

@ -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) =

View File

@ -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)

View File

@ -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)
}

View File

@ -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(

View File

@ -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)
}
}

View File

@ -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(

View File

@ -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)

View File

@ -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",

View File

@ -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 } >>

View File

@ -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

View File

@ -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)
}

View File

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

View File

@ -3,5 +3,5 @@ resolvers += Resolver.url(
url("https://raw.githubusercontent.com/ornicar/lila-maven/master")
)(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