Skip to content

Commit

Permalink
Merge branch 'master' into insufficient-material-draw-claims
Browse files Browse the repository at this point in the history
  • Loading branch information
johndoknjas authored Feb 19, 2025
2 parents 410472c + c5446f8 commit c91c2d9
Show file tree
Hide file tree
Showing 117 changed files with 828 additions and 291 deletions.
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = "3.8.6"
version = "3.9.0"
runner.dialect = scala3

align.preset = more
Expand Down
1 change: 1 addition & 0 deletions app/controllers/Account.scala
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ final class Account(
withFollows = apiC.userWithFollows,
withTrophies = false,
withCanChallenge = false,
withPlayban = getBool("playban"),
forWiki = wikiGranted
)
.dmap { JsonOk(_) }
Expand Down
5 changes: 2 additions & 3 deletions app/controllers/PlayApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,8 @@ final class PlayApi(env: Env)(using akka.stream.Materializer) extends LilaContro
env.bot.player.claimVictory(pov).pipe(toResult)
case Array("game", id, "berserk") =>
as(GameAnyId(id).gameId): pov =>
fuccess:
if env.bot.player.berserk(pov.game) then jsonOkResult
else JsonBadRequest(jsonError("Cannot berserk"))
if !me.isBot && env.bot.player.berserk(pov.game) then jsonOkResult
else JsonBadRequest(jsonError("Cannot berserk"))
case _ => notFoundJson("No such command")

def boardCommandGet(cmd: String) = ScopedBody(_.Board.Play) { _ ?=> me ?=>
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/Tournament.scala
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e
}
}

