Skip to content

Commit

Permalink
Merge branch 'master' into user-package
Browse files Browse the repository at this point in the history
  • Loading branch information
ornicar committed Feb 10, 2025
2 parents 8b9e27b + af04531 commit 30dbd1b
Show file tree
Hide file tree
Showing 73 changed files with 1,053 additions and 803 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
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.id)
.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
2 changes: 0 additions & 2 deletions app/views/base/embed.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ object embed:
(ctx.bg == "system").option(page.ui.systemThemeScript(ctx.nonce.some)),
page.ui.pieceSprite(ctx.pieceSet.name),
cssTag("common.theme.embed"),
link(rel := "stylesheet", href := assetUrl("css/theme/font-face.css")),
cssKeys.map(cssTag),
page.ui.scriptsPreload(modules.flatMap(_.map(_.key)))
),
Expand Down Expand Up @@ -67,7 +66,6 @@ object embed:
(ctx.bg == "system").option(page.ui.systemThemeScript(ctx.nonce.some)),
page.ui.pieceSprite(ctx.pieceSet.name),
cssTag("common.theme.embed"),
link(rel := "stylesheet", href := assetUrl("css/theme/font-face.css")),
cssKeys.map(cssTag),
page.ui.sitePreload(
List[I18nModule.Selector](_.site, _.timeago) ++ i18nModules,
Expand Down
11 changes: 6 additions & 5 deletions app/views/base/page.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ object page:
raw(s"""<meta name="theme-color" content="${ctx.pref.themeColor}">""")

private def boardPreload(using ctx: Context) = frag(
preload(staticAssetUrl(s"images/board/${ctx.pref.currentTheme.file}"), "image", crossorigin = false),
preload(assetUrl(s"images/board/${ctx.pref.currentTheme.file}"), "image", crossorigin = false),
ctx.pref.is3d.option(
preload(
staticAssetUrl(s"images/staunton/board/${ctx.pref.currentTheme3d.file}"),
assetUrl(s"images/staunton/board/${ctx.pref.currentTheme3d.file}"),
"image",
crossorigin = false
)
Expand Down Expand Up @@ -61,7 +61,6 @@ object page:
else s"${ctx.me.so(_.username.value + " ")} $prodTitle"
,
cssTag("common.theme.all"),
link(rel := "stylesheet", href := assetUrl("css/theme/font-face.css")),
cssTag("site"),
pref.is3d.option(cssTag("common.board-3d")),
ctx.data.inquiry.isDefined.option(cssTag("mod.inquiry")),
Expand All @@ -81,8 +80,10 @@ object page:
noTranslate,
p.openGraph.map(lila.web.ui.openGraph),
p.atomLinkTag | dailyNewsAtom,
(pref.bg == lila.pref.Pref.Bg.TRANSPARENT).option(pref.bgImgOrDefault).map { img =>
val url = escapeHtmlRaw(img).replace("&amp;", "&")
(pref.bg == lila.pref.Pref.Bg.TRANSPARENT).option(pref.bgImgOrDefault).map { loc =>
val url =
if loc.startsWith("/assets/") then assetUrl(loc.drop(8))
else escapeHtmlRaw(loc).replace("&amp;", "&")
raw(s"""<style id="bg-data">html.transp::before{background-image:url("$url");}</style>""")
},
fontPreload,
Expand Down
9 changes: 0 additions & 9 deletions modules/core/src/main/game/Game.scala
Original file line number Diff line number Diff line change
Expand Up @@ -151,18 +151,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
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
3 changes: 3 additions & 0 deletions modules/game/src/main/Game.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ object GameExt:
g.clock.map: c =>
g.start.withClock(c.start)

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

def playerCanOfferDraw(color: Color) =
g.started && g.playable &&
g.ply >= 2 &&
Expand Down
8 changes: 7 additions & 1 deletion modules/relay/src/main/RelayApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,13 @@ final class RelayApi(
round <- copyRoundSourceSettings(updated)
_ <- (from.name != round.name).so(studyApi.rename(round.studyId, round.name.into(StudyName)))
setters <- tryBdoc(round).toEither.toFuture
unsets = $unsetCompute(from, updated, ("caption", _.caption), ("finishedAt", _.finishedAt))
unsets = $unsetCompute(
from,
updated,
("caption", _.caption),
("startedAt", _.startedAt),
("finishedAt", _.finishedAt)
)
_ <- roundRepo.coll.update.one($id(round.id), $set(setters) ++ unsets).void
_ <- (round.sync.playing != from.sync.playing)
.so(sendToContributors(round.id, "relaySync", jsonView.sync(round)))
Expand Down
31 changes: 21 additions & 10 deletions modules/relay/src/main/RelayRoundForm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import play.api.data.*
import play.api.data.Forms.*
import play.api.data.format.Formatter
import scalalib.model.Seconds
import lila.common.Form.{ cleanText, formatter, into, stringIn, LocalDateTimeOrTimestamp }
import lila.common.Form.{ cleanText, formatter, into, stringIn, LocalDateTimeOrTimestamp, partial }
import lila.core.perm.Granter
import lila.relay.RelayRound.Sync
import lila.relay.RelayRound.Sync.Upstream
Expand Down Expand Up @@ -64,10 +64,12 @@ final class RelayRoundForm(using mode: Mode):
"syncUsers" -> optional(of[Upstream.Users]),
"startsAt" -> optional(LocalDateTimeOrTimestamp(tour.info.timeZoneOrDefault).mapping),
"startsAfterPrevious" -> optional(boolean),
"finished" -> optional(boolean),
"period" -> optional(number(min = 2, max = 60).into[Seconds]),
"delay" -> optional(number(min = 0, max = RelayDelay.maxSeconds.value).into[Seconds]),
"onlyRound" -> optional(number(min = 1, max = 999)),
"status" -> optional:
text.partial[Data.Status](_.toString):
case ok: Data.Status => ok,
"period" -> optional(number(min = 2, max = 60).into[Seconds]),
"delay" -> optional(number(min = 0, max = RelayDelay.maxSeconds.value).into[Seconds]),
"onlyRound" -> optional(number(min = 1, max = 999)),
"slices" -> optional:
nonEmptyText.transform[List[RelayGame.Slice]](RelayGame.Slices.parse, RelayGame.Slices.show)
)(Data.apply)(unapply)
Expand Down Expand Up @@ -205,7 +207,7 @@ object RelayRoundForm:
syncUsers: Option[Upstream.Users] = None,
startsAt: Option[Instant] = None,
startsAfterPrevious: Option[Boolean] = None,
finished: Option[Boolean] = None,
status: Option[Data.Status] = None,
period: Option[Seconds] = None,
delay: Option[Seconds] = None,
onlyRound: Option[Int] = None,
Expand All @@ -230,7 +232,10 @@ object RelayRoundForm:
caption = if Granter(_.StudyAdmin) then caption else relay.caption,
sync = if relay.sync.playing then sync.play(official) else sync,
startsAt = relayStartsAt,
finishedAt = finished.orZero.option(relay.finishedAt.|(nowInstant))
startedAt = status.fold(relay.startedAt):
case "new" => none
case _ => relay.startedAt.orElse(nowInstant.some),
finishedAt = status.has("finished").option(relay.finishedAt.|(nowInstant))
)

private def makeSync(prev: Option[RelayRound.Sync])(using Me): Sync =
Expand All @@ -254,13 +259,15 @@ object RelayRoundForm:
sync = makeSync(none),
createdAt = nowInstant,
crowd = none,
finishedAt = (~finished).option(nowInstant),
startsAt = relayStartsAt,
startedAt = none
startedAt = if status.has("new") then none else nowInstant.some,
finishedAt = status.has("finished").option(nowInstant)
)

object Data:

type Status = "new" | "started" | "finished"

def make(relay: RelayRound) =
Data(
name = relay.name,
Expand All @@ -283,7 +290,11 @@ object RelayRoundForm:
case users: Upstream.Users => users,
startsAt = relay.startsAtTime,
startsAfterPrevious = relay.startsAfterPrevious.option(true),
finished = relay.isFinished.option(true),
status = some:
if relay.isFinished then "finished"
else if relay.hasStarted then "started"
else "new"
,
period = relay.sync.period,
onlyRound = relay.sync.onlyRound,
slices = relay.sync.slices,
Expand Down
13 changes: 8 additions & 5 deletions modules/relay/src/main/ui/RelayFormUi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -338,12 +338,15 @@ final class RelayFormUi(helpers: Helpers, ui: RelayUi, tourUi: RelayTourUi):
).some,
half = true
)(form3.input(_, typ = "number")),
form3.checkbox(
form("finished"),
trb.completed(),
help = trb.completedHelp().some,
form3.group(
form("status"),
"Current status",
help = frag(
"Lichess can usually detect the round status, but you can also set it manually if needed."
).some,
half = true
)
):
form3.select(_, Seq("new" -> "New", "started" -> "Started", "finished" -> "Finished"))
)
),
Granter
Expand Down
10 changes: 8 additions & 2 deletions modules/round/src/main/Drawer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package lila.round

