lila/app/controllers/Api.scala

338 lines
11 KiB
Scala
Raw Normal View History

2013-12-30 19:00:56 -07:00
package controllers
2017-04-24 03:42:44 -06:00
import org.joda.time.DateTime
import ornicar.scalalib.Zero
import play.api.libs.iteratee._
2015-01-17 04:35:54 -07:00
import play.api.libs.json._
2017-01-15 05:26:08 -07:00
import play.api.mvc._
import scala.concurrent.duration._
2013-12-30 19:00:56 -07:00
2017-01-22 13:57:12 -07:00
import lila.api.Context
2013-12-30 19:00:56 -07:00
import lila.app._
import lila.common.PimpedJson._
2017-12-29 08:05:11 -07:00
import lila.common.{ HTTPRequest, IpAddress, MaxPerPage }
2013-12-30 19:00:56 -07:00
object Api extends LilaController {
2014-01-07 18:43:20 -07:00
private val userApi = Env.api.userApi
private val gameApi = Env.api.gameApi
private[controllers] implicit val limitedDefault = Zero.instance[ApiResult](Limited)
2016-08-31 15:59:31 -06:00
private lazy val apiStatusJson = {
val api = lila.api.Mobile.Api
Json.obj(
"api" -> Json.obj(
2016-07-15 11:41:48 -06:00
"current" -> api.currentVersion.value,
"olds" -> api.oldVersions.map { old =>
Json.obj(
2016-07-15 11:41:48 -06:00
"version" -> old.version.value,
"deprecatedAt" -> old.deprecatedAt,
"unsupportedAt" -> old.unsupportedAt
)
}
)
)
2015-01-17 04:35:54 -07:00
}
2016-08-02 04:43:13 -06:00
val status = Action { req =>
2017-11-03 23:49:52 -06:00
val appVersion = get("v", req)
2017-12-20 12:21:15 -07:00
lila.mon.mobile.version(appVersion | "none")()
2017-11-03 23:49:52 -06:00
val mustUpgrade = appVersion exists lila.api.Mobile.AppVersion.mustUpgrade _
Ok(apiStatusJson.add("mustUpgrade", mustUpgrade)) as JSON
2016-08-02 04:43:13 -06:00
}
2018-04-01 22:04:41 -06:00
def index = Action {
2018-04-07 19:46:41 -06:00
Ok(views.html.site.api())
2018-04-01 22:04:41 -06:00
}
2016-08-02 04:40:10 -06:00
def user(name: String) = ApiRequest { implicit ctx =>
userApi.extended(name, ctx.me) map toApiResult
2018-02-03 14:40:03 -07:00
}
private[controllers] val UsersRateLimitGlobal = new lila.memo.RateLimit[String](
credits = 1000,
duration = 1 minute,
2016-09-01 15:54:43 -06:00
name = "team users API global",
key = "team_users.api.global"
)
private[controllers] val UsersRateLimitPerIP = new lila.memo.RateLimit[IpAddress](
credits = 1000,
duration = 10 minutes,
2016-09-01 15:54:43 -06:00
name = "team users API per IP",
key = "team_users.api.ip"
)
2017-01-22 14:21:57 -07:00
def usersByIds = OpenBody(parse.tolerantText) { implicit ctx =>
val usernames = ctx.body.body.split(',').take(300).toList
val ip = HTTPRequest lastRemoteAddress ctx.req
val cost = usernames.size / 4
UsersRateLimitPerIP(ip, cost = cost) {
2017-02-15 17:53:15 -07:00
UsersRateLimitGlobal("-", cost = cost, msg = ip.value) {
2017-11-07 12:24:30 -07:00
lila.mon.api.users.cost(cost)
2017-01-22 14:21:57 -07:00
lila.user.UserRepo nameds usernames map {
_.map { Env.user.jsonView(_, none) }
} map toApiResult map toHttp
}
}
}
def usersStatus = ApiRequest { implicit ctx =>
2018-04-04 07:32:45 -06:00
val ids = get("ids").??(_.split(',').take(50).toList map lila.user.User.normalize)
Env.user.lightUserApi asyncMany ids dmap (_.flatten) map { users =>
val actualIds = users.map(_.id)
val onlineIds = Env.user.onlineUserIdMemo intersect actualIds
val playingIds = Env.relation.online.playing intersect actualIds
2018-04-03 12:52:02 -06:00
val streamingIds = Env.streamer.liveStreamApi.userIds
toApiResult {
users.map { u =>
2018-04-03 12:52:02 -06:00
lila.common.LightUser.lightUserWrites.writes(u)
.add("online" -> onlineIds(u.id))
.add("playing" -> playingIds(u.id))
.add("streaming" -> streamingIds(u.id))
}
}
}
}
2017-02-15 17:53:15 -07:00
private val UserGamesRateLimitPerIP = new lila.memo.RateLimit[IpAddress](
credits = 10 * 1000,
duration = 10 minutes,
2016-09-01 15:54:43 -06:00
name = "user games API per IP",
key = "user_games.api.ip"
)
2017-02-15 17:53:15 -07:00
private val UserGamesRateLimitPerUA = new lila.memo.RateLimit[String](
credits = 10 * 1000,
2016-08-01 07:11:18 -06:00
duration = 5 minutes,
2016-09-01 15:54:43 -06:00
name = "user games API per UA",
key = "user_games.api.ua"
)
2017-02-15 17:53:15 -07:00
private val UserGamesRateLimitGlobal = new lila.memo.RateLimit[String](
credits = 15 * 1000,
duration = 2 minute,
2016-09-01 15:54:43 -06:00
name = "user games API global",
key = "user_games.api.global"
)
2016-07-08 17:32:54 -06:00
2017-09-27 18:23:04 -06:00
private def UserRateLimit(cost: Int)(run: => Fu[ApiResult])(implicit ctx: Context) = {
2017-08-19 08:56:34 -06:00
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) {
run
}
}
}
}
2017-04-24 03:42:44 -06:00
private def gameFlagsFromRequest(implicit ctx: Context) =
lila.api.GameApi.WithFlags(
analysis = getBool("with_analysis"),
moves = getBool("with_moves"),
fens = getBool("with_fens"),
2017-04-24 03:42:44 -06:00
opening = getBool("with_opening"),
moveTimes = getBool("with_movetimes"),
token = get("token")
)
2016-08-02 04:40:10 -06:00
def userGames(name: String) = ApiRequest { implicit ctx =>
2016-08-31 05:00:39 -06:00
val page = (getInt("page") | 1) atLeast 1 atMost 200
val nb = (getInt("nb") | 10) atLeast 1 atMost 100
val cost = page * nb + 10
2017-08-19 08:56:34 -06:00
UserRateLimit(cost = cost) {
lila.mon.api.userGames.cost(cost)
lila.user.UserRepo named name flatMap {
_ ?? { user =>
gameApi.byUser(
user = user,
rated = getBoolOpt("rated"),
playing = getBoolOpt("playing"),
analysed = getBoolOpt("analysed"),
withFlags = gameFlagsFromRequest,
2017-12-29 08:05:11 -07:00
nb = MaxPerPage(nb),
2017-08-19 08:56:34 -06:00
page = page
) map some
2016-07-08 17:32:54 -06:00
}
2017-08-19 08:56:34 -06:00
} map toApiResult
2016-01-21 23:45:07 -07:00
}
2013-12-30 19:00:56 -07:00
}
2017-02-15 17:53:15 -07:00
private val GameRateLimitPerIP = new lila.memo.RateLimit[IpAddress](
2017-01-15 05:56:49 -07:00
credits = 100,
duration = 1 minute,
name = "game API per IP",
key = "game.api.one.ip"
)
2016-08-31 15:59:31 -06:00
2016-08-02 04:40:10 -06:00
def game(id: String) = ApiRequest { implicit ctx =>
2016-08-31 15:59:31 -06:00
val ip = HTTPRequest lastRemoteAddress ctx.req
2017-01-15 05:56:49 -07:00
GameRateLimitPerIP(ip, cost = 1) {
2016-08-31 15:59:31 -06:00
lila.mon.api.game.cost(1)
2017-04-24 03:42:44 -06:00
gameApi.one(id take lila.game.Game.gameIdSize, gameFlagsFromRequest) map toApiResult
2016-08-31 15:59:31 -06:00
}
2014-06-06 03:08:43 -06:00
}
2017-01-22 13:57:12 -07:00
def games = OpenBody(parse.tolerantText) { implicit ctx =>
val gameIds = ctx.body.body.split(',').take(300)
val ip = HTTPRequest lastRemoteAddress ctx.req
GameRateLimitPerIP(ip, cost = gameIds.size / 4) {
lila.mon.api.game.cost(1)
gameApi.many(
ids = gameIds,
withMoves = getBool("with_moves")
) map toApiResult map toHttp
2017-05-07 02:31:25 -06:00
}(Zero.instance(tooManyRequests.fuccess))
2017-01-22 13:57:12 -07:00
}
def gamesVs(u1: String, u2: String) = ApiRequest { implicit ctx =>
val page = (getInt("page") | 1) atLeast 1 atMost 200
val nb = (getInt("nb") | 10) atLeast 1 atMost 100
val cost = page * nb * 2 + 10
2017-08-19 08:56:34 -06:00
UserRateLimit(cost = cost) {
lila.mon.api.userGames.cost(cost)
for {
usersO <- lila.user.UserRepo.pair(
lila.user.User.normalize(u1),
lila.user.User.normalize(u2)
)
res <- usersO.?? { users =>
gameApi.byUsersVs(
users = users,
rated = getBoolOpt("rated"),
playing = getBoolOpt("playing"),
analysed = getBoolOpt("analysed"),
withFlags = gameFlagsFromRequest,
2017-12-29 08:05:11 -07:00
nb = MaxPerPage(nb),
2017-08-19 08:56:34 -06:00
page = page
) map some
}
2017-08-19 08:56:34 -06:00
} yield toApiResult(res)
}
}
def crosstable(u1: String, u2: String) = ApiRequest { implicit ctx =>
UserRateLimit(cost = 200) {
Env.game.crosstableApi(u1, u2, timeout = 15.seconds) map { ct =>
toApiResult {
ct map lila.game.JsonView.crosstableWrites.writes
}
}
}
}
2017-04-24 03:42:44 -06:00
def gamesVsTeam(teamId: String) = ApiRequest { implicit ctx =>
Env.team.api team teamId flatMap {
case None => fuccess {
Custom { BadRequest(jsonError("No such team.")) }
}
case Some(team) if team.nbMembers > 200 => fuccess {
Custom { BadRequest(jsonError(s"The team has too many players. ${team.nbMembers} > 200")) }
}
case Some(team) =>
lila.team.MemberRepo.userIdsByTeam(team.id) flatMap { userIds =>
val page = (getInt("page") | 1) atLeast 1 atMost 200
val nb = (getInt("nb") | 10) atLeast 1 atMost 100
val cost = page * nb * 5 + 10
2017-08-19 08:56:34 -06:00
UserRateLimit(cost = cost) {
lila.mon.api.userGames.cost(cost)
gameApi.byUsersVs(
userIds = userIds,
rated = getBoolOpt("rated"),
playing = getBoolOpt("playing"),
analysed = getBoolOpt("analysed"),
withFlags = gameFlagsFromRequest,
since = DateTime.now minusYears 1,
2017-12-29 08:05:11 -07:00
nb = MaxPerPage(nb),
2017-08-19 08:56:34 -06:00
page = page
) map some map toApiResult
2017-04-24 03:42:44 -06:00
}
}
}
}
def currentTournaments = ApiRequest { implicit ctx =>
2017-01-30 04:36:29 -07:00
Env.tournament.api.fetchVisibleTournaments flatMap
Env.tournament.scheduleJsonView.apply map Data.apply
}
def tournament(id: String) = ApiRequest { implicit ctx =>
2016-08-31 05:00:39 -06:00
val page = (getInt("page") | 1) atLeast 1 atMost 200
lila.tournament.TournamentRepo byId id flatMap {
_ ?? { tour =>
Env.tournament.jsonView(tour, page.some, none, none, none, ctx.lang) map some
}
} map toApiResult
}
2018-04-02 22:27:40 -06:00
def gameStream = Action.async(parse.tolerantText) { req =>
RequireHttp11(req) {
val userIds = req.body.split(',').take(300).toSet map lila.user.User.normalize
Ok.chunked(Env.game.stream.startedByUserIds(userIds)).fuccess
}
2016-10-30 17:21:48 -06:00
}
def activity(name: String) = ApiRequest { implicit ctx =>
val cost = 50
2017-08-19 08:56:34 -06:00
UserRateLimit(cost = cost) {
lila.mon.api.activity.cost(cost)
lila.user.UserRepo named name flatMap {
_ ?? { user =>
Env.activity.read.recent(user) flatMap {
_.map { Env.activity.jsonView(_, user) }.sequenceFu
}
}
2017-08-19 08:56:34 -06:00
} map toApiResult
}
}
2018-04-09 18:06:35 -06:00
private[controllers] val GlobalLinearLimitPerIP = new lila.memo.LinearLimit[IpAddress](
name = "linear API per IP",
key = "api.ip",
ttl = 6 hours
)
private[controllers] val GlobalLinearLimitPerUser = new lila.memo.LinearLimit[lila.user.User.ID](
name = "linear API per user",
key = "api.user",
ttl = 6 hours
)
private[controllers] def GlobalLinearLimitPerUserOption(user: Option[lila.user.User])(f: Fu[Result]): Fu[Result] =
user.fold(f) { u =>
GlobalLinearLimitPerUser(u.id)(f)
}
2016-08-02 04:40:10 -06:00
sealed trait ApiResult
case class Data(json: JsValue) extends ApiResult
case class JsonStream(value: Enumerator[JsObject]) extends ApiResult
2016-08-02 04:40:10 -06:00
case object NoData extends ApiResult
case object Limited extends ApiResult
2017-04-24 03:42:44 -06:00
case class Custom(result: Result) extends ApiResult
2017-01-22 13:57:12 -07:00
def toApiResult(json: Option[JsValue]): ApiResult = json.fold[ApiResult](NoData)(Data.apply)
def toApiResult(json: Seq[JsValue]): ApiResult = Data(JsArray(json))
def toApiResult(stream: Enumerator[JsObject]): ApiResult = JsonStream(stream)
2017-01-22 13:57:12 -07:00
def ApiRequest(js: Context => Fu[ApiResult]) = Open { implicit ctx =>
2017-01-22 13:57:12 -07:00
js(ctx) map toHttp
}
2018-04-09 18:06:35 -06:00
private[controllers] val tooManyRequests = TooManyRequest(jsonError("Try again later"))
2017-05-07 02:31:25 -06:00
private def toHttp(result: ApiResult)(implicit ctx: Context): Result = result match {
case Limited => tooManyRequests
case NoData => NotFound
case Custom(result) => result
case JsonStream(stream) =>
Ok.chunked {
stream &> Enumeratee.map { o =>
Json.stringify(o) + "\n"
}
}.withHeaders(CONTENT_TYPE -> "application/x-ndjson")
case Data(json) => get("callback") match {
case None => Ok(json) as JSON
case Some(callback) => Ok(s"$callback($json)") as JAVASCRIPT
2013-12-30 19:00:56 -07:00
}
}
}