package controllers import org.joda.time.DateTime import ornicar.scalalib.Zero import play.api.libs.iteratee._ import play.api.libs.json._ import play.api.mvc._ import scala.concurrent.duration._ import lila.api.{ Context, GameApiV2, UserApi } import lila.app._ import lila.common.PimpedJson._ import lila.common.{ HTTPRequest, IpAddress, MaxPerPage, MaxPerSecond } import lila.user.UserRepo object Api extends LilaController { private val userApi = Env.api.userApi private val gameApi = Env.api.gameApi private[controllers] implicit val limitedDefault = Zero.instance[ApiResult](Limited) private lazy val apiStatusJson = { val api = lila.api.Mobile.Api Json.obj( "api" -> Json.obj( "current" -> api.currentVersion.value, "olds" -> api.oldVersions.map { old => Json.obj( "version" -> old.version.value, "deprecatedAt" -> old.deprecatedAt, "unsupportedAt" -> old.unsupportedAt ) } ) ) } val status = Action { req => val appVersion = get("v", req) lila.mon.mobile.version(appVersion | "none")() val mustUpgrade = appVersion exists lila.api.Mobile.AppVersion.mustUpgrade _ Ok(apiStatusJson.add("mustUpgrade", mustUpgrade)) as JSON } def index = Action { Ok(views.html.site.bits.api) } def user(name: String) = CookieBasedApiRequest { ctx => userApi.extended(name, ctx.me) map toApiResult } private[controllers] val UsersRateLimitGlobal = new lila.memo.RateLimit[String]( credits = 1000, duration = 1 minute, name = "team users API global", key = "team_users.api.global" ) private[controllers] val UsersRateLimitPerIP = new lila.memo.RateLimit[IpAddress]( credits = 1000, duration = 10 minutes, name = "team users API per IP", key = "team_users.api.ip" ) def usersByIds = Action.async(parse.tolerantText) { req => val usernames = req.body.split(',').take(300).toList val ip = HTTPRequest lastRemoteAddress req val cost = usernames.size / 4 UsersRateLimitPerIP(ip, cost = cost) { UsersRateLimitGlobal("-", cost = cost, msg = ip.value) { lila.mon.api.users.cost(cost) UserRepo nameds usernames map { _.map { Env.user.jsonView(_, none) } } map toApiResult map toHttp } } } def usersStatus = ApiRequest { req => val ids = get("ids", req).??(_.split(',').take(50).toList map lila.user.User.normalize) Env.user.lightUserApi asyncMany ids dmap (_.flatten) map { users => val actualIds = users.map(_.id) val playingIds = Env.relation.online.playing intersect actualIds val streamingIds = Env.streamer.liveStreamApi.userIds toApiResult { users.map { u => lila.common.LightUser.lightUserWrites.writes(u) .add("online" -> Env.socket.isOnline(u.id)) .add("playing" -> playingIds(u.id)) .add("streaming" -> streamingIds(u.id)) } } } } def titledUsers = Action { ServiceUnavailable("This API is disabled at the moment.") } // def titledUsers = Action.async { req => // val titles = lila.user.Title get get("titles", req).??(_.split(',').take(20).toList) // GlobalLinearLimitPerIP(HTTPRequest lastRemoteAddress req) { // val config = UserApi.Titled( // titles = lila.user.Title get get("titles", req).??(_.split(',').take(20).toList), // online = getBool("online", req), // perSecond = MaxPerSecond(50) // ) // jsonStream(userApi.exportTitled(config)).fuccess // } // } private val UserGamesRateLimitPerIP = new lila.memo.RateLimit[IpAddress]( credits = 10 * 1000, duration = 10 minutes, name = "user games API per IP", key = "user_games.api.ip" ) private val UserGamesRateLimitPerUA = new lila.memo.RateLimit[String]( credits = 10 * 1000, duration = 5 minutes, name = "user games API per UA", key = "user_games.api.ua" ) private val UserGamesRateLimitGlobal = new lila.memo.RateLimit[String]( credits = 20 * 1000, duration = 2 minute, name = "user games API global", key = "user_games.api.global" ) private def UserGamesRateLimit(cost: Int, req: RequestHeader)(run: => Fu[ApiResult]) = { val ip = HTTPRequest lastRemoteAddress req UserGamesRateLimitPerIP(ip, cost = cost) { UserGamesRateLimitPerUA(~HTTPRequest.userAgent(req), cost = cost, msg = ip.value) { UserGamesRateLimitGlobal("-", cost = cost, msg = ip.value) { run } } } } private def gameFlagsFromRequest(req: RequestHeader) = lila.api.GameApi.WithFlags( analysis = getBool("with_analysis", req), moves = getBool("with_moves", req), fens = getBool("with_fens", req), opening = getBool("with_opening", req), moveTimes = getBool("with_movetimes", req), token = get("token", req) ) // for mobile app def userGames(name: String) = MobileApiRequest { req => val page = (getInt("page", req) | 1) atLeast 1 atMost 200 val nb = (getInt("nb", req) | 10) atLeast 1 atMost 100 val cost = page * nb + 10 UserGamesRateLimit(cost, req) { lila.mon.api.userGames.cost(cost) UserRepo named name flatMap { _ ?? { user => gameApi.byUser( user = user, rated = getBoolOpt("rated", req), playing = getBoolOpt("playing", req), analysed = getBoolOpt("analysed", req), withFlags = gameFlagsFromRequest(req), nb = MaxPerPage(nb), page = page ) map some } } map toApiResult } } private val GameRateLimitPerIP = new lila.memo.RateLimit[IpAddress]( credits = 100, duration = 1 minute, name = "game API per IP", key = "game.api.one.ip" ) def game(id: String) = ApiRequest { req => GameRateLimitPerIP(HTTPRequest lastRemoteAddress req, cost = 1) { lila.mon.api.game.cost(1) gameApi.one(id take lila.game.Game.gameIdSize, gameFlagsFromRequest(req)) map toApiResult } } private val CrosstableRateLimitPerIP = new lila.memo.RateLimit[IpAddress]( credits = 30, duration = 10 minutes, name = "crosstable API per IP", key = "crosstable.api.ip" ) def crosstable(u1: String, u2: String) = ApiRequest { req => CrosstableRateLimitPerIP(HTTPRequest lastRemoteAddress req, cost = 1) { Env.game.crosstableApi(u1, u2, timeout = 15.seconds) map { ct => toApiResult { lila.game.JsonView.crosstableWrites.writes(ct).some } } } } def currentTournaments = ApiRequest { implicit ctx => Env.tournament.api.fetchVisibleTournaments flatMap Env.tournament.apiJsonView.apply map Data.apply } def tournament(id: String) = ApiRequest { req => lila.tournament.TournamentRepo byId id flatMap { _ ?? { tour => val page = (getInt("page", req) | 1) atLeast 1 atMost 200 Env.tournament.jsonView( tour = tour, page = page.some, me = none, getUserTeamIds = _ => fuccess(Nil), getTeamName = Env.team.cached.name _, playerInfoExt = none, socketVersion = none, partial = false, lang = lila.i18n.defaultLang ) map some } } map toApiResult } def tournamentGames(id: String) = Action.async { req => lila.tournament.TournamentRepo byId id flatMap { _ ?? { tour => GlobalLinearLimitPerIP(HTTPRequest lastRemoteAddress req) { val format = GameApiV2.Format byRequest req val config = GameApiV2.ByTournamentConfig( tournamentId = tour.id, format = GameApiV2.Format byRequest req, flags = Game.requestPgnFlags(req, extended = false), perSecond = MaxPerSecond(20) ) Ok.chunked(Env.api.gameApiV2.exportByTournament(config)).withHeaders( noProxyBufferHeader, CONTENT_TYPE -> Game.gameContentType(config) ).fuccess } } } } def tournamentResults(id: String) = Action.async { req => lila.tournament.TournamentRepo byId id flatMap { _ ?? { tour => GlobalLinearLimitPerIP(HTTPRequest lastRemoteAddress req) { import lila.tournament.JsonView.playerResultWrites val nb = getInt("nb", req) | Int.MaxValue val enumerator = Env.tournament.api.resultStream(tour, MaxPerSecond(50), nb) &> Enumeratee.map(playerResultWrites.writes) jsonStream(enumerator).fuccess } } } } def tournamentsByOwner(name: String) = Action.async { req => (name != "lichess") ?? UserRepo.named(name) flatMap { _ ?? { user => GlobalLinearLimitPerIP(HTTPRequest lastRemoteAddress req) { val nb = getInt("nb", req) | Int.MaxValue val enumerator = Env.tournament.api.byOwnerStream(user, MaxPerSecond(20), nb) &> Enumeratee.mapM(Env.tournament.apiJsonView.fullJson) jsonStream(enumerator).fuccess } } } } def gamesByUsersStream = Action.async(parse.tolerantText) { req => val userIds = req.body.split(',').take(300).toSet map lila.user.User.normalize jsonStream(Env.game.gamesByUsersStream(userIds)).fuccess } def eventStream = Scoped(_.Bot.Play, _.Challenge.Read) { req => me => Env.round.proxy.urgentGames(me) flatMap { povs => Env.challenge.api.createdByDestId(me.id) map { challenges => jsonOptionStream(Env.api.eventStream(me, povs.map(_.game), challenges)) } } } private val UserActivityRateLimitPerIP = new lila.memo.RateLimit[IpAddress]( credits = 15, duration = 2 minutes, name = "user activity API per IP", key = "user_activity.api.ip" ) def activity(name: String) = ApiRequest { req => UserActivityRateLimitPerIP(HTTPRequest lastRemoteAddress req, cost = 1) { lila.mon.api.activity.cost(1) UserRepo named name flatMap { _ ?? { user => Env.activity.read.recent(user) flatMap { _.map { Env.activity.jsonView(_, user) }.sequenceFu } } } map toApiResult } } 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) } sealed trait ApiResult case class Data(json: JsValue) extends ApiResult case object NoData extends ApiResult case object Limited extends ApiResult case class Custom(result: Result) extends ApiResult def toApiResult(json: Option[JsValue]): ApiResult = json.fold[ApiResult](NoData)(Data.apply) def toApiResult(json: Seq[JsValue]): ApiResult = Data(JsArray(json)) def CookieBasedApiRequest(js: Context => Fu[ApiResult]) = Open { ctx => js(ctx) map toHttp } def ApiRequest(js: RequestHeader => Fu[ApiResult]) = Action.async { req => js(req) map toHttp } def MobileApiRequest(js: RequestHeader => Fu[ApiResult]) = Action.async { req => if (lila.api.Mobile.Api requested req) js(req) map toHttp else fuccess(NotFound) } private[controllers] val tooManyRequests = TooManyRequest(jsonError("Error 429: Too many requests! Try again later.")) private[controllers] def toHttp(result: ApiResult): Result = result match { case Limited => tooManyRequests case NoData => NotFound case Custom(result) => result case Data(json) => Ok(json) as JSON } private[controllers] def jsonStream(stream: Enumerator[JsObject]): Result = jsonStringStream { stream &> Enumeratee.map { o => Json.stringify(o) + "\n" } } private[controllers] def jsonOptionStream(stream: Enumerator[Option[JsObject]]): Result = jsonStringStream { stream &> Enumeratee.map { _ ?? Json.stringify + "\n" } } private def jsonStringStream(stream: Enumerator[String]): Result = Ok.chunked(stream).withHeaders(CONTENT_TYPE -> ndJsonContentType) |> noProxyBuffer }