bulk challenge API WIP - for #8059
parent
65b417f6a0
commit
5efe9e0e2c
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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) =
|
||||
|
|
Loading…
Reference in New Issue