typesafe ApiVersion

pull/2094/head
Thibault Duplessis 2016-07-15 19:41:48 +02:00
parent 98cc5a8a32
commit 605f4a46b0
18 changed files with 87 additions and 54 deletions

View File

@ -15,10 +15,10 @@ object Api extends LilaController {
val api = lila.api.Mobile.Api
Ok(Json.obj(
"api" -> Json.obj(
"current" -> api.currentVersion,
"current" -> api.currentVersion.value,
"olds" -> api.oldVersions.map { old =>
Json.obj(
"version" -> old.version,
"version" -> old.version.value,
"deprecatedAt" -> old.deprecatedAt,
"unsupportedAt" -> old.unsupportedAt)
})

View File

@ -12,7 +12,7 @@ import scalaz.Monoid
import lila.api.{ PageData, Context, HeaderContext, BodyContext, TokenBucket }
import lila.app._
import lila.common.{ LilaCookie, HTTPRequest }
import lila.common.{ LilaCookie, HTTPRequest, ApiVersion }
import lila.notify.Notification.Notifies
import lila.security.{ Permission, Granter, FingerprintedUser }
import lila.user.{ UserContext, User => UserModel }
@ -277,7 +277,7 @@ private[controllers] trait LilaController
res,
res withCookies LilaCookie.makeSessionId(req))
protected def negotiate(html: => Fu[Result], api: Int => Fu[Result])(implicit ctx: Context): Fu[Result] =
protected def negotiate(html: => Fu[Result], api: ApiVersion => Fu[Result])(implicit ctx: Context): Fu[Result] =
(lila.api.Mobile.Api.requestVersion(ctx.req) match {
case Some(v) => api(v) map (_ as JSON)
case _ => html

View File

@ -8,7 +8,7 @@ import play.twirl.api.Html
import lila.api.Context
import lila.app._
import lila.common.HTTPRequest
import lila.common.{ HTTPRequest, ApiVersion }
import lila.game.{ Pov, PlayerRef, GameRepo, Game => GameModel }
import lila.hub.actorApi.map.Tell
import lila.round.actorApi.round._
@ -31,7 +31,9 @@ object Round extends LilaController with TheftPrevention {
uid = uid,
user = ctx.me,
ip = ctx.ip,
userTv = get("userTv"))
userTv = get("userTv"),
apiVersion = lila.api.Mobile.Api.currentVersion // yeah it should be in the URL
)
}
}
@ -41,7 +43,7 @@ object Round extends LilaController with TheftPrevention {
if (isTheft(pov)) fuccess(Left(theftResponse))
else get("sri") match {
case Some(uid) => requestAiMove(pov) >> env.socketHandler.player(
pov, uid, ~get("ran"), ctx.me, ctx.ip
pov, uid, ~get("ran"), ctx.me, ctx.ip, ApiVersion(apiVersion)
) map Right.apply
case None => fuccess(Left(NotFound))
}

View File

@ -3,7 +3,7 @@
@jsTag("challenge.js")
@embedJs {
lichess.startChallenge(document.getElementById('challenge'), {
socketUrl: '@routes.Challenge.websocket(c.id, apiVersion)',
socketUrl: '@routes.Challenge.websocket(c.id, apiVersion.value)',
xhrUrl: '@routes.Challenge.show(c.id)',
owner: @owner,
data: @Html(toJson(json))

View File

@ -16,7 +16,7 @@ explorer: {
endpoint: "@explorerEndpoint",
tablebaseEndpoint: "@tablebaseEndpoint"
},
socketUrl: "@routes.Study.websocket(s.id, apiVersion)",
socketUrl: "@routes.Study.websocket(s.id, apiVersion.value)",
socketVersion: @socketVersion
};
}

View File

@ -4,39 +4,41 @@ import org.joda.time.DateTime
import play.api.http.HeaderNames
import play.api.mvc.RequestHeader
import lila.common.ApiVersion
object Mobile {
object Api {
case class Old(
version: Int,
version: ApiVersion,
// date when a newer version was released
deprecatedAt: DateTime,
// date when the server stops accepting requests
unsupportedAt: DateTime)
val currentVersion = 2
val currentVersion = ApiVersion(2)
val acceptedVersionNumbers = Set(1, 2)
val acceptedVersions: Set[ApiVersion] = Set(1, 2) map ApiVersion.apply
val oldVersions: List[Old] = List(
Old( // chat messages are html escaped
version = 1,
version = ApiVersion(1),
deprecatedAt = new DateTime("2016-08-13"),
unsupportedAt = new DateTime("2016-11-13"))
)
private val PathPattern = """^.+/socket/v(\d+)$""".r
def requestVersion(req: RequestHeader): Option[Int] = {
def requestVersion(req: RequestHeader): Option[ApiVersion] = {
val accepts = ~req.headers.get(HeaderNames.ACCEPT)
if (accepts contains "application/vnd.lichess.v2+json") Some(2)
else if (accepts contains "application/vnd.lichess.v1+json") Some(1)
if (accepts contains "application/vnd.lichess.v2+json") Some(ApiVersion(2))
else if (accepts contains "application/vnd.lichess.v1+json") Some(ApiVersion(1))
else req.path match {
case PathPattern(version) => parseIntOption(version)
case PathPattern(version) => parseIntOption(version) map ApiVersion.apply
case _ => None
}
} filter acceptedVersionNumbers.contains
} filter acceptedVersions.contains
def requested(req: RequestHeader) = requestVersion(req).isDefined
}

View File

@ -3,8 +3,8 @@ package lila.api
import play.api.libs.json._
import lila.analyse.{ JsonView => analysisJson, Analysis, Info }
import lila.common.LightUser
import lila.common.PimpedJson._
import lila.common.{ LightUser, ApiVersion }
import lila.game.{ Pov, Game, GameRepo }
import lila.pref.Pref
import lila.round.{ JsonView, Forecast }
@ -21,7 +21,7 @@ private[api] final class RoundApi(
getSimul: Simul.ID => Fu[Option[Simul]],
lightUser: String => Option[LightUser]) {
def player(pov: Pov, apiVersion: Int)(implicit ctx: Context): Fu[JsObject] =
def player(pov: Pov, apiVersion: ApiVersion)(implicit ctx: Context): Fu[JsObject] =
GameRepo.initialFen(pov.game) flatMap { initialFen =>
jsonView.playerJson(pov, ctx.pref, apiVersion, ctx.me,
withBlurs = ctx.me ?? Granter(_.ViewBlurs),
@ -40,7 +40,7 @@ private[api] final class RoundApi(
}
}
def watcher(pov: Pov, apiVersion: Int, tv: Option[lila.round.OnTv],
def watcher(pov: Pov, apiVersion: ApiVersion, tv: Option[lila.round.OnTv],
initialFenO: Option[Option[String]] = None)(implicit ctx: Context): Fu[JsObject] =
initialFenO.fold(GameRepo initialFen pov.game)(fuccess) flatMap { initialFen =>
jsonView.watcherJson(pov, ctx.pref, apiVersion, ctx.me, tv,
@ -60,7 +60,7 @@ private[api] final class RoundApi(
}
}
def review(pov: Pov, apiVersion: Int, tv: Option[lila.round.OnTv],
def review(pov: Pov, apiVersion: ApiVersion, tv: Option[lila.round.OnTv],
analysis: Option[Analysis] = None,
initialFenO: Option[Option[String]] = None,
withMoveTimes: Boolean = false,

View File

@ -5,6 +5,7 @@ import akka.pattern.{ ask, pipe }
import play.api.libs.json.JsObject
import scala.concurrent.duration._
import lila.common.ApiVersion
import lila.analyse.Analysis
import lila.game.Pov
import lila.pref.Pref
@ -20,11 +21,11 @@ private[api] final class RoundApiBalancer(
implicit val timeout = makeTimeout seconds 20
case class Player(pov: Pov, apiVersion: Int, ctx: Context)
case class Watcher(pov: Pov, apiVersion: Int, tv: Option[lila.round.OnTv],
case class Player(pov: Pov, apiVersion: ApiVersion, ctx: Context)
case class Watcher(pov: Pov, apiVersion: ApiVersion, tv: Option[lila.round.OnTv],
initialFenO: Option[Option[String]] = None,
ctx: Context)
case class Review(pov: Pov, apiVersion: Int, tv: Option[lila.round.OnTv],
case class Review(pov: Pov, apiVersion: ApiVersion, tv: Option[lila.round.OnTv],
analysis: Option[Analysis] = None,
initialFenO: Option[Option[String]] = None,
withMoveTimes: Boolean = false,
@ -58,7 +59,7 @@ private[api] final class RoundApiBalancer(
import implementation._
def player(pov: Pov, apiVersion: Int)(implicit ctx: Context): Fu[JsObject] = {
def player(pov: Pov, apiVersion: ApiVersion)(implicit ctx: Context): Fu[JsObject] = {
router ? Player(pov, apiVersion, ctx) mapTo manifest[JsObject] addFailureEffect { e =>
logger.error(pov.toString, e)
}
@ -67,12 +68,12 @@ private[api] final class RoundApiBalancer(
.logIfSlow(500, logger) { _ => s"outer player $pov" }
.result
def watcher(pov: Pov, apiVersion: Int, tv: Option[lila.round.OnTv],
def watcher(pov: Pov, apiVersion: ApiVersion, tv: Option[lila.round.OnTv],
initialFenO: Option[Option[String]] = None)(implicit ctx: Context): Fu[JsObject] = {
router ? Watcher(pov, apiVersion, tv, initialFenO, ctx) mapTo manifest[JsObject]
}.mon(_.round.api.watcher)
def review(pov: Pov, apiVersion: Int, tv: Option[lila.round.OnTv],
def review(pov: Pov, apiVersion: ApiVersion, tv: Option[lila.round.OnTv],
analysis: Option[Analysis] = None,
initialFenO: Option[Option[String]] = None,
withMoveTimes: Boolean = false,

View File

@ -13,6 +13,10 @@ trait PackageObject extends Steroids with WithFuture {
def value: String
override def toString = value
}
trait IntValue extends Any {
def value: Int
override def toString = value.toString
}
def !![A](msg: String): Valid[A] = msg.failureNel[A]

View File

@ -0,0 +1,3 @@
package lila.common
case class ApiVersion(value: Int) extends AnyVal with IntValue

View File

@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringEscapeUtils.escapeHtml4
import play.api.libs.json._
import lila.common.PimpedJson._
import lila.common.ApiVersion
import lila.game.JsonView._
import lila.game.{ Pov, Game, PerfPicker, Source, GameRepo, CorrespondenceClock }
import lila.pref.Pref
@ -34,7 +35,7 @@ final class JsonView(
def playerJson(
pov: Pov,
pref: Pref,
apiVersion: Int,
apiVersion: ApiVersion,
playerUser: Option[User],
initialFen: Option[String],
withBlurs: Boolean): Fu[JsObject] =
@ -119,7 +120,7 @@ final class JsonView(
def watcherJson(
pov: Pov,
pref: Pref,
apiVersion: Int,
apiVersion: ApiVersion,
user: Option[User],
tv: Option[OnTv],
withBlurs: Boolean,

View File

@ -141,9 +141,9 @@ private[round] final class Socket(
blackIsGone = blackIsGone)
} pipeTo sender
case Join(uid, user, color, playerId, ip, userTv) =>
case Join(uid, user, color, playerId, ip, userTv, apiVersion) =>
val (enumerator, channel) = Concurrent.broadcast[JsValue]
val member = Member(channel, user, color, playerId, ip, userTv = userTv)
val member = Member(channel, user, color, playerId, ip, userTv = userTv, apiVersion = apiVersion)
addMember(uid, member)
notifyCrowd
playerDo(color, _.ping)

View File

@ -10,6 +10,7 @@ import chess.format.Uci
import play.api.libs.json.{ JsObject, Json }
import actorApi._, round._
import lila.common.ApiVersion
import lila.common.PimpedJson._
import lila.game.{ Game, Pov, PovRef, PlayerRef, GameRepo }
import lila.hub.actorApi.map._
@ -105,9 +106,10 @@ private[round] final class SocketHandler(
uid: String,
user: Option[User],
ip: String,
userTv: Option[String]): Fu[Option[JsSocketHandler]] =
userTv: Option[String],
apiVersion: ApiVersion): Fu[Option[JsSocketHandler]] =
GameRepo.pov(gameId, colorName) flatMap {
_ ?? { join(_, none, uid, "", user, ip, userTv = userTv) map some }
_ ?? { join(_, none, uid, "", user, ip, userTv = userTv, apiVersion) map some }
}
def player(
@ -115,8 +117,9 @@ private[round] final class SocketHandler(
uid: String,
token: String,
user: Option[User],
ip: String): Fu[JsSocketHandler] =
join(pov, Some(pov.playerId), uid, token, user, ip, userTv = none)
ip: String,
apiVersion: ApiVersion): Fu[JsSocketHandler] =
join(pov, Some(pov.playerId), uid, token, user, ip, userTv = none, apiVersion)
private def join(
pov: Pov,
@ -125,14 +128,16 @@ private[round] final class SocketHandler(
token: String,
user: Option[User],
ip: String,
userTv: Option[String]): Fu[JsSocketHandler] = {
userTv: Option[String],
apiVersion: ApiVersion): Fu[JsSocketHandler] = {
val join = Join(
uid = uid,
user = user,
color = pov.color,
playerId = playerId,
ip = ip,
userTv = userTv)
userTv = userTv,
apiVersion = apiVersion)
socketHub ? Get(pov.gameId) mapTo manifest[ActorRef] flatMap { socket =>
Handler(hub, socket, uid, join, user map (_.id)) {
case Connected(enum, member) =>

View File

@ -7,6 +7,7 @@ import scala.concurrent.Promise
import chess.Color
import chess.format.Uci
import lila.common.ApiVersion
import lila.game.{ Game, Event, PlayerRef }
import lila.socket.SocketMember
import lila.user.User
@ -20,6 +21,7 @@ sealed trait Member extends SocketMember {
val troll: Boolean
val ip: String
val userTv: Option[String]
val apiVersion: ApiVersion
def owner = playerIdOption.isDefined
def watcher = !owner
@ -34,11 +36,12 @@ object Member {
color: Color,
playerIdOption: Option[String],
ip: String,
userTv: Option[String]): Member = {
userTv: Option[String],
apiVersion: ApiVersion): Member = {
val userId = user map (_.id)
val troll = user.??(_.troll)
playerIdOption.fold[Member](Watcher(channel, userId, color, troll, ip, userTv)) { playerId =>
Owner(channel, userId, playerId, color, troll, ip)
playerIdOption.fold[Member](Watcher(channel, userId, color, troll, ip, userTv, apiVersion)) { playerId =>
Owner(channel, userId, playerId, color, troll, ip, apiVersion)
}
}
}
@ -49,7 +52,8 @@ case class Owner(
playerId: String,
color: Color,
troll: Boolean,
ip: String) extends Member {
ip: String,
apiVersion: ApiVersion) extends Member {
val playerIdOption = playerId.some
val userTv = none
@ -61,7 +65,8 @@ case class Watcher(
color: Color,
troll: Boolean,
ip: String,
userTv: Option[String]) extends Member {
userTv: Option[String],
apiVersion: ApiVersion) extends Member {
val playerIdOption = none
}
@ -72,7 +77,8 @@ case class Join(
color: Color,
playerId: Option[String],
ip: String,
userTv: Option[String])
userTv: Option[String],
apiVersion: ApiVersion)
case class Connected(enumerator: JsEnumerator, member: Member)
case class Bye(color: Color)
case class IsGone(color: Color)

View File

@ -7,6 +7,7 @@ import play.api.data.Forms._
import play.api.mvc.RequestHeader
import reactivemongo.bson._
import lila.common.ApiVersion
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
import lila.user.{ User, UserRepo }
@ -46,7 +47,7 @@ final class Api(
private def authenticateCandidate(candidate: Option[User.LoginCandidate])(username: String, password: String): Option[User] =
candidate ?? { _(password) }
def saveAuthentication(userId: String, apiVersion: Option[Int])(implicit req: RequestHeader): Fu[String] =
def saveAuthentication(userId: String, apiVersion: Option[ApiVersion])(implicit req: RequestHeader): Fu[String] =
UserRepo mustConfirmEmail userId flatMap {
case true => fufail(Api MustConfirmEmail userId)
case false =>

View File

@ -6,10 +6,10 @@ import org.joda.time.DateTime
import play.api.mvc.RequestHeader
import reactivemongo.bson.Macros
import lila.db.dsl._
import lila.common.{ HTTPRequest, ApiVersion }
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
import lila.user.{ User, UserRepo }
import lila.common.HTTPRequest
object Store {
@ -20,7 +20,7 @@ object Store {
sessionId: String,
userId: String,
req: RequestHeader,
apiVersion: Option[Int]): Funit =
apiVersion: Option[ApiVersion]): Funit =
coll.insert($doc(
"_id" -> sessionId,
"user" -> userId,
@ -28,13 +28,16 @@ object Store {
"ua" -> HTTPRequest.userAgent(req).|("?"),
"date" -> DateTime.now,
"up" -> true,
"api" -> apiVersion
"api" -> apiVersion.map(_.value)
)).void
private val userIdProjection = $doc("user" -> true, "_id" -> false)
private val userIdFingerprintProjection = $doc("user" -> true, "fp" -> true, "_id" -> false)
def userId(sessionId: String): Fu[Option[String]] =
coll.find(
$doc("_id" -> sessionId, "up" -> true),
$doc("user" -> true, "_id" -> false)
userIdProjection
).uno[Bdoc] map { _ flatMap (_.getAs[String]("user")) }
case class UserIdAndFingerprint(user: String, fp: Option[String])
@ -43,7 +46,7 @@ object Store {
def userIdAndFingerprint(sessionId: String): Fu[Option[UserIdAndFingerprint]] =
coll.find(
$doc("_id" -> sessionId, "up" -> true),
$doc("user" -> true, "fp" -> true, "_id" -> false)
userIdFingerprintProjection
).uno[UserIdAndFingerprint]
def delete(sessionId: String): Funit =

View File

@ -5,6 +5,7 @@ import org.joda.time.DateTime
import reactivemongo.api._
import reactivemongo.bson._
import lila.common.ApiVersion
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
import lila.rating.{ Glicko, Perf, PerfType }
@ -219,7 +220,7 @@ object UserRepo {
def getPasswordHash(id: ID): Fu[Option[String]] =
coll.primitiveOne[String]($id(id), "password")
def create(username: String, password: String, email: Option[String], blind: Boolean, mobileApiVersion: Option[Int]): Fu[Option[User]] =
def create(username: String, password: String, email: Option[String], blind: Boolean, mobileApiVersion: Option[ApiVersion]): Fu[Option[User]] =
!nameExists(username) flatMap {
_ ?? {
val doc = newUser(username, password, email, blind, mobileApiVersion) ++
@ -347,7 +348,7 @@ object UserRepo {
def setEmailConfirmed(id: String): Funit = coll.update($id(id), $unset(F.mustConfirmEmail)).void
private def newUser(username: String, password: String, email: Option[String], blind: Boolean, mobileApiVersion: Option[Int]) = {
private def newUser(username: String, password: String, email: Option[String], blind: Boolean, mobileApiVersion: Option[ApiVersion]) = {
val salt = ornicar.scalalib.Random nextStringUppercase 32
implicit def countHandler = Count.countBSONHandler
@ -365,7 +366,7 @@ object UserRepo {
F.count -> Count.default,
F.enabled -> true,
F.createdAt -> DateTime.now,
F.createdWithApiVersion -> mobileApiVersion,
F.createdWithApiVersion -> mobileApiVersion.map(_.value),
F.seenAt -> DateTime.now) ++ {
if (blind) $doc("blind" -> true) else $empty
}

View File

@ -2007,6 +2007,10 @@ body.base .crosstable .loss {
.crosstable .user {
padding-left: 7px;
}
.crosstable .unavailable {
margin-top: 40px;
opacity: 0.7;
}
.bookmark {
float: right;
outline: none;