From df655e9e23cf945eea29306f4bfae34bac9f9401 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Thu, 9 Jul 2020 21:15:41 +0200 Subject: [PATCH] export game PGN with real player names and ratings through external file curl 'l.org/game/export/FUPEWMpY?players=https://gist.github.com/ornicar/bc9120bc3d7522be0ed3c6b8a5313930/raw/0ff573018fba0d08cbc0b655f359f8573b698ff3/gistfile1.txt' --- app/controllers/Game.scala | 3 +- modules/api/src/main/Env.scala | 3 ++ modules/api/src/main/GameApiV2.scala | 31 +++++++++---- modules/api/src/main/PgnDump.scala | 26 +++++------ modules/api/src/main/RealPlayer.scala | 66 +++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 26 deletions(-) create mode 100644 modules/api/src/main/RealPlayer.scala diff --git a/app/controllers/Game.scala b/app/controllers/Game.scala index ffc30925fd..e44c6d70f1 100644 --- a/app/controllers/Game.scala +++ b/app/controllers/Game.scala @@ -42,7 +42,8 @@ final class Game( format = if (HTTPRequest acceptsJson req) GameApiV2.Format.JSON else GameApiV2.Format.PGN, imported = getBool("imported", req), flags = requestPgnFlags(req, extended = true), - noDelay = get("key", req).exists(env.noDelaySecretSetting.get().value.contains) + noDelay = get("key", req).exists(env.noDelaySecretSetting.get().value.contains), + playerFile = get("players", req) ) env.api.gameApiV2.exportOne(game, config) flatMap { content => env.api.gameApiV2.filename(game, config.format) map { filename => diff --git a/modules/api/src/main/Env.scala b/modules/api/src/main/Env.scala index 0506de27d0..0d8c4f140a 100644 --- a/modules/api/src/main/Env.scala +++ b/modules/api/src/main/Env.scala @@ -40,6 +40,7 @@ final class Env( onlineApiUsers: lila.bot.OnlineApiUsers, challengeEnv: lila.challenge.Env, msgEnv: lila.msg.Env, + cacheApi: lila.memo.CacheApi, ws: WSClient, val mode: Mode )(implicit @@ -56,6 +57,8 @@ final class Env( lazy val gameApi = wire[GameApi] + lazy val realPlayers = wire[RealPlayerApi] + lazy val gameApiV2 = wire[GameApiV2] lazy val userGameApi = wire[UserGameApi] diff --git a/modules/api/src/main/GameApiV2.scala b/modules/api/src/main/GameApiV2.scala index b26bc7a80b..06ea23c965 100644 --- a/modules/api/src/main/GameApiV2.scala +++ b/modules/api/src/main/GameApiV2.scala @@ -27,7 +27,8 @@ final class GameApiV2( playerRepo: lila.tournament.PlayerRepo, swissApi: lila.swiss.SwissApi, analysisRepo: lila.analyse.AnalysisRepo, - getLightUser: LightUser.Getter + getLightUser: LightUser.Getter, + realPlayerApi: RealPlayerApi )(implicit ec: scala.concurrent.ExecutionContext, system: akka.actor.ActorSystem @@ -47,13 +48,21 @@ final class GameApiV2( game.pgnImport ifTrue config.imported match { case Some(imported) => fuccess(imported.pgn) case None => - enrich(config.flags)(game) flatMap { - case (game, initialFen, analysis) => - config.format match { - case Format.JSON => toJson(game, initialFen, analysis, config.flags) dmap Json.stringify - case Format.PGN => pgnDump.toPgnString(game, initialFen, analysis, config.flags) - } - } + for { + realPlayers <- config.playerFile.??(realPlayerApi.apply) + (game, initialFen, analysis) <- enrich(config.flags)(game) + export <- config.format match { + case Format.JSON => toJson(game, initialFen, analysis, config.flags) dmap Json.stringify + case Format.PGN => + pgnDump( + game, + initialFen, + analysis, + config.flags, + realPlayers = realPlayers + ) dmap pgnDump.toPgnString + } + } yield export } } @@ -240,7 +249,8 @@ final class GameApiV2( lightUsers <- gameLightUsers(g) pgn <- withFlags.pgnInJson ?? pgnDump - .toPgnString(g, initialFen, analysisOption, withFlags) + .apply(g, initialFen, analysisOption, withFlags) + .dmap(pgnDump.toPgnString) .dmap(some) } yield Json .obj( @@ -307,7 +317,8 @@ object GameApiV2 { format: Format, imported: Boolean, flags: WithFlags, - noDelay: Boolean + noDelay: Boolean, + playerFile: Option[String] ) extends Config case class ByUserConfig( diff --git a/modules/api/src/main/PgnDump.scala b/modules/api/src/main/PgnDump.scala index e4441f2d4c..6dd0491281 100644 --- a/modules/api/src/main/PgnDump.scala +++ b/modules/api/src/main/PgnDump.scala @@ -22,7 +22,8 @@ final class PgnDump( initialFen: Option[FEN], analysis: Option[Analysis], flags: WithFlags, - teams: Option[GameTeams] = None + teams: Option[GameTeams] = None, + realPlayers: Option[RealPlayers] = None ): Fu[Pgn] = dumper(game, initialFen, flags, teams) flatMap { pgn => if (flags.tags) (game.simulId ?? simulApi.idToName) map { simulName => @@ -36,6 +37,8 @@ final class PgnDump( val evaled = analysis.ifTrue(flags.evals).fold(pgn)(addEvals(pgn, _)) if (flags.literate) annotator(evaled, analysis, game.opening, game.winnerColor, game.status) else evaled + } map { pgn => + realPlayers.fold(pgn)(_.update(game, pgn)) } private def addEvals(p: Pgn, analysis: Analysis): Pgn = @@ -60,19 +63,12 @@ final class PgnDump( def formatter(flags: WithFlags) = (game: Game, initialFen: Option[FEN], analysis: Option[Analysis], teams: Option[GameTeams]) => - toPgnString(game, initialFen, analysis, flags, teams) + apply(game, initialFen, analysis, flags, teams) dmap toPgnString - def toPgnString( - game: Game, - initialFen: Option[FEN], - analysis: Option[Analysis], - flags: WithFlags, - teams: Option[GameTeams] = None - ) = - apply(game, initialFen, analysis, flags, teams) dmap { pgn => - // merge analysis & eval comments - // 1. e4 { [%eval 0.17] } { [%clk 0:00:30] } - // 1. e4 { [%eval 0.17] [%clk 0:00:30] } - s"$pgn\n\n\n".replaceIf("] } { [", "] [") - } + def toPgnString(pgn: Pgn) = { + // merge analysis & eval comments + // 1. e4 { [%eval 0.17] } { [%clk 0:00:30] } + // 1. e4 { [%eval 0.17] [%clk 0:00:30] } + s"$pgn\n\n\n".replaceIf("] } { [", "] [") + } } diff --git a/modules/api/src/main/RealPlayer.scala b/modules/api/src/main/RealPlayer.scala new file mode 100644 index 0000000000..7d5a7b2b69 --- /dev/null +++ b/modules/api/src/main/RealPlayer.scala @@ -0,0 +1,66 @@ +package lila.api + +import chess.format.pgn.{ Pgn, Tag, Tags } +import play.api.libs.ws.WSClient +import scala.concurrent.duration._ + +import lila.user.User + +final class RealPlayerApi( + cacheApi: lila.memo.CacheApi, + ws: WSClient +)(implicit ec: scala.concurrent.ExecutionContext) { + + def apply(url: String): Fu[Option[RealPlayers]] = cache get url + + private val cache = cacheApi[String, Option[RealPlayers]](4, "api.realPlayer") { + _.expireAfterAccess(10 seconds) + .buildAsyncFuture { url => + ws.url(url) + .withRequestTimeout(3.seconds) + .get() + .map { res => + val valid = + res.status == 200 && + res.headers + .get("Content-Type") + .exists(_.exists(_ startsWith "text/plain")) + valid ?? { + res.body.linesIterator + .take(9999) + .toList + .flatMap { line => + line.split(';').map(_.trim) match { + case Array(id, name, rating) => + Some(id -> RealPlayer(name.some.filter(_.nonEmpty), rating.toIntOption)) + case Array(id, name) => Some(id -> RealPlayer(name.some.filter(_.nonEmpty), none)) + case _ => none + } + } + .toMap + .some + .map(RealPlayers) + } + } + } + } +} + +case class RealPlayers(players: Map[User.ID, RealPlayer]) { + + def update(game: lila.game.Game, pgn: Pgn) = + pgn.copy( + tags = pgn.tags ++ Tags { + game.players.flatMap { player => + player.userId.flatMap(players.get) ?? { rp => + List( + rp.name.map { name => Tag(player.color.fold(Tag.White, Tag.Black), name) }, + rp.rating.map { rating => Tag(player.color.fold(Tag.WhiteElo, Tag.BlackElo), rating.toString) } + ).flatten + } + } + } + ) +} + +case class RealPlayer(name: Option[String], rating: Option[Int])