Skip to content

Commit

Permalink
Merge branch 'master' into prevent-select-diff-platforms
Browse files Browse the repository at this point in the history
  • Loading branch information
johndoknjas committed Dec 22, 2024
2 parents 19f87c4 + cfa1cfd commit 12d4d34
Show file tree
Hide file tree
Showing 150 changed files with 900 additions and 704 deletions.
33 changes: 15 additions & 18 deletions app/controllers/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -553,24 +553,21 @@ final class User(
}

def perfStat(username: UserStr, perfKey: PerfKey) = Open:
PerfType
.isLeaderboardable(perfKey)
.so:
Found(env.perfStat.api.data(username, perfKey)): data =>
negotiate(
Ok.async:
env.history
.ratingChartApi(data.user.user)
.map:
views.user.perfStatPage(data, _)
,
JsonOk:
getBool("graph")
.soFu:
env.history.ratingChartApi.singlePerf(data.user.user, data.stat.perfType.key)
.map: graph =>
env.perfStat.jsonView(data).add("graph", graph)
)
Found(env.perfStat.api.data(username, perfKey)): data =>
negotiate(
Ok.async:
env.history
.ratingChartApi(data.user.user)
.map:
views.user.perfStatPage(data, _)
,
JsonOk:
getBool("graph")
.soFu:
env.history.ratingChartApi.singlePerf(data.user.user, data.stat.perfType.key)
.map: graph =>
env.perfStat.jsonView(data).add("graph", graph)
)

def autocomplete = OpenOrScoped(): ctx ?=>
NoTor:
Expand Down
1 change: 1 addition & 0 deletions conf/base.conf
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ kamon {
process-metrics.enabled = yes
host-metrics.enabled = no
prometheus-reporter.enabled = yes
prometheus-reporter.factory = "lila.web.PrometheusReporter$Factory"
}
}
# Don't let play manage its own PID file
Expand Down
3 changes: 2 additions & 1 deletion modules/common/src/main/mon.scala
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,8 @@ object mon:
timer("relay.sync.time").withTags(relay(official, id, slug))
def httpGet(host: String, proxy: Option[String]) =
future("relay.http.get", tags("host" -> host, "proxy" -> proxy.getOrElse("none")))
val dedup = counter("relay.fetch.dedup").withoutTags()
val dedup = counter("relay.fetch.dedup").withoutTags()
def etag(hit: Boolean) = counter("relay.fetch.etag").withTag("hit", hit)

object bot:
def moves(username: String) = counter("bot.moves").withTag("name", username)
Expand Down
8 changes: 1 addition & 7 deletions modules/puzzle/src/main/PuzzleHistory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ object PuzzleHistory:
case class PuzzleSession(
theme: PuzzleTheme.Key,
puzzles: NonEmptyList[SessionRound] // chronological order, oldest first
) {
// val nb = puzzles.size
// val firstWins = puzzles.toList.count(_.round.firstWin)
// val fails = nb - firstWins
// def puzzleRatingAvg = puzzles.toList.foldLeft(0)(_ + _.puzzle.glicko.intRating)
// def performance = puzzleRatingAvg - 500 + math.round(1000 * (firstWins.toFloat / nb))
}
)

final class HistoryAdapter(user: WithPerf, colls: PuzzleColls)(using Executor)
extends AdapterLike[PuzzleSession]:
Expand Down
43 changes: 36 additions & 7 deletions modules/relay/src/main/RelayFetch.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import chess.{ Outcome, Ply }
import com.github.blemale.scaffeine.LoadingCache
import io.mola.galimatias.URL
import play.api.libs.json.*
import play.api.libs.ws.{ StandaloneWSRequest, StandaloneWSResponse }
import scalalib.model.Seconds

