Skip to content

Commit

Permalink
Merge pull request #16259 from lichess-org/tournament-recent-api
Browse files Browse the repository at this point in the history
recent tournaments played, API endpoint, WIP
  • Loading branch information
ornicar authored Oct 22, 2024
2 parents a46dd2a + 675229c commit 26f3381
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 50 deletions.
8 changes: 0 additions & 8 deletions app/controllers/Api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,6 @@ final class Api(
}
}

def tournamentsByOwner(name: UserStr, status: List[Int]) = Anon:
Found(meOrFetch(name).map(_.filterNot(_.is(UserId.lichess)))): user =>
val nb = getInt("nb") | Int.MaxValue
jsonDownload:
env.tournament.api
.byOwnerStream(user, status.flatMap(lila.core.tournament.Status.byId.get), MaxPerSecond(20), nb)
.mapAsync(1)(env.tournament.apiJsonView.fullJson)

def swissGames(id: SwissId) = AnonOrScoped(): ctx ?=>
Found(env.swiss.cache.swissCache.byId(id)): swiss =>
val config = GameApiV2.BySwissConfig(
Expand Down
18 changes: 17 additions & 1 deletion app/controllers/UserTournament.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package controllers

import lila.app.{ *, given }

final class UserTournament(env: Env) extends LilaController(env):
final class UserTournament(env: Env, apiC: => Api) extends LilaController(env):

def path(username: UserStr, path: String, page: Int) = Open:
Reasonable(page):
Expand Down Expand Up @@ -32,3 +32,19 @@ final class UserTournament(env: Env) extends LilaController(env):
Found(ctx.me): me =>
Redirect(routes.UserTournament.path(me.username, "upcoming"))
case _ => notFound

def apiTournamentsByOwner(name: UserStr, status: List[Int]) = Anon:
Found(meOrFetch(name).map(_.filterNot(_.is(UserId.lichess)))): user =>
val nb = getInt("nb") | Int.MaxValue
apiC.jsonDownload:
env.tournament.api
.byOwnerStream(user, status.flatMap(lila.core.tournament.Status.byId.get), MaxPerSecond(20), nb)
.mapAsync(1)(env.tournament.apiJsonView.fullJson)

def apiTournamentsByPlayer(name: UserStr) = Anon:
Found(meOrFetch(name).map(_.filterNot(_.is(UserId.lichess)))): user =>
val nb = getInt("nb") | Int.MaxValue
apiC.jsonDownload:
env.tournament.leaderboardApi
.byPlayerStream(user, MaxPerSecond(20), nb)
.map(env.tournament.apiJsonView.byPlayer)
3 changes: 2 additions & 1 deletion conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,8 @@ GET /api/puzzle/dashboard/$days<\d+> controllers.Puzzle.apiDashboard(days: Int
GET /api/puzzle/$id<\w{5}> controllers.Puzzle.apiShow(id: PuzzleId)
GET /api/puzzle/batch/:angle controllers.Puzzle.apiBatchSelect(angle)
POST /api/puzzle/batch/:angle controllers.Puzzle.apiBatchSolve(angle)
GET /api/user/:user/tournament/created controllers.Api.tournamentsByOwner(user: UserStr, status: List[Int])
GET /api/user/:user/tournament/created controllers.UserTournament.apiTournamentsByOwner(user: UserStr, status: List[Int])
GET /api/user/:user/tournament/played controllers.UserTournament.apiTournamentsByPlayer(user: UserStr)
GET /api/user/:user controllers.Api.user(user: UserStr)
GET /api/user/:user/activity controllers.Api.activity(user: UserStr)
GET /api/user/:user/note controllers.User.apiReadNote(user: UserStr)
Expand Down
15 changes: 9 additions & 6 deletions modules/tournament/src/main/ApiJsonView.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,18 @@ final class ApiJsonView(lightUserApi: lila.core.user.LightUserApi)(using Executo
)

def fullJson(tour: Tournament)(using Translate): Fu[JsObject] =
(tour.winnerId.so(lightUserApi.async)).map { winner =>
baseJson(tour).add("winner" -> winner.map(userJson))
tour.winnerId.so(lightUserApi.async).map { winner =>
baseJson(tour).add("winner" -> winner)
}

private def userJson(u: lila.core.LightUser) =
def byPlayer(e: LeaderboardApi.TourEntry)(using Translate): JsObject =
Json.obj(
"id" -> u.id,
"name" -> u.name,
"title" -> u.title
"tournament" -> baseJson(e.tour),
"player" -> Json.obj(
"games" -> e.entry.nbGames,
"score" -> e.entry.score,
"rank" -> e.entry.rank
)
)

private val perfPositions: Map[PerfKey, Int] = {
Expand Down
82 changes: 48 additions & 34 deletions modules/tournament/src/main/LeaderboardApi.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package lila.tournament

import akka.stream.scaladsl.*
import reactivemongo.api.bson.*
import reactivemongo.akkastream.cursorProducer
import scalalib.Maths
import scalalib.paginator.{ AdapterLike, Paginator }

Expand All @@ -10,15 +12,15 @@ import lila.db.dsl.{ *, given }
import lila.rating.PerfType

final class LeaderboardApi(
repo: LeaderboardRepo,
val repo: LeaderboardRepo,
tournamentRepo: TournamentRepo
)(using Executor)
)(using Executor, akka.stream.Materializer)
extends lila.core.tournament.leaderboard.Api:

import LeaderboardApi.*
import BSONHandlers.given

private val maxPerPage = MaxPerPage(15)
private val maxPerPage = MaxPerPage(20)

def recentByUser(user: User, page: Int) = paginator(user, page, sortBest = false)

Expand Down Expand Up @@ -58,19 +60,42 @@ final class LeaderboardApi(
}
.sortLike(lila.rating.PerfType.leaderboardable, _._1)

def getAndDeleteRecent(userId: UserId, since: Instant): Fu[List[TourId]] =
def getAndDeleteRecent(userId: UserId, since: Instant): Fu[List[TourId]] = for
entries <- repo.coll.list[Entry]($doc("u" -> userId, "d".$gt(since)))
_ <- entries.nonEmpty.so:
repo.coll.delete.one($inIds(entries.map(_.id))).void
yield entries.map(_.tourId)

def byPlayerStream(user: User, perSecond: MaxPerSecond, nb: Int): Source[TourEntry, ?] =
repo.coll
.list[Entry](
$doc(
"u" -> userId,
"d".$gt(since)
.aggregateWith[Bdoc](): fw =>
aggregateByPlayer(user, fw, fw.Descending("d"), nb, offset = 0).toList
.documentSource()
.mapConcat(readTourEntry)

private def aggregateByPlayer(
user: User,
framework: repo.coll.AggregationFramework.type,
sort: framework.SortOrder,
nb: Int,
offset: Int = 0
): NonEmptyList[framework.PipelineOperator] =
import framework.*
NonEmptyList.of(
Match($doc("u" -> user.id)),
Sort(sort),
Skip(offset),
Limit(nb),
PipelineOperator(
$lookup.simple(
from = tournamentRepo.coll,
as = "tour",
local = "t",
foreign = "_id"
)
)
.flatMap { entries =>
(entries.nonEmpty
.so(repo.coll.delete.one($inIds(entries.map(_.id))).void))
.inject(entries.map(_.tourId))
}
),
UnwindField("tour")
)

private def paginator(user: User, page: Int, sortBest: Boolean): Fu[Paginator[TourEntry]] =
Paginator(
Expand All @@ -83,28 +108,17 @@ final class LeaderboardApi(
repo.coll
.aggregateList(length, _.sec): framework =>
import framework.*
Match(selector) -> List(
Sort(if sortBest then Ascending("w") else Descending("d")),
Skip(offset),
Limit(length),
PipelineOperator(
$lookup.simple(
from = tournamentRepo.coll,
as = "tour",
local = "t",
foreign = "_id"
)
),
UnwindField("tour")
)
.map: docs =>
for
doc <- docs
entry <- doc.asOpt[Entry]
tour <- doc.getAsOpt[Tournament]("tour")
yield TourEntry(tour, entry)
val sort = if sortBest then framework.Ascending("w") else framework.Descending("d")
val pipe = aggregateByPlayer(user, framework, sort, length, offset)
pipe.head -> pipe.tail
.map(_.flatMap(readTourEntry))
)

private def readTourEntry(doc: Bdoc): Option[TourEntry] = for
entry <- doc.asOpt[Entry]
tour <- doc.getAsOpt[Tournament]("tour")
yield TourEntry(tour, entry)

object LeaderboardApi:

import lila.core.tournament.leaderboard.Ratio
Expand Down
1 change: 1 addition & 0 deletions modules/tournament/src/main/TournamentApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ final class TournamentApi(
pairingSystem: arena.PairingSystem,
callbacks: TournamentApi.Callbacks,
socket: TournamentSocket,
leaderboard: LeaderboardApi,
roundApi: lila.core.round.RoundApi,
gameProxy: lila.core.game.GameProxy,
trophyApi: lila.core.user.TrophyApi,
Expand Down

0 comments on commit 26f3381

Please sign in to comment.