Merge branch 'master' into analyse

* master:
  don't show cheater trophies
  better exports caching
  remove export logs
  better png export logging
  Revert "Bam! And the footer is fixed."
  pt "Português" translation #10165. Author: Arnica. Changed "spectatators" to a way that fulfill both Brazilian Portuguese and Portuguese.
  better external process logging
  fix round JS
  upgrade evaluator submodule
  include engine evaluator as a submodule
  Move Print button into FEN & PGN Menu
  log exports
  mt "Malti" translation #10164. Author: kittenthomas.
  fix auto report duplication
  stop auto block reports
  use board image creation in the og headers - through CDN
  expose PDF service
  print game to PDF, stream it as chunked HTTP and cache it in CDN

Conflicts:
	app/views/analyse/replay.scala.html
This commit is contained in:
Thibault Duplessis 2014-11-02 15:34:49 +01:00
commit ba5bf79dc2
30 changed files with 209 additions and 131 deletions

9
.gitmodules vendored
View file

@ -10,3 +10,12 @@
[submodule "public/staunton"]
path = public/staunton
url = https://github.com/clarkerubber/Staunton-Pieces
[submodule "submodules/pdfexporter"]
path = submodules/pdfexporter
url = https://github.com/clarkerubber/lichessPDFExporter
[submodule "submodules/boardcreator"]
path = submodules/boardcreator
url = https://github.com/clarkerubber/board-creator
[submodule "submodules/evaluator"]
path = submodules/evaluator
url = https://github.com/clarkerubber/engine-evaluator

View file

@ -31,7 +31,7 @@ private[app] final class Router(
case User(username) => sender ! R.User.show(username).url
case Player(fullId) => sender ! R.Round.player(fullId).url
case Watcher(gameId, color) => sender ! R.Round.watcher(gameId, color).url
case Pgn(gameId) => sender ! R.Analyse.pgn(gameId).url
case Pgn(gameId) => sender ! R.Export.pgn(gameId).url
case Tourney(tourId) => sender ! R.Tournament.show(tourId).url
case Puzzle(id) => sender ! R.Puzzle.show(id).url

View file

@ -72,22 +72,6 @@ object Analyse extends LilaController {
}
}
def pgn(id: String) = Open { implicit ctx =>
OptionFuResult(GameRepo game id) { game =>
(game.pgnImport.ifTrue(~get("as") == "imported") match {
case Some(i) => fuccess(i.pgn)
case None => for {
pgn Env.game.pgnDump(game)
analysis (~get("as") != "raw") ?? (env.analyser getDone game.id)
} yield Env.analyse.annotator(pgn, analysis, gameOpening(game), game.winnerColor, game.status, game.clock).toString
}) map { content =>
Ok(content).withHeaders(
CONTENT_TYPE -> ContentTypes.TEXT,
CONTENT_DISPOSITION -> ("attachment; filename=" + (Env.game.pgnDump filename game)))
}
}
}
private def gameOpening(game: GameModel) =
if (game.fromPosition || game.variant.exotic) none
else chess.OpeningExplorer openingOf game.pgnMoves

View file

@ -0,0 +1,50 @@
package controllers
import play.api.mvc.Action
import lila.app._
import lila.game.{ Game => GameModel, GameRepo }
import play.api.http.ContentTypes
import play.api.libs.iteratee.{ Iteratee, Enumerator }
import views._
object Export extends LilaController {
private def env = Env.game
def pgn(id: String) = Open { implicit ctx =>
OptionFuResult(GameRepo game id) { game =>
(game.pgnImport.ifTrue(~get("as") == "imported") match {
case Some(i) => fuccess(i.pgn)
case None => for {
pgn Env.game.pgnDump(game)
analysis (~get("as") != "raw") ?? (Env.analyse.analyser getDone game.id)
} yield Env.analyse.annotator(pgn, analysis, gameOpening(game), game.winnerColor, game.status, game.clock).toString
}) map { content =>
Ok(content).withHeaders(
CONTENT_TYPE -> ContentTypes.TEXT,
CONTENT_DISPOSITION -> ("attachment; filename=" + (Env.game.pgnDump filename game)))
}
}
}
def pdf(id: String) = Open { implicit ctx =>
OptionResult(GameRepo game id) { game =>
Ok.chunked(Enumerator.outputStream(env.pdfExport(game.id))).withHeaders(
CONTENT_TYPE -> "application/pdf",
CACHE_CONTROL -> "max-age=7200")
}
}
def png(id: String) = Open { implicit ctx =>
OptionResult(GameRepo game id) { game =>
Ok.chunked(Enumerator.outputStream(env.pngExport(game))).withHeaders(
CONTENT_TYPE -> "image/png",
CACHE_CONTROL -> "max-age=7200")
}
}
private def gameOpening(game: GameModel) =
if (game.fromPosition || game.variant.exotic) none
else chess.OpeningExplorer openingOf game.pgnMoves
}

