bulk challenge API WIP - for #8059

pull/8063/head
Thibault Duplessis 2021-02-01 12:08:39 +01:00
parent 65b417f6a0
commit 5efe9e0e2c
10 changed files with 153 additions and 14 deletions

View File

@ -277,7 +277,7 @@ final class Challenge(
.user(me)
.bindFromRequest()
.fold(
err => BadRequest(apiFormError(err)).fuccess,
newJsonFormError,
config => {
val cost = if (me.isApiHog) 0 else 1
ChallengeIpRateLimit(HTTPRequest ipAddress req, cost = cost) {
@ -309,6 +309,34 @@ final class Challenge(
)
}
def bulk(userId: String) =
ScopedBody(_.Challenge.Bulk) { implicit req => me =>
implicit val lang = reqLang
lila.setup.BulkChallenge.form
.bindFromRequest()
.fold(
newJsonFormError,
data =>
env.setup.bulk(data) flatMap {
case Left(badTokens) =>
import lila.setup.BulkChallenge.BadToken
import play.api.libs.json._
BadRequest(
Json.obj(
"tokens" -> JsObject {
badTokens.map { case BadToken(token, error) =>
token.value -> JsString(error.message)
}
}
)
).fuccess
case Right(bulk) =>
println(bulk)
???
}
)
}
def apiCreateAdmin(origName: String, destName: String) =
ScopedBody(_.Challenge.Write) { implicit req => admin =>
IfGranted(_.ApiChallengeAdmin, req, admin) {

View File

@ -225,7 +225,7 @@ lazy val lobby = module("lobby",
)
lazy val setup = module("setup",
Seq(common, db, memo, hub, socket, game, user, lobby, pref, relation),
Seq(common, db, memo, hub, socket, game, user, lobby, pref, relation, oauth),
reactivemongo.bundle
)

View File

@ -616,6 +616,7 @@ POST /api/challenge/$id<\w{8}>/accept controllers.Challenge.apiAccept(id: Strin
POST /api/challenge/$id<\w{8}>/decline controllers.Challenge.apiDecline(id: String)
POST /api/challenge/$id<\w{8}>/cancel controllers.Challenge.apiCancel(id: String)
POST /api/challenge/$id<\w{8}>/start-clocks controllers.Challenge.apiStartClocks(id: String)
POST /api/challenge/bulk controllers.Challenge.bulk
POST /api/round/$id<\w{8}>/add-time/:seconds controllers.Round.apiAddTime(id: String, seconds: Int)
GET /api/cloud-eval controllers.Api.cloudEval
GET /api/broadcast controllers.Relay.apiIndex

View File

@ -14,7 +14,7 @@ final class ChallengeApi(
repo: ChallengeRepo,
challengeMaker: ChallengeMaker,
userRepo: UserRepo,
joiner: Joiner,
joiner: ChallengeJoiner,
jsonView: JsonView,
gameCache: lila.game.Cached,
maxPlaying: Max,

View File

@ -8,7 +8,7 @@ import scala.util.chaining._
import lila.game.{ Game, Player, Pov, Source }
import lila.user.User
final private class Joiner(
final private class ChallengeJoiner(
gameRepo: lila.game.GameRepo,
userRepo: lila.user.UserRepo,
onStart: lila.round.OnStart
@ -20,13 +20,13 @@ final private class Joiner(
case _ if color.map(Challenge.ColorChoice.apply).has(c.colorChoice) => fuccess(None)
case _ =>
c.challengerUserId.??(userRepo.byId) flatMap { origUser =>
val game = Joiner.createGame(c, origUser, destUser, color)
val game = ChallengeJoiner.createGame(c, origUser, destUser, color)
(gameRepo insertDenormalized game) >>- onStart(game.id) inject Pov(game, !c.finalColor).some
}
}
}
private object Joiner {
private object ChallengeJoiner {
def createGame(
c: Challenge,

View File

@ -33,7 +33,7 @@ final class Env(
def version(challengeId: Challenge.ID): Fu[SocketVersion] =
socket.rooms.ask[SocketVersion](challengeId)(GetVersion)
private lazy val joiner = wire[Joiner]
private lazy val joiner = wire[ChallengeJoiner]
lazy val maker = wire[ChallengeMaker]

View File

@ -18,6 +18,7 @@ object OAuthScope {
object Challenge {
case object Read extends OAuthScope("challenge:read", "Read incoming challenges")
case object Write extends OAuthScope("challenge:write", "Create, accept, decline challenges")
case object Bulk extends OAuthScope("challenge:bulk", "Create many games at once for other players")
}
object Study {

View File

@ -0,0 +1,104 @@
package lila.setup
import akka.stream.scaladsl._
import chess.format.FEN
import chess.variant.Variant
import chess.{ Clock, Speed }
import org.joda.time.DateTime
import play.api.data._
import play.api.data.Forms._
import lila.game.Game
import lila.oauth.AccessToken
import lila.oauth.OAuthScope
import lila.oauth.OAuthServer
import lila.user.User
object BulkChallenge {
val maxGames = 500
case class BulkFormData(tokens: String, variant: Variant, clock: Clock.Config, rated: Boolean)
val form = Form[BulkFormData](
mapping(
"tokens" -> nonEmptyText
.verifying("Not enough tokens", t => extractTokenPairs(t).isEmpty)
.verifying(s"Too many tokens (max: ${maxGames * 2})", t => extractTokenPairs(t).sizeIs > maxGames),
SetupForm.api.variant,
"clock" -> SetupForm.api.clockMapping,
"rated" -> boolean
) { (tokens: String, variant: Option[String], clock: Clock.Config, rated: Boolean) =>
BulkFormData(tokens, Variant orDefault ~variant, clock, rated)
}(_ => None)
)
private[setup] def extractTokenPairs(str: String): List[(AccessToken.Id, AccessToken.Id)] =
str
.split(',')
.view
.map(_ split ":")
.collect { case Array(w, b) =>
w.trim -> b.trim
}
.collect {
case (w, b) if w.nonEmpty && b.nonEmpty => (AccessToken.Id(w), AccessToken.Id(b))
}
.toList
case class BadToken(token: AccessToken.Id, error: OAuthServer.AuthError)
case class ScheduledBulkPairing(
players: List[(User.ID, User.ID)],
variant: Variant,
clock: Clock.Config,
rated: Boolean,
pairAt: DateTime,
startClocksAt: DateTime
)
}
final class BulkChallengeApi(oauthServer: OAuthServer)(implicit
ec: scala.concurrent.ExecutionContext,
mat: akka.stream.Materializer
) {
import BulkChallenge._
def apply(data: BulkFormData): Fu[Either[List[BadToken], ScheduledBulkPairing]] =
Source(extractTokenPairs(data.tokens))
.mapConcat { case (whiteToken, blackToken) =>
List(whiteToken, blackToken) // flatten now, re-pair later!
}
.mapAsync(8) { token =>
oauthServer.auth(token, List(OAuthScope.Challenge.Write)) map {
_.left.map { BadToken(token, _) }
}
}
.runFold[Either[List[BadToken], List[User.ID]]](Right(Nil)) {
case (Left(bads), Left(bad)) => Left(bad :: bads)
case (Left(bads), _) => Left(bads)
case (Right(_), Left(bad)) => Left(bad :: Nil)
case (Right(users), Right(scoped)) => Right(scoped.user.id :: users)
}
.map {
_.map {
_.reverse
.grouped(2)
.collect { case List(w, b) => (w, b) }
.toList
}.left.map(_.reverse)
}
.map {
_.map { players =>
ScheduledBulkPairing(
players,
data.variant,
data.clock,
data.rated,
DateTime.now,
DateTime.now
)
}
}
}

View File

@ -4,6 +4,7 @@ import com.softwaremill.macwire._
import play.api.Configuration
import lila.common.config._
import lila.oauth.OAuthServer
@Module
final class Env(
@ -11,12 +12,15 @@ final class Env(
gameRepo: lila.game.GameRepo,
fishnetPlayer: lila.fishnet.Player,
onStart: lila.round.OnStart,
gameCache: lila.game.Cached
)(implicit ec: scala.concurrent.ExecutionContext) {
gameCache: lila.game.Cached,
oauthServer: OAuthServer
)(implicit ec: scala.concurrent.ExecutionContext, mat: akka.stream.Materializer) {
private lazy val maxPlaying = appConfig.get[Max]("setup.max_playing")
lazy val forms = wire[SetupForm]
lazy val forms = SetupForm
lazy val processor = wire[Processor]
lazy val bulk = wire[BulkChallengeApi]
}

View File

@ -8,7 +8,7 @@ import play.api.data.Forms._
import lila.rating.RatingRange
import lila.user.{ User, UserContext }
final class SetupForm {
object SetupForm {
import Mappings._
@ -104,14 +104,15 @@ final class SetupForm {
object api {
private lazy val clock = "clock" -> optional(
lazy val clockMapping =
mapping(
"limit" -> number.verifying(ApiConfig.clockLimitSeconds.contains _),
"increment" -> increment
)(chess.Clock.Config.apply)(chess.Clock.Config.unapply)
)
private lazy val variant =
lazy val clock = "clock" -> optional(clockMapping)
lazy val variant =
"variant" -> optional(text.verifying(Variant.byKey.contains _))
def user(from: User) =