def apiJoin(id: TourId) = ScopedBody(_.Tournament.Write) { ctx ?=> me ?=>
def apiJoin(id: TourId) = ScopedBody(_.Tournament.Write, _.Bot.Play) { ctx ?=> me ?=>
NoLame:
NoPlayban:
limit.tourJoin(me, rateLimited):
Expand All @@ -186,7 +186,7 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e
else Redirect(routes.Tournament.show(tour.id))
}

def apiWithdraw(id: TourId) = ScopedBody(_.Tournament.Write) { _ ?=> me ?=>
def apiWithdraw(id: TourId) = ScopedBody(_.Tournament.Write, _.Bot.Play) { _ ?=> me ?=>
Found(cachedTour(id)): tour =>
api.selfPause(tour.id, me).inject(jsonOkResult)
}
Expand Down
41 changes: 24 additions & 17 deletions app/views/user/show/header.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ object header:
private val dataToints = attr("data-toints")
private val dataTab = attr("data-tab")

private def possibleSeoBot(u: User) =
!u.isVerified && !u.hasTitle && u.count.game < 10 && (
u.profile.exists(_.links.isDefined) ||
u.profile.flatMap(_.bio).exists(_.contains("https://"))
)

def apply(u: User, info: UserInfo, angle: UserInfo.Angle, social: UserInfo.Social)(using ctx: Context) =
frag(
div(cls := "box__top user-show__header")(
Expand Down Expand Up @@ -174,24 +180,20 @@ object header:
else (ctx.is(u) && u.count.game < 10).option(ui.newPlayer(u)),
div(cls := "profile-side")(
div(cls := "user-infos")(
ctx
.isnt(u)
.option(
frag(
u.lame.option(
div(cls := "warning tos_warning")(
span(dataIcon := Icon.CautionCircle, cls := "is4"),
trans.site.thisAccountViolatedTos()
)
)
)
),
(u.lame && ctx.isnt(u)).option:
div(cls := "warning tos_warning")(
span(dataIcon := Icon.CautionCircle, cls := "is4"),
trans.site.thisAccountViolatedTos()
)
,
(ctx.kid.no && !hideTroll && !u.kid).option(
frag(
profile.nonEmptyRealName.map: name =>
strong(cls := "name")(name),
profile.nonEmptyBio.map: bio =>
p(cls := "bio")(richText(bio, nl2br = true))
profile.nonEmptyBio
.ifFalse(possibleSeoBot(u))
.map: bio =>
p(cls := "bio")(richText(bio, nl2br = true))
)
),
div(cls := "stats")(
Expand Down Expand Up @@ -228,9 +230,14 @@ object header:
),
(!hideTroll && !u.kid).option(
div(cls := "social_links col2")(
profile.actualLinks.nonEmpty.option(strong(trans.site.socialMediaLinks())),
profile.actualLinks.map: link =>
a(href := link.url, targetBlank, noFollow, relMe)(link.site.name)
profile.actualLinks.some
.filter(_.nonEmpty && !possibleSeoBot(u))
.map: links =>
frag(
strong(trans.site.socialMediaLinks()),
links.map: link =>
a(href := link.url, targetBlank, noFollow, relMe)(link.site.name)
)
)
),
(ctx.is(u) || !u.kid).option(
Expand Down
2 changes: 1 addition & 1 deletion modules/api/src/main/AccountTermination.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ final class AccountTermination(
_ <- streamerApi.demote(u.id)
reports <- reportApi.processAndGetBySuspect(lila.report.Suspect(u))
_ <-
if selfClose then modLogApi.selfCloseAccount(u.id, reports)
if selfClose then modLogApi.selfCloseAccount(u.id, forever, reports)
else if teacherClose then modLogApi.teacherCloseAccount(u.id)
else modLogApi.closeAccount(u.id)
_ <- appealApi.onAccountClose(u)
Expand Down
10 changes: 8 additions & 2 deletions modules/api/src/main/UserApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ final class UserApi(
shieldApi: lila.tournament.TournamentShieldApi,
revolutionApi: lila.tournament.RevolutionApi,
challengeGranter: lila.challenge.ChallengeGranter,
playbanApi: lila.playban.PlaybanApi,
net: NetConfig
)(using Executor, lila.core.i18n.Translator):

Expand All @@ -53,6 +54,7 @@ final class UserApi(
withFollows: Boolean,
withTrophies: Boolean,
withCanChallenge: Boolean,
withPlayban: Boolean = false,
forWiki: Boolean = false
)(using as: Option[Me], lang: Lang): Fu[JsObject] =
u.match
Expand All @@ -74,7 +76,8 @@ final class UserApi(
(withTrophies && !u.lame).soFu(getTrophiesAndAwards(u.user)),
streamerApi.listed(u.user),
withCanChallenge.so(challengeGranter.mayChallenge(u.user).dmap(some)),
forWiki.soFu(userRepo.email(u.id))
forWiki.soFu(userRepo.email(u.id)),
withPlayban.so(playbanApi.currentBan(u))
).mapN:
(
gameOption,
Expand All @@ -88,7 +91,8 @@ final class UserApi(
trophiesAndAwards,
streamer,
canChallenge,
email
email,
playban
) =>
jsonView.full(u.user, u.perfs.some, withProfile = true) ++ {
Json
Expand All @@ -111,13 +115,15 @@ final class UserApi(
"me" -> nbGamesWithMe
)
)
.add("kid", u.kid)
.add("email", email)
.add("groups", forWiki.option(wikiGroups(u.user)))
.add("streaming", liveStreamApi.isStreaming(u.id))
.add("nbFollowing", following)
.add("nbFollowers", withFollows.option(0))
.add("trophies", trophiesAndAwards.map(trophiesJson))
.add("canChallenge", canChallenge)
.add("playban", playban)
.add(
"streamer",
streamer.map: s =>
Expand Down
4 changes: 2 additions & 2 deletions modules/gathering/src/main/Condition.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ object Condition:

case class Bots(allowed: Boolean) extends Condition with FlatCond:
def name(pt: PerfType)(using Translate) =
if allowed then "Bot players are allowed."
else "Bot players are not allowed."
if allowed then "Bot players are allowed"
else "Bot players are not allowed"
def apply(pt: PerfType)(using me: Me, perf: Perf) =
if me.isBot && !allowed then Refused(name(pt)(using _)) else Accepted

Expand Down
6 changes: 4 additions & 2 deletions modules/mod/src/main/ModlogApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,14 @@ final class ModlogApi(repo: ModlogRepo, userRepo: UserRepo, ircApi: IrcApi, pres
def teacherCloseAccount(user: UserId)(using me: Me) = add:
Modlog(me, user.some, Modlog.teacherCloseAccount)

def selfCloseAccount(user: UserId, openReports: List[Report]) = add:
def selfCloseAccount(user: UserId, forever: Boolean, openReports: List[Report]) = add:
Modlog(
UserId.lichess.into(ModId),
user.some,
Modlog.selfCloseAccount,
details = openReports.map(r => s"${r.room.name} report").mkString(", ").some.filter(_.nonEmpty)
details = {
forever.so("forever ") + openReports.map(r => s"${r.room.name} report").mkString(", ")
}.some.filter(_.nonEmpty)
)

def closedByMod(user: User): Fu[Boolean] =
Expand Down
8 changes: 3 additions & 5 deletions modules/puzzle/src/main/PuzzleTrust.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ final private class PuzzleTrustApi(colls: PuzzleColls, userApi: lila.core.user.U
user.perfs.puzzle.glicko.establishedIntRating.fold(fuccess(-2)): userRating =>
colls
.puzzle(_.primitiveOne[Float]($id(round.id.puzzleId), s"${Puzzle.BSONFields.glicko}.r"))
.map {
_.fold(-2) { puzzleRating =>
.map:
_.fold(-2): puzzleRating =>
(math.abs(puzzleRating - userRating.value) > 300).so(-4)
}
}
.dmap(w +)
.dmap(w + _)
.dmap(_.some.filter(0 <))

def theme(user: User): Fu[Option[Int]] =
Expand Down
8 changes: 8 additions & 0 deletions modules/relay/src/main/BSONHandlers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ object BSONHandlers:
given BSONHandler[List[RelayGame.Slice]] =
stringIsoHandler[List[RelayGame.Slice]](using RelayGame.Slices.iso)

given BSONHandler[Sync.OnlyRound] = quickHandler[Sync.OnlyRound](
{
case BSONString(s) => Left(s)
case BSONInteger(i) => Right(i)
},
_.fold(BSONString(_), BSONInteger(_))
)

given BSONDocumentHandler[Sync] = Macros.handler

import RelayRound.Starts
Expand Down
3 changes: 3 additions & 0 deletions modules/relay/src/main/JsonView.scala
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ object JsonView:
)

import RelayRound.Sync

private given Writes[Sync.OnlyRound] = Writes(r => r.fold(JsString(_), JsNumber(_)))

private given OWrites[Sync] = OWrites: s =>
Json
.obj(
Expand Down
29 changes: 4 additions & 25 deletions modules/relay/src/main/RelayGame.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,6 @@ case class RelayGame(
override def toString =
s"RelayGame ${root.mainlineNodeList.size} ${tags.outcome} ${tags.names} ${tags.fideIds}"

// We don't use tags.boardNumber.
// Organizers change it at any time while reordering the boards.
def isSameGame(otherTags: Tags): Boolean =
isSameLichessGame(otherTags) || {
allSame(otherTags, RelayGame.eventTags) &&
otherTags.roundNumber == tags.roundNumber &&
playerTagsMatch(otherTags)
}

private def isSameLichessGame(otherTags: Tags) =
~(tags(_.GameId), otherTags(_.GameId)).mapN(_ == _)

private def playerTagsMatch(otherTags: Tags): Boolean =
val bothHaveFideIds = List(otherTags, tags).forall: ts =>
RelayGame.fideIdTags.forall(side => ts(side).exists(_ != "0"))
if bothHaveFideIds
then allSame(otherTags, RelayGame.fideIdTags)
else allSame(otherTags, RelayGame.nameTags)

private def allSame(otherTags: Tags, tagNames: RelayGame.TagNames) = tagNames.forall: tag =>
otherTags(tag) == tags(tag)

def isEmpty = tags.value.isEmpty && root.children.nodes.isEmpty

def hasMoves = root.children.nodes.nonEmpty
Expand Down Expand Up @@ -150,9 +128,10 @@ private object RelayGame:
mul => RelayFetch.multiPgnToGames.either(mul).fold(e => throw e, identity)
)

def filter(onlyRound: Option[Int])(games: RelayGames): RelayGames =
onlyRound.fold(games): round =>
games.filter(_.tags.roundNumber.has(round))
def filter(onlyRound: Option[Either[String, Int]])(games: RelayGames): RelayGames =
onlyRound.fold(games):
case Left(r) => games.filter(_.tags(_.Round).has(r))
case Right(r) => games.filter(_.tags.roundNumber.has(r))

// 1-indexed, both inclusive
case class Slice(from: Int, to: Int)
Expand Down
19 changes: 13 additions & 6 deletions modules/relay/src/main/RelayRound.scala
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ object RelayRound:
case AfterPrevious

case class Sync(
upstream: Option[Sync.Upstream], // if empty, needs a client to push PGN
until: Option[Instant], // sync until then; resets on move
nextAt: Option[Instant], // when to run next sync
period: Option[Seconds], // override time between two sync (rare)
delay: Option[Seconds], // add delay between the source and the study
onlyRound: Option[Int], // only keep games with [Round "x"]
upstream: Option[Sync.Upstream], // if empty, needs a client to push PGN
until: Option[Instant], // sync until then; resets on move
nextAt: Option[Instant], // when to run next sync
period: Option[Seconds], // override time between two sync (rare)
delay: Option[Seconds], // add delay between the source and the study
onlyRound: Option[Sync.OnlyRound], // only keep games with [Round "x"]
slices: Option[List[RelayGame.Slice]] = none,
log: SyncLog
):
Expand Down Expand Up @@ -134,6 +134,13 @@ object RelayRound:

object Sync:

// either an Int round number,
// or a string that matches exactly the Round tag
type OnlyRound = Either[String, Int]
object OnlyRound:
def toString(r: OnlyRound) = r.fold(identity, _.toString)
def parse(s: String) = s.toIntOption.fold(Left(s))(Right(_))

object url:
private val lccRegex = """view\.livechesscloud\.com/?#?([0-9a-f\-]+)/(\d+)""".r.unanchored
extension (url: URL)
Expand Down
20 changes: 14 additions & 6 deletions modules/relay/src/main/RelayRoundForm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ 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, partial }
import lila.common.Form.{
cleanText,
cleanNonEmptyText,
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 @@ -69,7 +77,7 @@ final class RelayRoundForm(using mode: Mode):
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)),
"onlyRound" -> optional(cleanNonEmptyText(maxLength = 50)),
"slices" -> optional:
nonEmptyText.transform[List[RelayGame.Slice]](RelayGame.Slices.parse, RelayGame.Slices.show)
)(Data.apply)(unapply)
Expand Down Expand Up @@ -144,7 +152,7 @@ object RelayRoundForm:
startsAfterPrevious = prev.exists(_.startsAfterPrevious).option(true),
period = prev.flatMap(_.sync.period),
delay = prev.flatMap(_.sync.delay),
onlyRound = prev.flatMap(_.sync.onlyRound).map(_ + 1),
onlyRound = prev.flatMap(_.sync.onlyRound).map(_.map(_ + 1)).map(Sync.OnlyRound.toString),
slices = prev.flatMap(_.sync.slices)
)

Expand Down Expand Up @@ -211,7 +219,7 @@ object RelayRoundForm:
status: Option[Data.Status] = None,
period: Option[Seconds] = None,
delay: Option[Seconds] = None,
onlyRound: Option[Int] = None,
onlyRound: Option[String] = None,
slices: Option[List[RelayGame.Slice]] = None
):
def upstream: Option[Upstream] = syncSource.match
Expand Down Expand Up @@ -246,7 +254,7 @@ object RelayRoundForm:
nextAt = none,
period = if Granter(_.StudyAdmin) then period else prev.flatMap(_.period),
delay = delay,
onlyRound = onlyRound.ifFalse(upstream.exists(_.isInternal)),
onlyRound = onlyRound.ifFalse(upstream.exists(_.isInternal)).map(Sync.OnlyRound.parse),
slices = slices,
log = SyncLog.empty
)
Expand Down Expand Up @@ -297,7 +305,7 @@ object RelayRoundForm:
else "new"
,
period = relay.sync.period,
onlyRound = relay.sync.onlyRound,
onlyRound = relay.sync.onlyRound.map(Sync.OnlyRound.toString),
slices = relay.sync.slices,
delay = relay.sync.delay
)
Loading

0 comments on commit c91c2d9

Please sign in to comment.