implement api/user/puzzle-request - closes #5014

documentation: https://lichess.org/api#operation/apiUserPuzzleActivity

Will be deployed along v2
v2
Thibault Duplessis 2019-04-28 18:04:16 +07:00
parent 5609655d15
commit 7f26207ef5
7 changed files with 133 additions and 1 deletions

View File

@ -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
}
}
}
}

View File

@ -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())

View File

@ -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)

View File

@ -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))

View File

@ -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
)

View File

@ -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 =>

View File

@ -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
)
}