Merge branch 'master' into ScalaEvaluator

* master:
  improve game widgets and sides, and TV history
  fix pt translation
  fix hook config color
  break lobby API BC for the lulz
  translate Q&A title
  show chess960 position number - closes #214
  catch pov priority sort errors
  disallow rated white seeks for some variants
  protect round xhr and websocket against theft
  protect round sockets - WIP

Conflicts:
	modules/chess
This commit is contained in:
Thibault Duplessis 2015-01-23 01:37:10 +01:00
commit 6072b18c49
29 changed files with 131 additions and 70 deletions

View file

@ -41,6 +41,17 @@ private[controllers] trait LilaController
protected def Socket[A: FrameFormatter](f: Context => Fu[(Iteratee[A, _], Enumerator[A])]) =
WebSocket.tryAccept[A] { req => reqToCtx(req) flatMap f map scala.util.Right.apply }
protected def SocketEither[A: FrameFormatter](f: Context => Fu[Either[Result, (Iteratee[A, _], Enumerator[A])]]) =
WebSocket.tryAccept[A] { req => reqToCtx(req) flatMap f }
protected def SocketOption[A: FrameFormatter](f: Context => Fu[Option[(Iteratee[A, _], Enumerator[A])]]) =
WebSocket.tryAccept[A] { req =>
reqToCtx(req) flatMap f map {
case None => Left(NotFound(Json.obj("error" -> "socket resource not found")))
case Some(pair) => Right(pair)
}
}
protected def Open(f: Context => Fu[Result]): Action[AnyContent] =
Open(BodyParsers.parse.anyContent)(f)

View file