View file

@ -14,6 +14,7 @@ trait AssetHelper {
val assetBaseUrl = s"http://$assetDomain"
def cdnUrl(path: String) = s"$assetBaseUrl$path"
def staticUrl(path: String) = s"$assetBaseUrl${routes.Assets.at(path)}"
def cssTag(name: String, staticDomain: Boolean = true) = cssAt("stylesheets/" + name, staticDomain)

View file

@ -14,6 +14,7 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
def netBaseUrl: String
def staticUrl(path: String): String
def cdnUrl(path: String): String
def mandatorySecondsToMove = lila.game.Env.current.MandatorySecondsToMove
@ -22,7 +23,7 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
val variant = pov.game.variant.exotic ?? s" ${pov.game.variant.name}"
Map(
'type -> "website",
'image -> staticUrl("images/large_tile.png"),
'image -> cdnUrl(routes.Export.png(pov.game.id).url),
'title -> s"$speed$variant Chess - ${playerText(pov.game.whitePlayer)} vs ${playerText(pov.game.blackPlayer)}",
'site_name -> "lichess.org",
'url -> s"$netBaseUrl${routes.Round.watcher(pov.game.id, pov.color.name).url}",

View file

@ -114,61 +114,28 @@ openGraph = povOpenGraph(pov)) {
}
</table>
}
</div>
<div class="underboard_content" style="display:none">
<div class="analysis_panels">
@if(game.analysable) {
<div class="panel computer_analysis">
@analysis.map { a =>
@advantageChart.map { chart =>
<div
id="adv_chart"
data-title="Advantage (up: white, down: black)"
data-max="@lila.analyse.AdvantageChart.max"
data-rows="@chart"></div>
}.getOrElse {
@analyse.computing()
<div class="panel fen_pgn">
<p><strong>FEN</strong><input type="input" readonly="true" spellcheck="false" class="copyable fen" /></p>
<p><strong>PGN</strong>
<a data-icon="x" href="@routes.Export.pgn(game.id)"> Download annotated</a>
@if(analysis.isDefined) {
/
<a data-icon="x" href="@routes.Export.pgn(game.id)?as=raw"> Download raw</a>
}
}.getOrElse {
@if(analysis.isEmpty) {
<form class="future_game_analysis@if(ctx.isAnon) { must_login }" action="@routes.Analyse.requestAnalysis(gameId)" method="post">
<button type="submit" class="button"><span class="is3" data-icon="A"> @trans.requestAComputerAnalysis()</span></button>
</form>
@if(game.isPgnImport) {
/
<a data-icon="x" href="@routes.Export.pgn(game.id)?as=imported"> Download imported</a>
}
}
<div class="view_game_analysis future_game_analysis" data-href="@routes.Round.watcher(pov.gameId, pov.color.name)">
<a class="button" href="@routes.Round.watcher(pov.gameId, pov.color.name)">
<span class="is3" data-icon="A"> @trans.viewTheComputerAnalysis()</span>
</a>
</div>
</div>
}
<div class="panel fen_pgn">
<p><strong>FEN</strong><input type="input" readonly="true" spellcheck="false" class="copyable fen" /></p>
<p><strong>PGN</strong>
<a data-icon="x" href="@routes.Analyse.pgn(game.id)"> Download annotated</a>
@if(analysis.isDefined) {
/
<a data-icon="x" href="@routes.Analyse.pgn(game.id)?as=raw"> Download raw</a>
}
@if(game.isPgnImport) {
/
<a data-icon="x" href="@routes.Analyse.pgn(game.id)?as=imported"> Download imported</a>
}
</p>
<div class="pgn">@Html(nl2br(escape(pgn)))</div>
</div>
<div class="panel move_times">
<div
id="movetimes_chart"
data-series="@timeChart.series"
data-max="@timeChart.maxTime"></div>
</div>
@cross.map { c =>
<div class="panel crosstable">
@views.html.game.crosstable(pov.player.userId.fold(c)(c.fromPov))
</div>
}
/
<a data-icon="x" target="_blank" href="@cdnUrl(routes.Export.pdf(game.id).url)"> Print-friendly PDF</a>
</p>
<div class="pgn">@Html(nl2br(escape(pgn)))</div>
</div>
<div class="panel move_times">
<div
id="movetimes_chart"
data-series="@timeChart.series"
data-max="@timeChart.maxTime"></div>
</div>
<div class="analysis_menu">
@if(game.analysable) {

View file

@ -63,6 +63,7 @@ openGraph = Map(
'description -> describeUser(u))) {
<div class="content_box no_padding user_show">
<div class="content_box_top">
@if(!u.engine) {
@info.ranks.map {
case (perf, rank) if rank == 1 => {
<span class="trophy hint--left" data-hint="@PerfType.name(perf) Champion!">
@ -86,6 +87,7 @@ openGraph = Map(
}
case _ => {}
}
}
<h1 class="lichess_title"><span@if(isOnline(u.id)) { class="connected" } data-icon="r"></span> @u.titleUsername</h1>
@if(u.disabled) {
<span class="staff">CLOSED</span>

View file

@ -74,7 +74,8 @@ fi
lilalog "Rsync scripts, binaries and assets"
stage="target/universal/stage"
rsync_command="rsync $RSYNC_OPTIONS bin $stage/bin $stage/lib public $REMOTE:$REMOTE_DIR"
include="bin $stage/bin $stage/lib public submodules"
rsync_command="rsync $RSYNC_OPTIONS $include $REMOTE:$REMOTE_DIR"
echo "$rsync_command"
$rsync_command
echo "rsync complete"

34
conf/messages.mt Normal file
View file

@ -0,0 +1,34 @@
playWithAFriend=Ilgħab ma' ħabib
playWithTheMachine=Ilgħab mal-kompjuter
toInviteSomeoneToPlayGiveThisUrl=Biex tistienden lil xi ħadd biex jilgħab, għatih/ha dan il-link
gameOver=Spiċċat il-logħba
waitingForOpponent=Stenna l-għadu
waiting=Stenna
yourTurn=Inti jmiss
aiNameLevelAiLevel=%s livell %s
level=Livell
toggleTheChat=Uża iċ-chat
toggleSound=Irranġa l-ħoss
chat=Chat
resign=Rrisenja
checkmate=Checkmate
stalemate=Stalemate
white=Abjad
black=Iswed
randomColor=Kulur li jkun
createAGame=Oħloq logħba
whiteIsVictorious=L-Abjad rebaħ il-logħba
blackIsVictorious=L-Iswed rebaħ il-logħba
playWithTheSameOpponentAgain=Ilgħab kontra l-istess għadu
newOpponent=Għadu ġdid
playWithAnotherOpponent=Ilgħab ma' għadu ieħor
yourOpponentWantsToPlayANewGameWithYou=L-għadu jixtieq jilgħab logħba oħra miegħek
joinTheGame=Ilqa' din l-istedina biex tilgħab din il-logħba
whitePlays=Abjad jmissu jilgħab
blackPlays=Iswed jmissu jilgħab
theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim=L-għadu seta' telaq il-logħba. Tista' tieħu r-rebħa, tgħid illi ġiet draw jew tistenna.
makeYourOpponentResign=Sforza ir-rebħa
forceResignation=Ħu r-rebħa
forceDraw=Għid illi ġiet draw
talkInChat=Tkellem b'chat
theFirstPersonToComeOnThisUrlWillPlayWithYou=L-ewwel persuna illi jiġi permezz tal-URL, ser jilgħab miegħek

View file

@ -128,7 +128,7 @@ recipient=Recipiente
send=Enviar
incrementInSeconds=Acréscimo em segundos
freeOnlineChess=Xadrez Online Gratuito
spectators=Espetadores:
spectators=Espectadores:
nbWins=%s vitórias
nbLosses=%s derrotas
nbDraws=%s empates

View file

@ -136,7 +136,10 @@ POST /team/:id/kick controllers.Team.kick(id: String)
POST /$gameId<\w{8}>/request-analysis controllers.Analyse.requestAnalysis(gameId: String)
POST /$gameId<\w{8}>/better-analysis/$color<white|black> controllers.Analyse.betterAnalysis(gameId: String, color: String)
POST /$gameId<\w{8}>/post-analysis controllers.Analyse.postAnalysis(gameId: String)
GET /$gameId<\w{8}>/pgn controllers.Analyse.pgn(gameId: String)
GET /game/pgn/$gameId<\w{8}>.pgn controllers.Export.pgn(gameId: String)
GET /game/export/pdf/$gameId<\w{8}>.pdf controllers.Export.pdf(gameId: String)
GET /game/export/png/$gameId<\w{8}>.png controllers.Export.png(gameId: String)
# Pref
POST /pref/:name controllers.Pref.set(name: String)

View file

@ -10,14 +10,14 @@ final class Env(
system: ActorSystem) {
private val CollectionEvaluation = config getString "collection.evaluation"
private val EvaluatorScriptPath = config getString "evaluator.script_path"
private val EvaluatorExecPath = config getString "evaluator.exec_path"
private val ActorName = config getString "actor.name"
private val ApiToken = config getString "api.token"
private val ApiUrl = config getString "api.url"
lazy val evaluator = new Evaluator(
coll = db(CollectionEvaluation),
script = EvaluatorScriptPath,
execPath = EvaluatorExecPath,
reporter = hub.actor.report,
analyser = hub.actor.analyser,
marker = hub.actor.mod,

View file

@ -19,7 +19,7 @@ import lila.user.{ User, UserRepo, Perfs }
final class Evaluator(
coll: Coll,
script: String,
execPath: String,
reporter: ActorSelection,
analyser: ActorSelection,
marker: ActorSelection,
@ -116,12 +116,13 @@ final class Evaluator(
}
private def run(userId: String, deep: Boolean): Try[String] = {
val command = s"""$script $userId ${deep.fold("true", "false")} $token $apiUrl/"""
import scala.sys.process._
import java.io.File
val exec = Process(Seq("php", "engine-evaluator.php", userId, deep.fold("true", "false"), token, s"$apiUrl/"), new File(execPath))
Try {
import scala.sys.process._
command!!
exec.!!
} match {
case Failure(e) => Failure(new Exception(s"$command $e"))
case Failure(e) => Failure(new Exception(s"$exec $e"))
case x => x
}
}

View file

@ -28,6 +28,8 @@ final class Env(
val ActorName = config getString "actor.name"
val UciMemoTtl = config duration "uci_memo.ttl"
val netBaseUrl = config getString "net.base_url"
val PdfExecPath = config getString "pdf.exec_path"
val PngExecPath = config getString "png.exec_path"
}
import settings._
@ -35,6 +37,10 @@ final class Env(
private[game] lazy val gameColl = db(CollectionGame)
lazy val pdfExport = PdfExport(PdfExecPath) _
lazy val pngExport = PngExport(PngExecPath) _
lazy val cached = new Cached(ttl = CachedNbTtl)
lazy val paginator = new PaginatorBuilder(

View file

@ -63,7 +63,7 @@ case class Game(
def opponent(c: Color): Player = player(!c)
private lazy val firstColor = (whitePlayer before blackPlayer).fold(White, Black)
lazy val firstColor = (whitePlayer before blackPlayer).fold(White, Black)
def firstPlayer = player(firstColor)
def secondPlayer = player(!firstColor)

View file

@ -0,0 +1,14 @@
package lila.game
import java.io.{ File, OutputStream }
import scala.sys.process._
object PdfExport {
private val logger = ProcessLogger(_ => (), _ => ())
def apply(execPath: String)(id: String)(out: OutputStream) {
val exec = Process(Seq("php", "main.php", id), new File(execPath))
exec #> out ! logger
}
}

View file

@ -0,0 +1,18 @@
package lila.game
import chess.format.Forsyth
import java.io.{ File, OutputStream }
import scala.sys.process._
object PngExport {
private val logger = ProcessLogger(_ => (), _ => ())
def apply(execPath: String)(game: Game)(out: OutputStream) {
val fen = (Forsyth >> game.toChess).split(' ').head
val color = game.firstColor.letter.toString
val lastMove = ~game.castleLastMoveTime.lastMoveString
val exec = Process(Seq("php", "board-creator.php", fen, color, lastMove), new File(execPath))
exec #> out ! logger
}
}

View file

@ -43,7 +43,6 @@ case object GetUids
package report {
case class Cheater(userId: String, text: String)
case class Check(userId: String)
case class Blocked(userId: String, blocked: Int, followed: Int)
}
package mod {

View file

@ -22,6 +22,7 @@ final class Env(
val CollectionTranslation = config getString "collection.translation"
val ContextGitUrl = config getString "context.git.url"
val ContextGitFile = config getString "context.git.file"
val CdnDomain = config getString "cdn_domain"
}
import settings._
@ -40,7 +41,10 @@ final class Env(
lazy val keys = new I18nKeys(translator)
lazy val requestHandler = new I18nRequestHandler(pool, RequestHandlerProtocol)
lazy val requestHandler = new I18nRequestHandler(
pool,
RequestHandlerProtocol,
CdnDomain)
lazy val jsDump = new JsDump(
path = appPath + "/" + WebPathRelative,

View file

@ -6,12 +6,18 @@ import play.api.mvc.{ Action, RequestHeader, Handler }
import lila.common.HTTPRequest
final class I18nRequestHandler(pool: I18nPool, protocol: String) {
final class I18nRequestHandler(
pool: I18nPool,
protocol: String,
cdnDomain: String) {
def apply(req: RequestHeader): Option[Handler] =
(HTTPRequest.isRedirectable(req) && !pool.domainLang(req).isDefined) option Action {
Redirect(redirectUrl(req))
}
(HTTPRequest.isRedirectable(req) &&
!pool.domainLang(req).isDefined &&
req.host != cdnDomain
) option Action {
Redirect(redirectUrl(req))
}
private def redirectUrl(req: RequestHeader) =
protocol +

View file

@ -7,7 +7,6 @@ import lila.db.api._
import lila.db.Implicits._
import lila.game.GameRepo
import lila.hub.actorApi.relation.ReloadOnlineFriends
import lila.hub.actorApi.report.Blocked
import lila.hub.actorApi.timeline.{ Propagate, Follow => FollowUser }
import lila.user.tube.userTube
import lila.user.{ User => UserModel, UserRepo }
@ -79,10 +78,7 @@ final class RelationApi(
case Some(Block) => funit
case _ => RelationRepo.block(u1, u2) >> limitBlock(u1) >> refresh(u1, u2) >>-
bus.publish(lila.hub.actorApi.relation.Block(u1, u2), 'relation) >>-
(nbBlockers(u2) zip nbFollowers(u2)).andThen {
case Success((blockers, followers)) if blockers >= 20 && blockers > followers =>
reporter ! Blocked(u2, blockers, followers)
}
(nbBlockers(u2) zip nbFollowers(u2))
}
def unfollow(u1: ID, u2: ID): Funit =

View file

@ -23,8 +23,6 @@ final class Env(
def receive = {
case lila.hub.actorApi.report.Cheater(userId, text) =>
api.autoCheatReport(userId, text)
case lila.hub.actorApi.report.Blocked(userId, blocked, followed) =>
api.autoBlockReport(userId, blocked, followed)
case lila.hub.actorApi.report.Check(userId) =>
api.autoProcess(userId)
}

View file

@ -12,7 +12,7 @@ import tube.reportTube
private[report] final class ReportApi(evaluator: ActorSelection) {
def create(setup: ReportSetup, by: User, update: Boolean = false): Funit =
def create(setup: ReportSetup, by: User): Funit =
Reason(setup.reason).fold[Funit](fufail("Invalid report reason " + setup.reason)) { reason =>
val user = setup.user
val report = Report.make(
@ -20,21 +20,17 @@ private[report] final class ReportApi(evaluator: ActorSelection) {
reason = reason,
text = setup.text,
createdBy = by)
!isAlreadySlayed(report, user) ?? {
findRecent(user, reason) flatMap {
case Some(existing) if update =>
$update($select(existing.id), $set("text" -> report.text))
case Some(_) =>
logger.info(s"skip existing report creation: $reason $user")
funit
case None => $insert(report) >>- {
if (report.isCheat && report.isManual) evaluator ! user
!isAlreadySlain(report, user) ?? {
reportTube.coll.update(
selectRecent(user, reason),
reportTube.toMongo(report).get,
upsert = true) map { res =>
if (report.isCheat && !res.updatedExisting) evaluator ! user
}
}
}
}
private def isAlreadySlayed(report: Report, user: User) =
private def isAlreadySlain(report: Report, user: User) =
(report.isCheat && user.engine) ||
(report.isAutomatic && report.isOther && user.troll) ||
(report.isTroll && user.troll)
@ -47,19 +43,6 @@ private[report] final class ReportApi(evaluator: ActorSelection) {
reason = "cheat",
text = text,
gameId = "",
move = ""), lichess, update = true)
case _ => funit
}
}
def autoBlockReport(userId: String, blocked: Int, followed: Int): Funit = {
logger.info(s"auto block report $userId: $blocked blockers & $followed followers")
UserRepo byId userId zip UserRepo.lichess flatMap {
case (Some(user), Some(lichess)) => create(ReportSetup(
user = user,
reason = "other",
text = s"[AUTOREPORT] Blocked $blocked times, followed by $followed players",
gameId = "",
move = ""), lichess)
case _ => funit
}
@ -84,11 +67,13 @@ private[report] final class ReportApi(evaluator: ActorSelection) {
def recentProcessed(nb: Int) = $find($query(processedSelect) sort $sort.createdDesc, nb)
private def selectRecent(user: User, reason: Reason) = Json.obj(
"createdAt" -> $gt($date(DateTime.now minusDays 3)),
"user" -> user.id,
"reason" -> reason.name)
private def findRecent(user: User, reason: Reason): Fu[Option[Report]] =
$find.one(Json.obj(
"createdAt" -> $gt($date(DateTime.now minusDays 3)),
"user" -> user.id,
"reason" -> reason.name))
$find.one(selectRecent(user, reason))
private val logger = play.api.Logger("report")
}

View file

@ -1969,6 +1969,7 @@ var storage = {
var $panels = $('div.analysis_panels > div');
$('div.analysis_menu').on('click', 'a', function() {
var panel = $(this).data('panel');
if (!panel) return;
$(this).siblings('.active').removeClass('active').end().addClass('active');
$panels.removeClass('active').filter('.' + panel).addClass('active');
if (panel == 'move_times') try {

View file

@ -493,7 +493,6 @@ div.content_box .loader:after {
display: none;
}
html {
position: relative;
min-height: 100%;
}
body {
@ -507,7 +506,6 @@ body {
background-image: linear-gradient(to bottom, #d7d7d7 0%, #eeeeee 116px);
background-repeat: no-repeat;
overflow-x: hidden;
margin: 0 0 150px;
}
a,
a:visited,
@ -605,14 +603,10 @@ body.tight #site_baseline {
display: none;
}
#footer_wrap {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 50px;
border-top: 1px solid #c9c9c9;
background: #e4e4e4;
padding: 30px 0;
margin-top: 60px;
}
#footer_wrap div.footer {
width: 1000px;

@ -0,0 +1 @@
Subproject commit a177efd0b4a023e09d8ce8f122c2b904ac312113

1
submodules/evaluator Submodule

@ -0,0 +1 @@
Subproject commit 4dffb1beb4a61d53fbffba3d32b4d288fd20ec97

@ -0,0 +1 @@
Subproject commit 8d6c9550432ce462e4e977a770751354da097cf4

View file

@ -8,6 +8,7 @@ module.exports = function(old, cfg) {
}
if (cfg.game.moves) data.game.moves = data.game.moves.split(' ');
else data.game.moves = [];
return data;
};