Merge branch 'master' into tournamentLeader

* master: (54 commits)
  fix email form
  the signum of zero
  longer crazyhouse and classical daily tournaments
  play opponent berserk sound
  crazyhouse comes first
  unparallize build script to catch errors
  Remove one odd 'entry' word
  bench when document doesn't exist, it's the same story
  benchmark mongodb exist check. count is faster
  disable powertips in game tournament standings
  the powertip on tournament pairings is mostly annoying
  fix unfortunate typo
  Revert "Revert "no longer log insights invalid games""
  ja "日本語" translation #15702. Author: danjyo.
  sr "Српски језик" translation #15701. Author: Charles_Martel.
  la "lingua Latina" translation #15700. Author: Orsi. Checkmate in latin is 'mattus' - https://la.wikipedia.org/wiki/Scacci
  ps "پښتو" translation #15699. Author: qimari. updated many words/sentences. going to do more tomorrow
  ps "پښتو" translation #15698. Author: qimari.
  de "Deutsch" translation #15697. Author: AKA121. 273, 296, 297, 346, 348, 361, 364, 415, 457, 481, 483, 484, 499, 502 (Die meisten Änderungen betreffen Du/Sie Konfigurationen. Ich habe jetzt das meiste zu Du geändert, da der Großteil schon Du war.)
  sl "slovenščina" translation #15696. Author: woodswoods. Better translation for word "board"
  ...
This commit is contained in:
Thibault Duplessis 2016-01-25 09:03:37 +07:00
commit 47ee9d615c
76 changed files with 446 additions and 367 deletions

View file

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

View file

