new chat WIP
parent
ea48ac669a
commit
a2735be314
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]) {
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue