281 lines
9.6 KiB
Scala
281 lines
9.6 KiB
Scala
package lila.lobby
|
|
|
|
import ornicar.scalalib.Zero
|
|
import play.api.libs.json._
|
|
import scala.concurrent.duration._
|
|
import scala.concurrent.Promise
|
|
|
|
import actorApi._
|
|
import lila.game.Pov
|
|
import lila.hub.actorApi.game.ChangeFeatured
|
|
import lila.hub.actorApi.lobby._
|
|
import lila.hub.actorApi.timeline._
|
|
import lila.hub.Trouper
|
|
import lila.pool.{ PoolApi, PoolConfig }
|
|
import lila.rating.RatingRange
|
|
import lila.socket.RemoteSocket.{ Protocol => P, _ }
|
|
import lila.socket.Socket.{ makeMessage, Sri, Sris }
|
|
import lila.user.{ User, UserRepo }
|
|
|
|
final class LobbySocket(
|
|
remoteSocketApi: lila.socket.RemoteSocket,
|
|
lobby: LobbyTrouper,
|
|
blocking: User.ID => Fu[Set[User.ID]],
|
|
poolApi: PoolApi,
|
|
system: akka.actor.ActorSystem
|
|
) {
|
|
|
|
import LobbySocket._
|
|
import Protocol._
|
|
type SocketController = PartialFunction[(String, JsObject), Unit]
|
|
|
|
val trouper: Trouper = new Trouper {
|
|
|
|
private val members = scala.collection.mutable.AnyRefMap.empty[SriStr, Member]
|
|
private var idleSris = collection.mutable.Set[SriStr]()
|
|
private var hookSubscriberSris = collection.mutable.Set[SriStr]()
|
|
private var removedHookIds = ""
|
|
|
|
val process: Trouper.Receive = {
|
|
|
|
case GetMember(sri, promise) => promise success members.get(sri.value)
|
|
|
|
case GetSrisP(promise) =>
|
|
promise success Sris(members.keySet.map(Sri.apply)(scala.collection.breakOut))
|
|
lila.mon.lobby.socket.idle(idleSris.size)
|
|
lila.mon.lobby.socket.hookSubscribers(hookSubscriberSris.size)
|
|
|
|
case Cleanup =>
|
|
idleSris retain members.contains
|
|
hookSubscriberSris retain members.contains
|
|
|
|
case Join(member) => members += (member.sri.value -> member)
|
|
|
|
case LeaveBatch(sris) => sris foreach quit
|
|
case LeaveAll =>
|
|
members.clear()
|
|
idleSris.clear()
|
|
hookSubscriberSris.clear()
|
|
|
|
case ReloadTournaments(html) => tellActive(makeMessage("tournaments", html))
|
|
|
|
case ReloadSimuls(html) => tellActive(makeMessage("simuls", html))
|
|
|
|
case ReloadTimelines(users) => send(Out.tellLobbyUsers(users, makeMessage("reload_timeline")))
|
|
|
|
case AddHook(hook) => send(P.Out.tellSris(
|
|
hookSubscriberSris diff idleSris filter { sri =>
|
|
members get sri exists { Biter.showHookTo(hook, _) }
|
|
} map Sri.apply,
|
|
makeMessage("had", hook.render)
|
|
))
|
|
|
|
case RemoveHook(hookId) => removedHookIds = s"$removedHookIds$hookId"
|
|
|
|
case SendHookRemovals =>
|
|
if (removedHookIds.nonEmpty) {
|
|
tellActiveHookSubscribers(makeMessage("hrm", removedHookIds))
|
|
removedHookIds = ""
|
|
}
|
|
system.scheduler.scheduleOnce(1249 millis)(this ! SendHookRemovals)
|
|
|
|
case JoinHook(sri, hook, game, creatorColor) =>
|
|
lila.mon.lobby.hook.join()
|
|
send(P.Out.tellSri(hook.sri, gameStartRedirect(game pov creatorColor)))
|
|
send(P.Out.tellSri(sri, gameStartRedirect(game pov !creatorColor)))
|
|
|
|
case JoinSeek(userId, seek, game, creatorColor) =>
|
|
lila.mon.lobby.seek.join()
|
|
send(Out.tellLobbyUsers(List(seek.user.id), gameStartRedirect(game pov creatorColor)))
|
|
send(Out.tellLobbyUsers(List(userId), gameStartRedirect(game pov !creatorColor)))
|
|
|
|
case PoolApi.Pairings(pairings) => send(Protocol.Out.pairings(pairings))
|
|
|
|
case HookIds(ids) => tellActiveHookSubscribers(makeMessage("hli", ids mkString ""))
|
|
|
|
case AddSeek(_) | RemoveSeek(_) => tellActive(makeMessage("reload_seeks"))
|
|
|
|
case lila.hub.actorApi.streamer.StreamsOnAir(html) => tellActive(makeMessage("streams", html))
|
|
|
|
case ChangeFeatured(_, msg) => tellActive(msg)
|
|
|
|
case SetIdle(sri, true) => idleSris += sri.value
|
|
case SetIdle(sri, false) => idleSris -= sri.value
|
|
|
|
case HookSub(member, false) => hookSubscriberSris -= member.sri.value
|
|
case AllHooksFor(member, hooks) =>
|
|
send(P.Out.tellSri(member.sri, makeMessage("hooks", JsArray(hooks.map(_.render)))))
|
|
hookSubscriberSris += member.sri.value
|
|
}
|
|
|
|
system.lilaBus.subscribe(this, 'changeFeaturedGame, 'streams, 'poolPairings, 'lobbySocket)
|
|
system.scheduler.scheduleOnce(7 seconds)(this ! SendHookRemovals)
|
|
system.scheduler.schedule(1 minute, 1 minute)(this ! Cleanup)
|
|
|
|
private def tellActive(msg: JsObject): Unit = send(Out.tellLobbyActive(msg))
|
|
|
|
private def tellActiveHookSubscribers(msg: JsObject): Unit =
|
|
send(P.Out.tellSris(hookSubscriberSris diff idleSris map Sri.apply, msg))
|
|
|
|
private def gameStartRedirect(pov: Pov) =
|
|
makeMessage("redirect", Json.obj(
|
|
"id" -> pov.fullId,
|
|
"url" -> s"/${pov.fullId}"
|
|
).add("cookie" -> lila.game.AnonCookie.json(pov)))
|
|
|
|
private def quit(sri: Sri): Unit = {
|
|
members -= sri.value
|
|
idleSris -= sri.value
|
|
hookSubscriberSris -= sri.value
|
|
}
|
|
}
|
|
|
|
// solve circular reference
|
|
lobby ! LobbyTrouper.SetSocket(trouper)
|
|
|
|
private val poolLimitPerSri = new lila.memo.RateLimit[SriStr](
|
|
credits = 25,
|
|
duration = 1 minute,
|
|
name = "lobby hook/pool per member",
|
|
key = "lobby.hook_pool.member"
|
|
)
|
|
|
|
private def HookPoolLimit[A: Zero](member: Member, cost: Int, msg: => String)(op: => A) =
|
|
poolLimitPerSri(k = member.sri.value, cost = cost, msg = msg)(op)
|
|
|
|
def controller(member: Member): SocketController = {
|
|
case ("join", o) if !member.bot => HookPoolLimit(member, cost = 5, msg = s"join $o") {
|
|
o str "d" foreach { id =>
|
|
lobby ! BiteHook(id, member.sri, member.user)
|
|
}
|
|
}
|
|
case ("cancel", _) => HookPoolLimit(member, cost = 1, msg = "cancel") {
|
|
lobby ! CancelHook(member.sri)
|
|
}
|
|
case ("joinSeek", o) if !member.bot => HookPoolLimit(member, cost = 5, msg = s"joinSeek $o") {
|
|
for {
|
|
id <- o str "d"
|
|
user <- member.user
|
|
} lobby ! BiteSeek(id, user)
|
|
}
|
|
case ("cancelSeek", o) => HookPoolLimit(member, cost = 1, msg = s"cancelSeek $o") {
|
|
for {
|
|
id <- o str "d"
|
|
user <- member.user
|
|
} lobby ! CancelSeek(id, user)
|
|
}
|
|
case ("idle", o) => trouper ! SetIdle(member.sri, ~(o boolean "d"))
|
|
// entering a pool
|
|
case ("poolIn", o) if !member.bot => HookPoolLimit(member, cost = 1, msg = s"poolIn $o") {
|
|
for {
|
|
user <- member.user
|
|
d <- o obj "d"
|
|
id <- d str "id"
|
|
ratingRange = d str "range" flatMap RatingRange.apply
|
|
blocking = d str "blocking"
|
|
} {
|
|
lobby ! CancelHook(member.sri) // in case there's one...
|
|
poolApi.join(
|
|
PoolConfig.Id(id),
|
|
PoolApi.Joiner(
|
|
userId = user.id,
|
|
sri = member.sri,
|
|
ratingMap = user.perfMap.mapValues(_.rating),
|
|
ratingRange = ratingRange,
|
|
lame = user.lame,
|
|
blocking = user.blocking ++ blocking
|
|
)
|
|
)
|
|
}
|
|
}
|
|
// leaving a pool
|
|
case ("poolOut", o) => HookPoolLimit(member, cost = 1, msg = s"poolOut $o") {
|
|
for {
|
|
id <- o str "d"
|
|
user <- member.user
|
|
} poolApi.leave(PoolConfig.Id(id), user.id)
|
|
}
|
|
// entering the hooks view
|
|
case ("hookIn", _) => HookPoolLimit(member, cost = 2, msg = "hookIn") {
|
|
lobby ! HookSub(member, true)
|
|
}
|
|
// leaving the hooks view
|
|
case ("hookOut", _) => trouper ! HookSub(member, false)
|
|
}
|
|
|
|
private def getOrConnect(sri: Sri, userOpt: Option[User.ID]): Fu[Member] =
|
|
trouper.ask[Option[Member]](GetMember(sri, _)) getOrElse {
|
|
userOpt ?? UserRepo.enabledById flatMap { user =>
|
|
(user ?? { u =>
|
|
remoteSocketApi.baseHandler(P.In.ConnectUser(u.id))
|
|
blocking(u.id)
|
|
}) map { blocks =>
|
|
val member = Member(sri, user map { LobbyUser.make(_, blocks) })
|
|
trouper ! Join(member)
|
|
member
|
|
}
|
|
}
|
|
}
|
|
|
|
private val handler: Handler = {
|
|
case P.In.ConnectSris(cons) => cons foreach {
|
|
case (sri, userId) => getOrConnect(sri, userId)
|
|
}
|
|
case P.In.DisconnectSris(sris) => trouper ! LeaveBatch(sris)
|
|
|
|
case P.In.WsBoot =>
|
|
logger.warn("Remote socket boot")
|
|
lobby ! LeaveAll
|
|
trouper ! LeaveAll
|
|
|
|
case tell @ P.In.TellSri(sri, user, tpe, msg) if messagesHandled(tpe) =>
|
|
getOrConnect(sri, user) foreach { member =>
|
|
controller(member).applyOrElse(tpe -> msg, {
|
|
case _ => logger.warn(s"Can't handle $tpe")
|
|
}: SocketController)
|
|
}
|
|
}
|
|
|
|
private val messagesHandled: Set[String] =
|
|
Set("join", "cancel", "joinSeek", "cancelSeek", "idle", "poolIn", "poolOut", "hookIn", "hookOut")
|
|
|
|
remoteSocketApi.subscribe("lobby-in", P.In.baseReader)(handler orElse remoteSocketApi.baseHandler)
|
|
|
|
private val send: String => Unit = remoteSocketApi.makeSender("lobby-out").apply _
|
|
}
|
|
|
|
private object LobbySocket {
|
|
|
|
type SriStr = String
|
|
|
|
case class Member(sri: Sri, user: Option[LobbyUser]) {
|
|
def bot = user.exists(_.bot)
|
|
def userId = user.map(_.id)
|
|
def isAuth = userId.isDefined
|
|
}
|
|
|
|
object Protocol {
|
|
object Out {
|
|
def pairings(pairings: List[PoolApi.Pairing]) = {
|
|
val redirs = for {
|
|
pairing <- pairings
|
|
color <- chess.Color.all
|
|
sri = pairing sri color
|
|
fullId = pairing.game fullIdOf color
|
|
} yield s"$sri:$fullId"
|
|
s"lobby/pairings ${P.Out.commas(redirs)}"
|
|
}
|
|
def tellLobby(payload: JsObject) = s"tell/lobby ${Json stringify payload}"
|
|
def tellLobbyActive(payload: JsObject) = s"tell/lobby/active ${Json stringify payload}"
|
|
def tellLobbyUsers(userIds: Iterable[User.ID], payload: JsObject) =
|
|
s"tell/lobby/users ${P.Out.commas(userIds)} ${Json stringify payload}"
|
|
}
|
|
}
|
|
|
|
case object Cleanup
|
|
case class Join(member: Member)
|
|
case class GetMember(sri: Sri, promise: Promise[Option[Member]])
|
|
object SendHookRemovals
|
|
case class SetIdle(sri: Sri, value: Boolean)
|
|
}
|