From f45fa62d83616ca3e41fef19a2189c8d89eb6995 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 24 Jun 2015 13:24:16 +0200 Subject: [PATCH] much progress on relay --- app/controllers/Relay.scala | 45 +++++ app/templating/Environment.scala | 3 +- app/templating/RelayHelper.scala | 21 ++ app/views/relay/home.scala.html | 29 +++ app/views/relay/jsI18n.scala.html | 4 + app/views/relay/layout.scala.html | 16 ++ app/views/relay/notFound.scala.html | 14 ++ app/views/relay/show.scala.html | 29 +++ app/views/relay/side.scala.html | 5 + conf/routes | 5 + modules/relay/src/main/Cached.scala | 13 ++ modules/relay/src/main/Env.scala | 67 +++++-- modules/relay/src/main/Importer.scala | 4 +- modules/relay/src/main/JsonView.scala | 25 +++ modules/relay/src/main/Relay.scala | 8 + modules/relay/src/main/RelayRepo.scala | 9 +- modules/relay/src/main/Socket.scala | 90 +++++++++ modules/relay/src/main/SocketHandler.scala | 58 ++++++ modules/relay/src/main/actorApi.scala | 28 +++ modules/relay/src/main/package.scala | 4 +- modules/simul/src/main/Env.scala | 2 - modules/simul/src/main/Socket.scala | 1 - project/Build.scala | 2 +- public/font42/fonts/lichess.woff | Bin 16700 -> 0 bytes public/{font42 => font43}/fonts/lichess.eot | Bin 23264 -> 23620 bytes public/{font42 => font43}/fonts/lichess.svg | 1 + public/{font42 => font43}/fonts/lichess.ttf | Bin 23100 -> 23456 bytes public/font43/fonts/lichess.woff | Bin 0 -> 17036 bytes .../{font42 => font43}/icons-reference.html | 8 + public/{font42 => font43}/styles.css | 3 + public/stylesheets/common.css | 4 +- public/stylesheets/relay.css | 187 ++++++++++++++++++ ui/build | 2 +- ui/relay/gulpfile.js | 54 +++++ ui/relay/package.json | 35 ++++ ui/relay/src/ctrl.js | 43 ++++ ui/relay/src/main.js | 24 +++ ui/relay/src/socket.js | 21 ++ ui/relay/src/view/games.js | 37 ++++ ui/relay/src/view/main.js | 16 ++ 40 files changed, 893 insertions(+), 24 deletions(-) create mode 100644 app/controllers/Relay.scala create mode 100644 app/templating/RelayHelper.scala create mode 100644 app/views/relay/home.scala.html create mode 100644 app/views/relay/jsI18n.scala.html create mode 100644 app/views/relay/layout.scala.html create mode 100644 app/views/relay/notFound.scala.html create mode 100644 app/views/relay/show.scala.html create mode 100644 app/views/relay/side.scala.html create mode 100644 modules/relay/src/main/Cached.scala create mode 100644 modules/relay/src/main/JsonView.scala create mode 100644 modules/relay/src/main/Socket.scala create mode 100644 modules/relay/src/main/SocketHandler.scala create mode 100644 modules/relay/src/main/actorApi.scala delete mode 100644 public/font42/fonts/lichess.woff rename public/{font42 => font43}/fonts/lichess.eot (89%) rename public/{font42 => font43}/fonts/lichess.svg (98%) rename public/{font42 => font43}/fonts/lichess.ttf (89%) create mode 100644 public/font43/fonts/lichess.woff rename public/{font42 => font43}/icons-reference.html (99%) rename public/{font42 => font43}/styles.css (99%) create mode 100644 public/stylesheets/relay.css create mode 100644 ui/relay/gulpfile.js create mode 100644 ui/relay/package.json create mode 100644 ui/relay/src/ctrl.js create mode 100644 ui/relay/src/main.js create mode 100644 ui/relay/src/socket.js create mode 100644 ui/relay/src/view/games.js create mode 100644 ui/relay/src/view/main.js diff --git a/app/controllers/Relay.scala b/app/controllers/Relay.scala new file mode 100644 index 0000000000000..652705d174add --- /dev/null +++ b/app/controllers/Relay.scala @@ -0,0 +1,45 @@ +package controllers + +import play.api.data.Form +import play.api.libs.json._ +import play.api.mvc._ + +import lila.api.Context +import lila.app._ +import lila.relay.{ Relay => RelayModel } +import views._ + +object Relay extends LilaController { + + private def env = Env.relay + + private def relayNotFound(implicit ctx: Context) = NotFound(html.relay.notFound()) + + val index = Open { implicit ctx => + env.repo recent 30 map { relays => + Ok(html.relay.home(relays)) + } + } + + def show(id: String, slug: String) = Open { implicit ctx => + env.repo byId id flatMap { + _.fold(relayNotFound.fuccess) { relay => + if (relay.slug != slug) Redirect(routes.Relay.show(id, relay.slug)).fuccess + else env.version(relay.id) zip env.jsonView(relay) zip chatOf(relay) map { + case ((version, data), chat) => html.relay.show(relay, version, data, chat) + } + } map NoCache + } + } + + def websocket(id: String, apiVersion: Int) = SocketOption[JsValue] { implicit ctx => + (getInt("version") |@| get("sri")).tupled ?? { + case (version, uid) => env.socketHandler.join(id, version, uid, ctx.me) + } + } + + private def chatOf(relay: RelayModel)(implicit ctx: Context) = + ctx.isAuth ?? { + Env.chat.api.userChat find relay.id map (_.forUser(ctx.me).some) + } +} diff --git a/app/templating/Environment.scala b/app/templating/Environment.scala index f88ed4e12c851..5d8d22751b6d0 100644 --- a/app/templating/Environment.scala +++ b/app/templating/Environment.scala @@ -38,7 +38,8 @@ object Environment with AnalysisHelper with IRCHelper with TournamentHelper - with SimulHelper { + with SimulHelper + with RelayHelper { implicit val LilaHtmlMonoid = scalaz.Monoid.instance[Html]( (a, b) => Html(a.body + b.body), diff --git a/app/templating/RelayHelper.scala b/app/templating/RelayHelper.scala new file mode 100644 index 0000000000000..41c28adb9dbc9 --- /dev/null +++ b/app/templating/RelayHelper.scala @@ -0,0 +1,21 @@ +package lila.app +package templating + +import controllers.routes +import lila.api.Context +import lila.relay.Relay +import lila.user.{ User, UserContext } +import lila.relay.Env.{ current => relayEnv } + +import play.api.libs.json.Json +import play.twirl.api.Html + +trait RelayHelper { self: I18nHelper => + + def relayLink(relay: Relay): Html = Html { + val url = routes.Relay.show(relay.id, relay.slug) + s"""${relay.name}""" + } + + def relayIdToName(id: String) = relayEnv.cached name id getOrElse "Chess event" +} diff --git a/app/views/relay/home.scala.html b/app/views/relay/home.scala.html new file mode 100644 index 0000000000000..a20f94ea3ff53 --- /dev/null +++ b/app/views/relay/home.scala.html @@ -0,0 +1,29 @@ +@(relays: List[lila.relay.Relay])(implicit ctx: Context) + +@relay.layout( +title = "Watch Chess events", +side = none) { +
+
+

Watch Chess events

+ + + + + + + + + + @relays.map { rel => + + + + + + } + +
DateEventStatus
@showDate(rel.date)@rel.name@rel.status
+
+
+} diff --git a/app/views/relay/jsI18n.scala.html b/app/views/relay/jsI18n.scala.html new file mode 100644 index 0000000000000..44a8e6acd7060 --- /dev/null +++ b/app/views/relay/jsI18n.scala.html @@ -0,0 +1,4 @@ +@()(implicit ctx: Context) +@Html(J.stringify(i18nJsObject( +trans.finished +))) diff --git a/app/views/relay/layout.scala.html b/app/views/relay/layout.scala.html new file mode 100644 index 0000000000000..2391e6ebad7ef --- /dev/null +++ b/app/views/relay/layout.scala.html @@ -0,0 +1,16 @@ +@(title: String, moreJs: Html = Html(""), side: Option[Html] = None, chat: Option[Html] = None, underchat: Option[Html] = None, chessground: Boolean = true)(body: Html)(implicit ctx: Context) + +@moreCss = { +@cssTag("relay.css") +} + +@base.layout( +title = title, +moreJs = moreJs, +moreCss = moreCss, +side = side, +chat = chat, +underchat = underchat, +chessground = chessground) { +@body +} diff --git a/app/views/relay/notFound.scala.html b/app/views/relay/notFound.scala.html new file mode 100644 index 0000000000000..dd2b356c6c617 --- /dev/null +++ b/app/views/relay/notFound.scala.html @@ -0,0 +1,14 @@ +@()(implicit ctx: Context) + +@relay.layout(title = "Event not found") { +
+
+

Event not found



+ This event does not exist.
+ It may have been canceled. +
+
+ Return to events homepage +
+
+} diff --git a/app/views/relay/show.scala.html b/app/views/relay/show.scala.html new file mode 100644 index 0000000000000..5dc6b25a2cb4d --- /dev/null +++ b/app/views/relay/show.scala.html @@ -0,0 +1,29 @@ +@(rel: lila.relay.Relay, socketVersion: Int, data: play.api.libs.json.JsObject, chat: Option[lila.chat.UserChat])(implicit ctx: Context) + +@underchat = { +
+ +
+} + +@moreJs = { +@jsAt(s"compiled/lichess.relay${isProd??(".min")}.js") +@embedJs { +lichess = lichess || {}; +lichess.relay = LichessRelay(document.getElementById('relay'), { +data: @Html(J.stringify(data)), +i18n: @jsI18n(), +socketVersion: @socketVersion +}); +} +} + +@relay.layout( +title = rel.name, +side = relay.side(rel).some, +chat = chat.map(c => base.chat(c, trans.chatRoom.str())), +underchat = underchat.some, +moreJs = moreJs, +chessground = false) { +
+} diff --git a/app/views/relay/side.scala.html b/app/views/relay/side.scala.html new file mode 100644 index 0000000000000..9cd8369587258 --- /dev/null +++ b/app/views/relay/side.scala.html @@ -0,0 +1,5 @@ +@(rel: lila.relay.Relay)(implicit ctx: Context) + + diff --git a/conf/routes b/conf/routes index fa52508e988e6..9f85465a1393f 100644 --- a/conf/routes +++ b/conf/routes @@ -316,6 +316,11 @@ POST /qa/:id/comment controllers.QaComment.question(id: Int) POST /qa/:id/:a/comment controllers.QaComment.answer(id: Int, a: Int) POST /qa/:id/:c/rm-comment controllers.QaComment.remove(id: Int, c: String) +# RELAY +GET /watch controllers.Relay.index +GET /watch/:id/:slug controllers.Relay.show(id: String, slug: String) +GET /watch/$id<\w{8}>/socket/v:apiVersion controllers.Relay.websocket(id: String, apiVersion: Int) + # API GET /api/user controllers.Api.users GET /api/user/:id controllers.Api.user(id: String) diff --git a/modules/relay/src/main/Cached.scala b/modules/relay/src/main/Cached.scala new file mode 100644 index 0000000000000..47a614c1d9264 --- /dev/null +++ b/modules/relay/src/main/Cached.scala @@ -0,0 +1,13 @@ +package lila.relay + +import scala.concurrent.duration._ + +private[relay] final class Cached(repo: RelayRepo) { + + private val nameCache = lila.memo.MixedCache[String, Option[String]]( + ((id: String) => repo byId id map2 { (relay: Relay) => relay.name }), + timeToLive = 6 hours, + default = _ => none) + + def name(id: String) = nameCache get id +} diff --git a/modules/relay/src/main/Env.scala b/modules/relay/src/main/Env.scala index af64adb83e952..fd4c5ef38d51f 100644 --- a/modules/relay/src/main/Env.scala +++ b/modules/relay/src/main/Env.scala @@ -1,15 +1,19 @@ package lila.relay import akka.actor._ +import akka.pattern.ask import com.typesafe.config.Config import lila.common.PimpedConfig._ +import makeTimeout.short final class Env( config: Config, db: lila.db.Env, system: ActorSystem, - roundMap: akka.actor.ActorRef, + flood: lila.security.Flood, + hub: lila.hub.Env, + lightUser: String => Option[lila.common.LightUser], scheduler: lila.common.Scheduler) { /* ACTOR ARCHITECTURE @@ -27,11 +31,18 @@ final class Env( * +- 1 telnet Actor */ - private val Enabled = config getBoolean "enabled" - private val UserId = config getString "user_id" - private val ImportMoveDelay = config duration "import.move_delay" - private val CollectionRelay = config getString "collection.relay" - private val TourneyActorMapName = config getString "actor.map.tourney.name" + private val settings = new { + val Enabled = config getBoolean "enabled" + val UserId = config getString "user_id" + val ImportMoveDelay = config duration "import.move_delay" + val CollectionRelay = config getString "collection.relay" + val TourneyActorMapName = config getString "actor.map.tourney.name" + val HistoryMessageTtl = config duration "history.message.ttl" + val UidTimeout = config duration "uid.timeout" + val SocketTimeout = config duration "socket.timeout" + val SocketName = config getString "socket.name" + } + import settings._ private val ficsConfig = FICS.Config( host = config getString "fics.host", @@ -46,12 +57,14 @@ final class Env( private val mainFics = system.actorOf(ficsProps, name = "fics") - private lazy val relayRepo = new RelayRepo(db(CollectionRelay)) + val repo = new RelayRepo(db(CollectionRelay)) + + lazy val api = new RelayApi(mainFics, repo, tourneyMap) - lazy val api = new RelayApi(mainFics, relayRepo, tourneyMap) + lazy val jsonView = new JsonView private val importer = new Importer( - roundMap, + hub.actor.roundMap, ImportMoveDelay, system.scheduler) @@ -59,15 +72,43 @@ final class Env( def mkActor(id: String) = new TourneyActor( id = id, ficsProps = ficsProps, - repo = relayRepo, + repo = repo, importer = importer) def receive = actorMapReceive }), name = TourneyActorMapName) + private val socketHub = system.actorOf( + Props(new lila.socket.SocketHubActor.Default[Socket] { + def mkActor(relayId: String) = new Socket( + relayId = relayId, + history = new lila.socket.History(ttl = HistoryMessageTtl), + getRelay = () => repo byId relayId, + jsonView = jsonView, + lightUser = lightUser, + uidTimeout = UidTimeout, + socketTimeout = SocketTimeout) + }), name = SocketName) + + lazy val socketHandler = new SocketHandler( + hub = hub, + socketHub = socketHub, + chat = hub.actor.chat, + flood = flood, + exists = repo.exists) + + lazy val cached = new Cached(repo) + + def version(relayId: String): Fu[Int] = + socketHub ? lila.hub.actorApi.map.Ask( + relayId, + lila.socket.actorApi.GetVersion) mapTo manifest[Int] + { import scala.concurrent.duration._ - api.refreshFromFics + scheduler.once(10 seconds) { + api.refreshFromFics + } scheduler.effect(5 minutes, "refresh FICS relays") { api.refreshFromFics } @@ -80,6 +121,8 @@ object Env { config = lila.common.PlayApp loadConfig "relay", db = lila.db.Env.current, system = lila.common.PlayApp.system, - roundMap = lila.round.Env.current.roundMap, + flood = lila.security.Env.current.flood, + hub = lila.hub.Env.current, + lightUser = lila.user.Env.current.lightUser, scheduler = lila.common.PlayApp.scheduler) } diff --git a/modules/relay/src/main/Importer.scala b/modules/relay/src/main/Importer.scala index d72242b631231..88844d468154b 100644 --- a/modules/relay/src/main/Importer.scala +++ b/modules/relay/src/main/Importer.scala @@ -3,7 +3,7 @@ package lila.relay import scala.concurrent.duration._ import scala.concurrent.Future -import akka.actor.ActorRef +import akka.actor.ActorSelection import akka.pattern.after import chess.format.UciMove import chess.variant.Standard @@ -13,7 +13,7 @@ import lila.hub.actorApi.map.Tell import lila.round.actorApi.round._ final class Importer( - roundMap: ActorRef, + roundMap: ActorSelection, delay: FiniteDuration, scheduler: akka.actor.Scheduler) { diff --git a/modules/relay/src/main/JsonView.scala b/modules/relay/src/main/JsonView.scala new file mode 100644 index 0000000000000..f6b4c4b73d16b --- /dev/null +++ b/modules/relay/src/main/JsonView.scala @@ -0,0 +1,25 @@ +package lila.relay + +import play.api.libs.json._ + +import lila.game.{ Game, GameRepo } + +final class JsonView { + + def apply(relay: Relay): Fu[JsObject] = + GameRepo.games(relay.gameIds) map { games => + Json.obj( + "id" -> relay.id, + "name" -> relay.name, + "status" -> relay.status.id, + "games" -> games.map(gameJson) + ) + } + + private def gameJson(g: Game) = Json.obj( + "id" -> g.id, + "status" -> g.status.id, + "fen" -> (chess.format.Forsyth exportBoard g.toChess.board), + "lastMove" -> ~g.castleLastMoveTime.lastMoveString, + "orient" -> g.firstPlayer.color.name) +} diff --git a/modules/relay/src/main/Relay.scala b/modules/relay/src/main/Relay.scala index 9a0854efc31db..8a1e74f1561b4 100644 --- a/modules/relay/src/main/Relay.scala +++ b/modules/relay/src/main/Relay.scala @@ -16,11 +16,19 @@ case class Relay( def gameIdByFicsId(ficsId: Int) = gameByFicsId(ficsId).map(_.id) + def gameIds = games.map(_.id) + def activeGames = games.filterNot(_.end) + + def slug = Relay.SlugR.replaceAllIn( + lila.common.String slugify name, + "-") } object Relay { + private val SlugR = """-{2,}""".r + def make(ficsId: Int, name: String, status: Status) = Relay( id = Random nextStringUppercase 8, ficsId = ficsId, diff --git a/modules/relay/src/main/RelayRepo.scala b/modules/relay/src/main/RelayRepo.scala index 38e847eb59278..ea5cc2614b480 100644 --- a/modules/relay/src/main/RelayRepo.scala +++ b/modules/relay/src/main/RelayRepo.scala @@ -8,11 +8,12 @@ import BSONHandlers._ import lila.db.BSON._ import lila.db.Implicits._ -private final class RelayRepo(coll: Coll) { +final class RelayRepo(coll: Coll) { private def selectId(id: String) = BSONDocument("_id" -> id) private def selectName(name: String) = BSONDocument("name" -> name) private val selectStarted = BSONDocument("status" -> Relay.Status.Started.id) + private val sortRecent = BSONDocument("date" -> -1) def byId(id: String): Fu[Option[Relay]] = coll.find(selectId(id)).one[Relay] @@ -20,6 +21,12 @@ private final class RelayRepo(coll: Coll) { def started: Fu[List[Relay]] = coll.find(selectStarted).cursor[Relay].collect[List]() + def recent(nb: Int): Fu[List[Relay]] = + coll.find(BSONDocument()).sort(sortRecent).cursor[Relay].collect[List](nb) + + def exists(id: String): Fu[Boolean] = + coll.db command Count(coll.name, selectId(id).some) map (0 !=) + def upsert(ficsId: Int, name: String, status: Relay.Status) = byName(name) flatMap { case None => coll insert Relay.make(ficsId, name, status) diff --git a/modules/relay/src/main/Socket.scala b/modules/relay/src/main/Socket.scala new file mode 100644 index 0000000000000..d6d41896fdcb1 --- /dev/null +++ b/modules/relay/src/main/Socket.scala @@ -0,0 +1,90 @@ +package lila.relay + +import akka.actor._ +import play.api.libs.iteratee._ +import play.api.libs.json._ +import scala.concurrent.duration._ + +import actorApi._ +import lila.hub.TimeBomb +import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.{ SocketActor, History, Historical } +import lila.common.LightUser + +private[relay] final class Socket( + relayId: String, + val history: History[Messadata], + getRelay: () => Fu[Option[Relay]], + jsonView: JsonView, + lightUser: String => Option[LightUser], + uidTimeout: Duration, + socketTimeout: Duration) extends SocketActor[Member](uidTimeout) with Historical[Member, Messadata] { + + private val timeBomb = new TimeBomb(socketTimeout) + + private var delayedCrowdNotification = false + + def receiveSpecific = { + + case Reload => withRelay { relay => + jsonView(relay) foreach { obj => + notifyVersion("reload", obj, Messadata()) + } + } + + case PingVersion(uid, v) => { + ping(uid) + timeBomb.delay + withMember(uid) { m => + history.since(v).fold(resync(m))(_ foreach sendMessage(m)) + } + } + + case Broom => { + broom + if (timeBomb.boom) self ! PoisonPill + } + + case lila.chat.actorApi.ChatLine(_, line) => line match { + case line: lila.chat.UserLine => + notifyVersion("message", lila.chat.Line toJson line, Messadata(line.troll)) + case _ => + } + + case GetVersion => sender ! history.version + + case Join(uid, user, version) => + val (enumerator, channel) = Concurrent.broadcast[JsValue] + val member = Member(channel, user) + addMember(uid, member) + notifyCrowd + sender ! Connected(enumerator, member) + + case Quit(uid) => + quit(uid) + notifyCrowd + + case NotifyCrowd => + delayedCrowdNotification = false + val (anons, users) = members.values.map(_.userId flatMap lightUser).foldLeft(0 -> List[LightUser]()) { + case ((anons, users), Some(user)) => anons -> (user :: users) + case ((anons, users), None) => (anons + 1) -> users + } + notifyVersion("crowd", showSpectators(users, anons), Messadata()) + } + + def withRelay(f: Relay => Unit): Unit = getRelay() foreach { + case None => sys error s"No such relay $relayId" + case Some(relay) => f(relay) + } + + def notifyCrowd { + if (!delayedCrowdNotification) { + delayedCrowdNotification = true + context.system.scheduler.scheduleOnce(500 millis, self, NotifyCrowd) + } + } + + protected def shouldSkipMessageFor(message: Message, member: Member) = + message.metadata.trollish && !member.troll +} diff --git a/modules/relay/src/main/SocketHandler.scala b/modules/relay/src/main/SocketHandler.scala new file mode 100644 index 0000000000000..27850ebac4145 --- /dev/null +++ b/modules/relay/src/main/SocketHandler.scala @@ -0,0 +1,58 @@ +package lila.relay + +import scala.concurrent.duration._ + +import akka.actor._ +import akka.pattern.ask + +import actorApi._ +import akka.actor.ActorSelection +import lila.common.PimpedJson._ +import lila.hub.actorApi.map._ +import lila.security.Flood +import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.Handler +import lila.user.User +import makeTimeout.short + +private[relay] final class SocketHandler( + hub: lila.hub.Env, + socketHub: ActorRef, + chat: ActorSelection, + flood: Flood, + exists: String => Fu[Boolean]) { + + def join( + simId: String, + version: Int, + uid: String, + user: Option[User]): Fu[Option[JsSocketHandler]] = + exists(simId) flatMap { + _ ?? { + for { + socket ← socketHub ? Get(simId) mapTo manifest[ActorRef] + join = Join(uid = uid, user = user, version = version) + handler ← Handler(hub, socket, uid, join, user map (_.id)) { + case Connected(enum, member) => + (controller(socket, simId, uid, member), enum, member) + } + } yield handler.some + } + } + + private def controller( + socket: ActorRef, + simId: String, + uid: String, + member: Member): Handler.Controller = { + case ("p", o) => o int "v" foreach { v => socket ! PingVersion(uid, v) } + case ("startWatching", o) => o str "d" foreach { ids => + hub.actor.moveBroadcast ! StartWatching(uid, member, ids.split(' ').toSet) + } + case ("talk", o) => o str "d" foreach { text => + member.userId foreach { userId => + chat ! lila.chat.actorApi.UserTalk(simId, userId, text, socket) + } + } + } +} diff --git a/modules/relay/src/main/actorApi.scala b/modules/relay/src/main/actorApi.scala new file mode 100644 index 0000000000000..49a0d9373bf43 --- /dev/null +++ b/modules/relay/src/main/actorApi.scala @@ -0,0 +1,28 @@ +package lila.relay +package actorApi + +import lila.socket.SocketMember + +private[relay] case class Member( + channel: JsChannel, + userId: Option[String], + troll: Boolean) extends SocketMember + +private[relay] object Member { + def apply(channel: JsChannel, user: Option[lila.user.User]): Member = Member( + channel = channel, + userId = user map (_.id), + troll = user.??(_.troll)) +} + +private[relay] case class Messadata(trollish: Boolean = false) + +private[relay] case class Join( + uid: String, + user: Option[lila.user.User], + version: Int) +private[relay] case class Talk(tourId: String, u: String, t: String, troll: Boolean) +private[relay] case class Connected(enumerator: JsEnumerator, member: Member) +private[relay] case object Reload + +private[relay] case object NotifyCrowd diff --git a/modules/relay/src/main/package.scala b/modules/relay/src/main/package.scala index d3d16d0caef83..727ab08c105e7 100644 --- a/modules/relay/src/main/package.scala +++ b/modules/relay/src/main/package.scala @@ -1,3 +1,5 @@ package lila -package object relay extends PackageObject with WithPlay +import lila.socket.WithSocket + +package object relay extends PackageObject with WithPlay with WithSocket diff --git a/modules/simul/src/main/Env.scala b/modules/simul/src/main/Env.scala index 33f83a744f2a3..3fc83d67ce9c6 100644 --- a/modules/simul/src/main/Env.scala +++ b/modules/simul/src/main/Env.scala @@ -20,7 +20,6 @@ final class Env( mongoCache: lila.memo.MongoCache.Builder, flood: lila.security.Flood, hub: lila.hub.Env, - roundMap: ActorRef, lightUser: String => Option[lila.common.LightUser], onGameStart: String => Unit, isOnline: String => Boolean) { @@ -129,7 +128,6 @@ object Env { mongoCache = lila.memo.Env.current.mongoCache, flood = lila.security.Env.current.flood, hub = lila.hub.Env.current, - roundMap = lila.round.Env.current.roundMap, lightUser = lila.user.Env.current.lightUser, onGameStart = lila.game.Env.current.onStart, isOnline = lila.user.Env.current.isOnline) diff --git a/modules/simul/src/main/Socket.scala b/modules/simul/src/main/Socket.scala index d4ec6f2cd40a7..965f25ea4dde4 100644 --- a/modules/simul/src/main/Socket.scala +++ b/modules/simul/src/main/Socket.scala @@ -9,7 +9,6 @@ import actorApi._ import lila.common.LightUser import lila.hub.actorApi.round.MoveEvent import lila.hub.TimeBomb -import lila.memo.ExpireSetMemo import lila.socket.actorApi.{ Connected => _, _ } import lila.socket.{ SocketActor, History, Historical } diff --git a/project/Build.scala b/project/Build.scala index 2dd4693f3d788..7408ef5191830 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -182,7 +182,7 @@ object ApplicationBuild extends Build { libraryDependencies ++= provided(play.api, RM, PRM) ) - lazy val relay = project("relay", Seq(common, chess, game, round)).settings( + lazy val relay = project("relay", Seq(common, chess, game, round, user)).settings( libraryDependencies ++= provided(play.api, RM, PRM) ) diff --git a/public/font42/fonts/lichess.woff b/public/font42/fonts/lichess.woff deleted file mode 100644 index 757b157fcccda23f3279672c897a2a11c23b64f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16700 zcmZU4b8IF~xOHvYwrv~l*0$ZPv2C}uZQFQjY~8JG+qT=gzc2Z6|G1N!OwO4nXOv7b z$@6$9N=YfIsVRbhhzfwfgZyi{^&p`C_5ZhtOG%M{fPlh)fCyBBfT-(#U}lg=NvZt+ z0TFEa7mfE11kSDW%+eB)QvbA>f1L0iNI;T6AQV-Z+5Txa|G3;g7%8_aNSfLkJA!}+ z3H~_PX5jIlw(N~P|M`hX{Nqsn4;D%wKaK6p|7n1KeCOXhp>GHukEVDj;gA&t&Z{b;}75CT-_+LC~RpgMjeSYUGAU1i#z#&C&?p)BWXv% zZet&-#OdEJCYKA6j%Uz23qzCgWR@GZ3Ez;8dpis1g;Qxq@)H>eyYo}g5(>gDvrEW% zxV-xl{9l*k2{@CX5(-faaoL#KsO=60lreSFoZVOo25T=m%}SyBl6{vQ zBaaFAWZY6eBYk+CT@L6II1_Zq4P$&5opldlM>FIfM>3M#e^d zCG>=66Tcz|-8Lq;kYP(Q#p>a-Q)~zmU!_TDNHRtFklqcA9463{X-d^bW~0689i)y( zlmAFSmKH)YZWttv@*%qm7#SvyA_q|5MMy45{fyf{=OqVn-t~VgLDe zYJ0S$1h&*~iTr7`T5sd3!akMv1?uCOsh2|bht}(g-cJwMLLX6a2)+-calUn@(WSmE zy+Z9DrsMo+)M?)@rg^U`h{T=Wrr#g8_(HXLx6O)!!mQ7uhj{|WNqO%FU+fP2_uqo& z4M*@<9^Aw>w7!*}7x&X&vt`qK#~&`$E)Ig%n>{O@zW0_>ZxdD68&0Crkbcjr{6g;^ zlH7)_7kkG{)*TY<2iL-7dL0LIe)`S(z%1tPGI}9r$z{y2I625SzmXh!`%$qZw+#ME5Jyt)ozvk`gUu$ThdBmFo0@mW$!N^$# zkE{21V3oP-LSFibG(OJxL2g4c3TPr;O3o$C}N9**W^a8zuc{;@`3 z{EYi2KA7)DpW|1i2g^1>Qy=? zm^lqSWk|BQVvD)-Q*ot;UPQ$m_#Mhb@7G-)E25P);e36)TiWZ^hujnE3iE{d5paGq zA=j&G0WoJr`$S>z-OHUyx3MOJv7d^BcSKZ!npQB&a_>)01tG6TFD@`NoPA#3w8JK% z03fJ}EoF>RDu@1fo6vB&+kU2J-}8y{REb?}X6)dr=tCtmFVte~xIYtg_1lqHV8s4n zJK5$s!efyC?y^3n7pcyr+_v1d(8Hr=g0?HVlWeRokdY)Jkz_m)00$|BW-hC)4x+9c zURcgnG?z!GY*GnUS&9~J4Wv4cEI3)wkciWG0A8I=YvkeixjOu)sZjUC%XK9t_Of%F zUU`{#0sKO~7{Y2t)-}VP2u}(b-IKA%_sie@AqMu=X4WCRxt(@N=ZeWUNGMX@*wiFC zCsjKFqs1mlRuT~_N~Jxf2H)K8&jS*q74w2310GU?J?+8f?Z9SsB>Rg&kdrY5AzSQWw-zbXMjZ%K1c8 zjev_hNuHbTAL`@X62sSu+Lw8Wybr3DPl;@Xo%N>(%wiJsVcAX5WkRSI7QMTFQ0EVK%BHN0oO&bQsf-?K0y|(?V|C+bA=FeKL#G z%^OatH;R|@hUdk^_Q&hjXwXlgL?;@JeIvrV`LgIvs;Me~CC*G+I_<0SkfAHUDO!fT z7&i&AoGer>W(JYRz8D(z_1E@GY|CuRcvOJ%<#*16=@#I-i%3`CD?b0gu<)Pgm9U0r zor9P_`7W$$WIld=;4pPluXK)5A)=(I!@#1eyrtP9bG^R>Ft3)x9-j-Yl!7a<%KaQ| zkW(VB`NGB?%c^W0R$nfhnH22UEJUlRRuIKS56m;Q$ z!q2H`cHi^6cF>Z+o*z|XTTI%fU3%hq-*Iy0@ai?~OCX%>-dfB z1dSJIkH|}I6bl^43}Y_b(Sc|ozR@@jT&)~oGMZ?#y;k3@8h+#Up@ zlY(8$FArZqOk~1Gs{|#0N>%v5ghWiB{sR99Ay)uFcI$U#*aueUsP-n20KGs`$YfPW zd zB4e8YS9w_W)bc|Qh%d-8uCVH>w@Q@-Ma(S~Mo3CejTP@wa}%6*)vvPCh^|y-9rIds zWUPuMw8oCm=U}6`?pwO zqD0b{iDNTGCPucEBq!n-gRR3DbFpew?e zL+8E)vQk;JQmn>4OBC#16toGG6}A4R^UlYAYpUzSZWkQ0b(COFUZa}h^4t3z34<8= zj)&mpDRv?T@MCw_;2J~#DGnQXXR&>%LZi#0u@yCsXrghZKA!GMWJ&Tc6nA0f@A5U^0D)l>i<(eS+_eao;$WMeNvUg(D|K8yAVU-JHrSRa`l)rM&IY0x_k`Ab%IVmk$ZxY^A(ntJ zjNpq>!du-IaK4-?8xKb1X@D8^3vF;Xby}6&xa3Fdj>(g%eClf+-`G##`d=QdHfLen z>|dus!+~P6(6CHMOeXcXsKGKYF%8e+h$0_RpHa9?&zYe?YQq6yD%-|EVoJeA5j_we zg@#53-_ntt`dgIT-|@QyNDUrDJcC;YTZmtYMx(oyIK!+;hDH&2Pv0r}Cqfq@7j+bu z7xm!kYsB=+gC`D@44Cc?;@lzW;bgt7R@Wi@GyZ4trP?Pu{EeF#R}x^tz#M^QOusdX zgHL-la8&_=$pv)IuI1BUBxf^TuGx>@U(i~WYctiU3ut&68?*JP5^-07^ebHh+K@I{ z_g+jK)jk8&dj|2qw&P@mv(fBaPT3`f^z0$+g{Hd#<4I8>L|f1l-031?NiUJX!f?x( z`lHiYEJx%=PO&jF#L3Beme}7`62rzTwd%H5#Y9v&W^ zj`(lu|7?Vmd6(T4d;n60rKE?)lIo$ockFw@2oV(!$(W%M)}=pHnLkUu3w9meqq+$h z?u3)g42Z;%D6}ABAn2u*U&e7EFp{4@&g^5WVN;>$y)pvn!h3^)*K|p~fk1wqu!29* z_~46lU_yf)1#q0oftkK1+?x}@p2&J4Qz~ReZtuq*SFH$Vst`&%Fi~IAU*A3qa*eTP zddoLApU8%%#KyA+=@_Mj$|8;DlCwOtqrg;r4j+elD|tMEI+79MN=D$ zkFnl~?;ReGy|!SlsO17-g|Ig8E(rBcg++Y$Utqi?Ja_Be2Kj)yA)HHi@3Dw`cuteM z{ITJK_jRV0Wqj)`ZtgA3ZVHzWbgMHEmXfGd!8K`#cuqEJs;bNrM>pQ4cRRTP>h&Dt{y-FuVN?tXz5bGQdQcA;tQkT^M97u|&)TJ7W za8GiC5j**;M(US9eRuiXb5au~UGpjvBpkEe@>B3Nf39*;HX&zy*S@+Y799q}+eF22 zPoJiVx=i!uOSYC40$t3_R{5>vk479h>4w)z?QPpmI-P@86L~tWrDutsSOy{3hJ$gM zSFcYsNUL7(1CP&6v}tDU8$znadbq4NZH7ampmGf1*=Qs#ID%xOfh#9XKrKr}ExSUd zkS90?Bte5tPTP&l^7ohvC|qjDMCG+xL+-zE&j%c`i2y>|aip?cOl`wgS`x8MqC?*QD!VU}cA;l7onybt9@PEPCGG>zMaumhZt&f)ld9zGU;<}>+?7qm~dkb&`)lxqN0 z@EZo1rkdJU9_0!Ee#4wicy5exGp?3JChiihc<-b)fGD~G&)kT}=dbyri;t@VWCqJf zZ)T7CoskDZA-lx<6!*7-Y+xOvp{)RCoZ{h41!9(@Qz^F}>$`98qi?y?*YwW0?HAos zN{ba+k}MbW`AsL+23uz-W6Relbe_Z0vSgm$-RQBdMcklJkPX`jU;h>xK4I-*=xNEW z4%OO##K zkk{Q5crXF+6gGo0&DvYx*?JtV*S#!$Q)Q{9UxEZ0ku>qlz6nTl0^-o> zo=`FJ0|7_^oFoT7x(@$X60Kt(v0<~jA&ir~#53pOxbJ2R6X|-=sjJ>2H}pLh!UznB zks6}7#FO?tn^?6wABxc$qE@XWZbvQUo1~1DB&v&QT!cfZ%TZ2w`G>jcK=!+Sgm1?yhx*JCZkZaof)D+)a5UCZB~32?tb{4AR8lZP-k z`;9a$?6iEMLoy%GTdVcmi8=j!G9dl3fCC9kQlYFqg(}GYNzl53j5ndZT2f4a6KJBKZP^p%$h?T?Lvp9sXecj{Dpu{vEfylh|rf;FgAvK}Y1gU^v##qbXcK z7kFw#fTjn>%>?T7K^WAQ)jM9n$4xY4l8rOrHn^_V=A@@nDLJVFPMwSPJFa;Xtw;zf zL7I(3$q-250#^o8S&SwJgCmR1p&*TE15;`g&Q5lVqa%27yJ5{-_GuK4_V8;*gI>@_+FOYxpoCI*EF3_(97C= z9Y#2Tw)qdxFSefN*k5qp7u!_C4L|W#fZle_3#=}ebDLx0gBTgt7r4@p3W3a!`xoCF z=i8@WzlSli_R8l9-XT3JsK1&!kBQ#yzSdMjIq$3-b#C1J2vZ%Gz6JSvkODnXhD8aa zQ=IJxU!3y3-dNToaVez=b8Xd!sxx8-zJ@a#ZDhbzO9;RNk{}MmY%oPibD`zb9N?X! zvkg;TPo)wc(7m1jl*vyC{z>fjHUY_qH~Gjgy50u?tn@!FnsJ&Kx6>GB@xly~Ub0 zvkd7*L5lp`&_W8j%JhjtNNwbR?Nb|A{l;@Hyd!M1f+8S-3atS%nfa3heRv*V3Ks)$ zZi+azKq7x2($trRRlof)wgR?p6TWf;iJO8=VfP!rS^gag!J)W@&*p|S@~{TJ%jveO zv$6u06VGofjr!N*og76%Hu-3MbqADz){WW-`xSWx8Xx0sEc{i*lktILr}@#ZMQK>$ zYn9IUEHnu$FW?uRt8*MzsA%1;s<@^4KHj1QpBurP?Y)+TJZJfHiUMWlGp)bqpYdkeWNLUWIkJIHyD+S9+)QE#yRNwC-4Z_nW8P z5UW1^sCEY8Jd(!|ykoUXm?yqt8fxk%x&8c-3^Pks!A?b#UvKg-gDBde3DdGQ2St9N5d(JE~G)gA?7F}?oAS3?eu!J_hcQD}sT zx`8}rvXLNou;;`~H8n`joSJS3PIjQV-Y4TGvDm zayxn~+>fHX1#%H;`v?L=WPhJr1;GqgIypE?r34K13AP+%#w>L?w};Ti{9Ps(Kc%q> zyunz`&?LDs9h%>TJXLvn)j}t*tLky<-065WJJrHT3BnhK?)?K!u)yvlNZB`4EZQFE z0vx|FGFr0+EG>#hshJN2Px8G25F=t}Xj9Too|aywkDvyqpck->mt`KY0<-lNRj@4T z@6SEHk9}dbhqQrpFbs%*4Nvm>$sLmO;h-Q<0j})_F8=i;tSmt^U&j0Sdtzh4$=NY0 zNB#UWm`TiwBkCk^{bmDtP=3&wj&v%xvd{$#`07-1-6tOTIi+cY3eEt9X1s9Lh(UTs z@c1mz6r3G(J5aLGzIl?Yk*z+b$|;d{mF6)Gb!PC}USNiE^InQ8ewcryOVOEuNB=q} z7q2S)da4HdXp(m7yf>$E!F?H;YgEoCK+&(PesW!OqN|JrvxK1|o6)NLTuv!z+$09t z5z!~vKpADjszp+7omFsi2^6qgYt4jMpk!Z>x!LpExlvpLDy^X_0bF5nmR|ZU^6?q-qn!woj z0Ks?$8%BUvYz5MQa10DvkPHDRuNb5ne3!utO%k+?Tz+P=r-_qrDDkZOeplgu@e^92 zUgA7NRsx_;Vykn ztCuPsSN}9;<F6cZ$4GRQ#cXe-Sntov5n1WsI9P^p2{eCZ zLE(z!=&?R)Sh%M=#dh88#Pz69i5i9o3IU*_N&$-uG0%=ei9Ffd@zVu=Af_k;Q)-H= zPLwO~`3f;39vKJ@fb=qRQw!vRKsmx^I=2;k=)Z#Ks9bLCgzwHMrl{&BL05=nIS3LA zXk0D+kwC;@VYkQq8mJ6mO;g0pQ0=v+-Iz)e)8TnMf#^TM6TA?*#n*64Gj>CN%hX@+ zH224|OLYQ!N#~b(SfgNB@qJt!wT#ANVQIh%84g3%Tdn;o#g_@d)st<1xwaz47$M_h zMe02vVO^$}z$y5?8Bjowk*Q0nB?R+T_AQ=FX`;GuP?5?y9y+U*-N&_*SKh{)DqyaX zHaC?s`~VqPLOf`rnnftvOTL5y^NRTpn8GU!@i=ic>HOSI5U``tmnQv;7asK8{ysqd zaQW<$cJ3>bZh1J`Kb+php`0c)36sBunNlBVZvE+o^W9`vA9ww9{V@GLRJ#+{VkGHk zph>-opFNF)m`|pMey&*do)x5C4hzT}~T2A%m$rFj6o$oB>M@*XP9&!2vEa|7HPYXOuti z7*|_O67im0(kCB~14qPDdW!Z<<;UEJ;PjMXr%0^IfF&U}4UdJ#pKMePM)zZyT=*51 zot^zB@}uS*%<))iOxG}Im;x$AsCSM+yZqIZ`|#-5B7jUeyy)(VZsk!c*_D81Wr?#0* zV!=`Bl&DvsE`yUst^&hcKD6_pI>1A-Zb4YW$tGNdR!Xv5PEdi5F00_n%LxMptAveL zcFQxUVp)D)tG1%wGZ3n}S}Yi7(Q-MGu)0bvy=JjoZEdSNfAQ@b|A&%JFv-VfeaqgP z8)@4%QFHBQg!y8%m?hV}i9v4;vm+!D_s8A8I(V6Md#PTJ}GoTHEo6`t_g`Jr;C~ z(F1;DEFo>^uxI%*c&`hO`FceNry5ar8kuUQ^Hk~Nt`fo0wDNU*SZWdq`7BPSb4st|{Q1Mr?=4kU_V;gTskAp=7I(4Mw37fI zw8XJ*B+_*=iNs@%coC!+lDA5eKib`|Vw*n97#Rb%zHgx}6e5uZ3cQsf4%d@yd4<=@ z+XIZ3ul55Yn(wetx0q5gXd)BZ$ilarH=1w;Xvug)DEXirjgrQM@=q!sW~}P<>7}E7 z7I+TSZxs7Vpx(k7TRv29~1cZsY}499Ziks)Owo{&)D@W6?rZp1V& zbSzcGB5MV>0+@D`3?!)_B^G9L}|R0Ps+~3n+P<^AT~n5)mPylVJYEzi{8q z*_spT?=bzdV0T9y4=;B!#@)lu?U~|dC;hwsMc5{qE4d!x&=UM1t@k`%`nc$2IaV0R#Zj5 z;9M)lT2S4au?bTus3KdL{|x-Nf1Maj!lkB)t@BR%F{qTS)N!w{pq=s#iq`HBw$AP0$gi$TQQUBcJA+73%sE$Wg9DmPXlrY7?sf`d3@ z;=#6OmLn^2?&<|CNRsOlI{OV#I>P@m>dX(S5HIDNB{T8}_r4ig8=|BaVsslQzN!Nh zPCC7=GY<`|YA&$w+rD#2g_wk1bxAcsJeoEMvJS8KU7?mI=TG!(T_NfgEHsw)ci4(N zD2O~9;a1vCf)HT+o1z@j+ro?!0V{C@p-+KjtIrya>WY|gZF zGdYd%u=3T0C`PRz@!8pZNluhm(d%XE;fd(qXWAi_b-M5O*W|>*i-i3kt`k&qq(2~= z*{SW`yn;fbIa9sy418bJ%*6Sq_kukX7tH)DQd7ylW!h5f8@#ocdGkSr4R({fz2adb zy-U-}-En&UsPz1c3c6~C9=YbtH4%{dN7-I%eYxSiZzAZlCT`iV`zm!UlGq-RY5n@m zQt%fEE@AE9GdADF4`lppO{8w)v+?O`O z1I5H-&H9>$-O*1EsXUyI3fU`(HGQYyKMwtkEC$t%@1Obb=&XxyhI6*)Zo8hc2uM zyNRy?yc3^WTMPXOJK|U=cPJy?IB19Skr14}=V^><)`?{{Vp_Kh9X4Zf{Jwu9xE+Kr z;s02oOxnY1a;HIEKSm4N$+`(M{5(U}+9*SFSsy*1xIg6n7|)eNS-D4a7g!2Ulo;pZ z8}lhaPf6A*FO{i*n`UVjYaqdv;j}W}9bMSx_ei>2&+zKn0JT2|U3ci&{kv}9^SF53 z{jxmSSWhpUO48}UzFx+KC9H?^%LTU>)$%b7pzW8-EN$YXkV*?{m8Y z%+3GO5CjAPYpv`*`zFLf?Jidu%+F)>-=8kmn(Nx3)GG#2{T{DZop81d*Tvm?2|ZYd zuH~5aLN!0PfnST7ETtw@Tx5#6MWkYy-`ThIr;${!H%fDmk%FR1lna-hE~xwi;xzc5 zWbiXW{_zH7vj&N1K30q~o;I9agt!PgW60e!I1(Z@G-H8X?C|~-ynsu?-;}>A(S$e= z03C0LZ)(_iUR!&@lg};B$M0g~hKv*3=ttvWuV2H>+FX92-|$%1MXYP=6}K-+&yHdL z9x6XG4|jKWi#20*3{D~nv6x7hoEcTX6Lr!M(MjOXy0R_dYyx_PLi?8!;hMOwHN!jP zzC;HQpQAkgj3E7#h}($UF@%Y}t&Q5N+pAl`6b-xmZ|lZ))UmXtOoQb!9ZcB7B5$tD z6qR<3%Cf=YTn1|S?6P)=dBNXW_)EhVr(=L$KE*$=2@i%+|R3Kd4k$R zI5z^8y#!q6N=bSZ*|Z*M`-j0PDlRs_R&VlCFP?dNW7E-86_qiaMI4UM3sDc2A!?V1 zukJ5Eq+*g-Ym@4Ft^G28bk3W?2+6dDL{!xcX6+cVbC8KTUD5*IHNMNaDzbANjqU&s z^lAdb4-chL$Aze@9o1Dkt|T-x4yui*1zd zCpIg$tPJ3}3H2^SBPqecR2Zd!?Z}pnAAN!?b<&117{R{#m76rkVy0^C86(fzYUCoZ zXLewAU>u+#9C3}1pz zfWI%_rjJ$fA}k}GFRb}5em8FO2_MtAqZ@`8H02D}NbXOnYUDMeVWyNZb2}G#hb9Re zSiN#b%09}{sEc({XnQGWM!h{T<|4Uv7*Z`PNa%HR>tb#bb=E2ro(Hu%BpY=}a}-OA zNNw4qfp8SOEz+z)#+i67c~M(N`a-~*WGItwA${(Nu#CpidD3ZcWWDOB2^roDv*w|- zBz6UQc^h~=1IN(PbL*J3(jcQ3iuz9r{&Cddt zRFh_&KyLEY+@&h0n9Se5Zok9eekJ}Y6m#Y3X~;kScF##AE!i4*d;bWy5A^$*P(6ks zw)k+EG#nt(#W3?jsqS1c#P+cY)aGJdTJa)uCkOrNXU=H&-5lf?{!&)wL(L<|^VxNO za?DTvIiH2x*SFTqdNE#)_l(V`SG~y_ceEJ`2M)v^R%|zIsrxb zl%g5C;cSNZQ~GBFsi2*h`vEZ`%ECM>DuvMbN2!+Mjr~&~7oOT2Rwqe#BX-xO6s_`v zaAahNN^wgob9gr)f%$eW;tlp)bm>Ub5pzZ^9?rHe= zTlVsM|6mw=Zdbzj#Z=<$FbdnIZeE3%A}r+K+xoKNd55OcVL7%&*JI`zs>AyB`e+4g ztr4&Xofhq9e1D|JMEM}X?b>$9InHK3kTLb+hFk=yz^>zJLG8QB)-NIY`LFk0nt|6B zp=~}iqDJi;XMv#LqrOT8(Hdt*m51hc?S`w98cu2$!=f08=tX(J%C>AuCsao$f7r0} z@bKST2Uj1@P1EyX?jQhStRw*M6jZTaK4;r|$6N6a$0J2_!D`h(r8a+oJ3MJLnKK5| zq0a2#MpuSdTZ2&d&v{ShE{`rt0UHHqTx7* zFPxzyGqTuabo9s3*;}W*yvyxz4Y?4XSf*6kWIFRE2*y6nzMn06g_bA(UQZP|bsexZ z=yoWzq4!AM;M&(z^ZSa|lImcoOl~}1fx8eRV)6Y98V-2!^uPiu0=8H+^?q%tKd9yx zwzJ0NI7rPK;8QrSx;_2roF#JYs<~47ko-!6k~^4%LlQqE?CC{MIAJtnLcd{<)p=bh zWFi44(kEeBZPi>0fph6k#otzQ0ZCB{#?%GYrSf(qAt~iHZ77inU-MZf!7fD4g%lzu zYJUl^O$az6Fifqkqf|hj3rDKp4-0?*vx0z&idZEqKybO-!YR|?NuV6P>X-(6u||0W z_zFzWZsY_|*L@SHY|SC2UU5f>*44H_WYL>nM3S91)Y*eEHEK)h)@w{of!jQb7egu5 z=VmM-VztBydfFX3#9*}+Vr?+7*Wp;81jEjvP7dEZYv3pV&*@4OqRnuR?%Vq_w8eTC z}qGV{-{17(W_r9qcNi@ zmUmKChfXax?bJvGpq4lsh4Cz#lCQAJvqSp9ng8V1Um3IaBs2qnXa@r_s`UqwKa*54 zg={jQ>KB_;q@h6x@;hJ=SDavEL+2qp7!r z{T=xQ*<_)8J#|}xHO=n6JxLS|k=JyTQ8ep{0@jzEnopeHzPF~{C(AHGKGhA-Umh~V458-CJ@$g znqhL@4tcynwE9M0s^l>sq?T^;F9dN{wmo5HRC5c^<+G+C#-+MnJZ~!Lw*7=zceSAX ztK9F3v0Bd-vM6pgiYTt8Vm%MWwKr7xxT1$nb~>IK_hUCg1mV_Z-}S+#OBzyKN= z%gOuT$}Vs47}%9eTRbRq5>}nz{)_xPBuGp-3W7}quA)1A(g_Tek_Jrk&_?nN4di1$ zna*OLM7=}~E8UK>QoYh(67qm5a8!n)V+g~pLq+!?84dxB8%hM)h$`<$)md3qRP>zt z{f{1{VDGXqMrd|I04wl3|8&*#C|@W)MnsmKbAT|bs@pDpkMJUBB#tsglQu6uazKsl zgXq=z5%N)a0U@@eEnA`db%iD=jN+)u(8Sx#Ct6SBj~dO>69wOkP=!BZ#`4`tYF7@k z6K2H1AZxj{>6PanE|rQ-RTd;9IbtvgMGtl4p-{dHG>g0_jhQ345U;}AI(ev-O4uZw z&HGSBO_u^dyh*@bWXgw(F$s4(0akQj#w@#yN(?=}WTHAx%qSkAg#vk2lAU%Igft_4 zi&;1ZO%-IxACzlFvqoMaMrS!ZYYN(#RvoSEsx~Fj+2-m*ZUF%e`V>8V7tTFH;KuCV z{zHpU721n4qP2(z6{Wj{!kfVGHBGffk;r6tYi;)H13E}W$00Ofe|HOvcldxP<8_Oa zzp>Ou!b7>86fX;VA#vtkbP|2^X)ni&!*PwbGjG8^X|=HtGP-^Gdnlo519~@E?dvJ`_U&7u_k)KA4i*2_8>pKr5vtiZOV-|M zIpbMnFA{S&a54pr?`XV=!s@0-FR^jEJJRXU&;tbLxQVk@C5&VmF^$SY4 z4_p^4Z1cBo=h4PmMk5EWj|e&CspdehM*wt?H69oKB_iz9rAczAII_vYQU1^^Go02^ zes&I?-J42uZ0xco@wDmZ+kT1OrCnHj`gm-}-)hVZYu?p;3o_Q&q0 zI`78rbw0Kkx*y)ong2XApMU4h)%Ns!HOuRM& zHS_yvjr%l1s^j@Z`}sqbZ{l+@DrF~o+AkNvz!BT+WWa8gWUcFZS5 z6be0(V)#0iqUn1G?Z7dh`WWQG&g0xCQk8F#_WWeI=c6f!E1s<{ngYrTmYC!rsk=Z6 zpg)Mz)=zFvPRBdV8oNlP&~@hwvl_`_EZUP9;8+L@9qGIZ*Dh+vf$^sUR%uZ=+2ea> z;n`G3OU}eF7^-I2OyUF25EY|Datu<9%ohSFXu1MyCOK^|!%q12wK&!ES$NJ=rXxzN z>A17;0qW-B&8NG}9l=Nr)H;>BcPIYUVGeBtNwWEv8y_HxKr6Xl;&bRy zc(BW-ebU;TdI?2x3bSbLw(Kbl9YzJlwN|9<4B_R^2y?xel7e+IzyuAn3bNa2&!|Pa z7(US|wkw+#C|rdjNsJQ!UmmTBp_Hda1x}DxiIH5b#i`kvU#=v4RtQINR0cy%-x3_w z^kGQ#L{Z4zfDMUEkQVu1*5ADHF4?lnWS~(Pb217XWB{3y>`)8Bw%c6Eqk1D%j7EZ5 zBp3FE@u&D$Fx!uQt2!tO$Wf*K)(|berwBW6H;WyRW>~EiX^wtbL~LdoAT55T{aK9U ziqc#|E3BAz35-O9bMJWfycu_Nrer}Yph#y?>OoelGnWaxtf<`Rp0ZqIh&H9(QwGi& zB#9|gq9A}SR3?pS22xfBRoi?PtE(`3IZ|WksES1-|45BK)-r`dkxOuxRyS9DNq%kT zV4|0(qfJO~MbgGbo$fx@2phG$fYw7&MQeJkV_(Uz{NM)4Edve>@fd0OwgS;3r4m(E zli`gC3-1S9tx*J(<#99t3Z_*NErI^OI-12kt3;=Xot{*}i+itl`e)2WT9c|3Egqu> zM^akf6YEywIRYSU(*#zG=x(@_p>?r|Fo|(d-)GZxxq?(8Okng_K<(2mU@F0hCFGCH z<4;q*G^qvBo!a~TP|Kv790d{m*qBG_=z$cgof4e}y|fwDGGwL`pNb21#ir0LOCQpK6*cjd6KaJFxM>a6fy5Ar-q)(NSn^@t-`8<;v7G*UcV} zN!UpJAoDQP-L3BLEFU0$FZ!HQpu96!_<&^``ym7fvon`NI9Tg;8Sqh?7kPLkW+f$@ zQp@!m*g@+>3*DzI`q_-pPs)DzYe37m9Ttqq*=JXh+f-ON3)!U#!QmsSD+2LL(ikwV z3SMNW5Bilak1&&C2()#2ieR;X7yiNumAp;M`PRVTs!>Efe!u0kpiUG2L0o7I^H#VB zXx^o72D^+7INpBE)4hX8y;K3`r?PZNVnr>*1IUGoudn9-ct8Wm9G9*i%{>6mJh&yw zX2yf|;oFJ^E3{~DN7Q;T7nesZ=yt51c_m9(p@MU=TrXE1 z#JOIZT!pzVb|X!G=2~dem4y$8<9R&wTFt2>B5+4Ql+{)~4-6xzXezt1r6zc+jq1Xg zVx7BFk)y86?pHR(w1NOdL1OiS0WWt*JA+nFJSp@B`k|Y`rQ!)CLv`qKP$|F*wz(o# zi4@&^+yfaB$$O91HvZDM+R+Sdscwdu0|5Y%hVpP^DCk{Lud5i5`ZBf9qbrg@wHkt}$}4&Sz>8T=s-xK7 z>qu21FNyoCnNOjLAM;Jx>AR)Q2kj25fz?&$KLu@QqIz5gHrYv{e9uIpmw`#(evSya8<6VBaZ^!B*_8is{#_TcN zAHL>XFrwEDj>#mAp|#uZWiO@vFR)g?bmEkl9v3)rAU3GJUl%7k{^=neb%h8;t{+1r&AwD2@ADjgM8IF zhc6<1v^)gP$#pfB#6n$yZN|^LL9CIeHcCO28YN75jUP-;rI^C8-RCYBY2a7Vp7nUh z8s9W;{C=5#Xw$!(Tx+BBVR+#zH%1$5@oLLLzj1HJ6;0v4_CvR=zcNG5&t9Ap+Yglz zI?|4~V5wI;l&yn)eN*G`X@ACmFA$G+XKp+HYPHsepoCZfYX- zi$Fdp!;_T{uLZ6jo#v;vl&HgLGEEGEOyR?jjlXCS4NC&PI7afIUbvY*w;vIR^F6ao zjf{-UKmzZbhJ6E0qHLp!jW|FQo1p?gK>iyuGcwu%H-UvhF)}fN=vO1L0KLZnfhhn< z|5xn%-?)W|QFd?dSa09SK>FX_-W{Zp9yS@aCKeVJ)_P~QUXUUXa4?AC_EHue78)TV zW1pLE&>$joHN^C`NzW`%P#9d$6F>Om|Gb*x*Hu83FhYebRe0ek%Sx%lpvem3*%M$A z&OkG_@}r1~Ezemy=H>L4)xRf%KD?hYA3x!hZvSv8jl}jdDKy;J2p#B2#i?954x=+) zpS^yZ6N6s;@;x4o_=t<@pfvcUD?pHNoDC)^w@^NxFRogt)6`Be2|HXo5`UP?UU_ih zEE#7D^?2o@|H3~!tU4U5ij8ekMRR+dBK#A`aC z5JwBD$EBQxq)*l!3dV)kuv@9vG3r-i6BUZ!^*S?Yah@6%3Ox=EP8RE8O;E3$(;gQqge0~#y zgQJ6MWrK@>f_&=2l>KkK2?&@VRp|G3ubmW>S}z0$Mz?sa z!PzpAuArh>FsIEbGD(Us8^qy$wNr?&o&)_;mlGFIB)t-5zY~s-v2SI*d>62PsO0c^ z-I=sNK^Y%N*!mWwGTk2S3^HJ-n||+PGvb?*AupwVAJ`)s536^n^!;wHva97lcD)en z`=b7wvwQ(kLAiFaCrQ zY|6ql0{cLb;}sk$D1|&j{($qj`b$3Se*(7vNdHJY^y$<4BjBEQ62_Hq!^(Ijem0)7 z=FK>j|1@53$G7oP{#ThvdMooVRgF9oKN-)dD2!A2590-od>Jq0e`skqa3nBj(>8}= z7zD2}HD|r=iModR-0lSxJsqd%jLHZ&Jh`Shwu=A#F3wYuu~B&CmAt4;-<_&H*1`F_ za9d|{MZU~7nJ-EuUHjWkCdxF^Uz|FQ^Vx+sG=1mFytrEX2jhS_k9eG8U}iuDj0|W1 I0052v1wYwmwEzGB diff --git a/public/font42/fonts/lichess.eot b/public/font43/fonts/lichess.eot similarity index 89% rename from public/font42/fonts/lichess.eot rename to public/font43/fonts/lichess.eot index d549ef251f3557daae9bad8725a9b5e3e39ce366..3b9f63d7bbecca19346f7747f433b2c4fa17c018 100644 GIT binary patch delta 808 zcmYjPUr19?82`@Qc4uB^)3$q;yAOBnb~n9E=lE;oXLP%P+45JZrQdg;Y5DNuwU*;BnnV7<-GnFSsA{`|i4`+nyf&X?an?>7-0 z8vt1D*J#B!x3_wzwcX&}wQm9RF8~04A=cRikAcQ`MB8?R7N5Kr0-zpXvyEkDvPVZ? zzMX*5S^Ol3Otciy|gM4$>&SqiH*uprT5F4;{#C zwNT;bMA6ydck;F#QRMu+ywl&|WyD&Q8uGeW)Y;;5IPKj+H7m!R4quDQ;_|8}T`O9t z!f#%vM5@7{B97-<(G<^f80ILzXJ|X=V1tEiF6>J6rAS3H9=MLcgAp R^`%r5{=Qc!C-dJ_{Q=sUt3v<) delta 411 zcmYjM&nrY>6g~I(=A{|2QKsj`NV1rPOnO;ZSoo1-F-6Q9(rY|3%9yF4Sj>>cBrC5k zn<;EqDA`P6#l}V~uwg;HmH$9l$hdD%Zr$&@=XCDvoa+m6&q#GmpwDzvL+luK$?0Ed zIuCaNW(a^jJTN$fU8s!V+y#5IvD14%+2YocRxVqqpazJ2u(me2xHf<6U$6n+DiD21 z+18v`K8na7b*?`zW)hZjkS_q@kkyZAYc-1t+5o7RacId(+s4C_3rvnPSIlN|%N;k{ zSHRQ@V-3VA?vicn%r^ABBcLz=Je)+dzrXUuzMy!@S{{CikwWs4+I=a?qrUt`BQ(LL zJ>$@;XF-jOyfqyHUYNS-0m0J@(kQrsgL>75S!7)NA|^tK>Yg8)*q0kMd$LtXf-H-Y t9QEC@hjLJTjmnwWCv{8H4D(ww*C{2{_`Q6Nhb2wxl$0&g&4GVGz5#USWD5WQ diff --git a/public/font42/fonts/lichess.svg b/public/font43/fonts/lichess.svg similarity index 98% rename from public/font42/fonts/lichess.svg rename to public/font43/fonts/lichess.svg index 2420798c9c265..5cfa5cb937364 100644 --- a/public/font42/fonts/lichess.svg +++ b/public/font43/fonts/lichess.svg @@ -108,4 +108,5 @@ + diff --git a/public/font42/fonts/lichess.ttf b/public/font43/fonts/lichess.ttf similarity index 89% rename from public/font42/fonts/lichess.ttf rename to public/font43/fonts/lichess.ttf index d1cbbc504564e564e57f811317f5993e2971aeef..87e6da557e478315ce840e2c8a6076ee0bb306d9 100644 GIT binary patch delta 820 zcmYjPUr1A76hGhHKk+)-Y`a(7eYn%x-E`^Z{@l%fl{puQ;mlYw|LhK3&T=`@2McpP zL_OHcx)BsaQbA9GU{H|ITVT{GFoY=at=9-7D43l)3%VcYobUJh&Yyd}@2o!~%g;#; z2mlyi5i}5uM9!SL{Hk*UVCXus+=*~^1nzZ@}&CTkq`wAaTU8u}m_%bnnS1 z0Cfw+GLf9g?(JbF7#ha&vWclH=O4ducLL~U0S3b9RB~K9|MD%KeL<1Z=~O<)r9Mwh zk0tXDpYj0OW#l(A$+;}7lSQl(Ld;)EW>V7D)($}Y1#&mC(=)S6MPVD@+%d#lsR!(1 zcjeR7#DVr*peYOk__*3J@z35S9lA+!4a{rD$w|_$()gbx*^dWOiAcQ-4z%zCZ8?+y zg$rN=7Iw7Qnflowu>*hiz>k$8%3x91p&pR5GOxLV+N`~=tY|l=`bTSXy085?Gsc<7 zcT8n~O0Yl;@F2or@InZVKp1);3NfrQ4j0k0R&QavlHX7#I;0?@QML6T!|DwIR@I`z zs#0jBpx(f+);fu`sx({jX9KEJP*qY;bOd~QM=pSQ*h4B+CmDRDBs$gErzeR*K3~Z5 zGO6M@|F(;%<;@;e5Nur@8)q311lAMdY@RMBEi}l~sMF4n?)JuNo3)p(Wn58Pbz{5T zXm`pa(IA+p;!lpRBC;-=^>Co);BZ!warrlQG8 zdtF&#Y^fiitxXlqh>?o=!!kt&rFXPwqFsVa%H3igZ4znwQJRvaNR%?V{5Kh6v#)o! zHAGw5+AMUaHKslfRN%y_uZV+zw{24Q%7wGvh+jD!ccEU0=g6c|jDMh@UnvZ_l;49j Yjx@SyF=T&kO5yKEU;k3G{69p005=4x=l}o! delta 425 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 mapping
+
  • +
    + +
  • Character mapping

      @@ -856,6 +860,10 @@

      Character mapping

      +
    • +
      + +