Skip to content

Commit

Permalink
implement player VS games API - closes lichess-org#2909
Browse files Browse the repository at this point in the history
  • Loading branch information
ornicar committed Apr 6, 2017
1 parent 60f753d commit 1afa633
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 9 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,14 @@ name | type | default | description

(1) All game statuses: https://github.com/ornicar/scalachess/blob/master/src/main/scala/Status.scala#L16-L25

### `GET /api/games/vs/<username>/<username>` fetch games between 2 users

```
> curl https://en.lichess.org/api/games/vs/thibault/legend?nb=10&page=2
```

Parameters and result are similar to the users games API.

### `GET /api/game/{id}` fetch one game by ID

```
Expand Down
38 changes: 36 additions & 2 deletions app/controllers/Api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ object Api extends LilaController {
)

private val UserGamesRateLimitGlobal = new lila.memo.RateLimit[String](
credits = 10 * 1000,
duration = 1 minute,
credits = 15 * 1000,
duration = 2 minute,
name = "user games API global",
key = "user_games.api.global"
)
Expand Down Expand Up @@ -188,6 +188,40 @@ object Api extends LilaController {
}
}

def gamesVs(u1: String, u2: String) = ApiRequest { implicit ctx =>
val page = (getInt("page") | 1) atLeast 1 atMost 200
val nb = (getInt("nb") | 10) atLeast 1 atMost 100
val cost = page * nb * 2 + 10
val ip = HTTPRequest lastRemoteAddress ctx.req
UserGamesRateLimitPerIP(ip, cost = cost) {
UserGamesRateLimitPerUA(~HTTPRequest.userAgent(ctx.req), cost = cost, msg = ip.value) {
UserGamesRateLimitGlobal("-", cost = cost, msg = ip.value) {
lila.mon.api.userGames.cost(cost)
for {
usersO <- lila.user.UserRepo.pair(
lila.user.User.normalize(u1),
lila.user.User.normalize(u2)
)
res <- usersO.?? { users =>
gameApi.byUsersVs(
users = users,
rated = getBoolOpt("rated"),
playing = getBoolOpt("playing"),
analysed = getBoolOpt("analysed"),
withAnalysis = getBool("with_analysis"),
withMoves = getBool("with_moves"),
withOpening = getBool("with_opening"),
withMoveTimes = getBool("with_movetimes"),
nb = nb,
page = page
) map some
}
} yield toApiResult(res)
}
}
}
}

def currentTournaments = ApiRequest { implicit ctx =>
Env.tournament.api.fetchVisibleTournaments flatMap
Env.tournament.scheduleJsonView.apply map Data.apply
Expand Down
1 change: 1 addition & 0 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ POST /api/users controllers.Api.usersByIds
GET /api/user/:name controllers.Api.user(name: String)
GET /api/user/:name/games controllers.Api.userGames(name: String)
GET /api/game/:id controllers.Api.game(id: String)
GET /api/games/vs/:u1/:u2 controllers.Api.gamesVs(u1: String, u2: String)
POST /api/games controllers.Api.games
GET /api/tournament controllers.Api.currentTournaments
GET /api/tournament/:id controllers.Api.tournament(id: String)
Expand Down
3 changes: 2 additions & 1 deletion modules/api/src/main/Env.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ final class Env(
netBaseUrl = Net.BaseUrl,
apiToken = apiToken,
pgnDump = pgnDump,
gameCache = gameCache
gameCache = gameCache,
crosstableApi = crosstableApi
)

val userGameApi = new UserGameApi(
Expand Down
53 changes: 51 additions & 2 deletions modules/api/src/main/GameApi.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lila.api

import play.api.libs.json._
import scala.concurrent.duration._
import reactivemongo.api.ReadPreference
import reactivemongo.bson._

Expand All @@ -11,14 +12,15 @@ import lila.db.dsl._
import lila.db.paginator.{ Adapter, CachedAdapter }
import lila.game.BSONHandlers._
import lila.game.Game.{ BSONFields => G }
import lila.game.{ Game, GameRepo, PerfPicker }
import lila.game.{ Game, GameRepo, PerfPicker, CrosstableApi }
import lila.user.User

private[api] final class GameApi(
netBaseUrl: String,
apiToken: String,
pgnDump: PgnDump,
gameCache: lila.game.Cached
gameCache: lila.game.Cached,
crosstableApi: CrosstableApi
) {

import lila.round.JsonView.openingWriter
Expand Down Expand Up @@ -108,6 +110,53 @@ private[api] final class GameApi(
) _
}

def byUsersVs(
users: (User, User),
rated: Option[Boolean],
playing: Option[Boolean],
analysed: Option[Boolean],
withAnalysis: Boolean,
withMoves: Boolean,
withOpening: Boolean,
withMoveTimes: Boolean,
nb: Int,
page: Int
): Fu[JsObject] = Paginator(
adapter = new CachedAdapter(
adapter = new Adapter[Game](
collection = GameRepo.coll,
selector = {
if (~playing) lila.game.Query.nowPlayingVs(users._1.id, users._2.id)
else lila.game.Query.opponents(users._1, users._2) ++ $doc(
G.status $gte chess.Status.Mate.id,
G.analysed -> analysed.map(_.fold[BSONValue](BSONBoolean(true), $doc("$exists" -> false)))
)
} ++ $doc(
G.rated -> rated.map(_.fold[BSONValue](BSONBoolean(true), $doc("$exists" -> false)))
),
projection = $empty,
sort = $doc(G.createdAt -> -1),
readPreference = ReadPreference.secondaryPreferred
),
nbResults =
if (~playing) gameCache.nbPlaying(users._1.id)
else crosstableApi(users._1.id, users._2.id, 5 seconds).map { _ ?? (_.nbGames) }
),
currentPage = page,
maxPerPage = nb
) flatMap { pag =>
gamesJson(
withAnalysis = withAnalysis,
withMoves = withMoves,
withOpening = withOpening,
withFens = false,
withMoveTimes = withMoveTimes,
token = none
)(pag.currentPageResults) map { games =>
PaginatorJson(pag withCurrentPageResults games)
}
}

private def makeUrl(game: Game) = s"$netBaseUrl/${game.id}/${game.firstPlayer.color.name}"

private def gamesJson(
Expand Down
8 changes: 4 additions & 4 deletions modules/game/src/main/CrosstableApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ final class CrosstableApi(
case _ => fuccess(none)
}

def apply(u1: String, u2: String): Fu[Option[Crosstable]] =
coll.uno[Crosstable](select(u1, u2)) orElse createFast(u1, u2)
def apply(u1: String, u2: String, timeout: FiniteDuration = 1.second): Fu[Option[Crosstable]] =
coll.uno[Crosstable](select(u1, u2)) orElse createWithTimeout(u1, u2, timeout)

def nbGames(u1: String, u2: String): Fu[Int] =
coll.find(
Expand Down Expand Up @@ -61,8 +61,8 @@ final class CrosstableApi(
case _ => funit
}

private def createFast(u1: String, u2: String) =
creationCache.get(u1 -> u2).withTimeoutDefault(1 second, none)(system)
private def createWithTimeout(u1: String, u2: String, timeout: FiniteDuration) =
creationCache.get(u1 -> u2).withTimeoutDefault(timeout, none)(system)

// to avoid creating it twice during a new matchup
private val creationCache = asyncCache.multi[(String, String), Option[Crosstable]](
Expand Down
2 changes: 2 additions & 0 deletions modules/game/src/main/Query.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ object Query {
def recentlyPlaying(u: String) =
nowPlaying(u) ++ $doc(F.updatedAt $gt DateTime.now.minusMinutes(5))

def nowPlayingVs(u1: String, u2: String) = $doc(F.playingUids $all List(u1, u2))

// use the us index
def win(u: String) = user(u) ++ $doc(F.winnerId -> u)

Expand Down
8 changes: 8 additions & 0 deletions modules/user/src/main/UserRepo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ object UserRepo {
y.??(yy => users.find(_.id == yy))
}

def pair(x: ID, y: ID): Fu[Option[(User, User)]] =
coll.byIds[User](List(x, y)) map { users =>
for {
xx <- users.find(_.id == x)
yy <- users.find(_.id == y)
} yield xx -> yy
}

def byOrderedIds(ids: Seq[ID], readPreference: ReadPreference): Fu[List[User]] =
coll.byOrderedIds[User, User.ID](ids, readPreference)(_.id)

Expand Down

0 comments on commit 1afa633

Please sign in to comment.