mobile friend list API WIP

This commit is contained in:
Thibault Duplessis
2026-05-13 13:11:36 +02:00
parent 143cf93f57
commit d7ed433f97
4 changed files with 69 additions and 29 deletions
+14 -8
View File
@@ -10,7 +10,6 @@ import lila.core.LightUser
import lila.core.perf.UserWithPerfs
import lila.rating.UserPerfsExt.bestRatedPerf
import lila.relation.Related
import lila.relation.RelationStream.Direction
final class Relation(env: Env, apiC: => Api) extends LilaController(env):
@@ -101,14 +100,21 @@ final class Relation(env: Env, apiC: => Api) extends LilaController(env):
)
yield res
def apiFollowing = Scoped(_.Follow.Read, _.Web.Mobile) { ctx ?=> me ?=>
def apiFollowing = Scoped(_.Follow.Read, _.Web.Mobile) { ctx ?=> _ ?=>
apiC.jsonDownload:
env.relation.stream
.follow(me, Direction.Following, MaxPerSecond(30))
.mapAsync(1): ids =>
env.user.api.listWithPerfs(ids.toList, includeClosed = false)
.mapConcat(identity)
.map(env.api.userApi.one(_, None))
getInt("recentlySeen").map(_.squeeze(1, 100)) match
case None =>
env.relation.stream
.follow(MaxPerSecond(30))
.mapAsync(1): ids =>
env.user.api.listWithPerfs(ids.toList, includeClosed = false)
.mapConcat(identity)
.map(env.api.userApi.one(_, None))
case Some(nb) =>
import env.user.lightUserApi.reader
env.relation.stream
.recentlySeen(nb, env.user.lightUserApi.projection)
.map(env.api.userApi.recentlySeen)
}
// for lichobile, remove at some point
+9
View File
@@ -26,6 +26,8 @@ final class UserApi(
streamerApi: lila.streamer.StreamerApi,
liveStreamApi: lila.streamer.LiveApi,
gameProxyRepo: lila.round.GameProxyRepo,
playingUsers: lila.round.PlayingUsers,
isOnline: lila.core.socket.IsOnline,
trophyApi: lila.user.TrophyApi,
shieldApi: lila.tournament.TournamentShieldApi,
revolutionApi: lila.tournament.RevolutionApi,
@@ -164,6 +166,13 @@ final class UserApi(
val roleTrophies = trophyApi.roleBasedTrophies(u)
UserApi.TrophiesAndAwards(userCache.rankingsOf(u.id), trophies ::: roleTrophies, shields, revols)
def recentlySeen(u: LightUser, seenAt: Option[Instant]): JsObject =
lila.common.Json.lightUser
.writeNoId(u)
.add("seenAt", seenAt)
.add("online", isOnline.exec(u.id))
.add("playing", playingUsers(u.id))
private def trophiesJson(all: UserApi.TrophiesAndAwards)(using Lang): JsArray =
JsArray:
all.ranks.toList
+42 -17
View File
@@ -4,34 +4,59 @@ import akka.stream.scaladsl.*
import reactivemongo.akkastream.cursorProducer
import lila.db.dsl.{ *, given }
import lila.core.user.UserRepo
import reactivemongo.api.bson.BSONDocumentReader
import lila.core.LightUser
final class RelationStream(colls: Colls)(using akka.stream.Materializer):
import RelationStream.*
final class RelationStream(colls: Colls, userRepo: UserRepo)(using akka.stream.Materializer):
private val coll = colls.relation
def follow(userId: UserId, direction: Direction, perSecond: MaxPerSecond): Source[Seq[UserId], ?] =
def follow(perSecond: MaxPerSecond)(using me: Me): Source[Seq[UserId], ?] =
coll
.find(
$doc(selectField(direction) -> userId, "r" -> lila.core.relation.Relation.Follow),
$doc(projectField(direction) -> true, "_id" -> false).some
$doc(F.from -> me.userId, "r" -> lila.core.relation.Relation.Follow),
$doc(F.to -> true, "_id" -> false).some
)
.batchSize(perSecond.value)
.cursor[Bdoc](ReadPref.sec)
.documentSource()
.grouped(perSecond.value)
.map(_.flatMap(_.getAsOpt[UserId](projectField(direction))))
.map(_.flatMap(_.getAsOpt[UserId](F.to)))
.throttle(1, 1.second)
private def selectField(d: Direction) = d match
case Direction.Following => "u1"
case Direction.Followers => "u2"
private def projectField(d: Direction) = d match
case Direction.Following => "u2"
case Direction.Followers => "u1"
def recentlySeen(nb: Int, projection: Bdoc)(using
reader: BSONDocumentReader[LightUser],
me: Me
): Source[(LightUser, Option[Instant]), ?] =
coll
.aggregateWith[Bdoc](readPreference = ReadPref.sec): framework =>
import framework.*
List(
Match($doc(F.from -> me.userId, "r" -> lila.core.relation.Relation.Follow)),
PipelineOperator(
$lookup.simple(
from = userRepo.coll,
as = "user",
local = F.to,
foreign = "_id",
pipe = List(
$doc("$match" -> $doc("enabled" -> true)),
$doc("$sort" -> $doc("seenAt" -> -1)),
$doc("$project" -> (projection ++ $doc("seenAt" -> true))),
$doc("$limit" -> nb)
)
)
),
ReplaceRootField("user.0")
)
.documentSource()
.mapConcat: doc =>
for
user <- doc.asOpt[LightUser]
seenAt = doc.getAsOpt[Instant]("seenAt")
yield (user, seenAt)
object RelationStream:
enum Direction:
case Following, Followers
private object F:
val from = "u1"
val to = "u2"
+4 -4
View File
@@ -45,7 +45,7 @@ final class LightUserApi(repo: UserRepo, cacheApi: CacheApi)(using Executor)
if id.isGhost then fuccess(LightUser.ghost.some)
else
repo.coll
.find($id(id), projection)
.find($id(id), projection.some)
.one[LightUser]
.recover:
case _: exceptions.BSONValueNotFoundException => LightUser.ghost.some
@@ -55,7 +55,7 @@ final class LightUserApi(repo: UserRepo, cacheApi: CacheApi)(using Executor)
expireAfter = Syncache.ExpireAfter.Write(10.minutes)
)
private given BSONDocumentReader[LightUser] with
given reader: BSONDocumentReader[LightUser] with
def readDocument(doc: BSONDocument) =
doc
.getAsTry[UserName](F.username)
@@ -81,11 +81,11 @@ final class LightUserApi(repo: UserRepo, cacheApi: CacheApi)(using Executor)
patronColor = patronColor
)
private val projection =
val projection =
$doc(
F.id -> false,
F.username -> true,
F.title -> true,
F.plan -> true,
F.flair -> true
).some
)