Skip to content

Commit

Permalink
Merge branch 'master' into use-lastPly-for-draw-offers
Browse files Browse the repository at this point in the history
  • Loading branch information
johndoknjas authored Feb 15, 2025
2 parents ddcd868 + 7d0166c commit 0c4ce31
Show file tree
Hide file tree
Showing 221 changed files with 674 additions and 558 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ instance.lock

# eslint cache
/ui/.eslintcache

# deploy bearer
.lila-cli
5 changes: 2 additions & 3 deletions app/UiEnv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ object UiEnv
def setEnv(e: Env) = envVar = Some(e)
def env: Env = envVar.get

def netConfig = env.net
def contactEmailInClear = env.net.email.value
def picfitUrl = env.memo.picfitUrl
def netConfig = env.net
def picfitUrl = env.memo.picfitUrl

given lila.core.config.NetDomain = env.net.domain
given (using ctx: PageContext): Option[Nonce] = ctx.nonce
Expand Down
90 changes: 46 additions & 44 deletions app/controllers/Challenge.scala
Original file line number Diff line number Diff line change
Expand Up @@ -279,51 +279,53 @@ final class Challenge(env: Env) extends LilaController(env):

def apiCreate(username: UserStr) =
ScopedBody(_.Challenge.Write, _.Bot.Play, _.Board.Play, _.Web.Mobile) { ctx ?=> me ?=>
(!me
.is(username))
.so(
bindForm(env.setup.forms.api.user)(
doubleJsonFormError,
config =>
limit.challenge(req.ipAddress, rateLimited, cost = if me.isApiHog then 0 else 1):
env.user.repo.enabledById(username).flatMap {
case None => JsonBadRequest(jsonError(s"No such user: $username"))
case Some(destUser) if destUser.isBot && !config.rules.isEmpty =>
JsonBadRequest(jsonError("Rules not applicable for bots"))
case Some(destUser) =>
val cost = if me.isApiHog then 0 else if destUser.isBot then 1 else 5
limit.challengeBot(req.ipAddress, rateLimited, cost = if me.isBot then 1 else 0):
limit.challengeUser(me, rateLimited, cost = cost):
for
challenge <- makeOauthChallenge(config, me, destUser)
grant <- env.challenge.granter.isDenied(destUser, config.perfKey.some)
res <- grant match
case Some(denied) =>
fuccess:
JsonBadRequest:
jsonError(lila.challenge.ChallengeDenied.translated(denied))
case _ =>
env.challenge.api.create(challenge).flatMap {
if _ then
ctx.isMobileOauth
.so(env.challenge.version(challenge.id).dmap(some))
.map: socketVersion =>
val json = env.challenge.jsonView
.apiAndMobile(
challenge,
socketVersion,
lila.challenge.Direction.Out.some
)
if config.keepAliveStream then
jsOptToNdJson:
ndJson.addKeepAlive(env.challenge.keepAliveStream(challenge, json))
else JsonOk(json)
else JsonBadRequest(jsonError("Challenge not created")).toFuccess
}
yield res
}
)
(!me.is(username)).so(
bindForm(env.setup.forms.api.user)(
doubleJsonFormError,
config =>
limit.challenge(req.ipAddress, rateLimited, cost = if me.isApiHog then 0 else 1):
env.user.repo.enabledById(username).flatMap {
case None => JsonBadRequest(jsonError(s"No such user: $username"))
case Some(destUser) if destUser.isBot && !config.rules.isEmpty =>
JsonBadRequest(jsonError("Rules not applicable for bots"))
case Some(destUser) =>
env.relation.api
.fetchFollows(destUser.id, me.userId)
.flatMap: isFriend =>
val cost = if isFriend || me.isApiHog then 0 else if destUser.isBot then 1 else 5
limit.challengeBot(req.ipAddress, rateLimited, cost = if me.isBot then 1 else 0):
limit.challengeUser(me, rateLimited, cost = cost):
for
challenge <- makeOauthChallenge(config, me, destUser)
grant <- env.challenge.granter.isDenied(destUser, config.perfKey.some)
res <- grant match
case Some(denied) =>
fuccess:
JsonBadRequest:
jsonError(lila.challenge.ChallengeDenied.translated(denied))
case _ =>
env.challenge.api.create(challenge).flatMap {
if _ then
ctx.isMobileOauth
.so(env.challenge.version(challenge.id).dmap(some))
.map: socketVersion =>
val json = env.challenge.jsonView
.apiAndMobile(
challenge,
socketVersion,
lila.challenge.Direction.Out.some
)
if config.keepAliveStream then
jsOptToNdJson:
ndJson
.addKeepAlive(env.challenge.keepAliveStream(challenge, json))
else JsonOk(json)
else JsonBadRequest(jsonError("Challenge not created")).toFuccess
}
yield res
}
)
)
}

private def makeOauthChallenge(config: ApiConfig, orig: lila.user.User, dest: lila.user.User) =
Expand Down
14 changes: 10 additions & 4 deletions app/controllers/PlayApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package controllers
import play.api.i18n.Lang
import play.api.mvc.*

import lila.app.*
import lila.app.{ *, given }
import lila.core.id.GameAnyId
import lila.core.perf.UserWithPerfs

Expand Down Expand Up @@ -131,9 +131,15 @@ final class PlayApi(env: Env)(using akka.stream.Materializer) extends LilaContro
BadRequest:
jsonError:
"This endpoint can only be used with a Bot account. See https://lichess.org/api#operation/botAccountUpgrade"
else if !lila.game.Game.isBotCompatible(pov.game) then
BadRequest(jsonError("This game cannot be played with the Bot API."))
else f(pov)
else
isReallyBotCompatible(pov.game).flatMap:
if _ then f(pov)
else BadRequest(jsonError("This game cannot be played with the Bot API."))

private def isReallyBotCompatible(game: lila.core.game.Game): Fu[Boolean] =
lila.game.Game.isBotCompatible(game) match
case Some(known) => fuccess(known)
case None => game.tournamentId.so(env.tournament.api.isForBots)

private def WithPovAsBoard(id: GameId)(f: Pov => Fu[Result])(using ctx: Context)(using Me) =
WithPov(id): pov =>
Expand Down
6 changes: 3 additions & 3 deletions app/controllers/Tournament.scala
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e
)

