diff --git a/app/templating/StringHelper.scala b/app/templating/StringHelper.scala index 7dec412023..ff75545b8e 100644 --- a/app/templating/StringHelper.scala +++ b/app/templating/StringHelper.scala @@ -36,17 +36,15 @@ trait StringHelper { private val urlRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r - def addLinks(text: String) = urlRegex.replaceAllIn(text, m ⇒ "%s".format( - (prependHttp _ compose delocalize _ compose quoteReplacement _)(m group 1), - (delocalize _ compose quoteReplacement _)(m group 1) - )) + def addLinks(text: String) = urlRegex.replaceAllIn(text, m ⇒ { + val url = delocalize(quoteReplacement(m group 1)) + "%s".format(prependHttp(url), url) + }) - private def prependHttp(url: String): String = - url startsWith "http" fold(url, "http://" + url) + private def prependHttp(url: String): String = + url startsWith "http" fold (url, "http://" + url) - private val delocalizeRegex = ("""\w+\.""" + quoteReplacement(netDomain)).r - - private def delocalize(url: String) = delocalizeRegex.replaceAllIn(url, netDomain) + private val delocalize = new lila.common.String.Delocalizer(netDomain) def showNumber(n: Int): String = (n > 0).fold("+" + n, n.toString) diff --git a/app/templating/UserHelper.scala b/app/templating/UserHelper.scala index 8fb98839f1..66168209c9 100644 --- a/app/templating/UserHelper.scala +++ b/app/templating/UserHelper.scala @@ -18,20 +18,17 @@ trait UserHelper { def isOnline(userId: String) = Env.user isOnline userId def userIdLink( - userId: Option[String], + userIdOption: Option[String], cssClass: Option[String] = None, withOnline: Boolean = true, truncate: Option[Int] = None): Html = Html { - (userId zmap Env.user.usernameOption) map { - _.fold(User.anonymous) { username ⇒ - """%s""".format( - withOnline ?? isOnline(username).fold(" online", " offline"), - cssClass.zmap(" " + _), - routes.User.show(username), - truncate.fold(username)(username.take) - ) - } - } await + userIdOption.fold(User.anonymous) { userId ⇒ + Env.user usernameOption userId map { + _.fold(User.anonymous) { username ⇒ + userIdNameLink(userId, username, cssClass, withOnline, truncate) + } + } await + } } def userIdLink( @@ -47,6 +44,29 @@ trait UserHelper { } await } + def usernameLink( + usernameOption: Option[String], + cssClass: Option[String] = None, + withOnline: Boolean = true, + truncate: Option[Int] = None): Html = Html { + usernameOption.fold(User.anonymous) { username ⇒ + userIdNameLink(username.toLowerCase, username, cssClass, withOnline, truncate) + } + } + + private def userIdNameLink( + userId: String, + username: String, + cssClass: Option[String] = None, + withOnline: Boolean = true, + truncate: Option[Int] = None): String = + """%s""".format( + withOnline ?? isOnline(userId).fold(" online", " offline"), + cssClass.zmap(" " + _), + routes.User.show(username), + truncate.fold(username)(username.take) + ) + def userLink( user: User, cssClass: Option[String] = None, diff --git a/app/views/lobby/log.scala.html b/app/views/lobby/log.scala.html index d702d6e808..b972c059fd 100644 --- a/app/views/lobby/log.scala.html +++ b/app/views/lobby/log.scala.html @@ -4,8 +4,6 @@ @base.layout(title = title) { -

@title



@@ -13,7 +11,7 @@ @messages.map { message => @showDate(message.date) - @userIdLink(message.userId.some) + @userIdLink(message.user) @Html(message.text) } diff --git a/app/views/round/watcherRoomInner.scala.html b/app/views/round/watcherRoomInner.scala.html index 82be804537..0be6680e7f 100644 --- a/app/views/round/watcherRoomInner.scala.html +++ b/app/views/round/watcherRoomInner.scala.html @@ -1,12 +1,7 @@ @(messages: List[(Option[String], String)])(implicit ctx: Context) @messages.map { -case (author, text) => { -
  • - - @userIdLink(author, withOnline = false, truncate = 12.some) - - @text -
  • +case (username, text) => { +
  • @usernameLink(username, withOnline = false, truncate = 12.some)@text
  • } } diff --git a/modules/common/src/main/String.scala b/modules/common/src/main/String.scala index 6fb1e66de1..967e279121 100644 --- a/modules/common/src/main/String.scala +++ b/modules/common/src/main/String.scala @@ -1,6 +1,7 @@ package lila.common import java.text.Normalizer +import java.util.regex.Matcher.quoteReplacement object String { @@ -10,4 +11,11 @@ object String { val slug = """[^\w-]""".r.replaceAllIn(normalized, "") slug.toLowerCase } + + final class Delocalizer(netDomain: String) { + + private val regex = ("""\w+\.""" + quoteReplacement(netDomain)).r + + def apply(url: String) = regex.replaceAllIn(url, netDomain) + } } diff --git a/modules/game/src/main/Event.scala b/modules/game/src/main/Event.scala index 8ce05c0f07..5a2ffd4171 100644 --- a/modules/game/src/main/Event.scala +++ b/modules/game/src/main/Event.scala @@ -114,23 +114,18 @@ object Event { case class Message(author: String, text: String) extends Event { def typ = "message" - def data = JsString("""
  • %s
  • """.format( - author, (author == "system") ?? " trans_me", escapeXml(text))) + def data = Json.obj("u" -> author, "t" -> escapeXml(text)) override def owner = true } - case class WatcherMessage(author: Option[String], text: String) extends Event { + // it *IS* a username, and not a user ID + // immediately used for rendering + case class WatcherMessage(username: Option[String], text: String) extends Event { def typ = "message" - def data = JsString(renderWatcherRoom(author, text)) + def data = Json.obj("u" -> username, "t" -> escapeXml(text)) override def watcher = true } - // TODO FIXME the username is @userId and there is no link - private def renderWatcherRoom(author: Option[String], text: String): String = - """
  • %s%s
  • """.format( - author.fold("Anonymous")("@" + _), - escapeXml(text)) - object End extends Empty { def typ = "end" } diff --git a/modules/lobby/src/main/Message.scala b/modules/lobby/src/main/Message.scala index e629e21aae..b342b4af98 100644 --- a/modules/lobby/src/main/Message.scala +++ b/modules/lobby/src/main/Message.scala @@ -4,28 +4,31 @@ import play.api.libs.json._ import reactivemongo.bson._ import org.joda.time.DateTime +// it is really a username, not a user ID case class Message( - userId: String, + user: Option[String], text: String, date: DateTime) { - def render = Json.obj("txt" -> text, "u" -> userId) + def render = Json.obj("u" -> user, "t" -> text) def isEmpty = text.isEmpty } object Message { - def make(userId: String, text: String) = new Message( - userId = userId, + def make(user: Option[String], text: String) = new Message( + user = user, text = text, date = DateTime.now) import lila.db.Tube import Tube.Helpers._ + private def defaults = Json.obj("user" -> none[String]) + private[lobby] lazy val tube = Tube[Message]( - (__.json update readDate('date)) andThen Json.reads[Message], + (__.json update (merge(defaults) andThen readDate('date))) andThen Json.reads[Message], Json.writes[Message] andThen (__.json update writeDate('date)), flags = Seq(_.NoId)) } diff --git a/modules/lobby/src/main/Messenger.scala b/modules/lobby/src/main/Messenger.scala index 0d94d868f3..2a0a818943 100644 --- a/modules/lobby/src/main/Messenger.scala +++ b/modules/lobby/src/main/Messenger.scala @@ -1,24 +1,21 @@ package lila.lobby import lila.user.{ UserRepo, User, Room } -import lila.user.tube.userTube import tube.messageTube import lila.db.api._ private[lobby] final class Messenger(val netDomain: String) extends Room { def apply(userId: String, text: String): Fu[Message] = for { - userOption ← $find.byId[User](userId) - message ← (for { - user ← userOption filter (_.canChat) toValid "This user cannot chat" - msg ← createMessage(user, text) - (u, t) = msg - } yield Message.make(u, t)).future + userOption ← UserRepo byId userId + message ← (userMessage(userOption, text) map { + case (u, t) ⇒ Message.make(u.some, t) + }).future _ ← $insert(message) } yield message def system(text: String): Fu[Message] = - Message.make(userId = "", text = text) |> { message ⇒ + Message.make(user = none, text = text) |> { message ⇒ $insert(message) inject message } diff --git a/modules/lobby/src/main/Socket.scala b/modules/lobby/src/main/Socket.scala index 59c94651f7..392070d719 100644 --- a/modules/lobby/src/main/Socket.scala +++ b/modules/lobby/src/main/Socket.scala @@ -39,13 +39,13 @@ private[lobby] final class Socket( case Talk(u, txt) ⇒ messenger(u, txt) effectFold ( e ⇒ logwarn(e.toString), message ⇒ notifyVersion("talk", Json.obj( - "u" -> message.userId, - "txt" -> message.text + "u" -> message.user, + "t" -> message.text )) ) case SysTalk(txt) ⇒ messenger system txt foreach { message ⇒ - notifyVersion("talk", Json.obj("txt" -> message.text)) + notifyVersion("talk", Json.obj("t" -> message.text)) } case UnTalk(regex) ⇒ (messenger remove regex) >>- diff --git a/modules/round/src/main/Env.scala b/modules/round/src/main/Env.scala index 432d197ad7..bf6ca511d7 100644 --- a/modules/round/src/main/Env.scala +++ b/modules/round/src/main/Env.scala @@ -16,6 +16,7 @@ final class Env( db: lila.db.Env, hub: lila.hub.Env, ai: lila.ai.Ai, + getUsername: String ⇒ Fu[Option[String]], i18nKeys: lila.i18n.I18nKeys, scheduler: lila.common.Scheduler) { @@ -31,6 +32,7 @@ final class Env( val SocketTimeout = config duration "socket.timeout" val FinisherLockTimeout = config duration "finisher.lock.timeout" val HijackTimeout = config duration "hijack.timeout" + val NetDomain = config getString "net.domain" } import settings._ @@ -72,7 +74,7 @@ final class Env( finisher = finisher, socketHub = socketHub) - lazy val messenger = new Messenger(i18nKeys) + lazy val messenger = new Messenger(NetDomain, i18nKeys, getUsername) lazy val eloCalculator = new chess.EloCalculator(false) @@ -122,6 +124,7 @@ object Env { db = lila.db.Env.current, hub = lila.hub.Env.current, ai = lila.ai.Env.current.ai, + getUsername = lila.user.Env.current.usernameOption, i18nKeys = lila.i18n.Env.current.keys, scheduler = lila.common.PlayApp.scheduler) } diff --git a/modules/round/src/main/Messenger.scala b/modules/round/src/main/Messenger.scala index 2b9c6898ac..2fd782e6f1 100644 --- a/modules/round/src/main/Messenger.scala +++ b/modules/round/src/main/Messenger.scala @@ -7,8 +7,14 @@ import chess.Color import lila.game.Event import tube.{ roomTube, watcherRoomTube } import lila.db.api._ +import lila.user.UserRepo -final class Messenger(i18nKeys: I18nKeys) { +import org.apache.commons.lang3.StringEscapeUtils.escapeXml + +final class Messenger( + val netDomain: String, + i18nKeys: I18nKeys, + getUsername: String ⇒ Fu[Option[String]]) extends lila.user.Room { private val nbMessagesCopiedToRematch = 20 @@ -31,7 +37,7 @@ final class Messenger(i18nKeys: I18nKeys) { } yield () def playerMessage(ref: PovRef, text: String): Fu[List[Event.Message]] = - cleanupText(text) zmap { t ⇒ + cleanupText(text).future flatMap { t ⇒ RoomRepo.addMessage(ref.gameId, ref.color.name, t) inject { Event.Message(ref.color.name, t) :: Nil } @@ -40,12 +46,12 @@ final class Messenger(i18nKeys: I18nKeys) { def watcherMessage( gameId: String, userId: Option[String], - text: String): Fu[List[Event.WatcherMessage]] = - cleanupText(text) zmap { t ⇒ - WatcherRoomRepo.addMessage(gameId, userId, t) inject { - Event.WatcherMessage(userId, text) :: Nil - } - } + text: String): Fu[List[Event.WatcherMessage]] = for { + userOption ← userId.zmap(UserRepo.byId) + message ← userOrAnonMessage(userOption, text).future + (u, t) = message + _ ← WatcherRoomRepo.addMessage(gameId, u, t) + } yield Event.WatcherMessage(u, t) :: Nil def systemMessages(game: Game, messages: List[SelectI18nKey]): Fu[List[Event]] = game.hasChat ?? { @@ -65,11 +71,6 @@ final class Messenger(i18nKeys: I18nKeys) { } } - private def cleanupText(text: String) = { - val cleanedUp = text.trim.replace(""""""", "'") - (cleanedUp.size <= 140 && cleanedUp.nonEmpty) option cleanedUp - } - private def messageToEn(message: SelectI18nKey): String = message(i18nKeys).en() } diff --git a/modules/round/src/main/WatcherRoom.scala b/modules/round/src/main/WatcherRoom.scala index 8201893736..1759dc46db 100644 --- a/modules/round/src/main/WatcherRoom.scala +++ b/modules/round/src/main/WatcherRoom.scala @@ -20,11 +20,11 @@ object WatcherRoom { Json.reads[WatcherRoom], Json.writes[WatcherRoom]) - def encode(userId: Option[String], text: String): String = - ~userId + "|" + text + def encode(username: Option[String], text: String): String = + ~username + "|" + text def decode(encoded: String): (Option[String], String) = encoded.span('|' !=) match { - case (userId, rest) ⇒ Some(userId).filter(_.nonEmpty) -> rest + case (username, rest) ⇒ Some(username).filter(_.nonEmpty) -> rest.drop(1) } } diff --git a/modules/round/src/main/WatcherRoomRepo.scala b/modules/round/src/main/WatcherRoomRepo.scala index f7020f9c37..983fefad9c 100644 --- a/modules/round/src/main/WatcherRoomRepo.scala +++ b/modules/round/src/main/WatcherRoomRepo.scala @@ -10,9 +10,9 @@ object WatcherRoomRepo { def addMessage( id: String, - userId: Option[String], + username: Option[String], text: String): Funit = $update( $select(id), - $push("messages", WatcherRoom.encode(userId, text)), + $push("messages", WatcherRoom.encode(username, text)), upsert = true) } diff --git a/modules/tournament/src/main/Env.scala b/modules/tournament/src/main/Env.scala index 0e01a2080d..51f051db6f 100644 --- a/modules/tournament/src/main/Env.scala +++ b/modules/tournament/src/main/Env.scala @@ -97,7 +97,7 @@ final class Env( } } - lazy val messenger = new Messenger(NetDomain) + lazy val messenger = new Messenger(getUsername, NetDomain) private[tournament] lazy val tournamentColl = db(CollectionTournament) private[tournament] lazy val roomColl = db(CollectionRoom) diff --git a/modules/tournament/src/main/Messenger.scala b/modules/tournament/src/main/Messenger.scala index 1f51b58178..4014360c86 100644 --- a/modules/tournament/src/main/Messenger.scala +++ b/modules/tournament/src/main/Messenger.scala @@ -1,29 +1,31 @@ package lila.tournament -import lila.db.api.$find +import lila.db.api._ import tube.tournamentTube import lila.user.{ User, UserRepo, Room ⇒ UserRoom } -private[tournament] final class Messenger(val netDomain: String) extends UserRoom { +private[tournament] final class Messenger( + getUsername: String ⇒ Fu[Option[String]], + val netDomain: String) extends UserRoom { import Room._ def init(tour: Created): Fu[List[Message]] = for { - userOption ← UserRepo named tour.data.createdBy - username = userOption.fold(tour.data.createdBy)(_.username) + username ← getUsername(tour.data.createdBy) flatMap { + _.fold[Fu[String]](fufail("No username found"))(fuccess(_)) + } message ← systemMessage(tour, "%s creates the tournament" format username) } yield List(message) def userMessage(tournamentId: String, userId: String, text: String): Fu[Message] = for { - userOption ← UserRepo named userId - tourOption ← $find byId tournamentId + userOption ← UserRepo byId userId + tourExists ← $count.exists($select(tournamentId)) message ← (for { - user ← userOption filter (_.canChat) toValid "This user cannot chat" - _ ← tourOption toValid "No such tournament" - msg ← createMessage(user, text) + _ ← Unit.validIf(tourExists, "No such tournament") + msg ← userMessage(userOption, text) (u, t) = msg } yield Message(u.some, t)).future - _ ← RoomRepo.addMessage(tournamentId, message) + _ ← RoomRepo.addMessage(tournamentId, message) } yield message def systemMessage(tour: Tournament, text: String): Fu[Message] = @@ -31,6 +33,6 @@ private[tournament] final class Messenger(val netDomain: String) extends UserRoo RoomRepo.addMessage(tour.id, message) inject message } - def getMessages(tournamentId: String): Fu[List[Room.Message]] = + def getMessages(tournamentId: String): Fu[List[Room.Message]] = RoomRepo room tournamentId map (_.decodedMessages) } diff --git a/modules/user/src/main/Room.scala b/modules/user/src/main/Room.scala index 0f1193381b..00aed0bbda 100644 --- a/modules/user/src/main/Room.scala +++ b/modules/user/src/main/Room.scala @@ -1,30 +1,31 @@ package lila.user -import org.apache.commons.lang3.StringEscapeUtils.escapeXml import java.util.regex.Matcher.quoteReplacement trait Room { def netDomain: String - def createMessage(user: User, text: String): Valid[(String, String)] = - if (user.isChatBan) !!("Chat banned " + user) - else if (user.disabled) !!("User disabled " + user) - else escapeXml(text.replace(""""""", "'").trim take 140) |> { escaped ⇒ - (escaped.nonEmpty).fold( - success(( - user.username, - urlRegex.replaceAllIn(escaped, m ⇒ quoteReplacement(netDomain + "/" + (m group 1))) - )), - !!("Empty message") - ) + def userMessage(userOption: Option[User], text: String): Valid[(String, String)] = + userOption toValid "Anonymous cannot talk in this room" flatMap { user ⇒ + if (user.isChatBan) !!("Chat banned " + user) + else if (user.disabled) !!("User disabled " + user) + else cleanupText(text) map { user.username -> _ } } + def userOrAnonMessage(userOption: Option[User], text: String): Valid[(Option[String], String)] = + cleanupText(text) map { userOption.map(_.username) -> _ } + + def cleanupText(text: String): Valid[String] = + (text.replace(""""""", "'").trim take 140) |> { t ⇒ + if (t.isEmpty) !!("Empty message") + else success(delocalize(noPrivateUrl(t))) + } + + private def noPrivateUrl(str: String): String = + urlRegex.replaceAllIn(str, m ⇒ quoteReplacement(netDomain + "/" + (m group 1))) + + private val delocalize = new lila.common.String.Delocalizer(netDomain) private val domainRegex = netDomain.replace(".", """\.""") private val urlRegex = (domainRegex + """/([\w-]{8})[\w-]{4}""").r - - private def cleanupText(text: String) = { - val cleanedUp = text.trim.replace(""""""", "'") - (cleanedUp.size <= 140 && cleanedUp.nonEmpty) option cleanedUp - } } diff --git a/modules/user/src/main/UserRepo.scala b/modules/user/src/main/UserRepo.scala index 81b27491fe..37f6cf8580 100644 --- a/modules/user/src/main/UserRepo.scala +++ b/modules/user/src/main/UserRepo.scala @@ -21,6 +21,8 @@ object UserRepo { val normalize = User normalize _ + def byId(id: ID): Fu[Option[User]] = $find byId id + def named(username: String): Fu[Option[User]] = $find byId normalize(username) def nameds(usernames: List[String]): Fu[List[User]] = $find byIds usernames.map(normalize) diff --git a/public/javascripts/big.js b/public/javascripts/big.js index 89943ba379..3a43082e0f 100644 --- a/public/javascripts/big.js +++ b/public/javascripts/big.js @@ -203,6 +203,10 @@ var lichess_translations = []; domain: document.domain.replace(/^\w+\.(.+)$/, '$1') }; + $.userLink = function(u, limit) { + return (u || false) ? '' + u + '' : 'Anonymous'; + } + var lichess = { socket: null, socketDefaults: { @@ -587,7 +591,14 @@ var lichess_translations = []; self.initTable(); self.initClocks(); if (self.$chat) self.$chat.chat({ - resize: true + resize: true, + render: function(u, t) { + if (self.options.player.spectator) { + return '
  • ' + $.userLink(u, 12) + '' + urlToLink(t) + '
  • '; + } else { + return '
  • ' + urlToLink(t) + '
  • '; + } + } }); self.$watchers.watchers(); if (self.isMyTurn() && self.options.game.turns == 0) { @@ -638,9 +649,9 @@ var lichess_translations = []; ack: function() { clearTimeout(self.socketAckTimeout); }, - message: function(event) { + message: function(e) { self.element.queue(function() { - if (self.$chat) self.$chat.chat("append", event); + if (self.$chat) self.$chat.chat("append", e.u, e.t); self.element.dequeue(); }); }, @@ -1182,18 +1193,7 @@ var lichess_translations = []; set: function(users) { var self = this; if (users.length > 0) { - var html = [], - user, i, w; - for (i in users) { - w = users[i]; - if (w.indexOf("Anonymous") == 0) { - user = w; - } else { - user = '' + w + ''; - } - html.push(user); - } - self.list.html(html.join(", ")); + self.list.html(_.map(users, $.userLink).join(", ")); self.element.show(); } else { self.element.hide(); @@ -1204,8 +1204,8 @@ var lichess_translations = []; $.widget("lichess.chat", { _create: function() { this.options = $.extend({ - render: function(t) { - return urlToLink(t); + render: function(u, t) { + console.debug("How to render " + u + " " + t + " in chat?"); }, resize: false }, this.options); @@ -1252,15 +1252,14 @@ var lichess_translations = []; } $toggle[0].checked = $toggle.data("enabled"); }, - append: function(msg, u) { - this._appendHtml(this.options.render(msg, u)); + append: function(u, t) { + this._appendHtml(this.options.render(u, t)); }, appendMany: function(objs) { var self = this, html = ""; $.each(objs, function() { - if (this.txt) html += self.options.render(this.txt, this.u); - else if (this.msg) html += self.options.render(this.msg, this.u); + html += self.options.render(this.u, this.t); }); this._appendHtml(html); }, @@ -1728,10 +1727,8 @@ var lichess_translations = []; }); var $chat = $("div.lichess_chat").chat({ - render: function(txt, u) { - return (u || false) - ? '
  • ' + u.substr(0, 12) + '' + urlToLink(txt) + '
  • ' - : '
  • ' + urlToLink(txt) + '
  • '; + render: function(u, t) { + return (u || false) ? '
  • ' + u.substr(0, 12) + '' + urlToLink(t) + '
  • ' : '
  • ' + urlToLink(t) + '
  • '; } }); @@ -1757,7 +1754,7 @@ var lichess_translations = []; }, events: { talk: function(e) { - $chat.chat('append', e.txt, e.u); + $chat.chat('append', e.u, e.t); }, untalk: function(e) { $chat.chat('remove', e.regex); @@ -1927,8 +1924,8 @@ var lichess_translations = []; var $watchers = $("div.watchers").watchers(); var $chat = $("div.lichess_chat").chat({ - render: function(txt, u) { - return '
  • ' + u.substr(0, 12) + '' + urlToLink(txt) + '
  • '; + render: function(u, t) { + return '
  • ' + userLink(u, 12) + '' + urlToLink(t) + '
  • '; } }); @@ -1956,7 +1953,7 @@ var lichess_translations = []; lichess.socket = new strongSocket(lichess.socketUrl + socketUrl, _ld_.version, $.extend(true, lichess.socketDefaults, { events: { talk: function(e) { - $chat.chat('append', e.txt, e.u); + $chat.chat('append', e.u, e.t); }, start: start, reload: reload, @@ -2004,8 +2001,8 @@ var lichess_translations = []; ignoreUnknownMessages: true }, events: { - message: function(event) { - $chat.chat("append", event); + message: function(e) { + $chat.chat("append", e.u, e.t); }, crowd: function(event) { $watchers.watchers("set", event.watchers); diff --git a/todo b/todo index ccff0ecc5d..8260d27244 100644 --- a/todo +++ b/todo @@ -82,6 +82,7 @@ stream game export show fen only after game is finished http://en.lichess.org/forum/lichess-feedback/please-disable-live-fen-notation?page=1 I owe the admins decent tools move db game.if (initialFen) to another collection to prevent systematic loading, and doc moving +tell opponent chat is disabled DEPLOY p21 ----------