@ -35,7 +35,7 @@ object Message extends LilaController {
implicit me =>
NotForKids {
OptionFuOk(api.thread(id, me)) { thread =>
relationApi.blocks(thread otherUserId me, me.id) map { blocked =>
relationApi.fetchBlocks(thread otherUserId me, me.id) map { blocked =>
html.message.thread(thread, forms.post, blocked,
answerable = !Env.message.LichessSenders.contains(thread.creatorId))
}
@ -48,7 +48,7 @@ object Message extends LilaController {
OptionFuResult(api.thread(id, me)) { thread =>
implicit val req = ctx.body
forms.post.bindFromRequest.fold(
err => relationApi.blocks(thread otherUserId me, me.id) map { blocked =>
err => relationApi.fetchBlocks(thread otherUserId me, me.id) map { blocked =>
BadRequest(html.message.thread(thread, err, blocked,
answerable = !Env.message.LichessSenders.contains(thread.creatorId)))
},

View file

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

View file

@ -50,7 +50,7 @@ object Report extends LilaController {
def thanks(reported: String) = Auth { implicit ctx =>
implicit me =>
Env.relation.api.blocks(me.id, reported) map { blocked =>
Env.relation.api.fetchBlocks(me.id, reported) map { blocked =>
html.report.thanks(reported, blocked)
}
}

View file

@ -61,9 +61,9 @@ object Setup extends LilaController with TheftPrevention {
private def challenge(user: lila.user.User)(implicit ctx: Context): Fu[Option[String]] = ctx.me match {
case None => fuccess("Only registered players can send challenges.".some)
case Some(me) => Env.relation.api.blocks(user.id, me.id) flatMap {
case Some(me) => Env.relation.api.fetchBlocks(user.id, me.id) flatMap {
case true => fuccess(s"{{user}} doesn't accept challenges from you.".some)
case false => Env.pref.api getPref user zip Env.relation.api.follows(user.id, me.id) map {
case false => Env.pref.api getPref user zip Env.relation.api.fetchFollows(user.id, me.id) map {
case (pref, follow) => lila.pref.Pref.Challenge.block(me, user, pref.challenge, follow,
fromCheat = me.engine && !user.engine)
}
@ -72,7 +72,7 @@ object Setup extends LilaController with TheftPrevention {
def friend(userId: Option[String]) = process(env.forms.friend) { config =>
implicit ctx =>
(ctx.userId ?? GameRepo.removeChallengesOf) >> {
(ctx.userId ?? GameRepo.removeRecentChallengesOf) >> {
env.processor friend config map { pov =>
pov -> routes.Setup.await(pov.fullId, userId)
}
@ -113,7 +113,7 @@ object Setup extends LilaController with TheftPrevention {
err => negotiate(
html = BadRequest(errorsAsJson(err).toString).fuccess,
api = _ => BadRequest(errorsAsJson(err)).fuccess),
config => (ctx.userId ?? Env.relation.api.blocking) flatMap {
config => (ctx.userId ?? Env.relation.api.fetchBlocking) flatMap {
blocking =>
env.processor.hook(config, uid, HTTPRequest sid req, blocking) map hookResponse recover {
case e: IllegalArgumentException => BadRequest(Json.obj("error" -> e.getMessage)) as JSON
@ -131,7 +131,7 @@ object Setup extends LilaController with TheftPrevention {
GameRepo game gameId map {
_.fold(config)(config.updateFrom)
} flatMap { config =>
(ctx.userId ?? Env.relation.api.blocking) flatMap { blocking =>
(ctx.userId ?? Env.relation.api.fetchBlocking) flatMap { blocking =>
env.processor.hook(config, uid, HTTPRequest sid ctx.req, blocking) map hookResponse recover {
case e: IllegalArgumentException => BadRequest(Json.obj("error" -> e.getMessage)) as JSON
}

View file

@ -75,7 +75,7 @@ object Team extends LilaController {
me => OptionFuResult(api team id) { team =>
Owner(team) {
MemberRepo userIdsByTeam team.id map { userIds =>
html.team.kick(team, userIds filterNot (me.id ==))
html.team.kick(team, userIds.filterNot(me.id ==).toList.sorted)
}
}
}

View file

@ -43,10 +43,10 @@ object User extends LilaController {
OptionFuResult(UserRepo named username) { user =>
GameRepo lastPlayedPlaying user zip
Env.donation.isDonor(user.id) zip
(ctx.userId ?? { relationApi.blocks(user.id, _) }) zip
(ctx.userId ?? { relationApi.fetchBlocks(user.id, _) }) zip
(ctx.userId ?? { Env.game.crosstableApi(user.id, _) }) zip
(ctx.isAuth ?? { Env.pref.api.followable(user.id) }) zip
(ctx.userId ?? { relationApi.relation(_, user.id) }) map {
(ctx.userId ?? { relationApi.fetchRelation(_, user.id) }) map {
case (((((pov, donor), blocked), crosstable), followable), relation) =>
Ok(html.user.mini(user, pov, blocked, followable, relation, crosstable, donor))
.withHeaders(CACHE_CONTROL -> "max-age=5")
@ -102,12 +102,12 @@ object User extends LilaController {
filter = filters.current,
me = ctx.me,
page = page)(ctx.body)
relation <- ctx.userId ?? { relationApi.relation(_, u.id) }
relation <- ctx.userId ?? { relationApi.fetchRelation(_, u.id) }
notes <- ctx.me ?? { me =>
relationApi friends me.id flatMap { env.noteApi.get(u, me, _) }
relationApi fetchFriends me.id flatMap { env.noteApi.get(u, me, _) }
}
followable <- ctx.isAuth ?? { Env.pref.api followable u.id }
blocked <- ctx.userId ?? { relationApi.blocks(u.id, _) }
blocked <- ctx.userId ?? { relationApi.fetchBlocks(u.id, _) }
searchForm = GameFilterMenu.searchForm(userGameSearch, filters.current)(ctx.body)
} yield html.user.show(u, info, pag, filters, searchForm, relation, notes, followable, blocked)
@ -211,8 +211,8 @@ object User extends LilaController {
fuccess(List.fill(50)(true))
) flatMap { followables =>
(ops zip followables).map {
case ((u, nb), followable) => ctx.userId ?? { myId =>
relationApi.relation(myId, u.id)
case ((u, nb), followable) => ctx.userId ?? {
relationApi.fetchRelation(_, u.id)
} map { lila.relation.Related(u, nb, followable, _) }
}.sequenceFu map { relateds =>
html.user.opponents(user, relateds)

View file

@ -74,9 +74,9 @@ object UserInfo {
gameCached.nbImportedBy(user.id) zip
(ctx.me.filter(user!=) ?? { me => crosstableApi(me.id, user.id) }) zip
getRatingChart(user) zip
relationApi.nbFollowing(user.id) zip
relationApi.nbFollowers(user.id) zip
(ctx.me ?? Granter(_.UserSpy) ?? { relationApi.nbBlockers(user.id) map (_.some) }) zip
relationApi.countFollowing(user.id) zip
relationApi.countFollowers(user.id) zip
(ctx.me ?? Granter(_.UserSpy) ?? { relationApi.countBlockers(user.id) map (_.some) }) zip
postApi.nbByUser(user.id) zip
isDonor(user.id) zip
trophyApi.findByUser(user) zip

View file

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

View file

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

View file

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

View file

@ -178,7 +178,7 @@ withLangAnnotations: Boolean = true)(body: Html)(implicit ctx: Context)
<div class="content list"></div>
<div class="nobody">
<span>@trans.noFriendsOnline()</span>
<a class="find button" href="@routes.Relation.suggest(me.username)">
<a class="find button" href="@routes.User.opponents(me.username)">
<span class="is3 text" data-icon="h">@trans.findFriends()</span>
</a>
</div>

View file

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

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@
<span class="rank">@rank</span>
}
}
@userInfosLink(player.userId, none, withOnline = false)
@userInfosLink(player.userId, none, withOnline = false, withPowerTip = false)
</td>
<td class="total">
<strong@if(player.fire) { class="is-gold" data-icon="Q" }>@player.score</strong>

View file

@ -68,7 +68,7 @@ description = describeUser(u)).some) {
<a class="button hint--bottom-left" href="@routes.Account.profile" data-hint="@trans.editProfile()">
<span data-icon="%"></span>
</a>
<a class="button hint--bottom-left" href="@routes.Relation.blocks" data-hint="@trans.listBlockedPlayers()">
<a class="button hint--bottom-left" href="@routes.Relation.blocks()" data-hint="@trans.listBlockedPlayers()">
<span data-icon="k"></span>
</a>
}
@ -176,7 +176,7 @@ description = describeUser(u)).some) {
<p>@trans.tpTimeSpentOnTV(showPeriod(tvPeriod))</p>
}
<div class="teams">
@teamIds(u.id).sortBy(t => !myTeam(t)).map { teamId =>
@teamIds(u.id).toList.sortBy(t => !myTeam(t)).map { teamId =>
@teamLink(teamId, myTeam(teamId).option("mine"))
}
</div>

View file

@ -1,10 +1,15 @@
@(users: List[lila.relation.Related])(implicit ctx: Context)
@(pager: Paginator[lila.relation.Related], call: Call)(implicit ctx: Context)
<table class="slist">
@if(users.size > 0) {
@if(pager.nbResults > 0) {
<tbody class="infinitescroll">
@users.map { r =>
<tr>
@pager.nextPage.map { np =>
<tr><th class="pager none">
<a href="@call.url?page=@np">Next</a>
</th></tr>
}
@pager.currentPageResults.map { r =>
<tr class="paginated_element">
<td>@userLink(r.user)</td>
<td>@showBestPerf(r.user)</td>
<td>@r.user.count.game @trans.games()</td>

View file

@ -0,0 +1,26 @@
var limit = 50 * 1000;
var coll = db.relation;
var query = {
_id: 'thibault/legendary22bcloud'
};
var expected = false;
function timer(name, f) {
print('Start ' + name);
var start = new Date().getTime();
if (f() !== expected) print('FAILS');
else {
for (var i = 0; i < limit; i++) f();
print(name + ': ' + (new Date().getTime() - start));
}
}
timer('count', function() {
return coll.count(query) === 1;
});
timer('find', function() {
return coll.find(query).limit(1).length() === 1;
});
timer('findOne', function() {
return coll.findOne(query) !== null;
});

View file

@ -168,11 +168,10 @@ build_lila() {
cd -- "$LILA_DIR"
git submodule update --init --recursive
./ui/build &
./bin/install-stockfish &
./bin/gen/geoip &
./bin/build-deps.sh &
wait
./ui/build
./bin/install-stockfish
./bin/gen/geoip
./bin/build-deps.sh
sbt compile
}
@ -194,7 +193,7 @@ main() {
local ip_address
ip_address=$(get_ip_address)
info 'Lila is all set up! Add this entry entry to your hosts file on your'
info 'Lila is all set up! Add this entry to your hosts file on your'
info 'host machine (not the virtual machine, or else I would have done it'
info 'for you):'
info

View file

@ -9,7 +9,7 @@ net {
ip = "5.196.91.160"
asset {
domain = ${net.domain}
version = 818
version = 819
}
}
play {

View file

@ -270,7 +270,7 @@ progressToday=Fortschritt heute
progressThisWeek=Fortschritt diese Woche
progressThisMonth=Fortschritt diesen Monat
leaderboardThisWeek=Führende Spieler diese Woche
leaderboardThisMonth=Führende Spieler dieser Monat
leaderboardThisMonth=Führende Spieler diesen Monat
activeToday=Heute aktiv
activePlayers=Aktive Spieler
bewareTheGameIsRatedButHasNoClock=Achtung! Das Spiel wird zwar gewertet, aber ohne Uhr gespielt!
@ -293,8 +293,8 @@ butYouCanKeepTrying=Aber Du kannst trotzdem weitermachen.
victory=Geschafft!
giveUp=Aufgeben
puzzleSolvedInXSeconds=Rätsel in %s Sekunden gelöst.
wasThisPuzzleAnyGood=Wie fandest du dieses Rätsel?
pleaseVotePuzzle=Mache Lichess besser, indem du abstimmst.
wasThisPuzzleAnyGood=Wie fandest Du dieses Rätsel?
pleaseVotePuzzle=Mache Lichess besser, indem Du abstimmst.
thankYou=Vielen Dank!
ratingX=Schwierigkeitsgrad: %s
playedXTimes=%s mal gespielt
@ -343,9 +343,9 @@ cheat=Betrug
insult=Beleidigung
troll=Troll
other=Anderes
reportDescriptionHelp=Füge den Link zum Spiel ein und erkläre die Auffälligkeiten bezüglich des Spielerverhaltens. Bitte schreibe nicht einfach nur „dieser Spieler betrügt“, sondern begründe auch, wie du zu diesem Schluss kommst. Dein Bericht wird schneller bearbeitet, wenn er in englischer Sprache verfasst ist.
reportDescriptionHelp=Füge den Link zum Spiel ein und erkläre die Auffälligkeiten bezüglich des Spielerverhaltens. Bitte schreibe nicht einfach nur „dieser Spieler betrügt“, sondern begründe auch, wie Du zu diesem Schluss kommst. Dein Bericht wird schneller bearbeitet, wenn er in englischer Sprache verfasst ist.
by=von %s
thisTopicIsNowClosed=Das Thema ist jetzt geschlossen.
thisTopicIsNowClosed=Dieses Thema ist jetzt geschlossen.
theming=Design
donate=Spenden
blog=Blog
@ -358,10 +358,10 @@ materialDifference=Materialunterschiede
closeAccount=Mitgliedschaft beenden
closeYourAccount=Deine Mitgliedschaft beenden
changedMindDoNotCloseAccount=Habe meine Meinung geändert, die Mitgliedschaft doch nicht beenden
closeAccountExplanation=Möchten Sie die Mitgliedschaft wirklich beenden? Diese Entscheidung ist endgültig. Ein Login ist danach nicht mehr möglich und die Profilseite nicht mehr verfügbar.
closeAccountExplanation=Möchtest Du die Mitgliedschaft wirklich beenden? Diese Entscheidung ist endgültig. Ein Login ist danach nicht mehr möglich und die Profilseite nicht mehr verfügbar.
thisAccountIsClosed=Dieses Mitgliedskonto ist geschlossen.
invalidUsernameOrPassword=Ungültiger Benutzername oder Passwort
emailMeALink=Sende mir einen Link per E-Mail
emailMeALink=Schicke mir einen Link per E-Mail
currentPassword=Derzeitiges Passwort
newPassword=Neues Passwort
newPasswordAgain=Neues Passwort (wiederholen)
@ -412,7 +412,7 @@ allInformationIsPublicAndOptional=Alle Informationen sind öffentlich und freiwi
yourCityRegionOrDepartment=Deine Stadt, Region, Kanton oder Bundesland.
biographyDescription=Über dich, was du am Schach magst, Lieblingseröffnungen, Spiele, Spieler…
maximumNbCharacters=Maximal: %s Zeichen.
blocks=%s gesperrt
blocks=%s gesperrte Spieler
listBlockedPlayers=Liste der gesperrten Spieler
human=Mensch
computer=Computer
@ -454,7 +454,7 @@ blackCastlingKingside=Schwarz O-O
blackCastlingQueenside=Schwarz O-O-O
nbForumPosts=%s Forumbeiträge
tpTimeSpentPlaying=Gesamte Spielzeit: %s
watchGames=Spiele ansehen
watchGames=Partien ansehen
tpTimeSpentOnTV=Zeit im TV: %s
watch=Zuschauen
internationalEvents=Internationale Events
@ -478,10 +478,10 @@ aboutSimulRealLife=Das Konzept ähnelt dem bei echten Simultanveranstaltungen, w
aboutSimulRules=Beim Start des Simultan beginnt der Alleinspieler mit Weiß und spielt so lange mit wechselnden Gegnern, bis alle Partien beendet sind.
aboutSimulSettings=Simultane sind immer ungewertet. Revanchen, Zugrücknahme und zusätzliche Zeit sind deaktiviert.
create=Erstellen
whenCreateSimul=Wenn Sie ein Simultan erzeugen, spielen Sie mit mehreren Gegnern gleichzeitig.
whenCreateSimul=Wenn Du ein Simultan erzeugst, spielst Du mit mehreren Gegnern gleichzeitig.
simulVariantsHint=Wurden mehrere Varianten gewählt, kann jeder Spieler eine Variante wählen.
simulClockHint=Fischer Bedenkzeit. Je mehr Gegner Sie haben, desto mehr Zeit werden Sie benötigen.
simulAddExtraTime=Je mehr zusätzliche Bedenkzeit Sie Ihrer eigenen Uhr gönnen, desto einfacher wird es für Sie die Lage zu meistern.
simulClockHint=Fischer Bedenkzeit. Je mehr Gegner Du hast, desto mehr Zeit wirst Du benötigen.
simulAddExtraTime=Du kannst zusätzliche Zeit hinzufügen, um mit dem Simultan zurechtzukommen.
simulHostExtraTime=Extra Bedenkzeit für Alleinspieler
lichessTournaments=Lichess Turnier
tournamentFAQ=Arena Turnier FAQ
@ -493,13 +493,13 @@ keyMoveBackwardOrForward=Zug zurück/vor
keyGoToStartOrEnd=Zum Anfang/Ende
keyShowOrHideComments=Zeige/verberge Kommentare
keyEnterOrExitVariation=Variante wählen/verlassen
keyYouCanDrawArrowsCirclesAndScrollToMove=Drücke Umschalt+Mausklick oder Rechtsklick um Kreise und Pfeile auf dem Brett zu zeichnen. Ebenso ist scrollen über dem Brett möglich, um die Züge zu durchlaufen.
keyYouCanDrawArrowsCirclesAndScrollToMove=Drücke Umschalt+Mausklick oder Rechtsklick, um Kreise und Pfeile auf dem Brett zu zeichnen. Ebenso ist scrollen über dem Brett möglich, um die Züge zu durchlaufen.
newTournament=Neues Turnier
tournamentHomeTitle=Schachturnier mit verschiedenen Zeitkontrollen und Schachvarianten
tournamentHomeDescription=Spielen Sie richtig schnelle Turniere! Treten Sie einem geplanten Turnier bei oder eröffnen Sie ein Neues. Bullet, Blitz, Classical, Chess960, King of the Hill, Threecheck und weitere Varianten/Optionen sind möglich für grenzenlosen Spaß.
tournamentHomeDescription=Spielen Sie richtig schnelle Turniere! Treten Sie einem geplanten Turnier bei oder erstellen Sie ein Neues. Bullet, Blitz, Classical, Chess960, King of the Hill, Threecheck und weitere Varianten für grenzenlosen Spaß!
tournamentNotFound=Turnier nicht gefunden
tournamentDoesNotExist=Dieses Turnier existiert nicht.
tournamentMayHaveBeenCanceled=Womöglich wurde es abgesagt, falls kein Spieler zu Turnierbeginn (mehr) registriert war.
tournamentMayHaveBeenCanceled=Womöglich wurde es abgesagt, weil kein Spieler zu Turnierbeginn (mehr) registriert war.
returnToTournamentsHomepage=Zurück zur Turnier Homepage
monthlyPerfTypeRatingDistribution=Monatliche %s-Wertungsverteilung
nbPerfTypePlayersThisMonth=%s %s Spieler diesen Monat.

View file

@ -107,7 +107,7 @@ declineInvitation=招待を断る
cancel=キャンセル
timeOut=時間切れ
drawOfferSent=引き分けの申し込みを送信しました
drawOfferDeclined=引き分けの申し込みは断られました
drawOfferDeclined=引き分けの申し込みは拒否されました
drawOfferAccepted=引き分けの申し込みに合意しました
drawOfferCanceled=引き分けの申し込みをキャンセルしました
whiteOffersDraw=白が引き分けを申し込んでいます
@ -116,7 +116,7 @@ whiteDeclinesDraw=白が引き分けを拒否しました
blackDeclinesDraw=黒が引き分けを拒否しました
yourOpponentOffersADraw=相手が引き分けを申し込みました
accept=承諾
decline=断る
decline=拒否
playingRightNow=対局中
finished=終了したトーナメント
abortGame=対局を中止する
@ -183,7 +183,7 @@ joinTeam=チームに参加
quitTeam=チームを辞める
anyoneCanJoin=誰でも参加可能
aConfirmationIsRequiredToJoin=参加には確認が必要
joiningPolicy=参加ポリシー
joiningPolicy=参加規則
teamLeader=チームリーダー
teamBestPlayers=チームのベストプレイヤー
teamRecentMembers=最新チームメンバー
@ -295,7 +295,7 @@ giveUp=ギブアップ
puzzleSolvedInXSeconds=パズルを解くのにかかった時間は%s秒です。
wasThisPuzzleAnyGood=このパズルが気に入りましたか?
pleaseVotePuzzle=改善のためご意見をお聞かせください。上矢印、下矢印をお使いください:
thankYou=謝辞
thankYou=ありが
ratingX=レーティング: %s
playedXTimes=挑戦回数%s回
fromGameLink=ゲーム%sから

View file

@ -201,7 +201,7 @@ fromPosition=정해진 보드판에서 시작
continueFromHere=여기서부터 시작
importGame=게임 불러오기
nbImportedGames=불러온 게임 %s개
thisIsAChessCaptcha=자동 입을 방지하기 위한 체스 퀴즈입니다.
thisIsAChessCaptcha=자동 입을 방지하기 위한 체스 퀴즈입니다.
clickOnTheBoardToMakeYourMove=보드를 클릭해서 체스 퍼즐을 풀고 당신이 사람임을 알려주세요.
notACheckmate=체크메이트가 아닙니다.
colorPlaysCheckmateInOne=%s의 차례입니다. 한 수 만에 체크메이트하세요.
@ -494,3 +494,5 @@ tournamentNotFound=토너먼트를 찾을 수 없습니다
tournamentDoesNotExist=존재하지 않는 토너먼트 입니다
tournamentMayHaveBeenCanceled=모든 플레이어가 퇴장하여 취소된 게임일 수 있습니다
returnToTournamentsHomepage=토너먼트 홈으로 돌아가기
yourPerfTypeRatingisRating=당신의 %s 레이팅은 %s입니다.
youDoNotHaveAnEstablishedPerfTypeRating=아직 확정된 %s 레이팅을 갖지 않으셨습니다.

View file

@ -11,7 +11,7 @@ toggleTheChat=Sermonem aperi aut claude
toggleSound=Sonum permitte aut nega
chat=Sermo
resign=Decede
checkmate=Rex alligatus
checkmate=Mattus
stalemate=Rex impeditus
white=Albus
black=Niger
@ -203,7 +203,7 @@ importGame=Lusionem imponere
nbImportedGames=%s impositae lusiones
thisIsAChessCaptcha=Hic ludus CAPTCHA est
clickOnTheBoardToMakeYourMove=Age motum ut tuum esse humanum confirmem.
notACheckmate=rex non captus est
notACheckmate=Nulus mattus
colorPlaysCheckmateInOne=%s agens; mattus moto uno
retry=Adparare iterum
reconnecting=Reconectens

View file

@ -5,22 +5,35 @@ gameOver=د لوبې پای
waitingForOpponent=سیال ته تم شئ
waiting=تم کېدل
yourTurn=ستا نوبت
level=پوړ
aiNameLevelAiLevel=%s سويه %s
level=سويه
toggleTheChat=دبنډار څرنګتیاونج تڼۍ
toggleSound=غږ څرنګتیاونج تڼۍ
chat=بنډار
resign=پرښودنه
checkmate=ټولمات
white=سپن
stalemate=بندون
white=سپين
black=تور
randomColor=توکلي
createAGame=لوبه جوړه کړئ
whiteIsVictorious=سپین سوبمن دی
blackIsVictorious=تور سوبمن دی
whiteIsVictorious=سپین فاتح دی
blackIsVictorious=تور فاتح دی
kingInTheCenter=باچا مينځ کښې
threeChecks=درې کشت
variantEnding=مختلف پای
playWithTheSameOpponentAgain=همدا سیال سره بیا لوبه وکړئ
newOpponent=نوی سیال
playWithAnotherOpponent=د بل سیال سره لوبه وکړئ
yourOpponentWantsToPlayANewGameWithYou=سیال مو نوې لوبه پیلول غواړي
joinTheGame=يوځلی بيا
whitePlays=سپین لوبه کوی
blackPlays=تور لوبه کوی
talkInChat=مرکځای
theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim=-سيال کېدې شي چی لوبه پرېښودلی وي -اقتدار لری چی لوبه ترلاسه كړی, يا مساوي كړی, او يا صبر وکړی
makeYourOpponentResign=په زوره لوبه وګټی
forceResignation=لوبه ترلاسه كړی
forceDraw=مساوي كړی
talkInChat=!لطفاٌ ښه وينه وکړی
whiteCreatesTheGame=سپین د لوبې جوړونکی دی
blackCreatesTheGame=تور د لوبې جوړونکی دی
whiteResigned=سپین غاړه کېښود

View file

@ -5,7 +5,7 @@ gameOver=Konec igre
waitingForOpponent=Čakam nasprotnika
waiting=Čakam
yourTurn=Ti si na potezi
aiNameLevelAiLevel=Ime: %s, stopnja: %s
aiNameLevelAiLevel=%s, stopnja: %s
level=Stopnja
toggleTheChat=Omogoči/onemogoči klepet
toggleSound=Vključi/Izključi zvok
@ -474,7 +474,7 @@ noSimulExplanation=Ta simultanka ne obstaja.
returnToSimulHomepage=Vrni se na Simultanke
aboutSimul=Simultanka je igra enega proti več igralcem hkrati.
aboutSimulImage=Od 50 nasprotnikov, je Fischer zmagal 47 partij, 2 remiziral in 1 izgubil.
aboutSimulRealLife=Koncept je vzet iz vsakdanjega življenja. V realnem svetu se igralec premika od deske do deske in odigra eno potezo.
aboutSimulRealLife=Koncept je vzet iz vsakdanjega življenja. V realnem svetu se igralec premika od šahovnice do šahovnice in odigra eno potezo.
aboutSimulRules=V simultanki ima igralec-gostitelj zmeraj bele figure. Simultanka se zaključi, ko so vse partije končane.
aboutSimulSettings=Simultanke so zmeraj nerangirane. Revanše, popravki potez in dodajanje časa niso možni.
create=Ustvari

View file

@ -502,3 +502,7 @@ tournamentDoesNotExist=Turnir ne postoji
tournamentMayHaveBeenCanceled=Možda je otkazan, jer su igrači napustili turnir pre njegovog početka
returnToTournamentsHomepage=Vratite se na "tournaments" početnu stranu
monthlyPerfTypeRatingDistribution=Месечна %s дитрибуција рејтинга
nbPerfTypePlayersThisMonth=%s %s играчи овај месец.
yourPerfTypeRatingisRating=твоје %s рејтинг је %s.
youAreBetterThanPercentOfPerfTypePlayers=ви сте бољи од %s од %s играча.
youDoNotHaveAnEstablishedPerfTypeRating=Немате утврђен %s рејтинг.

View file

@ -37,10 +37,9 @@ POST /rel/follow/:userId controllers.Relation.follow(userId: Strin
POST /rel/unfollow/:userId controllers.Relation.unfollow(userId: String)
POST /rel/block/:userId controllers.Relation.block(userId: String)
POST /rel/unblock/:userId controllers.Relation.unblock(userId: String)
GET /@/:username/following controllers.Relation.following(username: String)
GET /@/:username/followers controllers.Relation.followers(username: String)
GET /@/:username/suggestions controllers.Relation.suggest(username: String)
GET /rel/blocks controllers.Relation.blocks
GET /@/:username/following controllers.Relation.following(username: String, page: Int ?= 1)
GET /@/:username/followers controllers.Relation.followers(username: String, page: Int ?= 1)
GET /rel/blocks controllers.Relation.blocks(page: Int ?= 1)
# Insight
POST /insights/refresh/:username controllers.Insight.refresh(username: String)

View file

@ -54,7 +54,7 @@ final class Env(
_.flatMap(_.getAs[BSONNumberLike]("version"))
.fold(Net.AssetVersion)(_.toInt max Net.AssetVersion)
},
timeToLive = 1 minute,
timeToLive = 30.seconds,
default = Net.AssetVersion)
def get = cache get true
}

View file

@ -26,7 +26,7 @@ private[api] final class UserApi(
token: Option[String],
nb: Option[Int],
engine: Option[Boolean]): Fu[JsObject] = (team match {
case Some(teamId) => lila.team.MemberRepo.userIdsByTeam(teamId) flatMap UserRepo.enabledByIds
case Some(teamId) => lila.team.MemberRepo userIdsByTeam teamId flatMap UserRepo.enabledByIds
case None => $find(pimpQB($query(
UserRepo.enabledSelect ++ (engine ?? UserRepo.engineSelect)
)) sort UserRepo.sortPerfDesc(lila.rating.PerfType.Standard.key), makeNb(nb, token))
@ -45,12 +45,12 @@ private[api] final class UserApi(
case None => fuccess(none)
case Some(u) => GameRepo mostUrgentGame u zip
(ctx.me.filter(u!=) ?? { me => crosstableApi.nbGames(me.id, u.id) }) zip
relationApi.nbFollowing(u.id) zip
relationApi.nbFollowers(u.id) zip
relationApi.countFollowing(u.id) zip
relationApi.countFollowers(u.id) zip
ctx.isAuth.?? { prefApi followable u.id } zip
ctx.userId.?? { relationApi.relation(_, u.id) } zip
ctx.userId.?? { relationApi.relation(u.id, _) } map {
case ((((((gameOption, nbGamesWithMe), following), followers), followable), relation), revRelation) =>
ctx.userId.?? { relationApi.fetchRelation(_, u.id) } zip
ctx.userId.?? { relationApi.fetchFollows(u.id, _) } map {
case ((((((gameOption, nbGamesWithMe), following), followers), followable), relation), isFollowed) =>
jsonView(u, extended = true) ++ {
Json.obj(
"url" -> makeUrl(s"@/$username"),
@ -71,9 +71,9 @@ private[api] final class UserApi(
"me" -> nbGamesWithMe)
) ++ ctx.isAuth.??(Json.obj(
"followable" -> followable,
"following" -> relation.exists(true ==),
"blocking" -> relation.exists(false ==),
"followsYou" -> revRelation.exists(true ==)
"following" -> relation.contains(true),
"blocking" -> relation.contains(false),
"followsYou" -> isFollowed
))
}.noNull
} map (_.some)

View file

@ -2,6 +2,8 @@ package lila.bookmark
import org.joda.time.DateTime
import play.api.libs.json._
import reactivemongo.bson._
import lila.db.api._
import lila.db.Implicits._
import tube.bookmarkTube
@ -9,6 +11,7 @@ import tube.bookmarkTube
case class Bookmark(game: lila.game.Game, user: lila.user.User)
private[bookmark] object BookmarkRepo {
def toggle(gameId: String, userId: String): Fu[Boolean] =
$count exists selectId(gameId, userId) flatMap { e =>
e.fold(
@ -17,8 +20,8 @@ private[bookmark] object BookmarkRepo {
) inject !e
}
def gameIdsByUserId(userId: String): Fu[List[String]] =
$primitive(userIdQuery(userId), "g")(_.asOpt[String])
def gameIdsByUserId(userId: String): Fu[Set[String]] =
bookmarkTube.coll.distinct("g", BSONDocument("u" -> userId).some) map lila.db.BSON.asStringSet
def removeByGameId(gameId: String): Funit =
$remove(Json.obj("g" -> gameId))

View file

@ -6,7 +6,7 @@ import lila.memo.MixedCache
private[bookmark] final class Cached {
private[bookmark] val gameIdsCache = MixedCache[String, Set[String]](
(userId: String) => BookmarkRepo gameIdsByUserId userId map (_.toSet),
BookmarkRepo.gameIdsByUserId,
timeToLive = 1 day,
default = _ => Set.empty)

View file

@ -15,6 +15,9 @@ object PlayApp {
def startedSinceMinutes(minutes: Int) =
startedAt.isBefore(DateTime.now minusMinutes minutes)
def startedSinceSeconds(seconds: Int) =
startedAt.isBefore(DateTime.now minusSeconds seconds)
def loadConfig: Config = withApp(_.configuration.underlying)
def loadConfig(prefix: String): Config = loadConfig getConfig prefix

View file

@ -24,7 +24,7 @@ trait AdapterLike[A] {
def nbResults = AdapterLike.this.nbResults
def slice(offset: Int, length: Int) =
AdapterLike.this.slice(offset, length) map (_.toList) map2 f
AdapterLike.this.slice(offset, length) map { _ map f }
}
def mapFuture[B](f: A => Fu[B]): AdapterLike[B] = new AdapterLike[B] {

View file

@ -187,10 +187,21 @@ object BSON {
case (k, v) => s"$k: ${debug(v)}"
}).mkString("{", ", ", "}")
def asString(v: BSONValue): Option[String] = v match {
case BSONString(s) => Some(s)
case _ => None
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 asStrings(vs: List[BSONValue]): List[String] = vs flatMap asString
def asStringSet(vs: List[BSONValue]): Set[String] = {
val b = Set.newBuilder[String]
vs foreach {
case BSONString(s) => b += s
case _ =>
}
b.result
}
}

View file

@ -20,6 +20,15 @@ final class DonationApi(
userId => donatedByUser(userId).map(_ >= minAmount),
maxCapacity = 5000)
// in $ cents
private def donatedByUser(userId: String): Fu[Int] =
coll.aggregate(
Match(decentAmount ++ BSONDocument("userId" -> userId)), List(
Group(BSONNull)("net" -> SumField("net"))
)).map {
~_.documents.headOption.flatMap { _.getAs[Int]("net") }
}
private val decentAmount = BSONDocument("gross" -> BSONDocument("$gte" -> BSONInteger(minAmount)))
def list(nb: Int) = coll.find(decentAmount)
@ -32,9 +41,7 @@ final class DonationApi(
GroupField("userId")("total" -> SumField("net")),
Sort(Descending("total")),
Limit(nb))).map {
_.documents.flatMap { obj =>
obj.getAs[String]("_id")
}
_.documents.flatMap { _.getAs[String]("_id") }
}
def isDonor(userId: String) =
@ -53,15 +60,6 @@ final class DonationApi(
progress = prog.percent), 'donation)
}
// in $ cents
def donatedByUser(userId: String): Fu[Int] =
coll.find(
decentAmount ++ BSONDocument("userId" -> userId),
BSONDocument("net" -> true, "_id" -> false)
).cursor[BSONDocument]().collect[List]() map2 { (obj: BSONDocument) =>
~obj.getAs[Int]("net")
} map (_.sum)
def progress: Fu[Progress] = {
val from = DateTime.now withDayOfMonth 1 withHourOfDay 0 withMinuteOfHour 0 withSecondOfMinute 0
val to = from plusMonths 1

View file

@ -9,7 +9,7 @@ import tube._
private[forum] final class CategApi(env: Env) {
def list(teams: List[String], troll: Boolean): Fu[List[CategView]] = for {
def list(teams: Set[String], troll: Boolean): Fu[List[CategView]] = for {
categs CategRepo withTeams teams
views (categs map { categ =>
env.postApi get (categ lastPostId troll) map { topicPost =>

View file

@ -10,7 +10,7 @@ object CategRepo {
def bySlug(slug: String) = $find byId slug
def withTeams(teams: List[String]): Fu[List[Categ]] =
def withTeams(teams: Set[String]): Fu[List[Categ]] =
$find($query($or(Seq(
Json.obj("team" -> $exists(false)),
Json.obj("team" -> $in(teams))
@ -22,6 +22,6 @@ object CategRepo {
_ sort $sort.desc("pos")
)(_.asOpt[Int]) map (~_ + 1)
def nbPosts(id: String): Fu[Int] =
def nbPosts(id: String): Fu[Int] =
$primitive.one($select(id), "nbPosts")(_.asOpt[Int]) map (~_)
}

View file

@ -13,7 +13,7 @@ private[forum] final class Recent(
nb: Int,
publicCategIds: List[String]) {
private type GetTeams = String => List[String]
private type GetTeams = String => Set[String]
def apply(user: Option[User], getTeams: GetTeams): Fu[List[MiniForumPost]] =
userCacheKey(user, getTeams) |> { key => cache(key)(fetch(key)) }
@ -32,7 +32,7 @@ private[forum] final class Recent(
user.fold("en")(_.langs.mkString(",")) :: {
(user.??(_.troll) ?? List("[troll]")) :::
(user ?? MasterGranter(Permission.StaffForum)).fold(staffCategIds, publicCategIds) :::
((user.map(_.id) ?? getTeams) map teamSlug)
((user.map(_.id) ?? getTeams) map teamSlug).toList
} mkString ";"
private lazy val staffCategIds = "staff" :: publicCategIds

View file

@ -6,12 +6,12 @@ object BestOpponents {
def apply(userId: String, limit: Int): Fu[List[(User, Int)]] =
GameRepo.bestOpponents(userId, limit) flatMap { opponents =>
UserRepo enabledByIds opponents.map(_._1) map { users =>
(users map { user =>
UserRepo enabledByIds opponents.map(_._1) map {
_ flatMap { user =>
opponents find (_._1 == user.id) map { opponent =>
user -> opponent._2
}
}).flatten sortBy (-_._2)
} sortBy (-_._2)
}
}
}

View file

@ -77,11 +77,11 @@ final class CrosstableApi(coll: Coll) {
BSONDocument(Game.BSONFields.winnerId -> true)
).sort(BSONDocument(Game.BSONFields.createdAt -> -1))
.cursor[BSONDocument]().collect[List](maxGames).map {
_.map { doc =>
_.flatMap { doc =>
doc.getAs[String](Game.BSONFields.id).map { id =>
Result(id, doc.getAs[String](Game.BSONFields.winnerId))
}
}.flatten.reverse
}.reverse
}
nbGames <- gameColl.count(selector.some)
ctDraft = Crosstable(Crosstable.User(su1, 0), Crosstable.User(su2, 0), localResults, nbGames)

View file

@ -187,7 +187,10 @@ object GameRepo {
$count.exists($select(id) ++ Query.analysed(true))
def filterAnalysed(ids: Seq[String]): Fu[Set[String]] =
$primitive(($select byIds ids) ++ Query.analysed(true), "_id")(_.asOpt[String]) map (_.toSet)
gameTube.coll.distinct("_id", BSONDocument(
"_id" -> BSONDocument("$in" -> ids),
F.analysed -> true
).some) map lila.db.BSON.asStringSet
def incBookmarks(id: ID, value: Int) =
$update($select(id), $incBson(F.bookmarks -> value))
@ -255,8 +258,9 @@ object GameRepo {
$insert bson bson
}
def removeChallengesOf(userId: String) =
$remove(Query.created ++ Query.friend ++ Query.user(userId))
def removeRecentChallengesOf(userId: String) =
$remove(Query.created ++ Query.friend ++ Query.user(userId) ++
Query.createdSince(DateTime.now minusHours 1))
def setCheckAt(g: Game, at: DateTime) =
$update($select(g.id), BSONDocument("$set" -> BSONDocument(F.checkAt -> at)))
@ -313,7 +317,6 @@ object GameRepo {
}).sequenceFu
def bestOpponents(userId: String, limit: Int): Fu[List[(String, Int)]] = {
val col = gameTube.coll
import reactivemongo.api.collections.bson.BSONBatchCommands.AggregationFramework, AggregationFramework.{
Descending,
GroupField,
@ -323,8 +326,7 @@ object GameRepo {
SumValue,
Unwind
}
col.aggregate(Match(BSONDocument(F.playerUids -> userId)), List(
gameTube.coll.aggregate(Match(BSONDocument(F.playerUids -> userId)), List(
Match(BSONDocument(F.playerUids -> BSONDocument("$size" -> 2))),
Sort(Descending(F.createdAt)),
Limit(1000), // only look in the last 1000 games
@ -378,15 +380,6 @@ object GameRepo {
)
).one[BSONDocument] map { _ flatMap extractPgnMoves }
def associatePgn(ids: Seq[ID]): Fu[Map[String, PgnMoves]] =
gameTube.coll.find($select byIds ids)
.cursor[BSONDocument]()
.collect[List]() map2 { (obj: BSONDocument) =>
extractPgnMoves(obj) flatMap { moves =>
obj.getAs[String]("_id") map (_ -> moves)
}
} map (_.flatten.toMap)
def lastGameBetween(u1: String, u2: String, since: DateTime): Fu[Option[Game]] = {
$find.one(Json.obj(
F.playerUids -> Json.obj("$all" -> List(u1, u2)),
@ -410,7 +403,6 @@ object GameRepo {
).one[BSONDocument] map { ~_.flatMap(_.getAs[List[String]](F.playerUids)) }
def activePlayersSince(since: DateTime, max: Int): Fu[List[UidNb]] = {
val col = gameTube.coll
import reactivemongo.api.collections.bson.BSONBatchCommands.AggregationFramework, AggregationFramework.{
Descending,
GroupField,
@ -421,7 +413,7 @@ object GameRepo {
Unwind
}
col.aggregate(Match(BSONDocument(
gameTube.coll.aggregate(Match(BSONDocument(
F.createdAt -> BSONDocument("$gt" -> since),
F.status -> BSONDocument("$gte" -> chess.Status.Mate.id),
s"${F.playerUids}.0" -> BSONDocument("$exists" -> true)

View file

@ -56,7 +56,7 @@ object Env {
lazy val current: Env = "insight" boot new Env(
config = lila.common.PlayApp loadConfig "insight",
getPref = lila.pref.Env.current.api.getPrefById,
areFriends = lila.relation.Env.current.api.areFriends,
areFriends = lila.relation.Env.current.api.fetchAreFriends,
lightUser = lila.user.Env.current.lightUser,
system = lila.common.PlayApp.system,
lifecycle = lila.common.PlayApp.lifecycle)

View file

@ -29,9 +29,7 @@ private final class Indexer(storage: Storage, sequencer: ActorRef) {
def update(game: Game, userId: String, previous: Entry): Funit =
PovToEntry(game, userId, previous.provisional) flatMap {
case Right(e) => storage update e.copy(number = previous.number)
case Left(g) =>
logwarn(s"[insight $userId] invalid game http://l.org/${g.id}")
funit
case _ => funit
}
private def compute(user: User): Funit = storage.fetchLast(user.id) flatMap {
@ -71,12 +69,7 @@ private final class Indexer(storage: Storage, sequencer: ActorRef) {
PovToEntry(game, user.id, provisional = nb < 10).addFailureEffect { e =>
println(e)
e.printStackTrace
} map {
case Right(e) => e.some
case Left(g) =>
logwarn(s"[insight ${user.username}] invalid game http://lichess.org/${g.id}")
none
}
} map (_.toOption)
}
val query = $query(gameQuery(user) ++ Json.obj(Game.BSONFields.createdAt -> $gte($date(from))))
pimpQB(query)

View file

@ -45,7 +45,7 @@ private final class Storage(coll: Coll) {
def find(id: String) = coll.find(selectId(id)).one[Entry]
def ecos(userId: String): Fu[Set[String]] =
coll.distinct(F.eco, selectUserId(userId).some) map lila.db.BSON.asStrings map (_.toSet)
coll.distinct(F.eco, selectUserId(userId).some) map lila.db.BSON.asStringSet
def nbByPerf(userId: String): Fu[Map[PerfType, Int]] = coll.aggregate(
Match(BSONDocument(F.userId -> userId)),

View file

@ -92,7 +92,7 @@ object Env {
db = lila.db.Env.current,
hub = lila.hub.Env.current,
onStart = lila.game.Env.current.onStart,
blocking = lila.relation.Env.current.api.blocking,
blocking = lila.relation.Env.current.api.fetchBlocking,
playban = lila.playban.Env.current.api.currentBan _,
system = lila.common.PlayApp.system,
scheduler = lila.common.PlayApp.scheduler)

View file

@ -61,8 +61,8 @@ object Env {
db = lila.db.Env.current,
shutup = lila.hub.Env.current.actor.shutup,
mongoCache = lila.memo.Env.current.mongoCache,
blocks = lila.relation.Env.current.api.blocks,
follows = lila.relation.Env.current.api.follows,
blocks = lila.relation.Env.current.api.fetchBlocks,
follows = lila.relation.Env.current.api.fetchFollows,
getPref = lila.pref.Env.current.api.getPref,
system = lila.common.PlayApp.system)
}

View file

@ -5,9 +5,9 @@ import scala.concurrent.duration.Duration
import lila.db.BSON
import lila.db.Types._
import lila.hub.actorApi.SendTo
import lila.memo.AsyncCache
import lila.user.User
import lila.hub.actorApi.SendTo
import reactivemongo.bson._
final class PrefApi(
@ -115,15 +115,13 @@ final class PrefApi(
}
def unfollowableIds(userIds: List[String]): Fu[Set[String]] =
coll.find(BSONDocument(
coll.distinct("_id", BSONDocument(
"_id" -> BSONDocument("$in" -> userIds),
"follow" -> false
), BSONDocument("_id" -> true)).cursor[BSONDocument]().collect[List]() map {
_.flatMap(_.getAs[String]("_id")).toSet
}
).some) map lila.db.BSON.asStringSet
def followableIds(userIds: List[String]): Fu[Set[String]] =
unfollowableIds(userIds) map (uns => userIds.toSet diff uns)
unfollowableIds(userIds) map userIds.toSet.diff
def followables(userIds: List[String]): Fu[List[Boolean]] =
followableIds(userIds) map { followables => userIds map followables.contains }

View file

@ -1,25 +0,0 @@
package lila.relation
import lila.memo.AsyncCache
private[relation] final class Cached {
private[relation] val followers = AsyncCache(RelationRepo.followers, maxCapacity = 2000)
private[relation] val following = AsyncCache(RelationRepo.following, maxCapacity = 2000)
private[relation] val blockers = AsyncCache(RelationRepo.blockers, maxCapacity = 2000)
private[relation] val blocking = AsyncCache(RelationRepo.blocking, maxCapacity = 2000)
private[relation] val relation = AsyncCache(findRelation, maxCapacity = 20000)
private def findRelation(pair: (String, String)): Fu[Option[Relation]] = pair match {
case (u1, u2) => following(u1) flatMap { f =>
f(u2).fold(fuccess(true.some), blocking(u1) map { b =>
b(u2).fold(false.some, none)
})
}
}
private[relation] def invalidate(u1: ID, u2: ID): Funit =
(List(followers, following, blockers, blocking) flatMap { cache =>
List(u1, u2) map cache.remove
}).sequenceFu.void >> relation.remove(u1, u2)
}

View file

@ -26,18 +26,15 @@ final class Env(
import settings._
lazy val api = new RelationApi(
cached = cached,
coll = relationColl,
actor = hub.actor.relation,
bus = system.lilaBus,
getOnlineUserIds = getOnlineUserIds,
timeline = hub.actor.timeline,
reporter = hub.actor.report,
followable = followable,
maxFollow = MaxFollow,
maxBlock = MaxBlock)
private lazy val cached = new Cached
private[relation] val actor = system.actorOf(Props(new RelationActor(
getOnlineUserIds = getOnlineUserIds,
lightUser = lightUser,
@ -47,7 +44,7 @@ final class Env(
{
import scala.concurrent.duration._
scheduler.once(10 seconds) {
scheduler.once(15 seconds) {
scheduler.message(ActorNotifyFreq) {
actor -> actorApi.NotifyMovement
}

View file

@ -3,6 +3,7 @@ package lila.relation
import akka.actor.{ Actor, ActorSelection }
import akka.pattern.{ ask, pipe }
import play.api.libs.json.Json
import scala.concurrent.duration._
import actorApi._
import lila.common.LightUser
@ -44,13 +45,13 @@ private[relation] final class RelationActor(
private def onlineIds: Set[ID] = onlines.keySet
private def onlineFriends(userId: String): Fu[OnlineFriends] =
api following userId map { ids =>
api fetchFollowing userId map { ids =>
OnlineFriends(ids.flatMap(onlines.get).toList)
}
private def notifyFollowers(users: List[LightUser], message: String) {
users foreach { user =>
api followers user.id map (_ filter onlines.contains) foreach { ids =>
api fetchFollowers user.id map (_ filter onlines.contains) foreach { ids =>
if (ids.nonEmpty) bus.publish(SendTos(ids.toSet, message, user.titleName), 'users)
}
}

View file

@ -5,100 +5,142 @@ import scala.util.Success
import lila.db.api._
import lila.db.Implicits._
import lila.game.GameRepo
import lila.hub.actorApi.relation.ReloadOnlineFriends
import lila.db.paginator._
import lila.hub.actorApi.timeline.{ Propagate, Follow => FollowUser }
import lila.user.tube.userTube
import lila.user.{ User => UserModel, UserRepo }
import tube.relationTube
import BSONHandlers._
import reactivemongo.api.collections.bson.BSONBatchCommands.AggregationFramework._
import reactivemongo.bson._
final class RelationApi(
cached: Cached,
coll: Coll,
actor: ActorSelection,
bus: lila.common.Bus,
getOnlineUserIds: () => Set[String],
timeline: ActorSelection,
reporter: ActorSelection,
followable: String => Fu[Boolean],
followable: ID => Fu[Boolean],
maxFollow: Int,
maxBlock: Int) {
def followers(userId: ID) = cached followers userId
def following(userId: ID) = cached following userId
def blockers(userId: ID) = cached blockers userId
def blocking(userId: ID) = cached blocking userId
import RelationRepo.makeId
def blocks(userId: ID) = blockers(userId) blocking(userId)
def fetchRelation(u1: ID, u2: ID): Fu[Option[Relation]] = coll.find(
BSONDocument("u1" -> u1, "u2" -> u2),
BSONDocument("r" -> true, "_id" -> false)
).one[BSONDocument].map {
_.flatMap(_.getAs[Boolean]("r"))
}
def nbFollowers(userId: ID) = followers(userId) map (_.size)
def nbFollowing(userId: ID) = following(userId) map (_.size)
def nbBlocking(userId: ID) = blocking(userId) map (_.size)
def nbBlockers(userId: ID) = blockers(userId) map (_.size)
def fetchFollowing = RelationRepo following _
def friends(userId: ID) = following(userId) zip followers(userId) map {
case (f1, f2) => f1 intersect f2
def fetchFollowers = RelationRepo followers _
def fetchBlocking = RelationRepo blocking _
def fetchFriends(userId: ID) = coll.aggregate(Match(BSONDocument(
"$or" -> BSONArray(BSONDocument("u1" -> userId), BSONDocument("u2" -> userId)),
"r" -> Follow
)), List(
Group(BSONNull)(
"u1" -> AddToSet("u1"),
"u2" -> AddToSet("u2")),
Project(BSONDocument(
"_id" -> BSONDocument("$setIntersection" -> BSONArray("$u1", "$u2"))
))
)).map {
~_.documents.headOption.flatMap(_.getAs[Set[String]]("_id")) - userId
}
def areFriends(u1: ID, u2: ID) = friends(u1) map (_ contains u2)
def fetchFollows(u1: ID, u2: ID) =
coll.count(BSONDocument("_id" -> makeId(u1, u2), "r" -> Follow).some).map(0!=)
def follows(u1: ID, u2: ID) = following(u1) map (_ contains u2)
def blocks(u1: ID, u2: ID) = blocking(u1) map (_ contains u2)
def fetchBlocks(u1: ID, u2: ID) =
coll.count(BSONDocument("_id" -> makeId(u1, u2), "r" -> Block).some).map(0!=)
def relation(u1: ID, u2: ID): Fu[Option[Relation]] = cached.relation(u1, u2)
def fetchAreFriends(u1: ID, u2: ID) =
fetchFollows(u1, u2) flatMap { _ ?? fetchFollows(u2, u1) }
def onlinePopularUsers(max: Int): Fu[List[UserModel]] =
(getOnlineUserIds().toList map { id =>
nbFollowers(id) map (id -> _)
}).sequenceFu map (_ sortBy (-_._2) take max map (_._1)) flatMap UserRepo.byOrderedIds
def countFollowing(userId: ID) =
coll.count(BSONDocument("u1" -> userId, "r" -> Follow).some)
def countFollowers(userId: ID) =
coll.count(BSONDocument("u2" -> userId, "r" -> Follow).some)
def countBlocking(userId: ID) =
coll.count(BSONDocument("u1" -> userId, "r" -> Block).some)
def countBlockers(userId: ID) =
coll.count(BSONDocument("u2" -> userId, "r" -> Block).some)
def followingPaginatorAdapter(userId: ID) = new BSONAdapter[Followed](
collection = coll,
selector = BSONDocument("u1" -> userId, "r" -> Follow),
projection = BSONDocument("u2" -> true, "_id" -> false),
sort = BSONDocument()).map(_.userId)
def followersPaginatorAdapter(userId: ID) = new BSONAdapter[Follower](
collection = coll,
selector = BSONDocument("u2" -> userId, "r" -> Follow),
projection = BSONDocument("u1" -> true, "_id" -> false),
sort = BSONDocument()).map(_.userId)
def blockingPaginatorAdapter(userId: ID) = new BSONAdapter[Blocked](
collection = coll,
selector = BSONDocument("u1" -> userId, "r" -> Block),
projection = BSONDocument("u2" -> true, "_id" -> false),
sort = BSONDocument()).map(_.userId)
def follow(u1: ID, u2: ID): Funit =
if (u1 == u2) funit
else followable(u2) zip relation(u1, u2) zip relation(u2, u1) flatMap {
case ((false, _), _) => funit
case ((_, Some(Follow)), _) => funit
case ((_, _), Some(Block)) => funit
case _ => RelationRepo.follow(u1, u2) >> limitFollow(u1) >>
refresh(u1, u2) >>-
(timeline ! Propagate(
FollowUser(u1, u2)
).toFriendsOf(u1).toUsers(List(u2)))
else followable(u2) flatMap {
case false => funit
case true => fetchRelation(u1, u2) zip fetchRelation(u2, u1) flatMap {
case (Some(Follow), _) => funit
case (_, Some(Block)) => funit
case _ => RelationRepo.follow(u1, u2) >> limitFollow(u1) >>-
reloadOnlineFriends(u1, u2) >>-
(timeline ! Propagate(FollowUser(u1, u2)).toFriendsOf(u1).toUsers(List(u2)))
}
}
private def limitFollow(u: ID) = nbFollowing(u) flatMap { nb =>
private def limitFollow(u: ID) = countFollowing(u) flatMap { nb =>
(nb >= maxFollow) ?? RelationRepo.drop(u, true, nb - maxFollow + 1)
}
private def limitBlock(u: ID) = nbBlocking(u) flatMap { nb =>
private def limitBlock(u: ID) = countBlocking(u) flatMap { nb =>
(nb >= maxBlock) ?? RelationRepo.drop(u, false, nb - maxBlock + 1)
}
def block(u1: ID, u2: ID): Funit =
if (u1 == u2) funit
else relation(u1, u2) flatMap {
case Some(Block) => funit
case _ => RelationRepo.block(u1, u2) >> limitBlock(u1) >> refresh(u1, u2) >>-
bus.publish(lila.hub.actorApi.relation.Block(u1, u2), 'relation) >>-
(nbBlockers(u2) zip nbFollowers(u2))
else fetchBlocks(u1, u2) flatMap {
case true => funit
case _ => RelationRepo.block(u1, u2) >> limitBlock(u1) >>- reloadOnlineFriends(u1, u2) >>-
bus.publish(lila.hub.actorApi.relation.Block(u1, u2), 'relation)
}
def unfollow(u1: ID, u2: ID): Funit =
if (u1 == u2) funit
else relation(u1, u2) flatMap {
case Some(Follow) => RelationRepo.unfollow(u1, u2) >> refresh(u1, u2)
case _ => funit
else fetchFollows(u1, u2) flatMap {
case true => RelationRepo.unfollow(u1, u2) >>- reloadOnlineFriends(u1, u2)
case _ => funit
}
def unfollowAll(u1: ID): Funit = RelationRepo.unfollowAll(u1)
def unblock(u1: ID, u2: ID): Funit =
if (u1 == u2) funit
else relation(u1, u2) flatMap {
case Some(Block) => RelationRepo.unblock(u1, u2) >> refresh(u1, u2) >>-
else fetchBlocks(u1, u2) flatMap {
case true => RelationRepo.unblock(u1, u2) >>- reloadOnlineFriends(u1, u2) >>-
bus.publish(lila.hub.actorApi.relation.UnBlock(u1, u2), 'relation)
case _ => funit
}
private def refresh(u1: ID, u2: ID): Funit =
cached.invalidate(u1, u2) >>-
List(u1, u2).foreach(actor ! ReloadOnlineFriends(_))
private def reloadOnlineFriends(u1: ID, u2: ID) {
import lila.hub.actorApi.relation.ReloadOnlineFriends
List(u1, u2).foreach(actor ! ReloadOnlineFriends(_))
}
}

View file

@ -1,6 +1,7 @@
package lila.relation
import play.api.libs.json._
import reactivemongo.bson._
import lila.common.PimpedJson._
import lila.db.api._
@ -9,10 +10,7 @@ import tube.relationTube
private[relation] object RelationRepo {
def relation(id: ID): Fu[Option[Relation]] =
$primitive.one($select byId id, "r")(_.asOpt[Boolean])
def relation(u1: ID, u2: ID): Fu[Option[Relation]] = relation(makeId(u1, u2))
val coll = relationTube.coll
def followers(userId: ID) = relaters(userId, Follow)
def following(userId: ID) = relating(userId, Follow)
@ -21,14 +19,16 @@ private[relation] object RelationRepo {
def blocking(userId: ID) = relating(userId, Block)
private def relaters(userId: ID, relation: Relation): Fu[Set[ID]] =
$projection(Json.obj("u2" -> userId), Seq("u1", "r")) { obj =>
obj str "u1" map { _ -> ~(obj boolean "r") }
} map (_.filter(_._2 == relation).map(_._1).toSet)
coll.distinct("u1", BSONDocument(
"u2" -> userId,
"r" -> relation
).some) map lila.db.BSON.asStringSet
private def relating(userId: ID, relation: Relation): Fu[Set[ID]] =
$projection(Json.obj("u1" -> userId), Seq("u2", "r")) { obj =>
obj str "u2" map { _ -> ~(obj boolean "r") }
} map (_.filter(_._2 == relation).map(_._1).toSet)
coll.distinct("u2", BSONDocument(
"u1" -> userId,
"r" -> relation
).some) map lila.db.BSON.asStringSet
def follow(u1: ID, u2: ID): Funit = save(u1, u2, Follow)
def unfollow(u1: ID, u2: ID): Funit = remove(u1, u2)
@ -55,5 +55,5 @@ private[relation] object RelationRepo {
$remove(Json.obj("_id" -> $in(ids)))
}
private def makeId(u1: String, u2: String) = u1 + "/" + u2
def makeId(u1: String, u2: String) = s"$u1/$u2"
}

View file

@ -0,0 +1,22 @@
package lila.relation
import reactivemongo.bson._
case class Follower(u1: String) {
def userId = u1
}
case class Followed(u2: String) {
def userId = u2
}
case class Blocked(u2: String) {
def userId = u2
}
object BSONHandlers {
private[relation] implicit val followerBSONHandler = Macros.handler[Follower]
private[relation] implicit val followedBSONHandler = Macros.handler[Followed]
private[relation] implicit val blockedBSONHandler = Macros.handler[Blocked]
}

View file

@ -84,6 +84,8 @@ final class Firewall(
def clear { cache.clear }
def contains(ip: String) = apply map (_ contains strToIp(ip))
def fetch: Fu[Set[IP]] =
$primitive($select.all, "_id")(_.asOpt[String]) map { _.map(strToIp).toSet }
firewallTube.coll.distinct("_id") map { res =>
lila.db.BSON.asStringSet(res) map strToIp
}
}
}

View file

@ -24,7 +24,7 @@ private[setup] final class Challenger(
case msg@RemindChallenge(gameId, from, to) =>
UserRepo.named(from) zip UserRepo.named(to) zip (renderer ? msg) flatMap {
case ((Some(fromU), Some(toU)), html: Html) =>
prefApi.getPref(toU) zip relationApi.follows(toU.id, fromU.id) flatMap {
prefApi.getPref(toU) zip relationApi.fetchFollows(toU.id, fromU.id) flatMap {
case (pref, follows) =>
lila.pref.Pref.Challenge.block(fromU, toU, pref.challenge, follows,
fromCheat = fromU.engine && !toU.engine) match {

View file

@ -50,6 +50,6 @@ object Env {
config = lila.common.PlayApp loadConfig "shutup",
reporter = lila.hub.Env.current.actor.report,
system = lila.common.PlayApp.system,
follows = lila.relation.Env.current.api.follows _,
follows = lila.relation.Env.current.api.fetchFollows _,
db = lila.db.Env.current)
}

View file

@ -22,7 +22,7 @@ final class ShutupApi(
def getPublicLines(userId: String): Fu[List[String]] =
coll.find(BSONDocument("_id" -> userId), BSONDocument("pub" -> 1))
.one[BSONDocument].map {
~_.map(~_.getAs[List[String]]("pub"))
~_.flatMap(_.getAs[List[String]]("pub"))
}
def publicForumMessage(userId: String, text: String) = record(userId, text, TextType.PublicForumMessage)

View file

@ -11,9 +11,10 @@ private[team] final class Cached {
def name(id: String) = nameCache get id
private[team] val teamIdsCache = MixedCache[String, List[String]](MemberRepo.teamIdsByUser,
private[team] val teamIdsCache = MixedCache[String, Set[String]](
MemberRepo.teamIdsByUser,
timeToLive = 2 hours,
default = _ => Nil)
default = _ => Set.empty)
def teamIds(userId: String) = teamIdsCache get userId

View file

@ -2,6 +2,7 @@ package lila.team
import play.api.libs.json.Json
import reactivemongo.api._
import reactivemongo.bson._
import lila.db.api._
import tube.memberTube
@ -10,25 +11,25 @@ object MemberRepo {
type ID = String
def userIdsByTeam(teamId: ID): Fu[List[ID]] =
$primitive(teamQuery(teamId), "user")(_.asOpt[ID])
def userIdsByTeam(teamId: ID): Fu[Set[ID]] =
memberTube.coll.distinct("user", BSONDocument("team" -> teamId).some) map lila.db.BSON.asStringSet
def teamIdsByUser(userId: ID): Fu[List[ID]] =
$primitive(userQuery(userId), "team")(_.asOpt[ID])
def teamIdsByUser(userId: ID): Fu[Set[ID]] =
memberTube.coll.distinct("team", BSONDocument("user" -> userId).some) map lila.db.BSON.asStringSet
def removeByteam(teamId: ID): Funit =
def removeByteam(teamId: ID): Funit =
$remove(teamQuery(teamId))
def removeByUser(userId: ID): Funit =
def removeByUser(userId: ID): Funit =
$remove(userQuery(userId))
def exists(teamId: ID, userId: ID): Fu[Boolean] =
def exists(teamId: ID, userId: ID): Fu[Boolean] =
$count.exists(selectId(teamId, userId))
def add(teamId: String, userId: String): Funit =
def add(teamId: String, userId: String): Funit =
$insert(Member.make(team = teamId, user = userId))
def remove(teamId: String, userId: String): Funit =
def remove(teamId: String, userId: String): Funit =
$remove(selectId(teamId, userId))
def countByTeam(teamId: String): Fu[Int] =

View file

@ -4,6 +4,7 @@ import org.joda.time.{ DateTime, Period }
import play.api.libs.json.Json
import play.modules.reactivemongo.json.ImplicitBSONHandlers.JsObjectWriter
import reactivemongo.api._
import reactivemongo.bson._
import lila.db.api._
import lila.user.User
@ -17,7 +18,7 @@ object TeamRepo {
$find.one($select(id) ++ Json.obj("createdBy" -> createdBy))
def teamIdsByCreator(userId: String): Fu[List[String]] =
$primitive(Json.obj("createdBy" -> userId), "_id")(_.asOpt[String])
teamTube.coll.distinct("_id", BSONDocument("createdBy" -> userId).some) map lila.db.BSON.asStrings
def name(id: String): Fu[Option[String]] =
$primitive.one($select(id), "name")(_.asOpt[String])

View file

@ -55,8 +55,8 @@ object Env {
config = lila.common.PlayApp loadConfig "timeline",
db = lila.db.Env.current,
hub = lila.hub.Env.current,
getFriendIds = lila.relation.Env.current.api.friends _,
getFollowerIds = lila.relation.Env.current.api.followers _,
getFriendIds = lila.relation.Env.current.api.fetchFriends,
getFollowerIds = lila.relation.Env.current.api.fetchFollowers,
lobbySocket = lila.hub.Env.current.socket.lobby,
renderer = lila.hub.Env.current.actor.renderer,
system = lila.common.PlayApp.system)

View file

@ -41,8 +41,8 @@ private[timeline] final class Push(
private def propagate(propagations: List[Propagation]): Fu[List[String]] =
propagations.map {
case Users(ids) => fuccess(ids)
case Followers(id) => getFollowerIds(id) map (_.toList)
case Friends(id) => getFriendIds(id) map (_.toList)
case Followers(id) => getFollowerIds(id)
case Friends(id) => getFriendIds(id)
case StaffFriends(id) => getFriendIds(id) flatMap UserRepo.byIds map {
_ filter Granter(_.StaffForum) map (_.id)
}

View file

@ -22,11 +22,9 @@ 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.find(BSONDocument(
coll.distinct("_id", BSONDocument(
"_id" -> BSONDocument("$in" -> userIds.map { makeId(channel, _) })
)).cursor[BSONDocument]().collect[List]() map { docs =>
userIds diff docs.flatMap {
_.getAs[String]("_id") map (_ takeWhile ('@' !=))
}
).some) map lila.db.BSON.asStrings map { unsubs =>
userIds diff unsubs.map(_ takeWhile ('@' !=))
}
}

View file

@ -91,9 +91,11 @@ object Schedule {
case (Daily | Eastern, HyperBullet | Bullet, _) => 60
case (Daily | Eastern, SuperBlitz, _) => 90
case (Daily | Eastern, Blitz, Standard) => 90
case (Daily | Eastern, Blitz, Standard) => 120
case (Daily | Eastern, Classical, _) => 150
case (Daily | Eastern, Blitz, Crazyhouse) => 120
case (Daily | Eastern, Blitz, _) => 60 // variant daily is shorter
case (Daily | Eastern, Classical, _) => 60 * 2
case (Weekly, HyperBullet | Bullet, _) => 60 * 2
case (Weekly, SuperBlitz, _) => 60 * 2 + 30

View file

@ -57,9 +57,9 @@ trait UserRepo {
y.??(yy => users.find(_.id == yy))
}
def byOrderedIds(ids: Iterable[ID]): Fu[List[User]] = $find byOrderedIds ids
def byOrderedIds(ids: Seq[ID]): Fu[List[User]] = $find byOrderedIds ids
def enabledByIds(ids: Seq[ID]): Fu[List[User]] = $find(enabledSelect ++ $select.byIds(ids))
def enabledByIds(ids: Iterable[ID]): Fu[List[User]] = $find(enabledSelect ++ $select.byIds(ids))
def enabledById(id: ID): Fu[Option[User]] =
$find.one(enabledSelect ++ $select.byId(id))
@ -213,7 +213,8 @@ trait UserRepo {
def nameExists(username: String): Fu[Boolean] = idExists(normalize(username))
def idExists(id: String): Fu[Boolean] = $count exists id
def engineIds: Fu[Set[String]] = $primitive(Json.obj("engine" -> true), "_id")(_.asOpt[String]) map (_.toSet)
def engineIds: Fu[Set[String]] =
coll.distinct("_id", BSONDocument("engine" -> true).some) map lila.db.BSON.asStringSet
def usernamesLike(username: String, max: Int = 10): Fu[List[String]] = {
import java.util.regex.Matcher.quoteReplacement
@ -285,11 +286,12 @@ trait UserRepo {
}
def recentlySeenNotKidIds(since: DateTime) =
$primitive(enabledSelect ++ Json.obj(
"seenAt" -> $gt($date(since)),
"count.game" -> $gt(4),
"kid" -> $ne(true)
), "_id")(_.asOpt[String])
coll.distinct("_id", BSONDocument(
F.enabled -> true,
"seenAt" -> BSONDocument("$gt" -> since),
"count.game" -> BSONDocument("$gt" -> 9),
"kid" -> BSONDocument("$ne" -> true)
).some) map lila.db.BSON.asStrings
def setLang(id: ID, lang: String) = $update.field(id, "lang", lang)

View file

@ -78,12 +78,7 @@ private[video] final class VideoApi(
).void
def allIds: Fu[List[Video.ID]] =
videoColl.find(
BSONDocument(),
BSONDocument("_id" -> true)
).cursor[BSONDocument]().collect[List]() map { doc =>
doc flatMap (_.getAs[String]("_id"))
}
videoColl.distinct("_id", none) map lila.db.BSON.asStrings
def popular(user: Option[User], page: Int): Fu[Paginator[VideoView]] = Paginator(
adapter = new BSONAdapter[Video](
@ -160,16 +155,12 @@ private[video] final class VideoApi(
).some) map (0!=)
def seenVideoIds(user: User, videos: Seq[Video]): Fu[Set[Video.ID]] =
viewColl.find(
viewColl.distinct(View.BSONFields.videoId,
BSONDocument(
"_id" -> BSONDocument("$in" -> videos.map { v =>
View.makeId(v.id, user.id)
})
),
BSONDocument(View.BSONFields.videoId -> true, "_id" -> false)
).cursor[BSONDocument]().collect[List]() map { docs =>
docs.flatMap(_.getAs[String](View.BSONFields.videoId)).toSet
}
).some) map lila.db.BSON.asStringSet
}
object tag {

View file

@ -54,7 +54,7 @@ function renderPlot(ctrl, hook) {
intentPollInterval: 100,
fadeInTime: 0,
fadeOutTime: 0,
placement: hook.rating > 2200 ? 'se' : 'ne',
placement: hook.rating > 1800 ? 'se' : 'ne',
mouseOnToPopup: true,
closeDelay: 200,
popupId: 'hook'

View file

@ -354,6 +354,7 @@ module.exports = function(opts) {
this.setBerserk = function(color) {
if (this.vm.goneBerserk[color]) return;
this.vm.goneBerserk[color] = true;
if (color !== this.data.player.color) $.sound.berserk();
m.redraw();
}.bind(this);

View file

@ -26,7 +26,7 @@ module.exports = function(opts) {
steps: [{
title: "Racing Kings",
content: "This is a game of racing kings. " +
'Would you like to check out <a target="_blank" href="http://lichess.org/racing-kings">the rules</a>?',
'You might want to check out <a target="_blank" href="http://lichess.org/racing-kings">the rules</a>.',
target: "div.game_infos .variant-link",
placement: "bottom"
}]
@ -44,7 +44,7 @@ module.exports = function(opts) {
steps: [{
title: "Crazyhouse",
content: "This is a game of crazyhouse. " +
'Would you like to check out <a target="_blank" href="http://lichess.org/crazyhouse">the rules</a>?',
'You might want to check out <a target="_blank" href="http://lichess.org/crazyhouse">the rules</a>.',
target: "div.game_infos .variant-link",
placement: "bottom"
}]

View file

@ -3,7 +3,7 @@ var game = require('game').game;
function ratingDiff(player) {
if (typeof player.ratingDiff === 'undefined') return null;
if (player.ratingDiff === 0) return m('span.rp.null', 0);
if (player.ratingDiff === 0) return m('span.rp.null', '±0');
if (player.ratingDiff > 0) return m('span.rp.up', '+' + player.ratingDiff);
if (player.ratingDiff < 0) return m('span.rp.down', player.ratingDiff);
}

View file

@ -22,8 +22,7 @@ module.exports = function(ctrl) {
tag: 'a',
attrs: {
key: p.id,
href: '/' + p.id,
class: 'glpt'
href: '/' + p.id
},
children: [
user(p, 0),

View file

@ -16,7 +16,7 @@ function result(win, stat) {
function playerTitle(player) {
return m('h2', [
player.withdraw ? m('span.text[data-icon=b]') : m('span.rank', player.rank + '. '),
m('span.rank', player.rank + '. '),
util.player(player)
]);
}
@ -60,7 +60,12 @@ module.exports = function(ctrl) {
return m('tr', {
key: p.id,
'data-href': '/' + p.id + '/' + p.color,
class: 'glpt' + (res === '1' ? ' win' : (res === '0' ? ' loss' : ''))
class: 'glpt' + (res === '1' ? ' win' : (res === '0' ? ' loss' : '')),
config: function(el, isUpdate, ctx) {
if (!isUpdate) ctx.onunload = function() {
$.powerTip.destroy(el);
};
}
}, [
m('th', Math.max(nb.game, pairingsLen) - i),
m('td', (p.op.title ? p.op.title + ' ' : '') + p.op.name),

View file

@ -71,6 +71,11 @@ module.exports = {
var fullName = (p.title ? p.title + ' ' : '') + p.name;
var attrs = {
class: 'ulpt user_link' + (fullName.length > 15 ? ' long' : ''),
config: function(el, isUpdate, ctx) {
if (!isUpdate) ctx.onunload = function() {
$.powerTip.destroy(el);
};
}
};
attrs[tag === 'a' ? 'href' : 'data-href'] = '/@/' + p.name;
return {