From ced5e57c93f9e31e181f24169f86aa7346e263d2 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 20 Sep 2021 17:53:59 +0200 Subject: [PATCH] don't always publish user.online to discourage API misuse Use this instead: https://lichess.org/api#operation/apiUsersStatus --- app/controllers/Account.scala | 2 +- app/controllers/Api.scala | 2 +- app/controllers/Auth.scala | 2 +- app/controllers/PlayApi.scala | 2 +- app/controllers/Relation.scala | 19 +++++++---------- app/controllers/Team.scala | 2 +- app/controllers/User.scala | 2 +- modules/api/src/main/UserApi.scala | 11 ++++------ modules/game/src/main/Crosstable.scala | 29 +++++++++++++------------- modules/user/src/main/JsonView.scala | 4 ++-- 10 files changed, 34 insertions(+), 41 deletions(-) diff --git a/app/controllers/Account.scala b/app/controllers/Account.scala index ad35fb0334..5e7bcf7128 100644 --- a/app/controllers/Account.scala +++ b/app/controllers/Account.scala @@ -74,7 +74,7 @@ final class Account( env.playban.api.currentBan(me.id) map { case (((prefs, povs), nbChallenges), playban) => Ok { import lila.pref.JsonView._ - env.user.jsonView(me) ++ Json + env.user.jsonView(me, withOnline = true) ++ Json .obj( "prefs" -> prefs, "nowPlaying" -> JsArray(povs take 50 map env.api.lobbyApi.nowPlaying), diff --git a/app/controllers/Api.scala b/app/controllers/Api.scala index 0749406630..9b52fa8743 100644 --- a/app/controllers/Api.scala +++ b/app/controllers/Api.scala @@ -66,7 +66,7 @@ final class Api( UsersRateLimitPerIP(ip, cost = cost) { lila.mon.api.users.increment(cost.toLong) env.user.repo enabledNameds usernames map { - _.map { env.user.jsonView(_, none) } + _.map { env.user.jsonView(_, none, withOnline = false) } } map toApiResult map toHttp }(rateLimitedFu) } diff --git a/app/controllers/Auth.scala b/app/controllers/Auth.scala index 0d2ba1e2c0..8913385c06 100644 --- a/app/controllers/Auth.scala +++ b/app/controllers/Auth.scala @@ -28,7 +28,7 @@ final class Auth( private def mobileUserOk(u: UserModel, sessionId: String): Fu[Result] = env.round.proxyRepo urgentGames u map { povs => Ok { - env.user.jsonView(u) ++ Json.obj( + env.user.jsonView(u, withOnline = true) ++ Json.obj( "nowPlaying" -> JsArray(povs take 20 map env.api.lobbyApi.nowPlaying), "sessionId" -> sessionId ) diff --git a/app/controllers/PlayApi.scala b/app/controllers/PlayApi.scala index 61001a032c..711146ef7b 100644 --- a/app/controllers/PlayApi.scala +++ b/app/controllers/PlayApi.scala @@ -184,7 +184,7 @@ final class PlayApi( .botsByIdsCursor(env.bot.onlineApiUsers.get) .documentSource(getInt("nb", req) | Int.MaxValue) .throttle(50, 1 second) - .map { env.user.jsonView(_) } + .map { env.user.jsonView(_, withOnline = false) } } } } diff --git a/app/controllers/Relation.scala b/app/controllers/Relation.scala index 1d2a1bd421..e96dc0cf5f 100644 --- a/app/controllers/Relation.scala +++ b/app/controllers/Relation.scala @@ -142,18 +142,13 @@ final class Relation( ) } - def apiFollowing(name: String) = - Action.async { implicit req => - env.user.repo.enabledNamed(name) flatMap { - _ ?? { user => - apiC.jsonStream { - env.relation.stream - .follow(user, Direction.Following, MaxPerSecond(20)) - .map(env.api.userApi.one) - }.fuccess - } - } - } + def apiFollowing = Scoped() { implicit req => me => + apiC.jsonStream { + env.relation.stream + .follow(me, Direction.Following, MaxPerSecond(30)) + .map(env.api.userApi.one(_, withOnline = false)) + }.fuccess + } private def jsonRelatedPaginator(pag: Paginator[Related]) = { import lila.user.JsonView.nameWrites diff --git a/app/controllers/Team.scala b/app/controllers/Team.scala index 6975fd2386..a91f8c7f18 100644 --- a/app/controllers/Team.scala +++ b/app/controllers/Team.scala @@ -112,7 +112,7 @@ final class Team( apiC.jsonStream( env.team .memberStream(team, MaxPerSecond(20)) - .map(env.api.userApi.one) + .map(env.api.userApi.one(_, withOnline = false)) )(req) case false => Unauthorized } diff --git a/app/controllers/User.scala b/app/controllers/User.scala index 88268eb6c1..fe94682bbb 100644 --- a/app/controllers/User.scala +++ b/app/controllers/User.scala @@ -207,7 +207,7 @@ final class User( Json.toJson( users .take(getInt("nb", req).fold(10)(_ min max)) - .map(env.user.jsonView(_)) + .map(env.user.jsonView(_, withOnline = false)) ) ) } diff --git a/modules/api/src/main/UserApi.scala b/modules/api/src/main/UserApi.scala index 511ae5d024..4648452898 100644 --- a/modules/api/src/main/UserApi.scala +++ b/modules/api/src/main/UserApi.scala @@ -20,11 +20,8 @@ final private[api] class UserApi( net: NetConfig )(implicit ec: scala.concurrent.ExecutionContext) { - def pagerJson(pag: Paginator[User]): JsObject = - Json.obj("paginator" -> PaginatorJson(pag mapResults one)) - - def one(u: User): JsObject = - addPlayingStreaming(jsonView(u), u.id) ++ + def one(u: User, withOnline: Boolean): JsObject = + addStreaming(jsonView(u, withOnline = withOnline), u.id) ++ Json.obj("url" -> makeUrl(s"@/${u.username}")) // for app BC def extended(username: String, as: Option[User], withFollows: Boolean): Fu[Option[JsObject]] = @@ -62,7 +59,7 @@ final private[api] class UserApi( case ((((((((((gameOption,nbGamesWithMe),following),followers),followable), relation),isFollowed),nbBookmarks),nbPlaying),nbImported),completionRate)=> // format: on - jsonView(u) ++ { + jsonView(u, withOnline = true) ++ { Json .obj( "url" -> makeUrl(s"@/${u.username}"), // for app BC @@ -99,7 +96,7 @@ final private[api] class UserApi( } } - private def addPlayingStreaming(js: JsObject, id: User.ID) = + private def addStreaming(js: JsObject, id: User.ID) = js.add("streaming", liveStreamApi.isStreaming(id)) private def makeUrl(path: String): String = s"${net.baseUrl}/$path" diff --git a/modules/game/src/main/Crosstable.scala b/modules/game/src/main/Crosstable.scala index 1c85e6ecc0..d0fbcf81a9 100644 --- a/modules/game/src/main/Crosstable.scala +++ b/modules/game/src/main/Crosstable.scala @@ -13,10 +13,10 @@ case class Crosstable( def nonEmpty = results.nonEmpty option this - def nbGames = users.nbGames - def showScore = users.showScore _ - def showOpponentScore = users.showOpponentScore _ - def fromPov(userId: String) = copy(users = users fromPov userId) + def nbGames = users.nbGames + def showScore = users.showScore _ + def showOpponentScore = users.showOpponentScore _ + def fromPov(userId: lila.user.User.ID) = copy(users = users fromPov userId) lazy val size = results.size @@ -33,19 +33,19 @@ object Crosstable { Nil ) - case class User(id: String, score: Int) // score is x10 + case class User(id: lila.user.User.ID, score: Int) // score is x10 case class Users(user1: User, user2: User) { val nbGames = (user1.score + user2.score) / 10 - def user(id: String): Option[User] = + def user(id: lila.user.User.ID): Option[User] = if (id == user1.id) Some(user1) else if (id == user2.id) Some(user2) else None def toList = List(user1, user2) - def showScore(userId: String) = { + def showScore(userId: lila.user.User.ID) = { val byTen = user(userId) ?? (_.score) s"${byTen / 10}${(byTen % 10 != 0).??("½")}" match { case "0½" => "½" @@ -53,12 +53,12 @@ object Crosstable { } } - def showOpponentScore(userId: String) = + def showOpponentScore(userId: lila.user.User.ID) = if (userId == user1.id) showScore(user2.id).some else if (userId == user2.id) showScore(user1.id).some else none - def fromPov(userId: String) = + def fromPov(userId: lila.user.User.ID) = if (userId == user2.id) copy(user1 = user2, user2 = user1) else this @@ -68,22 +68,23 @@ object Crosstable { else None } - case class Result(gameId: Game.ID, winnerId: Option[String]) + case class Result(gameId: Game.ID, winnerId: Option[lila.user.User.ID]) case class Matchup(users: Users) { // score is x10 - def fromPov(userId: String) = copy(users = users fromPov userId) - def nonEmpty = users.nbGames > 0 + def fromPov(userId: lila.user.User.ID) = copy(users = users fromPov userId) + def nonEmpty = users.nbGames > 0 } case class WithMatchup(crosstable: Crosstable, matchup: Option[Matchup]) { - def fromPov(userId: String) = + def fromPov(userId: lila.user.User.ID) = copy( crosstable fromPov userId, matchup map (_ fromPov userId) ) } - private[game] def makeKey(u1: String, u2: String): String = if (u1 < u2) s"$u1/$u2" else s"$u2/$u1" + private[game] def makeKey(u1: lila.user.User.ID, u2: lila.user.User.ID): String = + if (u1 < u2) s"$u1/$u2" else s"$u2/$u1" import reactivemongo.api.bson._ import lila.db.BSON diff --git a/modules/user/src/main/JsonView.scala b/modules/user/src/main/JsonView.scala index e78474fb62..225ea323e3 100644 --- a/modules/user/src/main/JsonView.scala +++ b/modules/user/src/main/JsonView.scala @@ -12,15 +12,15 @@ final class JsonView(isOnline: lila.socket.IsOnline) { implicit private val profileWrites = Json.writes[Profile] implicit private val playTimeWrites = Json.writes[PlayTime] - def apply(u: User, onlyPerf: Option[PerfType] = None): JsObject = + def apply(u: User, onlyPerf: Option[PerfType] = None, withOnline: Boolean): JsObject = Json .obj( "id" -> u.id, "username" -> u.username, - "online" -> isOnline(u.id), "perfs" -> perfs(u, onlyPerf), "createdAt" -> u.createdAt ) + .add("online" -> withOnline.option(isOnline(u.id))) .add("disabled" -> u.disabled) .add("tosViolation" -> u.lame) .add("profile" -> u.profile.map(p => profileWrites.writes(p.filterTroll(u.marks.troll)).noNull))