def join(id: TourId) = AuthBody(parse.json) { ctx ?=> me ?=>
NoLameOrBot:
NoLame:
NoPlayban:
limit.tourJoin(me, rateLimited):
val data = TournamentForm.TournamentJoin(
Expand All @@ -162,7 +162,7 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e
}

def apiJoin(id: TourId) = ScopedBody(_.Tournament.Write) { ctx ?=> me ?=>
NoLameOrBot:
NoLame:
NoPlayban:
limit.tourJoin(me, rateLimited):
val data =
Expand All @@ -175,7 +175,7 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e

private def doJoin(tourId: TourId, data: TournamentForm.TournamentJoin)(using me: Me) =
data.team
.so { env.team.api.isGranted(_, me, _.Tour) }
.so(env.team.api.isGranted(_, me, _.Tour))
.flatMap: isLeader =>
api.joinWithResult(tourId, data = data, isLeader)

Expand Down
8 changes: 4 additions & 4 deletions app/views/user/show/page.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ object page:
)
.js(pageModule(info))
.js(esModules())
.css("bits.user.show")
.css("user.show")
.css(isGranted(_.UserModView).option("mod.user"))
.flag(_.noRobots, !indexable(u)):
main(cls := "page-menu", ui.dataUsername := u.username)(
Expand All @@ -59,8 +59,8 @@ object page:
Page(s"${u.username} $filterName$pageName")
.js(pageModule(info))
.js(esModules(filters.current.name == "search"))
.css("bits.user.show")
.css((filters.current.name == "search").option("bits.user.show.search"))
.css("user.show")
.css((filters.current.name == "search").option("user.show.search"))
.css(isGranted(_.UserModView).option("mod.user"))
.flag(_.noRobots, !indexable(u)):
main(cls := "page-menu", ui.dataUsername := u.username)(
Expand All @@ -75,7 +75,7 @@ object page:

private def esModules(withSearch: Boolean = false)(using Context): EsmList =
infiniteScrollEsmInit
++ esmInit("bits.user")
++ esmInit("user")
++ withSearch.so(Esm("bits.gameSearch"))
++ isGranted(_.UserModView).so(Esm("mod.user"))

Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ lazy val root = Project("lila", file("."))
.dependsOn(api)
.aggregate(api)
.settings(buildSettings)
.settings(scalacOptions ++= Seq("-unchecked", "-deprecation"))

organization := "org.lichess"
Compile / run / fork := true
javaOptions ++= Seq("-Xms64m", "-Xmx512m", "-Dlogger.file=conf/logger.dev.xml")
ThisBuild / scalacOptions ++= Seq("-unchecked", "-deprecation")
ThisBuild / usePipelining := false
// shorter prod classpath
scriptClasspath := Seq("*")
Expand Down
2 changes: 1 addition & 1 deletion modules/analyse/src/main/Analyser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class Analyser(
analysis.id match
case Analysis.Id.Game(id) =>
gameRepo.game(id).flatMapz { prev =>
val game = prev.focus(_.metadata.analysed).set(true)
val game = prev.focus(_.metadata.analysed).replace(true)
for
_ <- gameRepo.setAnalysed(game.id, true)
_ <- analysisRepo.save(analysis)
Expand Down
10 changes: 6 additions & 4 deletions modules/api/src/main/ChatFreshness.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ final class ChatFreshness(tourCache: TournamentCache, swissCache: SwissCache)(us
case _ => fuTrue

def of(tour: Tournament) =
tour.finishedSinceSeconds.forall:
_ < (tour.nbPlayers + 120) * 30
tour.finishedSinceSeconds match
case Some(finishedSinceSeconds) => finishedSinceSeconds < (tour.nbPlayers + 120) * 30
case None => tour.startsAt.isBefore(nowInstant.plusWeeks(1))

def of(swiss: Swiss) =
swiss.finishedSinceSeconds.forall:
_ < (swiss.nbPlayers + 60) * 60
swiss.finishedSinceSeconds match
case Some(finishedSinceSeconds) => finishedSinceSeconds < (swiss.nbPlayers + 60) * 60
case None => swiss.startsAt.isBefore(nowInstant.plusWeeks(1))
2 changes: 1 addition & 1 deletion modules/api/src/main/Env.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ final class Env(
cacheApi: lila.memo.CacheApi,
webConfig: lila.web.WebConfig,
realPlayerApi: lila.web.RealPlayerApi,
bookmarkExists: lila.core.bookmark.BookmarkExists,
bookmarkExists: lila.core.misc.BookmarkExists,
manifest: lila.web.AssetManifest,
tokenApi: lila.oauth.AccessTokenApi
)(using val mode: Mode, scheduler: Scheduler)(using
Expand Down
2 changes: 1 addition & 1 deletion modules/api/src/main/EventStream.scala
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ final class EventStream(
gameJsonView
.ownerPreview(pov)(using lightUserApi.sync)
.add("source" -> game.source.map(_.name)) ++ compatJson(
bot = me.isBot && lila.game.Game.isBotCompatible(game),
bot = me.isBot && lila.game.Game.isBotCompatible(game).|(true),
board = lila.game.Game.isBoardCompatible(game)
) ++ Json.obj(
"id" -> game.id // API BC
Expand Down
2 changes: 1 addition & 1 deletion modules/bookmark/src/main/Env.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ final class Env(

lazy val api = wire[BookmarkApi]

def exists: lila.core.bookmark.BookmarkExists = api.exists
def exists: lila.core.misc.BookmarkExists = api.exists

lila.common.Bus.subscribeFun("roundUnplayed"):
case lila.core.round.DeleteUnplayed(gameId) => api.removeByGameId(gameId)
2 changes: 1 addition & 1 deletion modules/challenge/src/main/ChallengeApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ final class ChallengeApi(
then fuccess(Left("The challenge has been canceled."))
else if c.declined
then fuccess(Left("The challenge has been declined."))
else if me.exists(_.isBot) && !lila.game.Game.isBotCompatible(chess.Speed(c.clock.map(_.config)))
else if me.exists(_.isBot) && !c.clock.map(_.config).forall(lila.core.game.isBotCompatible)
then fuccess(Left("Game incompatible with a BOT account"))
else if c.open.exists(!_.canJoin)
then fuccess(Left("The challenge is not for you to accept."))
Expand Down
7 changes: 4 additions & 3 deletions modules/common/src/main/HTTPRequest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ object HTTPRequest:
// spiders/crawlers
"""Googlebot|GoogleOther|AdsBot|Google-Read-Aloud|bingbot|BingPreview|facebookexternalhit|meta-externalagent|SemrushBot|AhrefsBot|PetalBot|Applebot|YandexBot|YandexAdNet|YandexImages|Twitterbot|Baiduspider|Amazonbot|Bytespider|yacybot|ImagesiftBot|ChatGLM-Spider|YisouSpider|Yeti/""" +
// http libs
"""|HeadlessChrome|okhttp|axios|wget|curl|python-requests|aiohttp|commons-httpclient|python-urllib|python-httpx|Nessus|imroc/req"""
"""|HeadlessChrome|okhttp|axios|undici|wget|curl|python-requests|aiohttp|commons-httpclient|python-urllib|python-httpx|Nessus|imroc/req"""

final class UaMatcher(rStr: String):
private val pattern = rStr.r.pattern
Expand Down Expand Up @@ -128,8 +128,9 @@ object HTTPRequest:
private def isGameExport(req: RequestHeader) =
"^/@/[\\w-]{2,30}/download$".r.matches(req.path) ||
"^/(api/games/user|games/export)/[\\w-]{2,30}($|/.+)".r.matches(req.path)
private def isStudyExport(req: RequestHeader) = "^/study/by/[\\w-]{2,30}/export.pgn$".r.matches(req.path)
private def isAccountClose(req: RequestHeader) = req.path == "/account/close"
private def isStudyExport(req: RequestHeader) = "^/study/by/[\\w-]{2,30}/export.pgn$".r.matches(req.path)
private def isAccountClose(req: RequestHeader) =
req.path == "/account/close" || req.path == "/account/delete"

def isClosedLoginPath(req: RequestHeader) =
isDataDump(req) || isAppeal(req) || isStudyExport(req) || isGameExport(req) || isAccountClose(req)
Expand Down
4 changes: 0 additions & 4 deletions modules/core/src/main/bookmark.scala

This file was deleted.

15 changes: 3 additions & 12 deletions modules/core/src/main/game/Game.scala
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,6 @@ case class Game(
def playableCorrespondenceClock: Option[CorrespondenceClock] =
if playable then correspondenceClock else none

def speed = Speed(chess.clock.map(_.config))

def perfKey: PerfKey = PerfKey(variant, speed)

def ratingVariant: Variant =
Expand All @@ -151,18 +149,9 @@ case class Game(

def aiPov: Option[Pov] = players.findColor(_.isAi).map(pov)

def mapPlayers(f: Player => Player) = copy(players = players.map(f))

def swissPreventsDraw = isSwiss && playedTurns < 60
def rulePreventsDraw = hasRule(_.noEarlyDraw) && playedTurns < 60

def playerHasOfferedDrawRecently(color: Color) =
drawOffers.lastBy(color).exists(_ >= ply - 20)

def offerDraw(color: Color) = copy(
metadata = metadata.copy(drawOffers = drawOffers.add(color, ply))
).updatePlayer(color, _.copy(isOfferingDraw = true))

def boosted = rated && finished && bothPlayersHaveMoved && playedTurns < 10

def abortable = status == Status.Started && playedTurns < 2 && nonMandatory
Expand Down Expand Up @@ -222,7 +211,9 @@ case class Game(
def isCorrespondence = speed == Speed.Correspondence
def isSpeed(s: Speed) = speed == s

def hasClock = clock.isDefined
def hasClock = clock.isDefined
def clockConfig = clock.map(_.config)
def speed = Speed(clockConfig)

def hasCorrespondenceClock = daysPerTurn.isDefined

Expand Down
1 change: 1 addition & 0 deletions modules/core/src/main/game/misc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ def allowRated(variant: Variant, clock: Option[Clock.Config]) =
c.limitSeconds > 0 || c.incrementSeconds > 1

def isBoardCompatible(clock: Clock.Config): Boolean = Speed(clock) >= Speed.Rapid
def isBotCompatible(clock: Clock.Config): Boolean = Speed(clock) >= Speed.Bullet

def interleave[A](a: Seq[A], b: Seq[A]): Vector[A] =
val iterA = a.iterator
Expand Down
2 changes: 2 additions & 0 deletions modules/core/src/main/misc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,5 @@ trait PicfitUrl:
def thumbnail(id: lila.core.id.ImageId, width: Int, height: Int): String
def resize(id: lila.core.id.ImageId, size: Either[Int, Int]): String
def raw(id: lila.core.id.ImageId): String

type BookmarkExists = (game.Game, Option[userId.UserId]) => Fu[Boolean]
2 changes: 0 additions & 2 deletions modules/coreI18n/src/main/key.scala
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,6 @@ object I18nKey:
val `addRound`: I18nKey = "broadcast:addRound"
val `ongoing`: I18nKey = "broadcast:ongoing"
val `upcoming`: I18nKey = "broadcast:upcoming"
val `completed`: I18nKey = "broadcast:completed"
val `completedHelp`: I18nKey = "broadcast:completedHelp"
val `roundName`: I18nKey = "broadcast:roundName"
val `roundNumber`: I18nKey = "broadcast:roundNumber"
val `tournamentName`: I18nKey = "broadcast:tournamentName"
Expand Down
6 changes: 2 additions & 4 deletions modules/db/src/main/BSON.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ abstract class BSONReadOnly[T] extends BSONDocumentReader[T]:

def reads(reader: Reader): T

def readDocument(doc: Bdoc) =
Try {
reads(new Reader(doc))
}
def readDocument(doc: Bdoc) = Try:
reads(new Reader(doc))

def read(doc: Bdoc) = readDocument(doc).get

Expand Down
3 changes: 2 additions & 1 deletion modules/db/src/main/Handlers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ trait Handlers:
def typedMapHandlerIso[K, V: BSONHandler](using keyIso: StringIso[K]): BSONHandler[Map[K, V]] =
mapHandler[V].as[Map[K, V]](_.mapKeys(keyIso.from), _.mapKeys(keyIso.to))

def ifPresentHandler[A](a: A) = quickHandler({ case BSONBoolean(true) => a }, _ => BSONBoolean(true))
def ifPresentHandler[A](a: A): BSONHandler[A] =
quickHandler[A]({ case BSONBoolean(true) => a }, _ => BSONBoolean(true))

given [T: BSONHandler]: BSONHandler[NonEmptyList[T]] =
def listWriter = BSONWriter.collectionWriter[T, List[T]]
Expand Down
2 changes: 1 addition & 1 deletion modules/fide/src/main/FidePlayerSync.scala
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ final private class FidePlayerSync(repo: FideRepo, ws: StandaloneWSClient)(using
nbAll <- repo.player.countAll
yield
lila.mon.fideSync.updated.update(nbUpdated)
lila.mon.fideSync.players.update(nbAll)
lila.mon.fideSync.players.update(nbAll.toDouble)
logger.info(s"RelayFidePlayerApi.update upserted: $nbUpdated, total: $nbAll")
yield ()

Expand Down
Loading

0 comments on commit 0c4ce31

Please sign in to comment.