Merge branch 'master' into asso

* master: (23 commits)
  display game completion rate on user profile
  hide link to favourite opponents
  make many DB accesses more type safe
  don't count forum topics
  tweak ranking API
  prevent heavy forum DB queries
  upgrade scalachess
  catch and log link adder exceptions
  playban penalties for clock sitters
  upgrade scalachess
  fix fishnet analysis nodes config
  remove useless user count
  apply game limit to challenges
  Update stockfish.js and use submodule
  improve seek limit
  play up to 200 concurrent games per user
  limit opening trainer to 10,000 openings
  use distinct for opening selection
  asset version
  analysis with 4 million nodes per move
  ...
This commit is contained in:
Thibault Duplessis 2016-07-18 18:38:43 +02:00
commit b247f15597
63 changed files with 338 additions and 269 deletions

3
.gitmodules vendored
View file

@ -28,3 +28,6 @@
[submodule "public/vendor/shepherd"]
path = public/vendor/shepherd
url = https://github.com/HubSpot/shepherd
[submodule "public/vendor/stockfish.js"]
path = public/vendor/stockfish.js
url = https://github.com/niklasf/stockfish.js

View file

@ -30,7 +30,6 @@ final class Env(
lightUser = Env.user.lightUser)
lazy val userInfo = mashup.UserInfo(
countUsers = () => Env.user.countEnabled,
bookmarkApi = Env.bookmark.api,
relationApi = Env.relation.api,
trophyApi = Env.user.trophyApi,
@ -43,7 +42,8 @@ final class Env(
isHostingSimul = Env.simul.isHosting,
isStreamer = Env.tv.isStreamer.apply,
insightShare = Env.insight.share,
getPlayTime = Env.game.playTime.apply) _
getPlayTime = Env.game.playTime.apply,
completionRate = Env.playban.api.completionRate) _
system.actorOf(Props(new actor.Renderer), name = RendererName)
@ -70,6 +70,7 @@ final class Env(
Env.tv,
Env.blog,
Env.video,
Env.playban, // required to load the actor
Env.shutup, // required to load the actor
Env.insight, // required to load the actor
Env.worldMap, // required to load the actor

View file

@ -13,9 +13,11 @@ object ForumCateg extends LilaController with ForumController {
def show(slug: String, page: Int) = Open { implicit ctx =>
NotForKids {
CategGrantRead(slug) {
OptionOk(categApi.show(slug, page, ctx.troll)) {
case (categ, topics) => html.forum.categ.show(categ, topics)
Reasonable(page, 50, errorPage = notFound) {
CategGrantRead(slug) {
OptionOk(categApi.show(slug, page, ctx.troll)) {
case (categ, topics) => html.forum.categ.show(categ, topics)
}
}
}
}

View file

@ -331,11 +331,10 @@ private[controllers] trait LilaController
}
protected def XhrOnly(res: => Fu[Result])(implicit ctx: Context) =
if (HTTPRequest isXhr ctx.req) res
else notFound
if (HTTPRequest isXhr ctx.req) res else notFound
protected def Reasonable(page: Int, max: Int = 40)(result: => Fu[Result]): Fu[Result] =
(page < max).fold(result, BadRequest("resource too old").fuccess)
protected def Reasonable(page: Int, max: Int = 40, errorPage: => Fu[Result] = BadRequest("resource too old").fuccess)(result: => Fu[Result]): Fu[Result] =
if (page < max) result else errorPage
protected def NotForKids(f: => Fu[Result])(implicit ctx: Context) =
if (ctx.kid) notFound else f

View file

@ -3,10 +3,10 @@ package controllers
import scala.util.{ Try, Success, Failure }
import play.api.data._, Forms._
import play.api.mvc._
import play.twirl.api.Html
import play.api.Play.current
import play.api.i18n.Messages.Implicits._
import play.api.mvc._
import play.api.Play.current
import play.twirl.api.Html
import lila.api.Context
import lila.app._
@ -25,9 +25,9 @@ object Opening extends LilaController {
private def renderShow(opening: OpeningModel)(implicit ctx: Context) =
env userInfos ctx.me zip identify(opening) map {
case (infos, identified) =>
views.html.opening.show(opening, identified, infos, env.AnimationDuration)
}
case (infos, identified) =>
views.html.opening.show(opening, identified, infos, env.AnimationDuration)
}
private def makeData(
opening: OpeningModel,
@ -46,12 +46,16 @@ object Opening extends LilaController {
animationDuration = env.AnimationDuration)) as JSON
}
private val noMoreOpeningJson = jsonError("No more openings for you!")
def home = Open { implicit ctx =>
if (HTTPRequest isXhr ctx.req) env.selector(ctx.me) zip (env userInfos ctx.me) flatMap {
case (opening, infos) => makeData(opening, infos, true, none, none)
case (Some(opening), infos) => makeData(opening, infos, true, none, none)
case (None, _) => NotFound(noMoreOpeningJson).fuccess
}
else env.selector(ctx.me) flatMap { opening =>
renderShow(opening) map { Ok(_) }
else env.selector(ctx.me) flatMap {
case Some(opening) => renderShow(opening) map { Ok(_) }
case None => fuccess(Ok(html.opening.noMore()))
}
}

View file

@ -11,6 +11,7 @@ import lila.api.{ Context, BodyContext }
import lila.app._
import lila.common.{ HTTPRequest, LilaCookie }
import lila.game.{ GameRepo, Pov, AnonCookie }
import lila.setup.Processor.HookResult
import lila.setup.{ HookConfig, ValidFen }
import lila.user.UserRepo
import views._
@ -89,9 +90,14 @@ object Setup extends LilaController with TheftPrevention {
destUser = destUser,
rematchOf = none)
env.processor.saveFriendConfig(config) >>
(Env.challenge.api create challenge) >> negotiate(
html = fuccess(Redirect(routes.Round.watcher(challenge.id, "white"))),
api = _ => Challenge showChallenge challenge)
(Env.challenge.api create challenge) flatMap {
case true => negotiate(
html = fuccess(Redirect(routes.Round.watcher(challenge.id, "white"))),
api = _ => Challenge showChallenge challenge)
case false => negotiate(
html = fuccess(Redirect(routes.Lobby.home)),
api = _ => fuccess(BadRequest(jsonError("Challenge not created"))))
}
}
}
}
@ -108,10 +114,14 @@ object Setup extends LilaController with TheftPrevention {
}
}
private def hookResponse(hookId: String) =
Ok(Json.obj(
private def hookResponse(res: HookResult) = res match {
case HookResult.Created(id) => Ok(Json.obj(
"ok" -> true,
"hook" -> Json.obj("id" -> hookId))) as JSON
"hook" -> Json.obj("id" -> id))) as JSON
case HookResult.Refused => BadRequest(jsonError("Game was not created"))
}
private val hookRefused = BadRequest(jsonError("Game was not created"))
def hook(uid: String) = OpenBody { implicit ctx =>
implicit val req = ctx.body
@ -123,9 +133,7 @@ object Setup extends LilaController with TheftPrevention {
api = _ => BadRequest(errorsAsJson(err)).fuccess),
config => (ctx.userId ?? Env.relation.api.fetchBlocking) flatMap {
blocking =>
env.processor.hook(config, uid, HTTPRequest sid req, blocking) map hookResponse recover {
case e: IllegalArgumentException => BadRequest(jsonError(e.getMessage)) as JSON
}
env.processor.hook(config, uid, HTTPRequest sid req, blocking) map hookResponse
}
)
}
@ -140,9 +148,7 @@ object Setup extends LilaController with TheftPrevention {
_.fold(config)(config.updateFrom)
} flatMap { config =>
(ctx.userId ?? Env.relation.api.fetchBlocking) flatMap { blocking =>
env.processor.hook(config, uid, HTTPRequest sid ctx.req, blocking) map hookResponse recover {
case e: IllegalArgumentException => BadRequest(jsonError(e.getMessage)) as JSON
}
env.processor.hook(config, uid, HTTPRequest sid ctx.req, blocking) map hookResponse
}
}
}

View file