@ -48,9 +48,9 @@ object Lobby extends LilaController {
)
}
def socket(apiVersion: Int) = Socket[JsValue] { implicit ctx =>
def socket(apiVersion: Int) = SocketOption[JsValue] { implicit ctx =>
get("sri") ?? { uid =>
Env.lobby.socketHandler(uid = uid, user = ctx.me)
Env.lobby.socketHandler(uid = uid, user = ctx.me) map some
}
}

View file

@ -34,9 +34,9 @@ object Main extends LilaController {
}
}
def websocket = Socket { implicit ctx =>
def websocket = SocketOption { implicit ctx =>
get("sri") ?? { uid =>
Env.site.socketHandler(uid, ctx.userId, get("flag"))
Env.site.socketHandler(uid, ctx.userId, get("flag")) map some
}
}

View file

@ -17,8 +17,10 @@ object Monitor extends LilaController {
Ok(views.html.monitor.monitor())
}
def websocket = Socket[JsValue] { implicit ctx =>
get("sri") ?? env.socketHandler.apply
def websocket = SocketOption[JsValue] { implicit ctx =>
get("sri") ?? { sri =>
env.socketHandler(sri) map some
}
}
def status = Action.async { implicit req =>

View file

@ -22,7 +22,7 @@ object Round extends LilaController with TheftPrevention {
private def bookmarkApi = Env.bookmark.api
private def analyser = Env.analyse.analyser
def websocketWatcher(gameId: String, color: String) = Socket[JsValue] { implicit ctx =>
def websocketWatcher(gameId: String, color: String) = SocketOption[JsValue] { implicit ctx =>
(get("sri") |@| getInt("version")).tupled ?? {
case (uid, version) => env.socketHandler.watcher(
gameId = gameId,
@ -35,13 +35,21 @@ object Round extends LilaController with TheftPrevention {
}
}
def websocketPlayer(fullId: String, apiVersion: Int) = Socket[JsValue] { implicit ctx =>
private lazy val theftResponse = Unauthorized(Json.obj(
"error" -> "This game requires authentication"
)) as JSON
def websocketPlayer(fullId: String, apiVersion: Int) = SocketEither[JsValue] { implicit ctx =>
GameRepo pov fullId flatMap {
_ ?? { pov =>
(get("sri") |@| getInt("version")).tupled ?? {
case (uid, version) => env.socketHandler.player(pov, version, uid, ~get("ran"), ctx.me, ctx.ip)
case Some(pov) =>
if (isTheft(pov)) fuccess(Left(theftResponse))
else (get("sri") |@| getInt("version")).tupled match {
case Some((uid, version)) => env.socketHandler.player(
pov, version, uid, ~get("ran"), ctx.me, ctx.ip
) map Right.apply
case None => fuccess(Left(NotFound))
}
}
case None => fuccess(Left(NotFound))
}
}
@ -65,7 +73,8 @@ object Round extends LilaController with TheftPrevention {
)
},
api = apiVersion => {
if (pov.game.playableByAi) env.roundMap ! Tell(pov.game.id, AiPlay)
if (isTheft(pov)) theftResponse
else if (pov.game.playableByAi) env.roundMap ! Tell(pov.game.id, AiPlay)
Env.api.roundApi.player(pov, apiVersion, Nil) map { Ok(_) }
}
)
@ -163,9 +172,11 @@ object Round extends LilaController with TheftPrevention {
}
private def side(pov: Pov, isPlayer: Boolean)(implicit ctx: Context) =
pov.game.tournamentId ?? TournamentRepo.byId map { tour =>
Ok(html.game.side(pov, tour, withTourStanding = isPlayer))
}
(pov.game.tournamentId ?? TournamentRepo.byId) zip
GameRepo.initialFen(pov.game) map {
case (tour, initialFen) =>
Ok(html.game.side(pov, initialFen, tour, withTourStanding = isPlayer))
}
def continue(id: String, mode: String) = Open { implicit ctx =>
OptionResult(GameRepo game id) { game =>

View file

@ -6,8 +6,8 @@ import play.api.mvc._
import lila.api.Context
import lila.app._
import lila.game.{ Pov, GameRepo }
import lila.common.HTTPRequest
import lila.game.{ Pov, GameRepo }
import lila.tournament.{ System, TournamentRepo, Created, Started, Finished, Tournament => Tourney }
import lila.user.UserRepo
import views._
@ -119,10 +119,10 @@ object Tournament extends LilaController {
}
}
def websocket(id: String, apiVersion: Int) = Socket[JsValue] { implicit ctx =>
~(getInt("version") |@| get("sri") apply {
def websocket(id: String, apiVersion: Int) = SocketOption[JsValue] { implicit ctx =>
(getInt("version") |@| get("sri")).tupled ?? {
case (version, uid) => env.socketHandler.join(id, version, uid, ctx.me)
})
}
}
private def chatOf(tour: lila.tournament.Tournament)(implicit ctx: Context) =

View file

@ -45,7 +45,7 @@ i18n: @round.jsI18n()
@analyse.layout(
title = title,
side = views.html.game.side(pov, tour, withTourStanding = false, userTv = userTv).some,
side = views.html.game.side(pov, (data\"game"\"initialFen").asOpt[String], tour, withTourStanding = false, userTv = userTv).some,
chat = base.chatDom(trans.spectatorRoom.str(), ctx.isAuth).some,
underchat = underchat.some,
moreCss = moreCss,

View file

@ -0,0 +1,8 @@
@(game: Game)(implicit ctx: Context)
@game.perfType match {
case _ if game.fromPosition => {*}
case _ if game.hasAi => {:}
case _ if game.imported => {/}
case Some(p) => {@p.iconChar}
case _ => {8}
}

View file

@ -1,20 +1,17 @@
@(pov: Pov, tour: Option[lila.tournament.Tournament], withTourStanding: Boolean, userTv: Option[User] = None)(implicit ctx: Context)
@(pov: Pov, initialFen: Option[String], tour: Option[lila.tournament.Tournament], withTourStanding: Boolean, userTv: Option[User] = None)(implicit ctx: Context)
@import pov._
@import lila.tournament.arena
<div class="side">
<div class="side_box padded">
<div class="game_infos" data-icon="@game.perfType match {
case _ if game.fromPosition => {*}
case _ if game.hasAi => {:}
case _ if game.imported => {/}
case Some(p) => {@p.iconChar}
case _ => {8}
}">
<div class="game_infos" data-icon="@gameIcon(game)">
<div class="header">
<span class="setup">
@bookmark.toggle(game)
@if(game.imported) {
<a class="hint--top" href="@routes.Importer.importGame" data-hint="@trans.importGame()">IMPORT</a>
} else {
@game.clock.map(_.show).getOrElse {
@game.daysPerTurn.map { days =>
<span data-hint="@trans.correspondence()" class="hint--top">@{(days == 1).fold(trans.oneDay(), trans.nbDays(days))}</span>
@ -29,6 +26,7 @@
<span class="hint--top" data-hint="@pt.title">@pt.name.toUpperCase</span>
}
} • @game.rated.fold(trans.rated.str(), trans.casual.str()).toUpperCase
}
</span>
@game.pgnImport.flatMap(_.date).getOrElse(
game.isBeingPlayed.fold(trans.playingRightNow(), momentFormat(game.createdAt))
@ -67,6 +65,13 @@
<span class="player is color-icon white">@game.checkCount.black</span>
</div>
}
@if(game.variant.chess960) {
@initialFen.map { fen =>
@chess.variant.Chess960.positionNumber(fen).map { number =>
Chess960 start position: <strong>@number</strong>
}
}
}
</div>
@userTv.map { u =>

View file

@ -27,16 +27,16 @@
@defining(user flatMap g.player) { fromPlayer =>
@defining(fromPlayer | g.firstPlayer ) { firstPlayer =>
@gameFen(g, firstPlayer.color, ownerLink, withTitle = false)
<div class="infos" data-icon="@g.perfType match {
case _ if g.fromPosition => {*}
case _ if g.hasAi => {:}
case _ if g.imported => {/}
case Some(p) => {@p.iconChar}
case _ => {8}
}">
<div class="infos" data-icon="@gameIcon(g)">
@bookmark.toggle(g)
<div class="header">
<strong>
@if(g.imported) {
<span>IMPORT</span>
@g.pgnImport.flatMap(_.user).map { user =>
@trans.by(userIdLink(user.some, None, false))
}
} else {
@g.clock.map(_.show).getOrElse {
@g.daysPerTurn.map { days =>
<span data-hint="@trans.correspondence()" class="hint--top">@if(days == 1) {@trans.oneDay()} else {@trans.nbDays(days)}</span>
@ -46,6 +46,7 @@
}
• @g.perfType.map(_.name).getOrElse {@chess.variant.FromPosition.name}
• @g.rated.fold(trans.rated(), trans.casual())
}
</strong>
@g.pgnImport.flatMap(_.date).getOrElse(momentFormat(g.createdAt))
</div>

View file

@ -10,7 +10,7 @@ moreJs = jsTag("vendor/jquery.infinitescroll.min.js"),
side = side.some) {
<div class="content_box_top">
<h1 data-icon="&" class="is4 lichess_title"> Questions &amp; Answers</h1>
<h1 data-icon="&" class="is4 text lichess_title">@trans.questionsAndAnswers()</h1>
</div>
<div class="content_box_inter meta">
<div class="big_search">

View file

@ -19,7 +19,7 @@ i18n: @jsI18n()
@round.layout(
title = title,
side = views.html.game.side(pov, tour, withTourStanding = true),
side = views.html.game.side(pov, (data\"game"\"initialFen").asOpt[String], tour, withTourStanding = true),
chat = pov.game.hasChat.option(base.chatDom(trans.chatRoom.str(), ctx.isAuth)),
underchat = views.html.game.watchers().some,
moreJs = moreJs,

View file

@ -17,7 +17,7 @@ i18n: @jsI18n()
@round.layout(
title = title,
side = views.html.game.side(pov, tour, withTourStanding = false, userTv = userTv),
side = views.html.game.side(pov, (data\"game"\"initialFen").asOpt[String], tour, withTourStanding = false, userTv = userTv),
chat = base.chatDom(trans.spectatorRoom.str()).some,
underchat = views.html.game.watchers().some,
moreJs = moreJs,

View file

@ -1,6 +1,7 @@
@(form: Form[_], typ: String, title: Html, route: Call, fields: Html, error: Option[Html] = None)(implicit ctx: Context)
<div class="lichess_overboard game_config game_config_@typ"
data-white-variants="@lila.game.Game.variantsWhereWhiteIsBetter.map(_.id).mkString(",")"
@if(ctx.isAnon){data-anon="1"}>
<a href="@routes.Lobby.home" class="close icon" title="@trans.cancel()" data-icon="L"></a>
<h2>@title</h2>

View file

@ -22,7 +22,7 @@
<tbody>
@games.map { g =>
<tr>
<td><a class="view" href="@routes.Round.watcher(g.id, g.firstPlayer.color.name)" data-icon="v"></a></td>
<td><a class="icon" href="@routes.Round.watcher(g.id, g.firstPlayer.color.name)" data-icon="@game.gameIcon(g)"></a></td>
<td>
@playerLink(g.firstPlayer, withOnline = false, withDiff = true)<br />
@playerLink(g.secondPlayer, withOnline = false, withDiff = true)

View file

@ -403,7 +403,7 @@ whenTimeRemainingLessThanThirtySeconds=Quando o tempo restante for menor do que
difficultyEasy=Fácil
difficultyNormal=Normal
difficultyHard=Difícil
xLeftANoteOnY=% deixou uma nota para %s
xLeftANoteOnY=%s deixou uma nota para %s
xCompetesInY=%s compete em %s
xAskedY=%s perguntou %s
xAnsweredY=%s respondeu %s

View file

@ -54,7 +54,7 @@ final class LobbyApi(
"id" -> pov.opponent.userId,
"username" -> lila.game.Namer.playerString(pov.opponent, withRating = false)(lightUser),
"rating" -> pov.opponent.rating,
"aiLevel" -> pov.opponent.aiLevel).noNull,
"ai" -> pov.opponent.aiLevel).noNull,
"isMyTurn" -> pov.isMyTurn,
"secondsLeft" -> pov.remainingSeconds)
}

@ -1 +1 @@
Subproject commit 116abbb200fd913934c76574930cff967d00e991
Subproject commit cd73bd5c14ba3fd6ba84384a64789196ca6da5cc

View file

@ -422,8 +422,14 @@ object Game {
chess.variant.Standard,
chess.variant.Chess960,
chess.variant.KingOfTheHill)
val unanalysableVariants: Set[Variant] = Variant.all.toSet -- analysableVariants
val variantsWhereWhiteIsBetter: Set[Variant] = Set(
chess.variant.ThreeCheck,
chess.variant.Atomic,
chess.variant.Antichess)
val gameIdSize = 8
val playerIdSize = 4
val fullIdSize = 12

View file

@ -121,8 +121,16 @@ object GameRepo {
}
def nowPlaying(user: User): Fu[List[Pov]] =
$find(Query nowPlaying user.id) map {
_ flatMap { Pov(_, user) } sortWith Pov.priority
$find(Query nowPlaying user.id) map { games =>
val povs = games flatMap { Pov(_, user) }
try {
povs sortWith Pov.priority
}
catch {
case e: IllegalArgumentException =>
logerr(s"GameRepo.nowPlaying(${user.id}) ${povs.size} ${e.getMessage}")
povs
}
}
// gets most urgent game to play

View file

@ -91,9 +91,9 @@ private[round] final class SocketHandler(
uid: String,
user: Option[User],
ip: String,
userTv: Option[String]): Fu[JsSocketHandler] =
userTv: Option[String]): Fu[Option[JsSocketHandler]] =
GameRepo.pov(gameId, colorName) flatMap {
_ ?? { join(_, none, version, uid, "", user, ip, userTv = userTv) }
_ ?? { join(_, none, version, uid, "", user, ip, userTv = userTv) map some }
}
def player(

View file

@ -17,6 +17,11 @@ case class HookConfig(
color: Color,
ratingRange: RatingRange) extends HumanConfig {
def fixColor = copy(
color = if (mode == Mode.Rated &&
lila.game.Game.variantsWhereWhiteIsBetter(variant) &&
color == Color.White) Color.Random else color)
// allowAnons -> membersOnly
def >> = (variant.id, timeMode.id, time, increment, days, mode.id.some, !allowAnon, ratingRange.toString.some, color.name).some

View file

@ -50,11 +50,12 @@ private[setup] final class Processor(
}
def hook(
config: HookConfig,
configBase: HookConfig,
uid: String,
sid: Option[String],
blocking: Set[String])(implicit ctx: UserContext): Fu[String] =
saveConfig(_ withHook config) >> {
blocking: Set[String])(implicit ctx: UserContext): Fu[String] = {
val config = configBase.fixColor
saveConfig(_ withHook config) >> {
config.hook(uid, ctx.me, sid, blocking) match {
case Left(hook) => fuccess {
lobby ! AddHook(hook)
@ -67,6 +68,7 @@ private[setup] final class Processor(
case _ => fufail("Can't create seek")
}
}
}
private def saveConfig(map: UserConfig => UserConfig)(implicit ctx: UserContext): Funit =
ctx.me.fold(AnonConfigRepo.update(ctx.req) _)(user => UserConfigRepo.update(user) _)(map)

View file

@ -41,17 +41,8 @@ object Handler {
).map(_ => socket ! Quit(uid))
}
(socket ? join map connecter map {
socket ? join map connecter map {
case (controller, enum) => iteratee(controller) -> enum
}) recover {
case t: Exception => errorHandler(t.getMessage)
}
}
def errorHandler(err: String): JsSocketHandler =
Iteratee.skipToEof[JsValue] ->
Enumerator[JsValue](Json.obj(
"error" -> "Socket handler error: %s".format(err)
)).andThen(Enumerator.eof)
}

View file

@ -12,7 +12,4 @@ trait WithSocket {
type JsEnumerator = Enumerator[JsValue]
type JsIteratee = Iteratee[JsValue, _]
type JsSocketHandler = (JsIteratee, JsEnumerator)
implicit val LilaJsSocketHandlerZero: Zero[JsSocketHandler] =
Zero.instance(Handler errorHandler "default error handler used")
}

View file

@ -26,7 +26,7 @@ private[tournament] final class SocketHandler(
tourId: String,
version: Int,
uid: String,
user: Option[User]): Fu[JsSocketHandler] =
user: Option[User]): Fu[Option[JsSocketHandler]] =
TournamentRepo.exists(tourId) flatMap {
_ ?? {
for {
@ -36,7 +36,7 @@ private[tournament] final class SocketHandler(
case Connected(enum, member) =>
controller(socket, tourId, uid, member) -> enum
}
} yield handler
} yield handler.some
}
}

View file

@ -1614,12 +1614,18 @@ lichess.storage = {
var $daysInput = $form.find('.days_choice input');
var isHook = $form.hasClass('game_config_hook');
var $ratings = $form.find('.ratings > div');
var whiteVariants = $form.data('white-variants').split(',');
var toggleButtons = function() {
var timeMode = $timeModeSelect.val();
var rated = $rated.prop('checked');
var timeOk = timeMode != '1' || $timeInput.val() > 0 || $incrementInput.val() > 0;
var ratedOk = !isHook || !rated || timeMode != '0';
$form.find('.color_submits button').toggle(timeOk && ratedOk);
if (timeOk && ratedOk) {
$form.find('.color_submits button').show();
$form.find('.color_submits button.white').toggle(
!rated || whiteVariants.indexOf($variantSelect.val()) === -1);
} else
$form.find('.color_submits button').hide();
};
var showRating = function() {
var timeMode = $timeModeSelect.val();
@ -1783,6 +1789,7 @@ lichess.storage = {
$modeChoicesWrap.toggle(!fen);
if (fen) $casual.click();
showRating();
toggleButtons();
}).trigger('change');
$form.find('div.level').each(function() {

View file

@ -17,12 +17,18 @@
font-weight: bold;
color: #a0a0a0;
}
#tv_history a.view {
padding-left: 5px;
#tv_history a.icon {
font-size: 26px;
padding-left: 10px;
text-decoration: none;
opacity: 0.7;
transition: 0.3s;
}
#tv_history tr:hover a.icon {
opacity: 1;
}
#tv_history td {
padding: 6px 5px 6px 5px;
padding: 4px 0px;
border-top: 1px solid #eaeaea;
}
#tv_history tr:first-child td {

View file

@ -35,7 +35,7 @@ module.exports = function(ctrl) {
},
boardContent),
m('span.meta', [
pov.opponent.aiLevel ? ctrl.trans('aiNameLevelAiLevel', 'Stockfish', pov.opponent.aiLevel) : pov.opponent.username,
pov.opponent.ai ? ctrl.trans('aiNameLevelAiLevel', 'Stockfish', pov.opponent.aiLevel) : pov.opponent.username,
m('span.indicator',
pov.isMyTurn ? (pov.secondsLeft ? timer(pov) : ctrl.trans('yourTurn')) : m.trust('&nbsp;'))
])