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