user activity feed API - WIP - for #3473

pull/3486/merge
Thibault Duplessis 2017-08-19 00:38:50 -05:00
parent 7200587c11
commit 278db0a245
11 changed files with 117 additions and 13 deletions

View File

@ -314,6 +314,16 @@ name | type | default | description
(1) All game statuses: https://github.com/ornicar/scalachess/blob/master/src/main/scala/Status.scala#L16-L28
### `GET /api/user/<username>/activity` fetch recent user activity
This API is a work in progress
```
> curl https://lichess.org/api/user/thibault/activityy
```
Returns data to generate the activity feed on https://lichess.org/@/thibault
### `GET /api/games/vs/<username>/<username>` fetch games between 2 users
```

View File

@ -1,10 +1,10 @@
package controllers
import org.joda.time.DateTime
import ornicar.scalalib.Zero
import play.api.libs.json._
import play.api.mvc._
import scala.concurrent.duration._
import ornicar.scalalib.Zero
import lila.api.Context
import lila.app._
@ -273,6 +273,25 @@ object Api extends LilaController {
Ok.chunked(Env.game.stream.startedByUserIds(userIds))
}
def activity(name: String) = ApiRequest { implicit ctx =>
val cost = 50
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.activity.cost(cost)
lila.user.UserRepo named name flatMap {
_ ?? { user =>
Env.activity.read.recent(user) flatMap {
_.map { Env.activity.jsonView.apply _ }.sequenceFu
}
}
} map toApiResult
}
}
}
}
sealed trait ApiResult
case class Data(json: JsValue) extends ApiResult
case object NoData extends ApiResult

View File

@ -10,7 +10,7 @@ net {
ip = "5.196.91.160"
asset {
domain = ${net.domain}
version = 1848
version = 1849
}
email = "contact@lichess.org"
crawlable = false

View File

@ -473,6 +473,7 @@ GET /api/user controllers.Api.users
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/user/:name/activity controllers.Api.activity(name: String)
GET /api/game/:id controllers.Api.game(id: String)
GET /api/games/vs/:u1/:u2 controllers.Api.gamesVs(u1: String, u2: String)
GET /api/games/team/:teamId controllers.Api.gamesVsTeam(teamId: String)

View File

@ -14,7 +14,9 @@ final class Env(
postApi: lila.forum.PostApi,
simulApi: lila.simul.SimulApi,
studyApi: lila.study.StudyApi,
tourLeaderApi: lila.tournament.LeaderboardApi
lightUserApi: lila.user.LightUserApi,
tourLeaderApi: lila.tournament.LeaderboardApi,
getTourName: lila.tournament.Tournament.ID => Option[String]
) {
private val activityColl = db(config getString "collection.activity")
@ -33,6 +35,11 @@ final class Env(
tourLeaderApi = tourLeaderApi
)
lazy val jsonView = new JsonView(
lightUserApi = lightUserApi,
getTourName = getTourName
)
system.lilaBus.subscribe(
system.actorOf(Props(new Actor {
def receive = {
@ -66,6 +73,8 @@ object Env {
postApi = lila.forum.Env.current.postApi,
simulApi = lila.simul.Env.current.api,
studyApi = lila.study.Env.current.api,
tourLeaderApi = lila.tournament.Env.current.leaderboardApi
lightUserApi = lila.user.Env.current.lightUserApi,
tourLeaderApi = lila.tournament.Env.current.leaderboardApi,
getTourName = lila.tournament.Env.current.cached.name _
)
}

View File

@ -0,0 +1,60 @@
package lila.activity
import org.joda.time.DateTime
import play.api.libs.json._
import lila.common.Iso
import lila.common.PimpedJson._
import lila.rating.PerfType
import lila.tournament.JsonView._
import lila.tournament.LeaderboardApi.{ Entry => TourEntry, Ratio => TourRatio }
import lila.tournament.Tournament
import lila.tournament.{ Cached => TourCached }
import activities._
import model._
final class JsonView(
lightUserApi: lila.user.LightUserApi,
getTourName: Tournament.ID => Option[String]
) {
private object Writers {
implicit val perfTypeWrites = Writes[PerfType](pt => JsString(pt.key))
implicit val ratingWrites = intIsoWriter(Iso.int[Rating](Rating.apply, _.value))
implicit val ratingProgWrites = Json.writes[RatingProg]
implicit val scoreWrites = Json.writes[Score]
implicit val gamesWrites = OWrites[Games] { games =>
JsObject { games.value.map { case (pt, score) => pt.key -> scoreWrites.writes(score) } }
}
implicit val dateWrites = Writes[DateTime] { date =>
JsNumber(date.getSeconds)
}
implicit val variantWrites: Writes[chess.variant.Variant] = Writes { v =>
JsString(v.key)
}
implicit val tourRatioWrites = intIsoWriter(Iso.int[TourRatio](r => TourRatio(r.toDouble), _.value.toInt))
implicit val tourEntryWrites = OWrites[TourEntry] { e =>
Json.obj(
"tournament" -> Json.obj(
"id" -> e.tourId,
"name" -> ~getTourName(e.tourId)
),
"nbGames" -> e.nbGames,
"score" -> e.score,
"rank" -> e.rank,
"rankRatio" -> e.rankRatio
)
}
implicit val toursWrites = Json.writes[ActivityView.Tours]
implicit val puzzlesWrites = Json.writes[Puzzles]
}
import Writers._
def apply(a: ActivityView): Fu[JsObject] = fuccess {
Json.obj()
.add("games", a.games map gamesWrites.writes)
.add("puzzles", a.puzzles map puzzlesWrites.writes)
.add("tournaments", a.tours map toursWrites.writes)
}
}

View File

@ -11,6 +11,7 @@ object PimpedJson {
def stringAnyValWriter[O](f: O => String): Writes[O] = anyValWriter[O, String](f)
def stringIsoWriter[O](iso: Iso[String, O]): Writes[O] = anyValWriter[O, String](iso.to)
def intIsoWriter[O](iso: Iso[Int, O]): Writes[O] = anyValWriter[O, Int](iso.to)
def stringIsoReader[O](iso: Iso[String, O]): Reads[O] = Reads.of[String] map iso.from

View File

@ -464,6 +464,9 @@ object mon {
object game {
val cost = incX("api.game.cost")
}
object activity {
val cost = incX("api.activity.cost")
}
}
object export {
object pgn {

View File

@ -5,9 +5,8 @@ import org.joda.time.format.ISODateTimeFormat
import play.api.libs.json._
import scala.concurrent.duration._
import chess.Clock.{ Config => TournamentClock }
import lila.common.PimpedJson._
import lila.common.LightUser
import lila.common.PimpedJson._
import lila.game.{ GameRepo, Pov }
import lila.quote.Quote.quoteWriter
import lila.rating.PerfType
@ -63,7 +62,7 @@ final class JsonView(
"perf" -> tour.perfType,
"nbPlayers" -> tour.nbPlayers,
"minutes" -> tour.minutes,
"clock" -> clockJson(tour.clock),
"clock" -> tour.clock,
"verdicts" -> verdicts,
"variant" -> tour.variant.key,
"isStarted" -> tour.isStarted,
@ -343,10 +342,12 @@ object JsonView {
"speed" -> s.speed.name
)
private[tournament] def clockJson(clock: TournamentClock) = Json.obj(
"limit" -> clock.limitSeconds,
"increment" -> clock.incrementSeconds
)
implicit val clockWrites: OWrites[chess.Clock.Config] = OWrites { clock =>
Json.obj(
"limit" -> clock.limitSeconds,
"increment" -> clock.incrementSeconds
)
}
private[tournament] def positionJson(s: chess.StartingPosition) = Json.obj(
"eco" -> s.eco,

View File

@ -84,7 +84,7 @@ object LeaderboardApi {
case class Entry(
id: String, // same as tournament player id
userId: String,
tourId: String,
tourId: Tournament.ID,
nbGames: Int,
score: Int,
rank: Int,

View File

@ -27,7 +27,7 @@ final class ScheduleJsonView(lightUser: LightUser.Getter) {
"createdBy" -> tour.createdBy,
"system" -> tour.system.toString.toLowerCase,
"minutes" -> tour.minutes,
"clock" -> clockJson(tour.clock),
"clock" -> tour.clock,
"position" -> tour.position.some.filterNot(_.initial).map(positionJson),
"rated" -> tour.mode.rated,
"fullName" -> tour.fullName,