Merge branch 'master' into persistentChallenge

* master: (1107 commits)
  fix typo
  fix #1525
  implement AnalysisRepo.associateToGames
  upgrade scalachess
  variant doc style fixes
  Revert "disable tournament TV for now, it's not quite ready"
  upgrade scalachess
  display material score
  fix some variant doc style
  left menu dark theme
  icons on rating stats left menu
  hover effect on left side menus
  variants page
  variant doc style
  improve variant doc
  refactor variant documentation
  stockfish current commit
  unfuck Prismic.getBookmark
  fix tournament TV selector
  disable tournament TV for now, it's not quite ready
  ...
This commit is contained in:
Thibault Duplessis 2016-01-27 09:41:22 +07:00
commit 1b3e61b509
695 changed files with 15237 additions and 8101 deletions

1
.gitignore vendored
View file

@ -17,5 +17,6 @@ data/
dist/
node_modules/
local/
.vagrant
RUNNING_PID

6
.gitmodules vendored
View file

@ -25,3 +25,9 @@
[submodule "public/vendor/ChessPursuit"]
path = public/vendor/ChessPursuit
url = https://github.com/ornicar/ChessPursuit
[submodule "public/vendor/multiple-select"]
path = public/vendor/multiple-select
url = https://github.com/ornicar/multiple-select
[submodule "public/vendor/hopscotch"]
path = public/vendor/hopscotch
url = https://github.com/linkedin/hopscotch

34
.travis.yml Normal file
View file

@ -0,0 +1,34 @@
language: scala
# https://docs.travis-ci.com/user/notifications/#IRC-notification
notifications:
irc:
channels:
- "chat.freenode.net#lichess"
on_success: change
on_failure: always
use_notice: true
skip_join: true
slack: lichess:sVTqlE0OQNMPq1n6qRnVnfrz
on_success: change
on_failure: always
# https://docs.travis-ci.com/user/languages/java/#Testing-Against-Multiple-JDKs
jdk:
- oraclejdk8
env:
- TRAVIS_NODE_VERSION="4.0.0"
install:
# http://austinpray.com/ops/2015/09/20/change-travis-node-version.html
- rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install "$TRAVIS_NODE_VERSION"
- npm install -g gulp
- git submodule update --init --recursive
- ./ui/build
- ./bin/build-deps.sh
script:
- sbt compile
- sbt test

View file

