typesafe ApiVersion

This commit is contained in:
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 val api = lila.api.Mobile.Api
Ok(Json.obj( Ok(Json.obj(
"api" -> Json.obj( "api" -> Json.obj(
"current" -> api.currentVersion, "current" -> api.currentVersion.value,
"olds" -> api.oldVersions.map { old => "olds" -> api.oldVersions.map { old =>
Json.obj( Json.obj(
"version" -> old.version, "version" -> old.version.value,
"deprecatedAt" -> old.deprecatedAt, "deprecatedAt" -> old.deprecatedAt,
"unsupportedAt" -> old.unsupportedAt) "unsupportedAt" -> old.unsupportedAt)
}) })

View file

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

View file

@ -8,7 +8,7 @@ import play.twirl.api.Html
import lila.api.Context import lila.api.Context
import lila.app._ import lila.app._
import lila.common.HTTPRequest import lila.common.{ HTTPRequest, ApiVersion }
import lila.game.{ Pov, PlayerRef, GameRepo, Game => GameModel } import lila.game.{ Pov, PlayerRef, GameRepo, Game => GameModel }
import lila.hub.actorApi.map.Tell import lila.hub.actorApi.map.Tell
import lila.round.actorApi.round._ import lila.round.actorApi.round._
@ -31,7 +31,9 @@ object Round extends LilaController with TheftPrevention {
uid = uid, uid = uid,
user = ctx.me, user = ctx.me,
ip = ctx.ip, 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)) if (isTheft(pov)) fuccess(Left(theftResponse))
else get("sri") match { else get("sri") match {
case Some(uid) => requestAiMove(pov) >> env.socketHandler.player( 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 ) map Right.apply
case None => fuccess(Left(NotFound)) case None => fuccess(Left(NotFound))
} }

View file

@ -3,7 +3,7 @@
@jsTag("challenge.js") @jsTag("challenge.js")
@embedJs { @embedJs {
lichess.startChallenge(document.getElementById('challenge'), { 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)', xhrUrl: '@routes.Challenge.show(c.id)',
owner: @owner, owner: @owner,
data: @Html(toJson(json)) data: @Html(toJson(json))

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,10 @@ trait PackageObject extends Steroids with WithFuture {
def value: String def value: String
override def toString = value 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] 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 play.api.libs.json._
import lila.common.PimpedJson._ import lila.common.PimpedJson._
import lila.common.ApiVersion
import lila.game.JsonView._ import lila.game.JsonView._
import lila.game.{ Pov, Game, PerfPicker, Source, GameRepo, CorrespondenceClock } import lila.game.{ Pov, Game, PerfPicker, Source, GameRepo, CorrespondenceClock }
import lila.pref.Pref import lila.pref.Pref
@ -34,7 +35,7 @@ final class JsonView(
def playerJson( def playerJson(
pov: Pov, pov: Pov,
pref: Pref, pref: Pref,
apiVersion: Int, apiVersion: ApiVersion,
playerUser: Option[User], playerUser: Option[User],
initialFen: Option[String], initialFen: Option[String],
withBlurs: Boolean): Fu[JsObject] = withBlurs: Boolean): Fu[JsObject] =
@ -119,7 +120,7 @@ final class JsonView(
def watcherJson( def watcherJson(
pov: Pov, pov: Pov,
pref: Pref, pref: Pref,
apiVersion: Int, apiVersion: ApiVersion,
user: Option[User], user: Option[User],
tv: Option[OnTv], tv: Option[OnTv],
withBlurs: Boolean, withBlurs: Boolean,

View file

@ -141,9 +141,9 @@ private[round] final class Socket(
blackIsGone = blackIsGone) blackIsGone = blackIsGone)
} pipeTo sender } 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 (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) addMember(uid, member)
notifyCrowd notifyCrowd
playerDo(color, _.ping) playerDo(color, _.ping)

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import play.api.data.Forms._
import play.api.mvc.RequestHeader import play.api.mvc.RequestHeader
import reactivemongo.bson._ import reactivemongo.bson._
import lila.common.ApiVersion
import lila.db.BSON.BSONJodaDateTimeHandler import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._ import lila.db.dsl._
import lila.user.{ User, UserRepo } 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] = private def authenticateCandidate(candidate: Option[User.LoginCandidate])(username: String, password: String): Option[User] =
candidate ?? { _(password) } 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 { UserRepo mustConfirmEmail userId flatMap {
case true => fufail(Api MustConfirmEmail userId) case true => fufail(Api MustConfirmEmail userId)
case false => case false =>

View file

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

View file

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

View file

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