@ -15,7 +15,6 @@ import lila.user.{ User, Trophy, Trophies, TrophyApi }
case class UserInfo(
user: User,
ranks: lila.rating.UserRankMap,
nbUsers: Int,
nbPlaying: Int,
hasSimul: Boolean,
crosstable: Option[Crosstable],
@ -29,7 +28,8 @@ case class UserInfo(
playTime: User.PlayTime,
trophies: Trophies,
isStreamer: Boolean,
insightVisible: Boolean) {
insightVisible: Boolean,
completionRate: Option[Double]) {
def nbRated = user.count.rated
@ -37,6 +37,8 @@ case class UserInfo(
def percentRated: Int = math.round(nbRated / user.count.game.toFloat * 100)
def completionRatePercent = completionRate.map { cr => math.round(cr * 100) }
def allTrophies = List(
isStreamer option Trophy(
_id = "",
@ -49,7 +51,6 @@ case class UserInfo(
object UserInfo {
def apply(
countUsers: () => Fu[Int],
bookmarkApi: BookmarkApi,
relationApi: RelationApi,
trophyApi: TrophyApi,
@ -62,9 +63,9 @@ object UserInfo {
isHostingSimul: String => Fu[Boolean],
isStreamer: String => Boolean,
insightShare: lila.insight.Share,
getPlayTime: User => Fu[User.PlayTime])(user: User, ctx: Context): Fu[UserInfo] =
countUsers() zip
getRanks(user.id) zip
getPlayTime: User => Fu[User.PlayTime],
completionRate: User.ID => Fu[Option[Double]])(user: User, ctx: Context): Fu[UserInfo] =
getRanks(user.id) zip
(gameCached nbPlaying user.id) zip
gameCached.nbImportedBy(user.id) zip
(ctx.me.filter(user!=) ?? { me => crosstableApi(me.id, user.id) }) zip
@ -75,13 +76,13 @@ object UserInfo {
studyRepo.countByOwner(user.id) zip
trophyApi.findByUser(user) zip
(user.count.rated >= 10).??(insightShare.grant(user, ctx.me)) zip
getPlayTime(user) flatMap {
case ((((((((((((nbUsers, ranks), nbPlaying), nbImported), crosstable), ratingChart), nbFollowers), nbBlockers), nbPosts), nbStudies), trophies), insightVisible), playTime) =>
getPlayTime(user) zip
completionRate(user.id) flatMap {
case ((((((((((((ranks, nbPlaying), nbImported), crosstable), ratingChart), nbFollowers), nbBlockers), nbPosts), nbStudies), trophies), insightVisible), playTime), completionRate) =>
(nbPlaying > 0) ?? isHostingSimul(user.id) map { hasSimul =>
new UserInfo(
user = user,
ranks = ranks,
nbUsers = nbUsers,
nbPlaying = nbPlaying,
hasSimul = hasSimul,
crosstable = crosstable,
@ -95,7 +96,8 @@ object UserInfo {
playTime = playTime,
trophies = trophies,
isStreamer = isStreamer(user.id),
insightVisible = insightVisible)
insightVisible = insightVisible,
completionRate = completionRate)
}
}
}

View file

@ -7,7 +7,7 @@ trait PaginatorHelper {
implicit def toRichPager[A](pager: Paginator[A]) = new {
def sliding(length: Int): List[Option[Int]] = {
def sliding(length: Int, showPost: Boolean = true): List[Option[Int]] = {
val fromPage = 1 max (pager.currentPage - length)
val toPage = pager.nbPages min (pager.currentPage + length)
val pre = fromPage match {
@ -18,7 +18,8 @@ trait PaginatorHelper {
val post = toPage match {
case x if x == pager.nbPages => Nil
case x if x == pager.nbPages - 1 => List(pager.nbPages.some)
case x => List(none, pager.nbPages.some)
case x if showPost => List(none, pager.nbPages.some)
case _ => List(none)
}
pre ::: (fromPage to toPage).toList.map(some) ::: post
}

View file

@ -5,7 +5,7 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.regex.Matcher.quoteReplacement
import lila.user.{User, UserContext}
import lila.user.{ User, UserContext }
import org.apache.commons.lang3.StringEscapeUtils.escapeHtml4
import play.twirl.api.Html
@ -50,10 +50,10 @@ trait StringHelper { self: NumberHelper =>
private val urlRegex = """(?i)\b((https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,6}/)((?:[^\s<>]+|\(([^\s<>]+|(\([^\s<>]+\)))*\))+(?:\(([^\s<>]+|(\([^\s<>]+\)))*\)|[^\s`!\[\]{};:'".,<>?«»“”‘’])))""".r
/**
* Creates hyperlinks to user profiles mentioned using the '@' prefix. e.g. @ornicar
* @param text The text to regex match
* @return The text as a HTML hyperlink
*/
* Creates hyperlinks to user profiles mentioned using the '@' prefix. e.g. @ornicar
* @param text The text to regex match
* @return The text as a HTML hyperlink
*/
def addUserProfileLinks(text: String) = User.atUsernameRegex.replaceAllIn(text, m => {
val user = m group 1
val url = s"//$netDomain/@/$user"
@ -61,27 +61,37 @@ trait StringHelper { self: NumberHelper =>
s"""<a href="$url">@$user</a>"""
})
def addLinks(text: String) = urlRegex.replaceAllIn(text, m => {
if (m.group(2) == "http://" || m.group(2) == "https://") {
if (s"${delocalize(m.group(3))}/" startsWith s"$netDomain/") {
// internal
val link = delocalize(m.group(3))
s"""<a rel="nofollow" href="//$link">$link</a>"""
} else {
// external
s"""<a rel="nofollow" href="${m.group(1)}" target="_blank">${m.group(1)}</a>"""
def addLinks(text: String) = try {
urlRegex.replaceAllIn(text, m => {
if (m.group(2) == "http://" || m.group(2) == "https://") {
if (s"${delocalize(m.group(3))}/" startsWith s"$netDomain/") {
// internal
val link = delocalize(m.group(3))
s"""<a rel="nofollow" href="//$link">$link</a>"""
}
else {
// external
s"""<a rel="nofollow" href="${m.group(1)}" target="_blank">${m.group(1)}</a>"""
}
}
} else {
if (s"${delocalize(m.group(2))}/" startsWith s"$netDomain/") {
// internal
val link = delocalize(m.group(1))
s"""<a rel="nofollow" href="//$link">$link</a>"""
} else {
// external
s"""<a rel="nofollow" href="http://${m.group(1)}" target="_blank">${m.group(1)}</a>"""
else {
if (s"${delocalize(m.group(2))}/" startsWith s"$netDomain/") {
// internal
val link = delocalize(m.group(1))
s"""<a rel="nofollow" href="//$link">$link</a>"""
}
else {
// external
s"""<a rel="nofollow" href="http://${m.group(1)}" target="_blank">${m.group(1)}</a>"""
}
}
}
})
})
}
catch {
case e: IllegalArgumentException =>
lila.log("templating").error(s"addLinks($text)", e)
text
}
private val delocalize = new lila.common.String.Delocalizer(netDomain)

View file

@ -12,7 +12,7 @@
} else {
<span class="disabled" data-icon="I"></span>
}
@pager.sliding(3).map {
@pager.sliding(3, showPost = false).map {
case None => { ... }
case Some(p) => {
@if(p == pager.currentPage) {

View file

@ -0,0 +1,10 @@
@()(implicit ctx: Context)
@site.message("You played all the openings!", back = false) {
<a href="http://mr--jack.deviantart.com/art/Chess-Set-106198801">
<img width="100%" src="@staticUrl("images/futurist-set.jpg")" alt="Futurist chess set drawing" />
</a>
<br /><br /><br />
Sorry, we don't have any more openings for you.<br /><br />
You played quite a lot of them, didn't you :)
}

View file

@ -69,8 +69,8 @@ description = describeUser(u)).some) {
<a href="@routes.UserTournament.path(u.username, "recent")" class="intertab tournament_stats" data-toints="@u.toints">@splitNumber(u.toints + " " + trans.tournamentPoints())</a>
<a href="@routes.Study.byOwnerDefault(u.username)" class="intertab">@splitNumber(info.nbStudies + " studies")</a>
<a class="intertab" @NotForKids {
href="@routes.ForumPost.search("user:" + u.username, 1)"
}>@splitNumber(trans.nbForumPosts(info.nbPosts))</a>
href="@routes.ForumPost.search("user:" + u.username, 1)"
}>@splitNumber(trans.nbForumPosts(info.nbPosts))</a>
@NotForKids {
@if(ctx.isAuth && !ctx.is(u)) {
<a class="intertab note_zone_toggle">@splitNumber(notes.size + " Notes")</a>
@ -181,7 +181,10 @@ description = describeUser(u)).some) {
@u.seenAt.map { seen =>
<p class="thin">@trans.lastSeenActive(momentFromNow(seen))</p>
}
<a href="@routes.User.opponents(u.username)">@trans.favoriteOpponents()</a>
@info.completionRatePercent.map { c =>
<p class="thin">Game completion rate: @c%</p>
}
<!-- <a href="@routes.User.opponents(u.username)">@trans.favoriteOpponents()</a> -->
<br />
<br />
<p>@trans.tpTimeSpentPlaying(showPeriod(info.playTime.totalPeriod))</p>

View file

@ -9,7 +9,7 @@ net {
ip = "5.196.91.160"
asset {
domain = ${net.domain}
version = 984
version = 989
}
}
forcedev = false
@ -146,7 +146,7 @@ opening {
step = 160
max = 1000
}
modulo = 20000
max_attempts = 10000
}
animation.duration = ${chessground.animation.duration}
api.token = ${api.token}
@ -346,6 +346,7 @@ lobby {
max_per_page = 14
max_per_user = 5
}
max_playing = ${setup.max_playing}
}
timeline {
collection {
@ -504,6 +505,7 @@ setup {
user_config = config
anon_config = config_anon
}
max_playing = 200
}
challenge {
collection.challenge = challenge
@ -514,6 +516,7 @@ challenge {
}
history.message.ttl = 40 seconds
uid.timeout = 7 seconds
max_playing = ${setup.max_playing}
}
study {
collection.study = study

View file

@ -27,7 +27,7 @@ private[bookmark] object BookmarkRepo {
coll.remove($doc("g" -> gameId)).void
def removeByGameIds(gameIds: List[String]): Funit =
coll.remove($doc("g" -> $in(gameIds: _*))).void
coll.remove($doc("g" $in gameIds)).void
private def add(gameId: String, userId: String, date: DateTime): Funit =
coll.insert($doc(

View file

@ -33,6 +33,8 @@ case class Challenge(
def challengerIsAnon = challenger.isLeft
def destUserId = destUser.map(_.id)
def userIds = List(challengerUserId, destUserId).flatten
def daysPerTurn = timeControl match {
case TimeControl.Correspondence(d) => d.some
case _ => none

View file

@ -14,6 +14,8 @@ final class ChallengeApi(
repo: ChallengeRepo,
joiner: Joiner,
jsonView: JsonView,
gameCache: lila.game.Cached,
maxPlaying: Int,
socketHub: ActorRef,
userRegister: ActorSelection,
lilaBus: lila.common.Bus) {
@ -23,10 +25,14 @@ final class ChallengeApi(
def allFor(userId: User.ID): Fu[AllChallenges] =
createdByDestId(userId) zip createdByChallengerId(userId) map (AllChallenges.apply _).tupled
def create(c: Challenge): Funit = {
repo like c flatMap { _ ?? repo.cancel }
} >> (repo insert c) >> uncacheAndNotify(c) >>- {
lilaBus.publish(Event.Create(c), 'challenge)
// returns boolean success
def create(c: Challenge): Fu[Boolean] = isLimitedByMaxPlaying(c) flatMap {
case true => fuccess(false)
case false => {
repo like c flatMap { _ ?? repo.cancel }
} >> (repo insert c) >> uncacheAndNotify(c) >>- {
lilaBus.publish(Event.Create(c), 'challenge)
} inject true
}
def byId = repo byId _
@ -82,7 +88,13 @@ final class ChallengeApi(
} yield success
}
def removeByUserId = repo removeByUserId _
def removeByUserId = repo removeByUserId _
private def isLimitedByMaxPlaying(c: Challenge) =
if (c.hasClock) fuccess(false)
else c.userIds.map { userId =>
gameCache.nbPlaying(userId) map (maxPlaying <=)
}.sequenceFu.map(_ exists identity)
private[challenge] def sweep: Funit =
repo.realTimeUnseenSince(DateTime.now minusSeconds 10, max = 50).flatMap { cs =>

View file

@ -14,6 +14,7 @@ final class Env(
config: Config,
system: ActorSystem,
onStart: String => Unit,
gameCache: lila.game.Cached,
lightUser: String => Option[lila.common.LightUser],
hub: lila.hub.Env,
db: lila.db.Env,
@ -26,6 +27,7 @@ final class Env(
val UidTimeout = config duration "uid.timeout"
val SocketTimeout = config duration "socket.timeout"
val SocketName = config getString "socket.name"
val MaxPlaying = config getInt "max_playing"
}
import settings._
@ -51,6 +53,8 @@ final class Env(
repo = repo,
joiner = new Joiner(onStart = onStart),
jsonView = jsonView,
gameCache = gameCache,
maxPlaying = MaxPlaying,
socketHub = socketHub,
userRegister = hub.actor.userRegister,
lilaBus = system.lilaBus)
@ -73,6 +77,7 @@ object Env {
system = lila.common.PlayApp.system,
onStart = lila.game.Env.current.onStart,
hub = lila.hub.Env.current,
gameCache = lila.game.Env.current.cached,
lightUser = lila.user.Env.current.lightUser,
db = lila.db.Env.current,
scheduler = lila.common.PlayApp.scheduler)

@ -1 +1 @@
Subproject commit 8b58cf1a837a47b92f47a65aeea7742296642e57
Subproject commit 7addd334e6052189d87d33374ef2c67fdb0d29b6

View file

@ -286,7 +286,7 @@ trait dsl {
}
/** Matches any of the values that exist in an array specified in the query.*/
def $in[T](values: T*)(implicit writer: BSONWriter[T, _ <: BSONValue]): SimpleExpression[BSONDocument] = {
def $in[T](values: Iterable[T])(implicit writer: BSONWriter[T, _ <: BSONValue]): SimpleExpression[BSONDocument] = {
SimpleExpression(field, $doc("$in" -> values))
}
@ -306,7 +306,7 @@ trait dsl {
}
/** Matches values that do not exist in an array specified to the query. */
def $nin[T](values: T*)(implicit writer: BSONWriter[T, _ <: BSONValue]): SimpleExpression[BSONDocument] = {
def $nin[T](values: Iterable[T])(implicit writer: BSONWriter[T, _ <: BSONValue]): SimpleExpression[BSONDocument] = {
SimpleExpression(field, $doc("$nin" -> values))
}

View file

@ -179,8 +179,8 @@ object JsonApi {
implicit val WorkWrites = OWrites[Work] { work =>
Json.obj(
"work" -> (work match {
case a: Analysis => Json.obj("type" -> "analysis", "id" -> work.id)
case m: Move => Json.obj("type" -> "move", "id" -> work.id, "level" -> m.level)
case a: Analysis => Json.obj("type" -> "analysis", "id" -> a.id, "nodes" -> a.nodes)
case m: Move => Json.obj("type" -> "move", "id" -> m.id, "level" -> m.level)
})
) ++ Json.toJson(work.game).as[JsObject]
}

View file

@ -14,8 +14,8 @@ object CategRepo {
def withTeams(teams: Set[String]): Fu[List[Categ]] =
coll.find($or(
"team" $exists false,
$doc("team" -> $doc("$in" -> teams))
)).sort($sort asc "pos").cursor[Categ]().gather[List]()
$doc("team" $in teams))
).sort($sort asc "pos").cursor[Categ]().gather[List]()
def nextPosition: Fu[Int] =
coll.primitiveOne[Int]($empty, $sort desc "pos", "pos") map (~_ + 1)

View file

@ -48,16 +48,16 @@ sealed abstract class PostRepo(troll: Boolean) {
multi = true).void
def selectTopic(topicId: String) = $doc("topicId" -> topicId) ++ trollFilter
def selectTopics(topicIds: List[String]) = $doc("topicId" $in (topicIds: _*)) ++ trollFilter
def selectTopics(topicIds: List[String]) = $doc("topicId" $in topicIds) ++ trollFilter
def selectCateg(categId: String) = $doc("categId" -> categId) ++ trollFilter
def selectCategs(categIds: List[String]) = $doc("categId" $in (categIds: _*)) ++ trollFilter
def selectCategs(categIds: List[String]) = $doc("categId" $in categIds) ++ trollFilter
val selectNotHidden = $doc("hidden" -> false)
def selectLangs(langs: List[String]) =
if (langs.isEmpty) $empty
else $doc("lang" $in (langs: _*))
else $doc("lang" $in langs)
def findDuplicate(post: Post): Fu[Option[Post]] = coll.uno[Post]($doc(
"createdAt" $gt DateTime.now.minusHours(1),

View file

@ -78,8 +78,8 @@ private[forum] final class TopicApi(
} >>- mentionNotifier.notifyMentionedUsers(post, topic) inject topic
}
def paginator(categ: Categ, page: Int, troll: Boolean): Fu[Paginator[TopicView]] = Paginator(
adapter = new Adapter[Topic](
def paginator(categ: Categ, page: Int, troll: Boolean): Fu[Paginator[TopicView]] = {
val adapter = new Adapter[Topic](
collection = env.topicColl,
selector = TopicRepo(troll) byCategQuery categ,
projection = $empty,
@ -88,9 +88,15 @@ private[forum] final class TopicApi(
env.postColl.byId[Post](topic lastPostId troll) map { post =>
TopicView(categ, topic, post, env.postApi lastPageOf topic, troll)
}
},
currentPage = page,
maxPerPage = maxPerPage)
}
val cachedAdapter =
if (categ.isTeam) adapter
else new CachedAdapter(adapter, nbResults = fuccess(1000))
Paginator(
adapter = cachedAdapter,
currentPage = page,
maxPerPage = maxPerPage)
}
def delete(categ: Categ, topic: Topic): Funit =
PostRepo.idsByTopicId(topic.id) flatMap { postIds =>

View file

@ -187,12 +187,11 @@ object GameRepo {
coll.exists($id(id) ++ Query.analysed(true))
def filterAnalysed(ids: Seq[String]): Fu[Set[String]] =
coll.distinct("_id", $doc(
"_id" -> $doc("$in" -> ids),
coll.distinct("_id", ($inIds(ids) ++ $doc(
F.analysed -> true
).some) map lila.db.BSON.asStringSet
)).some) map lila.db.BSON.asStringSet
def exists(id: String) = coll.exists($doc("_id" -> id))
def exists(id: String) = coll.exists($id(id))
def incBookmarks(id: ID, value: Int) =
coll.update($id(id), $inc(F.bookmarks -> value)).void
@ -357,10 +356,10 @@ object GameRepo {
def findMirror(game: Game): Fu[Option[Game]] = coll.uno[Game]($doc(
F.id -> $doc("$ne" -> game.id),
F.playerUids -> $doc("$in" -> game.userIds),
F.playerUids $in game.userIds,
F.status -> Status.Started.id,
F.createdAt -> $doc("$gt" -> (DateTime.now minusMinutes 15)),
F.updatedAt -> $doc("$gt" -> (DateTime.now minusMinutes 5)),
F.createdAt $gt (DateTime.now minusMinutes 15),
F.updatedAt $gt (DateTime.now minusMinutes 5),
"$or" -> $arr(
$doc(s"${F.whitePlayer}.ai" -> $doc("$exists" -> true)),
$doc(s"${F.blackPlayer}.ai" -> $doc("$exists" -> true))

View file

@ -28,7 +28,7 @@ object Query {
val mate: Bdoc = status(Status.Mate)
val draw: Bdoc = F.status $in (Status.Draw.id, Status.Stalemate.id)
val draw: Bdoc = F.status $in List(Status.Draw.id, Status.Stalemate.id)
def draw(u: String): Bdoc = user(u) ++ draw
@ -50,7 +50,7 @@ object Query {
def user(u: String): Bdoc = F.playerUids $eq u
def user(u: User): Bdoc = F.playerUids $eq u.id
def users(u: Seq[String]) = F.playerUids $in (u: _*)
def users(u: Seq[String]) = F.playerUids $in u
val noAi: Bdoc = $doc(
"p0.ai" $exists false,
@ -65,7 +65,7 @@ object Query {
def win(u: String) = user(u) ++ $doc(F.winnerId -> u)
def loss(u: String) = user(u) ++ $doc(
F.status $in (Status.finishedWithWinner.map(_.id): _*),
F.status $in Status.finishedWithWinner.map(_.id),
F.winnerId -> $exists(true).++($ne(u))
)

View file

@ -145,7 +145,7 @@ object Dimension {
def filtersOf[X](d: Dimension[X], selected: List[X]): Bdoc = d match {
case Dimension.MovetimeRange => selected match {
case Nil => $empty
case xs => $doc(d.dbKey -> $doc("$in" -> xs.flatMap(_.tenths.list)))
case xs => $doc(d.dbKey $in xs.flatMap(_.tenths.list))
}
case _ => selected map d.bson.write match {
case Nil => $empty

View file

@ -13,6 +13,7 @@ final class Env(
onStart: String => Unit,
blocking: String => Fu[Set[String]],
playban: String => Fu[Option[lila.playban.TempBan]],
gameCache: lila.game.Cached,
system: ActorSystem,
scheduler: lila.common.Scheduler) {
@ -29,6 +30,7 @@ final class Env(
val CollectionSeekArchive = config getString "collection.seek_archive"
val SeekMaxPerPage = config getInt "seek.max_per_page"
val SeekMaxPerUser = config getInt "seek.max_per_user"
val MaxPlaying = config getInt "max_playing"
}
import settings._
@ -49,6 +51,8 @@ final class Env(
new Lobby(
socket = socket,
seekApi = seekApi,
gameCache = gameCache,
maxPlaying = MaxPlaying,
blocking = blocking,
playban = playban,
onStart = onStart)
@ -80,6 +84,7 @@ object Env {
onStart = lila.game.Env.current.onStart,
blocking = lila.relation.Env.current.api.fetchBlocking,
playban = lila.playban.Env.current.api.currentBan _,
gameCache = lila.game.Env.current.cached,
system = lila.common.PlayApp.system,
scheduler = lila.common.PlayApp.scheduler)
}

View file

@ -15,6 +15,8 @@ import org.joda.time.DateTime
private[lobby] final class Lobby(
socket: ActorRef,
seekApi: SeekApi,
gameCache: lila.game.Cached,
maxPlaying: Int,
blocking: String => Fu[Set[String]],
playban: String => Fu[Option[lila.playban.TempBan]],
onStart: String => Unit) extends Actor {
@ -72,10 +74,14 @@ private[lobby] final class Lobby(
}
case BiteSeek(seekId, user) => NoPlayban(user.some) {
lila.mon.lobby.seek.join()
seekApi find seekId foreach {
_ foreach { seek =>
Biter(seek, user) pipeTo self
gameCache.nbPlaying(user.id) foreach { nbPlaying =>
if (nbPlaying < maxPlaying) {
lila.mon.lobby.seek.join()
seekApi find seekId foreach {
_ foreach { seek =>
Biter(seek, user) pipeTo self
}
}
}
}
}

View file

@ -30,7 +30,7 @@ final class Env(
api = api,
toleranceStep = config getInt "selector.tolerance.step",
toleranceMax = config getInt "selector.tolerance.max",
modulo = config getInt "selector.modulo")
maxAttempts = config getInt "selector.max_attempts")
lazy val finisher = new Finisher(
api = api,

View file

@ -33,21 +33,14 @@ private[opening] final class OpeningApi(
def add(a: Attempt) = attemptColl insert a void
def hasPlayed(user: User, opening: Opening): Fu[Boolean] =
attemptColl.count($doc(
attemptColl.exists($doc(
Attempt.BSONFields.id -> Attempt.makeId(opening.id, user.id)
).some) map (0!=)
))
def playedIds(user: User, max: Int): Fu[BSONArray] = {
val col = attemptColl
import reactivemongo.api.collections.bson.BSONBatchCommands.AggregationFramework.{ Group, Limit, Match, Push }
val playedIdsGroup =
Group($boolean(true))("ids" -> Push(Attempt.BSONFields.openingId))
col.aggregate(Match($doc(Attempt.BSONFields.userId -> user.id)),
List(Limit(max), playedIdsGroup)).map(_.documents.headOption.flatMap(
_.getAs[BSONArray]("ids")).getOrElse(BSONArray()))
}
def playedIds(user: User): Fu[BSONArray] =
attemptColl.distinct(Attempt.BSONFields.openingId,
$doc(Attempt.BSONFields.userId -> user.id).some
) map BSONArray.apply
}
object identify {

View file

@ -12,32 +12,32 @@ private[opening] final class Selector(
api: OpeningApi,
toleranceStep: Int,
toleranceMax: Int,
modulo: Int) {
maxAttempts: Int) {
val anonSkipMax = 1500
def apply(me: Option[User]): Fu[Opening] = (me match {
def apply(me: Option[User]): Fu[Option[Opening]] = (me match {
case None =>
openingColl.find($empty)
.skip(Random nextInt anonSkipMax)
.uno[Opening] flatten "Can't find a opening for anon player!"
case Some(user) => api.attempt.playedIds(user, modulo) flatMap { ids =>
.uno[Opening]
case Some(user) if user.perfs.opening.nb >= maxAttempts => fuccess(none)
case Some(user) => api.attempt.playedIds(user) flatMap { ids =>
tryRange(user, toleranceStep, ids)
} recoverWith {
case e: Exception => apply(none)
}
}).mon(_.opening.selector.time) >>- lila.mon.opening.selector.count()
private def tryRange(user: User, tolerance: Int, ids: BSONArray): Fu[Opening] =
private def tryRange(user: User, tolerance: Int, ids: BSONArray): Fu[Option[Opening]] =
openingColl.uno[Opening]($doc(
Opening.BSONFields.id -> $doc("$nin" -> ids),
Opening.BSONFields.rating $gt
(user.perfs.opening.intRating - tolerance) $lt
(user.perfs.opening.intRating + tolerance)
)) flatMap {
case Some(opening) => fuccess(opening)
case None => if ((tolerance + toleranceStep) <= toleranceMax)
case None if (tolerance + toleranceStep) <= toleranceMax =>
tryRange(user, tolerance + toleranceStep, ids)
else fufail(s"Can't find a opening for user $user!")
case res => fuccess(res)
}
}

View file

@ -1,6 +1,6 @@
package lila.playban
import akka.actor.{ ActorSelection, ActorSystem }
import akka.actor._
import com.typesafe.config.Config
import scala.concurrent.duration._
@ -16,9 +16,7 @@ final class Env(
}
import settings._
lazy val api = new PlaybanApi(coll = coll, isRematch = isRematch)
private lazy val coll = db(CollectionPlayban)
lazy val api = new PlaybanApi(coll = db(CollectionPlayban), isRematch = isRematch)
}
object Env {

View file

@ -3,7 +3,6 @@ package lila.playban
import org.joda.time.DateTime
import reactivemongo.bson._
import reactivemongo.bson.Macros
import reactivemongo.core.commands._
import scala.concurrent.duration._
import chess.Color
@ -43,38 +42,57 @@ final class PlaybanApi(
game.player(quitterColor).userId ?? save(Outcome.RageQuit)
}
def sittingOrGood(game: Game, sitterColor: Color): Funit = blameable(game) ?? {
(for {
userId <- game.player(sitterColor).userId
lmt <- game.lastMoveTimeInSeconds
seconds = nowSeconds - lmt
clock <- game.clock
// a tenth of the total time, at least 15s, at most 3 minutes
limit = (clock.estimateTotalTime / 10) max 15 min (3 * 60)
if seconds >= limit
} yield save(Outcome.Sitting)(userId)) | goodFinish(game)
}
def goodFinish(game: Game): Funit = blameable(game) ?? {
game.userIds.map(save(Outcome.Good)).sequenceFu.void
}
def currentBan(userId: String): Fu[Option[TempBan]] = coll.find(
$doc("_id" -> userId, "b.0" -> $doc("$exists" -> true)),
$doc("_id" -> userId, "b.0" $exists true),
$doc("_id" -> false, "b" -> $doc("$slice" -> -1))
).uno[Bdoc].map {
_.flatMap(_.getAs[List[TempBan]]("b")).??(_.find(_.inEffect))
}
def bans(userId: String): Fu[List[TempBan]] = coll.find(
$doc("_id" -> userId, "b.0" -> $doc("$exists" -> true)),
$doc("_id" -> false, "b" -> true)
).uno[Bdoc].map {
~_.flatMap(_.getAs[List[TempBan]]("b"))
def completionRate(userId: String): Fu[Option[Double]] =
coll.primitiveOne[List[Outcome]]($id(userId), "o").map(~_) map { outcomes =>
outcomes.collect {
case Outcome.RageQuit | Outcome.Sitting => false
case Outcome.Good => true
} match {
case c if c.size >= 5 => Some(c.count(identity).toDouble / c.size)
case _ => none
}
}
def bans(userId: String): Fu[List[TempBan]] =
coll.primitiveOne[List[TempBan]]($doc("_id" -> userId, "b.0" $exists true), "b").map(~_)
def bans(userIds: List[String]): Fu[Map[String, Int]] = coll.find(
$inIds(userIds),
$doc("b" -> true)
).cursor[Bdoc]().gather[List]().map {
_.flatMap { obj =>
obj.getAs[String]("_id") flatMap { id =>
obj.getAs[BSONArray]("b") map { id -> _.stream.size }
obj.getAs[Barr]("b") map { id -> _.stream.size }
}
}.toMap
}
private def save(outcome: Outcome): String => Funit = userId => {
coll.findAndUpdate(
selector = $doc("_id" -> userId),
selector = $id(userId),
update = $doc("$push" -> $doc(
"o" -> $doc(
"$each" -> List(outcome),
@ -89,15 +107,13 @@ final class PlaybanApi(
private def legiferate(record: UserRecord): Funit = record.newBan ?? { ban =>
coll.update(
$doc("_id" -> record.userId),
$doc(
"$unset" -> $doc("o" -> true),
"$push" -> $doc(
$id(record.userId),
$unset("o") ++
$push(
"b" -> $doc(
"$each" -> List(ban),
"$slice" -> -30)
)
)
).void
}
}

View file

@ -62,9 +62,10 @@ object Outcome {
case object Good extends Outcome(0, "Nothing unusual")
case object Abort extends Outcome(1, "Aborts the game")
case object NoPlay extends Outcome(2, "Won't play a move")
case object RageQuit extends Outcome(3, "Quit without resigning")
case object RageQuit extends Outcome(3, "Quits without resigning")
case object Sitting extends Outcome(4, "Lets time run out")
val all = List(Good, Abort, NoPlay, RageQuit)
val all = List(Good, Abort, NoPlay, RageQuit, Sitting)
val byId = all map { v => (v.id, v) } toMap

View file

@ -115,10 +115,9 @@ final class PrefApi(
}
def unfollowableIds(userIds: List[String]): Fu[Set[String]] =
coll.distinct("_id", BSONDocument(
"_id" -> BSONDocument("$in" -> userIds),
coll.distinct("_id", ($inIds(userIds) ++ $doc(
"follow" -> false
).some) map lila.db.BSON.asStringSet
)).some) map lila.db.BSON.asStringSet
def followableIds(userIds: List[String]): Fu[Set[String]] =
unfollowableIds(userIds) map userIds.toSet.diff

View file

@ -95,11 +95,11 @@ private[puzzle] final class PuzzleApi(
def add(a: Attempt) = attemptColl insert a void
def hasPlayed(user: User, puzzle: Puzzle): Fu[Boolean] =
attemptColl.count($doc(
attemptColl.exists($doc(
Attempt.BSONFields.id -> Attempt.makeId(puzzle.id, user.id)
).some) map (0!=)
))
def playedIds(user: User, max: Int): Fu[BSONArray] =
def playedIds(user: User): Fu[BSONArray] =
attemptColl.distinct(Attempt.BSONFields.puzzleId,
$doc(Attempt.BSONFields.userId -> user.id).some
) map BSONArray.apply

View file

@ -35,11 +35,11 @@ private[puzzle] final class Selector(
puzzleColl.find(popularSelector(isMate) ++ mateSelector(isMate))
.skip(Random nextInt anonSkipMax)
.uno[Puzzle]
case Some(user) if user.perfs.puzzle.nb > maxAttempts => fuccess(none)
case Some(user) if user.perfs.puzzle.nb >= maxAttempts => fuccess(none)
case Some(user) =>
val rating = user.perfs.puzzle.intRating min 2300 max 900
val step = toleranceStepFor(rating)
api.attempt.playedIds(user, maxAttempts) flatMap { ids =>
api.attempt.playedIds(user) flatMap { ids =>
tryRange(rating, step, step, difficultyDecay(difficulty), ids, isMate)
}
}

View file

@ -60,7 +60,7 @@ final class QaApi(
questionColl.find($doc("_id" -> id)).uno[Question]
def findByIds(ids: List[QuestionId]): Fu[List[Question]] =
questionColl.find($doc("_id" -> $doc("$in" -> ids.distinct))).cursor[Question]().gather[List]()
questionColl.find($inIds(ids.distinct)).cursor[Question]().gather[List]()
def accept(q: Question) = questionColl.update(
$doc("_id" -> q.id),
@ -99,7 +99,7 @@ final class QaApi(
.cursor[Question]().gather[List](max)
def byTags(tags: List[String], max: Int): Fu[List[Question]] =
questionColl.find($doc("tags" -> $doc("$in" -> tags.map(_.toLowerCase)))).cursor[Question]().gather[List](max)
questionColl.find($doc("tags" $in tags.map(_.toLowerCase))).cursor[Question]().gather[List](max)
def addComment(c: Comment)(q: Question) = questionColl.update(
$doc("_id" -> q.id),
@ -117,7 +117,7 @@ final class QaApi(
}
def incViews(q: Question) = questionColl.update(
$doc("_id" -> q.id),
$id(q.id),
$doc("$inc" -> $doc("views" -> BSONInteger(1))))
def recountAnswers(id: QuestionId) = answer.countByQuestionId(id) flatMap {

View file

@ -115,7 +115,7 @@ private[report] final class ReportApi(coll: Coll) {
def processEngine(userId: String, byModId: String): Funit = coll.update(
$doc(
"user" -> userId,
"reason" -> $in(Reason.Cheat.name, Reason.CheatPrint.name)
"reason" $in List(Reason.Cheat.name, Reason.CheatPrint.name)
) ++ unprocessedSelect,
$set("processedBy" -> byModId),
multi = true).void >>- monitorUnprocessed
@ -123,7 +123,7 @@ private[report] final class ReportApi(coll: Coll) {
def processTroll(userId: String, byModId: String): Funit = coll.update(
$doc(
"user" -> userId,
"reason" -> $in(Reason.Insult.name, Reason.Troll.name, Reason.Other.name)
"reason" $in List(Reason.Insult.name, Reason.Troll.name, Reason.Other.name)
) ++ unprocessedSelect,
$set("processedBy" -> byModId),
multi = true).void >>- monitorUnprocessed

View file

@ -26,11 +26,12 @@ private[round] final class Finisher(
}
def rageQuit(game: Game, winner: Option[Color])(implicit proxy: GameProxy): Fu[Events] =
apply(game, _.Timeout, winner) >>- winner.?? { color => playban.rageQuit(game, !color) }
apply(game, _.Timeout, winner) >>-
winner.?? { color => playban.rageQuit(game, !color) }
def outOfTime(game: Game)(implicit proxy: GameProxy): Fu[Events] = {
import lila.common.PlayApp
if (!PlayApp.startedSinceSeconds(120) && game.updatedAt.exists(_ isBefore PlayApp.startedAt)) {
if (!PlayApp.startedSinceSeconds(60) && game.updatedAt.exists(_ isBefore PlayApp.startedAt)) {
logger.info(s"Aborting game last played before JVM boot: ${game.id}")
other(game, _.Aborted, none)
}
@ -38,7 +39,8 @@ private[round] final class Finisher(
val winner = Some(!game.player.color) filterNot { color =>
game.toChess.board.variant.insufficientWinningMaterial(game.toChess.situation.board, color)
}
other(game, _.Outoftime, winner)
apply(game, _.Outoftime, winner) >>-
winner.?? { color => playban.sittingOrGood(game, !color) }
}
}

View file

@ -28,7 +28,7 @@ final class ForecastApi(coll: Coll, roundMap: akka.actor.ActorSelection) {
private def saveSteps(pov: Pov, steps: Forecast.Steps): Funit = {
lila.mon.round.forecast.create()
coll.update(
$doc("_id" -> pov.fullId),
$id(pov.fullId),
Forecast(
_id = pov.fullId,
steps = steps,
@ -37,7 +37,7 @@ final class ForecastApi(coll: Coll, roundMap: akka.actor.ActorSelection) {
}
def save(pov: Pov, steps: Forecast.Steps): Funit = firstStep(steps) match {
case None => coll.remove($doc("_id" -> pov.fullId)).void
case None => coll.remove($id(pov.fullId)).void
case Some(step) if pov.game.turns == step.ply - 1 => saveSteps(pov, steps)
case _ => fufail(Forecast.OutOfSync)
}
@ -59,7 +59,7 @@ final class ForecastApi(coll: Coll, roundMap: akka.actor.ActorSelection) {
}
def loadForDisplay(pov: Pov): Fu[Option[Forecast]] =
pov.forecastable ?? coll.find($doc("_id" -> pov.fullId)).uno[Forecast] flatMap {
pov.forecastable ?? coll.find($id(pov.fullId)).uno[Forecast] flatMap {
case None => fuccess(none)
case Some(fc) =>
if (firstStep(fc.steps).exists(_.ply != pov.game.turns + 1)) clearPov(pov) inject none
@ -67,7 +67,7 @@ final class ForecastApi(coll: Coll, roundMap: akka.actor.ActorSelection) {
}
def loadForPlay(pov: Pov): Fu[Option[Forecast]] =
pov.game.forecastable ?? coll.find($doc("_id" -> pov.fullId)).uno[Forecast] flatMap {
pov.game.forecastable ?? coll.find($id(pov.fullId)).uno[Forecast] flatMap {
case None => fuccess(none)
case Some(fc) =>
if (firstStep(fc.steps).exists(_.ply != pov.game.turns)) clearPov(pov) inject none
@ -79,7 +79,7 @@ final class ForecastApi(coll: Coll, roundMap: akka.actor.ActorSelection) {
case None => fuccess(none)
case Some(fc) => fc(g, last) match {
case Some((newFc, uciMove)) if newFc.steps.nonEmpty =>
coll.update($doc("_id" -> fc._id), newFc) inject uciMove.some
coll.update($id(fc._id), newFc) inject uciMove.some
case Some((newFc, uciMove)) => clearPov(Pov player g) inject uciMove.some
case _ => clearPov(Pov player g) inject none
}
@ -88,9 +88,7 @@ final class ForecastApi(coll: Coll, roundMap: akka.actor.ActorSelection) {
private def firstStep(steps: Forecast.Steps) = steps.headOption.flatMap(_.headOption)
def clearGame(g: Game) = coll.remove($doc(
"_id" -> $doc("$in" -> chess.Color.all.map(g.fullIdOf))
)).void
def clearGame(g: Game) = coll.remove($inIds(chess.Color.all.map(g.fullIdOf))).void
def clearPov(pov: Pov) = coll.remove($doc("_id" -> pov.fullId)).void
def clearPov(pov: Pov) = coll.remove($id(pov.fullId)).void
}

View file

@ -10,7 +10,7 @@ final class NoteApi(coll: Coll) {
coll.primitiveOne[String]($id(makeId(gameId, userId)), "t") map (~_)
def set(gameId: String, userId: String, text: String) = {
if (text.isEmpty) coll.remove(BSONDocument("_id" -> makeId(gameId, userId)))
if (text.isEmpty) coll.remove($id(makeId(gameId, userId)))
else coll.update(
$id(makeId(gameId, userId)),
$set("t" -> text),

View file

@ -100,8 +100,8 @@ final class Api(
case values => coll.distinct(
"user",
$doc(
field -> $doc("$in" -> values),
"user" -> $doc("$ne" -> userId)
field $in values,
"user" $ne userId
).some
) map lila.db.BSON.asStrings
}
@ -115,7 +115,7 @@ final class Api(
"user",
$doc(
field -> value,
"date" -> $doc("$gt" -> DateTime.now.minusYears(1))
"date" $gt DateTime.now.minusYears(1)
).some
) map lila.db.BSON.asStrings
}

View file

@ -74,8 +74,8 @@ object UserSpy {
values.nonEmpty ?? {
coll.distinct("user",
$doc(
field -> $doc("$in" -> values),
"user" -> $doc("$ne" -> user.id)
field $in values,
"user" $ne user.id
).some
) map lila.db.BSON.asStrings flatMap { userIds =>
userIds.nonEmpty ?? (UserRepo byIds userIds) map (_.toSet)

View file

@ -15,9 +15,11 @@ final class Env(
onStart: String => Unit,
prefApi: lila.pref.PrefApi,
relationApi: lila.relation.RelationApi,
gameCache: lila.game.Cached,
system: ActorSystem) {
private val FriendMemoTtl = config duration "friend.memo.ttl"
private val MaxPlaying = config getInt "max_playing"
private val CollectionUserConfig = config getString "collection.user_config"
private val CollectionAnonConfig = config getString "collection.anon_config"
@ -30,6 +32,8 @@ final class Env(
lazy val processor = new Processor(
lobby = hub.actor.lobby,
gameCache = gameCache,
maxPlaying = MaxPlaying,
fishnetPlayer = fishnetPlayer,
onStart = onStart)
@ -47,5 +51,6 @@ object Env {
onStart = lila.game.Env.current.onStart,
prefApi = lila.pref.Env.current.api,
relationApi = lila.relation.Env.current.api,
gameCache = lila.game.Env.current.cached,
system = lila.common.PlayApp.system)
}

View file

@ -15,6 +15,8 @@ import makeTimeout.short
private[setup] final class Processor(
lobby: ActorSelection,
gameCache: lila.game.Cached,
maxPlaying: Int,
fishnetPlayer: lila.fishnet.Player,
onStart: String => Unit) {
@ -40,20 +42,24 @@ private[setup] final class Processor(
configBase: HookConfig,
uid: String,
sid: Option[String],
blocking: Set[String])(implicit ctx: UserContext): Fu[String] = {
blocking: Set[String])(implicit ctx: UserContext): Fu[Processor.HookResult] = {
import Processor.HookResult._
val config = configBase.fixColor
saveConfig(_ withHook config) >> {
config.hook(uid, ctx.me, sid, blocking) match {
case Left(hook) => fuccess {
lobby ! AddHook(hook)
hook.id
Created(hook.id)
}
case Right(Some(seek)) => fuccess {
lobby ! AddSeek(seek)
seek.id
case Right(Some(seek)) => ctx.userId.??(gameCache.nbPlaying) map { nbPlaying =>
if (nbPlaying >= maxPlaying) Refused
else {
lobby ! AddSeek(seek)
Created(seek.id)
}
}
case Right(None) if ctx.me.isEmpty => fufail(new IllegalArgumentException("Anon can't create seek"))
case _ => fufail("Can't create seek for some unknown reason")
case Right(None) if ctx.me.isEmpty => fuccess(Refused)
case _ => fuccess(Refused)
}
}
}
@ -64,3 +70,12 @@ private[setup] final class Processor(
private def saveConfig(map: UserConfig => UserConfig)(implicit ctx: UserContext): Funit =
ctx.me.fold(AnonConfigRepo.update(ctx.req) _)(user => UserConfigRepo.update(user) _)(map)
}
object Processor {
sealed trait HookResult
object HookResult {
case class Created(id: String) extends HookResult
case object Refused extends HookResult
}
}

View file

@ -56,7 +56,7 @@ final class ChapterRepo(coll: Coll) {
def namesByStudyIds(studyIds: Seq[Study.ID]): Fu[Map[Study.ID, Vector[String]]] =
coll.find(
$doc("studyId" -> $doc("$in" -> studyIds)),
$doc("studyId" $in studyIds),
$doc("studyId" -> true, "name" -> true)
).sort($sort asc "order").list[Bdoc]().map { docs =>
docs.foldLeft(Map.empty[Study.ID, Vector[String]]) {

View file

@ -33,5 +33,5 @@ object RequestRepo {
def selectId(teamId: ID, userId: ID) = $id(Request.makeId(teamId, userId))
def teamQuery(teamId: ID) = $doc("team" -> teamId)
def teamsQuery(teamIds: List[ID]) = $doc("team".$in(teamIds:_*))
def teamsQuery(teamIds: List[ID]) = $doc("team" $in teamIds)
}

View file

@ -21,9 +21,8 @@ private[timeline] final class UnsubApi(coll: Coll) {
coll.count(select(channel, userId).some) map (0 !=)
def filterUnsub(channel: String, userIds: List[String]): Fu[List[String]] =
coll.distinct("_id", $doc(
"_id" $in (userIds.map { makeId(channel, _) }: _*)
).some) map lila.db.BSON.asStrings map { unsubs =>
userIds diff unsubs.map(_ takeWhile ('@' !=))
}
coll.distinct("_id", $inIds(userIds.map { makeId(channel, _) }).some) map
lila.db.BSON.asStrings map { unsubs =>
userIds diff unsubs.map(_ takeWhile ('@' !=))
}
}

View file

@ -30,7 +30,7 @@ object PairingRepo {
def lastOpponents(tourId: String, userIds: Iterable[String], nb: Int): Fu[Pairing.LastOpponents] =
coll.find(
selectTour(tourId) ++ $doc("u" -> $doc("$in" -> userIds)),
selectTour(tourId) ++ $doc("u" $in userIds),
$doc("_id" -> false, "u" -> true)
).sort(recentSort).cursor[Bdoc]().enumerate(nb) |>>>
Iteratee.fold(scala.collection.immutable.Map.empty[String, String]) { (acc, doc) =>

View file

@ -131,7 +131,7 @@ object PlayerRepo {
}
def byTourAndUserIds(tourId: String, userIds: Iterable[String]): Fu[List[Player]] =
coll.find(selectTour(tourId) ++ $doc("uid" -> $doc("$in" -> userIds)))
coll.find(selectTour(tourId) ++ $doc("uid" $in userIds))
.list[Player]()
.chronometer.logIfSlow(200, logger) { players =>
s"PlayerRepo.byTourAndUserIds $tourId ${userIds.size} user IDs, ${players.size} players"

View file

@ -13,10 +13,8 @@ object TournamentRepo {
private lazy val coll = Env.current.tournamentColl
private def $id(id: String) = $doc("_id" -> id)
private val enterableSelect = $doc(
"status" $in (Status.Created.id, Status.Started.id))
"status" $in List(Status.Created.id, Status.Started.id))
private val createdSelect = $doc("status" -> Status.Created.id)
private val startedSelect = $doc("status" -> Status.Started.id)
@ -203,7 +201,7 @@ object TournamentRepo {
def lastFinishedScheduledByFreq(freq: Schedule.Freq, since: DateTime): Fu[List[Tournament]] = coll.find(
finishedSelect ++ sinceSelect(since) ++ variantSelect(chess.variant.Standard) ++ $doc(
"schedule.freq" -> freq.name,
"schedule.speed".$in(Schedule.Speed.mostPopular.map(_.name): _*)
"schedule.speed" $in Schedule.Speed.mostPopular.map(_.name)
)
).sort($doc("startsAt" -> -1))
.list[Tournament](Schedule.Speed.mostPopular.size.some)
@ -213,18 +211,18 @@ object TournamentRepo {
$doc("schedule.freq" -> Schedule.Freq.Daily.name)
).sort($doc("startsAt" -> -1)).uno[Tournament]
def update(tour: Tournament) = coll.update($doc("_id" -> tour.id), tour)
def update(tour: Tournament) = coll.update($id(tour.id), tour)
def insert(tour: Tournament) = coll.insert(tour)
def remove(tour: Tournament) = coll.remove($doc("_id" -> tour.id))
def remove(tour: Tournament) = coll.remove($id(tour.id))
def exists(id: String) = coll.count($doc("_id" -> id).some) map (0 !=)
def exists(id: String) = coll exists $id(id)
def toursToWithdrawWhenEntering(tourId: String): Fu[List[Tournament]] =
coll.find(enterableSelect ++ $doc(
"_id" -> $doc("$ne" -> tourId),
"schedule.freq" $nin (
"_id" $ne tourId,
"schedule.freq" $nin List(
Schedule.Freq.Marathon.name,
Schedule.Freq.Unique.name
)

View file

@ -28,8 +28,6 @@ final class Cached(
f = userColl.count(UserRepo.enabledSelect.some),
timeToLive = nbTtl)
def countEnabled: Fu[Int] = countCache(true)
private implicit val LightUserBSONHandler = Macros.handler[LightUser]
private implicit val LightPerfBSONHandler = Macros.handler[LightPerf]
private implicit val LightCountBSONHandler = Macros.handler[LightCount]

View file

@ -45,8 +45,6 @@ final class Env(
def isOnline(userId: String) = onlineUserIdMemo get userId
def countEnabled = cached.countEnabled
def cli = new lila.common.Cli {
def process = {
case "user" :: "email" :: userId :: email :: Nil =>

View file

@ -23,8 +23,8 @@ final class NoteApi(
coll.find(
$doc(
"to" -> user.id,
"from" -> $doc("$in" -> (myFriendIds + me.id))
) ++ me.troll.fold($doc(), $doc("troll" -> false))
"from" $in (myFriendIds + me.id)
) ++ me.troll.fold($empty, $doc("troll" -> false))
).sort($doc("date" -> -1)).cursor[Note]().gather[List](100)
def write(to: User, text: String, from: User) = {

View file

@ -17,7 +17,7 @@ final class RankingApi(
lightUser: String => Option[lila.common.LightUser]) {
import RankingApi._
private implicit val rankingBSONHandler = reactivemongo.bson.Macros.handler[Ranking]
private implicit val rankingBSONHandler = Macros.handler[Ranking]
def save(userId: User.ID, perfType: Option[PerfType], perfs: Perfs): Funit =
perfType ?? { pt =>
@ -25,9 +25,7 @@ final class RankingApi(
}
def save(userId: User.ID, perfType: PerfType, perf: Perf): Funit =
(perf.nb >= 2) ?? coll.update($doc(
"_id" -> s"$userId:${perfType.id}"
), $doc(
(perf.nb >= 2) ?? coll.update($id(makeId(userId, perfType)), $doc(
"user" -> userId,
"perf" -> perfType.id,
"rating" -> perf.intRating,
@ -38,16 +36,17 @@ final class RankingApi(
def remove(userId: User.ID): Funit = UserRepo byId userId flatMap {
_ ?? { user =>
coll.remove($doc(
"_id" -> $doc("$in" -> PerfType.leaderboardable.filter { pt =>
coll.remove($inIds(
PerfType.leaderboardable.filter { pt =>
user.perfs(pt).nonEmpty
}.map { pt =>
s"${user.id}:${pt.id}"
})
}.map { makeId(user.id, _) }
)).void
}
}
private def makeId(userId: User.ID, perfType: PerfType) =
s"${userId}:${perfType.id}"
def topPerf(perfId: Perf.ID, nb: Int): Fu[List[User.LightPerf]] =
PerfType.id2key(perfId) ?? { perfKey =>
coll.find($doc("perf" -> perfId, "stable" -> true))
@ -82,10 +81,10 @@ final class RankingApi(
val enumerator = coll.find(
$doc("perf" -> perfId, "stable" -> true),
$doc("user" -> true, "_id" -> false)
).sort($doc("rating" -> -1)).cursor[BSONDocument]().enumerate()
).sort($doc("rating" -> -1)).cursor[Bdoc]().enumerate()
var rank = 1
val b = Map.newBuilder[User.ID, Rank]
val mapBuilder: Iteratee[BSONDocument, Unit] = Iteratee.foreach { doc =>
val mapBuilder: Iteratee[Bdoc, Unit] = Iteratee.foreach { doc =>
doc.getAs[User.ID]("user") foreach { user =>
b += (user -> rank)
rank = rank + 1
@ -115,9 +114,9 @@ final class RankingApi(
List(Project($doc(
"_id" -> false,
"r" -> $doc(
"$subtract" -> BSONArray(
"$subtract" -> $arr(
"$rating",
$doc("$mod" -> BSONArray("$rating", Stat.group))
$doc("$mod" -> $arr("$rating", Stat.group))
)
)
)),

View file

@ -85,7 +85,7 @@ object UserRepo {
def orderByGameCount(u1: String, u2: String): Fu[Option[(String, String)]] = {
coll.find(
$doc("_id".$in(u1, u2)),
$inIds(List(u1, u2)),
$doc(s"${F.count}.game" -> true)
).cursor[Bdoc]().gather[List]() map { docs =>
docs.sortBy {
@ -100,7 +100,7 @@ object UserRepo {
def firstGetsWhite(u1O: Option[String], u2O: Option[String]): Fu[Boolean] =
(u1O |@| u2O).tupled.fold(fuccess(scala.util.Random.nextBoolean)) {
case (u1, u2) => coll.find(
$doc("_id".$in(u1, u2)),
$inIds(List(u1, u2)),
$id(true)
).sort($doc(F.colorIt -> 1)).uno[Bdoc].map {
_.fold(scala.util.Random.nextBoolean) { doc =>

View file

@ -41,7 +41,7 @@ private[video] final class VideoApi(
private val maxPerPage = 18
def find(id: Video.ID): Fu[Option[Video]] =
videoColl.find($doc("_id" -> id)).uno[Video]
videoColl.find($id(id)).uno[Video]
def search(user: Option[User], query: String, page: Int): Fu[Paginator[VideoView]] = {
val q = query.split(' ').map { word => s""""$word"""" } mkString " "
@ -61,18 +61,16 @@ private[video] final class VideoApi(
def save(video: Video): Funit =
videoColl.update(
$doc("_id" -> video.id),
$id(video.id),
$doc("$set" -> video),
upsert = true).void
def removeNotIn(ids: List[Video.ID]) =
videoColl.remove(
$doc("_id" $nin (ids: _*))
).void
videoColl.remove($doc("_id" $nin ids)).void
def setMetadata(id: Video.ID, metadata: Youtube.Metadata) =
videoColl.update(
$doc("_id" -> id),
$id(id),
$doc("$set" -> $doc("metadata" -> metadata)),
upsert = false
).void
@ -115,7 +113,7 @@ private[video] final class VideoApi(
def similar(user: Option[User], video: Video, max: Int): Fu[Seq[VideoView]] =
videoColl.find($doc(
"tags" $in (video.tags: _*),
"tags" $in video.tags,
"_id" $ne video.id
)).sort($doc("metadata.likes" -> -1))
.cursor[Video]()

1
public/vendor/stockfish.js vendored Submodule

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

File diff suppressed because one or more lines are too long

View file

@ -13,7 +13,7 @@ module.exports = function(possible, variant, emit) {
var enabled = m.prop(possible() && allowed() && lichess.storage.get(storageKey) === '1');
var started = false;
var pool = makePool({
path: '/assets/vendor/stockfish7.js', // Can't CDN because same-origin policy
path: '/assets/vendor/stockfish.js/stockfish.js', // Can't CDN because same-origin policy
minDepth: minDepth,
maxDepth: maxDepth,
variant: variant

View file

@ -13,6 +13,10 @@ function uncache(url) {
return url + '?_=' + new Date().getTime();
}
function reloadPage() {
location.href = '/training/opening';
}
module.exports = {
attempt: function(ctrl) {
showLoading(ctrl);
@ -35,6 +39,6 @@ module.exports = {
}).then(function(cfg) {
ctrl.reload(cfg);
ctrl.pushState(cfg);
});
}, reloadPage);
}
};

View file

@ -54,7 +54,7 @@ function retry(ctrl) {
}
function reloadPage() {
location.href = '/training';
location.href = '/training';
}
function setDifficulty(ctrl, d) {