@ -1 +1,22 @@
See https://github.com/ornicar/lila/wiki/Lichess-Development-Onboarding
#### I need help contributing code to Lichess.
__For setting up your development environment, [read this guide](https://github.com/ornicar/lila/wiki/Lichess-Development-Onboarding).__
If you experience any issues, __fix them yourself__ or __demonstrate your efforts and make it easy to help__. As stated in the read-me file, I do **not** offer support for your Lichess instance.
#### I want to report a bug or a problem about Lichess.
[__Make an issue ticket.__](https://github.com/ornicar/lila/issues/new?title=Submitting a forum thread with the word "thibault" in its title crashes my browser!) However, note that issues that provide little value compared to the required effort may be closed. Before creating an issue, make sure that:
1. You list the steps to reproduce the problem to show that other users may experience it as well, if the issue is not self-descriptive.
2. Search to make sure it isn't a duplicate [The advanced search syntax](https://help.github.com/articles/searching-issues/) may come in handy.
3. It is not a trivial problem or demand unrealistic dev time to fix - Pluralization bugs and the such fall under this category.
#### I want to suggest a feature for Lichess.
Issue tickets on features that lack potential or effectiveness are not useful and may be closed. Discussions regarding whether a proposed new feature would be useful can be done on [The Lichess Feedback Forum](http://lichess.org/forum/lichess-feedback) to gauge feedback. The developers may also discuss the idea there, and if it is exemplary, a corresponding issue ticket will be made. __When you're ready, [make an issue ticket](https://github.com/ornicar/lila/issues/new?title=Please implement this chess variant idea I came up with)__ and link relevant, constructive comments regarding it in your issue ticket (such as a detailed Reddit post; Linking to an empty forum thread with only your own commentary adds no value). Make sure that the feature you propose:
1. Is __effective in delivering a goal__. A feature that adds nothing new is purely fancy; Please develop a userscript or userstyle for your personal use instead.
2. Doesn't rely on mundane assumptions. Non-technical people have the tendency to measure how difficult / easy a feature is to implement based on their unreliable instincts, and such assumptions wastes everyone's time. __Point out what needs to happen__, not what you think will happen.
3. Is __unique, if you're aiming to solve a problem__. Features that can easily be replaced by easier ideas have little value and may not have to be brought up to begin with.
4. Is __clear and concise__. If ambiguities exist, define them or propose options.

View file

@ -1,4 +1,4 @@
[lichess.org](http://lichess.org)
[lichess.org](http://lichess.org) [![Build Status](https://travis-ci.org/ornicar/lila.svg?branch=master)](https://travis-ci.org/ornicar/lila)
---------------------------------
<img src="https://raw.githubusercontent.com/ornicar/lila/master/public/images/homepage_light.1200.png" alt="lichess.org" />
@ -6,7 +6,7 @@
It's a free online chess game focused on [realtime](http://lichess.org/games) and ease of use
It has a [search engine](http://lichess.org/games/search),
[computer analysis](http://lichess.org/analyse/ief49lif),
[computer analysis](http://lichess.org/ief49lif),
[tournaments](http://lichess.org/tournament),
[simuls](http://lichess.org/simul),
[forums](http://lichess.org/forum),
@ -165,10 +165,10 @@ $.ajax({
});
```
### `GET /api/game` fetch many games
### `GET /api/user/<username>/games` fetch user games
```
> curl http://en.lichess.org/api/game?username=thibault&rated=1&nb=10
> curl http://en.lichess.org/api/user/thibault/games?nb=50&page=2
```
Games are returned by descendant chronological order.
@ -176,18 +176,23 @@ All parameters are optional.
name | type | default | description
--- | --- | --- | ---
**username** | string | - | filter games by user
**rated** | 1 or 0 | - | filter rated or casual games
**analysed** | 1 or 0 | - | filter only analysed (or not analysed) games
**nb** | int | 10 | maximum number of games to return
**nb** | int | 100 | maximum number of games to return per page
**page** | int | 1 | for pagination
**with_analysis** | 1 or 0 | 0 | include deep analysis data in the result
**with_moves** | 1 or 0 | 0 | include a list of PGN moves
**with_opening** | 1 or 0 | 0 | include opening informations
**token** | string | - | security token (unlocks secret game data)
**with_movetimes** | 1 or 0 | 0 | include move time informations
**rated** | 1 or 0 | - | filter rated or casual games
```javascript
{
"list": [
"currentPage": 3,
"previousPage": 2,
"nextPage": 4,
"maxPerPage": 100,
"nbPages": 43,
"nbResults": 4348,
"currentPageResults": [
{
"id": "39b12Ikl",
"variant": "chess960", // standard/chess960/fromPosition/kingOfTheHill/threeCheck
@ -213,7 +218,9 @@ name | type | default | description
"blunder": 1,
"inaccuracy": 0,
"mistake": 2
}
},
// rounded move times in tenths of seconds
"moveTimes":[30,40,10,40,40,100,50,200,400,150,150,40,50,200,80]
},
"black": ... // other player
}
@ -261,8 +268,9 @@ name | type | default | description
--- | --- | --- | ---
**with_analysis** | 1 or 0 | 0 | include deep analysis data in the result
**with_moves** | 1 or 0 | 0 | include a list of PGN moves
**with_movetimes** | 1 or 0 | 0 | include move time informations
**with_opening** | 1 or 0 | 0 | include opening informations
**with_fens** | 1 or 0 | 0 | include a list of FEN states
**token** | string | - | security token (unlocks secret game data)
```javascript
{
@ -291,7 +299,9 @@ name | type | default | description
"blunder": 1,
"inaccuracy": 0,
"mistake": 2
}
},
// rounded move times in tenths of seconds
"moveTimes":[30,40,10,40,40,100,50,200,400,150,150,40,50,200,80]
},
"black": ... // other player
},

50
Vagrantfile vendored Normal file
View file

@ -0,0 +1,50 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure(2) do |config|
config.vm.box = "ubuntu/vivid64"
# Use this script to set up and compile the Lila installation. We set
# `privileged` to `false` because otherwise the provisioning script will run
# as root. This isn't a problem to install packages globally (`apt-get
# install`), but `sbt publish-local` will publish to `root`'s home directory!
# Then we would not be able to use those packages when logged in as
# `vagrant`.
config.vm.provision "shell", path: "bin/provision-vagrant.sh", privileged: false
# IP address to use to connect to the virtual machine. This should be an
# entry in your hosts file. We use a static IP so that the developer doesn't
# have to keep adding new entries to their hosts file.
config.vm.network "private_network", ip: "192.168.34.34"
# From https://stefanwrobel.com/how-to-make-vagrant-performance-not-suck. You
# may want to set `cpus` and `mem` yourself.
config.vm.provider "virtualbox" do |v|
host = RbConfig::CONFIG['host_os']
# Fraction of memory of host OS to allocate to VM. More is better!
memory_fraction = 0.5
# Give VM allocated system memory & access to all cpu cores on the host
if host =~ /darwin/
cpus = `sysctl -n hw.ncpu`.to_i
# sysctl returns Bytes and we need to convert to MB
mem = `sysctl -n hw.memsize`.to_i / 1024 / 1024
elsif host =~ /linux/
cpus = `nproc`.to_i
# meminfo shows KB and we need to convert to MB
mem = `grep 'MemTotal' /proc/meminfo | sed -e 's/MemTotal://' -e 's/ kB//'`.to_i / 1024
else # sorry Windows folks, I can't help you
cpus = 2
mem = 4096
end
mem *= memory_fraction
mem = mem.to_i
# Needed to use multiple CPUs.
v.customize ["modifyvm", :id, "--ioapic", "on"]
v.customize ["modifyvm", :id, "--cpus", cpus]
v.customize ["modifyvm", :id, "--memory", mem]
end
end

View file

@ -5,6 +5,7 @@ import com.typesafe.config.Config
final class Env(
config: Config,
val scheduler: lila.common.Scheduler,
system: ActorSystem,
appPath: String) {
@ -22,7 +23,7 @@ final class Env(
tourneyWinners = Env.tournament.winners.scheduled,
timelineEntries = Env.timeline.entryRepo.userEntries _,
dailyPuzzle = Env.puzzle.daily,
streamsOnAir = () => Env.tv.streamsOnAir,
streamsOnAir = () => Env.tv.streamsOnAir.all,
countRounds = Env.round.count,
lobbyApi = Env.api.lobbyApi,
getPlayban = Env.playban.api.currentBan _,
@ -40,7 +41,8 @@ final class Env(
getRanks = Env.user.cached.ranking.getAll,
isDonor = Env.donation.isDonor,
isHostingSimul = Env.simul.isHosting,
isStreamer = Env.tv.isStreamer.apply) _
isStreamer = Env.tv.isStreamer.apply,
insightShare = Env.insight.share) _
system.actorOf(Props(new actor.Renderer), name = RendererName)
@ -75,7 +77,12 @@ final class Env(
Env.tv,
Env.blog,
Env.video,
Env.shutup // required to load the actor
Env.shutup, // required to load the actor
Env.insight, // required to load the actor
Env.worldMap, // required to load the actor
Env.push, // required to load the actor
Env.perfStat, // required to load the actor
Env.slack // required to load the actor
)
play.api.Logger("boot").info("Preloading complete")
}
@ -87,6 +94,7 @@ object Env {
lazy val current = "app" boot new Env(
config = lila.common.PlayApp.loadConfig,
scheduler = lila.common.PlayApp.scheduler,
system = lila.common.PlayApp.system,
appPath = lila.common.PlayApp withApp (_.path.getCanonicalPath))
@ -136,5 +144,8 @@ object Env {
def video = lila.video.Env.current
def playban = lila.playban.Env.current
def shutup = lila.shutup.Env.current
def coach = lila.coach.Env.current
def insight = lila.insight.Env.current
def push = lila.push.Env.current
def perfStat = lila.perfStat.Env.current
def slack = lila.slack.Env.current
}

View file

@ -2,6 +2,7 @@ package controllers
import play.api.mvc._, Results._
import lila.api.Context
import lila.app._
import lila.common.LilaCookie
import lila.db.api.$find
@ -35,14 +36,19 @@ object Account extends LilaController {
me =>
negotiate(
html = notFound,
api = _ => lila.game.GameRepo urgentGames me map { povs =>
Env.current.bus.publish(lila.user.User.Active(me), 'userActive)
Ok {
import play.api.libs.json._
Env.user.jsonView(me, extended = true) ++ Json.obj(
"nowPlaying" -> JsArray(povs take 20 map Env.api.lobbyApi.nowPlaying))
api = _ =>
Env.pref.api getPref me flatMap { prefs =>
lila.game.GameRepo urgentGames me map { povs =>
Env.current.bus.publish(lila.user.User.Active(me), 'userActive)
Ok {
import play.api.libs.json._
import lila.pref.JsonView._
Env.user.jsonView(me, extended = true) ++ Json.obj(
"prefs" -> prefs,
"nowPlaying" -> JsArray(povs take 20 map Env.api.lobbyApi.nowPlaying))
}
}
}
}
)
}
@ -136,16 +142,23 @@ object Account extends LilaController {
(UserRepo toggleKid me) inject Redirect(routes.Account.kid)
}
private def currentSessionId(implicit ctx: Context) =
~Env.security.api.reqSessionId(ctx.req)
def security = Auth { implicit ctx =>
me =>
Env.security.api.dedup(me.id, ctx.req) >>
Env.security.api.locatedOpenSessions(me.id, 50) map { sessions =>
Ok(html.account.security(me, sessions, ~Env.security.api.reqSessionId(ctx.req)))
Ok(html.account.security(me, sessions, currentSessionId))
}
}
def signout(sessionId: String) = Auth { ctx =>
def signout(sessionId: String) = Auth { implicit ctx =>
me =>
lila.security.Store.closeUserAndSessionId(me.id, sessionId)
if (sessionId == "all")
lila.security.Store.closeUserExceptSessionId(me.id, currentSessionId) inject
Redirect(routes.Account.security)
else
lila.security.Store.closeUserAndSessionId(me.id, sessionId)
}
}

View file

@ -28,7 +28,7 @@ object Analyse extends LilaController {
def requestAnalysis(id: String) = Auth { implicit ctx =>
me =>
makeAnalysis(id, me) injectAnyway
Ok(html.analyse.computing())
Ok(html.analyse.computing(id))
}
private def makeAnalysis(id: String, me: lila.user.User)(implicit ctx: Context) =

View file

@ -28,9 +28,9 @@ object Api extends LilaController {
)) as JSON
}
def user(username: String) = ApiResult { implicit ctx =>
def user(name: String) = ApiResult { implicit ctx =>
userApi.one(
username = username,
username = name,
token = get("token"))
}
@ -43,17 +43,23 @@ object Api extends LilaController {
) map (_.some)
}
def games = ApiResult { implicit ctx =>
gameApi.list(
username = get("username"),
rated = getBoolOpt("rated"),
analysed = getBoolOpt("analysed"),
withAnalysis = getBool("with_analysis"),
withMoves = getBool("with_moves"),
withOpening = getBool("with_opening"),
token = get("token"),
nb = getInt("nb")
) map (_.some)
def userGames(name: String) = ApiResult { implicit ctx =>
lila.user.UserRepo named name flatMap {
_ ?? { user =>
gameApi.byUser(
user = user,
rated = getBoolOpt("rated"),
analysed = getBoolOpt("analysed"),
withAnalysis = getBool("with_analysis"),
withMoves = getBool("with_moves"),
withOpening = getBool("with_opening"),
withMoveTimes = getBool("with_movetimes"),
token = get("token"),
nb = getInt("nb"),
page = getInt("page")
) map (_.some)
}
}
}
def game(id: String) = ApiResult { implicit ctx =>
@ -63,6 +69,7 @@ object Api extends LilaController {
withMoves = getBool("with_moves"),
withOpening = getBool("with_opening"),
withFens = getBool("with_fens"),
withMoveTimes = getBool("with_movetimes"),
token = get("token"))
}

View file

@ -118,8 +118,10 @@ object Auth extends LilaController {
UserRepo.create(data.username, data.password, email.some, ctx.blindMode, none)
.flatten(s"No user could be created for ${data.username}")
.map(_ -> email).flatMap {
case (user, email) => env.emailConfirm.send(user, email) inject
Redirect(routes.Auth.checkYourEmail(user.username))
case (user, email) => env.emailConfirm.send(user, email) >> {
if (env.emailConfirm.effective) Redirect(routes.Auth.checkYourEmail(user.username)).fuccess
else saveAuthAndRedirect(user)
}
}
}),
api = apiVersion => forms.signup.mobile.bindFromRequest.fold(
@ -143,23 +145,37 @@ object Auth extends LilaController {
}
def signupConfirmEmail(token: String) = Open { implicit ctx =>
implicit val req = ctx.req
Env.security.emailConfirm.confirm(token) flatMap {
case Some(user) => api.saveAuthentication(user.id, ctx.mobileApiVersion) map { sessionId =>
Redirect(routes.User.show(user.username)) withCookies LilaCookie.session("sessionId", sessionId)
} recoverWith authRecovery
case _ => notFound
_.fold(notFound)(saveAuthAndRedirect)
}
}
private def saveAuthAndRedirect(user: UserModel)(implicit ctx: Context) = {
implicit val req = ctx.req
api.saveAuthentication(user.id, ctx.mobileApiVersion) map { sessionId =>
Redirect(routes.User.show(user.username)) withCookies LilaCookie.session("sessionId", sessionId)
} recoverWith authRecovery
}
private def noTorResponse(implicit ctx: Context) = negotiate(
html = Unauthorized(html.auth.tor()).fuccess,
api = _ => Unauthorized(Json.obj("error" -> "Can't login from TOR, sorry!")).fuccess)
def setFingerprint(hash: String, ms: Int) = Auth { ctx =>
def setFingerprint(fp: String, ms: Int) = Auth { ctx =>
me =>
// if (ms > 1000) logwarn(s"[Fingerprint] ${me.username} $ms ms / ${~HTTPRequest.userAgent(ctx.req)}")
api.setFingerprint(ctx.req, hash) inject Ok
api.setFingerprint(ctx.req, fp) flatMap {
_ ?? { hash =>
!me.lame ?? {
api.recentUserIdsByFingerprint(hash).map(_.filter(me.id!=)) flatMap {
case otherIds if otherIds.size >= 2 => UserRepo countEngines otherIds flatMap {
case nb if nb >= 2 && nb >= otherIds.size / 2 => Env.report.api.autoCheatPrintReport(me.id)
case _ => funit
}
case _ => funit
}
}
}
} inject Ok
}
def passwordReset = Open { implicit ctx =>
@ -175,9 +191,10 @@ object Auth extends LilaController {
BadRequest(html.auth.passwordReset(err, captcha, false.some))
},
data => {
UserRepo enabledByEmail data.email flatMap {
case Some(user) if env.emailAddress.isValid(data.email) =>
Env.security.passwordReset.send(user, data.email) inject Redirect(routes.Auth.passwordResetSent(data.email))
val email = env.emailAddress.validate(data.email) | data.email
UserRepo enabledByEmail email flatMap {
case Some(user) =>
Env.security.passwordReset.send(user, email) inject Redirect(routes.Auth.passwordResetSent(data.email))
case _ => forms.passwordResetWithCaptcha map {
case (form, captcha) => BadRequest(html.auth.passwordReset(form, captcha, false.some))
}

View file

@ -1,94 +0,0 @@
package controllers
import lila.api.Context
import lila.app._
import play.api.libs.json.Json
import play.api.mvc.Result
import views._
object Coach extends LilaController {
private def env = Env.coach
def opening(username: String, colorStr: String) = Open { implicit ctx =>
chess.Color(colorStr).fold(notFound) { color =>
Accessible(username) { user =>
env.statApi.count(user.id) map { nbPeriods =>
Ok(html.coach.opening(user, color, nbPeriods))
}
}
}
}
def openingJson(username: String, colorStr: String) = Open { implicit ctx =>
chess.Color(colorStr).fold(notFoundJson(s"No such color: $colorStr")) { color =>
AccessibleJson(username) { user =>
WithRange { range =>
env.statApi.fetchRangeForOpenings(user.id, range) flatMap {
_.fold(notFoundJson(s"Data not generated yet")) { period =>
env.jsonView.opening(period, color) map { data =>
Ok(data)
}
}
}
}
}
}
}
def move(username: String) = Open { implicit ctx =>
Accessible(username) { user =>
env.statApi.count(user.id) map { nbPeriods =>
Ok(html.coach.move(user, nbPeriods))
}
}
}
def moveJson(username: String) = Open { implicit ctx =>
AccessibleJson(username) { user =>
WithRange { range =>
env.statApi.fetchRangeForMoves(user.id, range) flatMap {
_.fold(notFoundJson(s"Data not generated yet")) { period =>
env.jsonView.move(period) map { data =>
Ok(data)
}
}
}
}
}
}
def refresh(username: String) = Open { implicit ctx =>
Accessible(username) { user =>
env.aggregator(user) inject Ok
}
}
private def WithRange(f: Range => Fu[Result])(implicit ctx: Context): Fu[Result] =
get("range").flatMap {
_.split('-') match {
case Array(a, b) => (parseIntOption(a) |@| parseIntOption(b))(Range.apply)
case _ => none
}
}.fold(notFoundJson("No range provided"))(f)
private def Accessible(username: String)(f: lila.user.User => Fu[Result])(implicit ctx: Context) =
lila.user.UserRepo named username flatMap {
case None => notFound
case Some(u) => env.share.grant(u, ctx.me) flatMap {
case true => f(u)
case false if isGranted(_.UserSpy) => f(u)
case false => fuccess(Forbidden(html.coach.forbidden(u)))
}
}
private def AccessibleJson(username: String)(f: lila.user.User => Fu[Result])(implicit ctx: Context) =
lila.user.UserRepo named username flatMap {
case None => notFoundJson(s"No such user: $username")
case Some(u) => env.share.grant(u, ctx.me) flatMap {
case true => f(u)
case false if isGranted(_.UserSpy) => f(u)
case false => fuccess(Forbidden(Json.obj("error" -> s"User $username data is protected")))
}
} map (_ as JSON)
}

View file

@ -8,9 +8,9 @@ import views._
object Donation extends LilaController {
def index = Open { implicit ctx =>
OptionFuOk(Prismic.oneShotBookmark("donate")) {
OptionFuOk(Prismic.getBookmark("donate")) {
case (doc, resolver) => Env.donation.api.list(100) zip
Env.donation.api.top(5) zip
Env.donation.api.top(10) zip
Env.donation.api.progress map {
case ((donations, top), progress) =>
views.html.donation.index(doc, resolver, donations, top, progress)
@ -19,7 +19,7 @@ object Donation extends LilaController {
}
def thanks = Open { implicit ctx =>
OptionOk(Prismic.oneShotBookmark("donate-thanks")) {
OptionOk(Prismic.getBookmark("donate-thanks")) {
case (doc, resolver) => views.html.site.page(doc, resolver)
}
}

View file

@ -33,9 +33,10 @@ object Editor extends LilaController {
def game(id: String) = Open { implicit ctx =>
OptionResult(GameRepo game id) { game =>
Redirect(routes.Editor.load(
get("fen") | (chess.format.Forsyth >> game.toChess)
))
Redirect {
if (game.playable) routes.Round.watcher(game.id, "white")
else routes.Editor.load(get("fen") | (chess.format.Forsyth >> game.toChess))
}
}
}
}

View file

@ -1,10 +1,14 @@
package controllers
import scala.concurrent.duration._
import lila.app._
import views._
object ForumPost extends LilaController with ForumController {
private val CreateRateLimit = new lila.memo.RateLimit(4, 5 minutes)
def search(text: String, page: Int) = OpenBody { implicit ctx =>
NotForKids {
text.trim.isEmpty.fold(
@ -25,21 +29,23 @@ object ForumPost extends LilaController with ForumController {
}
def create(categSlug: String, slug: String, page: Int) = OpenBody { implicit ctx =>
CategGrantWrite(categSlug) {
implicit val req = ctx.body
OptionFuResult(topicApi.show(categSlug, slug, page, ctx.troll)) {
case (categ, topic, posts) =>
if (topic.closed) fuccess(BadRequest("This topic is closed"))
else forms.post.bindFromRequest.fold(
err => forms.anyCaptcha flatMap { captcha =>
ctx.userId ?? Env.timeline.status(s"forum:${topic.id}") map { unsub =>
BadRequest(html.forum.topic.show(categ, topic, posts, Some(err -> captcha), unsub))
CreateRateLimit(ctx.req.remoteAddress) {
CategGrantWrite(categSlug) {
implicit val req = ctx.body
OptionFuResult(topicApi.show(categSlug, slug, page, ctx.troll)) {
case (categ, topic, posts) =>
if (topic.closed) fuccess(BadRequest("This topic is closed"))
else forms.post.bindFromRequest.fold(
err => forms.anyCaptcha flatMap { captcha =>
ctx.userId ?? Env.timeline.status(s"forum:${topic.id}") map { unsub =>
BadRequest(html.forum.topic.show(categ, topic, posts, Some(err -> captcha), unsub))
}
},
data => postApi.makePost(categ, topic, data) map { post =>
Redirect(routes.ForumPost.redirect(post.id))
}
},
data => postApi.makePost(categ, topic, data) map { post =>
Redirect(routes.ForumPost.redirect(post.id))
}
)
)
}
}
}
}

View file

@ -1,11 +1,15 @@
package controllers
import scala.concurrent.duration._
import lila.app._
import lila.forum.CategRepo
import views._
object ForumTopic extends LilaController with ForumController {
private val CreateRateLimit = new lila.memo.RateLimit(2, 5 minutes)
def form(categSlug: String) = Open { implicit ctx =>
NotForKids {
CategGrantWrite(categSlug) {
@ -17,17 +21,19 @@ object ForumTopic extends LilaController with ForumController {
}
def create(categSlug: String) = OpenBody { implicit ctx =>
CategGrantWrite(categSlug) {
implicit val req = ctx.body
OptionFuResult(CategRepo bySlug categSlug) { categ =>
forms.topic.bindFromRequest.fold(
err => forms.anyCaptcha map { captcha =>
BadRequest(html.forum.topic.form(categ, err, captcha))
},
data => topicApi.makeTopic(categ, data) map { topic =>
Redirect(routes.ForumTopic.show(categ.slug, topic.slug, 1))
}
)
CreateRateLimit(ctx.req.remoteAddress) {
CategGrantWrite(categSlug) {
implicit val req = ctx.body
OptionFuResult(CategRepo bySlug categSlug) { categ =>
forms.topic.bindFromRequest.fold(
err => forms.anyCaptcha map { captcha =>
BadRequest(html.forum.topic.form(categ, err, captcha))
},
data => topicApi.makeTopic(categ, data) map { topic =>
Redirect(routes.ForumTopic.show(categ.slug, topic.slug, 1))
}
)
}
}
}
}

View file

@ -1,6 +1,7 @@
package controllers
import play.api.data.Form
import play.api.libs.json.Json
import lila.api.Context
import lila.app._
@ -18,18 +19,28 @@ object I18n extends LilaController {
implicit val req = ctx.body
Form(single("lang" -> text.verifying(env.pool contains _))).bindFromRequest.fold(
_ => notFound,
lang => (ctx.me ?? { me => lila.user.UserRepo.setLang(me.id, lang) }) inject Redirect {
s"${Env.api.Net.Protocol}${lang}.${Env.api.Net.Domain}" + {
HTTPRequest.referer(ctx.req).fold(routes.Lobby.home.url) { str =>
try {
new java.net.URL(str).getPath
}
catch {
case e: java.net.MalformedURLException => routes.Lobby.home.url
lang => {
ctx.me.filterNot(_.lang contains lang) ?? { me =>
lila.user.UserRepo.setLang(me.id, lang)
}
} >> negotiate(
html = Redirect {
s"${Env.api.Net.Protocol}${lang}.${Env.api.Net.Domain}" + {
HTTPRequest.referer(ctx.req).fold(routes.Lobby.home.url) { str =>
try {
val pageUrl = new java.net.URL(str);
val path = pageUrl.getPath
val query = pageUrl.getQuery
if (query == null) path
else path + "?" + query
}
catch {
case e: java.net.MalformedURLException => routes.Lobby.home.url
}
}
}
}
}
}.fuccess,
api = _ => Ok(Json.obj("lang" -> lang)).fuccess)
)
}

View file

@ -20,7 +20,7 @@ object Importer extends LilaController {
failure => fuccess {
Ok(html.game.importGame(failure))
},
data => env.importer(data, ctx.userId, ctx.ip) map { game =>
data => env.importer(data, ctx.userId) map { game =>
if (game.analysable) Analyse.addCallbacks(game.id) {
Env.analyse.analyser.getOrGenerate(
game.id,

View file

@ -0,0 +1,76 @@
package controllers
import lila.api.Context
import lila.app._
import lila.insight.{ Metric, Dimension }
import play.api.libs.json.Json
import play.api.mvc._
import views._
object Insight extends LilaController {
private def env = Env.insight
def refresh(username: String) = Open { implicit ctx =>
Accessible(username) { user =>
env.api indexAll user inject Ok
}
}
def index(username: String) = path(username,
metric = Metric.MeanCpl.key,
dimension = Dimension.Perf.key,
filters = "")
def path(username: String, metric: String, dimension: String, filters: String) = Open { implicit ctx =>
Accessible(username) { user =>
import lila.insight.InsightApi.UserStatus._
env.api userStatus user flatMap {
case NoGame => Ok(html.insight.noGame(user)).fuccess
case Empty => Ok(html.insight.empty(user)).fuccess
case s => for {
cache <- env.api userCache user
prefId <- env.share getPrefId user
} yield Ok(html.insight.index(
u = user,
cache = cache,
prefId = prefId,
ui = env.jsonView.ui(cache.ecos),
question = env.jsonView.question(metric, dimension, filters),
stale = s == Stale))
}
}
}
def json(username: String) = OpenBody(BodyParsers.parse.json) { implicit ctx =>
import lila.insight.JsonQuestion, JsonQuestion._
Accessible(username) { user =>
ctx.body.body.validate[JsonQuestion].fold(
err => BadRequest(Json.obj("error" -> err.toString)).fuccess,
qJson => qJson.question.fold(BadRequest.fuccess) { q =>
env.api.ask(q, user) map
lila.insight.Chart.fromAnswer(Env.user.lightUser) map
env.jsonView.chart.apply map { Ok(_) }
}
)
}
}
private def Accessible(username: String)(f: lila.user.User => Fu[Result])(implicit ctx: Context) =
lila.user.UserRepo named username flatMap {
_.fold(notFound) { u =>
env.share.grant(u, ctx.me) flatMap {
_.fold(f(u), fuccess(Forbidden(html.insight.forbidden(u))))
}
}
}
private def AccessibleJson(username: String)(f: lila.user.User => Fu[Result])(implicit ctx: Context) =
lila.user.UserRepo named username flatMap {
_.fold(notFoundJson(s"No such user: $username")) { u =>
env.share.grant(u, ctx.me) flatMap {
_.fold(f(u), fuccess(Forbidden(Json.obj("error" -> s"User $username data is protected"))))
}
}
} map (_ as JSON)
}

View file

@ -59,12 +59,6 @@ object Main extends LilaController {
}
}
def irc = Open { implicit ctx =>
ctx.me ?? Env.team.api.mine map {
html.site.irc(_)
}
}
def themepicker = Open { implicit ctx =>
fuccess {
html.base.themepicker()
@ -78,17 +72,30 @@ object Main extends LilaController {
}
def mobile = Open { implicit ctx =>
OptionOk(Prismic oneShotBookmark "mobile-apk") {
OptionOk(Prismic getBookmark "mobile-apk") {
case (doc, resolver) => html.mobile.home(doc, resolver)
}
}
def jslog = Open { ctx =>
def mobileRegister(platform: String, deviceId: String) = Auth { implicit ctx =>
me =>
Env.push.registerDevice(me, platform, deviceId)
}
def mobileUnregister = Auth { implicit ctx =>
me =>
Env.push.unregisterDevices(me)
}
def jslog(id: String) = Open { ctx =>
val referer = HTTPRequest.referer(ctx.req)
loginfo(s"[jslog] ${ctx.req.remoteAddress} ${ctx.userId} $referer")
ctx.userId.?? {
Env.report.api.autoBotReport(_, referer)
}
lila.game.GameRepo pov id map {
_ ?? lila.game.GameRepo.setBorderAlert
} inject Ok
}
def notFound(req: RequestHeader): Fu[Result] =

View file

@ -9,6 +9,7 @@ import play.twirl.api.Html
import lila.api.Context
import lila.app._
import lila.user.{ User => UserModel, UserRepo }
import lila.security.Granter
import views._
object Message extends LilaController {
@ -21,7 +22,7 @@ object Message extends LilaController {
def inbox(page: Int) = Auth { implicit ctx =>
me =>
NotForKids {
api updateUser me.id
api updateUser me
api.inbox(me, page) map { html.message.inbox(me, _) }
}
}
@ -34,11 +35,11 @@ object Message extends LilaController {
implicit me =>
NotForKids {
OptionFuOk(api.thread(id, me)) { thread =>
relationApi.blocks(thread otherUserId me, me.id) map { blocked =>
relationApi.fetchBlocks(thread otherUserId me, me.id) map { blocked =>
html.message.thread(thread, forms.post, blocked,
answerable = !Env.message.LichessSenders.contains(thread.creatorId))
}
}
} map NoCache
}
}
@ -47,7 +48,7 @@ object Message extends LilaController {
OptionFuResult(api.thread(id, me)) { thread =>
implicit val req = ctx.body
forms.post.bindFromRequest.fold(
err => relationApi.blocks(thread otherUserId me, me.id) map { blocked =>
err => relationApi.fetchBlocks(thread otherUserId me, me.id) map { blocked =>
BadRequest(html.message.thread(thread, err, blocked,
answerable = !Env.message.LichessSenders.contains(thread.creatorId)))
},
@ -78,7 +79,8 @@ object Message extends LilaController {
private def renderForm(me: UserModel, title: Option[String], f: Form[_] => Form[_])(implicit ctx: Context): Fu[Html] =
get("user") ?? UserRepo.named flatMap { user =>
user.fold(fuccess(true))(u => security.canMessage(me.id, u.id)) map { canMessage =>
html.message.form(f(forms thread me), user, title, canMessage)
html.message.form(f(forms thread me), user, title,
canMessage = canMessage || Granter(_.MessageAnyone)(me))
}
}

View file

@ -78,6 +78,13 @@ object Mod extends LilaController {
}
}
def notifySlack(username: String) = Auth { implicit ctx =>
me =>
OptionFuResult(UserRepo named username) { user =>
Env.slack.api.userMod(user = user, mod = me) inject redirect(user.username)
}
}
def log = Secure(_.SeeReport) { implicit ctx =>
me => modLogApi.recent map { html.mod.log(_) }
}
@ -121,4 +128,28 @@ object Mod extends LilaController {
def refreshUserAssess(username: String) = Secure(_.MarkEngine) { implicit ctx =>
me => assessApi.refreshAssessByUsername(username) inject redirect(username)
}
def gamify = Secure(_.SeeReport) { implicit ctx =>
me =>
Env.mod.gamify.leaderboards zip
Env.mod.gamify.history(orCompute = true) map {
case (leaderboards, history) => Ok(html.mod.gamify.index(leaderboards, history))
}
}
def gamifyPeriod(periodStr: String) = Secure(_.SeeReport) { implicit ctx =>
me =>
lila.mod.Gamify.Period(periodStr).fold(notFound) { period =>
Env.mod.gamify.leaderboards map { leaderboards =>
Ok(html.mod.gamify.period(leaderboards, period))
}
}
}
def search = Secure(_.UserSearch) { implicit ctx =>
me =>
val query = (~get("q")).trim
Env.mod.search(query) map { users =>
html.mod.search(query, users)
}
}
}

View file

@ -7,23 +7,38 @@ import views._
object Page extends LilaController {
private def page(bookmark: String) = Open { implicit ctx =>
OptionOk(Prismic oneShotBookmark bookmark) {
private def bookmark(name: String) = Open { implicit ctx =>
OptionOk(Prismic getBookmark name) {
case (doc, resolver) => views.html.site.page(doc, resolver)
}
}
def thanks = page("thanks")
def thanks = bookmark("thanks")
def tos = page("tos")
def tos = bookmark("tos")
def helpLichess = page("help")
def contribute = bookmark("help")
def streamHowTo = page("stream-howto")
def streamHowTo = bookmark("stream-howto")
def contact = page("contact")
def contact = bookmark("contact")
def kingOfTheHill = page("king-of-the-hill")
def master = bookmark("master")
def privacy = page("privacy")
def privacy = bookmark("privacy")
def variantHome = Open { implicit ctx =>
OptionOk(Prismic getBookmark "variant") {
case (doc, resolver) => views.html.site.variantHome(doc, resolver)
}
}
def variant(key: String) = Open { implicit ctx =>
(for {
variant <- chess.variant.Variant.byKey get key
perfType <- lila.rating.PerfType byVariant variant
} yield OptionOk(Prismic getVariant variant) {
case (doc, resolver) => views.html.site.variant(doc, resolver, variant, perfType)
}) | notFound
}
}

View file

@ -27,10 +27,6 @@ object Prismic {
case _ => routes.Lobby.home.url
}
def getBookmark(name: String): Fu[Option[Document]] = prismicApi flatMap { api =>
api.bookmarks.get(name) ?? getDocument
}
def getDocument(id: String): Fu[Option[Document]] = prismicApi flatMap { api =>
api.forms("everything")
.query(s"""[[:d = at(document.id, "$id")]]""")
@ -40,9 +36,22 @@ object Prismic {
}
}
def oneShotBookmark(name: String) = fetchPrismicApi(true) flatMap { api =>
getBookmark(name) map2 { (doc: io.prismic.Document) =>
def getBookmark(name: String) = fetchPrismicApi(true) flatMap { api =>
api.bookmarks.get(name) ?? getDocument map2 { (doc: io.prismic.Document) =>
doc -> makeLinkResolver(api)
}
} recover {
case e: Exception =>
play.api.Logger("prismic").error(s"bookmark:$name $e")
none
}
def getVariant(variant: chess.variant.Variant) = prismicApi flatMap { api =>
api.forms("variant")
.query(s"""[[:d = at(my.variant.key, "${variant.key}")]]""")
.ref(api.master.ref)
.submit() map {
_.results.headOption map (_ -> makeLinkResolver(api))
}
}
}

View file

@ -6,6 +6,7 @@ import play.twirl.api.Html
import lila.api.Context
import lila.app._
import lila.common.paginator.{ Paginator, AdapterLike }
import lila.relation.Related
import lila.user.{ User => UserModel, UserRepo }
import views._
@ -15,9 +16,9 @@ object Relation extends LilaController {
private def env = Env.relation
private def renderActions(userId: String, mini: Boolean)(implicit ctx: Context) =
(ctx.userId ?? { env.api.relation(_, userId) }) zip
(ctx.userId ?? { env.api.fetchRelation(_, userId) }) zip
(ctx.isAuth ?? { Env.pref.api followable userId }) zip
(ctx.userId ?? { env.api.blocks(userId, _) }) flatMap {
(ctx.userId ?? { env.api.fetchBlocks(userId, _) }) flatMap {
case ((relation, followable), blocked) => negotiate(
html = fuccess(Ok(mini.fold(
html.relation.mini(userId, blocked = blocked, followable = followable, relation = relation),
@ -25,8 +26,8 @@ object Relation extends LilaController {
))),
api = _ => fuccess(Ok(Json.obj(
"followable" -> followable,
"following" -> relation.exists(true ==),
"blocking" -> relation.exists(false ==)
"following" -> relation.contains(true),
"blocking" -> relation.contains(false)
)))
)
}
@ -51,70 +52,46 @@ object Relation extends LilaController {
env.api.unblock(me.id, userId).nevermind >> renderActions(userId, getBool("mini"))
}
def following(username: String) = Open { implicit ctx =>
def following(username: String, page: Int) = Open { implicit ctx =>
OptionFuOk(UserRepo named username) { user =>
env.api.following(user.id) flatMap followship flatMap { rels =>
env.api nbFollowers user.id map { followers =>
html.relation.following(user, rels, followers)
env.api countFollowers user.id flatMap { nbFollowers =>
RelatedPager(env.api.followingPaginatorAdapter(user.id), page) map { pag =>
html.relation.following(user, pag, nbFollowers)
}
}
}
}
def followers(username: String) = Open { implicit ctx =>
def followers(username: String, page: Int) = Open { implicit ctx =>
OptionFuOk(UserRepo named username) { user =>
env.api.followers(user.id) flatMap followship flatMap { rels =>
env.api nbFollowing user.id map { following =>
html.relation.followers(user, rels, following)
env.api countFollowing user.id flatMap { nbFollowing =>
RelatedPager(env.api.followersPaginatorAdapter(user.id), page) map { pag =>
html.relation.followers(user, pag, nbFollowing)
}
}
}
}
def blocks = Auth { implicit ctx =>
def blocks(page: Int) = Auth { implicit ctx =>
me =>
env.api.blocking(me.id) flatMap followship map { rels =>
html.relation.blocks(me, rels)
RelatedPager(env.api.blockingPaginatorAdapter(me.id), page) map { pag =>
html.relation.blocks(me, pag)
}
}
private def followship(userIds: Set[String])(implicit ctx: Context): Fu[List[Related]] =
private def RelatedPager(adapter: AdapterLike[String], page: Int)(implicit ctx: Context) = Paginator(
adapter = adapter mapFutureList followship,
currentPage = page,
maxPerPage = 30)
private def followship(userIds: Seq[String])(implicit ctx: Context): Fu[List[Related]] =
UserRepo byIds userIds flatMap { users =>
(ctx.isAuth ?? { Env.pref.api.followableIds(users map (_.id)) }) flatMap { followables =>
users.map { u =>
ctx.userId ?? { env.api.relation(_, u.id) } map { rel =>
ctx.userId ?? { env.api.fetchRelation(_, u.id) } map { rel =>
lila.relation.Related(u, 0, followables(u.id), rel)
}
}.sequenceFu
}
}
def suggest(username: String) = Open { implicit ctx =>
OptionFuResult(UserRepo named username) { user =>
lila.game.BestOpponents(user.id, 50) flatMap { opponents =>
Env.pref.api.followableIds(opponents map (_._1.id)) zip
env.api.onlinePopularUsers(20) flatMap {
case (followables, popular) =>
popular.filterNot(user ==).foldLeft(opponents filter {
case (u, _) => followables contains u.id
}) {
case (xs, x) => xs.exists(_._1 == x).fold(xs, xs :+ (x, 0))
}.map {
case (u, nb) => env.api.relation(user.id, u.id) map {
lila.relation.Related(u, nb, true, _)
}
}.sequenceFu flatMap { rels =>
negotiate(
html = fuccess(Ok(html.relation.suggest(user, rels))),
api = _ => fuccess {
implicit val userWrites = play.api.libs.json.Writes[UserModel] { Env.user.jsonView(_, true) }
Ok(Json.obj(
"user" -> user,
"suggested" -> play.api.libs.json.JsArray(rels.map(_.toJson))))
})
}
}
}
}
}
}

View file

@ -43,15 +43,15 @@ object Report extends LilaController {
BadRequest(html.report.form(err, user, captcha))
}
},
data => api.create(data, me) map { thread =>
Redirect(routes.Report.thanks)
data => api.create(data, me) map { report =>
Redirect(routes.Report.thanks(data.user.username))
})
}
def thanks = Auth { implicit ctx =>
def thanks(reported: String) = Auth { implicit ctx =>
implicit me =>
fuccess {
html.report.thanks()
Env.relation.api.fetchBlocks(me.id, reported) map { blocked =>
html.report.thanks(reported, blocked)
}
}
}

View file

@ -19,8 +19,7 @@ object Setup extends LilaController with TheftPrevention {
private def env = Env.setup
private val FormRateLimit = new lila.memo.RateLimitByKey(500 millis)
private val PostRateLimit = new lila.memo.RateLimitByKey(500 millis)
private val PostRateLimit = new lila.memo.RateLimit(5, 1 minute)
def aiForm = Open { implicit ctx =>
if (HTTPRequest isXhr ctx.req) {
@ -62,10 +61,11 @@ object Setup extends LilaController with TheftPrevention {
private def challenge(user: lila.user.User)(implicit ctx: Context): Fu[Option[String]] = ctx.me match {
case None => fuccess("Only registered players can send challenges.".some)
case Some(me) => Env.relation.api.blocks(user.id, me.id) flatMap {
case Some(me) => Env.relation.api.fetchBlocks(user.id, me.id) flatMap {
case true => fuccess(s"{{user}} doesn't accept challenges from you.".some)
case false => Env.pref.api getPref user zip Env.relation.api.follows(user.id, me.id) map {
case (pref, follow) => lila.pref.Pref.Challenge.block(me, user, pref.challenge, follow)
case false => Env.pref.api getPref user zip Env.relation.api.fetchFollows(user.id, me.id) map {
case (pref, follow) => lila.pref.Pref.Challenge.block(me, user, pref.challenge, follow,
fromCheat = me.engine && !user.engine)
}
}
}
@ -107,27 +107,14 @@ object Setup extends LilaController with TheftPrevention {
}
def hookForm = Open { implicit ctx =>
if (HTTPRequest isXhr ctx.req) FormRateLimit(ctx.req.remoteAddress) {
NoPlaybanOrCurrent {
env.forms.hookFilled(timeModeString = get("time")) map { html.setup.hook(_) }
}
if (HTTPRequest isXhr ctx.req) NoPlaybanOrCurrent {
env.forms.hookFilled(timeModeString = get("time")) map { html.setup.hook(_) }
}
else fuccess {
Redirect(routes.Lobby.home + "#hook")
}
}
// if request comes from mobile
// and the hook is casual,
// reuse the saved "membersOnly" value
// from the site preferred hook setup
private def mobileHookAllowAnon(config: HookConfig)(implicit ctx: Context): Fu[HookConfig] =
if (lila.api.Mobile.Api requested ctx.req)
env.forms.hookConfig map { saved =>
config.copy(allowAnon = saved.allowAnon)
}
else fuccess(config)
private def hookResponse(hookId: String) =
Ok(Json.obj(
"ok" -> true,
@ -141,13 +128,12 @@ object Setup extends LilaController with TheftPrevention {
err => negotiate(
html = BadRequest(errorsAsJson(err).toString).fuccess,
api = _ => BadRequest(errorsAsJson(err)).fuccess),
preConfig => (ctx.userId ?? Env.relation.api.blocking) zip
mobileHookAllowAnon(preConfig) flatMap {
case (blocking, config) =>
env.processor.hook(config, uid, HTTPRequest sid req, blocking) map hookResponse recover {
case e: IllegalArgumentException => BadRequest(Json.obj("error" -> e.getMessage)) as JSON
}
}
config => (ctx.userId ?? Env.relation.api.fetchBlocking) flatMap {
blocking =>
env.processor.hook(config, uid, HTTPRequest sid req, blocking) map hookResponse recover {
case e: IllegalArgumentException => BadRequest(Json.obj("error" -> e.getMessage)) as JSON
}
}
)
}
}
@ -160,7 +146,7 @@ object Setup extends LilaController with TheftPrevention {
GameRepo game gameId map {
_.fold(config)(config.updateFrom)
} flatMap { config =>
(ctx.userId ?? Env.relation.api.blocking) flatMap { blocking =>
(ctx.userId ?? Env.relation.api.fetchBlocking) flatMap { blocking =>
env.processor.hook(config, uid, HTTPRequest sid ctx.req, blocking) map hookResponse recover {
case e: IllegalArgumentException => BadRequest(Json.obj("error" -> e.getMessage)) as JSON
}

View file

@ -8,7 +8,7 @@ import views._
object Stat extends LilaController {
def ratingDistribution(perfKey: lila.rating.Perf.Key) = Open { implicit ctx =>
lila.rating.PerfType(perfKey).filter(lila.rating.PerfType.nonPuzzle.contains) match {
lila.rating.PerfType(perfKey).filter(lila.rating.PerfType.isGame) match {
case Some(perfType) => Env.user.cached.ratingDistribution(perfType.key) map { data =>
Ok(html.stat.ratingDistribution(perfType, data))
}

View file

@ -75,7 +75,7 @@ object Team extends LilaController {
me => OptionFuResult(api team id) { team =>
Owner(team) {
MemberRepo userIdsByTeam team.id map { userIds =>
html.team.kick(team, userIds filterNot (me.id ==))
html.team.kick(team, userIds.filterNot(me.id ==).toList.sorted)
}
}
}
@ -90,6 +90,15 @@ object Team extends LilaController {
}
}
def close(id: String) = Secure(_.CloseTeam) { implicit ctx =>
me =>
OptionFuResult(api team id) { team =>
(api delete team) >>
Env.mod.logApi.deleteTeam(me.id, team.name, team.description) inject
Redirect(routes.Team all 1)
}
}
def form = Auth { implicit ctx =>
me =>
NotForKids {

View file

@ -19,14 +19,25 @@ object Tournament extends LilaController {
private def tournamentNotFound(implicit ctx: Context) = NotFound(html.tournament.notFound())
val home = Open { implicit ctx =>
env.api.fetchVisibleTournaments zip
repo.scheduledDedup zip
repo.finished(30) zip
UserRepo.allSortToints(10) map {
case (((visible, scheduled), finished), leaderboard) =>
Ok(html.tournament.home(scheduled, finished, leaderboard, env scheduleJsonView visible))
} map NoCache
def home(page: Int) = Open { implicit ctx =>
negotiate(
html = {
val finishedPaginator = repo.finishedPaginator(maxPerPage = 30, page = page)
if (HTTPRequest isXhr ctx.req) finishedPaginator map { pag =>
Ok(html.tournament.finishedPaginator(pag))
}
else env.api.fetchVisibleTournaments zip
repo.scheduledDedup zip
finishedPaginator zip
UserRepo.allSortToints(10) map {
case (((visible, scheduled), finished), leaderboard) =>
Ok(html.tournament.home(scheduled, finished, leaderboard, env scheduleJsonView visible))
} map NoCache
},
api = _ => env.api.fetchVisibleTournaments map { tours =>
Ok(env scheduleJsonView tours)
}
)
}
def help(sysStr: Option[String]) = Open { implicit ctx =>
@ -129,6 +140,15 @@ object Tournament extends LilaController {
}
}
def terminate(id: String) = Secure(_.TerminateTournament) { implicit ctx =>
me =>
OptionResult(repo startedById id) { tour =>
env.api finish tour
Env.mod.logApi.terminateTournament(me.id, tour.fullName)
Redirect(routes.Tournament show tour.id)
}
}
def form = Auth { implicit ctx =>
me =>
NoLame {

View file

@ -39,8 +39,9 @@ object Tv extends LilaController {
Env.api.roundApi.watcher(pov, lila.api.Mobile.Api.currentVersion, tv = onTv.some) zip
Env.game.crosstableApi(game) zip
Env.tv.tv.getChampions map {
case ((data, cross), champions) =>
case ((data, cross), champions) => NoCache {
Ok(html.tv.index(channel, champions, pov, data, cross, flip))
}
}
},
api = apiVersion => Env.api.roundApi.watcher(pov, apiVersion, tv = onTv.some) map { Ok(_) }
@ -56,20 +57,24 @@ object Tv extends LilaController {
private def lichessGames(channel: lila.tv.Tv.Channel)(implicit ctx: Context) =
Env.tv.tv.getChampions zip
Env.tv.tv.getGames(channel, 9) map {
case (champs, games) =>
case (champs, games) => NoCache {
Ok(html.tv.games(channel, games map lila.game.Pov.first, champs))
}
}
def streamIn(id: String) = Open { implicit ctx =>
Env.tv.streamsOnAir flatMap { streams =>
streams find (_.id == id) match {
case None => notFound
case Some(s) => fuccess(Ok(html.tv.stream(s, streams filterNot (_.id == id))))
OptionFuResult(Env.tv.streamerList find id) { streamer =>
Env.tv.streamsOnAir.all flatMap { streams =>
val others = streams.filter(_.id != id)
streams find (_.id == id) match {
case None => fuccess(Ok(html.tv.notStreaming(streamer, others)))
case Some(s) => fuccess(Ok(html.tv.stream(s, others)))
}
}
}
}
def streamOut = Action.async {
def feed = Action.async {
import makeTimeout.short
import akka.pattern.ask
import lila.round.TvBroadcast

View file

@ -42,11 +42,11 @@ object User extends LilaController {
def showMini(username: String) = Open { implicit ctx =>
OptionFuResult(UserRepo named username) { user =>
GameRepo lastPlayedPlaying user zip
Env.donation.isDonor(user.id) zip
(ctx.userId ?? { relationApi.blocks(user.id, _) }) zip
Env.donation.isDonor(user.id) zip
(ctx.userId ?? { relationApi.fetchBlocks(user.id, _) }) zip
(ctx.userId ?? { Env.game.crosstableApi(user.id, _) }) zip
(ctx.isAuth ?? { Env.pref.api.followable(user.id) }) zip
(ctx.userId ?? { relationApi.relation(_, user.id) }) map {
(ctx.userId ?? { relationApi.fetchRelation(_, user.id) }) map {
case (((((pov, donor), blocked), crosstable), followable), relation) =>
Ok(html.user.mini(user, pov, blocked, followable, relation, crosstable, donor))
.withHeaders(CACHE_CONTROL -> "max-age=5")
@ -102,12 +102,12 @@ object User extends LilaController {
filter = filters.current,
me = ctx.me,
page = page)(ctx.body)
relation <- ctx.userId ?? { relationApi.relation(_, u.id) }
relation <- ctx.userId ?? { relationApi.fetchRelation(_, u.id) }
notes <- ctx.me ?? { me =>
relationApi friends me.id flatMap { env.noteApi.get(u, me, _) }
relationApi fetchFriends me.id flatMap { env.noteApi.get(u, me, _) }
}
followable <- ctx.isAuth ?? { Env.pref.api followable u.id }
blocked <- ctx.userId ?? { relationApi.blocks(u.id, _) }
blocked <- ctx.userId ?? { relationApi.fetchBlocks(u.id, _) }
searchForm = GameFilterMenu.searchForm(userGameSearch, filters.current)(ctx.body)
} yield html.user.show(u, info, pag, filters, searchForm, relation, notes, followable, blocked)
@ -130,20 +130,12 @@ object User extends LilaController {
def list = Open { implicit ctx =>
val nb = 10
for {
bullet env.cached topPerf PerfType.Bullet.key
blitz env.cached topPerf PerfType.Blitz.key
classical env.cached topPerf PerfType.Classical.key
chess960 env.cached topPerf PerfType.Chess960.key
kingOfTheHill env.cached topPerf PerfType.KingOfTheHill.key
threeCheck env.cached topPerf PerfType.ThreeCheck.key
antichess <- env.cached topPerf PerfType.Antichess.key
atomic <- env.cached topPerf PerfType.Atomic.key
horde <- env.cached topPerf PerfType.Horde.key
nbAllTime env.cached topNbGame nb map2 { (user: UserModel) =>
user -> user.count.game
}
nbDay Env.game.cached activePlayerUidsDay nb flatMap { pairs =>
UserRepo.byOrderedIds(pairs.map(_.userId)) map (_ zip pairs.map(_.nb))
leaderboards <- env.cached.leaderboards
nbAllTime env.cached topNbGame nb
nbDay Env.game.cached activePlayerUidsDay nb map {
_ flatMap { pair =>
env lightUser pair.userId map { UserModel.LightCount(_, pair.nb) }
}
}
tourneyWinners Env.tournament.winners scheduled nb
online env.cached topOnline 50
@ -151,42 +143,51 @@ object User extends LilaController {
html = fuccess(Ok(html.user.list(
tourneyWinners = tourneyWinners,
online = online,
bullet = bullet,
blitz = blitz,
classical = classical,
chess960 = chess960,
kingOfTheHill = kingOfTheHill,
threeCheck = threeCheck,
antichess = antichess,
atomic = atomic,
horde = horde,
leaderboards = leaderboards,
nbDay = nbDay,
nbAllTime = nbAllTime))),
api = _ => fuccess {
implicit val userWrites = play.api.libs.json.Writes[UserModel] { env.jsonView(_, true) }
implicit val lightPerfWrites = play.api.libs.json.Writes[UserModel.LightPerf] { l =>
Json.obj(
"id" -> l.user.id,
"username" -> l.user.name,
"title" -> l.user.title,
"perfs" -> Json.obj(
l.perfKey -> Json.obj("rating" -> l.rating, "progress" -> l.progress)))
}
Ok(Json.obj(
"online" -> online,
"bullet" -> bullet,
"blitz" -> blitz,
"classical" -> classical,
"chess960" -> chess960,
"kingOfTheHill" -> kingOfTheHill,
"threeCheck" -> threeCheck,
"antichess" -> antichess,
"atomic" -> atomic,
"horde" -> horde))
"bullet" -> leaderboards.bullet,
"blitz" -> leaderboards.blitz,
"classical" -> leaderboards.classical,
"crazyhouse" -> leaderboards.crazyhouse,
"chess960" -> leaderboards.chess960,
"kingOfTheHill" -> leaderboards.kingOfTheHill,
"threeCheck" -> leaderboards.threeCheck,
"antichess" -> leaderboards.antichess,
"atomic" -> leaderboards.atomic,
"horde" -> leaderboards.horde,
"racingKings" -> leaderboards.racingKings))
})
} yield res
}
def top200(perfKey: String) = Open { implicit ctx =>
lila.rating.PerfType(perfKey).fold(notFound) { perfType =>
env.cached top200Perf perfType.key map { users =>
Ok(html.user.top200(perfType, users))
}
}
}
def mod(username: String) = Secure(_.UserSpy) { implicit ctx =>
me => OptionFuOk(UserRepo named username) { user =>
(!isGranted(_.SetEmail, user) ?? UserRepo.email(user.id)) zip
(Env.security userSpy user.id) zip
(Env.mod.assessApi.getPlayerAggregateAssessmentWithGames(user.id)) flatMap {
case ((email, spy), playerAggregateAssessment) =>
(Env.mod.assessApi.getPlayerAggregateAssessmentWithGames(user.id)) zip
Env.mod.logApi.userHistory(user.id) flatMap {
case ((((email, spy), playerAggregateAssessment), history)) =>
(Env.playban.api bans spy.usersSharingIp.map(_.id)) map { bans =>
html.user.mod(user, email, spy, playerAggregateAssessment, bans)
html.user.mod(user, email, spy, playerAggregateAssessment, bans, history)
}
}
}
@ -210,8 +211,8 @@ object User extends LilaController {
fuccess(List.fill(50)(true))
) flatMap { followables =>
(ops zip followables).map {
case ((u, nb), followable) => ctx.userId ?? { myId =>
relationApi.relation(myId, u.id)
case ((u, nb), followable) => ctx.userId ?? {
relationApi.fetchRelation(_, u.id)
} map { lila.relation.Related(u, nb, followable, _) }
}.sequenceFu map { relateds =>
html.user.opponents(user, relateds)
@ -221,6 +222,25 @@ object User extends LilaController {
}
}
def perfStat(username: String, perfKey: String) = Open { implicit ctx =>
OptionFuResult(UserRepo named username) { u =>
if ((u.disabled || (u.lame && !ctx.is(u))) && !isGranted(_.UserSpy)) notFound
else lila.rating.PerfType(perfKey).fold(notFound) { perfType =>
for {
perfStat <- Env.perfStat.get(u, perfType)
ranks <- Env.user.cached.ranking.getAll(u.id)
distribution <- u.perfs(perfType).established ?? {
Env.user.cached.ratingDistribution(perfType.key) map some
}
data = Env.perfStat.jsonView(u, perfStat, ranks get perfType.key, distribution)
response <- negotiate(
html = Ok(html.user.perfStat(u, ranks, perfType, data)).fuccess,
api = _ => Ok(data).fuccess)
} yield response
}
}
}
def autocomplete = Open { implicit ctx =>
get("term", ctx.req).filter(_.nonEmpty).fold(BadRequest("No search term provided").fuccess: Fu[Result]) { term =>
JsonOk(UserRepo usernamesLike term)

View file

@ -5,9 +5,11 @@ import chess.format.Forsyth.SituationPlus
import chess.Situation
import play.api.libs.json.Json
import play.api.mvc._
import scala.concurrent.duration._
import lila.app._
import lila.game.{ GameRepo, Pov }
import lila.round.Forecast.{ forecastStepJsonFormat, forecastJsonWriter }
import views._
object UserAnalysis extends LilaController with TheftPrevention {
@ -54,8 +56,6 @@ object UserAnalysis extends LilaController with TheftPrevention {
me =>
import lila.round.Forecast
OptionFuResult(GameRepo pov fullId) { pov =>
import lila.round.Forecast.forecastStepJsonFormat
import lila.round.Forecast.forecastJsonWriter
if (isTheft(pov)) fuccess(theftResponse)
else ctx.body.body.validate[Forecast.Steps].fold(
err => BadRequest(err.toString).fuccess,
@ -68,4 +68,24 @@ object UserAnalysis extends LilaController with TheftPrevention {
})
}
}
def forecastsOnMyTurn(fullId: String, uci: String) = AuthBody(BodyParsers.parse.json) { implicit ctx =>
me =>
import lila.round.Forecast
OptionFuResult(GameRepo pov fullId) { pov =>
if (isTheft(pov)) fuccess(theftResponse)
else {
ctx.body.body.validate[Forecast.Steps].fold(
err => BadRequest(err.toString).fuccess,
forecasts => {
def wait = 50 + (Forecast maxPlies forecasts min 10) * 50
Env.round.forecastApi.playAndSave(pov, uci, forecasts) >>
Env.current.scheduler.after(wait.millis) {
Ok(Json.obj("reload" -> true))
}
}
)
}
}
}
}

View file

@ -0,0 +1,28 @@
package controllers
import lila.app._
import lila.user.UserRepo
import play.api.mvc._
import views._
object UserTournament extends LilaController {
def path(username: String, path: String, page: Int) = Open { implicit ctx =>
OptionFuResult(UserRepo named username) { user =>
path match {
case "recent" =>
Env.tournament.leaderboardApi.recentByUser(user, page).map { entries =>
Ok(html.userTournament.recent(user, entries))
}
case "best" =>
Env.tournament.leaderboardApi.bestByUser(user, page).map { entries =>
Ok(html.userTournament.best(user, entries))
}
case "chart" => Env.tournament.leaderboardApi.chart(user).map { data =>
Ok(html.userTournament.chart(user, data))
}
case _ => notFound
}
}
}
}

View file

@ -1,7 +1,5 @@
package controllers
import play.api.data._, Forms._
import play.api.mvc._
import play.twirl.api.Html
import lila.api.Context

View file

@ -13,9 +13,11 @@ object WorldMap extends LilaController {
Ok(views.html.site.worldMap())
}
def stream = Action {
Ok.chunked(
Env.worldMap.stream.producer &> EventSource()
) as "text/event-stream"
def stream = Action.async {
Env.worldMap.getStream map { stream =>
Ok.chunked(
stream &> EventSource()
) as "text/event-stream"
}
}
}

View file

@ -3,7 +3,7 @@ package mashup
import lila.common.paginator.Paginator
import lila.db.api.SortOrder
import lila.game.{ Game, Query }
import lila.game.{ Game, Query, GameRepo }
import lila.user.User
import play.api.libs.json._
@ -109,10 +109,12 @@ object GameFilterMenu {
case Win => std(Query win user)
case Loss => std(Query loss user)
case Draw => std(Query draw user)
case Playing => pag.apply(
case Playing => pag(
selector = Query nowPlaying user.id,
sort = Seq(),
nb = nb)(page)
nb = nb)(page) addEffect { p =>
p.currentPageResults.filter(_.finishedOrAborted) foreach GameRepo.unsetPlayingUids
}
case Search => userGameSearch(user, page)
}
}

View file

@ -16,7 +16,7 @@ import play.api.libs.json._
final class Preload(
tv: Tv,
leaderboard: Boolean => Fu[List[(User, PerfType)]],
leaderboard: Boolean => Fu[List[User.LightPerf]],
tourneyWinners: Int => Fu[List[Winner]],
timelineEntries: String => Fu[List[Entry]],
streamsOnAir: () => Fu[List[StreamOnAir]],
@ -26,7 +26,7 @@ final class Preload(
getPlayban: String => Fu[Option[TempBan]],
lightUser: String => Option[LightUser]) {
private type Response = (JsObject, List[Entry], List[MiniForumPost], List[Tournament], List[Simul], Option[Game], List[(User, PerfType)], List[Winner], Option[lila.puzzle.DailyPuzzle], List[StreamOnAir], List[lila.blog.MiniPost], Option[TempBan], Option[Preload.CurrentGame], Int)
private type Response = (JsObject, List[Entry], List[MiniForumPost], List[Tournament], List[Simul], Option[Game], List[User.LightPerf], List[Winner], Option[lila.puzzle.DailyPuzzle], List[StreamOnAir], List[lila.blog.MiniPost], Option[TempBan], Option[Preload.CurrentGame], Int)
def apply(
posts: Fu[List[MiniForumPost]],

View file

@ -14,7 +14,7 @@ import lila.user.{ User, Trophy, Trophies, TrophyApi }
case class UserInfo(
user: User,
ranks: Map[lila.rating.Perf.Key, Int],
ranks: lila.rating.UserRankMap,
nbUsers: Int,
nbPlaying: Int,
hasSimul: Boolean,
@ -29,7 +29,8 @@ case class UserInfo(
playTime: User.PlayTime,
donor: Boolean,
trophies: Trophies,
isStreamer: Boolean) {
isStreamer: Boolean,
insightVisible: Boolean) {
def nbRated = user.count.rated
@ -65,21 +66,23 @@ object UserInfo {
getRanks: String => Fu[Map[String, Int]],
isDonor: String => Fu[Boolean],
isHostingSimul: String => Fu[Boolean],
isStreamer: String => Boolean)(user: User, ctx: Context): Fu[UserInfo] =
isStreamer: String => Boolean,
insightShare: lila.insight.Share)(user: User, ctx: Context): Fu[UserInfo] =
countUsers() zip
getRanks(user.id) zip
(gameCached nbPlaying user.id) zip
gameCached.nbImportedBy(user.id) zip
(ctx.me.filter(user!=) ?? { me => crosstableApi(me.id, user.id) }) zip
getRatingChart(user) zip
relationApi.nbFollowing(user.id) zip
relationApi.nbFollowers(user.id) zip
(ctx.me ?? Granter(_.UserSpy) ?? { relationApi.nbBlockers(user.id) map (_.some) }) zip
relationApi.countFollowing(user.id) zip
relationApi.countFollowers(user.id) zip
(ctx.me ?? Granter(_.UserSpy) ?? { relationApi.countBlockers(user.id) map (_.some) }) zip
postApi.nbByUser(user.id) zip
isDonor(user.id) zip
trophyApi.findByUser(user) zip
(user.count.rated >= 10).??(insightShare.grant(user, ctx.me)) zip
PlayTime(user) flatMap {
case ((((((((((((nbUsers, ranks), nbPlaying), nbImported), crosstable), ratingChart), nbFollowing), nbFollowers), nbBlockers), nbPosts), isDonor), trophies), playTime) =>
case (((((((((((((nbUsers, ranks), nbPlaying), nbImported), crosstable), ratingChart), nbFollowing), nbFollowers), nbBlockers), nbPosts), isDonor), trophies), insightVisible), playTime) =>
(nbPlaying > 0) ?? isHostingSimul(user.id) map { hasSimul =>
new UserInfo(
user = user,
@ -98,7 +101,8 @@ object UserInfo {
playTime = playTime,
donor = isDonor,
trophies = trophies,
isStreamer = isStreamer(user.id))
isStreamer = isStreamer(user.id),
insightVisible = insightVisible)
}
}
}

View file

@ -40,6 +40,11 @@ trait AssetHelper { self: I18nHelper =>
test = "window.Highcharts",
local = staticUrl("vendor/highcharts4/highcharts.js"))
val highchartsLatestTag = cdnOrLocal(
cdn = "http://code.highcharts.com/4.1/highcharts.js",
test = "window.Highcharts",
local = staticUrl("vendor/highcharts4/highcharts-4.1.9.js"))
val highchartsMoreTag = Html {
"""<script src="http://code.highcharts.com/4.1.4/highcharts-more.js"></script>"""
}

View file

@ -36,7 +36,6 @@ object Environment
with SecurityHelper
with TeamHelper
with AnalysisHelper
with IRCHelper
with TournamentHelper
with SimulHelper {
@ -68,6 +67,15 @@ object Environment
val mod = Html("&#xe002;")
}
val nonPuzzlePerfTypeNameIcons = {
import play.api.libs.json.Json
Html {
Json stringify {
Json toJson lila.rating.PerfType.nonPuzzleIconByName
}
}
}
def NotForKids[Html](f: => Html)(implicit ctx: lila.api.Context) =
if (ctx.kid) Html("") else f
}

View file

@ -51,6 +51,8 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
case chess.variant.Antichess => "Lose all your pieces to win"
case chess.variant.Atomic => "Explode or mate your opponent's king to win"
case chess.variant.Horde => "Destroy the horde to win"
case chess.variant.RacingKings => "Race to the eighth rank to win"
case chess.variant.Crazyhouse => "Drop captured pieces on the board"
case _ => "Variant ending"
}
case _ => "Game is still being played"
@ -156,6 +158,7 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
case S.VariantEnd => game.variant match {
case chess.variant.KingOfTheHill => trans.kingInTheCenter()
case chess.variant.ThreeCheck => trans.threeChecks()
case chess.variant.RacingKings => trans.raceFinished()
case _ => trans.variantEnding()
}
case _ => Html("")

View file

@ -1,8 +1,6 @@
package lila.app
package templating
import scala.util.Random.shuffle
import controllers._
import play.api.i18n.{ Lang, Messages }
import play.api.mvc.{ RequestHeader, Call }
@ -35,11 +33,7 @@ trait I18nHelper {
def shortLangName(lang: Lang): Option[String] = shortLangName(lang.language)
def shortLangName(lang: String): Option[String] = langName(lang) map (_ takeWhile (','!=))
def translationCall(implicit ctx: UserContext) =
if (ctx.isAnon || ctx.req.cookies.get(hideCallsCookieName).isDefined) None
else (~ctx.me.map(_.count.game) >= i18nEnv.CallThreshold) ?? shuffle(
(ctx.req.acceptLanguages map transInfos.get).flatten filter (_.nonComplete)
).headOption
def translationCall(implicit ctx: UserContext) = i18nEnv.call(ctx.me, ctx.req)
def transValidationPattern(trans: String) =
(trans contains "%s") option ".*%s.*"

View file

@ -1,23 +0,0 @@
package lila.app
package templating
import lila.api.Context
import lila.team.Team
trait IRCHelper { self: TeamHelper with SecurityHelper with I18nHelper =>
private val prompt = "1"
private val uio = "OT10cnVlde"
def myIrcUrl(teams: List[Team])(implicit ctx: Context) =
"""http://webchat.freenode.net?nick=%s&channels=%s&prompt=%s&uio=%s""".format(
ctx.username | "Anon-.",
teamChans(teams) mkString ",",
prompt,
uio)
def teamChans(teams: List[Team]) = teams flatMap teamIrcChan
def teamIrcChan(team: Team) = team.irc option "lichess-team-" + team.id
}

View file

@ -37,24 +37,28 @@ trait SetupHelper { self: I18nHelper =>
(variant.id.toString, variant.name, variant.title.some)
def translatedVariantChoices(implicit ctx: Context) = List(
(chess.variant.Standard.id.toString, trans.standard.str(), chess.variant.Standard.title.some),
variantTuple(chess.variant.Chess960)
(chess.variant.Standard.id.toString, trans.standard.str(), chess.variant.Standard.title.some)
)
def translatedVariantChoicesWithVariants(implicit ctx: Context) =
translatedVariantChoices(ctx) :+
variantTuple(chess.variant.Crazyhouse) :+
variantTuple(chess.variant.Chess960) :+
variantTuple(chess.variant.KingOfTheHill) :+
variantTuple(chess.variant.ThreeCheck) :+
variantTuple(chess.variant.Antichess) :+
variantTuple(chess.variant.Atomic) :+
variantTuple(chess.variant.Horde)
variantTuple(chess.variant.Horde) :+
variantTuple(chess.variant.RacingKings)
def translatedVariantChoicesWithFen(implicit ctx: Context) =
translatedVariantChoices(ctx) :+
variantTuple(chess.variant.Chess960) :+
variantTuple(chess.variant.FromPosition)
def translatedAiVariantChoices(implicit ctx: Context) =
translatedVariantChoices(ctx) :+
variantTuple(chess.variant.Chess960) :+
variantTuple(chess.variant.KingOfTheHill) :+
variantTuple(chess.variant.ThreeCheck) :+
variantTuple(chess.variant.FromPosition)
@ -146,4 +150,7 @@ trait SetupHelper { self: I18nHelper =>
(Pref.Message.ALWAYS, trans.always.str())
)
def translatedBlindfoldChoices(implicit ctx: Context) = List(
Pref.Blindfold.NO -> trans.no.str(),
Pref.Blindfold.YES -> trans.yes.str())
}

View file

@ -14,7 +14,7 @@ trait TeamHelper {
def myTeam(teamId: String)(implicit ctx: Context): Boolean =
ctx.me.??(me => api.belongsTo(teamId, me.id))
def teamIds(userId: String): List[String] = api teamIds userId
def teamIds(userId: String): Set[String] = api teamIds userId
def teamIdToName(id: String): String = api teamName id getOrElse id

View file

@ -39,15 +39,14 @@ trait TournamentHelper { self: I18nHelper with DateHelper with UserHelper =>
def tournamentIdToName(id: String) = tournamentEnv.cached name id getOrElse "Tournament"
object scheduledTournamentNameShortHtml {
import lila.rating.PerfType._
private def icon(c: Char) = s"""<span data-icon="$c"></span>"""
private val replacements = List(
"Lichess " -> "",
"Bullet" -> icon(Bullet.iconChar),
"Blitz" -> icon(Blitz.iconChar),
"SuperBlitz" -> icon(Blitz.iconChar),
"Classical" -> icon(Classical.iconChar)
)
"Marathon" -> icon('\\'),
"SuperBlitz" -> icon(lila.rating.PerfType.Blitz.iconChar)
) ::: lila.rating.PerfType.leaderboardable.map { pt =>
pt.name -> icon(pt.iconChar)
}
def apply(name: String) = Html {
replacements.foldLeft(name) {
case (n, (from, to)) => n.replace(from, to)
@ -66,10 +65,10 @@ trait TournamentHelper { self: I18nHelper with DateHelper with UserHelper =>
private def longTournamentDescription(tour: Tournament) =
s"${tour.nbPlayers} players compete in the ${showEnglishDate(tour.startsAt)} ${tour.fullName}. " +
s"${tour.clock.show} ${tour.mode.name} games are played during ${tour.minutes} minutes. " +
tour.winnerId.fold("Winner is not yet decided.") { winnerId =>
s"${usernameOrId(winnerId)} takes the prize home!"
}
s"${tour.clock.show} ${tour.mode.name} games are played during ${tour.minutes} minutes. " +
tour.winnerId.fold("Winner is not yet decided.") { winnerId =>
s"${usernameOrId(winnerId)} takes the prize home!"
}
def tournamentOpenGraph(tour: Tournament) = lila.app.ui.OpenGraph(
title = s"${tour.fullName}: ${tour.variant.name} ${tour.clock.show} ${tour.mode.name} #${tour.id}",

View file

@ -33,14 +33,16 @@ trait UserHelper { self: I18nHelper with StringHelper with NumberHelper =>
PerfType.Correspondence,
PerfType.Antichess,
PerfType.Atomic,
PerfType.Horde)
PerfType.Horde,
PerfType.RacingKings,
PerfType.Crazyhouse)
private def best4Of(u: User, perfTypes: List[PerfType]) =
perfTypes.sortBy { pt => -u.perfs(pt).nb } take 4
def miniViewSortedPerfTypes(u: User): List[PerfType] =
best4Of(u, List(PerfType.Bullet, PerfType.Blitz, PerfType.Classical, PerfType.Correspondence)) :::
best4Of(u, List(PerfType.Chess960, PerfType.KingOfTheHill, PerfType.ThreeCheck, PerfType.Antichess, PerfType.Atomic, PerfType.Horde))
best4Of(u, List(PerfType.Crazyhouse, PerfType.Chess960, PerfType.KingOfTheHill, PerfType.ThreeCheck, PerfType.Antichess, PerfType.Atomic, PerfType.Horde, PerfType.RacingKings))
def showPerfRating(rating: Int, name: String, nb: Int, provisional: Boolean, icon: Char, klass: String)(implicit ctx: Context) = Html {
val title = s"$name rating over ${nb.localize} games"
@ -65,7 +67,7 @@ trait UserHelper { self: I18nHelper with StringHelper with NumberHelper =>
def showRatingDiff(diff: Int) = Html {
diff match {
case 0 => """<span class="rp null">+0</span>"""
case 0 => """<span class="rp null">±0</span>"""
case d if d > 0 => s"""<span class="rp up">+$d</span>"""
case d => s"""<span class="rp down">$d</span>"""
}
@ -190,11 +192,12 @@ trait UserHelper { self: I18nHelper with StringHelper with NumberHelper =>
userId: String,
rating: Option[Int],
cssClass: Option[String] = None,
withPowerTip: Boolean = true,
withTitle: Boolean = false,
withOnline: Boolean = true) = {
val user = lightUser(userId)
val name = user.fold(userId)(_.name)
val klass = userClass(userId, cssClass, withOnline)
val klass = userClass(userId, cssClass, withOnline, withPowerTip)
val href = userHref(name)
val content = rating.fold(name)(e => s"$name&nbsp;($e)")
val titleS = titleTag(user.flatMap(_.title) ifTrue withTitle)

View file

@ -16,7 +16,7 @@
case false => {<span class="is-red" data-icon="L"></span>}
}
</h1>
@form("email").value.map { email =>
@form("email").value.filter(_.nonEmpty).map { email =>
<p>You have already registered the email: @email</p>
}.getOrElse {
<p>@trans.emailIsOptional()</p>

View file

@ -38,7 +38,7 @@
</li>
<li>
<h2>@trans.blindfoldChess()</h2>
@base.radios(form("blindfold"), Seq(0 -> trans.no.str(), 1 -> trans.yes.str()))
@base.radios(form("blindfold"), translatedBlindfoldChoices)
</li>
</ul>
}
@ -100,6 +100,10 @@
<h2>Let other players message you</h2>
@base.radios(form("message"), translatedMessageChoices)
</li>
<li>
<h2>Share your insights data</h2>
@base.radios(form("insightShare"), lila.pref.Pref.InsightShare.choices)
</li>
</ul>
}
<p class="saved text none" data-icon="E">@trans.yourPreferencesHaveBeenSaved()</p>

View file

@ -10,6 +10,14 @@
This is a list of devices that have logged into your account.
Revoke any sessions that you do not recognize.
</p>
@if(sessions.length > 1){
<div class="explanation">
Alternatively you can
<form class="revoke-all" action="@routes.Account.signout("all")" method="POST">
<button type="submit" class="button hint--top thin confirm">revoke all sessions</button>
</form>.
</div>
}
<table class="slist">
@sessions.map { s =>
<tr>

View file

@ -1,6 +1,8 @@
@()(implicit ctx: Context)
@(id: String)(implicit ctx: Context)
<div class="future_game_analysis progress">
@trans.computerAnalysisInProgress()
<div class="loader"><span></span></div>
<div class="quote">
@base.quote(lila.quote.Quote.one(id))
</div>
</div>

View file

@ -61,7 +61,7 @@ atom = atom.some) {
<thead>
<tr>
<td>
<span class="is color-icon @color"></span>
<span class="is color-icon @color.name"></span>
</td>
<th>
@playerLink(pov.game.player(color), withOnline = false)
@ -76,7 +76,7 @@ atom = atom.some) {
</tr>
}
<tr>
<td><strong>@lila.analyse.Accuracy(pov.withColor(color), a)</strong></td>
<td><strong>@lila.analyse.Accuracy.mean(pov.withColor(color), a)</strong></td>
<th>@trans.averageCentipawnLoss()</th>
</tr>
<tr><td class="spacerlol" colspan=2></td></tr>
@ -98,7 +98,7 @@ atom = atom.some) {
data-max="@lila.analyse.AdvantageChart.max"
data-rows="@chart"></div>
}.getOrElse {
@analyse.computing()
@analyse.computing(pov.gameId)
}
}.getOrElse {
@if(analysis.isEmpty) {
@ -114,6 +114,9 @@ atom = atom.some) {
</div>
</div>
}
<div class="panel quote">
@base.quote(lila.quote.Quote.one(pov.gameId))
</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>
@ -160,6 +163,7 @@ atom = atom.some) {
}
}
<a data-panel="fen_pgn" class="fen_pgn">FEN &amp; PGN</a>
<a data-panel="quote" class="quote" data-icon="c" title="Irrelevant chess quote"></a>
</div>
</div>
}

View file

@ -82,7 +82,7 @@ chessground = true) {
</tr>
}
<tr>
<td><strong>@lila.analyse.Accuracy(pov.withColor(color), a)</strong></td>
<td><strong>@lila.analyse.Accuracy.mean(pov.withColor(color), a)</strong></td>
<th>Average centipawn loss</th>
</tr>
<tr><td class="spacerlol" colspan=2></td></tr>

View file

@ -5,7 +5,7 @@ title = "TOR exit node",
zen = true) {
<div class="content_box small_box signup">
<div class="signup_box">
<h1 class="lichess_title text" data-icon="2">Cannot login from your IP</h1>
<h1 class="lichess_title text" data-icon="2">Cannot log in from your IP</h1>
<p>
We have detected that you are using TOR to remain anonymous on the Internet.
<br />

View file

@ -44,7 +44,7 @@
<section>
<h2>@trans.play()</h2>
<a href="/?any#hook">@trans.createAGame()</a>
<a href="@routes.Tournament.home">@trans.tournament()</a>
<a href="@routes.Tournament.home()">@trans.tournament()</a>
<a href="@routes.Simul.home">@trans.simultaneousExhibitions()</a>
<a href="@routes.Tv.index">Lichess TV</a>
</section>
@ -78,6 +78,7 @@
<a href="/mobile">@trans.mobileApp()</a> ı
}
<a href="/blog">@trans.blog()</a> ı
<a href="/network">World map</a> ı
@NotForKids {
<a href="/developers">@trans.webmasters()</a> ı
<a href="/help-lichess">@trans.contribute()</a> ı
@ -87,10 +88,9 @@
<a href="/donate">@trans.donate()</a> ı
}
<a href="/contact">@trans.contact()</a> ı
<a href="@routes.Page.tos">@trans.termsOfService()</a> ı
<a href="@routes.Page.tos">@trans.termsOfService()</a>
@NotForKids {
<a href="https://github.com/ornicar/lila" target="_blank">@trans.sourceCode()</a> ı
ı <a href="https://github.com/ornicar/lila" target="_blank">@trans.sourceCode()</a>
}
<a href="/network" target="_blank" title="@trans.realTimeWorldMapOfChessMoves()">@trans.map()</a>
</div>
</div>

View file

@ -41,7 +41,7 @@ withLangAnnotations: Boolean = true)(body: Html)(implicit ctx: Context)
@atom.getOrElse {
<link href="@routes.Blog.atom()" type="application/atom+xml" rel="alternate" title="Latest blog posts" />
}
<link rel="mask-icon" href="@staticUrl("favicon.svg")" color="white">
<link rel="mask-icon" href="@staticUrl("favicon.svg")" color="black">
@if(withLangAnnotations){@langAnnotations}
@ctx.transpBgImg.map { img =>
<style type="text/css" id="bg-data">body.transp::before{background-image:url('@img');}</style>
@ -61,6 +61,7 @@ withLangAnnotations: Boolean = true)(body: Html)(implicit ctx: Context)
data-piece-set="@ctx.currentPieceSet"
data-sound-set="@ctx.currentSoundSet"
data-bg="@ctx.currentBg"
data-asset-url="@assetBaseUrl"
data-accept-languages="@acceptLanguages.mkString(",")">
<form id="blind_mode" action="@routes.Main.toggleBlindMode" method="POST">
<input type="hidden" name="enable" value="@ctx.blindMode.fold(0,1)" />
@ -85,6 +86,7 @@ withLangAnnotations: Boolean = true)(body: Html)(implicit ctx: Context)
<div class="auth fright">
@auth.userDropdown(me, playing)
</div>
@if(ctx.noKid) {
<div id="message_notifications_parent" class="message_notifications fright @if(ctx.nbMessages == 0) {none}">
<a id="message_notifications_tag" class="toggle link data-count" data-count="@ctx.nbMessages" data-href="@routes.Message.preview">
<span class="hint--bottom-left" data-hint="@trans.inbox()">
@ -96,6 +98,7 @@ withLangAnnotations: Boolean = true)(body: Html)(implicit ctx: Context)
<div class="title"><a href="@routes.Message.inbox(page=1)">@trans.inbox() »</a></div>
</div>
</div>
}
<div class="challenge_notifications fright">
<a id="challenge_notifications_tag" class="toggle link none data-count" data-count="0">
<span class="hint--bottom-left" data-hint="Challenges">
@ -175,7 +178,7 @@ withLangAnnotations: Boolean = true)(body: Html)(implicit ctx: Context)
<div class="content list"></div>
<div class="nobody">
<span>@trans.noFriendsOnline()</span>
<a class="find button" href="@routes.Relation.suggest(me.username)">
<a class="find button" href="@routes.User.opponents(me.username)">
<span class="is3 text" data-icon="h">@trans.findFriends()</span>
</a>
</div>

View file

@ -0,0 +1,6 @@
@(quote: lila.quote.Quote)
<blockquote class="pull-quote@if(quote.text.size > 330){ long}">
<p>@quote.text</p>
<footer>@quote.author</footer>
</blockquote>

View file

@ -5,7 +5,7 @@
<a href="/">@trans.play()</a>
<div>
<a href="/?any#hook">@trans.createAGame()</a>
<a href="@routes.Tournament.home">@trans.tournament()</a>
<a href="@routes.Tournament.home()">@trans.tournament()</a>
<a href="@routes.Simul.home">@trans.simultaneousExhibitions()</a>
</div>
</section>
@ -21,7 +21,7 @@
<a href="@routes.Tv.index">@trans.watch()</a>
<div>
<a href="@routes.Tv.index">Lichess TV</a>
<a href="@routes.Tv.games">Games in progress</a>
<a href="@routes.Tv.games">@trans.gamesBeingPlayedRightNow()</a>
<a href="@routes.Video.index">@trans.videoLibrary()</a>
</div>
</section>
@ -29,7 +29,7 @@
<a href="@routes.User.list">@trans.community()</a>
<div>
<a href="@routes.User.list">@trans.players()</a>
<a href="@routes.Stat.ratingDistribution("blitz")">Rating stats</a>
<a href="@routes.Stat.ratingDistribution("blitz")">@trans.ratingStats()</a>
@NotForKids {
<a href="@routes.Team.home()">@trans.teams()</a>
<a href="@routes.ForumCateg.index">@trans.forum()</a>

View file

@ -1,6 +0,0 @@
@(u: User)(implicit ctx: Context)
@coach.layout(u, title = s"${u.username} coach data is protected", withSide = false) {
You cannot see @userLink(u) coach data!
}

View file

@ -1,54 +0,0 @@
@(u: User, title: String, robots: Boolean = true, evenMoreJs: Html = Html(""), evenMoreCss: Html = Html(""), openGraph: Option[lila.app.ui.OpenGraph] = None, chessground: Boolean = true, withSide: Boolean = true)(body: Html)(implicit ctx: Context)
@moreCss = {
@cssTag("coach.css")
@evenMoreCss
}
@moreJs = {
@jsTag("coach.js")
@evenMoreJs
}
@sideSection = {
@if(withSide) {
<div id="coach_side">
<div class="navigation">
<a href="@routes.Coach.opening(u.username, "white")">Openings as White</a>
<a href="@routes.Coach.opening(u.username, "black")">Openings as Black</a>
@* <a href="@routes.Coach.move(u.username)">Moves</a> *@
</div>
@if(true) {
<div data-icon="E">This data is fresh</div>
}
@if(u.count.rated > 0) {
<form class="refresh" method="post" action="@routes.Coach.refresh(u.username)">
<button class="button" type="submit">Refresh @u.username stats</button>
</form>
}
<br />
<div class="refreshing none">
Hang on while we crunch data<br />
from @u.username @if(u.count.rated > 5000) {
@{5000.localize} last
} else {
@u.count.rated.localize
} rated games!
<br /><br />
It shouldn't take long.
<br /><br /><br />
<iframe src='http://en.lichess.org/tv/frame' class='lichess-tv-iframe' allowtransparency='true' frameBorder='0' style='width: 224px; height: 264px;' title='Lichess free online chess'>
</iframe>
</div>
</div>
}
}
@base.layout(
title = title,
side = sideSection.some,
moreCss = moreCss,
moreJs = moreJs,
robots = robots,
chessground = chessground,
openGraph = openGraph)(body)

View file

@ -1,21 +0,0 @@
@(u: User, nbPeriods: Int)(implicit ctx: Context)
@moreJs = {
@highchartsTag
@jsAt(s"compiled/lichess.coach.move${isProd??(".min")}.js")
@embedJs {
LichessCoachMove(document.getElementById('coach_move'), {
nbPeriods: @nbPeriods,
user: @Html(J.stringify(J.obj("id" -> u.id, "name" -> u.username)))
});
}
}
@coach.layout(u,
title = s"${u.username} moves",
evenMoreJs = moreJs,
evenMoreCss = cssTag("coachMove.css"),
chessground = false) {
<div id="coach_move" class="content_box no_padding coach_main"></div>
}

View file

@ -1,22 +0,0 @@
@(u: User, color: chess.Color, nbPeriods: Int)(implicit ctx: Context)
@moreJs = {
@highchartsTag
@jsAt(s"compiled/lichess.coach.opening${isProd??(".min")}.js")
@embedJs {
LichessCoachOpening(document.getElementById('coach_opening'), {
nbPeriods: @nbPeriods,
user: @Html(J.stringify(J.obj("id" -> u.id, "name" -> u.username))),
color: "@color.name"
});
}
}
@coach.layout(u,
title = s"${u.username} openings as ${color.name}",
evenMoreJs = moreJs,
evenMoreCss = cssTag("coachOpening.css"),
chessground = false) {
<div id="coach_opening" class="content_box no_padding coach_main"></div>
}

View file

@ -1,34 +0,0 @@
@(sections: lila.coach.GameSections)(implicit ctx: Context)
@sectionTable(section: lila.coach.GameSections.Section) = {
@if(section.nb != 0) {
<table>
<tbody>
<tr>
<th>Moves played</th>
<td>@section.moves.avg.map(_.localize)</td>
</tr>
<tr>
<th>Centipawn loss</th>
<td>@section.acpl.avg.fold("N/A")(_.localize.toString)</td>
</tr>
</tbody>
</table>
}
}
<tr>
<th>Overall</th>
<td>@sectionTable(sections.all)</td>
</tr>
<tr>
<th>Opening</th>
<td>@sectionTable(sections.opening)</td>
</tr>
<tr>
<th>Middlegame</th>
<td>@sectionTable(sections.middle)</td>
</tr>
<tr>
<th>Endgame</th>
<td>@sectionTable(sections.end)</td>
</tr>

View file

@ -1,39 +0,0 @@
@(r: lila.coach.PerfResults, title: Option[String])(implicit ctx: Context)
<table>
@title.map { t =>
<thead>
<tr>
<th colspan=2>@t</th>
</tr>
</thead>
}
<tbody>
@List("Win" -> r.outcomeStatuses.win, "Loss" -> r.outcomeStatuses.loss).map {
case (name, statuses) => {
<tr>
<th>@name statuses</th>
<td>
<table>
<tbody>
@statuses.sorted.map {
case (status, nb) => {
<tr>
<th>@status.name</th>
<td>@nb.localize (@(nb * 100 / statuses.sum)%)</td>
</tr>
}
}
</tbody>
</table>
</td>
</tr>
}
}
<tr>
<th>Best Rating</th>
<td>@r.bestRating.map { br =>
<strong>@br.rating</strong> after <a href="@routes.Round.watcher(br.id, "white")">playing</a> @userIdLink(br.userId.some)
}</td>
</tr>
</tbody>
</table>

View file

@ -1,53 +0,0 @@
@(r: lila.coach.Results, title: Option[String])(implicit ctx: Context)
<table>
@title.map { t =>
<thead>
<tr>
<th colspan=2>@t</th>
</tr>
</thead>
}
<tbody>
<tr>
<th>Games</th>
<td>@r.nbGames.localize</td>
</tr>
<tr>
<th>Analysed games</th>
<td>@r.nbAnalysis.localize (@((r.nbAnalysis * 100 / r.nbGames).localize)%)</td>
</tr>
<tr>
<th>Wins</th>
<td>@r.nbWin.localize (@((r.nbWin * 100 / r.nbGames).localize)%)</td>
</tr>
<tr>
<th>Losses</th>
<td>@r.nbLoss.localize (@((r.nbLoss * 100 / r.nbGames).localize)%)</td>
</tr>
<tr>
<th>Draws</th>
<td>@r.nbDraw.localize (@((r.nbDraw * 100 / r.nbGames).localize)%)</td>
</tr>
<tr>
<th>Rating diff</th>
<td>@r.ratingDiffAvg.map { rd =>
@showProgress(rd)
}</td>
</tr>
@gameSectionsTable(r.gameSections)
<tr>
<th>Best Win</th>
<td>@r.bestWin.map { bw =>
<a href="@routes.Round.watcher(bw.id, "white")">@userIdSpanMini(bw.userId, withOnline = true) <strong>@bw.rating</strong></a>
}</td>
</tr>
<tr>
<th>Opponent average rating</th>
<td>@r.opponentRatingAvg</td>
</tr>
<tr>
<th>Last played</th>
<td>@r.lastPlayed.map(momentFromNow)</td>
</tr>
</tbody>
</table>

View file

@ -1,4 +1,4 @@
@(doc: io.prismic.Document, resolver: io.prismic.DocumentLinkResolver, donations: List[lila.donation.Donation], top: List[lila.donation.Donation], progress: lila.donation.Progress)(implicit ctx: Context)
@(doc: io.prismic.Document, resolver: io.prismic.DocumentLinkResolver, donations: List[lila.donation.Donation], top: List[User.ID], progress: lila.donation.Progress)(implicit ctx: Context)
@payPalVars = {
<input type="hidden" name="cmd" value="_s-xclick">
@ -19,8 +19,8 @@
</div>
<h2>Top Donors</h2>
<div class="donors">
@top.map { donation =>
<div>@userIdLink(donation.userId)</div>
@top.map { userId =>
<div>@userIdLink(userId.some)</div>
}
</div>
<h2>Recent Donors</h2>
@ -56,7 +56,7 @@ description = "Your donations help pay for the web servers, show that lichess is
<! -- $5 -->
@payPalVars
<input type="hidden" name="hosted_button_id" value="JBSUYNPRCMT9U">
<strong>$5</strong> = <strong>9 hours</strong> of lichess costs:
<strong>$5</strong> = <strong>6 hours</strong> of lichess costs:
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
</form>
@ -64,7 +64,7 @@ description = "Your donations help pay for the web servers, show that lichess is
<! -- $10 -->
@payPalVars
<input type="hidden" name="hosted_button_id" value="ZZ96PGW3UNRPQ">
<strong>$10</strong> = <strong>18 hours</strong> of lichess costs:
<strong>$10</strong> = <strong>12 hours</strong> of lichess costs:
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
</form>
@ -72,7 +72,7 @@ description = "Your donations help pay for the web servers, show that lichess is
<! -- $20 -->
@payPalVars
<input type="hidden" name="hosted_button_id" value="VTKN4LNQZ2FNL">
<strong>$20</strong> = <strong>one day and a half</strong> of lichess costs:
<strong>$20</strong> = <strong>one day</strong> of lichess costs:
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
</form>
@ -80,7 +80,7 @@ description = "Your donations help pay for the web servers, show that lichess is
<! -- $50 -->
@payPalVars
<input type="hidden" name="hosted_button_id" value="XXYXZXY7GJQYJ">
<strong>$50</strong> = <strong>half a week</strong> of lichess costs:
<strong>$50</strong> = <strong>2.5 days</strong> of lichess costs:
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
</form>
@ -102,10 +102,10 @@ description = "Your donations help pay for the web servers, show that lichess is
<input type="hidden" name="hosted_button_id" value="36KH4D465UB9G">
<input type="hidden" name="on0" value="Monthly donation">Monthly donation
<select name="os0">
<option value="9 hours">9 hours: $5.00 USD - monthly</option>
<option value="18 hours">18 hours: $10.00 USD - monthly</option>
<option value="one day and a half">one day and a half: $20.00 USD - monthly</option>
<option value="half a week">half a week: $50.00 USD - monthly</option>
<option value="$5">6 hours: $5.00 USD - monthly</option>
<option value="$10">12 hours: $10.00 USD - monthly</option>
<option value="$20">one day: $20.00 USD - monthly</option>
<option value="$50">2.5 days: $50.00 USD - monthly</option>
</select>
<input type="hidden" name="currency_code" value="USD">
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_subscribe_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">

View file

@ -1,7 +1,7 @@
@(posts: List[lila.forum.MiniForumPost])(implicit ctx: Context)
@posts.map { p =>
<li>
<a @if(p.isTeam) { data-icon="f" } class="post_link" href="@routes.ForumPost.redirect(p.postId)"> @shorten(p.topicName, 30)</a>
<a @if(p.isTeam) { data-icon="f" } class="post_link" href="@routes.ForumPost.redirect(p.postId)" title="@Html(escapeEvenDoubleQuotes(p.topicName))"> @shorten(p.topicName, 30)</a>
@userIdLink(p.userId, withOnline = false)
<span class="extract">@shorten(p.text, 70)</span>
</li>

View file

@ -6,7 +6,7 @@
<li><h1>@categ.name</h1></li>
</ol>
<div class="warning">
<h1 data-icon="!">Important</h1>
<h1 data-icon="!"> Important</h1>
<p>
To report a user for cheating or bad behaviour,<br />
<strong><a href="@routes.Report.form">use the report form</a></strong>.

View file

@ -2,14 +2,8 @@
@url = {
@variant match {
case chess.variant.Standard => {https://en.wikipedia.org/wiki/Chess}
case chess.variant.Chess960 => {https://en.wikipedia.org/wiki/Chess960}
case chess.variant.KingOfTheHill => {@routes.Page.kingOfTheHill}
case chess.variant.ThreeCheck => {http://en.wikipedia.org/wiki/Three-check_chess}
case chess.variant.Antichess => {http://en.wikipedia.org/wiki/Losing_chess}
case chess.variant.FromPosition => {@routes.Editor.index?fen=@initialFen.map(_.replace(" ", "_"))}
case chess.variant.Atomic => {http://www.freechess.org/Help/HelpFiles/atomic.html}
case chess.variant.Horde => {http://en.wikipedia.org/wiki/Dunsany%27s_chess#Horde_variant}
case _ => {}
case v => {@routes.Page.variant(v.key)}
}
}
<a href="@url" rel="nofollow" target="_blank" @if(hintAsTitle){title}else{data-hint}="@variant.title" class="@cssClass">@name</a>
<a href="@url" rel="nofollow" target="_blank" @if(hintAsTitle){title}else{data-hint}="@variant.title" class="@cssClass variant-link">@name</a>

View file

@ -62,7 +62,7 @@ No need to submit a complete translation. You can just translate some sentences,
value="@form("comment").value"
name="@form("comment").name"
id="@form("comment").id"
placeholder="Briefly describe your changes, in english" />
placeholder="Briefly describe your changes, in English" />
@errMsg(form("comment"))
</div>
<br />

View file

@ -0,0 +1,16 @@
@(u: User)(implicit ctx: Context)
@insight.layout(u,
title = s"${u.username}'s chess insights",
moreJs = jsTag("insight-refresh.js")) {
<div class="content_box small_box">
<div class="head">
<h1 class="text" data-icon="7">@u.username chess insights</h1>
</div>
<br /><br /><br />
<p>@userLink(u) has no chess insights yet!</p>
<br /><br />
@refreshForm(u, s"Generate ${u.username}'s chess insights")
</div>
}

View file

@ -0,0 +1,16 @@
@(u: User)(implicit ctx: Context)
@site.message(
title = s"${u.username}'s chess insights are protected",
back = true,
icon = Html("7").some) {
Sorry, you cannot see @userLink(u)'s chess insights.
<br />
<br />
<br />
Maybe ask them to change their <a class="button" href="@routes.Pref.form("privacy")">privacy settings</a> ?
<br />
<br />
<br />
}

View file

@ -0,0 +1,52 @@
@(u: User, cache: lila.insight.UserCache, prefId: Int, ui: play.api.libs.json.JsObject, question: play.api.libs.json.JsObject, stale: Boolean)(implicit ctx: Context)
@moreJs = {
@highchartsLatestTag
@jsAt("vendor/multiple-select/multiple-select.js")
@jsAt(s"compiled/lichess.insight${isProd??(".min")}.js")
@jsTag("insight-refresh.js")
@jsTag("insight-tour.js")
@embedJs {
$(function() {
lichess = lichess || {};
lichess.insight = LichessInsight(document.getElementById('insight'), {
ui: @Html(toJson(ui)),
initialQuestion: @Html(toJson(question)),
i18n: {},
myUserId: @Html(ctx.userId.fold("null")(id => s""""$id"""")),
user: {
id: "@u.id",
name: "@u.username",
nbGames: @cache.count,
stale: @stale,
shareId: @prefId
},
pageUrl: "@routes.Insight.index(u.username)",
postUrl: "@routes.Insight.json(u.username)"
});
});
}
}
@moreCss = {
@cssTag("insight.css")
@cssVendorTag("multiple-select/multiple-select.css")
@ctx.currentBg match {
case "dark" => { @cssTag("insight.dark.css") }
case "transp" => { @cssTag("insight.dark.css")@cssTag("insight.transp.css") }
case _ => {}
}
}
@insight.layout(u,
title = s"${u.username}'s chess insights",
moreJs = moreJs,
moreCss = moreCss) {
<div id="insight"></div>
@if(stale) {
<div class="insight-stale none">
<p>There are new games to learn from!</p>
@refreshForm(u, "Update insights")
</div>
}
}

View file

@ -0,0 +1,8 @@
@(u: User, title: String, moreJs: Html = Html(""), moreCss: Html = Html(""), openGraph: Option[lila.app.ui.OpenGraph] = None, chessground: Boolean = true)(body: Html)(implicit ctx: Context)
@base.layout(
title = title,
moreCss = moreCss,
moreJs = moreJs,
chessground = chessground,
openGraph = openGraph)(body)

View file

@ -0,0 +1,12 @@
@(u: User)(implicit ctx: Context)
@site.message(
title = s"${u.username} has not played a rated game yet!",
back = true,
icon = Html("7").some) {
Before using chess insights, @userLink(u) has to play at least one rated game.
<br />
<br />
<br />
}

View file

@ -0,0 +1,18 @@
@(u: User, action: String)(implicit ctx: Context)
<form class="insight-refresh" method="post" action="@routes.Insight.refresh(u.username)">
<button data-icon="E" class="button text">@action</button>
<div class="crunching none">
<span class="square-spin"></span>
<br />
<strong>Now crunching data just for you!</strong>
<br />
<br />
<p>Would you like to watch<br />a live game while you wait?</p>
<br />
<script src="http://en.lichess.org/tv/embed?theme=brown&bg=light"></script>
<br />
<br />
<p>This can take a while,<br />maybe reload this page later!</p>
</div>
</form>

View file

@ -1,4 +1,4 @@
@(data: play.api.libs.json.JsObject, userTimeline: List[lila.timeline.Entry], forumRecent: List[lila.forum.MiniForumPost], tours: List[Tournament], simuls: List[lila.simul.Simul], featured: Option[Game], leaderboard: List[(User, lila.rating.PerfType)], tournamentWinners: List[lila.tournament.Winner], puzzle: Option[lila.puzzle.DailyPuzzle], streams: List[lila.tv.StreamOnAir], lastPost: List[lila.blog.MiniPost], playban: Option[lila.playban.TempBan], currentGame: Option[lila.app.mashup.Preload.CurrentGame], nbRounds: Int)(implicit ctx: Context)
@(data: play.api.libs.json.JsObject, userTimeline: List[lila.timeline.Entry], forumRecent: List[lila.forum.MiniForumPost], tours: List[Tournament], simuls: List[lila.simul.Simul], featured: Option[Game], leaderboard: List[User.LightPerf], tournamentWinners: List[lila.tournament.Winner], puzzle: Option[lila.puzzle.DailyPuzzle], streams: List[lila.tv.StreamOnAir], lastPost: List[lila.blog.MiniPost], playban: Option[lila.playban.TempBan], currentGame: Option[lila.app.mashup.Preload.CurrentGame], nbRounds: Int)(implicit ctx: Context)
@underchat = {
<div id="featured_game">
@ -62,6 +62,7 @@
@embedJs {
lichess = lichess || {};
lichess.lobby = {
perfIcons: @nonPuzzlePerfTypeNameIcons,
data: @Html(J.stringify(data)),
playban: @playban.fold(Html("null")){ pb =>
@Html(J.stringify(J.obj("minutes" -> pb.mins, "remainingSeconds" -> (pb.remainingSeconds + 3))))
@ -113,7 +114,7 @@ description = trans.freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrati
}
<div class="undertable">
<div class="undertable_top">
<a class="more hint--bottom" data-hint="@trans.seeAllTournaments()" href="@routes.Tournament.home">@trans.more() »</a>
<a class="more hint--bottom" data-hint="@trans.seeAllTournaments()" href="@routes.Tournament.home()">@trans.more() »</a>
<span class="title text" data-icon="g">@trans.openTournaments()</span>
</div>
<div id="enterable_tournaments" class="enterable_list undertable_inner scroll-shadow-hard">
@ -139,15 +140,15 @@ description = trans.freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrati
<div class="undertable_inner scroll-shadow-hard">
<table>
<tbody>
@leaderboard.map {
case (u, pt) => {
@leaderboard.map { l =>
<tr>
<td>@userLink(u)</td>
<td>@showPerfRating(u, pt, klass = "title")</td>
<td>@showProgress(u.perfs(pt).progress, withTitle = false)</td>
<td>@lightUserLink(l.user)</td>
@lila.rating.PerfType(l.perfKey).map { pt =>
<td data-icon="@pt.iconChar">@l.rating</td>
}
<td>@showProgress(l.progress, withTitle = false)</td>
</tr>
}
}
</tbody>
</table>
</div>

View file

@ -19,7 +19,7 @@
block them using the block button on their profile page.</p>
<h2>What now?</h2>
<p>Just wait until you can create/join games again!</p>
<p>Maybe <a href="@routes.Tournament.home">join a tournament</a>,<br />
<p>Maybe <a href="@routes.Tournament.home()">join a tournament</a>,<br />
or play some <a href="@routes.Puzzle.home">training puzzles</a>,<br />
or watch <a href="@routes.Video.index">chess videos</a>!
</p>

View file

@ -0,0 +1,26 @@
@(champ: Option[lila.mod.Gamify.ModMixed], img: String, period: lila.mod.Gamify.Period)(implicit ctx: Context)
<div class="champ">
<img src="@staticUrl(s"images/mod/$img.png")" />
<h2>Mod of the @period.name</h2>
@champ.map { m =>
<h3>@userIdLink(m.modId.some, withOnline = false)</h3>
<table>
<tbody>
<tr>
<th>Total score</th>
<td>@m.score</td>
</tr>
<tr>
<th>Actions taken</th>
<td>@m.action</td>
</tr>
<tr>
<th>Reports closed</th>
<td>@m.report</td>
</tr>
</tbody>
</table>
}.getOrElse { Nobody! }
<a class="current button" href="@routes.Mod.gamifyPeriod(period.name)">View @period.name leaderboard</a>
</div>

View file

@ -0,0 +1,57 @@
@(leaderboards: lila.mod.Gamify.Leaderboards, history: List[lila.mod.Gamify.HistoryMonth])(implicit ctx: Context)
@import lila.mod.Gamify.Period
@title = @{ "Moderator hall of fame" }
@yearHeader(year: Int) = @{
Html(s"""<tr class="year">
<th>$year</th>
<th>Champions of the past</th>
<th>Score</th>
<th>Actions taken</th>
<th>Reports closed</th>
</tr>""")
}
@mod.layout(
title = title,
active = "gamify",
moreCss = cssTag("mod-gamify.css")) {
<div id="mod-gamify" class="content_box no_padding index">
<h1>@title</h1>
<div class="champs clearfix">
<div class="third">
@champion(leaderboards.daily.headOption, "reward1", Period.Day)
</div>
<div class="third">
@champion(leaderboards.weekly.headOption, "reward2", Period.Week)
</div>
<div class="third">
@champion(leaderboards.monthly.headOption, "reward3", Period.Month)
</div>
</div>
<div class="history">
<table class="slist">
<tbody>
@history.headOption.filterNot(_.date.getMonthOfYear == 12).map { h =>
@yearHeader(h.date.getYear)
}
@history.map { h =>
@if(h.date.getMonthOfYear == 12) {
@yearHeader(h.date.getYear)
}
<tr>
<th>@h.date.monthOfYear.getAsText</th>
<th>@userIdLink(h.champion.modId.some, withOnline = false)</th>
<td class="score">@h.champion.score.localize</td>
<td>@h.champion.action.localize</td>
<td>@h.champion.report.localize</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}

View file

@ -0,0 +1,40 @@
@(leaderboards: lila.mod.Gamify.Leaderboards, period: lila.mod.Gamify.Period)(implicit ctx: Context)
@title = @{ s"Moderators of the ${period.name}" }
@mod.layout(
title = title,
active = "gamify",
moreCss = cssTag("mod-gamify.css")) {
<div id="mod-gamify" class="content_box no_padding">
<h1>
<a href="@routes.Mod.gamify" data-icon="I" class="text">@title</a>
</h1>
<div class="period">
<table class="slist">
<thead>
<tr>
<th colspan="2"></th>
<th>Actions</th>
<th>Reports</th>
<th>Score</th>
</tr>
</thead>
<tbody>
@leaderboards(period).zipWithIndex.map {
case (m, i) => {
<tr>
<th>@(i + 1)</th>
<th>@userIdLink(m.modId.some, withOnline = false)</th>
<td>@m.action.localize</td>
<td>@m.report.localize</td>
<td class="score">@m.score.localize</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
}

View file

@ -4,6 +4,12 @@
@if(isGranted(_.SeeReport)) {
<a class="@active.active("report")" href="@routes.Report.list">Reports</a>
}
@if(isGranted(_.SeeReport)) {
<a class="@active.active("gamify")" href="@routes.Mod.gamify">Hall of fame</a>
}
@if(isGranted(_.UserSearch)) {
<a class="@active.active("search")" href="@routes.Mod.search">Search users</a>
}
@if(isGranted(_.StreamConfig)) {
<a class="@active.active("stream")" href="@routes.Tv.streamConfig">Streams</a>
}

View file

@ -0,0 +1,58 @@
@(query: String, users: List[User])(implicit ctx: Context)
@title = @{ "Search users" }
@layout(
title = title,
active = "search") {
<style type="text/css">
#mod-search form {
margin: 30px 0;
text-align: center;
}
#mod-search form input {
padding: 15px 25px;
font-size: 1.2em;
width: 400px;
margin: auto;
position: relative;
}
</style>
<div id="mod-search" class="content_box">
<h1 data-icon="y" class="text">@title</h1>
<form class="search" action="@routes.Mod.search" method="GET">
<input name="q" placeholder="Search by IP, email, or username" value="@query" />
</form>
@if(users.nonEmpty) {
<table class="slist">
<thead>
<tr>
<th>User</th>
<th>Games</th>
<th>Marks</th>
<th>IPban</th>
<th>Closed</th>
<th>Created</th>
</tr>
</thead>
<tbody>
@users.map { u =>
<tr>
<td>@userLink(u, withBestRating = true, params = "?mod")</td>
<td>@u.count.game.localize</td>
<td>
@if(u.engine){ENGINE}
@if(u.booster){BOOSTER}
@if(u.troll){TROLL}
</td>
<td>@if(u.ipBan){IPBAN}</td>
<td>@if(u.disabled){CLOSED}</td>
<td>@momentFromNow(u.createdAt)</td>
</tr>
}
</tbody></table>
}
</div>
}

View file

@ -49,6 +49,6 @@ url = s"$netBaseUrl${routes.Opening.show(opening.id).url}",
description = s"Opening training #${opening.id}: " + opening.color.fold(
trans.findTheBestMoveForWhite,
trans.findTheBestMoveForBlack
).str() + s". Played by ${opening.attempts} players.").some) {
).str() + s" Played by ${opening.attempts} players.").some) {
<div class="round cg-512">@miniBoardContent</div>
}

View file

@ -64,6 +64,6 @@ url = s"$netBaseUrl${routes.Puzzle.show(puzzle.id).url}",
description = s"Tactic puzzle #${puzzle.id}: " + puzzle.color.fold(
trans.findTheBestMoveForWhite,
trans.findTheBestMoveForBlack
).str() + s". Played by ${puzzle.attempts} players.").some) {
).str() + s" Played by ${puzzle.attempts} players.").some) {
<div class="round cg-512">@miniBoardContent</div>
}

View file

@ -3,7 +3,9 @@
@sideSection = {
<div class="side">
@searchForm()
@NotForKids {
<a class="ask" href="@routes.QaQuestion.ask">Ask a question</a>
}
@side
</div>
}

View file

@ -1,12 +1,12 @@
@(u: User, users: List[lila.relation.Related])(implicit ctx: Context)
@(u: User, pag: Paginator[lila.relation.Related])(implicit ctx: Context)
@user.layout(title = u.username + " - " + trans.blocks(users.size)) {
@user.layout(title = u.username + " - " + trans.blocks(pag.nbResults)) {
<div class="content_box no_padding">
<h1>
@userLink(u, withOnline = false)
@trans.blocks(users.size)
@trans.blocks(pag.nbResults)
</h1>
@user.simpleTable(users)
@user.simpleTable(pag, routes.Relation.blocks())
</div>
}

View file

@ -1,13 +1,13 @@
@(u: User, users: List[lila.relation.Related], following: Int)(implicit ctx: Context)
@(u: User, pag: Paginator[lila.relation.Related], nbFollowing: Int)(implicit ctx: Context)
@user.layout(title = u.username + " - " + trans.nbFollowers(users.size)) {
@user.layout(title = u.username + " - " + trans.nbFollowers(pag.nbResults)) {
<div class="content_box no_padding">
<h1>
@userLink(u, withOnline = false)
@trans.nbFollowers(users.size)
@trans.nbFollowers(pag.nbResults)
&amp;
<a href="@routes.Relation.following(u.username)">@trans.nbFollowing(following)</a>
<a href="@routes.Relation.following(u.username)">@trans.nbFollowing(nbFollowing)</a>
</h1>
@user.simpleTable(users)
@user.simpleTable(pag, routes.Relation.followers(u.username))
</div>
}

View file

@ -1,13 +1,13 @@
@(u: User, users: List[lila.relation.Related], followers: Int)(implicit ctx: Context)
@(u: User, pag: Paginator[lila.relation.Related], nbFollowers: Int)(implicit ctx: Context)
@user.layout(title = u.username + " - " + trans.nbFollowing(users.size)) {
@user.layout(title = u.username + " - " + trans.nbFollowing(pag.nbResults)) {
<div class="content_box no_padding">
<h1>
@userLink(u, withOnline = false)
@trans.nbFollowing(users.size)
@trans.nbFollowing(pag.nbResults)
&amp;
<a href="@routes.Relation.followers(u.username)">@trans.nbFollowers(followers)</a>
<a href="@routes.Relation.followers(u.username)">@trans.nbFollowers(nbFollowers)</a>
</h1>
@user.simpleTable(users)
@user.simpleTable(pag, routes.Relation.following(u.username))
</div>
}

View file

@ -1,11 +0,0 @@
@(u: User, sugs: List[lila.relation.Related])(implicit ctx: Context)
@title = @{ "%s - %s".format(u.username, trans.findFriends()) }
@user.layout(title = title) {
<div class="content_box no_padding">
<h1>@userLink(u, withOnline = false) @trans.findFriends()</h1>
@user.relatedTable(u, sugs)
</div>
}

View file

@ -45,7 +45,6 @@
@errMsg(form("text"))
</div>
@base.captcha(form("move"), form("gameId"), captcha)
@errMsg(form)
<div class="actions">
<input class="send button" type="submit" value="@trans.send()" />
<a class="cancel" href="@routes.Lobby.home()">@trans.cancel()</a>

View file

@ -1,16 +1,39 @@
@()(implicit ctx: Context)
@(userId: String, blocked: Boolean)(implicit ctx: Context)
@title = @{ "Thanks for the report" }
@site.layout(title = title, moreCss = cssTag("report.css")) {
@moreJs = {
@embedJs {
$('button.report-block').one('click', function() {
var $button = $(this);
$button.find('span').text('Blocking...');
$.ajax({
url:$button.data('action'),
method:'post',
success: function() {
$button.find('span').text('Blocked!');
}
});
});
}
}
@site.layout(title = title, moreCss = cssTag("report.css"), moreJs = moreJs) {
<div class="content_box small_box">
<h1 class="lichess_title">@title</h1>
<p>The moderators will review it very soon, and take appropriate action.</p>
<br /><br /><br />
@if(!blocked) {
In the mean time, you can block this user:
<button data-action="@routes.Relation.block(userId)" class="report-block icon button hint--top inline" type="submit" data-hint="@trans.block()">
<span class="text" data-icon="k">Block @usernameOrId(userId)</span>
</button>
<br /><br /><br />
}
<p>
<br />
<br />
<a href="@routes.Lobby.home">Return to lichess homepage</a>
<a href="@routes.Lobby.home">Return to lichess homepage</a>
</p>
</div>
}

View file

@ -4,8 +4,10 @@
@game.players.map { p =>
@if(game.playerBlurPercent(p.color) > 30) {
<br />
<span class="mod blurs">
@playerLink(p, cssClass = s"is color-icon ${p.color.name}".some, withOnline = false, mod = true) @p.blurs/@game.playerMoves(p.color) blurs
<strong>@game.playerBlurPercent(p.color)%</strong>
</span>
}
}
}

View file

@ -4,8 +4,10 @@
@game.players.map { p =>
@p.holdAlert.map { h =>
<br />
<span class="mod hold">
@playerLink(p, cssClass = s"is color-icon ${p.color.name}".some, mod = true, withOnline = false) hold alert<br />
(ply: @h.ply, mean: @h.mean ms, SD: @h.sd)<br />
(ply: @h.ply, mean: @h.mean ms, SD: @h.sd)
</span><br />
}
}
}

View file

@ -47,7 +47,7 @@
</div>
<div>
<h2>@trans.blindfoldChess()</h2>
@base.radios(form("blindfold"), lila.pref.Pref.Blindfold.choices, container = true, prefix = "igp_")
@base.radios(form("blindfold"), translatedBlindfoldChoices, container = true, prefix = "igp_")
</div>
</form>
<a target="_blank" class="prefs button text" data-icon="%" href="@routes.Pref.form("game-display")">@trans.preferences()</a>

View file

@ -55,6 +55,7 @@ side = Html("")) {
@trans.mode(): @modeName(c.mode)
</p>
}
@base.quote(lila.quote.Quote.one(pov.gameId))
</div>
</div>
}

View file

@ -8,10 +8,7 @@ title = trans.hostANewSimul.str()) {
<div class="content_box small_box simul_box">
<h1>@trans.hostANewSimul()</h1>
<form class="plain create content_box_content" action="@routes.Simul.create" method="POST">
<p class="help">
@trans.whenCreateSimul()<br />
<a href="@routes.Simul.home">@trans.joinExistingSimul()</a>
</p>
<p class="help">@trans.whenCreateSimul()</p>
@form.globalError.map { error =>
<p class="error">@error.message</p>
}

View file

@ -21,9 +21,15 @@
</div>
}
@title = @{ trans.simultaneousExhibitions.str() }
@simul.layout(
title = trans.simultaneousExhibitions.str(),
side = side.some) {
title = title,
side = side.some,
openGraph = lila.app.ui.OpenGraph(
title = title,
url = s"$netBaseUrl${routes.Simul.home}",
description = trans.aboutSimul.str()).some) {
<div id="simul_list" data-href="@routes.Simul.homeReload()">
@simul.homeInner(opens, starteds, finisheds)
</div>

View file

@ -1,4 +1,4 @@
@(title: String, moreJs: Html = Html(""), side: Option[Html] = None, chat: Option[Html] = None, underchat: Option[Html] = None, chessground: Boolean = true)(body: Html)(implicit ctx: Context)
@(title: String, moreJs: Html = Html(""), side: Option[Html] = None, chat: Option[Html] = None, underchat: Option[Html] = None, chessground: Boolean = true, openGraph: Option[lila.app.ui.OpenGraph] = None)(body: Html)(implicit ctx: Context)
@moreCss = {
@cssTag("simul.css")
@ -11,6 +11,7 @@ moreCss = moreCss,
side = side,
chat = chat,
underchat = underchat,
chessground = chessground) {
chessground = chessground,
openGraph = openGraph) {
@body
}

Some files were not shown because too many files have changed in this diff Show more