lila/app/controllers/Setup.scala

300 lines
10 KiB
Scala

package controllers
import play.api.data.Form
import play.api.libs.json.Json
import play.api.mvc.Results
import scala.concurrent.duration._
import chess.format.FEN
import lila.api.{ BodyContext, Context }
import lila.app._
import lila.common.{ HTTPRequest, IpAddress }
import lila.game.{ AnonCookie, Pov }
import lila.setup.Processor.HookResult
import lila.setup.ValidFen
import lila.socket.Socket.Sri
import views._
final class Setup(
env: Env,
challengeC: => Challenge,
apiC: => Api
) extends LilaController(env)
with TheftPrevention {
private def forms = env.setup.forms
private def processor = env.setup.processor
private[controllers] val PostRateLimit = new lila.memo.RateLimit[IpAddress](
5,
1.minute,
name = "setup post",
key = "setup_post",
enforce = env.net.rateLimit.value
)
def aiForm =
Open { implicit ctx =>
if (HTTPRequest isXhr ctx.req) {
fuccess(forms aiFilled get("fen").map(FEN)) map { form =>
html.setup.forms.ai(
form,
env.fishnet.aiPerfApi.intRatings,
form("fen").value flatMap ValidFen(getBool("strict"))
)
}
} else Redirect(s"${routes.Lobby.home()}#ai").fuccess
}
def ai =
process(_ => forms.ai) { config => implicit ctx =>
processor ai config
}
def friendForm(userId: Option[String]) =
Open { implicit ctx =>
if (HTTPRequest isXhr ctx.req)
fuccess(forms friendFilled get("fen").map(FEN)) flatMap { form =>
val validFen = form("fen").value flatMap ValidFen(false)
userId ?? env.user.repo.named flatMap {
case None => Ok(html.setup.forms.friend(form, none, none, validFen)).fuccess
case Some(user) =>
env.challenge.granter(ctx.me, user, none) map {
case Some(denied) => BadRequest(lila.challenge.ChallengeDenied.translated(denied))
case None => Ok(html.setup.forms.friend(form, user.some, none, validFen))
}
}
}
else
fuccess {
Redirect(s"${routes.Lobby.home()}#friend")
}
}
def friend(userId: Option[String]) =
OpenBody { implicit ctx =>
implicit val req = ctx.body
PostRateLimit(HTTPRequest lastRemoteAddress ctx.req) {
forms
.friend(ctx)
.bindFromRequest()
.fold(
err =>
negotiate(
html = keyPages.home(Results.BadRequest),
api = _ => jsonFormError(err)
),
config =>
userId ?? env.user.repo.enabledById flatMap { destUser =>
destUser ?? { env.challenge.granter(ctx.me, _, config.perfType) } flatMap {
case Some(denied) =>
val message = lila.challenge.ChallengeDenied.translated(denied)
negotiate(
html = BadRequest(html.site.message.challengeDenied(message)).fuccess,
api = _ => BadRequest(jsonError(message)).fuccess
)
case None =>
import lila.challenge.Challenge._
val timeControl = config.makeClock map {
TimeControl.Clock.apply
} orElse config.makeDaysPerTurn.map {
TimeControl.Correspondence.apply
} getOrElse TimeControl.Unlimited
val challenge = lila.challenge.Challenge.make(
variant = config.variant,
initialFen = config.fen,
timeControl = timeControl,
mode = config.mode,
color = config.color.name,
challenger = (ctx.me, HTTPRequest sid req) match {
case (Some(user), _) => toRegistered(config.variant, timeControl)(user)
case (_, Some(sid)) => Challenger.Anonymous(sid)
case _ => Challenger.Open
},
destUser = destUser,
rematchOf = none
)
(env.challenge.api create challenge) flatMap {
case true =>
negotiate(
html = fuccess(Redirect(routes.Round.watcher(challenge.id, "white"))),
api = _ => challengeC showChallenge challenge
)
case false =>
negotiate(
html = fuccess(Redirect(routes.Lobby.home())),
api = _ => fuccess(BadRequest(jsonError("Challenge not created")))
)
}
}
}
)
}(rateLimitedFu)
}
def hookForm =
Open { implicit ctx =>
NoBot {
if (HTTPRequest isXhr ctx.req) NoPlaybanOrCurrent {
fuccess(forms.hookFilled(timeModeString = get("time"))) map { html.setup.forms.hook(_) }
}
else
fuccess {
Redirect(s"${routes.Lobby.home()}#hook")
}
}
}
private def hookResponse(res: HookResult) =
res match {
case HookResult.Created(id) =>
Ok(
Json.obj(
"ok" -> true,
"hook" -> Json.obj("id" -> id)
)
) as JSON
case HookResult.Refused => BadRequest(jsonError("Game was not created"))
}
def hook(sri: String) =
OpenBody { implicit ctx =>
NoBot {
implicit val req = ctx.body
PostRateLimit(HTTPRequest lastRemoteAddress ctx.req) {
NoPlaybanOrCurrent {
forms
.hook(ctx)
.bindFromRequest()
.fold(
jsonFormError,
userConfig =>
(ctx.userId ?? env.relation.api.fetchBlocking) flatMap { blocking =>
processor.hook(
userConfig withinLimits ctx.me,
Sri(sri),
HTTPRequest sid req,
blocking
) map hookResponse
}
)
}
}(rateLimitedFu)
}
}
def like(sri: String, gameId: String) =
Open { implicit ctx =>
NoBot {
PostRateLimit(HTTPRequest lastRemoteAddress ctx.req) {
NoPlaybanOrCurrent {
env.game.gameRepo game gameId flatMap {
_ ?? { game =>
for {
blocking <- ctx.userId ?? env.relation.api.fetchBlocking
hookConfig = lila.setup.HookConfig.default withRatingRange get("rr") updateFrom game
sameOpponents = game.userIds
hookResult <-
processor
.hook(hookConfig, Sri(sri), HTTPRequest sid ctx.req, blocking ++ sameOpponents)
} yield hookResponse(hookResult)
}
}
}
}(rateLimitedFu)
}
}
private val BoardApiHookConcurrencyLimitPerUser = new lila.memo.ConcurrencyLimit[String](
name = "Board API hook Stream API concurrency per user",
key = "boardApiHook.concurrency.limit.user",
ttl = 10.minutes,
maxConcurrency = 1
)
def boardApiHook =
ScopedBody(_.Board.Play) { implicit req => me =>
implicit val lang = reqLang
if (me.isBot) notForBotAccounts.fuccess
else
forms.boardApiHook.bindFromRequest().fold(
newJsonFormError,
config =>
env.relation.api.fetchBlocking(me.id) flatMap { blocking =>
val uniqId = s"sri:${me.id}"
config.fixColor.hook(Sri(uniqId), me.some, sid = uniqId.some, blocking) match {
case Left(hook) =>
PostRateLimit(HTTPRequest lastRemoteAddress req) {
BoardApiHookConcurrencyLimitPerUser(me.id)(
env.lobby.boardApiHookStream(hook.copy(boardApi = true))
)(apiC.sourceToNdJsonOption).fuccess
}(rateLimitedFu)
case _ => BadRequest(jsonError("Invalid board API seek")).fuccess
}
}
)
}
def filterForm =
Open { implicit ctx =>
fuccess(html.setup.filter(forms.filter))
}
def validateFen =
Open { implicit ctx =>
get("fen") flatMap ValidFen(getBool("strict")) match {
case None => BadRequest.fuccess
case Some(v) => Ok(html.game.bits.miniBoard(v.fen, v.color)).fuccess
}
}
def apiAi =
ScopedBody(_.Challenge.Write, _.Bot.Play, _.Board.Play) { implicit req => me =>
implicit val lang = reqLang
PostRateLimit(HTTPRequest lastRemoteAddress req) {
forms.api.ai.bindFromRequest().fold(
jsonFormError,
config =>
processor.apiAi(config, me) map { pov =>
Created(env.game.jsonView(pov.game, config.fen)) as JSON
}
)
}(rateLimitedFu)
}
private def process[A](form: Context => Form[A])(op: A => BodyContext[_] => Fu[Pov]) =
OpenBody { implicit ctx =>
PostRateLimit(HTTPRequest lastRemoteAddress ctx.req) {
implicit val req = ctx.body
form(ctx).bindFromRequest().fold(
err =>
negotiate(
html = keyPages.home(Results.BadRequest),
api = _ => jsonFormError(err)
),
config =>
op(config)(ctx) flatMap { pov =>
negotiate(
html = fuccess(redirectPov(pov)),
api = apiVersion =>
env.api.roundApi.player(pov, none, apiVersion) map { data =>
Created(data) as JSON
}
)
}
)
}(rateLimitedFu)
}
private[controllers] def redirectPov(pov: Pov)(implicit ctx: Context) = {
val redir = Redirect(routes.Round.watcher(pov.gameId, "white"))
if (ctx.isAuth) redir
else
redir withCookies env.lilaCookie.cookie(
AnonCookie.name,
pov.playerId,
maxAge = AnonCookie.maxAge.some,
httpOnly = false.some
)
}
}