Merge branch 'master' into messagingAPI
This commit is contained in:
commit
5f20bc0b66
14
.gitmodules
vendored
14
.gitmodules
vendored
|
@ -31,9 +31,6 @@
|
|||
[submodule "public/vendor/stockfish.js"]
|
||||
path = public/vendor/stockfish.js
|
||||
url = https://github.com/niklasf/stockfish.js
|
||||
[submodule "public/vendor/Sunsetter"]
|
||||
path = public/vendor/Sunsetter
|
||||
url = https://github.com/niklasf/Sunsetter.git
|
||||
[submodule "public/vendor/flatpickr"]
|
||||
path = public/vendor/flatpickr
|
||||
url = https://github.com/chmln/flatpickr
|
||||
|
@ -43,3 +40,14 @@
|
|||
[submodule "public/visualizer"]
|
||||
path = public/visualizer
|
||||
url = https://github.com/ornicar/chess_game_visualizer
|
||||
[submodule "public/vendor/Sunsetter"]
|
||||
path = public/vendor/Sunsetter
|
||||
url = https://github.com/niklasf/Sunsetter
|
||||
branch = js
|
||||
[submodule "public/vendor/stockfish.pexe"]
|
||||
path = public/vendor/stockfish.pexe
|
||||
url = https://github.com/niklasf/stockfish.pexe
|
||||
branch = ddugovic
|
||||
[submodule "public/vendor/jquery-textcomplete"]
|
||||
path = public/vendor/jquery-textcomplete
|
||||
url = https://github.com/yuku-t/jquery-textcomplete
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
sudo: false
|
||||
language: scala
|
||||
|
||||
# https://docs.travis-ci.com/user/notifications/#IRC-notification
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,5 +1,3 @@
|
|||
Copyright (c) 2012-2015 Thibault Duplessis
|
||||
|
||||
The MIT license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -48,7 +48,10 @@ drop us an email at contact@lichess.org and we'll discuss it.
|
|||
|
||||
### API Limits
|
||||
|
||||
To respect the API servers and avoid an IP ban, please wait 1 second between requests. If you receive an HTTP response with a [429 status](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#429), please wait a full minute before resuming API usage.
|
||||
To respect the API servers and avoid an IP ban, please wait 1 second between requests.
|
||||
If you receive an HTTP response with a [429 status](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#429), please wait a full minute before resuming API usage.
|
||||
|
||||
Please do not automate computer analysis requests. They're very expensive.
|
||||
|
||||
### `GET /api/user/<username>` fetch one user
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ final class Env(
|
|||
val CliUsername = config getString "cli.username"
|
||||
|
||||
private val RendererName = config getString "app.renderer.name"
|
||||
private val WebPath = config getString "app.web_path"
|
||||
|
||||
lazy val bus = lila.common.Bus(system)
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ object Global extends GlobalSettings {
|
|||
|
||||
override def onRouteRequest(req: RequestHeader): Option[Handler] = {
|
||||
lila.mon.http.request.all()
|
||||
if (req.remoteAddress contains ":") lila.mon.http.request.ipv6()
|
||||
Env.i18n.requestHandler(req) orElse super.onRouteRequest(req)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import play.api.mvc._, Results._
|
|||
import lila.api.Context
|
||||
import lila.app._
|
||||
import lila.common.LilaCookie
|
||||
import lila.common.PimpedJson._
|
||||
import lila.security.Permission
|
||||
import lila.user.{ User => UserModel, UserRepo }
|
||||
import views._
|
||||
|
@ -51,8 +52,10 @@ object Account extends LilaController {
|
|||
"nowPlaying" -> JsArray(povs take 20 map Env.api.lobbyApi.nowPlaying),
|
||||
"nbFollowing" -> nbFollowing,
|
||||
"nbFollowers" -> nbFollowers,
|
||||
"kid" -> me.kid,
|
||||
"nbChallenges" -> nbChallenges)
|
||||
"kid" -> me.kid.option(true),
|
||||
"troll" -> me.troll.option(true),
|
||||
"nbChallenges" -> nbChallenges
|
||||
).noNull
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,32 @@
|
|||
package controllers
|
||||
|
||||
import scala.util.{ Success, Failure }
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import akka.pattern.ask
|
||||
import play.api.http.ContentTypes
|
||||
import play.api.mvc._
|
||||
import play.twirl.api.Html
|
||||
|
||||
import lila.api.Context
|
||||
import lila.app._
|
||||
import lila.common.HTTPRequest
|
||||
import lila.evaluation.PlayerAssessments
|
||||
import lila.game.{ Pov, Game => GameModel, GameRepo, PgnDump }
|
||||
import lila.hub.actorApi.map.Tell
|
||||
import lila.game.{ Pov, GameRepo }
|
||||
import views._
|
||||
|
||||
import chess.Color
|
||||
|
||||
object Analyse extends LilaController {
|
||||
|
||||
private def env = Env.analyse
|
||||
private def bookmarkApi = Env.bookmark.api
|
||||
private val divider = Env.game.divider
|
||||
|
||||
def requestAnalysis(id: String) = Auth { implicit ctx =>
|
||||
me =>
|
||||
OptionFuResult(GameRepo game id) { game =>
|
||||
Env.fishnet.analyser(game, lila.fishnet.Work.Sender(
|
||||
userId = me.id.some,
|
||||
ip = HTTPRequest.lastRemoteAddress(ctx.req).some,
|
||||
mod = isGranted(_.Hunter),
|
||||
system = false)) map {
|
||||
case true => Ok
|
||||
case false => Unauthorized
|
||||
}
|
||||
def requestAnalysis(id: String) = Auth { implicit ctx => me =>
|
||||
OptionFuResult(GameRepo game id) { game =>
|
||||
Env.fishnet.analyser(game, lila.fishnet.Work.Sender(
|
||||
userId = me.id.some,
|
||||
ip = HTTPRequest.lastRemoteAddress(ctx.req).some,
|
||||
mod = isGranted(_.Hunter),
|
||||
system = false)) map {
|
||||
case true => Ok
|
||||
case false => Unauthorized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def replay(pov: Pov, userTv: Option[lila.user.User])(implicit ctx: Context) =
|
||||
|
@ -56,19 +48,19 @@ object Analyse extends LilaController {
|
|||
withMoveTimes = true,
|
||||
withDivision = true,
|
||||
withOpening = true) map { data =>
|
||||
Ok(html.analyse.replay(
|
||||
pov,
|
||||
data,
|
||||
initialFen,
|
||||
Env.analyse.annotator(pgn, analysis, pov.game.opening, pov.game.winnerColor, pov.game.status, pov.game.clock).toString,
|
||||
analysis,
|
||||
analysisInProgress,
|
||||
simul,
|
||||
crosstable,
|
||||
userTv,
|
||||
chat,
|
||||
bookmarked = bookmarked))
|
||||
}
|
||||
Ok(html.analyse.replay(
|
||||
pov,
|
||||
data,
|
||||
initialFen,
|
||||
Env.analyse.annotator(pgn, analysis, pov.game.opening, pov.game.winnerColor, pov.game.status, pov.game.clock).toString,
|
||||
analysis,
|
||||
analysisInProgress,
|
||||
simul,
|
||||
crosstable,
|
||||
userTv,
|
||||
chat,
|
||||
bookmarked = bookmarked))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,6 +96,10 @@ object Auth extends LilaController {
|
|||
}
|
||||
}
|
||||
|
||||
private def mustConfirmEmailByIP(ip: String): Fu[Boolean] =
|
||||
api.recentByIpExists(ip) >>|
|
||||
Mod.ipIntelCache(ip).map(80 <).recover { case _: Exception => false }
|
||||
|
||||
def signupPost = OpenBody { implicit ctx =>
|
||||
implicit val req = ctx.body
|
||||
NoTor {
|
||||
|
@ -106,7 +110,7 @@ object Auth extends LilaController {
|
|||
data => env.recaptcha.verify(~data.recaptchaResponse, req).flatMap {
|
||||
case false => BadRequest(html.auth.signup(forms.signup.website fill data, env.RecaptchaPublicKey)).fuccess
|
||||
case true =>
|
||||
api.recentByIpExists(HTTPRequest lastRemoteAddress ctx.req) flatMap { mustConfirmEmail =>
|
||||
mustConfirmEmailByIP(HTTPRequest lastRemoteAddress ctx.req) flatMap { mustConfirmEmail =>
|
||||
lila.mon.user.register.website()
|
||||
lila.mon.user.register.mustConfirmEmail(mustConfirmEmail)()
|
||||
val email = env.emailAddress.validate(data.email) err s"Invalid email ${data.email}"
|
||||
|
|
|
@ -14,22 +14,17 @@ object Coach extends LilaController {
|
|||
|
||||
def allDefault(page: Int) = all(CoachPager.Order.Login.key, page)
|
||||
|
||||
private val canViewCoaches = (u: UserModel) =>
|
||||
isGranted(_.Admin, u) || isGranted(_.Coach, u) || isGranted(_.PreviewCoach, u)
|
||||
|
||||
def all(o: String, page: Int) = SecureF(canViewCoaches) { implicit ctx =>
|
||||
me =>
|
||||
val order = CoachPager.Order(o)
|
||||
Env.coach.pager(order, page) map { pager =>
|
||||
Ok(html.coach.index(pager, order))
|
||||
}
|
||||
def all(o: String, page: Int) = Open { implicit ctx =>
|
||||
val order = CoachPager.Order(o)
|
||||
Env.coach.pager(order, page) map { pager =>
|
||||
Ok(html.coach.index(pager, order))
|
||||
}
|
||||
}
|
||||
|
||||
def show(username: String) = SecureF(canViewCoaches) { implicit ctx =>
|
||||
me =>
|
||||
def show(username: String) = Open { implicit ctx =>
|
||||
OptionFuResult(api find username) { c =>
|
||||
WithVisibleCoach(c) {
|
||||
Env.study.api.byIds {
|
||||
Env.study.api.publicByIds {
|
||||
c.coach.profile.studyIds.map(_.value)
|
||||
} flatMap Env.study.pager.withChaptersAndLiking(ctx.me) flatMap { studies =>
|
||||
api.reviews.approvedByCoach(c.coach) flatMap { reviews =>
|
||||
|
@ -68,7 +63,7 @@ object Coach extends LilaController {
|
|||
}
|
||||
|
||||
private def WithVisibleCoach(c: CoachModel.WithUser)(f: Fu[Result])(implicit ctx: Context) =
|
||||
if ((c.coach.isListed || ctx.me.??(c.coach.is) || isGranted(_.Admin)) && ctx.me.??(canViewCoaches)) f
|
||||
if (c.coach.isListed || ctx.me.??(c.coach.is) || isGranted(_.Admin)) f
|
||||
else notFound
|
||||
|
||||
def edit = Secure(_.Coach) { implicit ctx =>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package controllers
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.HTTPRequest
|
||||
import lila.app._
|
||||
import views._
|
||||
|
@ -53,6 +52,18 @@ object ForumPost extends LilaController with ForumController {
|
|||
}
|
||||
}
|
||||
|
||||
def edit(postId: String) = AuthBody { implicit ctx =>
|
||||
me =>
|
||||
implicit val req = ctx.body
|
||||
|
||||
forms.postEdit.bindFromRequest.fold(err => Redirect(routes.ForumPost.redirect(postId)).fuccess,
|
||||
data =>
|
||||
postApi.editPost(postId, data.changes, me).map { post =>
|
||||
Redirect(routes.ForumPost.redirect(post.id))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def delete(categSlug: String, id: String) = Auth { implicit ctx =>
|
||||
me =>
|
||||
CategGrantMod(categSlug) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import scala.concurrent.duration._
|
|||
import lila.common.HTTPRequest
|
||||
import lila.app._
|
||||
import lila.forum.CategRepo
|
||||
import play.api.libs.json._
|
||||
import views._
|
||||
|
||||
object ForumTopic extends LilaController with ForumController {
|
||||
|
@ -73,4 +74,14 @@ object ForumTopic extends LilaController with ForumController {
|
|||
routes.ForumTopic.show(categSlug, slug, pag.nbPages)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of the usernames of people participating in a forum topic conversation
|
||||
*/
|
||||
def participants(topicId: String) = Auth { implicit ctx => me =>
|
||||
postApi.userIds(topicId) map { ids =>
|
||||
val usernames = Env.user.lightUserApi.getList(ids.sorted).map(_.titleName)
|
||||
Ok(Json.toJson(usernames))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,9 +9,11 @@ object Importer extends LilaController {
|
|||
|
||||
private def env = Env.importer
|
||||
|
||||
def importGame = Open { implicit ctx =>
|
||||
def importGame = OpenBody { implicit ctx =>
|
||||
fuccess {
|
||||
Ok(html.game.importGame(env.forms.importForm))
|
||||
val pgn = ctx.body.queryString.get("pgn").flatMap(_.headOption).getOrElse("")
|
||||
val data = lila.importer.ImportData(pgn, None)
|
||||
Ok(html.game.importGame(env.forms.importForm.fill(data)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,16 +50,26 @@ private[controllers] trait LilaController
|
|||
)
|
||||
|
||||
protected def Socket[A: FrameFormatter](f: Context => Fu[(Iteratee[A, _], Enumerator[A])]) =
|
||||
WebSocket.tryAccept[A] { req => reqToCtx(req) flatMap f map scala.util.Right.apply }
|
||||
WebSocket.tryAccept[A] { req =>
|
||||
SocketCSRF(req) {
|
||||
reqToCtx(req) flatMap f map scala.util.Right.apply
|
||||
}
|
||||
}
|
||||
|
||||
protected def SocketEither[A: FrameFormatter](f: Context => Fu[Either[Result, (Iteratee[A, _], Enumerator[A])]]) =
|
||||
WebSocket.tryAccept[A] { req => reqToCtx(req) flatMap f }
|
||||
WebSocket.tryAccept[A] { req =>
|
||||
SocketCSRF(req) {
|
||||
reqToCtx(req) flatMap f
|
||||
}
|
||||
}
|
||||
|
||||
protected def SocketOption[A: FrameFormatter](f: Context => Fu[Option[(Iteratee[A, _], Enumerator[A])]]) =
|
||||
WebSocket.tryAccept[A] { req =>
|
||||
reqToCtx(req) flatMap f map {
|
||||
case None => Left(NotFound(jsonError("socket resource not found")))
|
||||
case Some(pair) => Right(pair)
|
||||
SocketCSRF(req) {
|
||||
reqToCtx(req) flatMap f map {
|
||||
case None => Left(NotFound(jsonError("socket resource not found")))
|
||||
case Some(pair) => Right(pair)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,8 +86,10 @@ private[controllers] trait LilaController
|
|||
|
||||
protected def Open[A](p: BodyParser[A])(f: Context => Fu[Result]): Action[A] =
|
||||
Action.async(p) { req =>
|
||||
reqToCtx(req) flatMap { ctx =>
|
||||
Env.i18n.requestHandler.forUser(req, ctx.me).fold(f(ctx))(fuccess)
|
||||
CSRF(req) {
|
||||
reqToCtx(req) flatMap { ctx =>
|
||||
Env.i18n.requestHandler.forUser(req, ctx.me).fold(f(ctx))(fuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,19 +97,22 @@ private[controllers] trait LilaController
|
|||
OpenBody(BodyParsers.parse.anyContent)(f)
|
||||
|
||||
protected def OpenBody[A](p: BodyParser[A])(f: BodyContext[A] => Fu[Result]): Action[A] =
|
||||
Action.async(p)(req => reqToCtx(req) flatMap f)
|
||||
|
||||
protected def OpenNoCtx(f: RequestHeader => Fu[Result]): Action[AnyContent] =
|
||||
Action.async(f)
|
||||
Action.async(p) { req =>
|
||||
CSRF(req) {
|
||||
reqToCtx(req) flatMap f
|
||||
}
|
||||
}
|
||||
|
||||
protected def Auth(f: Context => UserModel => Fu[Result]): Action[Unit] =
|
||||
Auth(BodyParsers.parse.empty)(f)
|
||||
|
||||
protected def Auth[A](p: BodyParser[A])(f: Context => UserModel => Fu[Result]): Action[A] =
|
||||
Action.async(p) { req =>
|
||||
reqToCtx(req) flatMap { implicit ctx =>
|
||||
ctx.me.fold(authenticationFailed) { me =>
|
||||
Env.i18n.requestHandler.forUser(req, ctx.me).fold(f(ctx)(me))(fuccess)
|
||||
CSRF(req) {
|
||||
reqToCtx(req) flatMap { implicit ctx =>
|
||||
ctx.me.fold(authenticationFailed) { me =>
|
||||
Env.i18n.requestHandler.forUser(req, ctx.me).fold(f(ctx)(me))(fuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,8 +122,10 @@ private[controllers] trait LilaController
|
|||
|
||||
protected def AuthBody[A](p: BodyParser[A])(f: BodyContext[A] => UserModel => Fu[Result]): Action[A] =
|
||||
Action.async(p) { req =>
|
||||
reqToCtx(req) flatMap { implicit ctx =>
|
||||
ctx.me.fold(authenticationFailed)(me => f(ctx)(me))
|
||||
CSRF(req) {
|
||||
reqToCtx(req) flatMap { implicit ctx =>
|
||||
ctx.me.fold(authenticationFailed)(me => f(ctx)(me))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,21 +136,18 @@ private[controllers] trait LilaController
|
|||
Secure(BodyParsers.parse.anyContent)(perm)(f)
|
||||
|
||||
protected def Secure[A](p: BodyParser[A])(perm: Permission)(f: Context => UserModel => Fu[Result]): Action[A] =
|
||||
Auth(p) { implicit ctx =>
|
||||
me =>
|
||||
isGranted(perm).fold(f(ctx)(me), fuccess(authorizationFailed(ctx.req)))
|
||||
Auth(p) { implicit ctx => me =>
|
||||
isGranted(perm).fold(f(ctx)(me), fuccess(authorizationFailed(ctx.req)))
|
||||
}
|
||||
|
||||
protected def SecureF(s: UserModel => Boolean)(f: Context => UserModel => Fu[Result]): Action[AnyContent] =
|
||||
Auth(BodyParsers.parse.anyContent) { implicit ctx =>
|
||||
me =>
|
||||
s(me).fold(f(ctx)(me), fuccess(authorizationFailed(ctx.req)))
|
||||
Auth(BodyParsers.parse.anyContent) { implicit ctx => me =>
|
||||
s(me).fold(f(ctx)(me), fuccess(authorizationFailed(ctx.req)))
|
||||
}
|
||||
|
||||
protected def SecureBody[A](p: BodyParser[A])(perm: Permission)(f: BodyContext[A] => UserModel => Fu[Result]): Action[A] =
|
||||
AuthBody(p) { implicit ctx =>
|
||||
me =>
|
||||
isGranted(perm).fold(f(ctx)(me), fuccess(authorizationFailed(ctx.req)))
|
||||
AuthBody(p) { implicit ctx => me =>
|
||||
isGranted(perm).fold(f(ctx)(me), fuccess(authorizationFailed(ctx.req)))
|
||||
}
|
||||
|
||||
protected def SecureBody(perm: Permission.type => Permission)(f: BodyContext[_] => UserModel => Fu[Result]): Action[AnyContent] =
|
||||
|
@ -242,8 +256,8 @@ private[controllers] trait LilaController
|
|||
|
||||
def notFound(implicit ctx: Context): Fu[Result] = negotiate(
|
||||
html =
|
||||
if (HTTPRequest isSynchronousHttp ctx.req) Main notFound ctx.req
|
||||
else fuccess(Results.NotFound("Resource not found")),
|
||||
if (HTTPRequest isSynchronousHttp ctx.req) Main notFound ctx.req
|
||||
else fuccess(Results.NotFound("Resource not found")),
|
||||
api = _ => notFoundJson("Resource not found")
|
||||
)
|
||||
|
||||
|
@ -289,16 +303,15 @@ private[controllers] trait LilaController
|
|||
case _ => html
|
||||
}) map (_.withHeaders("Vary" -> "Accept"))
|
||||
|
||||
protected def reqToCtx(req: RequestHeader): Fu[HeaderContext] =
|
||||
restoreUser(req) flatMap { d =>
|
||||
val ctx = UserContext(req, d.map(_.user))
|
||||
pageDataBuilder(ctx, d.??(_.hasFingerprint)) map { Context(ctx, _) }
|
||||
}
|
||||
protected def reqToCtx(req: RequestHeader): Fu[HeaderContext] = restoreUser(req) flatMap { d =>
|
||||
val ctx = UserContext(req, d.map(_.user))
|
||||
pageDataBuilder(ctx, d.exists(_.hasFingerprint)) map { Context(ctx, _) }
|
||||
}
|
||||
|
||||
protected def reqToCtx[A](req: Request[A]): Fu[BodyContext[A]] =
|
||||
restoreUser(req) flatMap { d =>
|
||||
val ctx = UserContext(req, d.map(_.user))
|
||||
pageDataBuilder(ctx, d.??(_.hasFingerprint)) map { Context(ctx, _) }
|
||||
pageDataBuilder(ctx, d.exists(_.hasFingerprint)) map { Context(ctx, _) }
|
||||
}
|
||||
|
||||
import lila.hub.actorApi.relation._
|
||||
|
@ -341,6 +354,15 @@ private[controllers] trait LilaController
|
|||
}
|
||||
}
|
||||
|
||||
private val csrfCheck = Env.security.csrfRequestHandler.check _
|
||||
private val csrfForbiddenResult = Forbidden("Cross origin request forbidden").fuccess
|
||||
|
||||
private def CSRF(req: RequestHeader)(f: => Fu[Result]): Fu[Result] =
|
||||
if (csrfCheck(req)) f else csrfForbiddenResult
|
||||
|
||||
protected def SocketCSRF[A](req: RequestHeader)(f: => Fu[Either[Result, A]]): Fu[Either[Result, A]] =
|
||||
if (csrfCheck(req)) f else csrfForbiddenResult map Left.apply
|
||||
|
||||
protected def XhrOnly(res: => Fu[Result])(implicit ctx: Context) =
|
||||
if (HTTPRequest isXhr ctx.req) res else notFound
|
||||
|
||||
|
|
|
@ -18,27 +18,29 @@ trait LilaSocket { self: LilaController =>
|
|||
|
||||
def rateLimitedSocket[A: FrameFormatter](consumer: TokenBucket.Consumer, name: String)(f: AcceptType[A]): WebSocket[A, A] =
|
||||
WebSocket[A, A] { req =>
|
||||
reqToCtx(req) flatMap { ctx =>
|
||||
val ip = HTTPRequest lastRemoteAddress req
|
||||
def userInfo = {
|
||||
val sri = get("sri", req) | "none"
|
||||
val username = ctx.usernameOrAnon
|
||||
s"user:$username sri:$sri"
|
||||
}
|
||||
// logger.debug(s"socket:$name socket connect $ip $userInfo")
|
||||
f(ctx).map { resultOrSocket =>
|
||||
resultOrSocket.right.map {
|
||||
case (readIn, writeOut) => (e, i) => {
|
||||
writeOut |>> i
|
||||
e &> Enumeratee.mapInputM { in =>
|
||||
consumer(ip).map { credit =>
|
||||
if (credit >= 0) in
|
||||
else {
|
||||
logger.info(s"socket:$name socket close $ip $userInfo $in")
|
||||
Input.EOF
|
||||
SocketCSRF(req) {
|
||||
reqToCtx(req) flatMap { ctx =>
|
||||
val ip = HTTPRequest lastRemoteAddress req
|
||||
def userInfo = {
|
||||
val sri = get("sri", req) | "none"
|
||||
val username = ctx.usernameOrAnon
|
||||
s"user:$username sri:$sri"
|
||||
}
|
||||
// logger.debug(s"socket:$name socket connect $ip $userInfo")
|
||||
f(ctx).map { resultOrSocket =>
|
||||
resultOrSocket.right.map {
|
||||
case (readIn, writeOut) => (e, i) => {
|
||||
writeOut |>> i
|
||||
e &> Enumeratee.mapInputM { in =>
|
||||
consumer(ip).map { credit =>
|
||||
if (credit >= 0) in
|
||||
else {
|
||||
logger.info(s"socket:$name socket close $ip $userInfo $in")
|
||||
Input.EOF
|
||||
}
|
||||
}
|
||||
}
|
||||
} |>> readIn
|
||||
} |>> readIn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package controllers
|
|||
|
||||
import play.api.libs.json._
|
||||
import play.api.mvc._
|
||||
import play.twirl.api.Html
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.api.Context
|
||||
import lila.app._
|
||||
|
@ -27,12 +29,7 @@ object Lobby extends LilaController {
|
|||
}
|
||||
|
||||
def renderHome(status: Results.Status)(implicit ctx: Context): Fu[Result] = {
|
||||
Env.current.preloader(
|
||||
posts = Env.forum.recent(ctx.me, Env.team.cached.teamIds),
|
||||
tours = Env.tournament.cached promotable true,
|
||||
events = Env.event.api promotable true,
|
||||
simuls = Env.simul allCreatedFeaturable true
|
||||
) map (html.lobby.home.apply _).tupled map { status(_) } map ensureSessionId(ctx.req)
|
||||
HomeCache(ctx) map { status(_) } map ensureSessionId(ctx.req)
|
||||
}.mon(_.http.response.home)
|
||||
|
||||
def seeks = Open { implicit ctx =>
|
||||
|
@ -58,8 +55,60 @@ object Lobby extends LilaController {
|
|||
}
|
||||
}
|
||||
|
||||
def timeline = Auth { implicit ctx =>
|
||||
me =>
|
||||
Env.timeline.entryRepo.userEntries(me.id) map { html.timeline.entries(_) }
|
||||
def timeline = Auth { implicit ctx => me =>
|
||||
Env.timeline.entryRepo.userEntries(me.id) map { html.timeline.entries(_) }
|
||||
}
|
||||
|
||||
private object HomeCache {
|
||||
|
||||
private case class RequestKey(
|
||||
uri: String,
|
||||
headers: Headers)
|
||||
|
||||
private val cache = lila.memo.AsyncCache[RequestKey, Html](
|
||||
f = renderRequestKey,
|
||||
timeToLive = 1 second)
|
||||
|
||||
private def renderCtx(implicit ctx: Context): Fu[Html] = Env.current.preloader(
|
||||
posts = Env.forum.recent(ctx.me, Env.team.cached.teamIds),
|
||||
tours = Env.tournament.cached promotable true,
|
||||
events = Env.event.api promotable true,
|
||||
simuls = Env.simul allCreatedFeaturable true
|
||||
) map (html.lobby.home.apply _).tupled
|
||||
|
||||
private def renderRequestKey(r: RequestKey): Fu[Html] = renderCtx {
|
||||
lila.mon.lobby.cache.miss()
|
||||
val req = new RequestHeader {
|
||||
def id = 1000l
|
||||
def tags = Map.empty
|
||||
def uri = r.uri
|
||||
def path = "/"
|
||||
def method = "GET"
|
||||
def version = "1.1"
|
||||
def queryString = Map.empty
|
||||
def headers = r.headers
|
||||
def remoteAddress = "0.0.0.0"
|
||||
def secure = true
|
||||
}
|
||||
new lila.api.HeaderContext(
|
||||
headerContext = new lila.user.HeaderUserContext(req, none),
|
||||
data = lila.api.PageData.default)
|
||||
}
|
||||
|
||||
def apply(ctx: Context) =
|
||||
if (ctx.isAuth) {
|
||||
lila.mon.lobby.cache.user()
|
||||
renderCtx(ctx)
|
||||
}
|
||||
else {
|
||||
lila.mon.lobby.cache.anon()
|
||||
cache(RequestKey(
|
||||
uri = ctx.req.uri,
|
||||
headers = new Headers(
|
||||
ctx.req.headers.get(COOKIE) ?? { cookie =>
|
||||
List(COOKIE -> cookie)
|
||||
}
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,10 @@ object Main extends LilaController {
|
|||
}
|
||||
}
|
||||
|
||||
def apiWebsocket = WebSocket.tryAccept { req =>
|
||||
Env.site.apiSocketHandler.apply map Right.apply
|
||||
}
|
||||
|
||||
def captchaCheck(id: String) = Open { implicit ctx =>
|
||||
Env.hub.actor.captcher ? ValidCaptcha(id, ~get("solution")) map {
|
||||
case valid: Boolean => Ok(valid fold (1, 0))
|
||||
|
@ -77,22 +81,23 @@ object Main extends LilaController {
|
|||
}
|
||||
}
|
||||
|
||||
def mobileRegister(platform: String, deviceId: String) = Auth { implicit ctx =>
|
||||
me =>
|
||||
Env.push.registerDevice(me, platform, deviceId)
|
||||
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 mobileUnregister = Auth { implicit ctx => me =>
|
||||
Env.push.unregisterDevices(me)
|
||||
}
|
||||
|
||||
def jslog(id: String) = Open { ctx =>
|
||||
val known = ctx.me.??(_.engine)
|
||||
val referer = HTTPRequest.referer(ctx.req)
|
||||
val name = get("n", ctx.req) | "?"
|
||||
lila.log("cheat").branch("jslog").info(s"${ctx.req.remoteAddress} ${ctx.userId} $referer $name")
|
||||
if (!known) {
|
||||
lila.log("cheat").branch("jslog").info(s"${ctx.req.remoteAddress} ${ctx.userId} $referer $name")
|
||||
}
|
||||
lila.mon.cheat.cssBot()
|
||||
ctx.userId.?? {
|
||||
ctx.userId.ifFalse(known) ?? {
|
||||
Env.report.api.autoBotReport(_, referer, name)
|
||||
}
|
||||
lila.game.GameRepo pov id map {
|
||||
|
@ -100,15 +105,18 @@ object Main extends LilaController {
|
|||
} inject Ok
|
||||
}
|
||||
|
||||
def glyphs = Action { req =>
|
||||
private lazy val glyphsResult: Result = {
|
||||
import chess.format.pgn.Glyph
|
||||
import lila.socket.tree.Node.glyphWriter
|
||||
Ok(Json.obj(
|
||||
"move" -> Glyph.MoveAssessment.all,
|
||||
"position" -> Glyph.PositionAssessment.all,
|
||||
"observation" -> Glyph.Observation.all
|
||||
"move" -> Glyph.MoveAssessment.display,
|
||||
"position" -> Glyph.PositionAssessment.display,
|
||||
"observation" -> Glyph.Observation.display
|
||||
)) as JSON
|
||||
}
|
||||
def glyphs = Action { req =>
|
||||
glyphsResult
|
||||
}
|
||||
|
||||
def image(id: String, hash: String, name: String) = Action.async { req =>
|
||||
Env.db.image.fetch(id) map {
|
||||
|
@ -122,6 +130,15 @@ object Main extends LilaController {
|
|||
}
|
||||
}
|
||||
|
||||
val robots = Action { _ =>
|
||||
Ok {
|
||||
if (Env.api.Net.Crawlable)
|
||||
"User-agent: *\nAllow: /\nDisallow: /game/export"
|
||||
else
|
||||
"User-agent: *\nDisallow: /"
|
||||
}
|
||||
}
|
||||
|
||||
def notFound(req: RequestHeader): Fu[Result] =
|
||||
reqToCtx(req) map { implicit ctx =>
|
||||
lila.mon.http.response.code404()
|
||||
|
|
|
@ -122,7 +122,7 @@ object Mod extends LilaController {
|
|||
}
|
||||
}
|
||||
|
||||
private val ipIntelCache =
|
||||
private[controllers] val ipIntelCache =
|
||||
lila.memo.AsyncCache[String, Int](ip => {
|
||||
import play.api.libs.ws.WS
|
||||
import play.api.Play.current
|
||||
|
|
37
app/controllers/Monitor.scala
Normal file
37
app/controllers/Monitor.scala
Normal file
|
@ -0,0 +1,37 @@
|
|||
package controllers
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import play.api.libs.ws.WS
|
||||
import play.api.mvc._, Results._
|
||||
import play.api.Play.current
|
||||
|
||||
import lila.app._
|
||||
import views._
|
||||
|
||||
object Monitor extends LilaController {
|
||||
|
||||
private val url = "http://api.monitor.lichess.org/render"
|
||||
private object path {
|
||||
val coachPageView = "servers.lichess.statsite.counts.main.counter.coach.page_view.profile"
|
||||
}
|
||||
private val coachPageViewCache = lila.memo.AsyncCache[lila.user.User.ID, Result](
|
||||
f = userId => WS.url(url).withQueryString(
|
||||
"format" -> "json",
|
||||
"target" -> s"""summarize(${path.coachPageView}.$userId,"1d","sum",false)""",
|
||||
// "target" -> s"""summarize(servers.lichess.statsite.counts.main.counter.insight.request,'1d','sum',false)""",
|
||||
"from" -> "-7d",
|
||||
"until" -> "now"
|
||||
).get() map {
|
||||
case res if res.status == 200 => Ok(res.body)
|
||||
case res =>
|
||||
lila.log("monitor").warn(s"coach ${res.status} ${res.body}")
|
||||
NotFound
|
||||
},
|
||||
timeToLive = 10 seconds
|
||||
)
|
||||
|
||||
def coachPageView = Secure(_.Coach) { ctx =>
|
||||
me =>
|
||||
coachPageViewCache(me.id)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package controllers
|
||||
|
||||
import lila.app._
|
||||
import lila.common.paginator.PaginatorJson
|
||||
import lila.notify.Notification.Notifies
|
||||
|
||||
import play.api.libs.json._
|
||||
|
|
|
@ -181,13 +181,15 @@ object Puzzle extends LilaController {
|
|||
_ ?? lila.game.GameRepo.gameWithInitialFen flatMap {
|
||||
case Some((game, initialFen)) =>
|
||||
Ok(Env.api.pgnDump(game, initialFen.map(_.value)).toString).fuccess
|
||||
case _ => lila.game.GameRepo.findRandomFinished(1000) flatMap {
|
||||
_ ?? { game =>
|
||||
lila.game.GameRepo.initialFen(game) map { fen =>
|
||||
Ok(Env.api.pgnDump(game, fen).toString)
|
||||
case _ =>
|
||||
lila.log("puzzle import").info("No recent good game, serving a random one :-/")
|
||||
lila.game.GameRepo.findRandomFinished(1000) flatMap {
|
||||
_ ?? { game =>
|
||||
lila.game.GameRepo.initialFen(game) map { fen =>
|
||||
Ok(Env.api.pgnDump(game, fen).toString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ trait AssetHelper { self: I18nHelper =>
|
|||
def jsTagCompiled(name: String) = if (isProd) jsAt("compiled/" + name) else jsTag(name)
|
||||
|
||||
val jQueryTag = cdnOrLocal(
|
||||
cdn = "//cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js",
|
||||
cdn = "//cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js",
|
||||
test = "window.jQuery",
|
||||
local = staticUrl("javascripts/vendor/jquery.min.js"))
|
||||
|
||||
|
@ -44,9 +44,9 @@ trait AssetHelper { self: I18nHelper =>
|
|||
local = staticUrl("vendor/highcharts4/highcharts.js"))
|
||||
|
||||
val highchartsLatestTag = cdnOrLocal(
|
||||
cdn = "//code.highcharts.com/4.1/highcharts.js",
|
||||
cdn = "//code.highcharts.com/4.2/highcharts.js",
|
||||
test = "window.Highcharts",
|
||||
local = staticUrl("vendor/highcharts4/highcharts-4.1.9.js"))
|
||||
local = staticUrl("vendor/highcharts4/highcharts-4.2.5.js"))
|
||||
|
||||
val highchartsMoreTag = Html {
|
||||
"""<script src="//code.highcharts.com/4.1.4/highcharts-more.js"></script>"""
|
||||
|
|
|
@ -46,6 +46,7 @@ object Environment
|
|||
|
||||
def netDomain = apiEnv.Net.Domain
|
||||
def netBaseUrl = apiEnv.Net.BaseUrl
|
||||
val isGloballyCrawlable = apiEnv.Net.Crawlable
|
||||
|
||||
def isProd = apiEnv.isProd
|
||||
|
||||
|
|
|
@ -34,8 +34,10 @@ evenMoreCss =cssTag("material.form.css")) {
|
|||
</div>
|
||||
}
|
||||
<div>
|
||||
@base.form.group(form("fideRating"), Html("FIDE rating"), klass = "half") {
|
||||
@base.form.input(form("fideRating"), typ="number")
|
||||
@List("fide", "uscf", "ecf").map { rn =>
|
||||
@base.form.group(form(s"${rn}Rating"), Html(s"${rn.toUpperCase} rating"), help=Html("If none, leave empty").some, klass = "third") {
|
||||
@base.form.input(form(s"${rn}Rating"), typ="number")
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@errMsg(form)
|
||||
|
|
|
@ -75,7 +75,7 @@ atom = atom.some) {
|
|||
}
|
||||
/
|
||||
<a data-icon="x" class="text" target="_blank" rel="nofollow" href="@cdnUrl(routes.Export.pdf(game.id).url)">@trans.printFriendlyPDF()</a>
|
||||
@if(game.variant.standard) {
|
||||
@if(lila.game.Game.visualisableVariants(game.variant)) {
|
||||
/
|
||||
<a data-icon="x" class="text" target="_blank" href="@routes.Export.visualizer(game.id)">Generate images</a>
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ side: Option[Html] = None,
|
|||
menu: Option[Html] = None,
|
||||
chat: Option[Html] = None,
|
||||
underchat: Option[Html] = None,
|
||||
robots: Boolean = true,
|
||||
robots: Boolean = isGloballyCrawlable,
|
||||
moreCss: Html = Html(""),
|
||||
moreJs: Html = Html(""),
|
||||
zen: Boolean = false,
|
||||
|
@ -149,7 +149,7 @@ withGtm: Boolean = false)(body: Html)(implicit ctx: Context)
|
|||
<a class="fright link text data-count" href="@routes.Report.list" data-count="@reportNbUnprocessed" data-icon="@icon.mod"></a>
|
||||
}
|
||||
}
|
||||
<a id="reconnecting" class="fright link" onclick="location.reload();" data-icon="B"> @trans.reconnecting()</a>
|
||||
<a id="reconnecting" class="fright link" onclick="window.location.reload()" data-icon="B"> @trans.reconnecting()</a>
|
||||
</div>
|
||||
<div id="fpmenu">@fpmenu()</div>
|
||||
<div class="content @ctx.is3d.fold("is3d", "is2d")">
|
||||
|
|
|
@ -16,9 +16,7 @@
|
|||
<a href="@routes.Puzzle.home">@trans.training()</a>
|
||||
<a href="@routes.Coordinate.home">@trans.coordinates()</a>
|
||||
<a href="@routes.Study.allDefault(1)">Study</a>
|
||||
@if(isGranted(_.Coach)) {
|
||||
<a href="@routes.Coach.allDefault(1)">Chess coaches</a>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
|
|
|
@ -16,22 +16,24 @@
|
|||
@moreJs = {
|
||||
<script src="//oss.maxcdn.com/jquery.form/3.50/jquery.form.min.js"></script>
|
||||
@jsAt("vendor/bar-rating/dist/jquery.barrating.min.js")
|
||||
@jsTag("chart/coachPageView.js")
|
||||
@jsTag("coach.form.js")
|
||||
}
|
||||
|
||||
@side = {
|
||||
<a href="@routes.Coach.show(c.user.username)" class="button text" data-icon="v">Preview coach page</a>
|
||||
<a href="@routes.Coach.show(c.user.username)" class="preview button text" data-icon="v">Preview coach page</a>
|
||||
}
|
||||
|
||||
@layout(title = s"${c.user.titleUsername} coach page",
|
||||
moreCss = moreCss,
|
||||
moreJs = moreJs) {
|
||||
moreJs = moreJs,
|
||||
side = side.some) {
|
||||
<div class="coach_edit content_box no_padding">
|
||||
<div class="top">
|
||||
<div class="picture_wrap">
|
||||
@if(c.coach.hasPicture) {
|
||||
<a class="upload_picture" href="@routes.Coach.picture" title="Change/delete your profile picture">
|
||||
@pic(c, 200)
|
||||
@pic(c, 250)
|
||||
</a>
|
||||
} else {
|
||||
<div class="upload_picture">
|
||||
|
@ -43,12 +45,12 @@ moreJs = moreJs) {
|
|||
<h1>
|
||||
@c.user.title.map { t => @t }@c.user.profileOrDefault.nonEmptyRealName.getOrElse(c.user.username)
|
||||
</h1>
|
||||
<div class="todo" data-profile="c.user.profileOrDefault.isComplete">
|
||||
<div class="todo" data-profile="@c.user.profileOrDefault.isComplete">
|
||||
<h3>TODO list before publishing your coach profile</h3>
|
||||
<ul></ul>
|
||||
</div>
|
||||
<div class="analytics">
|
||||
Soon here: Coach page analytics
|
||||
<div class="pageview_chart">@base.spinner()</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -92,11 +94,11 @@ moreJs = moreJs) {
|
|||
@textarea(form("profile.methodology"), Html("Teaching methodology"), help = Html("How you prepare and run lessons. How you follow up with students.").some)
|
||||
</div>
|
||||
<div class="panel contents">
|
||||
@textarea(form("profile.youtubeVideos"), Html("Featured youtube videos"), help = Html("Up to 6 Youtube video URLs, one per line").some)
|
||||
@textarea(form("profile.publicStudies"), Html("Featured public lichess studies"), help = Html("Up to 6 lichess study URLs, one per line").some)
|
||||
@base.form.group(form("profile.youtubeChannel"), Html("URL of your Youtube channel")) {
|
||||
@base.form.input(form("profile.youtubeChannel"))
|
||||
}
|
||||
@textarea(form("profile.publicStudies"), Html("Featured public lichess studies"), help = Html("Up to 6 lichess study URLs, one per line").some)
|
||||
@textarea(form("profile.youtubeVideos"), Html("Featured youtube videos"), help = Html("Up to 6 Youtube video URLs, one per line").some)
|
||||
</div>
|
||||
<div class="panel reviews">
|
||||
<p class="help text" data-icon="">Reviews are visible only after you approve them.</p>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@(pager: Paginator[lila.coach.Coach.WithUser], order: lila.coach.CoachPager.Order)(implicit ctx: Context)
|
||||
|
||||
@side = {
|
||||
<div class="coach-intro">
|
||||
<div class="coach-intro coach-side">
|
||||
<img src="@staticUrl("images/icons/certification.svg")" class="certification" />
|
||||
<h2>Certified coaches</h2>
|
||||
<p>
|
||||
|
@ -16,7 +16,7 @@
|
|||
make your choice and enjoy learning chess!
|
||||
</p>
|
||||
</div>
|
||||
<div class="coach-add">
|
||||
<div class="coach-add coach-side">
|
||||
<p>
|
||||
Are you a great chess coach?<br />
|
||||
Do you want to be part of this list?<br />
|
||||
|
@ -58,7 +58,7 @@ side = side.some) {
|
|||
}
|
||||
@pager.nextPage.map { np =>
|
||||
<div class="pager none">
|
||||
<a href="@addQueryParameter(routes.Coach.all(order.key).toString, "page", np)">Next</a>
|
||||
<a rel="next" href="@addQueryParameter(routes.Coach.all(order.key).toString, "page", np)">Next</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
@cssTag("coach.form.css")
|
||||
}
|
||||
|
||||
@layout(title = s"${c.user.titleUsername} coach picture", moreCss = moreCss) {
|
||||
@layout(title = s"${c.user.titleUsername} coach picture",
|
||||
moreJs = jsTag("coach.form.js"),
|
||||
moreCss = moreCss) {
|
||||
<div class="coach_picture content_box small_box no_padding">
|
||||
<h1 class="lichess_title">
|
||||
@userLink(c.user) coach picture
|
||||
|
@ -28,7 +30,7 @@
|
|||
}
|
||||
}
|
||||
<div class="cancel">
|
||||
<a href="@routes.Coach.edit" class="text" data-icon="I">Cancel and return to coach page form</a>
|
||||
<a href="@routes.Coach.edit" class="text" data-icon="I">Return to coach page form</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@(c: lila.coach.Coach.WithUser, approval: Boolean)(implicit ctx: Context)
|
||||
|
||||
<div class="review-form">
|
||||
<div class="review-form coach-side">
|
||||
@if(approval) {
|
||||
<div class="approval">
|
||||
<p>Thank you for the review!</p>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@(c: lila.coach.Coach.WithUser, reviews: lila.coach.CoachReview.Reviews)(implicit ctx: Context)
|
||||
|
||||
@if(reviews.list.nonEmpty) {
|
||||
<div class="reviews">
|
||||
<div class="reviews coach-side">
|
||||
<h2>@pluralize("Player review", reviews.list.size)</h2>
|
||||
@reviews.list.map { r =>
|
||||
<div class="review">
|
||||
|
|
|
@ -73,8 +73,8 @@ moreCss = moreCss) {
|
|||
<tr class="rating">
|
||||
<th>Rating</th>
|
||||
<td>
|
||||
@profile.fideRating.map { r =>
|
||||
FIDE: @r,
|
||||
@profile.officialRating.map { r =>
|
||||
@r.name.toUpperCase: @r.rating,
|
||||
}
|
||||
<a href="@routes.User.show(c.user.username)">
|
||||
@c.user.best8Perfs.take(6).filter(c.user.hasEstablishedRating).map { pt =>
|
||||
|
@ -135,6 +135,18 @@ moreCss = moreCss) {
|
|||
@section("Best skills", profile.skills)
|
||||
@section("Teaching methodology", profile.methodology)
|
||||
</div>
|
||||
@if(studies.nonEmpty) {
|
||||
<section class="studies">
|
||||
<h1>Public studies</h1>
|
||||
<div class="list">
|
||||
@studies.map { s =>
|
||||
<div class="study">
|
||||
@study.widget(s)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
@if(profile.youtubeUrls.nonEmpty) {
|
||||
<section class="youtube">
|
||||
<h1>
|
||||
|
@ -150,18 +162,6 @@ moreCss = moreCss) {
|
|||
</div>
|
||||
</section>
|
||||
}
|
||||
@if(studies.nonEmpty) {
|
||||
<section class="studies">
|
||||
<h1>Public studies</h1>
|
||||
<div class="list">
|
||||
@studies.map { s =>
|
||||
<div class="study">
|
||||
@study.widget(s)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ moreCss = cssTag("event.css")) {
|
|||
}
|
||||
</td>
|
||||
<td>
|
||||
<span class="date">@showDateTime(e.startsAt)</span>
|
||||
<span class="date">@momentFormat(e.startsAt)</span>
|
||||
@momentFromNow(e.startsAt)
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
}
|
||||
}
|
||||
@if(pager.hasNextPage) {
|
||||
<a href="@url(pager.nextPage.get)" data-icon="H"></a>
|
||||
<a rel="next" href="@url(pager.nextPage.get)" data-icon="H"></a>
|
||||
} else {
|
||||
<span class="disabled" data-icon="H"></span>
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
@(text: Field, author: Field)(implicit ctx: Context)
|
||||
@(text: Field, author: Field, topic: Option[lila.forum.Topic])(implicit ctx: Context)
|
||||
<label>
|
||||
<span class="required">@trans.message()</span>
|
||||
<textarea name="@text.name" id="@text.id">@text.value</textarea>
|
||||
@topic match {
|
||||
case Some(topic) => { <textarea class="post-text-area" name="@text.name" data-topic="@topic.id">@text.value</textarea> }
|
||||
case None => { <textarea class="post-text-area" name="@text.name">@text.value</textarea> }
|
||||
}
|
||||
@errMsg(text)
|
||||
</label>
|
||||
|
||||
@if(!ctx.isAuth) {
|
||||
<label>
|
||||
<span>Author</span>
|
||||
|
|
|
@ -17,7 +17,7 @@ searchText = text
|
|||
<tbody class="infinitescroll">
|
||||
@views.nextPage.map { n =>
|
||||
<tr><th class="pager none">
|
||||
<a href="@routes.ForumPost.search(text, n)">Next</a>
|
||||
<a rel="next" href="@routes.ForumPost.search(text, n)">Next</a>
|
||||
</th></tr>
|
||||
<tr></tr>
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@(categ: lila.forum.Categ, form: Form[_], captcha: lila.common.Captcha)(implicit ctx: Context)
|
||||
|
||||
@forum.layout(title = "New forum topic") {
|
||||
@forum.layout(title = "New forum topic", moreJs=jsTag("forum-post.js")) {
|
||||
<ol class="crumbs">
|
||||
<li><a style="text-decoration:none" data-icon="d" class="is4" href="@routes.ForumCateg.index"> Forum</a></li>
|
||||
<li><h1>@categ.name</h1></li>
|
||||
|
@ -23,7 +23,7 @@
|
|||
<input class="subject" autofocus="true" minlength=3 maxlength=100 value="@form("name").value" type="text" name="@form("name").name" id="@form("name").id">
|
||||
@errMsg(form("name"))
|
||||
</label>
|
||||
@forum.post.formFields(form("post")("text"), form("post")("author"))
|
||||
@forum.post.formFields(form("post")("text"), form("post")("author"), None)
|
||||
@base.captcha(form("post")("move"), form("post")("gameId"), captcha)
|
||||
@errMsg(form("post"))
|
||||
<button class="submit button" type="submit" data-icon="E"> @trans.createTheTopic()</button>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
@forum.layout(
|
||||
title = s"${categ.name} / ${topic.name} page ${posts.currentPage}",
|
||||
menu = modMenu.some.ifTrue(categ.isStaff),
|
||||
moreJs = jsTag("forum-post.js"),
|
||||
openGraph = lila.app.ui.OpenGraph(
|
||||
title = s"Forum: ${categ.name} / ${topic.name} page ${posts.currentPage}",
|
||||
url = s"$netBaseUrl${routes.ForumTopic.show(categ.slug, topic.slug).url}",
|
||||
|
@ -36,16 +37,36 @@ description = shorten(posts.currentPageResults.headOption.??(_.text).replace("\n
|
|||
<div class="post" id="@post.number">
|
||||
<div class="metas clearfix">
|
||||
@authorLink(post, "author".some)
|
||||
@momentFromNow(post.createdAt)
|
||||
|
||||
@post.updatedAt.map { updatedAt =>
|
||||
<span class="post-edited">edited</span>
|
||||
@momentFromNow(updatedAt)
|
||||
}.getOrElse {
|
||||
@momentFromNow(post.createdAt)
|
||||
}
|
||||
|
||||
<a class="anchor" href="@routes.ForumTopic.show(categ.slug, topic.slug, posts.currentPage)#@post.number">#@post.number</a>
|
||||
|
||||
@if(isGranted(_.IpBan)) {
|
||||
<span class="mod postip">@post.ip</span>
|
||||
}
|
||||
@if(ctx.userId.fold(false)(post.shouldShowEditForm(_))) {
|
||||
<a class="mod thin edit button" data-icon="m"> Edit</a>
|
||||
}
|
||||
@if(isGrantedMod(categ.slug)) {
|
||||
<a class="mod thin delete button" href="@routes.ForumPost.delete(categ.slug, post.id)" data-icon="q"> Delete</a>
|
||||
}
|
||||
</div>
|
||||
<p class="message">@autoLink(post.text)</p>
|
||||
@if(ctx.userId.fold(false)(post.shouldShowEditForm(_))) {
|
||||
<form class="edit-post-form" method="post" action="@routes.ForumPost.edit(post.id)">
|
||||
<textarea data-topic="@topic.id" name="changes" class="post-text-area edit-post-box" minlength="3" required>@post.text</textarea>
|
||||
<div class="edit-buttons">
|
||||
<a class="edit-post-cancel" href="@routes.ForumPost.redirect(post.id)" style="margin-left:20px">@trans.cancel()</a>
|
||||
<button type="submit" class="submit button">@trans.apply()</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -73,7 +94,7 @@ description = shorten(posts.currentPageResults.headOption.??(_.text).replace("\n
|
|||
case (form, captcha) => {
|
||||
<h2 class="postNewTitle" id="reply">@trans.replyToThisTopic()</h2>
|
||||
<form class="wide" action="@routes.ForumPost.create(categ.slug, topic.slug, posts.currentPage)#reply" method="POST" novalidate>
|
||||
@forum.post.formFields(form("text"), form("author"))
|
||||
@forum.post.formFields(form("text"), form("author"), topic.some)
|
||||
@base.captcha(form("move"), form("gameId"), captcha)
|
||||
@errMsg(form)
|
||||
<button type="submit" class="submit button" data-icon="E"> @trans.reply()</button>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
}
|
||||
</span>
|
||||
@game.pgnImport.flatMap(_.date).getOrElse(
|
||||
game.isBeingPlayed.fold(trans.playingRightNow(), Html(s"<span title='${showDateTime(game.createdAt)}'>${momentFormat(game.createdAt)}</span>"))
|
||||
game.isBeingPlayed.fold(trans.playingRightNow(), momentFormat(game.createdAt))
|
||||
)
|
||||
</div>
|
||||
@game.pgnImport.flatMap(_.date).map { date =>
|
||||
|
|
|
@ -26,7 +26,7 @@ title = trans.inbox.str()) {
|
|||
<tbody class="infinitescroll">
|
||||
@if(threads.hasToPaginate) {
|
||||
<tr><th class="pager none">
|
||||
<a href="@routes.Message.inbox(threads.nextPage | 1)">Next</a>
|
||||
<a rel="next" href="@routes.Message.inbox(threads.nextPage | 1)">Next</a>
|
||||
</th></tr>
|
||||
}
|
||||
@threads.currentPageResults.map { thread =>
|
||||
|
|
|
@ -28,7 +28,7 @@ description = "Community knowledge and frequently asked questions about lichess.
|
|||
}
|
||||
@questions.nextPage.map { next =>
|
||||
<tr><th class="pager none">
|
||||
<a href="@routes.QaQuestion.index(next.some)">Next</a>
|
||||
<a rel="next" href="@routes.QaQuestion.index(next.some)">Next</a>
|
||||
</th></tr>
|
||||
}
|
||||
</tbody>
|
||||
|
|
|
@ -207,7 +207,7 @@ description = s"Search in ${nbGames.localize} chess games using advanced criteri
|
|||
</div>
|
||||
<div class="search_infinitescroll">
|
||||
@pager.nextPage.map { n =>
|
||||
<div class="pager none"><a href="@routes.Search.index(n)">Next</a></div>
|
||||
<div class="pager none"><a rel="next" href="@routes.Search.index(n)">Next</a></div>
|
||||
}.getOrElse {
|
||||
<div class="none"></div>
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
}
|
||||
@pager.nextPage.map { np =>
|
||||
<div class="pager none">
|
||||
<a href="@addQueryParameter(url.toString, "page", np)">Next</a>
|
||||
<a rel="next" href="@addQueryParameter(url.toString, "page", np)">Next</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@ currentTab = tab.some) {
|
|||
<tbody class="infinitescroll">
|
||||
@next.map { n =>
|
||||
<tr><th class="pager none">
|
||||
<a href="@n">Next</a>
|
||||
<a rel="next" href="@n">Next</a>
|
||||
</th></tr>
|
||||
<tr></tr>
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<h2>@trans.teamRecentMembers()</h2>
|
||||
<div class="userlist infinitescroll">
|
||||
@members.nextPage.map { np =>
|
||||
<div class="pager none"><a href="@routes.Team.show(t.id, np)">Next</a></div>
|
||||
<div class="pager none"><a rel="next" href="@routes.Team.show(t.id, np)">Next</a></div>
|
||||
}
|
||||
@members.currentPageResults.map { member =>
|
||||
<div class="paginated_element">@userLink(member.user)</div>
|
||||
|
|
|
@ -60,5 +60,13 @@
|
|||
@base.form.select(form("conditions.maxRating.perf"), Condition.DataForm.perfChoices)
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
@base.form.group(form("conditions.minRating.rating"), Html("Minimum top rating"), half = true) {
|
||||
@base.form.select(form("conditions.minRating.rating"), Condition.DataForm.minRatingChoices)
|
||||
}
|
||||
@base.form.group(form("conditions.minRating.perf"), Html("In variant"), half = true) {
|
||||
@base.form.select(form("conditions.minRating.perf"), Condition.DataForm.perfChoices)
|
||||
}
|
||||
</div>
|
||||
|
||||
@base.form.submit()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<tbody class="infinitescroll">
|
||||
@finished.nextPage.map { np =>
|
||||
<tr><th class="pager none">
|
||||
<a href="@routes.Tournament.home(np)">Next</a>
|
||||
<a rel="next" href="@routes.Tournament.home(np)">Next</a>
|
||||
</th></tr>
|
||||
}
|
||||
@finished.currentPageResults.map { t =>
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
} else {
|
||||
@trans.by(usernameOrId(tour.createdBy))
|
||||
}
|
||||
• <span title="@showDateTime(tour.startsAt)">@momentFormat(tour.startsAt)</span>
|
||||
• @momentFormat(tour.startsAt)
|
||||
@if(!tour.position.initial) {
|
||||
<br /><br />
|
||||
<a target="_blank" href="@tour.position.url">
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
<div class="search_infinitescroll">
|
||||
@pager.nextPage.map { n =>
|
||||
<div class="pager none"><a href="@routes.User.showFilter(u.username, filterName, n)">Next</a></div>
|
||||
<div class="pager none"><a rel="next" href="@routes.User.showFilter(u.username, filterName, n)">Next</a></div>
|
||||
}.getOrElse {
|
||||
<div class="none"></div>
|
||||
}
|
||||
|
@ -25,7 +25,7 @@
|
|||
} else {
|
||||
<div class="games infinitescroll @if(filterName == "playing" && pager.nbResults > 2) {game_list playing center}">
|
||||
@pager.nextPage.map { np =>
|
||||
<div class="pager none"><a href="@routes.User.showFilter(u.username, filterName, np)">Next</a></div>
|
||||
<div class="pager none"><a rel="next" href="@routes.User.showFilter(u.username, filterName, np)">Next</a></div>
|
||||
}
|
||||
@if(filterName == "playing" && pager.nbResults > 2) {
|
||||
@pager.currentPageResults.flatMap{ Pov(_, u) }.map { p =>
|
||||
|
|
|
@ -76,7 +76,6 @@
|
|||
<div class="preferences">
|
||||
<strong class="text inline" data-icon="%">Notable preferences:</strong>
|
||||
@if(pref.keyboardMove != Pref.KeyboardMove.NO) { [keyboard moves] }
|
||||
@if(pref.blindfold != Pref.Blindfold.NO) { [blindfold] }
|
||||
</div>
|
||||
@optionAggregateAssessment.fold{
|
||||
<div class="evaluation">
|
||||
|
@ -262,13 +261,10 @@
|
|||
<br />
|
||||
}
|
||||
</div>
|
||||
@if(spy.otherUsers.size == 1) {
|
||||
<strong data-icon="f"> No similar user found</strong>
|
||||
} else {
|
||||
<table class="others slist">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>@spy.otherUsers.size similar user(s)</th>
|
||||
<th>@spy.otherUsers.size user(s) on these IPs</th>
|
||||
<th>Same</th>
|
||||
<th>Games</th>
|
||||
<th>Marks</th>
|
||||
|
@ -306,7 +302,6 @@
|
|||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
<div class="listings clearfix">
|
||||
<div class="spy_ips">
|
||||
<strong>@spy.ips.size IP addresses</strong> <ul>@spy.ipsByLocations.map {
|
||||
|
|
|
@ -172,8 +172,8 @@ description = describeUser(u)).some) {
|
|||
}
|
||||
}
|
||||
<div class="stats">
|
||||
@profile.fideRating.map { rating =>
|
||||
<div class="fide_rating">FIDE rating: <strong>@rating</strong></div>
|
||||
@profile.officialRating.map { r =>
|
||||
<div>@r.name.toUpperCase rating: <strong>@r.rating</strong></div>
|
||||
}
|
||||
@NotForKids {
|
||||
@profile.nonEmptyLocation.map { l =>
|
||||
|
@ -224,7 +224,7 @@ description = describeUser(u)).some) {
|
|||
}
|
||||
</div>
|
||||
}
|
||||
@if(u.hasGames || info.nbImported > 0) {
|
||||
@if(u.hasGames || info.nbImported > 0 || info.nbBookmark > 0) {
|
||||
<div class="content_box_inter tabs" id="games">
|
||||
@filters.list.map { f =>
|
||||
<a class="intertab@{ (filters.current == f).??(" active") }" href="@routes.User.showFilter(u.username, f.name)">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<tbody class="infinitescroll">
|
||||
@pager.nextPage.map { np =>
|
||||
<tr><th class="pager none">
|
||||
<a href="@call.url?page=@np">Next</a>
|
||||
<a rel="next" href="@call.url?page=@np">Next</a>
|
||||
</th></tr>
|
||||
}
|
||||
@pager.currentPageResults.map { r =>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<tbody class="infinitescroll">
|
||||
@pager.nextPage.map { np =>
|
||||
<tr><th class="pager none">
|
||||
<a href="@routes.UserTournament.path(u.username, path, np)">Next</a>
|
||||
<a rel="next" href="@routes.UserTournament.path(u.username, path, np)">Next</a>
|
||||
</th></tr>
|
||||
}
|
||||
@pager.currentPageResults.map { e =>
|
||||
|
@ -36,7 +36,7 @@
|
|||
} else {
|
||||
@e.tour.perfType.map(_.name)
|
||||
} •
|
||||
@showDateTime(e.tour.startsAt)
|
||||
@momentFormat(e.tour.startsAt)
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -16,7 +16,7 @@ control = control) {
|
|||
}
|
||||
@videos.nextPage.map { next =>
|
||||
<div class="pager none">
|
||||
<a href="@routes.Video.author(author)?@control.queryString&page=@next">Next</a>
|
||||
<a rel="next" href="@routes.Video.author(author)?@control.queryString&page=@next">Next</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -48,7 +48,7 @@ control = control) {
|
|||
}
|
||||
@videos.nextPage.map { next =>
|
||||
<div class="pager none">
|
||||
<a href="@routes.Video.index?@control.queryString&page=@next">Next</a>
|
||||
<a rel="next" href="@routes.Video.index?@control.queryString&page=@next">Next</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -29,7 +29,7 @@ control = control) {
|
|||
}
|
||||
@videos.nextPage.map { next =>
|
||||
<div class="pager none">
|
||||
<a href="@routes.Video.index?@control.queryString&page=@next">Next</a>
|
||||
<a rel="next" href="@routes.Video.index?@control.queryString&page=@next">Next</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -10,12 +10,6 @@ cd gfc-semver
|
|||
sbt publish-local
|
||||
cd ..
|
||||
|
||||
git clone https://github.com/ornicar/ReactiveMongo --branch lichess
|
||||
cd ReactiveMongo
|
||||
git checkout b3e895f1c723d7cb518763f31fba62dc74311eab
|
||||
sbt publish-local
|
||||
cd ..
|
||||
|
||||
git clone https://github.com/ornicar/scalalib
|
||||
cd scalalib
|
||||
sbt publish-local
|
||||
|
|
45
bin/mongodb/puzzle-disable-broken-mate.js
Normal file
45
bin/mongodb/puzzle-disable-broken-mate.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
var puzzles = db.puzzle;
|
||||
var count = 0;
|
||||
|
||||
function depthOf(obj) {
|
||||
var level = 1;
|
||||
var key;
|
||||
|
||||
for (key in obj) {
|
||||
if (!obj.hasOwnProperty(key)) continue;
|
||||
|
||||
if (typeof obj[key] === 'object') {
|
||||
var depth = depthOf(obj[key]) + 1;
|
||||
level = Math.max(depth, level);
|
||||
}
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
puzzles.find({
|
||||
"mate": true,
|
||||
"_id": {
|
||||
"$gt": 60120
|
||||
},
|
||||
'vote.sum': {
|
||||
'$gt': -8000
|
||||
}
|
||||
}).forEach(function(p) {
|
||||
var depth = depthOf(p);
|
||||
if (depth % 2 === 1) {
|
||||
count++;
|
||||
puzzles.update({
|
||||
_id: p._id
|
||||
}, {
|
||||
$set: {
|
||||
vote: {
|
||||
up: NumberInt(0),
|
||||
down: NumberInt(9000),
|
||||
sum: NumberInt(-9000)
|
||||
}
|
||||
}
|
||||
});
|
||||
print(p._id);
|
||||
}
|
||||
});
|
||||
print("Disabled " + count + " puzzles");
|
21
bin/mongodb/puzzle-disable-endgames.js
Normal file
21
bin/mongodb/puzzle-disable-endgames.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
var puzzles = db.puzzle;
|
||||
var count = 0;
|
||||
puzzles.find().forEach(function(p) {
|
||||
var parts = p.fen.split(/\s/);
|
||||
var pieceCount = parts[0].split(/[nbrqkp]/i).length - 1;
|
||||
if (pieceCount < 9 && p.vote.sum < 50 && p.vote.sum > -1000) {
|
||||
count++;
|
||||
puzzles.update({
|
||||
_id: p._id
|
||||
}, {
|
||||
$set: {
|
||||
vote: {
|
||||
up: NumberInt(0),
|
||||
down: NumberInt(0),
|
||||
sum: NumberInt(-9000)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
print("Disabled " + count + " puzzles");
|
10
bin/mongodb/puzzle-salvage-old-good.js
Normal file
10
bin/mongodb/puzzle-salvage-old-good.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
var coll = db.puzzle;
|
||||
coll.find({_id:{$lt:60120},'vote.sum':{$gte:100}}).sort({_id:1}).forEach(function(p) {
|
||||
if (coll.count({fen:p.fen}) == 1) {
|
||||
var nextId = coll.find({},{_id:1}).sort({_id:-1}).limit(1)[0]._id + 1;
|
||||
p.salvaged = NumberInt(p._id);
|
||||
p._id = NumberInt(nextId);
|
||||
print(p.salvaged + ' -> ' + p._id);
|
||||
coll.insert(p);
|
||||
}
|
||||
});
|
|
@ -33,7 +33,9 @@ if [ $mode = "main" ]; then
|
|||
$CLI deploy pre
|
||||
fi
|
||||
|
||||
bin/dev ";stage;exit"
|
||||
SBT_OPTS=""
|
||||
export JAVA_OPTS="-Xms1024M -Xmx1024M -XX:ReservedCodeCacheSize=64m -XX:+UseConcMarkSweepGC"
|
||||
sbt ";stage;exit"
|
||||
if [ $? != 0 ]; then
|
||||
lilalog "Deploy canceled"
|
||||
exit 1
|
||||
|
|
|
@ -18,7 +18,9 @@ fi
|
|||
|
||||
lilalog "Deploy assets to $mode server $REMOTE:$REMOTE_DIR"
|
||||
|
||||
bin/prod/compile-client
|
||||
# if [ $2 != "skip" ]; then
|
||||
bin/prod/compile-client
|
||||
# fi
|
||||
|
||||
lilalog "Rsync scripts"
|
||||
rsync --archive --no-o --no-g --progress public $REMOTE:$REMOTE_DIR
|
||||
|
|
|
@ -10,9 +10,10 @@ net {
|
|||
ip = "5.196.91.160"
|
||||
asset {
|
||||
domain = ${net.domain}
|
||||
version = 1099
|
||||
version = 1145
|
||||
}
|
||||
email = "contact@lichess.org"
|
||||
crawlable = false
|
||||
}
|
||||
forcedev = false
|
||||
play {
|
||||
|
@ -217,6 +218,7 @@ bookmark {
|
|||
}
|
||||
analyse {
|
||||
collection.analysis = analysis2
|
||||
collection.requester = analysis_requester
|
||||
net.domain = ${net.domain}
|
||||
cached.nb.ttl = ${game.cached.nb.ttl}
|
||||
paginator.max_per_page = ${game.paginator.max_per_page}
|
||||
|
@ -253,13 +255,14 @@ security {
|
|||
refresh_delay = 1 hour
|
||||
}
|
||||
disposable_email {
|
||||
provider_url = "https://raw.githubusercontent.com/ornicar/disposable-email-domains/master/index.json"
|
||||
provider_url = "https://raw.githubusercontent.com/ornicar/disposable-email-domains/master/list"
|
||||
refresh_delay = 10 minutes
|
||||
}
|
||||
recaptcha = ${recaptcha}
|
||||
whois {
|
||||
key = "matewithknightandbishop"
|
||||
}
|
||||
net.domain = ${net.domain}
|
||||
}
|
||||
recaptcha {
|
||||
endpoint = "https://www.google.com/recaptcha/api/siteverify"
|
||||
|
@ -394,6 +397,7 @@ tv {
|
|||
google.api_key = ""
|
||||
keyword = "lichess.org"
|
||||
hitbox.url = "http://api.hitbox.tv/media/live/"
|
||||
twitch.client_id = ""
|
||||
}
|
||||
}
|
||||
explorer {
|
||||
|
|
|
@ -26,6 +26,13 @@
|
|||
<!-- </appender> -->
|
||||
<!-- </logger> -->
|
||||
|
||||
<logger name="reactivemongo" level="DEBUG">
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${application.home}/logs/reactivemongo.log</file>
|
||||
<encoder><pattern>%date [%level] %message%n%xException</pattern></encoder>
|
||||
</appender>
|
||||
</logger>
|
||||
|
||||
<logger name="lobby" level="DEBUG">
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${application.home}/logs/lobby.log</file>
|
||||
|
@ -56,6 +63,12 @@
|
|||
<encoder><pattern>%date [%level] %message%n%xException</pattern></encoder>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="csrf" level="DEBUG">
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${application.home}/logs/csrf.log</file>
|
||||
<encoder><pattern>%date [%level] %message%n%xException</pattern></encoder>
|
||||
</appender>
|
||||
</logger>
|
||||
|
||||
<!-- <logger name="java.io.IOException" level="OFF" /> -->
|
||||
<!-- <logger name="java.nio.channels.ClosedChannelException" level="OFF" /> -->
|
||||
|
|
|
@ -125,6 +125,16 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="csrf" level="DEBUG">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/csrf.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>/var/log/lichess/csrf-log-%d{yyyy-MM-dd}.gz</fileNamePattern>
|
||||
<maxHistory>7</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
|
||||
<!-- Set a specific actor to DEBUG -->
|
||||
<!-- <logger name="actors.MyActor" level="DEBUG" /> -->
|
||||
|
|
|
@ -357,11 +357,13 @@ GET /forum/search controllers.ForumPost.search(text: String
|
|||
GET /forum/:slug controllers.ForumCateg.show(slug: String, page: Int ?= 1)
|
||||
GET /forum/:categSlug/form controllers.ForumTopic.form(categSlug: String)
|
||||
POST /forum/:categSlug/new controllers.ForumTopic.create(categSlug: String)
|
||||
GET /forum/participants/:topicId controllers.ForumTopic.participants(topicId: String)
|
||||
GET /forum/:categSlug/:slug controllers.ForumTopic.show(categSlug: String, slug: String, page: Int ?= 1)
|
||||
POST /forum/:categSlug/:slug/close controllers.ForumTopic.close(categSlug: String, slug: String)
|
||||
POST /forum/:categSlug/:slug/hide controllers.ForumTopic.hide(categSlug: String, slug: String)
|
||||
POST /forum/:categSlug/:slug/new controllers.ForumPost.create(categSlug: String, slug: String, page: Int ?= 1)
|
||||
POST /forum/:categSlug/delete/:id controllers.ForumPost.delete(categSlug: String, id: String)
|
||||
POST /forum/post/:id controllers.ForumPost.edit(id: String)
|
||||
GET /forum/redirect/post/:id controllers.ForumPost.redirect(id: String)
|
||||
|
||||
# Message
|
||||
|
@ -437,6 +439,7 @@ GET /api/game/:id controllers.Api.game(id: String)
|
|||
GET /api/tournament controllers.Api.currentTournaments
|
||||
GET /api/tournament/:id controllers.Api.tournament(id: String)
|
||||
GET /api/status controllers.Api.status
|
||||
GET /api/socket controllers.Main.apiWebsocket
|
||||
|
||||
# Events
|
||||
GET /event controllers.Event.index
|
||||
|
@ -481,8 +484,13 @@ GET /help/contribute controllers.Page.contribute
|
|||
GET /help/master controllers.Page.master
|
||||
GET /help/stream-on-lichess controllers.Page.streamHowTo
|
||||
|
||||
# Graphite
|
||||
GET /monitor/coach/pageview controllers.Monitor.coachPageView
|
||||
|
||||
POST /jslog/$id<\w{12}> controllers.Main.jslog(id)
|
||||
|
||||
# Assets
|
||||
GET /glyphs controllers.Main.glyphs
|
||||
GET /assets/*file controllers.Assets.at(path="/public", file)
|
||||
|
||||
GET /robots.txt controllers.Main.robots
|
||||
|
|
|
@ -3,12 +3,14 @@ package lila.analyse
|
|||
import akka.actor.ActorSelection
|
||||
|
||||
import chess.format.FEN
|
||||
import lila.db.dsl._
|
||||
import lila.game.actorApi.InsertGame
|
||||
import lila.game.{ Game, GameRepo }
|
||||
import lila.hub.actorApi.map.Tell
|
||||
|
||||
final class Analyser(
|
||||
indexer: ActorSelection,
|
||||
requesterApi: RequesterApi,
|
||||
roundSocket: ActorSelection,
|
||||
bus: lila.common.Bus) {
|
||||
|
||||
|
@ -21,6 +23,7 @@ final class Analyser(
|
|||
sendAnalysisProgress(analysis) >>- {
|
||||
bus.publish(actorApi.AnalysisReady(game, analysis), 'analysisReady)
|
||||
indexer ! InsertGame(game)
|
||||
requesterApi save analysis
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,19 @@ final class Env(
|
|||
indexer: ActorSelection) {
|
||||
|
||||
private val CollectionAnalysis = config getString "collection.analysis"
|
||||
private val CollectionRequester = config getString "collection.requester"
|
||||
private val NetDomain = config getString "net.domain"
|
||||
private val CachedNbTtl = config duration "cached.nb.ttl"
|
||||
private val PaginatorMaxPerPage = config getInt "paginator.max_per_page"
|
||||
private val ActorName = config getString "actor.name"
|
||||
|
||||
private[analyse] lazy val analysisColl = db(CollectionAnalysis)
|
||||
lazy val analysisColl = db(CollectionAnalysis)
|
||||
|
||||
lazy val requesterApi = new RequesterApi(db(CollectionRequester))
|
||||
|
||||
lazy val analyser = new Analyser(
|
||||
indexer = indexer,
|
||||
requesterApi = requesterApi,
|
||||
roundSocket = roundSocket,
|
||||
bus = system.lilaBus)
|
||||
|
||||
|
|
24
modules/analyse/src/main/RequesterApi.scala
Normal file
24
modules/analyse/src/main/RequesterApi.scala
Normal file
|
@ -0,0 +1,24 @@
|
|||
package lila.analyse
|
||||
|
||||
import org.joda.time._
|
||||
|
||||
import lila.db.dsl._
|
||||
import lila.user.User
|
||||
|
||||
final class RequesterApi(coll: Coll) {
|
||||
|
||||
private val formatter = format.DateTimeFormat.forPattern("YYYY-MM-dd")
|
||||
|
||||
private def today = formatter.print(DateTime.now)
|
||||
|
||||
def save(analysis: Analysis): Funit = coll.update(
|
||||
$id(analysis.uid | "anonymous"),
|
||||
$inc("total" -> 1) ++
|
||||
$inc(today -> 1) ++
|
||||
$set("last" -> analysis.id),
|
||||
upsert = true
|
||||
).void
|
||||
|
||||
def countToday(userId: User.ID): Fu[Int] =
|
||||
coll.primitiveOne[Int]($id(userId), today) map (~_)
|
||||
}
|
|
@ -4,9 +4,9 @@ import play.api.libs.json.{ JsObject, JsArray }
|
|||
import play.api.mvc.{ Request, RequestHeader }
|
||||
|
||||
import lila.common.HTTPRequest
|
||||
import lila.hub.actorApi.relation.OnlineFriends
|
||||
import lila.pref.Pref
|
||||
import lila.user.{ UserContext, HeaderUserContext, BodyUserContext }
|
||||
import lila.hub.actorApi.relation.OnlineFriends
|
||||
|
||||
case class PageData(
|
||||
onlineFriends: OnlineFriends,
|
||||
|
@ -69,7 +69,7 @@ sealed trait Context extends lila.user.UserContextWrapper {
|
|||
def requiresFingerprint = isAuth && !pageData.hasFingerprint
|
||||
|
||||
private def ctxPref(name: String): Option[String] =
|
||||
userContext.req.session get name orElse { pref get name }
|
||||
req.session get name orElse { pref get name }
|
||||
}
|
||||
|
||||
sealed abstract class BaseContext(
|
||||
|
|
|
@ -44,6 +44,7 @@ final class Env(
|
|||
val AssetDomain = config getString "net.asset.domain"
|
||||
val AssetVersion = config getInt "net.asset.version"
|
||||
val Email = config getString "net.email"
|
||||
val Crawlable = config getBoolean "net.crawlable"
|
||||
}
|
||||
val PrismicApiUrl = config getString "prismic.api_url"
|
||||
val EditorAnimationDuration = config duration "editor.animation.duration"
|
||||
|
|
|
@ -37,40 +37,40 @@ private[api] final class GameApi(
|
|||
nb: Int,
|
||||
page: Int): Fu[JsObject] = Paginator(
|
||||
adapter = new CachedAdapter(
|
||||
adapter = new Adapter[Game](
|
||||
collection = GameRepo.coll,
|
||||
selector = {
|
||||
if (~playing) lila.game.Query.nowPlaying(user.id)
|
||||
else $doc(
|
||||
G.playerUids -> user.id,
|
||||
G.status $gte chess.Status.Mate.id,
|
||||
G.analysed -> analysed.map(_.fold[BSONValue](BSONBoolean(true), $doc("$exists" -> false)))
|
||||
)
|
||||
} ++ $doc(
|
||||
G.rated -> rated.map(_.fold[BSONValue](BSONBoolean(true), $doc("$exists" -> false)))
|
||||
),
|
||||
projection = $empty,
|
||||
sort = $doc(G.createdAt -> -1),
|
||||
readPreference = ReadPreference.secondaryPreferred
|
||||
),
|
||||
nbResults =
|
||||
if (~playing) gameCache.nbPlaying(user.id)
|
||||
else fuccess {
|
||||
rated.fold(user.count.game)(_.fold(user.count.rated, user.count.casual))
|
||||
}
|
||||
),
|
||||
adapter = new Adapter[Game](
|
||||
collection = GameRepo.coll,
|
||||
selector = {
|
||||
if (~playing) lila.game.Query.nowPlaying(user.id)
|
||||
else $doc(
|
||||
G.playerUids -> user.id,
|
||||
G.status $gte chess.Status.Mate.id,
|
||||
G.analysed -> analysed.map(_.fold[BSONValue](BSONBoolean(true), $doc("$exists" -> false)))
|
||||
)
|
||||
} ++ $doc(
|
||||
G.rated -> rated.map(_.fold[BSONValue](BSONBoolean(true), $doc("$exists" -> false)))
|
||||
),
|
||||
projection = $empty,
|
||||
sort = $doc(G.createdAt -> -1),
|
||||
readPreference = ReadPreference.secondaryPreferred
|
||||
),
|
||||
nbResults =
|
||||
if (~playing) gameCache.nbPlaying(user.id)
|
||||
else fuccess {
|
||||
rated.fold(user.count.game)(_.fold(user.count.rated, user.count.casual))
|
||||
}
|
||||
),
|
||||
currentPage = page,
|
||||
maxPerPage = nb) flatMap { pag =>
|
||||
gamesJson(
|
||||
withAnalysis = withAnalysis,
|
||||
withMoves = withMoves,
|
||||
withOpening = withOpening,
|
||||
withFens = false,
|
||||
withMoveTimes = withMoveTimes,
|
||||
token = token)(pag.currentPageResults) map { games =>
|
||||
PaginatorJson(pag withCurrentPageResults games)
|
||||
}
|
||||
gamesJson(
|
||||
withAnalysis = withAnalysis,
|
||||
withMoves = withMoves,
|
||||
withOpening = withOpening,
|
||||
withFens = false,
|
||||
withMoveTimes = withMoveTimes,
|
||||
token = token)(pag.currentPageResults) map { games =>
|
||||
PaginatorJson(pag withCurrentPageResults games)
|
||||
}
|
||||
}
|
||||
|
||||
def one(
|
||||
id: String,
|
||||
|
@ -177,7 +177,11 @@ private[api] final class GameApi(
|
|||
"moves" -> withMoves.option(g.pgnMoves mkString " "),
|
||||
"opening" -> withOpening.??(g.opening),
|
||||
"fens" -> (withFens && g.finished) ?? {
|
||||
chess.Replay.boards(g.pgnMoves, initialFen, g.variant).toOption map { boards =>
|
||||
chess.Replay.boards(
|
||||
moveStrs = g.pgnMoves,
|
||||
initialFen = initialFen map chess.format.FEN,
|
||||
variant = g.variant
|
||||
).toOption map { boards =>
|
||||
JsArray(boards map chess.format.Forsyth.exportBoard map JsString.apply)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -29,9 +29,9 @@ final class BookmarkApi(
|
|||
user ?? { u =>
|
||||
val candidateIds = games.filter(_.bookmarks > 0).map(_.id)
|
||||
if (candidateIds.isEmpty) fuccess(Set.empty)
|
||||
else coll.distinct("g", Some(
|
||||
else coll.distinct[String, Set]("g", Some(
|
||||
userIdQuery(u.id) ++ $doc("g" $in candidateIds)
|
||||
)) map lila.db.BSON.asStringSet
|
||||
))
|
||||
}
|
||||
|
||||
def removeByGameId(gameId: String): Funit =
|
||||
|
|
|
@ -53,7 +53,7 @@ private final class ChallengeRepo(coll: Coll, maxPerUser: Int) {
|
|||
)).cursor[Challenge]().gather[List](max)
|
||||
|
||||
private[challenge] def expired(max: Int): Fu[List[Challenge]] =
|
||||
coll.find($doc("expiresAt" -> $doc("$lt" -> DateTime.now))).list[Challenge](max)
|
||||
coll.find($doc("expiresAt" -> $lt(DateTime.now))).list[Challenge](max)
|
||||
|
||||
def setSeenAgain(id: Challenge.ID) = coll.update(
|
||||
$id(id),
|
||||
|
|
|
@ -25,7 +25,7 @@ private[challenge] final class SocketHandler(
|
|||
owner: Boolean): Fu[Option[JsSocketHandler]] = for {
|
||||
socket ← socketHub ? Get(challengeId) mapTo manifest[ActorRef]
|
||||
join = Socket.Join(uid = uid, userId = userId, owner = owner)
|
||||
handler ← Handler(hub, socket, uid, join, userId) {
|
||||
handler ← Handler(hub, socket, uid, join) {
|
||||
case Socket.Connected(enum, member) =>
|
||||
(controller(socket, challengeId, uid, member), enum, member)
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 8c4592ad012f2f4e1e2631ea2db64f6cde9698a3
|
||||
Subproject commit a9df5e707a86565e192958b3d5574bf9510bd1b5
|
|
@ -104,11 +104,14 @@ final class CoachApi(
|
|||
approved = false,
|
||||
updatedAt = DateTime.now)
|
||||
}
|
||||
reviewColl.update($id(id), review, upsert = true) >>
|
||||
notifyApi.addNotification(Notification(
|
||||
notifies = Notification.Notifies(coach.id.value),
|
||||
content = lila.notify.CoachReview
|
||||
)) >> refreshCoachNbReviews(coach.id) inject review
|
||||
if (me.troll) fuccess(review)
|
||||
else {
|
||||
reviewColl.update($id(id), review, upsert = true) >>
|
||||
notifyApi.addNotification(Notification(
|
||||
notifies = Notification.Notifies(coach.id.value),
|
||||
content = lila.notify.CoachReview
|
||||
)) >> refreshCoachNbReviews(coach.id) inject review
|
||||
}
|
||||
}
|
||||
|
||||
def byId(id: String) = reviewColl.byId[CoachReview](id)
|
||||
|
|
|
@ -11,7 +11,7 @@ object UrlList {
|
|||
def apply(text: String): List[Url] =
|
||||
text.lines.toList.map(_.trim).filter(_.nonEmpty) flatMap toUrl take max
|
||||
|
||||
private val UrlRegex = """.*(?:youtube\.com|youtu\.be)/(?:watch)?(?:\?v=)(.+)$""".r
|
||||
private val UrlRegex = """.*(?:youtube\.com|youtu\.be)/(?:watch)?(?:\?v=)?([^"&?\/ ]{11}).*""".r
|
||||
|
||||
/*
|
||||
* https://www.youtube.com/watch?v=wEwoyYp_iw8
|
|
@ -9,11 +9,12 @@ object HTTPRequest {
|
|||
(req.headers get "X-Requested-With") contains "XMLHttpRequest"
|
||||
|
||||
def isSocket(req: RequestHeader): Boolean =
|
||||
(req.headers get HeaderNames.UPGRADE) ?? (_.toLowerCase == "websocket")
|
||||
(req.headers get HeaderNames.UPGRADE).exists(_.toLowerCase == "websocket")
|
||||
|
||||
def isSynchronousHttp(req: RequestHeader) = !isXhr(req) && !isSocket(req)
|
||||
|
||||
def isSafe(req: RequestHeader) = req.method == "GET"
|
||||
def isSafe(req: RequestHeader) = req.method == "GET" || req.method == "HEAD" || req.method == "OPTIONS"
|
||||
def isUnsafe(req: RequestHeader) = !isSafe(req)
|
||||
|
||||
def isRedirectable(req: RequestHeader) = isSynchronousHttp(req) && isSafe(req)
|
||||
|
||||
|
@ -28,6 +29,8 @@ object HTTPRequest {
|
|||
def isChrome(req: RequestHeader) = uaContains(req, "Chrome/")
|
||||
def isSafari(req: RequestHeader) = uaContains(req, "Safari/") && !isChrome(req)
|
||||
|
||||
def origin(req: RequestHeader): Option[String] = req.headers get HeaderNames.ORIGIN
|
||||
|
||||
def referer(req: RequestHeader): Option[String] = req.headers get HeaderNames.REFERER
|
||||
|
||||
def lastRemoteAddress(req: RequestHeader): String =
|
||||
|
@ -56,4 +59,7 @@ object HTTPRequest {
|
|||
|
||||
def hasFileExtension(req: RequestHeader) =
|
||||
fileExtensionPattern.matcher(req.path).matches
|
||||
|
||||
def print(req: RequestHeader) =
|
||||
s"${req.method} ${req.domain}${req.uri} ${lastRemoteAddress(req)} origin:${~origin(req)} referer:${~referer(req)} ua:${~userAgent(req)}"
|
||||
}
|
||||
|
|
|
@ -36,4 +36,7 @@ object Maths {
|
|||
val s = math.pow(10, p)
|
||||
(math floor n * s) / s
|
||||
}
|
||||
|
||||
def toInt(l: Long): Int = l.min(Int.MaxValue).max(Int.MinValue).toInt
|
||||
def toInt(l: Option[Long]): Option[Int] = l map toInt
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ object mon {
|
|||
object http {
|
||||
object request {
|
||||
val all = inc("http.request.all")
|
||||
val ipv6 = inc("http.request.ipv6")
|
||||
}
|
||||
object response {
|
||||
val code400 = inc("http.response.4.00")
|
||||
|
@ -45,7 +46,12 @@ object mon {
|
|||
val timeout = inc("http.mailgun.timeout")
|
||||
}
|
||||
object userGames {
|
||||
def cost = incX(s"http.user-games.cost")
|
||||
def cost = incX("http.user-games.cost")
|
||||
}
|
||||
object csrf {
|
||||
val missingOrigin = inc("http.csrf.missing_origin")
|
||||
val forbidden = inc("http.csrf.forbidden")
|
||||
val websocket = inc("http.csrf.websocket")
|
||||
}
|
||||
}
|
||||
object lobby {
|
||||
|
@ -63,6 +69,11 @@ object mon {
|
|||
val member = rec("lobby.socket.member")
|
||||
val resync = inc("lobby.socket.resync")
|
||||
}
|
||||
object cache {
|
||||
val user = inc("lobby.cache.count.user")
|
||||
val anon = inc("lobby.cache.count.anon")
|
||||
val miss = inc("lobby.cache.count.miss")
|
||||
}
|
||||
}
|
||||
object round {
|
||||
object api {
|
||||
|
@ -228,6 +239,12 @@ object mon {
|
|||
val created = rec("tournament.created")
|
||||
val started = rec("tournament.started")
|
||||
val player = rec("tournament.player")
|
||||
object startedOrganizer {
|
||||
val tickTime = rec("tournament.started_organizer.tick_time")
|
||||
}
|
||||
object createdOrganizer {
|
||||
val tickTime = rec("tournament.created_organizer.tick_time")
|
||||
}
|
||||
}
|
||||
object donation {
|
||||
val goal = rec("donation.goal")
|
||||
|
@ -259,6 +276,7 @@ object mon {
|
|||
object selector {
|
||||
val count = inc("puzzle.selector")
|
||||
val time = rec("puzzle.selector")
|
||||
def vote(v: Int) = rec("puzzle.selector.vote")(1000 + v) // vote sum of selected puzzle
|
||||
}
|
||||
object attempt {
|
||||
val user = inc("puzzle.attempt.user")
|
||||
|
@ -356,6 +374,8 @@ object mon {
|
|||
def totalMeganode = incX(s"fishnet.analysis.total.meganode.$client")
|
||||
def totalSecond = incX(s"fishnet.analysis.total.second.$client")
|
||||
def totalPosition = incX(s"fishnet.analysis.total.position.$client")
|
||||
def endgameCount = incX(s"fishnet.analysis.total.endgame.count.$client")
|
||||
def endgameTime = incX(s"fishnet.analysis.total.endgame.time.$client")
|
||||
}
|
||||
val post = rec("fishnet.analysis.post")
|
||||
}
|
||||
|
|
|
@ -196,23 +196,6 @@ object BSON extends Handlers {
|
|||
def debugDoc(doc: Bdoc): String = (doc.elements.toList map {
|
||||
case (k, v) => s"$k: ${debug(v)}"
|
||||
}).mkString("{", ", ", "}")
|
||||
|
||||
def hashDoc(doc: Bdoc): String = debugDoc(doc).replace(" ", "")
|
||||
|
||||
def asStrings(vs: List[BSONValue]): List[String] = {
|
||||
val b = new scala.collection.mutable.ListBuffer[String]
|
||||
vs foreach {
|
||||
case BSONString(s) => b += s
|
||||
case _ =>
|
||||
}
|
||||
b.toList
|
||||
}
|
||||
|
||||
def asStringSet(vs: List[BSONValue]): Set[String] = {
|
||||
val b = Set.newBuilder[String]
|
||||
vs foreach {
|
||||
case BSONString(s) => b += s
|
||||
case _ =>
|
||||
}
|
||||
b.result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,10 +34,13 @@ trait CollExt { self: dsl with QueryBuilderExt =>
|
|||
def exists(selector: Bdoc): Fu[Boolean] = countSel(selector).map(0!=)
|
||||
|
||||
def byOrderedIds[D: BSONDocumentReader](ids: Iterable[String], readPreference: ReadPreference = ReadPreference.primary)(docId: D => String): Fu[List[D]] =
|
||||
coll.find($inIds(ids)).cursor[D](readPreference = readPreference).collect[List]() map { docs =>
|
||||
val docsMap = docs.map(u => docId(u) -> u).toMap
|
||||
ids.flatMap(docsMap.get).toList
|
||||
}
|
||||
coll.find($inIds(ids)).cursor[D](readPreference = readPreference).
|
||||
collect[List](Int.MaxValue, err = Cursor.FailOnError[List[D]]()).
|
||||
map { docs =>
|
||||
val docsMap = docs.map(u => docId(u) -> u).toMap
|
||||
ids.flatMap(docsMap.get).toList
|
||||
}
|
||||
|
||||
// def byOrderedIds[A <: Identified[String]: TubeInColl](ids: Iterable[String]): Fu[List[A]] =
|
||||
// byOrderedIds[String, A](ids)
|
||||
|
||||
|
|
|
@ -7,11 +7,11 @@ import reactivemongo.bson._
|
|||
|
||||
trait CursorExt { self: dsl =>
|
||||
|
||||
// Can be refactor as CursorProducer
|
||||
final implicit class ExtendCursor[A: BSONDocumentReader](val c: Cursor[A]) {
|
||||
|
||||
// like collect, but with stopOnError defaulting to false
|
||||
def gather[M[_]](upTo: Int = Int.MaxValue)(implicit cbf: CanBuildFrom[M[_], A, M[A]]): Fu[M[A]] =
|
||||
c.collect[M](upTo, stopOnError = false)
|
||||
def gather[M[_]](upTo: Int = Int.MaxValue)(implicit cbf: CanBuildFrom[M[_], A, M[A]]): Fu[M[A]] = c.collect[M](upTo, Cursor.ContOnError[M[A]]())
|
||||
|
||||
def list(limit: Option[Int]): Fu[List[A]] = gather[List](limit | Int.MaxValue)
|
||||
|
||||
|
@ -20,7 +20,7 @@ trait CursorExt { self: dsl =>
|
|||
def list(): Fu[List[A]] = list(none)
|
||||
|
||||
// like headOption, but with stopOnError defaulting to false
|
||||
def uno: Fu[Option[A]] =
|
||||
c.collect[Iterable](1, stopOnError = false).map(_.headOption)
|
||||
def uno: Fu[Option[A]] = c.collect[Iterable](
|
||||
1, Cursor.ContOnError[Iterable[A]]()).map(_.headOption)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,64 @@
|
|||
package lila.db
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import reactivemongo.api._
|
||||
import dsl.Coll
|
||||
import reactivemongo.api.{ DefaultDB, MongoConnection, MongoDriver }
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{ Success, Failure }
|
||||
import dsl._
|
||||
import scala.concurrent.{ Await, ExecutionContext, Future }
|
||||
import scala.util.{ Failure, Success, Try }
|
||||
|
||||
final class Env(
|
||||
name: String,
|
||||
config: Config,
|
||||
lifecycle: play.api.inject.ApplicationLifecycle) {
|
||||
|
||||
lazy val db = {
|
||||
val parsedUri: MongoConnection.ParsedURI =
|
||||
MongoConnection.parseURI(config.getString("uri")) match {
|
||||
case Success(parsedURI) => parsedURI
|
||||
case Failure(e) => sys error s"Invalid mongodb.uri"
|
||||
}
|
||||
lazy val (connection, dbName) = {
|
||||
val driver = new MongoDriver(Some(config))
|
||||
val connection = driver.connection(parsedUri)
|
||||
|
||||
parsedUri.db.fold[DefaultDB](sys error s"cannot resolve database from URI: $parsedUri") { dbUri =>
|
||||
val db = DB(dbUri, connection)
|
||||
registerDriverShutdownHook(driver)
|
||||
logger.info(s"""ReactiveMongoApi successfully started with DB '$dbUri'! Servers: ${parsedUri.hosts.map { s => s"[${s._1}:${s._2}]" }.mkString("\n\t\t")}""")
|
||||
db
|
||||
}
|
||||
registerDriverShutdownHook(driver)
|
||||
|
||||
(for {
|
||||
parsedUri <- MongoConnection.parseURI(config getString "uri")
|
||||
con <- driver.connection(parsedUri, true)
|
||||
db <- parsedUri.db match {
|
||||
case Some(name) => Success(name)
|
||||
case _ => Failure[String](new IllegalArgumentException(
|
||||
s"cannot resolve connection from URI: $parsedUri"))
|
||||
}
|
||||
} yield con -> db).get
|
||||
}
|
||||
|
||||
def apply(name: String): Coll = db(name)
|
||||
private lazy val lnm = s"${connection.supervisor}/${connection.name}"
|
||||
|
||||
@inline private def resolveDB(ec: ExecutionContext) =
|
||||
connection.database(dbName)(ec).andThen {
|
||||
case _ => /*logger.debug*/ println(s"[$lnm] MongoDB resolved: $dbName")
|
||||
}
|
||||
|
||||
def db(implicit ec: ExecutionContext): DefaultDB =
|
||||
Await.result(resolveDB(ec), 10.seconds)
|
||||
|
||||
def apply(name: String)(implicit ec: ExecutionContext): Coll =
|
||||
db(ec).apply(name)
|
||||
|
||||
object image {
|
||||
private lazy val imageColl = apply(config getString "image.collection")
|
||||
import dsl._
|
||||
import DbImage.DbImageBSONHandler
|
||||
def fetch(id: String): Fu[Option[DbImage]] = imageColl.byId[DbImage](id)
|
||||
}
|
||||
|
||||
private def registerDriverShutdownHook(mongoDriver: MongoDriver): Unit =
|
||||
lifecycle.addStopHook { () => Future(mongoDriver.close()) }
|
||||
lifecycle.addStopHook { () =>
|
||||
logger.info(s"[$lnm] Stopping the MongoDriver...")
|
||||
Future(mongoDriver.close())
|
||||
}
|
||||
}
|
||||
|
||||
object Env {
|
||||
|
||||
lazy val current = "db" boot new Env(
|
||||
name = "main",
|
||||
config = lila.common.PlayApp loadConfig "mongodb",
|
||||
lifecycle = lila.common.PlayApp.lifecycle)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ trait QueryBuilderExt { self: dsl =>
|
|||
|
||||
// like collect, but with stopOnError defaulting to false
|
||||
def gather[A, M[_]](upTo: Int = Int.MaxValue)(implicit cbf: CanBuildFrom[M[_], A, M[A]], reader: BSONDocumentReader[A]): Fu[M[A]] =
|
||||
b.cursor[A]().collect[M](upTo, stopOnError = false)
|
||||
b.cursor[A]().collect[M](upTo, Cursor.ContOnError[M[A]]())
|
||||
|
||||
def list[A: BSONDocumentReader](limit: Option[Int]): Fu[List[A]] = gather[A, List](limit | Int.MaxValue)
|
||||
|
||||
|
@ -27,10 +27,9 @@ trait QueryBuilderExt { self: dsl =>
|
|||
// like one, but with stopOnError defaulting to false
|
||||
def uno[A: BSONDocumentReader]: Fu[Option[A]] = uno[A](ReadPreference.primary)
|
||||
|
||||
def uno[A: BSONDocumentReader](readPreference: ReadPreference): Fu[Option[A]] =
|
||||
b.copy(options = b.options.batchSize(1))
|
||||
.cursor[A](readPreference = readPreference)
|
||||
.collect[Iterable](1, stopOnError = false)
|
||||
.map(_.headOption)
|
||||
def uno[A: BSONDocumentReader](readPreference: ReadPreference): Fu[Option[A]] = b.copy(options = b.options.batchSize(1))
|
||||
.cursor[A](readPreference = readPreference)
|
||||
.collect[Iterable](1, Cursor.ContOnError[Iterable[A]]())
|
||||
.map(_.headOption)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,10 +55,11 @@ case class Assessible(analysed: Analysed) {
|
|||
// SF1 SF2 BLR1 BLR2 MTs1 MTs2 Holds
|
||||
case PlayerFlags(T, T, T, T, T, T, T) => Cheating // all T, obvious cheat
|
||||
case PlayerFlags(T, _, T, _, _, T, _) => Cheating // high accuracy, high blurs, no fast moves
|
||||
|
||||
case PlayerFlags(T, _, _, T, _, _, _) => Cheating // high accuracy, moderate blurs
|
||||
|
||||
case PlayerFlags(_, _, _, T, T, _, _) => LikelyCheating // high accuracy, moderate blurs => 93% chance cheating
|
||||
case PlayerFlags(T, _, _, _, _, _, T) => LikelyCheating // Holds are bad, hmk?
|
||||
case PlayerFlags(_, T, _, _, _, _, T) => LikelyCheating // Holds are bad, hmk?
|
||||
case PlayerFlags(T, _, _, T, _, T, _) => LikelyCheating // high accuracy, moderate blurs, no fast moves
|
||||
case PlayerFlags(_, T, _, T, T, _, _) => LikelyCheating // always has advantage, moderate blurs, highly consistent move times
|
||||
case PlayerFlags(_, T, T, _, _, _, _) => LikelyCheating // always has advantage, high blurs
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import lila.common.PimpedConfig._
|
|||
final class Env(
|
||||
config: Config,
|
||||
uciMemo: lila.game.UciMemo,
|
||||
requesterApi: lila.analyse.RequesterApi,
|
||||
hub: lila.hub.Env,
|
||||
db: lila.db.Env,
|
||||
system: ActorSystem,
|
||||
|
@ -48,11 +49,11 @@ final class Env(
|
|||
monitor = monitor,
|
||||
sink = sink,
|
||||
socketExists = id => {
|
||||
import lila.hub.actorApi.map.Exists
|
||||
import akka.pattern.ask
|
||||
import makeTimeout.short
|
||||
hub.socket.round ? Exists(id) mapTo manifest[Boolean]
|
||||
},
|
||||
import lila.hub.actorApi.map.Exists
|
||||
import akka.pattern.ask
|
||||
import makeTimeout.short
|
||||
hub.socket.round ? Exists(id) mapTo manifest[Boolean]
|
||||
},
|
||||
offlineMode = OfflineMode,
|
||||
analysisNodes = AnalysisNodes)(system)
|
||||
|
||||
|
@ -61,11 +62,15 @@ final class Env(
|
|||
uciMemo = uciMemo,
|
||||
maxPlies = MovePlies)
|
||||
|
||||
private val limiter = new Limiter(
|
||||
analysisColl = analysisColl,
|
||||
requesterApi = requesterApi)
|
||||
|
||||
val analyser = new Analyser(
|
||||
repo = repo,
|
||||
uciMemo = uciMemo,
|
||||
sequencer = sequencer,
|
||||
limiter = new Limiter(analysisColl))
|
||||
limiter = limiter)
|
||||
|
||||
val aiPerfApi = new AiPerfApi
|
||||
|
||||
|
@ -110,6 +115,7 @@ object Env {
|
|||
lazy val current: Env = "fishnet" boot new Env(
|
||||
system = lila.common.PlayApp.system,
|
||||
uciMemo = lila.game.Env.current.uciMemo,
|
||||
requesterApi = lila.analyse.Env.current.requesterApi,
|
||||
hub = lila.hub.Env.current,
|
||||
db = lila.db.Env.current,
|
||||
config = lila.common.PlayApp loadConfig "fishnet",
|
||||
|
|
|
@ -6,6 +6,7 @@ import play.api.libs.json._
|
|||
import chess.format.{ Uci, Forsyth, FEN }
|
||||
import chess.variant.Variant
|
||||
|
||||
import lila.common.Maths
|
||||
import lila.fishnet.{ Work => W }
|
||||
|
||||
object JsonApi {
|
||||
|
@ -80,7 +81,7 @@ object JsonApi {
|
|||
def medianNodes = analysis
|
||||
.filterNot(_.mateFound)
|
||||
.filterNot(_.deadDraw)
|
||||
.flatMap(_.nodes).toNel map lila.common.Maths.median[Int]
|
||||
.flatMap(_.nodes).toNel map Maths.median[Int]
|
||||
|
||||
def strong = medianNodes.fold(true)(_ > Evaluation.acceptableNodes)
|
||||
def weak = !strong
|
||||
|
@ -153,6 +154,7 @@ object JsonApi {
|
|||
def analysisFromWork(nodes: Int)(m: Work.Analysis) = Analysis(m.id.value, fromGame(m.game), nodes)
|
||||
|
||||
object readers {
|
||||
import play.api.libs.functional.syntax._
|
||||
implicit val ClientVersionReads = Reads.of[String].map(new Client.Version(_))
|
||||
implicit val ClientPythonReads = Reads.of[String].map(new Client.Python(_))
|
||||
implicit val ClientKeyReads = Reads.of[String].map(new Client.Key(_))
|
||||
|
@ -164,7 +166,14 @@ object JsonApi {
|
|||
implicit val MoveResultReads = Json.reads[Request.MoveResult]
|
||||
implicit val PostMoveReads = Json.reads[Request.PostMove]
|
||||
implicit val ScoreReads = Json.reads[Request.Evaluation.Score]
|
||||
implicit val EvaluationReads = Json.reads[Request.Evaluation]
|
||||
implicit val EvaluationReads: Reads[Request.Evaluation] = (
|
||||
(__ \ "pv").readNullable[String] and
|
||||
(__ \ "score").read[Request.Evaluation.Score] and
|
||||
(__ \ "time").readNullable[Int] and
|
||||
(__ \ "nodes").readNullable[Long].map(Maths.toInt) and
|
||||
(__ \ "nps").readNullable[Long].map(Maths.toInt) and
|
||||
(__ \ "depth").readNullable[Int]
|
||||
)(Request.Evaluation.apply _)
|
||||
implicit val EvaluationOptionReads = Reads[Option[Request.Evaluation]] {
|
||||
case JsNull => JsSuccess(None)
|
||||
case obj => EvaluationReads reads obj map some
|
||||
|
|
|
@ -1,12 +1,27 @@
|
|||
package lila.fishnet
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import reactivemongo.bson._
|
||||
|
||||
import lila.db.dsl.Coll
|
||||
|
||||
private final class Limiter(analysisColl: Coll) {
|
||||
private final class Limiter(
|
||||
analysisColl: Coll,
|
||||
requesterApi: lila.analyse.RequesterApi) {
|
||||
|
||||
def apply(sender: Work.Sender): Fu[Boolean] = sender match {
|
||||
def apply(sender: Work.Sender): Fu[Boolean] =
|
||||
concurrentCheck(sender) flatMap {
|
||||
case false => fuccess(false)
|
||||
case true => perDayCheck(sender)
|
||||
}
|
||||
|
||||
private val RequestLimitPerIP = new lila.memo.RateLimit(
|
||||
credits = 50,
|
||||
duration = 20 hours,
|
||||
name = "request analysis per IP",
|
||||
key = "request_analysis.ip")
|
||||
|
||||
private def concurrentCheck(sender: Work.Sender) = sender match {
|
||||
case Work.Sender(_, _, mod, system) if (mod || system) => fuccess(true)
|
||||
case Work.Sender(Some(userId), _, _, _) => analysisColl.count(BSONDocument(
|
||||
"sender.userId" -> userId
|
||||
|
@ -16,4 +31,13 @@ private final class Limiter(analysisColl: Coll) {
|
|||
).some) map (0 ==)
|
||||
case _ => fuccess(false)
|
||||
}
|
||||
|
||||
private def perDayCheck(sender: Work.Sender) = sender match {
|
||||
case Work.Sender(_, _, mod, system) if (mod || system) => fuccess(true)
|
||||
case Work.Sender(Some(userId), _, _, _) => requesterApi.countToday(userId) map (_ < 25)
|
||||
case Work.Sender(_, Some(ip), _, _) => fuccess {
|
||||
RequestLimitPerIP(ip, cost = 1)(true)
|
||||
}
|
||||
case _ => fuccess(false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ private final class Monitor(
|
|||
|
||||
private case class AnalysisMeta(time: Int, nodes: Int, nps: Int, depth: Int, pvSize: Int)
|
||||
|
||||
private def sumOf[A](ints: List[A])(f: A => Option[Int]) = ints.foldLeft(0) {
|
||||
private def sumOf[A](items: List[A])(f: A => Option[Int]) = items.foldLeft(0) {
|
||||
case (acc, a) => acc + f(a).getOrElse(0)
|
||||
}
|
||||
|
||||
|
@ -47,6 +47,19 @@ private final class Monitor(
|
|||
avgOf(_.cappedNps) foreach { monitor.nps(_) }
|
||||
avgOf(_.depth) foreach { monitor.depth(_) }
|
||||
avgOf(_.pvList.size.some) foreach { monitor.pvSize(_) }
|
||||
|
||||
// endgame positions count and total time
|
||||
if (work.game.variant.standard)
|
||||
chess.Replay.boardsFromUci(~work.game.uciList, work.game.initialFen, work.game.variant).fold(
|
||||
err => logger.warn(s"Monitor couldn't get ${work.game.id}'s boards"),
|
||||
boards => {
|
||||
val (count, time) = (boards zip result.analysis).foldLeft(0 -> 0) {
|
||||
case ((count, time), (board, _)) if board.pieces.size > 5 => (count, time)
|
||||
case ((count, time), (_, eval)) => (count + 1, time + ~eval.time)
|
||||
}
|
||||
if (count > 0) monitor.endgameCount(count)
|
||||
if (time > 0) monitor.endgameTime(time)
|
||||
})
|
||||
}
|
||||
|
||||
private def sample[A](elems: List[A], n: Int) =
|
||||
|
|
|
@ -2,7 +2,7 @@ package lila.fishnet
|
|||
|
||||
import org.joda.time.DateTime
|
||||
|
||||
import chess.format.FEN
|
||||
import chess.format.{ Uci, FEN }
|
||||
import chess.variant.Variant
|
||||
|
||||
sealed trait Work {
|
||||
|
@ -48,6 +48,8 @@ object Work {
|
|||
moves: String) {
|
||||
|
||||
def moveList = moves.split(' ').toList
|
||||
|
||||
def uciList = Uci readList moves
|
||||
}
|
||||
|
||||
case class Sender(
|
||||
|
|
|
@ -7,7 +7,10 @@ import reactivemongo.bson._
|
|||
private object BSONHandlers {
|
||||
|
||||
implicit val CategBSONHandler = Macros.handler[Categ]
|
||||
|
||||
implicit val PostEditBSONHandler = Macros.handler[OldVersion]
|
||||
implicit val PostBSONHandler = Macros.handler[Post]
|
||||
|
||||
private val topicHandler: BSONDocumentReader[Topic] with BSONDocumentWriter[Topic] with BSONHandler[Bdoc, Topic] = Macros.handler[Topic]
|
||||
implicit val TopicBSONHandler: BSONDocumentReader[Topic] with BSONDocumentWriter[Topic] with BSONHandler[Bdoc, Topic] = LoggingHandler(logger)(topicHandler)
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ private[forum] final class DataForm(val captcher: akka.actor.ActorSelection) ext
|
|||
|
||||
val post = Form(postMapping)
|
||||
|
||||
val postEdit = Form(mapping("changes" -> text(minLength=3))(PostEdit.apply)(PostEdit.unapply))
|
||||
|
||||
def postWithCaptcha = withCaptcha(post)
|
||||
|
||||
val topic = Form(mapping(
|
||||
|
@ -36,4 +38,6 @@ object DataForm {
|
|||
case class TopicData(
|
||||
name: String,
|
||||
post: PostData)
|
||||
|
||||
case class PostEdit(changes: String)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package lila.forum
|
||||
|
||||
import lila.user.User
|
||||
import org.joda.time.DateTime
|
||||
import ornicar.scalalib.Random
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.user.User
|
||||
case class OldVersion(text: String, createdAt: DateTime)
|
||||
|
||||
case class Post(
|
||||
_id: String,
|
||||
|
@ -17,7 +19,12 @@ case class Post(
|
|||
troll: Boolean,
|
||||
hidden: Boolean,
|
||||
lang: Option[String],
|
||||
createdAt: DateTime) {
|
||||
editHistory: Option[List[OldVersion]] = None,
|
||||
createdAt: DateTime,
|
||||
updatedAt: Option[DateTime] = None) {
|
||||
|
||||
private val permitEditsFor = 4 hours
|
||||
private val showEditFormFor = 3 hours
|
||||
|
||||
def id = _id
|
||||
|
||||
|
@ -28,6 +35,29 @@ case class Post(
|
|||
def isTeam = categId startsWith teamSlug("")
|
||||
|
||||
def isStaff = categId == "staff"
|
||||
|
||||
def updatedOrCreatedAt = updatedAt | createdAt
|
||||
|
||||
def canStillBeEdited() = {
|
||||
updatedOrCreatedAt.plus(permitEditsFor.toMillis).isAfter(DateTime.now)
|
||||
}
|
||||
|
||||
def canBeEditedBy(editingId: String): Boolean = userId.fold(false)(editingId == _)
|
||||
|
||||
def shouldShowEditForm(editingId: String) =
|
||||
canBeEditedBy(editingId) &&
|
||||
updatedOrCreatedAt.plus(showEditFormFor.toMillis).isAfter(DateTime.now)
|
||||
|
||||
def editPost(updated: DateTime, newText: String): Post = {
|
||||
val oldVersion = new OldVersion(text, updatedOrCreatedAt)
|
||||
|
||||
// We only store a maximum of 5 historical versions of the post to prevent abuse of storage space
|
||||
val history = (oldVersion :: ~editHistory).take(5)
|
||||
|
||||
copy(editHistory = history.some, text = newText, updatedAt = updated.some)
|
||||
}
|
||||
|
||||
def hasEdits = editHistory.isDefined
|
||||
}
|
||||
|
||||
object Post {
|
||||
|
@ -44,17 +74,20 @@ object Post {
|
|||
number: Int,
|
||||
lang: Option[String],
|
||||
troll: Boolean,
|
||||
hidden: Boolean): Post = Post(
|
||||
_id = Random nextStringUppercase idSize,
|
||||
topicId = topicId,
|
||||
author = author,
|
||||
userId = userId,
|
||||
ip = ip,
|
||||
text = text,
|
||||
number = number,
|
||||
lang = lang,
|
||||
troll = troll,
|
||||
hidden = hidden,
|
||||
createdAt = DateTime.now,
|
||||
categId = categId)
|
||||
hidden: Boolean): Post = {
|
||||
|
||||
Post(
|
||||
_id = Random nextStringUppercase idSize,
|
||||
topicId = topicId,
|
||||
author = author,
|
||||
userId = userId,
|
||||
ip = ip,
|
||||
text = text,
|
||||
number = number,
|
||||
lang = lang,
|
||||
troll = troll,
|
||||
hidden = hidden,
|
||||
createdAt = DateTime.now,
|
||||
categId = categId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,13 @@ package lila.forum
|
|||
import actorApi._
|
||||
import akka.actor.ActorSelection
|
||||
import org.joda.time.DateTime
|
||||
|
||||
import lila.common.paginator._
|
||||
import lila.db.dsl._
|
||||
import lila.db.paginator._
|
||||
import lila.hub.actorApi.timeline.{ Propagate, ForumPost }
|
||||
import lila.hub.actorApi.timeline.{ForumPost, Propagate}
|
||||
import lila.mod.ModlogApi
|
||||
import lila.security.{ Granter => MasterGranter }
|
||||
import lila.user.{ User, UserContext }
|
||||
import lila.security.{Granter => MasterGranter}
|
||||
import lila.user.{User, UserContext}
|
||||
|
||||
final class PostApi(
|
||||
env: Env,
|
||||
|
@ -69,6 +68,25 @@ final class PostApi(
|
|||
}
|
||||
}
|
||||
|
||||
def editPost(postId: String, newText: String, user: User) : Fu[Post] = {
|
||||
|
||||
get(postId) flatMap { post =>
|
||||
val now = DateTime.now
|
||||
|
||||
post match {
|
||||
case Some((_, post)) if !post.canBeEditedBy(user.id) =>
|
||||
fufail("You are not authorized to modify this post.")
|
||||
case Some((_, post)) if !post.canStillBeEdited() =>
|
||||
fufail("Post can no longer be edited")
|
||||
case Some((_,post)) =>
|
||||
val spamEscapedTest = lila.security.Spam.replace(newText)
|
||||
val newPost = post.editPost(now, spamEscapedTest)
|
||||
env.postColl.update($id(post.id), newPost) inject newPost
|
||||
case None => fufail("Post no longer exists.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val quickHideCategs = Set("lichess-feedback", "off-topic-discussion")
|
||||
|
||||
private def shouldHideOnPost(topic: Topic) =
|
||||
|
@ -169,4 +187,6 @@ final class PostApi(
|
|||
def nbByUser(userId: String) = env.postColl.countSel($doc("userId" -> userId))
|
||||
|
||||
def userIds(topic: Topic) = PostRepo userIdsByTopicId topic.id
|
||||
|
||||
def userIds(topicId: String) = PostRepo userIdsByTopicId topicId
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package lila.forum
|
||||
|
||||
import lila.db.dsl._
|
||||
import lila.user.User.BSONFields
|
||||
import org.joda.time.DateTime
|
||||
import reactivemongo.api.ReadPreference
|
||||
|
||||
object PostRepo extends PostRepo(false) {
|
||||
|
||||
|
@ -68,10 +70,10 @@ sealed abstract class PostRepo(troll: Boolean) {
|
|||
def sortQuery = $sort.createdAsc
|
||||
|
||||
def userIdsByTopicId(topicId: String): Fu[List[String]] =
|
||||
coll.distinct("userId", $doc("topicId" -> topicId).some) map lila.db.BSON.asStrings
|
||||
coll.distinct[String, List]("userId", $doc("topicId" -> topicId).some)
|
||||
|
||||
def idsByTopicId(topicId: String): Fu[List[String]] =
|
||||
coll.distinct("_id", $doc("topicId" -> topicId).some) map lila.db.BSON.asStrings
|
||||
coll.distinct[String, List]("_id", $doc("topicId" -> topicId).some)
|
||||
|
||||
import reactivemongo.api.ReadPreference
|
||||
def cursor(
|
||||
|
|
|
@ -134,4 +134,5 @@ private[forum] final class TopicApi(
|
|||
updatedAtTroll = lastPostTroll.fold(topic.updatedAtTroll)(_.createdAt)
|
||||
)).void
|
||||
} yield ()
|
||||
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue