automated chan eviction

This commit is contained in:
Thibault Duplessis 2013-12-30 17:58:18 +01:00
parent 33b900d3ca
commit a20f292810
14 changed files with 216 additions and 87 deletions

View file

@ -185,13 +185,11 @@ private[controllers] trait LilaController
private def contextBuilder(ctx: lila.user.UserContext, withChan: Option[Chan] = None): Fu[(Option[Chat], Option[lila.pref.Pref], Option[JsObject])] =
ctx.me.fold(fuccess((none[Chat], none[lila.pref.Pref], none[JsObject]))) { me
(HTTPRequest.isSynchronousHttp(ctx.req) ?? {
Env.chat.api.get(me) flatMap { chan
Env.chat.api.populate(withChan.fold(chan)(chan.withPageChan), me) recoverWith {
case e: Exception {
play.api.Logger("controller").info(e.getMessage)
Env.chat.api.populate(chan, me)
}
Env.chat.api get me flatMap { chat
val pageChat = withChan.fold(chat) { chan
Env.chat.api.truncate(me, chat withPageChan chan, chan.key)
}
Env.chat.api.populate(pageChat, me)
} map (_.some)
} recover {
case e: Exception {

View file

@ -2,48 +2,61 @@ package lila.chat
import java.util.regex.Matcher.quoteReplacement
import Line.{ BSONFields L }
import org.apache.commons.lang3.StringEscapeUtils.escapeXml
import play.api.i18n.Lang
import play.api.libs.json._
import lila.db.api._
import lila.db.Implicits._
import lila.user.{ User, UserRepo }
import tube.lineTube
private[chat] final class Api(
namer: Namer,
chanVoter: ChanVoter,
flood: lila.security.Flood,
relationApi: lila.relation.RelationApi,
prefApi: lila.pref.PrefApi,
netDomain: String) {
def get(user: User): Fu[ChatHead] = prefApi getPref user flatMap { p
val langChan = LangChan(Lang(user.lang | "en"))
p.chat.isDefault.fold({
val p2 = p.updateChat(c ChatHead(c) join langChan updatePref c)
prefApi setPref p2 inject p2.chat
}, fuccess(p.chat)) map { pref
ChatHead(pref).setChan(langChan, true)
}
def join(user: User, chat: ChatHead, chan: Chan): ChatHead = {
chanVoter(user.id, chan.key)
truncate(user, chat join chan, chan.key)
}
def get(userId: String): Fu[ChatHead] =
(UserRepo byId userId) flatten s"No such user: $userId" flatMap get
def join(member: ChatMember, chan: Chan): Fu[ChatHead] =
getUser(member.userId) map { user join(user, member.head, chan) }
def show(user: User, chat: ChatHead, chan: Chan): ChatHead =
truncate(user, chat.setChan(chan, true), chan.key)
def truncate(user: User, chat: ChatHead, not: String): ChatHead =
if (chat.chans.size <= Chat.maxChans) chat else {
val nots = Set(LangChan(user).key, not)
def filter(keys: Seq[String]) = keys filterNot nots.contains
filter(chat.inactiveChanKeys).headOption match {
case Some(key) truncate(user, chat.unsetChanKey(key), not)
case _ chanVoter.lessVoted(user.id, filter(chat.chanKeys)) match {
case Some(key) truncate(user, chat.unsetChanKey(key), not)
case _ filter(chat.chanKeys).headOption match {
case Some(key) truncate(user, chat unsetChanKey key, not)
case _ chat
}
}
}
}
def get(user: User): Fu[ChatHead] = prefApi getPref user map { pref
show(user, ChatHead(pref.chat), LangChan(user))
}
def get(userId: String): Fu[ChatHead] = getUser(userId) flatMap get
def populate(head: ChatHead, user: User): Fu[Chat] =
relationApi blocking user.id flatMap { blocks
val selectTroll = user.troll.fold(Json.obj(), Json.obj(L.troll -> false))
val selectBlock = Json.obj(L.username -> $nin(blocks))
namer.chans(head.chans, user) zip
$find($query(
selectTroll ++
selectBlock ++
Json.obj(L.chan -> $in(head.activeChanKeys))
) sort $sort.desc(L.date), 20) map {
case (namedChans, lines) Chat(head, namedChans, lines.reverse)
namer.chans(head.chans, user) zip {
relationApi blocking user.id flatMap {
LineRepo.find(head.activeChanKeys, user.troll, _, 20) flatMap {
_.map(namer.line).sequenceFu
}
}
} map {
case (namedChans, namedLines) Chat(head, namedChans, namedLines.reverse)
}
def makeLine(chanName: String, userId: String, t1: String): Fu[Option[Line]] =
@ -67,8 +80,7 @@ private[chat] final class Api(
case UserChan(u1, u2) relationApi.areFriends(u1, u2)
case _ fuccess(true)
}) flatMap {
case true write(line) inject line.some
case false fuccess(none)
_ ?? (LineRepo insert line inject line.some)
}
case Some(line) {
logger.info(s"Flood: $userId @ $chanName : $text")
@ -76,13 +88,14 @@ private[chat] final class Api(
}
}
def write(line: Line): Funit = $insert bson line
def systemWrite(chan: Chan, text: String): Fu[Line] = {
val line = Line.system(chan, text)
$insert bson line inject line
LineRepo insert line inject line
}
private[chat] def getUser(userId: String) =
(UserRepo byId userId) flatten s"No such user: $userId"
private val logger = play.api.Logger("chat")
private object Writer {

View file

@ -4,6 +4,7 @@ import play.api.i18n.Lang
import play.api.libs.json._
import lila.i18n.LangList
import lila.user.User
sealed trait Chan extends Ordered[Chan] {
def typ: String
@ -36,20 +37,24 @@ sealed abstract class IdChan(
def idOption = id.some
def compare(other: Chan) = other match {
case c: LangChan -c.compare(this)
case c: IdChan id compare c.id
case _ -1
case c: LangChan 1
case c: ContextualChan => -1
case c: IdChan typ compare c.typ match {
case 0 id compare c.id
case x x
}
case _ -1
}
}
sealed trait ContextualChan { self: Chan
override val contextual = true
override def compare(other: Chan) = 1
}
sealed abstract class AutoActiveChan(typ: String, i: String)
extends IdChan(typ, true) with ContextualChan {
val id = i
override def compare(other: Chan) = 1
}
object TvChan extends StaticChan(Chan.typ.tv, "TV") with ContextualChan
@ -67,6 +72,7 @@ case class LangChan(lang: Lang) extends IdChan(Chan.typ.lang, false) {
}
object LangChan {
def apply(code: String): Option[LangChan] = LangList.exists(code) option LangChan(Lang(code))
def apply(user: User): LangChan = LangChan(Lang(user.lang | "en"))
}
case class UserChan(u1: String, u2: String) extends IdChan("user", false) {

View file

@ -0,0 +1,28 @@
package lila.chat
import scala.concurrent.duration._
private[chat] final class ChanVoter {
private type UserId = String
private type ChanKey = String
private val cache = lila.memo.Builder.expiry[UserId, List[ChanKey]](1.hour)
def apply(userId: UserId, chanKey: ChanKey) {
cache.put(userId, chanKey :: votedKeysOf(userId).filterNot(chanKey==))
}
def lessVoted(userId: UserId, in: Seq[ChanKey]): Option[ChanKey] = {
val votedKeys = votedKeysOf(userId)
(in map { key
key -> (votedKeys indexOf key match {
case -1 Int.MaxValue
case x x
})
} sortBy (-_._2)).headOption.map(_._1)
}
private def votedKeysOf(userId: UserId): List[ChanKey] =
~Option(cache getIfPresent userId)
}

View file

@ -18,9 +18,12 @@ case class ChatHead(
def setChan(c: Chan, value: Boolean) = if (value) {
if (chans contains c) this else copy(chans = c :: chans).sorted
}
else {
if (chans contains c) copy(chans = chans filterNot (c==)) else this
}
else unsetChanKey(c.key)
def unsetChanKey(key: String) = copy(
chans = chans filterNot (_.key == key),
activeChanKeys = activeChanKeys filterNot (key==),
mainChanKey = mainChanKey filterNot (key==))
def withPageChan(c: Chan) = setChan(c, true).copy(
pageChanKey = c.key.some,
@ -28,12 +31,16 @@ case class ChatHead(
mainChanKey = c.autoActive.fold(c.key.some, mainChanKey))
def setActiveChanKey(key: String, value: Boolean) =
copy(activeChanKeys = if (value) activeChanKeys + key else activeChanKeys - key)
copy(activeChanKeys = if (value && exists(key)) activeChanKeys + key else activeChanKeys - key)
def exists(key: String) = chanKeys contains key
def setMainChanKey(key: Option[String]) = copy(mainChanKey = key)
def join(c: Chan) = setChan(c, true).setActiveChanKey(c.key, true).setMainChanKey(c.key.some)
def inactiveChanKeys = chanKeys filterNot activeChanKeys.contains
def updatePref(pref: ChatPref) = ChatPref(
on = pref.on,
chans = chans filterNot (_.contextual) map (_.key),
@ -52,7 +59,7 @@ object ChatHead {
mainChanKey = pref.mainChan).sorted
}
case class Chat(head: ChatHead, namedChans: List[NamedChan], lines: List[Line]) {
case class Chat(head: ChatHead, namedChans: List[NamedChan], lines: List[NamedLine]) {
def toJson = Json.obj(
"lines" -> lines.map(_.toJson),
@ -67,5 +74,5 @@ object Chat {
val maxChans = 7
val systemUsername = "Lichess"
val systemUserId = "lichess"
}

View file

@ -11,7 +11,6 @@ import lila.hub.actorApi.chat._
import lila.pref.{ Pref, PrefApi }
import lila.socket.actorApi.{ SocketEnter, SocketLeave }
import lila.socket.Socket
import lila.user.UserRepo
private[chat] final class ChatActor(
api: Api,
@ -34,12 +33,13 @@ private[chat] final class ChatActor(
def receive = {
case line: Line {
lazy val json = lineMessage(line)
case line: Line lineMessage(line) foreach { json
members.values foreach { m if (m wants line) m tell json }
}
case Tell(uid, line) members get uid foreach { _ tell lineMessage(line) }
case Tell(uid, line) lineMessage(line) foreach { json
members get uid foreach { _ tell json }
}
case System(chanTyp, chanId, text) Chan(chanTyp, chanId) foreach { chan
api.systemWrite(chan, text) pipeTo self
@ -47,8 +47,8 @@ private[chat] final class ChatActor(
case SetOpen(member, value) prefApi.setChatPref(member.userId, _.copy(on = value))
case Join(member, chan) {
member.updateHead(_ join chan)
case Join(member, chan) api.join(member, chan) foreach { head
member setHead head
saveAndReload(member)
}
@ -58,11 +58,12 @@ private[chat] final class ChatActor(
}
case Query(member, toId)
UserRepo byId toId flatten s"Can't query non existing user $toId" foreach { to
api getUser toId foreach { to
relationApi.follows(to.id, member.userId) onSuccess {
case true
member.updateHead(_ join UserChan(member.userId, toId))
case true api.join(member, UserChan(member.userId, toId)) foreach { head
member setHead head
saveAndReload(member)
}
}
}
@ -79,14 +80,18 @@ private[chat] final class ChatActor(
case Input(uid, o) (o str "t") |@| (o obj "d") |@| (members get uid) apply {
case (typ, data, member) typ match {
case "chat.register" relationApi blocking member.userId foreach { blocks
member.setHead(ChatHead(
chans = (~data.arrAs("chans")(_.asOpt[String]) map Chan.parse).flatten,
pageChanKey = data str "pageChan",
activeChanKeys = (~data.arrAs("activeChans")(_.asOpt[String])).toSet,
mainChanKey = data str "mainChan"))
member setBlocks blocks
reload(member)
case "chat.register" api getUser member.userId foreach { user
relationApi blocking user.id zip (api get user) foreach {
case (blocks, head) {
val pageHead = (data str "pageChan" flatMap Chan.parse).fold(head) {
case c if c.autoActive api.join(user, head, c)
case c api.show(user, head, c)
}
member setHead pageHead
member setBlocks blocks
reload(member)
}
}
}
case "chat.tell" data str "text" foreach { text
val chanOption = data str "chan" flatMap Chan.parse
@ -105,7 +110,9 @@ private[chat] final class ChatActor(
case SocketLeave(uid) members -= uid
}
private def lineMessage(line: Line) = Socket.makeMessage("chat.line", line.toJson)
private def lineMessage(line: Line) = namer line line map { namedLine
Socket.makeMessage("chat.line", namedLine.toJson)
}
private def withMembersOf(userId: String)(f: ChatMember Unit) {
members.values foreach { member
@ -118,7 +125,8 @@ private[chat] final class ChatActor(
}
private def reload(m: ChatMember) {
UserRepo byId m.userId flatten s"User of $m not found" foreach { user
Thread sleep 500
api getUser m.userId foreach { user
api.populate(m.head, user) foreach { chat
m tell Socket.makeMessage("chat.reload", chat.toJson)
}

View file

@ -24,6 +24,7 @@ final class Env(
lazy val api = new Api(
namer = namer,
chanVoter = new ChanVoter,
flood = flood,
relationApi = relationApi,
prefApi = prefApi,

View file

@ -11,23 +11,24 @@ import lila.user.User
case class Line(
id: String,
chan: Chan,
username: String,
userId: String,
date: DateTime,
text: String,
troll: Boolean) {
def system = username == Chat.systemUsername
def system = userId == Chat.systemUserId
def html = Html {
escapeXml(text)
}
}
case class NamedLine(line: Line, username: String) {
def toJson = Json.obj(
"chan" -> chan.key,
"chan" -> line.chan.key,
"user" -> username,
"html" -> html.toString)
def userId = username.toLowerCase
"html" -> line.html.toString)
}
object Line {
@ -37,7 +38,7 @@ object Line {
def make(chan: Chan, user: User, text: String): Line = Line(
id = Random nextString idSize,
chan = chan,
username = user.username,
userId = user.id,
date = DateTime.now,
text = text,
troll = user.troll)
@ -45,7 +46,7 @@ object Line {
def system(chan: Chan, text: String): Line = Line(
id = Random nextString idSize,
chan = chan,
username = Chat.systemUsername,
userId = Chat.systemUserId,
date = DateTime.now,
text = text,
troll = false)
@ -55,7 +56,7 @@ object Line {
object BSONFields {
val id = "_id"
val chan = "c"
val username = "u"
val userId = "ui"
val date = "d"
val text = "t"
val troll = "tr"
@ -72,7 +73,7 @@ object Line {
def reads(r: BSON.Reader): Line = Line(
id = r str id,
chan = Chan parse (r str chan) err s"Line has invalid chan: ${r.doc}",
username = r str username,
userId = r str userId,
text = r str text,
date = r date date,
troll = r bool troll)
@ -80,7 +81,7 @@ object Line {
def writes(w: BSON.Writer, o: Line) = BSONDocument(
id -> o.id,
chan -> o.chan.key,
username -> o.username,
userId -> o.userId,
text -> o.text,
date -> o.date,
troll -> o.troll)

View file

@ -0,0 +1,65 @@
package lila.chat
import org.joda.time.DateTime
import play.api.libs.json._
import play.modules.reactivemongo.json.BSONFormats.toJSON
import play.modules.reactivemongo.json.ImplicitBSONHandlers._
import reactivemongo.api._
import reactivemongo.bson._
import lila.common.PimpedJson._
import lila.db.api._
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.Implicits._
import tube.lineTube
object LineRepo {
import Line.{ BSONFields L }
def find(chanKeys: Set[String], troll: Boolean, blocks: Set[String], limit: Int): Fu[List[Line]] =
$find($query(
troll.fold(Json.obj(), Json.obj(L.troll -> false)) ++
Json.obj(L.userId -> $nin(blocks)) ++
Json.obj(L.chan -> $in(chanKeys))
) sort $sort.desc(L.date), limit)
def insert(line: Line) = $insert bson line
def leastTalked(userId: String, chanKeys: Seq[String]): Fu[Option[String]] = {
import reactivemongo.core.commands._
val command = Aggregate(lineTube.coll.name, Seq(
Match(JsObjectWriter write Json.obj(
L.userId -> userId,
L.chan -> $in(chanKeys),
L.date -> $gt($date(DateTime.now.minusMinutes(30)))
)),
GroupField(L.chan)("nb" -> SumValue(1)),
Sort(Seq(Ascending("nb"))),
Limit(1)
))
lineTube.coll.db.command(command) map { stream
stream.headOption flatMap { obj
toJSON(obj).asOpt[JsObject] flatMap { _ str "_id" }
}
}
}
def leastActive(chanKeys: Seq[String]): Fu[Option[String]] = {
import reactivemongo.core.commands._
val command = Aggregate(lineTube.coll.name, Seq(
Match(JsObjectWriter write Json.obj(
L.chan -> $in(chanKeys),
L.date -> $gt($date(DateTime.now.minusMinutes(15)))
)),
GroupField(L.chan)("nb" -> SumValue(1)),
Sort(Seq(Ascending("nb"))),
Limit(1)
))
lineTube.coll.db.command(command) map { stream
stream.headOption flatMap { obj
toJSON(obj).asOpt[JsObject] flatMap { _ str "_id" }
}
}
}
}

View file

@ -10,6 +10,8 @@ import lila.user.User
private[chat] final class Namer(getUsername: String Fu[String]) {
def line(l: Line): Fu[NamedLine] = getUsername(l.userId) map { NamedLine(l, _) }
def chan(c: Chan, as: User): Fu[NamedChan] =
chanCache(c -> as) map { NamedChan(c, _) }

View file

@ -65,7 +65,7 @@ object ApplicationBuild extends Build {
)
lazy val chat = project("chat", Seq(
common, db, hub, socket, user, security, pref, relation, game, tournament, i18n)).settings(
common, db, hub, memo, socket, user, security, pref, relation, game, tournament, i18n)).settings(
libraryDependencies ++= provided(
play.api, RM, PRM)
)

View file

@ -883,10 +883,7 @@ var storage = {
$('body').on('socket.open', function() {
lichess.socket.send('chat.register', {
chans: _.keys(self.head.chans),
pageChan: self.head.pageChan,
activeChans: self.head.activeChans,
mainChan: self.head.mainChan
pageChan: self.head.pageChan
});
});
},

View file

@ -511,8 +511,8 @@ div.footer div.right {
#chat .chan {
display: block;
text-align: right;
line-height: 31px;
height: 31px;
line-height: 27px;
height: 27px;
padding: 0px 10px;
border-bottom: 1px solid #2a2a2a;
cursor: pointer;
@ -529,7 +529,7 @@ div.footer div.right {
float: right;
width: 20px;
height: 20px;
margin: 5px 16px 0 -13px;
margin: 3px 16px 0 -13px;
position: relative;
}
#chat div.check input[type=checkbox] {
@ -578,9 +578,11 @@ div.footer div.right {
#chat > .room > .lines {
height: 171px;
overflow: hidden;
/* overflow-y: auto; */
border-bottom: 1px solid #444;
}
#chat > .room > .lines:hover {
overflow-y: auto;
}
#chat .line {
width: 100%;
padding: 2px 10px;

5
todo
View file

@ -99,7 +99,7 @@ follow a user #chess claymore http://www.freechess.org/Help/HelpFiles/follow.htm
team menu should lead to "My teams"
atwar style chat system http://imgur.com/a/buwy3 (but better)
replay games using move time
antiboost tool for mods
antifarming tool for mods
list all players by ranking.
automatic ai cheat detection is broken
time pie chart colors http://en.lichess.org/52ede6hu/stats
@ -109,4 +109,5 @@ full page recent forum posts
deploy
------
db.chat.ensureIndex({tr:1,c:1,d:-1}
db.chat.ensureIndex({tr:1,c:1,d:-1})
db.chat.ensureIndex({u:1,c:1,d:-1})