Merge pull request #18745 from kraktus/bulk_puz_api_theme

Puzzles: add batch theme vote API
This commit is contained in:
Thibault Duplessis
2025-11-26 08:45:48 +01:00
committed by GitHub
6 changed files with 66 additions and 6 deletions
+2 -2
View File
@@ -276,10 +276,10 @@ abstract private[controllers] class LilaController(val env: Env)
}
/* OAuth requests requiring certain permissions, with a body */
def SecuredScopedBody(perm: Permission.Selector)(
def SecuredScopedBody(perm: Permission.Selector)(scopes: OAuthScope.Selector*)(
f: BodyContext[?] ?=> Me ?=> Fu[Result]
) =
ScopedBody() { _ ?=> _ ?=>
ScopedBody(scopes*) { _ ?=> _ ?=>
IfGranted(perm)(f)
}
+25
View File
@@ -22,6 +22,7 @@ import lila.rating.PerfType
import lila.ui.LangPath
import scalalib.model.Days
import lila.common.HTTPRequest
import lila.common.Json.given
final class Puzzle(env: Env, apiC: => Api) extends LilaController(env):
@@ -156,6 +157,30 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env):
)
}
def apiBatchVoteThemes = SecuredScopedBody(_.PuzzleCurator)(_.Puzzle.Write) { _ ?=> me ?=>
bindForm(env.puzzle.forms.batchVotes)(
jsonFormError,
_.votes
.sequentially(puzzleVotes =>
puzzleVotes.themes
.sequentially: themeVote =>
allow:
env.puzzle.api.theme
.vote(puzzleVotes.puzzleId, themeVote.theme, themeVote.vote)
.inject(none)
.rescue: err =>
fuccess(Json.obj("theme" -> themeVote.theme, "msg" -> err.message).some)
.map:
_.flatten.map: errors =>
Json.obj("puzzleId" -> puzzleVotes.puzzleId, "errors" -> errors)
)
.map(_.flatten)
.map:
case Nil => jsonOkResult
case errors => BadRequest(jsonError(errors))
)
}
def voteTheme(id: PuzzleId, themeStr: String) = AuthOrScopedBody(_.Puzzle.Write) { _ ?=> me ?=>
NoBot:
import lila.puzzle.PuzzleTheme.VoteError.*
+1
View File
@@ -716,6 +716,7 @@ GET /api/puzzle/activity controllers.Puzzle.activity
GET /api/puzzle/dashboard/$days<\d+> controllers.Puzzle.apiDashboard(days: Days)
GET /api/puzzle/$id<\w{5}> controllers.Puzzle.apiShow(id: PuzzleId)
GET /api/puzzle/next controllers.Puzzle.apiNext
POST /api/puzzle/vote-themes controllers.Puzzle.apiBatchVoteThemes
GET /api/puzzle/batch/:angle controllers.Puzzle.apiBatchSelect(angle)
POST /api/puzzle/batch/:angle controllers.Puzzle.apiBatchSolve(angle)
GET /api/puzzle/replay/$days<\d+>/:theme controllers.Puzzle.apiReplay(days: Days, theme)
+28
View File
@@ -23,6 +23,18 @@ object PuzzleForm:
):
def streakPuzzleId = streakId.flatMap(Puzzle.toId)
case class ThemeVote(
theme: String,
vote: Option[Boolean]
)
case class ThemesVote(
puzzleId: PuzzleId,
themes: List[ThemeVote]
)
case class BatchThemesVotes(votes: List[ThemesVote])
val round = Form(
mapping(
"win" -> of[PuzzleWin],
@@ -46,6 +58,22 @@ object PuzzleForm:
single("vote" -> optional(boolean))
)
lazy val batchVotes = Form:
mapping(
"votes" -> list(
mapping(
"puzzleId" -> nonEmptyText.into[PuzzleId],
"themes" -> list(
mapping(
"theme" -> nonEmptyText,
"vote" -> optional(boolean)
)(ThemeVote.apply)(unapply)
).verifying("At least one theme", _.nonEmpty)
.verifying("No more than 500", _.sizeIs <= 500)
)(ThemesVote.apply)(unapply)
)
)(BatchThemesVotes.apply)(_.votes.some)
val difficulty = Form(
single("difficulty" -> stringIn(PuzzleDifficulty.all.map(_.key).toSet))
)
@@ -18,6 +18,9 @@ object PuzzleTheme:
enum VoteError:
case Fail(msg: String) extends VoteError
case Unchanged extends VoteError
def message: String = this match
case Fail(msg) => msg
case Unchanged => "unchanged"
val mix = PuzzleTheme(i.mix, i.mixDescription)
val advancedPawn = PuzzleTheme(i.advancedPawn, i.advancedPawnDescription)
+7 -4
View File
@@ -13,13 +13,13 @@ import lila.common.HTTPRequest
import lila.core.id.SessionId
import lila.core.mod.{ LoginWithBlankedPassword, LoginWithWeakPassword }
import lila.core.email.UserStrOrEmail
import lila.core.perm.Permission
import lila.core.net.{ ApiVersion, IpAddress }
import lila.core.misc.oauth.AccessTokenId
import lila.core.security.{ ClearPassword, FingerHash, Ip2ProxyApi, IsProxy }
import lila.db.dsl.{ *, given }
import lila.oauth.{ OAuthScope, OAuthServer }
import lila.security.LoginCandidate.Result
import lila.core.user.RoleDbKey
final class SecurityApi(
userRepo: lila.user.UserRepo,
@@ -174,8 +174,6 @@ final class SecurityApi(
val mobile = Mobile.LichessMobileUa.parse(req)
store.upsertOAuth(access.me.userId, access.tokenId, mobile, req)
private lazy val nonModRoles: Set[RoleDbKey] = lila.core.perm.Permission.nonModPermissions.map(_.dbKey)
private def stripRolesOfOAuthUser(scoped: OAuthScope.Scoped) =
if scoped.scopes.has(_.Web.Mod) then scoped
else scoped.copy(me = stripRolesOf(scoped.me))
@@ -186,7 +184,12 @@ final class SecurityApi(
private def stripRolesOf(me: Me) =
if me.roles.nonEmpty
then me.map(_.copy(roles = me.roles.filter(nonModRoles.contains)))
then
def expandWithoutModPerms(perm: Permission): List[Permission] =
if Permission.nonModPermissions.contains(perm)
then perm :: perm.alsoGrants.flatMap(expandWithoutModPerms)
else perm.alsoGrants.flatMap(expandWithoutModPerms)
me.map(_.copy(roles = Permission(me).flatMap(expandWithoutModPerms).toSet.map(_.dbKey).toList))
else me
def locatedOpenSessions(userId: UserId, nb: Int): Fu[List[LocatedSession]] =