Files
Thibault Duplessis a59986b531 rename Polygon -> Takex3
but keep the `web:polygon` oauth scope untouched for backward compat
2026-04-06 12:49:13 +02:00

242 lines
9.7 KiB
Scala

package controllers
import chess.format.Fen
import play.api.libs.json.Json
import play.api.mvc.{ EssentialAction, Result }
import lila.app.{ *, given }
import lila.common.HTTPRequest
import lila.core.socket.Sri
import lila.game.AnonCookie
import lila.setup.Processor.HookResult
import lila.setup.ValidFen
final class Setup(
env: Env,
challengeC: => Challenge
) extends LilaController(env)
with lila.web.TheftPrevention:
import env.setup.{ forms, processor }
def ai = OpenBody:
limit.setupBotAi(ctx.userId | UserId(""), rateLimited, cost = ctx.me.exists(_.isBot).so(1)):
limit.setupPost(ctx.ip, rateLimited):
bindForm(forms.ai)(
doubleJsonFormError,
config =>
processor.ai(config).flatMap { pov =>
negotiateApi(
html = redirectPov(pov),
api = _ => env.api.roundApi.player(pov, scalalib.data.Preload.none, none).map(Created(_))
)
}
)
def friend(userId: Option[UserStr]) =
OpenBody: ctx ?=>
limit.setupPost(ctx.ip, rateLimited):
bindForm(forms.friend)(
doubleJsonFormError,
config =>
for
origUser <- ctx.user.traverse(env.user.perfsRepo.withPerf(_, config.perfType))
destUser <- userId.so(env.user.api.enabledWithPerf(_, config.perfType))
denied <- destUser.so(u => env.challenge.granter.isDenied(u.user, config.perfKey.some))
result <- denied match
case Some(denied) =>
val message = lila.challenge.ChallengeDenied.translated(denied)
negotiate(
// 403 tells setupCtrl.ts to close the setup modal
forbiddenJson(message), // TODO test
JsonBadRequest(message)
)
case None =>
import lila.challenge.Challenge.*
(origUser, ctx.req.sid)
.match
case (Some(orig), _) => toRegistered(orig).some
case (_, Some(sid)) => Challenger.Anonymous(sid).some
case _ if HTTPRequest.isLichobile(ctx.req) => Challenger.Open.some
case _ => none
.so: challenger =>
val timeControl = makeTimeControl(config.makeClock, config.makeDaysPerTurn)
val challenge = lila.challenge.Challenge.make(
variant = config.variant,
initialFen = config.fen,
timeControl = timeControl,
rated = config.rated,
color = config.color.name,
challenger = challenger,
destUser = destUser,
rematchOf = none
)
env.challenge.api
.create(challenge)
.flatMap:
if _ then
negotiate(
Redirect(routes.Round.watcher(challenge.gameId, Color.white)),
challengeC.showChallenge(challenge, justCreated = true)
)
else
negotiate(
Redirect(routes.Lobby.home),
JsonBadRequest("Challenge not created")
)
yield result
)
private def hookResponse(res: HookResult) = res match
case HookResult.Created(id) =>
JsonOk:
Json.obj(
"ok" -> true,
"hook" -> Json.obj("id" -> id)
)
case HookResult.Refused => JsonBadRequest(("Game was not created"))
def hook(sri: Sri) = OpenOrScopedBody(parse.anyContent)(_.Web.Mobile, _.Web.Takex3): ctx ?=>
NoBot:
NoPlaybanOrCurrent:
bindForm(forms.hook)(
doubleJsonFormError,
userConfig =>
limit.setupPost(req.ipAddress, rateLimited):
limit.setupAnonHook(req.ipAddress, rateLimited, cost = ctx.isAnon.so(1)):
for
me <- ctx.user.traverse(env.user.api.withPerfs)
given Perf = me.fold(lila.rating.Perf.default)(_.perfs(userConfig.perfType))
blocking <- ctx.userId.so(env.relation.api.fetchBlocking)
res <- processor.hook(
userConfig.withinLimits,
sri,
req.sid,
lila.core.pool.Blocking(blocking)
)(using me)
yield hookResponse(res)
)
def like(sri: Sri, gameId: GameId) = Open:
NoBot:
limit.setupPost(ctx.ip, rateLimited):
NoPlaybanOrCurrent:
Found(env.game.gameRepo.game(gameId)): game =>
for
orig <- ctx.user.traverse(env.user.api.withPerfs)
blocking <- ctx.userId.so(env.relation.api.fetchBlocking)
hookConfig = lila.setup.HookConfig.default(ctx.isAuth)
hookConfigWithRating = get("rr")
.fold(
hookConfig.withRatingRange(
orig.fold(lila.rating.Perf.default)(_.perfs(game.perfKey)).intRating.some,
get("deltaMin"),
get("deltaMax")
)
)(hookConfig.withRatingRange)
.updateFrom(game)
allBlocking = lila.core.pool.Blocking(blocking ++ game.userIds)
hookResult <- processor.hook(hookConfigWithRating, sri, ctx.req.sid, allBlocking)(using orig)
yield hookResponse(hookResult)
def boardApiHook = WithBoardApiHookAuthor { (author, reqSri) => ctx ?=>
forms
.boardApiHook:
ctx.isMobileOauth || ctx.isTakex3 || (ctx.isAnon && HTTPRequest.isLichessMobile(ctx.req))
.bindFromRequest()
.fold(
doubleJsonFormError,
config =>
for
me <- ctx.me.so(env.user.api.withPerfs)
blocking <- ctx.me.so(env.relation.api.fetchBlocking(_))
sri = orUserSri(author)
_ = lila.mon.lobby.hook
.apiCreate:
if ctx.isMobileOauth then env.oAuth.signedClients.mobile.clientId.value
else if ctx.isTakex3 then env.oAuth.signedClients.takex3.clientId.value
else "other"
.increment()
forcedColor <- env.lobby.boardApiHookStream.mustPlayAsColor(config.color)
res <- forcedColor.match
case Some(forced) => fuccess(JsonBadRequest(s"You must also play some games as $forced"))
case None =>
config
.hook(reqSri | sri, me, sid = sri.value.some, lila.core.pool.Blocking(blocking))
.match
case Left(hook) =>
limit.setupPost(req.ipAddress, rateLimited):
limit
.boardApiConcurrency(author.map(_.id), msg = req.userAgent.value)(
env.lobby.boardApiHookStream(hook.copy(boardApi = true))
)(jsOptToNdJson)
.toFuccess
case Right(Some(seek)) =>
author match
case Left(_) => JsonBadRequest("Anonymous users cannot create seeks").toFuccess
case Right(me) =>
env.setup.processor.createSeekIfAllowed(seek, me.id).map {
case HookResult.Refused => JsonBadRequest("Already playing too many games")
case HookResult.Created(id) => Ok(Json.obj("id" -> id))
}
case Right(None) => notFoundJson().toFuccess
yield res
)
}
def boardApiHookCancel = WithBoardApiHookAuthor { (author, reqSri) => _ ?=>
env.lobby.boardApiHookStream.cancel(orUserSri(author), reqSri)
NoContent
}
private def orUserSri(author: Either[Sri, lila.user.User]): Sri =
author.fold(identity, u => Sri(s"user:${u.id}"))
private def WithBoardApiHookAuthor(
f: (Either[Sri, lila.user.User], Option[Sri]) => BodyContext[?] ?=> Fu[Result]
): EssentialAction =
AnonOrScopedBody(parse.anyContent)(_.Board.Play, _.Web.Mobile, _.Web.Takex3): ctx ?=>
NoBot:
val reqSri = getAs[Sri]("sri")
ctx.me match
case Some(u) => f(Right(u), reqSri)
case None =>
reqSri match
case Some(sri) => f(Left(sri), reqSri)
case None => JsonBadRequest("Authentication required")
def filterForm = Open:
Ok.snip(views.setup.filter(forms.filter))
def validateFen = Open:
(get("fen").map(Fen.Full.clean): Option[Fen.Full]).flatMap(ValidFen(getBool("strict"))) match
case None => BadRequest
case Some(v) => Ok.snip(views.analyse.ui.miniSpan(v.fen.board, v.color))
def apiAi = ScopedBody(_.Challenge.Write, _.Bot.Play, _.Board.Play, _.Web.Mobile, _.Web.Takex3) {
ctx ?=> me ?=>
limit.setupBotAi(me, rateLimited, cost = me.isBot.so(1)):
limit.setupPost(req.ipAddress, rateLimited):
bindForm(forms.api.ai)(
doubleJsonFormError,
config =>
processor.apiAi(config).map { pov =>
val json = env.game.jsonView.apiAiNewGame(pov, config.fen)
Created(json).as(JSON)
}
)
}
private[controllers] def redirectPov(pov: Pov)(using ctx: Context) =
val redir = Redirect(routes.Round.watcher(pov.gameId, Color.white))
if ctx.isAuth then redir
else
redir.withCookies(
env.security.lilaCookie.cookie(
AnonCookie.name,
pov.playerId.value,
maxAge = AnonCookie.maxAge.some,
httpOnly = false.some
)
)