new chat WIP

chat
Thibault Duplessis 2016-06-10 20:20:56 +02:00
parent ea48ac669a
commit a2735be314
14 changed files with 153 additions and 47 deletions

View File

@ -114,6 +114,10 @@ chat {
max_lines = 50
net.domain = ${net.domain}
actor.name = chat
timeout {
duration = 20 seconds
check_every = 5 seconds
}
}
puzzle {
mongodb {

View File

@ -21,6 +21,13 @@ case class UserChat(
def forUser(u: Option[User]) = u.??(_.troll).fold(this,
copy(lines = lines filterNot (_.troll)))
def markDeleted(u: User) = copy(
lines = lines.map { l =>
if (l.userId == u.id) l.delete else l
})
def add(line: UserLine) = copy(lines = lines :+ line)
}
case class MixedChat(

View File

@ -7,8 +7,10 @@ import lila.user.{ User, UserRepo }
final class ChatApi(
coll: Coll,
chatTimeout: ChatTimeout,
flood: lila.security.Flood,
shutup: akka.actor.ActorSelection,
lilaBus: lila.common.Bus,
maxLinesPerChat: Int,
netDomain: String) {
@ -23,7 +25,7 @@ final class ChatApi(
findOption(chatId) map (_ | Chat.makeUser(chatId))
def write(chatId: ChatId, userId: String, text: String, public: Boolean): Fu[Option[UserLine]] =
makeLine(userId, text) flatMap {
makeLine(chatId, userId, text) flatMap {
_ ?? { line =>
pushLine(chatId, line) >>- {
shutup ! public.fold(
@ -38,14 +40,36 @@ final class ChatApi(
pushLine(chatId, line) inject line.some
}
private[ChatApi] def makeLine(userId: String, t1: String): Fu[Option[UserLine]] = UserRepo byId userId map {
_ flatMap { user =>
Writer cut t1 ifFalse user.disabled flatMap { t2 =>
def timeout(chatId: ChatId, modId: String, username: String, reason: ChatTimeout.Reason): Funit =
coll.byId[UserChat](chatId) zip UserRepo.named(modId) zip UserRepo.named(username) flatMap {
case ((Some(chat), Some(mod)), Some(user)) if isMod(mod) => doTimeout(chat, mod, user, reason)
case _ => fuccess(none)
}
private def doTimeout(c: UserChat, mod: User, user: User, reason: ChatTimeout.Reason): Funit = {
val line = UserLine(
username = systemUserId,
text = s"${user.username} was timed out 10 minutes for ${reason.name}.",
troll = false, deleted = false)
val chat = c.markDeleted(user) add line
coll.update($id(chat.id), chat).void >>
chatTimeout.add(c, mod, user, reason) >>- {
val channel = Symbol(s"chat-${chat.id}")
lilaBus.publish(actorApi.MarkDeleted(user.username), channel)
lilaBus.publish(actorApi.ChatLine(chat.id, line), channel)
}
}
private def isMod(user: User) = lila.security.Granter(_.MarkTroll)(user)
private[ChatApi] def makeLine(chatId: String, userId: String, t1: String): Fu[Option[UserLine]] =
UserRepo.byId(userId) zip chatTimeout.isActive(chatId, userId) map {
case (Some(user), false) if !user.disabled => Writer cut t1 flatMap { t2 =>
flood.allowMessage(user.id, t2) option
UserLine(user.username, Writer preprocessUserInput t2, troll = user.troll, deleted = false)
}
case _ => none
}
}
}
object playerChat {

View File

@ -5,23 +5,17 @@ import lila.user.{ User, UserRepo }
import org.joda.time.DateTime
import reactivemongo.bson._
import scala.concurrent.duration._
final class ChatTimeout(
chatColl: Coll,
timeoutColl: Coll) {
timeoutColl: Coll,
duration: FiniteDuration) {
import ChatTimeout._
private val minutes = 10
def add(chatId: ChatId, modId: String, username: String, reason: Reason): Funit =
chatColl.byId[UserChat](chatId) zip UserRepo.named(modId) zip UserRepo.named(username) flatMap {
case ((Some(chat), Some(mod)), Some(user)) if isMod(mod) => add(chat, mod, user, reason)
case _ => fuccess(none)
}
private def add(chat: UserChat, mod: User, user: User, reason: Reason): Funit =
isActive(chat, user) flatMap {
def add(chat: UserChat, mod: User, user: User, reason: Reason): Funit =
isActive(chat.id, user.id) flatMap {
case true => funit
case false => timeoutColl.insert($doc(
"_id" -> makeId,
@ -30,13 +24,13 @@ final class ChatTimeout(
"user" -> user.id,
"reason" -> reason,
"createdAt" -> DateTime.now,
"expiresAt" -> DateTime.now.plusMinutes(minutes))).void
"expiresAt" -> DateTime.now.plusSeconds(duration.toSeconds.toInt))).void
}
def isActive(chat: UserChat, user: User): Fu[Boolean] =
def isActive(chatId: String, userId: User.ID): Fu[Boolean] =
timeoutColl.exists($doc(
"chat" -> chat.id,
"user" -> user.id,
"chat" -> chatId,
"user" -> userId,
"expiresAt" $exists true))
def activeUserIds(chat: UserChat): Fu[List[String]] =
@ -55,8 +49,6 @@ final class ChatTimeout(
private val idSize = 8
private def makeId = scala.util.Random.alphanumeric take idSize mkString
private def isMod(user: User) = lila.security.Granter(_.MarkTroll)(user)
}
object ChatTimeout {
@ -64,10 +56,10 @@ object ChatTimeout {
sealed abstract class Reason(val key: String, val name: String)
object Reason {
case object PublicShaming extends Reason("shaming", "Public shaming; please use lichess.org/report")
case object Insult extends Reason("insult", "Disrespecting other players")
case object Spam extends Reason("spam", "Spamming the chat")
case object Other extends Reason("other", "Inappropriate behavior")
case object PublicShaming extends Reason("shaming", "public shaming; please use lichess.org/report")
case object Insult extends Reason("insult", "disrespecting other players")
case object Spam extends Reason("spam", "spamming the chat")
case object Other extends Reason("other", "inappropriate behavior")
val all = List(PublicShaming, Insult, Spam, Other)
def apply(key: String) = all.find(_.key == key)
}

View File

@ -2,6 +2,7 @@ package lila.chat
import akka.actor.{ ActorSystem, Props, ActorSelection }
import com.typesafe.config.Config
import scala.concurrent.duration._
import lila.common.PimpedConfig._
@ -18,21 +19,30 @@ final class Env(
val MaxLinesPerChat = config getInt "max_lines"
val NetDomain = config getString "net.domain"
val ActorName = config getString "actor.name"
val TimeoutDuration = config duration "timeout.duration"
val TimeoutCheckEvery = config duration "timeout.check_every"
}
import settings._
private val chatTimeout = new ChatTimeout(
chatColl = chatColl,
timeoutColl = timeoutColl,
duration = TimeoutDuration)
val api = new ChatApi(
coll = chatColl,
chatTimeout = chatTimeout,
flood = flood,
shutup = shutup,
lilaBus = system.lilaBus,
maxLinesPerChat = MaxLinesPerChat,
netDomain = NetDomain)
private val timeout = new ChatTimeout(
chatColl = chatColl,
timeoutColl = timeoutColl)
system.scheduler.schedule(TimeoutCheckEvery, TimeoutCheckEvery) {
chatTimeout.checkExpired
}
system.actorOf(Props(new FrontActor(api, timeout)), name = ActorName)
system.actorOf(Props(new FrontActor(api)), name = ActorName)
private[chat] lazy val chatColl = db(CollectionChat)
private[chat] lazy val timeoutColl = db(CollectionTimeout)

View File

@ -6,9 +6,7 @@ import chess.Color
import actorApi._
import lila.common.PimpedJson._
private[chat] final class FrontActor(
api: ChatApi,
timeout: ChatTimeout) extends Actor {
private[chat] final class FrontActor(api: ChatApi) extends Actor {
def receive = {
@ -26,7 +24,7 @@ private[chat] final class FrontActor(
modId <- member.userId
username <- data.str("username")
reason <- data.str("reason") flatMap ChatTimeout.Reason.apply
} timeout.add(chatId, modId, username, reason)
} api.userChat.timeout(chatId, modId, username, reason)
}
def publish(chatId: String, replyTo: ActorRef)(lineOption: Option[Line]) {

View File

@ -1,5 +1,6 @@
package lila.chat
import lila.common.PimpedJson._
import play.api.libs.json._
object JsonView {
@ -14,7 +15,7 @@ object JsonView {
lazy val timeoutReasons = Json toJson ChatTimeout.Reason.all
implicit val timeoutReasonWriter: Writes[ChatTimeout.Reason] = OWrites[ChatTimeout.Reason] { r =>
Json.obj( "key" -> r.key, "name" -> r.name)
Json.obj("key" -> r.key, "name" -> r.name)
}
implicit val mixedChatWriter: Writes[MixedChat] = Writes[MixedChat] { c =>
@ -31,8 +32,11 @@ object JsonView {
}
private implicit val userLineWriter = Writes[UserLine] { l =>
val o = Json.obj("u" -> l.username, "t" -> l.text)
if (l.troll) o + ("r" -> JsBoolean(true)) else o
Json.obj(
"u" -> l.username,
"t" -> l.text,
"r" -> l.troll.option(true),
"d" -> l.deleted.option(true)).noNull
}
private implicit val playerLineWriter = Writes[PlayerLine] { l =>

View File

@ -1,5 +1,7 @@
package lila.chat
import lila.user.User
import chess.Color
sealed trait Line {
@ -16,7 +18,12 @@ case class UserLine(
text: String,
troll: Boolean,
deleted: Boolean) extends Line {
def author = username
def userId = User normalize username
def delete = copy(deleted = true)
}
case class PlayerLine(
color: Color,
@ -54,7 +61,12 @@ object Line {
case UserLineRegex(username, "?", text) => UserLine(username, text, troll = false, deleted = true).some
case _ => none
}
def userLineToStr(x: UserLine) = s"${x.username}${if (x.troll) "!" else " "}${x.text}"
def userLineToStr(x: UserLine) = {
val sep = if (x.troll) "!"
else if (x.deleted) "?"
else " "
s"${x.username}$sep${x.text}"
}
def strToLine(str: String): Option[Line] = strToUserLine(str) orElse {
str.headOption flatMap Color.apply map { color =>

View File

@ -9,3 +9,5 @@ case class PlayerTalk(chatId: String, white: Boolean, text: String, replyTo: Act
case class SystemTalk(chatId: String, text: String, replyTo: ActorRef)
case class ChatLine(chatId: String, line: Line)
case class Timeout(chatId: String, member: lila.socket.SocketMember, data: JsValue)
case class MarkDeleted(username: String)

View File

@ -21,6 +21,15 @@ private[simul] final class Socket(
uidTimeout: Duration,
socketTimeout: Duration) extends SocketActor[Member](uidTimeout) with Historical[Member, Messadata] {
override def preStart() {
lilaBus.subscribe(self, Symbol(s"chat-$simulId"))
}
override def postStop() {
super.postStop()
lilaBus.unsubscribe(self)
}
private val timeBomb = new TimeBomb(socketTimeout)
private var delayedCrowdNotification = false
@ -74,9 +83,12 @@ private[simul] final class Socket(
case _ =>
}
case GetVersion => sender ! history.version
case lila.chat.actorApi.MarkDeleted(username) =>
notifyVersion("chat_mark_deleted", username, Messadata())
case Socket.GetUserIds => sender ! userIds
case GetVersion => sender ! history.version
case Socket.GetUserIds => sender ! userIds
case Join(uid, user) =>
val (enumerator, channel) = Concurrent.broadcast[JsValue]

View File

@ -745,6 +745,16 @@ div.mchat .moderation {
div.mchat .lichess_say {
border-width: 1px 0 0 0;
}
div.mchat .deleted {
opacity: 0.5;
}
div.mchat .system {
display: block;
opacity: 0.8;
font-style: italic;
text-align: center;
font-size: 0.8em;
}
.notification .message_infos,
.notification .game_infos,
div.side_box .game_infos {

View File

@ -11,10 +11,21 @@ module.exports = function(opts) {
isMod: opts.mod,
placeholderKey: 'talkInChat',
moderating: m.prop(null),
loading: m.prop(false)
loading: m.prop(false),
timedOut: m.prop(false)
};
var socket = makeSocket(lichess.socket.send);
var timeout = function(username) {
lines.forEach(function(l) {
if (l.u === username) l.d = true;
});
m.redraw();
};
var socket = makeSocket({
send: lichess.socket.send,
timeout: timeout
});
var moderation = vm.isMod ? makeModeration({
reasons: opts.timeoutReasons,
send: socket.send

View File

@ -1,12 +1,16 @@
var m = require('mithril');
module.exports = function(send) {
module.exports = function(opts) {
var handlers = {};
var handlers = {
timeout: opts.timeout
};
return {
send: send,
send: opts.send,
receive: function(type, data) {
console.log(type, data);
console.log(handlers[type]);
if (handlers[type]) {
handlers[type](data);
return true;

View File

@ -1,18 +1,34 @@
var m = require('mithril');
var moderationView = require('./moderation').view;
var deletedDom = m('em.deleted', '<deleted>');
function renderLine(ctrl) {
return function(line) {
if (line.u === 'lichess') return m('li', m('em.system', line.t));
return m('li', {
'data-username': line.u
}, [
ctrl.vm.isMod ? moderationView.lineAction : null,
m.trust($.userLinkLimit(line.u, 14)),
line.t
line.d ? deletedDom : line.t
]);
};
}
function sameLines(l1, l2) {
return l1.d && l2.d && l1.u === l2.u;
}
function dedupLines(lines) {
var prev, ls = [];
lines.forEach(function(l) {
if (!prev || !sameLines(prev, l)) ls.push(l);
prev = l;
});
return ls;
}
function discussion(ctrl) {
return m('div.discussion', [
m('div.top', [
@ -35,7 +51,7 @@ function discussion(ctrl) {
}, 500);
}
},
ctrl.lines.map(renderLine(ctrl))
dedupLines(ctrl.lines).map(renderLine(ctrl))
),
m('input', {
class: 'lichess_say',