try to unify chat messages / round is OK

pull/83/head
Thibault Duplessis 2013-05-14 13:30:55 -03:00
parent 59e1f64012
commit c9484b7552
19 changed files with 156 additions and 135 deletions

View File

@ -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 "<a href='%s'>%s</a>".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))
"<a href='%s'>%s</a>".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)

View File

@ -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
"""<a class="user_link%s%s" href="%s">%s</a>""".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 =
"""<a class="user_link%s%s" href="%s">%s</a>""".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,

View File

@ -4,8 +4,6 @@
@base.layout(title = title) {
<style type="text/css">
</style>
<div class="content_box">
<h1>@title</h1>
<br /><br />
@ -13,7 +11,7 @@
@messages.map { message =>
<tr>
<td>@showDate(message.date)</td>
<td>@userIdLink(message.userId.some)</td>
<td>@userIdLink(message.user)</td>
<td>@Html(message.text)</td>
</tr>
}

View File

@ -1,12 +1,7 @@
@(messages: List[(Option[String], String)])(implicit ctx: Context)
@messages.map {
case (author, text) => {
<li>
<span>
@userIdLink(author, withOnline = false, truncate = 12.some)
</span>
@text
</li>
case (username, text) => {
<li><span>@usernameLink(username, withOnline = false, truncate = 12.some)</span>@text</li>
}
}

View File

@ -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)
}
}

View File

@ -114,23 +114,18 @@ object Event {
case class Message(author: String, text: String) extends Event {
def typ = "message"
def data = JsString("""<li class="%s%s">%s</li>""".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 =
"""<li><span>%s</span>%s</li>""".format(
author.fold("Anonymous")("@" + _),
escapeXml(text))
object End extends Empty {
def typ = "end"
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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) >>-

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -203,6 +203,10 @@ var lichess_translations = [];
domain: document.domain.replace(/^\w+\.(.+)$/, '$1')
};
$.userLink = function(u, limit) {
return (u || false) ? '<a class="user_link" href="/@/' + u + '">' + u + '</a>' : '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 '<li><span>' + $.userLink(u, 12) + '</span>' + urlToLink(t) + '</li>';
} else {
return '<li class="' + u + (u == 'system' ? ' trans_me' : '') + '">' + urlToLink(t) + '</li>';
}
}
});
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 = '<a href="/@/' + w + '">' + w + '</a>';
}
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)
? '<li><span><a class="user_link" href="/@/' + u + '">' + u.substr(0, 12) + '</a></span>' + urlToLink(txt) + '</li>'
: '<li>' + urlToLink(txt) + '</li>';
render: function(u, t) {
return (u || false) ? '<li><span><a class="user_link" href="/@/' + u + '">' + u.substr(0, 12) + '</a></span>' + urlToLink(t) + '</li>' : '<li>' + urlToLink(t) + '</li>';
}
});
@ -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 '<li><span><a class="user_link" href="/@/' + u + '">' + u.substr(0, 12) + '</a></span>' + urlToLink(txt) + '</li>';
render: function(u, t) {
return '<li><span>' + userLink(u, 12) + '</span>' + urlToLink(t) + '</li>';
}
});
@ -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);

1
todo
View File

@ -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
----------