import chess.Centis
import play.api.i18n.Lang
import monocle.syntax.all.*

import lila.common.Bus
import lila.core.i18n.{ I18nKey as trans, Translator, defaultLang }
import lila.game.GameExt.playerCanOfferDraw
import lila.game.GameExt.*
import lila.game.{ Event, Progress }
import lila.pref.{ Pref, PrefApi }

Expand Down Expand Up @@ -51,7 +52,7 @@ final private[round] class Drawer(
Messenger.SystemMessage.Persistent(trans.site.drawOfferAccepted.txt()).some
)
case Pov(g, color) if g.playerCanOfferDraw(color) =>
val progress = Progress(g).map { _.offerDraw(color) }
val progress = Progress(g).map(offerDraw(color))
messenger.system(g, color.fold(trans.site.whiteOffersDraw, trans.site.blackOffersDraw).txt())
for
_ <- proxy.save(progress)
Expand Down Expand Up @@ -85,6 +86,11 @@ final private[round] class Drawer(

def force(game: Game)(using GameProxy): Fu[Events] = finisher.other(game, _.Draw, None, None)

private def offerDraw(color: Color)(game: Game) = game
.updatePlayer(color, _.copy(isOfferingDraw = true))
.focus(_.metadata.drawOffers)
.modify(_.add(color, game.ply))

private def publishDrawOffer(game: Game): Unit = if game.nonAi then
if game.isCorrespondence then
Bus.publish(
Expand Down
2 changes: 1 addition & 1 deletion modules/web/src/main/AssetManifest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ final class AssetManifest(environment: Environment, net: NetConfig, ws: Standalo
.map { (k, asset) =>
val hash = (asset \ "hash").as[String]
val name = k.substring(k.lastIndexOf('/') + 1)
val extPos = name.indexOf('.')
val extPos = name.lastIndexOf('.')
val hashedName =
if extPos < 0 then s"${name}.$hash"
else s"${name.slice(0, extPos)}.$hash${name.substring(extPos)}"
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@
"url": "https://github.com/lichess-org/lila/issues"
},
"homepage": "https://lichess.org",
"packageManager": "pnpm@10.0.0+sha512.b8fef5494bd3fe4cbd4edabd0745df2ee5be3e4b0b8b08fa643aa3e4c6702ccc0f00d68fa8a8c9858a735a0032485a44990ed2810526c875e416f001b17df12b",
"packageManager": "pnpm@10.2.1+sha512.398035c7bd696d0ba0b10a688ed558285329d27ea994804a52bad9167d8e3a72bcb993f9699585d3ca25779ac64949ef422757a6c31102c12ab932e5cbe5cc92",
"engines": {
"node": ">=22.6",
"pnpm": "^10"
},
"dependencies": {
"@types/lichess": "workspace:*",
"@types/web": "^0.0.194",
"@typescript-eslint/eslint-plugin": "^8.20.0",
"@typescript-eslint/parser": "^8.20.0",
"@types/web": "^0.0.201",
"@typescript-eslint/eslint-plugin": "^8.23.0",
"@typescript-eslint/parser": "^8.23.0",
"ab": "github:lichess-org/ab-stub",
"chessground": "^9.1.1",
"chessops": "^0.14.2",
"eslint": "^9.18.0",
"lint-staged": "^15.3.0",
"eslint": "^9.20.0",
"lint-staged": "^15.4.3",
"onchange": "^7.1.0",
"prettier": "^3.4.2",
"snabbdom": "3.5.1",
Expand Down
Loading

0 comments on commit 30dbd1b

Please sign in to comment.