import lila.common.LilaScheduler
Expand Down Expand Up @@ -238,7 +239,7 @@ final private class RelayFetch(
private val createdGames =
cacheApi.notLoadingSync[LccGameKey, GameJson](256, "relay.fetch.createdLccGames"):
_.expireAfter[LccGameKey, GameJson](
create = (key, _) => (if key.startsWith("started ") then 1 minute else 5 minutes),
create = (key, _) => (if key.startsWith("started ") then 40.seconds else 3.minutes),
update = (_, _, current) => current,
read = (_, _, current) => current
).build()
Expand Down Expand Up @@ -300,7 +301,7 @@ final private class RelayFetch(
.map { MultiPgn.split(_, RelayFetch.maxGamesToRead(rt.tour.official)) }
.flatMap(multiPgnToGames.future)
case RelayFormat.LccWithGames(lcc) =>
httpGetJson[RoundJson](lcc.indexUrl).flatMap: round =>
lccRoundJsonWithEtag(lcc.indexUrl).flatMap: round =>
val lookForStart: Boolean =
rt.round.startsAtTime
.map(_.minusSeconds(rt.round.sync.delay.so(_.value) + 5 * 60))
Expand All @@ -310,7 +311,7 @@ final private class RelayFetch(
val game = i + 1
val tags = pairing.tags(lcc.round, game, round.date)
lccCache(lcc, game, tags, lookForStart): () =>
httpGetJson[GameJson](lcc.gameUrl(game)).recover:
lccGameJsonWithEtag(lcc.gameUrl(game)).recover:
case _: Exception => GameJson(moves = Nil, result = none)
.map { _.toPgn(tags) }
.recover: _ =>
Expand All @@ -321,7 +322,7 @@ final private class RelayFetch(
MultiPgn(pgns.sortBy(_._1).map(_._2))
.flatMap(multiPgnToGames.future)
case RelayFormat.LccWithoutGames(lcc) =>
httpGetJson[RoundJson](lcc.indexUrl)
lccRoundJsonWithEtag(lcc.indexUrl)
.map: round =>
MultiPgn:
round.pairings.mapWithIndex: (pairing, i) =>
Expand All @@ -332,12 +333,40 @@ final private class RelayFetch(
private def httpGetPgn(url: URL)(using CanProxy): Fu[PgnStr] =
PgnStr.from(formatApi.httpGetAndGuessCharset(url))

private def httpGetJson[A: Reads](url: URL)(using CanProxy): Fu[A] = for
str <- formatApi.httpGet(url)
json <- Future(Json.parse(str)) // Json.parse throws exceptions (!)
private def readAsJson[A: Reads](url: URL)(body: String): Fu[A] = for
json <- Future(Json.parse(body)) // Json.parse throws exceptions (!)
data <- summon[Reads[A]].reads(json).fold(err => fufail(s"Invalid JSON from $url: $err"), fuccess)
yield data

// lcc supports https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
private def fetchJsonWithEtag[A: Reads](initialCapacity: Int): URL => CanProxy ?=> Fu[A] =
import RelayFormat.Etag
val cache = cacheApi.notLoadingSync[URL, (Etag, A)](initialCapacity, "relay.fetch.jsonWithEtag"):
_.expireAfterWrite(5 minutes).build()
url =>
CanProxy ?=>
cache
.getIfPresent(url)
.match
case None =>
for
(body, newEtag) <- formatApi.httpGetWithEtag(url, none)
data <- readAsJson[A](url)(~body)
yield (data, newEtag)
case Some((etag, prev)) =>
for
(body, newEtag) <- formatApi.httpGetWithEtag(url, etag.some)
isHit = body.isEmpty && newEtag.forall(_ == etag) // on 304 response, Etag might be empty
_ = lila.mon.relay.etag(isHit).increment()
data <- if isHit then fuccess(prev) else readAsJson[A](url)(~body)
yield (data, newEtag.orElse(etag.some))
.map: (data, newEtag) =>
newEtag.foreach(e => cache.put(url, e -> data))
data

private val lccGameJsonWithEtag = fetchJsonWithEtag[DgtJson.GameJson](512)
private val lccRoundJsonWithEtag = fetchJsonWithEtag[DgtJson.RoundJson](32)

private object RelayFetch:

val maxChaptersToShow: Max = Max(100)
Expand Down
60 changes: 37 additions & 23 deletions modules/relay/src/main/RelayFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import play.api.libs.ws.{
StandaloneWSRequest,
StandaloneWSResponse
}

import scala.util.matching.Regex

import lila.core.config.{ Credentials, HostPort }
Expand Down Expand Up @@ -69,37 +68,51 @@ final private class RelayFormatApi(
.so: id =>
roundRepo.exists(id).map(_.option(RelayFormat.Round(id)))

private[relay] def httpGet(url: URL)(using CanProxy): Fu[String] =
httpGetResponse(url).map(_.body)
def httpGet(url: URL)(using CanProxy): Fu[String] =
httpGetResponse(toRequest(url)).map(_.body)

private[relay] def httpGetAndGuessCharset(url: URL)(using CanProxy): Fu[String] =
httpGetResponse(url).map: res =>
def httpGetAndGuessCharset(url: URL)(using CanProxy): Fu[String] =
httpGetResponse(toRequest(url)).map: res =>
responseHeaderCharset(res) match
case None => lila.common.String.charset.guessAndDecode(res.bodyAsBytes)
case Some(known) => res.bodyAsBytes.decodeString(known)

def httpGetWithEtag(url: URL, etag: Option[Etag])(using CanProxy): Fu[(Option[String], Option[Etag])] =
val req = etag
.foldLeft(toRequest(url))((req, etag) => req.addHttpHeaders("If-None-Match" -> etag))
httpGetResponse(req)
.flatMap: res =>
val newEtag = res.header("Etag")
if res.status == 304 then fuccess(none -> newEtag.orElse(etag))
else fuccess((res.body: String).some -> newEtag)
.monSuccess(_.relay.httpGet(url.host.toString, req.proxyServer.map(_.host)))

private def httpGetResponse(req: StandaloneWSRequest)(using CanProxy): Future[StandaloneWSResponse] =
Future
.fromTry(lila.common.url.parse(req.url))
.flatMap: url =>
req
.get()
.flatMap: res =>
if res.status == 200 || res.status == 304 then fuccess(res)
else if res.status == 404 then fufail(NotFound(url))
else fufail(s"[${res.status}] ${req.url}")
.monSuccess(_.relay.httpGet(url.host.toString, req.proxyServer.map(_.host)))

private def responseHeaderCharset(res: StandaloneWSResponse): Option[java.nio.charset.Charset] =
import play.shaded.ahc.org.asynchttpclient.util.HttpUtils
Option(HttpUtils.extractContentTypeCharsetAttribute(res.contentType)).orElse:
res.contentType.startsWith("text/").option(java.nio.charset.StandardCharsets.ISO_8859_1)

private def httpGetResponse(url: URL)(using CanProxy): Future[StandaloneWSResponse] =
val (req, proxy) = addProxy(url):
ws.url(url.toString)
.withRequestTimeout(5.seconds)
.withFollowRedirects(false)
req
.get()
.flatMap: res =>
if res.status == 200 then fuccess(res)
else if res.status == 404 then fufail(NotFound(url))
else fufail(s"[${res.status}] $url")
.monSuccess(_.relay.httpGet(url.host.toString, proxy))

private def addProxy(url: URL)(ws: StandaloneWSRequest)(using
allowed: CanProxy
): (StandaloneWSRequest, Option[String]) =
def server = for
private def toRequest(url: URL)(using CanProxy): StandaloneWSRequest =
val req = ws
.url(url.toString)
.withRequestTimeout(5.seconds)
.withFollowRedirects(false)
proxyServerFor(url).foldLeft(req)(_ withProxyServer _)

private def proxyServerFor(url: URL)(using allowed: CanProxy): Option[DefaultWSProxyServer] =
for
hostPort <- proxyHostPort.get()
if allowed.yes
proxyRegex = proxyDomainRegex.get()
Expand All @@ -112,7 +125,6 @@ final private class RelayFormatApi(
principal = creds.map(_.user),
password = creds.map(_.password.value)
)
server.foldLeft(ws)(_ withProxyServer _) -> server.map(_.host)

private def looksLikePgn(body: String)(using CanProxy): Boolean =
MultiPgn
Expand All @@ -138,6 +150,8 @@ private enum RelayFormat:

private object RelayFormat:

type Etag = String

opaque type CanProxy = Boolean
object CanProxy extends YesNo[CanProxy]

Expand Down
8 changes: 8 additions & 0 deletions modules/tournament/src/main/Tournament.scala
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ case class Tournament(

def secondsToFinish = (finishesAt.toSeconds - nowSeconds).toInt.atLeast(0)

def progressPercent: Int =
if isCreated then 0
else if isFinished then 100
else
val total = minutes * 60
val remaining = secondsToFinish
100 - (remaining * 100 / total)

def pairingsClosed = secondsToFinish < math.max(30, math.min(clock.limitSeconds.value / 2, 120))

def isStillWorthEntering =
Expand Down
13 changes: 9 additions & 4 deletions modules/tournament/src/main/ui/TournamentUi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,18 @@ final class TournamentUi(helpers: Helpers)(getTourName: GetTourName):
tours.map: tour =>
val visiblePlayers = (tour.nbPlayers >= 10).option(tour.nbPlayers)
tr(
td(cls := "name")(
td(
a(cls := "text", dataIcon := tournamentIcon(tour), href := routes.Tournament.show(tour.id)):
tour.name(full = false)
),
td(
if tour.isStarted then timeRemaining(tour.finishesAt)
else momentFromNow(tour.schedule.fold(tour.startsAt)(_.atInstant))
td(cls := "progress-td")(
span(cls := "progress")(
(
if tour.isStarted then timeRemaining(tour.finishesAt)
else momentFromNow(tour.schedule.fold(tour.startsAt)(_.atInstant))
) (cls := "progress__text"),
span(cls := "progress__bar", st.style := s"width:${tour.progressPercent}%")
)
),
td(tour.durationString),
tour.conditions.teamMember match
Expand Down
Binary file modified public/flair/img/activity.shogi-king.webp
Binary file not shown.
12 changes: 6 additions & 6 deletions translation/dest/appeal/ar-SA.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cleanAllGood">حسابك غير مميز أو مقيد. أنت بخير!</string>
<string name="engineMarked" comment="This denotes that the player's account is marked for cheating, specifically for using external assistance.&#10;&#10;Please note that while the key is 'engineMarked', Lichess has a wider definition of what constitutes 'external assistance'. External assistance is usually relying on a chess engine, but it can also mean looking through opening lines, taking advice from a strong player, and more.">حسابك مشار إليه كمساعدة الخارجية في الألعاب.</string>
<string name="engineMarked">حسابك مشار إليه كمساعدة الخارجية في الألعاب.</string>
<string name="engineMarkedInfo">نحن نعرف هذا بأنه استخدام أي مساعدة خارجية لتعزيز معرفتك و/أو مهاراتك في الحساب من أجل اكتساب أفضلية غير عادلة على خصمك. راجع صفحة %s لمزيد من التفاصيل.</string>
<string name="arenaBanned">حسابك محظور من الانضمام إلى الساحات.</string>
<string name="prizeBanned">تم حظر حسابك من البطولات ذو جوائز حقيقية.</string>
<string name="boosterMarked">تم تحديد حسابك للتلاعب بالتقييم.</string>
<string name="boosterMarkedInfo">نحن نعرف هذا بأنه التلاعب المتعمد بالتصنيف عن طريق فقدان الأدوار عن قصد أو اللعب ضد حساب آخر يفقد الأدوار عمدا.</string>
<string name="accountMuted" comment="muted in the sense of: You cannot chat with your opponent anymore or post messages.">تم كتم صوت حسابك.</string>
<string name="accountMutedInfo" comment="%s is &quot;communication guidelines&quot;, which is a string separately available for translation.">اقرأ %s الخاص بنا. الفشل في اتباع القواعد الإرشادية للتواصل يمكن أن يؤدي إلى كتم الحسابات.</string>
<string name="accountMuted">تم كتم صوت حسابك.</string>
<string name="accountMutedInfo">اقرأ %s الخاص بنا. الفشل في اتباع القواعد الإرشادية للتواصل يمكن أن يؤدي إلى كتم الحسابات.</string>
<string name="excludedFromLeaderboards">تم استبعاد حسابك من لوحات المتصدرين.</string>
<string name="excludedFromLeaderboardsInfo" comment="Follows the string excludedFromLeaderboards: &quot;Your account has been excluded from leaderboards.&quot;">نحن نعرّف هذا على أنه استخدام أي طريقة غير عادلة للدخول إلى لوحة المتصدرين.</string>
<string name="excludedFromLeaderboardsInfo">نحن نعرّف هذا على أنه استخدام أي طريقة غير عادلة للدخول إلى لوحة المتصدرين.</string>
<string name="closedByModerators">تم إغلاق حسابك من قبل المشرفين.</string>
<string name="hiddenBlog">تم إخفاء مدوناتك من قبل المشرفين.</string>
<string name="hiddenBlogInfo">تأكد من قراءة %s مرة أخرى.</string>
<string name="playTimeout" comment="play timeout = Lichess prevents you temporarily from playing any games">تم إيقافك عن اللعب.</string>
<string name="communicationGuidelines" comment="Part of a longer sentence:&#10;&#10;Read our communication guidelines. Failure to follow the communication guidelines can result in accounts being muted.">قواعد التواصل</string>
<string name="playTimeout">تم إيقافك عن اللعب.</string>
<string name="communicationGuidelines">قواعد التواصل</string>
<string name="blogRules">قواعد المدونة</string>
<string name="fairPlay">اللعب العادل</string>
</resources>
6 changes: 3 additions & 3 deletions translation/dest/appeal/be-BY.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cleanAllGood">Ваш уліковы запіс не пазначаны і не заблакаваны. У вас усё добра!</string>
<string name="engineMarked" comment="This denotes that the player's account is marked for cheating, specifically for using external assistance.&#10;&#10;Please note that while the key is 'engineMarked', Lichess has a wider definition of what constitutes 'external assistance'. External assistance is usually relying on a chess engine, but it can also mean looking through opening lines, taking advice from a strong player, and more.">Ваш уліковы запіс пазначаная для атрымання знешняй дапамогі ў гульнях.</string>
<string name="engineMarked">Ваш уліковы запіс пазначаная для атрымання знешняй дапамогі ў гульнях.</string>
<string name="engineMarkedInfo">Мы вызначаем гэта як выкарыстанне любой знешняй дапамогі для ўмацавання вашых ведаў і / або навыкаў разліку з мэтай атрымання несправядлівага перавагі над вашым апанентам. Больш падрабязную інфармацыю глядзіце на старонцы %s.</string>
<string name="arenaBanned">Вашай ўліковага запісу забаронена ўдзельнічаць у арэнах.</string>
<string name="prizeBanned">Вашаму акаўнта забаронена ўдзельнічаць у турнірах з рэальнымі прызамі.</string>
<string name="boosterMarked">Ваш уліковы запіс пазначаная для маніпулявання рэйтынгам.</string>
<string name="boosterMarkedInfo">Мы вызначаем гэта як наўмыснае маніпуліраванне рэйтынгам шляхам наўмыснага пройгрышу ў гульнях або шляхам гульні супраць іншай уліковага запісу, якая наўмысна прайграе ў гульнях.</string>
<string name="accountMuted" comment="muted in the sense of: You cannot chat with your opponent anymore or post messages.">Ваш уліковы запіс адключаная.</string>
<string name="accountMutedInfo" comment="%s is &quot;communication guidelines&quot;, which is a string separately available for translation.">Азнаёмцеся з нашымі %s. Невыкананне правілаў абмену паведамленнямі можа прывесці да адключэння доступу да акантаў.</string>
<string name="accountMuted">Ваш уліковы запіс адключаная.</string>
<string name="accountMutedInfo">Азнаёмцеся з нашымі %s. Невыкананне правілаў абмену паведамленнямі можа прывесці да адключэння доступу да акантаў.</string>
<string name="excludedFromLeaderboards">Ваш уліковы запіс быў выключаны са спісу лідараў.</string>
</resources>
Loading

0 comments on commit 12d4d34

Please sign in to comment.