API to immediately accept a challenge

pull/6298/head
Thibault Duplessis 2020-04-04 10:37:28 -06:00
parent de8169864f
commit 8d0b466418
6 changed files with 99 additions and 46 deletions

View File

@ -1,6 +1,7 @@
package controllers
import play.api.mvc.Result
import play.api.libs.json.Json
import scala.concurrent.duration._
import lila.api.Context
@ -9,6 +10,7 @@ import lila.challenge.{ Challenge => ChallengeModel }
import lila.common.{ HTTPRequest, IpAddress }
import lila.game.{ AnonCookie, Pov }
import lila.socket.Socket.SocketVersion
import lila.user.{ User => UserModel }
import views.html
final class Challenge(
@ -193,41 +195,71 @@ final class Challenge(
ChallengeUserRateLimit(me.id) {
env.user.repo enabledById userId.toLowerCase flatMap {
destUser =>
destUser ?? { env.challenge.granter(me.some, _, config.perfType) } flatMap {
case Some(denied) =>
BadRequest(jsonError(lila.challenge.ChallengeDenied.translated(denied))).fuccess
import lila.challenge.Challenge._
val challenge = lila.challenge.Challenge.make(
variant = config.variant,
initialFen = config.position,
timeControl = config.clock map { c =>
TimeControl.Clock(c)
} orElse config.days.map {
TimeControl.Correspondence.apply
} getOrElse TimeControl.Unlimited,
mode = config.mode,
color = config.color.name,
challenger = Right(me),
destUser = destUser,
rematchOf = none
)
(destUser, config.acceptByToken) match {
case (Some(dest), Some(strToken)) => apiChallengeAccept(dest, challenge, strToken)
case _ =>
import lila.challenge.Challenge._
val challenge = lila.challenge.Challenge.make(
variant = config.variant,
initialFen = config.position,
timeControl = config.clock map { c =>
TimeControl.Clock(c)
} orElse config.days.map {
TimeControl.Correspondence.apply
} getOrElse TimeControl.Unlimited,
mode = config.mode,
color = config.color.name,
challenger = Right(me),
destUser = destUser,
rematchOf = none
)
(env.challenge.api create challenge) map {
case true =>
JsonOk(
env.challenge.jsonView
.show(challenge, SocketVersion(0), lila.challenge.Direction.Out.some)
)
case false =>
BadRequest(jsonError("Challenge not created"))
}
} map (_ as JSON)
destUser ?? { env.challenge.granter(me.some, _, config.perfType) } flatMap {
case Some(denied) =>
BadRequest(jsonError(lila.challenge.ChallengeDenied.translated(denied))).fuccess
case _ =>
(env.challenge.api create challenge) map {
case true =>
JsonOk(
env.challenge.jsonView
.show(challenge, SocketVersion(0), lila.challenge.Direction.Out.some)
)
case false =>
BadRequest(jsonError("Challenge not created"))
}
} map (_ as JSON)
}
}
}
}
)
}
private def apiChallengeAccept(
dest: UserModel,
challenge: lila.challenge.Challenge,
strToken: String
) =
env.security.api.oauthScoped(
lila.oauth.AccessToken.Id(strToken),
List(lila.oauth.OAuthScope.Challenge.Write)
) flatMap {
_.fold(
err => BadRequest(jsonError(err.message)).fuccess,
scoped =>
if (scoped.user is dest)
env.challenge.api.oauthAccept(dest, challenge) map {
case None => BadRequest(jsonError("Couldn't create game"))
case Some(g) =>
Ok(
Json.obj(
"game" -> env.game.jsonView(g, challenge.initialFen)
)
)
}
else BadRequest(jsonError("dest and accept user don't match")).fuccess
)
}
def rematchOf(gameId: String) = Auth { implicit ctx => me =>
OptionFuResult(env.game.gameRepo game gameId) { g =>
Pov.opponentOfUserId(g, me.id).flatMap(_.userId) ?? env.user.repo.byId flatMap {

View File

@ -89,6 +89,9 @@ final class ChallengeApi(
lila.common.Future.applySequentially(cs)(remove).void
}
def oauthAccept(dest: User, challenge: Challenge): Fu[Option[Game]] =
joiner(challenge, dest.some).map2(_.game)
private def isLimitedByMaxPlaying(c: Challenge) =
if (c.hasClock) fuFalse
else

View File

@ -21,19 +21,24 @@ final class OAuthServer(
import OAuthServer._
def auth(req: RequestHeader, scopes: List[OAuthScope]): Fu[AuthResult] =
reqToTokenId(req).fold[Fu[AuthResult]](fufail(MissingAuthorizationHeader)) { tokenId =>
accessTokenCache.get(tokenId) orFailWith NoSuchToken flatMap {
case at if scopes.nonEmpty && !scopes.exists(at.scopes.contains) => fufail(MissingScope(at.scopes))
case at =>
userRepo enabledById at.userId flatMap {
case None => fufail(NoSuchUser)
case Some(u) => fuccess(OAuthScope.Scoped(u, at.scopes))
}
} dmap Right.apply
reqToTokenId(req).fold[Fu[AuthResult]](fufail(MissingAuthorizationHeader)) {
auth(_, scopes)
} recover {
case e: AuthError => Left(e)
}
def auth(tokenId: AccessToken.Id, scopes: List[OAuthScope]): Fu[AuthResult] =
accessTokenCache.get(tokenId) orFailWith NoSuchToken flatMap {
case at if scopes.nonEmpty && !scopes.exists(at.scopes.contains) => fufail(MissingScope(at.scopes))
case at =>
userRepo enabledById at.userId flatMap {
case None => fufail(NoSuchUser)
case Some(u) => fuccess(OAuthScope.Scoped(u, at.scopes))
}
} dmap Right.apply recover {
case e: AuthError => Left(e)
}
def fetchAppAuthor(req: RequestHeader): Fu[Option[User.ID]] =
reqToTokenId(req) ?? { tokenId =>
tokenColl {

View File

@ -13,7 +13,7 @@ import scala.concurrent.duration._
import lila.common.{ ApiVersion, EmailAddress, IpAddress }
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
import lila.oauth.OAuthServer
import lila.oauth.{ AccessToken, OAuthServer }
import lila.user.{ User, UserRepo }
import User.LoginCandidate
@ -121,6 +121,15 @@ final class SecurityApi(
case Some(server) => server.auth(req, scopes)
}
def oauthScoped(
tokenId: AccessToken.Id,
scopes: List[lila.oauth.OAuthScope]
): Fu[lila.oauth.OAuthServer.AuthResult] =
tryOauthServer().flatMap {
case None => fuccess(Left(OAuthServer.ServerOffline))
case Some(server) => server.auth(tokenId, scopes)
}
def locatedOpenSessions(userId: User.ID, nb: Int): Fu[List[LocatedSession]] =
store.openSessions(userId, nb) map {
_.map { session =>

View File

@ -13,12 +13,13 @@ final case class ApiConfig(
days: Option[Int],
rated: Boolean,
color: Color,
position: Option[FEN] = None
position: Option[FEN] = None,
acceptByToken: Option[String] = None
) extends {
val strictFen = false
def >> = (variant.key.some, clock, days, rated, color.name.some, position.map(_.value)).some
def >> = (variant.key.some, clock, days, rated, color.name.some, position.map(_.value), acceptByToken).some
def perfType: Option[PerfType] = PerfPicker.perfType(chess.Speed(clock), variant, days)
@ -41,7 +42,8 @@ object ApiConfig extends BaseHumanConfig {
d: Option[Int],
r: Boolean,
c: Option[String],
pos: Option[String]
pos: Option[String],
tok: Option[String]
) =
new ApiConfig(
variant = chess.variant.Variant.orDefault(~v),
@ -49,6 +51,7 @@ object ApiConfig extends BaseHumanConfig {
days = d,
rated = r,
color = Color.orDefault(~c),
position = pos map FEN
position = pos map FEN,
acceptByToken = tok
)
}

View File

@ -132,10 +132,11 @@ final class FormFactory(
"increment" -> increment
)(chess.Clock.Config.apply)(chess.Clock.Config.unapply)
),
"days" -> optional(days),
"rated" -> boolean,
"color" -> optional(color),
"fen" -> fen
"days" -> optional(days),
"rated" -> boolean,
"color" -> optional(color),
"fen" -> fen,
"acceptByToken" -> optional(nonEmptyText)
)(ApiConfig.<<)(_.>>).verifying("invalidFen", _.validFen)
)