implement api/user/puzzle-request - closes #5014
documentation: https://lichess.org/api#operation/apiUserPuzzleActivity Will be deployed along v2v2
parent
5609655d15
commit
7f26207ef5
|
@ -5,6 +5,7 @@ import play.api.mvc._
|
|||
|
||||
import lila.api.Context
|
||||
import lila.app._
|
||||
import lila.common.{ HTTPRequest, IpAddress, MaxPerSecond }
|
||||
import lila.game.PgnDump
|
||||
import lila.puzzle.{ PuzzleId, Result, Puzzle => PuzzleModel, UserInfos }
|
||||
import lila.user.UserRepo
|
||||
|
@ -231,4 +232,21 @@ object Puzzle extends LilaController {
|
|||
case Some(daily) => html.puzzle.embed(daily)
|
||||
}
|
||||
}
|
||||
|
||||
def activity = Scoped(_.Puzzle.Read) { req => me =>
|
||||
Api.GlobalLinearLimitPerIP(HTTPRequest lastRemoteAddress req) {
|
||||
Api.GlobalLinearLimitPerUserOption(me.some) {
|
||||
val config = lila.puzzle.PuzzleActivity.Config(
|
||||
user = me,
|
||||
max = getInt("max", req) map (_ atLeast 1),
|
||||
perSecond = MaxPerSecond(20)
|
||||
)
|
||||
Ok.chunked(env.activity.stream(config)).withHeaders(
|
||||
noProxyBufferHeader,
|
||||
CONTENT_TYPE -> ndJsonContentType
|
||||
).fuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ lazy val api = module("api", moduleCPDeps)
|
|||
lazy val puzzle = module("puzzle", Seq(
|
||||
common, memo, hub, history, db, user, rating, pref, tree, game
|
||||
)).settings(
|
||||
libraryDependencies ++= provided(play.api, reactivemongo.driver)
|
||||
libraryDependencies ++= provided(play.api, reactivemongo.driver, reactivemongo.iteratees)
|
||||
)
|
||||
|
||||
lazy val quote = module("quote", Seq())
|
||||
|
|
|
@ -468,6 +468,7 @@ GET /stat/rating/distribution/:perf controllers.Stat.ratingDistribution(perf:
|
|||
# API
|
||||
GET /api controllers.Api.index
|
||||
POST /api/users controllers.Api.usersByIds
|
||||
GET /api/user/puzzle-activity controllers.Puzzle.activity
|
||||
GET /api/user/:name controllers.Api.user(name: String)
|
||||
GET /api/user/:name/activity controllers.Api.activity(name: String)
|
||||
GET /api/user/:name/following controllers.Relation.apiFollowing(name: String)
|
||||
|
|
|
@ -114,6 +114,21 @@ trait CollExt { self: dsl with QueryBuilderExt =>
|
|||
_ flatMap { _.getAs[V](field) }
|
||||
}
|
||||
|
||||
def primitiveMap[I: BSONValueReader: BSONValueWriter, V](
|
||||
ids: Iterable[I],
|
||||
field: String,
|
||||
fieldExtractor: Bdoc => Option[V]
|
||||
): Fu[Map[I, V]] =
|
||||
coll.find($inIds(ids), $doc(field -> true))
|
||||
.list[Bdoc]()
|
||||
.dmap {
|
||||
_ flatMap { obj =>
|
||||
obj.getAs[I]("_id") flatMap { id =>
|
||||
fieldExtractor(obj) map { id -> _ }
|
||||
}
|
||||
} toMap
|
||||
}
|
||||
|
||||
def updateField[V: BSONValueWriter](selector: Bdoc, field: String, value: V) =
|
||||
coll.update(selector, $set(field -> value))
|
||||
|
||||
|
|
|
@ -24,6 +24,10 @@ object OAuthScope {
|
|||
case object Write extends OAuthScope("tournament:write", "Create tournaments")
|
||||
}
|
||||
|
||||
object Puzzle {
|
||||
case object Read extends OAuthScope("puzzle:read", "Read puzzle activity")
|
||||
}
|
||||
|
||||
object Bot {
|
||||
case object Play extends OAuthScope("bot:play", "Play as a bot")
|
||||
}
|
||||
|
@ -37,6 +41,7 @@ object OAuthScope {
|
|||
Email.Read,
|
||||
Challenge.Read, Challenge.Write,
|
||||
Tournament.Write,
|
||||
Puzzle.Read,
|
||||
Bot.Play
|
||||
)
|
||||
|
||||
|
|
|
@ -77,6 +77,11 @@ final class Env(
|
|||
system.scheduler
|
||||
)
|
||||
|
||||
lazy val activity = new PuzzleActivity(
|
||||
puzzleColl = puzzleColl,
|
||||
roundColl = roundColl
|
||||
)(system)
|
||||
|
||||
def cli = new lila.common.Cli {
|
||||
def process = {
|
||||
case "puzzle" :: "disable" :: id :: Nil => parseIntOption(id) ?? { id =>
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package lila.puzzle
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import play.api.libs.iteratee._
|
||||
import play.api.libs.json._
|
||||
import reactivemongo.api.ReadPreference
|
||||
import reactivemongo.play.iteratees.cursorProducer
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.MaxPerSecond
|
||||
import lila.db.dsl._
|
||||
import lila.user.User
|
||||
|
||||
final class PuzzleActivity(
|
||||
puzzleColl: Coll,
|
||||
roundColl: Coll
|
||||
)(implicit system: akka.actor.ActorSystem) {
|
||||
|
||||
import PuzzleActivity._
|
||||
import Round.RoundBSONHandler
|
||||
|
||||
def stream(config: Config): Enumerator[String] = {
|
||||
|
||||
val selector = $doc("_id" $startsWith config.user.id)
|
||||
|
||||
val query = roundColl.find(selector).sort($sort desc "_id")
|
||||
|
||||
val infinite = query.copy(options = query.options.batchSize(config.perSecond.value))
|
||||
.cursor[Round](ReadPreference.secondaryPreferred)
|
||||
.bulkEnumerator() &>
|
||||
Enumeratee.mapM[Iterator[Round]].apply[Seq[JsObject]] { rounds =>
|
||||
enrich(rounds.toSeq)
|
||||
} &>
|
||||
lila.common.Iteratee.delay(1 second) &>
|
||||
Enumeratee.mapConcat(_.toSeq)
|
||||
|
||||
val stream = config.max.fold(infinite) { max =>
|
||||
// I couldn't figure out how to do it properly :( :( :(
|
||||
// the nb can't be set as bulkEnumerator(nb)
|
||||
// because games are further filtered after being fetched
|
||||
var nb = 0
|
||||
infinite &> Enumeratee.mapInput { in =>
|
||||
nb = nb + 1
|
||||
if (nb <= max) in
|
||||
else Input.EOF
|
||||
}
|
||||
}
|
||||
|
||||
stream &> formatter
|
||||
}
|
||||
|
||||
private def enrich(rounds: Seq[Round]): Fu[Seq[JsObject]] =
|
||||
puzzleColl.primitiveMap[Int, Double](
|
||||
ids = rounds.map(_.id.puzzleId).toSeq,
|
||||
field = "perf.gl.r",
|
||||
fieldExtractor = obj => for {
|
||||
perf <- obj.getAs[Bdoc]("perf")
|
||||
gl <- perf.getAs[Bdoc]("gl")
|
||||
rating <- gl.getAs[Double]("r")
|
||||
} yield rating
|
||||
) map { ratings =>
|
||||
rounds.toSeq flatMap { round =>
|
||||
ratings get round.id.puzzleId map { puzzleRating =>
|
||||
Json.obj(
|
||||
"id" -> round.id.puzzleId,
|
||||
"date" -> round.date,
|
||||
"rating" -> round.rating,
|
||||
"ratingDiff" -> round.ratingDiff,
|
||||
"puzzleRating" -> puzzleRating.toInt
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def formatter =
|
||||
Enumeratee.map[JsObject].apply[String] { json =>
|
||||
s"${Json.stringify(json)}\n"
|
||||
}
|
||||
}
|
||||
|
||||
object PuzzleActivity {
|
||||
|
||||
case class Config(
|
||||
user: User,
|
||||
max: Option[Int] = None,
|
||||
perSecond: MaxPerSecond
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue