lila/modules/study/src/main/StudySocket.scala

464 lines
16 KiB
Scala
Raw Normal View History

2016-02-26 05:08:11 -07:00
package lila.study
2019-10-25 04:46:42 -06:00
import actorApi.Who
2020-08-12 00:53:51 -06:00
import cats.data.Validated
2017-07-01 08:44:24 -06:00
import chess.Centis
2019-10-25 04:46:42 -06:00
import chess.format.pgn.{ Glyph, Glyphs }
2020-08-12 00:53:51 -06:00
import play.api.libs.json._
import scala.concurrent.duration._
import lila.common.Bus
2019-10-25 04:46:42 -06:00
import lila.room.RoomSocket.{ Protocol => RP, _ }
import lila.socket.RemoteSocket.{ Protocol => P, _ }
2019-12-13 07:30:20 -07:00
import lila.socket.Socket.{ makeMessage, Sri }
import lila.socket.{ AnaAny, AnaDests, AnaDrop, AnaMove }
import lila.tree.Node.{ defaultNodeJsonWriter, Comment, Gamebook, Shape, Shapes }
2016-04-20 01:04:38 -06:00
import lila.user.User
2016-02-26 05:08:11 -07:00
2019-12-13 07:30:20 -07:00
final private class StudySocket(
2019-10-25 04:46:42 -06:00
api: StudyApi,
2016-05-11 03:20:19 -06:00
jsonView: JsonView,
2019-10-25 04:46:42 -06:00
remoteSocketApi: lila.socket.RemoteSocket,
chatApi: lila.chat.ChatApi
2020-06-24 03:37:18 -06:00
)(implicit
ec: scala.concurrent.ExecutionContext,
mode: play.api.Mode
) {
2016-02-26 05:08:11 -07:00
2018-12-07 05:18:57 -07:00
import StudySocket._
2016-04-20 01:04:38 -06:00
2019-12-13 07:30:20 -07:00
implicit def roomIdToStudyId(roomId: RoomId) = Study.Id(roomId.value)
2019-10-25 04:46:42 -06:00
implicit def studyIdToRoomId(studyId: Study.Id) = RoomId(studyId.value)
2016-04-23 01:39:55 -06:00
lazy val rooms = makeRoomMap(send)
2016-06-13 05:29:00 -06:00
2020-04-23 11:43:54 -06:00
subscribeChat(rooms, _.Study)
2020-04-22 08:59:36 -06:00
2019-10-25 04:46:42 -06:00
def isPresent(studyId: Study.Id, userId: User.ID): Fu[Boolean] =
remoteSocketApi.request[Boolean](
id => send(Protocol.Out.getIsPresent(id, studyId, userId)),
_ == "true"
)
2020-05-05 22:11:15 -06:00
def onServerEval(studyId: Study.Id, eval: ServerEval.Progress): Unit =
eval match {
case ServerEval.Progress(chapterId, tree, analysis, division) =>
import lila.game.JsonView.divisionWriter
import JsonView._
send(
RP.Out.tellRoom(
studyId,
makeMessage(
"analysisProgress",
Json.obj(
"analysis" -> analysis,
"ch" -> chapterId,
"tree" -> defaultNodeJsonWriter.writes(tree),
"division" -> division
)
2019-12-13 07:30:20 -07:00
)
)
)
2020-05-05 22:11:15 -06:00
}
2016-06-13 05:29:00 -06:00
2019-10-25 04:46:42 -06:00
private lazy val studyHandler: Handler = {
2019-10-28 14:29:03 -06:00
case RP.In.ChatSay(roomId, userId, msg) => api.talk(userId, roomId, msg)
2019-10-31 03:27:41 -06:00
case RP.In.TellRoomSri(studyId, P.In.TellSri(sri, user, tpe, o)) =>
2019-10-25 04:46:42 -06:00
import Protocol.In.Data._
import JsonView.shapeReader
def who = user map { Who(_, sri) }
tpe match {
2019-12-13 07:30:20 -07:00
case "setPath" =>
reading[AtPosition](o) { position =>
who foreach api.setPath(studyId, position.ref)
2019-10-25 04:46:42 -06:00
}
2019-12-13 07:30:20 -07:00
case "like" =>
(o \ "d" \ "liked").asOpt[Boolean] foreach { v =>
who foreach api.like(studyId, v)
2019-10-25 04:46:42 -06:00
}
2019-12-13 07:30:20 -07:00
case "anaMove" =>
AnaMove parse o foreach { move =>
who foreach moveOrDrop(studyId, move, MoveOpts parse o)
2019-10-25 04:46:42 -06:00
}
2019-12-13 07:30:20 -07:00
case "anaDrop" =>
AnaDrop parse o foreach { drop =>
who foreach moveOrDrop(studyId, drop, MoveOpts parse o)
2019-10-25 04:46:42 -06:00
}
2019-12-13 07:30:20 -07:00
case "deleteNode" =>
reading[AtPosition](o) { position =>
(o \ "d" \ "jumpTo").asOpt[String] map Path.apply foreach { jumpTo =>
who foreach api.setPath(studyId, position.ref.withPath(jumpTo))
who foreach api.deleteNodeAt(studyId, position.ref)
}
2019-10-25 04:46:42 -06:00
}
2019-12-13 07:30:20 -07:00
case "promote" =>
reading[AtPosition](o) { position =>
(o \ "d" \ "toMainline").asOpt[Boolean] foreach { toMainline =>
who foreach api.promote(studyId, position.ref, toMainline)
}
2019-10-25 04:46:42 -06:00
}
2019-12-13 07:30:20 -07:00
case "forceVariation" =>
reading[AtPosition](o) { position =>
(o \ "d" \ "force").asOpt[Boolean] foreach { force =>
who foreach api.forceVariation(studyId, position.ref, force)
}
2019-10-25 04:46:42 -06:00
}
2019-12-13 07:30:20 -07:00
case "setRole" =>
reading[SetRole](o) { d =>
who foreach api.setRole(studyId, d.userId, d.role)
}
case "kick" =>
o str "d" foreach { username =>
who foreach api.kick(studyId, username)
}
case "leave" =>
who foreach { w =>
api.kick(studyId, w.u)(w)
}
case "shapes" =>
reading[AtPosition](o) { position =>
(o \ "d" \ "shapes").asOpt[List[Shape]] foreach { shapes =>
who foreach api.setShapes(studyId, position.ref, Shapes(shapes take 32))
}
}
case "addChapter" =>
reading[ChapterMaker.Data](o) { data =>
val sticky = o.obj("d").flatMap(_.boolean("sticky")) | true
who foreach api.addChapter(studyId, data, sticky = sticky, withRatings = true)
2019-12-13 07:30:20 -07:00
}
case "setChapter" =>
o.get[Chapter.Id]("d") foreach { chapterId =>
who foreach api.setChapter(studyId, chapterId)
}
case "editChapter" =>
reading[ChapterMaker.EditData](o) { data =>
who foreach api.editChapter(studyId, data)
}
case "descStudy" =>
o str "d" foreach { desc =>
who foreach api.descStudy(studyId, desc)
}
case "descChapter" =>
reading[ChapterMaker.DescData](o) { data =>
who foreach api.descChapter(studyId, data)
}
case "deleteChapter" =>
o.get[Chapter.Id]("d") foreach { id =>
who foreach api.deleteChapter(studyId, id)
}
case "clearAnnotations" =>
o.get[Chapter.Id]("d") foreach { id =>
who foreach api.clearAnnotations(studyId, id)
}
case "sortChapters" =>
o.get[List[Chapter.Id]]("d") foreach { ids =>
who foreach api.sortChapters(studyId, ids)
}
case "editStudy" =>
(o \ "d").asOpt[Study.Data] foreach { data =>
who foreach api.editStudy(studyId, data)
}
case "setTag" =>
reading[actorApi.SetTag](o) { setTag =>
who foreach api.setTag(studyId, setTag)
}
case "setComment" =>
reading[AtPosition](o) { position =>
(o \ "d" \ "text").asOpt[String] foreach { text =>
who foreach api.setComment(studyId, position.ref, Comment sanitize text)
}
}
case "deleteComment" =>
reading[AtPosition](o) { position =>
(o \ "d" \ "id").asOpt[String] foreach { id =>
who foreach api.deleteComment(studyId, position.ref, Comment.Id(id))
}
}
case "setGamebook" =>
reading[AtPosition](o) { position =>
(o \ "d" \ "gamebook").asOpt[Gamebook].map(_.cleanUp) foreach { gamebook =>
who foreach api.setGamebook(studyId, position.ref, gamebook)
}
}
case "toggleGlyph" =>
reading[AtPosition](o) { position =>
(o \ "d" \ "id").asOpt[Int] flatMap Glyph.find foreach { glyph =>
who foreach api.toggleGlyph(studyId, position.ref, glyph)
}
}
2020-02-21 12:27:22 -07:00
case "setTopics" =>
2020-02-21 14:17:22 -07:00
o strs "d" foreach { topics =>
2020-02-21 12:27:22 -07:00
who foreach api.setTopics(studyId, topics)
}
2019-12-13 07:30:20 -07:00
case "explorerGame" =>
reading[actorApi.ExplorerGame](o) { data =>
who foreach api.explorerGame(studyId, data)
}
case "requestAnalysis" =>
o.get[Chapter.Id]("d") foreach { chapterId =>
user foreach { api.analysisRequest(studyId, chapterId, _) }
}
case "invite" =>
for {
w <- who
username <- o str "d"
} InviteLimitPerUser(w.u, cost = 1) {
api.invite(
w.u,
studyId,
username,
isPresent = userId => isPresent(studyId, userId),
onError = err => send(P.Out.tellSri(w.sri, makeMessage("error", err)))
)
}(funit)
2019-12-13 07:30:20 -07:00
case "relaySync" =>
who foreach { w =>
Bus.publish(actorApi.RelayToggle(studyId, ~(o \ "d").asOpt[Boolean], w), "relayToggle")
2019-10-25 04:46:42 -06:00
}
case t => logger.warn(s"Unhandled study socket message: $t")
}
2019-10-25 04:46:42 -06:00
}
2019-12-13 07:30:20 -07:00
private lazy val rHandler: Handler = roomHandler(
rooms,
chatApi,
logger,
2019-12-08 09:58:50 -07:00
_ => _ => none, // the "talk" event is handled by the study API
2019-10-28 14:29:03 -06:00
localTimeout = Some { (roomId, modId, suspectId) =>
2019-10-27 13:56:42 -06:00
api.isContributor(roomId, modId) >>& !api.isMember(roomId, suspectId)
2020-04-23 11:43:54 -06:00
},
chatBusChan = _.Study
2019-12-13 07:30:20 -07:00
)
2019-10-27 13:56:42 -06:00
2020-05-05 22:11:15 -06:00
private def moveOrDrop(studyId: Study.Id, m: AnaAny, opts: MoveOpts)(who: Who) =
m.branch match {
2020-08-12 00:53:51 -06:00
case Validated.Valid(branch) if branch.ply < Node.MAX_PLIES =>
2020-05-05 22:11:15 -06:00
m.chapterId.ifTrue(opts.write) foreach { chapterId =>
api.addNode(
studyId,
Position.Ref(Chapter.Id(chapterId), Path(m.path)),
Node.fromBranch(branch) withClock opts.clock,
opts
)(who)
}
case _ =>
}
2017-06-10 12:21:56 -06:00
2019-10-25 04:46:42 -06:00
private lazy val send: String => Unit = remoteSocketApi.makeSender("study-out").apply _
2016-04-21 22:48:02 -06:00
2020-03-28 23:43:12 -06:00
remoteSocketApi.subscribe("study-in", RP.In.reader)(
2019-10-27 13:56:42 -06:00
studyHandler orElse rHandler orElse remoteSocketApi.baseHandler
) >>- send(P.Out.boot)
2016-02-26 05:08:11 -07:00
// send API
import JsonView._
import jsonView.membersWrites
2019-12-13 07:30:20 -07:00
import lila.tree.Node.{
clockWrites,
commentWriter,
defaultNodeJsonWriter,
glyphsWriter,
openingWriter,
shapesWrites
}
private type SendToStudy = Study.Id => Unit
2019-12-13 07:30:20 -07:00
private def version[A: Writes](tpe: String, data: A): SendToStudy =
studyId => rooms.tell(studyId.value, NotifyVersion(tpe, data))
private def notify[A: Writes](tpe: String, data: A): SendToStudy =
studyId => send(RP.Out.tellRoom(studyId, makeMessage(tpe, data)))
private def notifySri[A: Writes](sri: Sri, tpe: String, data: A): SendToStudy =
_ => send(P.Out.tellSri(sri, makeMessage(tpe, data)))
def setPath(pos: Position.Ref, who: Who) = version("path", Json.obj("p" -> pos, "w" -> who))
def addNode(
2019-12-13 07:30:20 -07:00
pos: Position.Ref,
node: Node,
variant: chess.variant.Variant,
sticky: Boolean,
relay: Option[Chapter.Relay],
who: Who
) = {
val dests = AnaDests(variant, node.fen, pos.path.toString, pos.chapterId.value.some)
2019-12-13 07:30:20 -07:00
version(
"addNode",
Json
.obj(
"n" -> defaultNodeJsonWriter.writes(TreeBuilder.toBranch(node, variant)),
"p" -> pos,
"w" -> who,
"d" -> dests.dests,
"o" -> dests.opening,
"s" -> sticky
)
.add("relay", relay)
)
2019-10-25 04:46:42 -06:00
}
def deleteNode(pos: Position.Ref, who: Who) = version("deleteNode", Json.obj("p" -> pos, "w" -> who))
2019-12-13 07:30:20 -07:00
def promote(pos: Position.Ref, toMainline: Boolean, who: Who) =
version(
"promote",
Json.obj(
"p" -> pos,
"toMainline" -> toMainline,
"w" -> who
)
)
def setLiking(liking: Study.Liking, who: Who) = notify("liking", Json.obj("l" -> liking, "w" -> who))
2019-12-13 07:30:20 -07:00
def setShapes(pos: Position.Ref, shapes: Shapes, who: Who) =
version(
"shapes",
Json.obj(
"p" -> pos,
"s" -> shapes,
"w" -> who
)
)
2020-09-25 08:30:59 -06:00
def reloadMembers(members: StudyMembers, sendTo: Iterable[User.ID])(studyId: Study.Id) =
2020-09-24 23:38:37 -06:00
send(RP.Out.tellRoomUsers(studyId, sendTo, makeMessage("members", members)))
2019-12-13 07:30:20 -07:00
def setComment(pos: Position.Ref, comment: Comment, who: Who) =
version(
"setComment",
Json.obj(
"p" -> pos,
"c" -> comment,
"w" -> who
)
)
def deleteComment(pos: Position.Ref, commentId: Comment.Id, who: Who) =
version(
"deleteComment",
Json.obj(
"p" -> pos,
"id" -> commentId,
"w" -> who
)
)
def setGlyphs(pos: Position.Ref, glyphs: Glyphs, who: Who) =
version(
"glyphs",
Json.obj(
"p" -> pos,
"g" -> glyphs,
"w" -> who
)
)
def setClock(pos: Position.Ref, clock: Option[Centis], who: Who) =
version(
"clock",
Json.obj(
"p" -> pos,
"c" -> clock,
"w" -> who
)
)
def forceVariation(pos: Position.Ref, force: Boolean, who: Who) =
version(
"forceVariation",
Json.obj(
"p" -> pos,
"force" -> force,
"w" -> who
)
)
2021-04-27 02:48:37 -06:00
private[study] def reloadChapters(chapters: List[Chapter.Metadata]) = version("chapters", chapters)
def reloadAll = version("reload", JsNull)
def changeChapter(pos: Position.Ref, who: Who) = version("changeChapter", Json.obj("p" -> pos, "w" -> who))
2019-12-13 07:30:20 -07:00
def updateChapter(chapterId: Chapter.Id, who: Who) =
version("updateChapter", Json.obj("chapterId" -> chapterId, "w" -> who))
def descChapter(chapterId: Chapter.Id, desc: Option[String], who: Who) =
version(
"descChapter",
Json.obj(
"chapterId" -> chapterId,
"desc" -> desc,
"w" -> who
)
)
def descStudy(desc: Option[String], who: Who) = version("descStudy", Json.obj("desc" -> desc, "w" -> who))
2020-02-21 12:27:22 -07:00
def setTopics(topics: StudyTopics, who: Who) =
version("setTopics", Json.obj("topics" -> topics, "w" -> who))
2019-12-13 07:30:20 -07:00
def addChapter(pos: Position.Ref, sticky: Boolean, who: Who) =
version(
"addChapter",
Json.obj(
"p" -> pos,
"w" -> who,
"s" -> sticky
)
)
def setConceal(pos: Position.Ref, ply: Option[Chapter.Ply]) =
version(
"conceal",
Json.obj(
"p" -> pos,
"ply" -> ply.map(_.value)
)
)
def setTags(chapterId: Chapter.Id, tags: chess.format.pgn.Tags, who: Who) =
version(
"setTags",
Json.obj(
"chapterId" -> chapterId,
"tags" -> tags,
"w" -> who
)
)
def reloadSri(sri: Sri) = notifySri(sri, "reload", JsNull)
2019-12-13 07:30:20 -07:00
def reloadSriBecauseOf(sri: Sri, chapterId: Chapter.Id) =
notifySri(sri, "reload", Json.obj("chapterId" -> chapterId))
def validationError(error: String, sri: Sri) = notifySri(sri, "validationError", Json.obj("error" -> error))
2019-10-25 04:46:42 -06:00
private val InviteLimitPerUser = new lila.memo.RateLimit[User.ID](
credits = 50,
duration = 24 hour,
key = "study_invite.user"
)
2019-11-07 17:38:02 -07:00
api registerSocket this
2019-10-25 04:46:42 -06:00
}
2016-02-26 05:08:11 -07:00
2019-10-25 04:46:42 -06:00
object StudySocket {
2016-02-26 05:08:11 -07:00
2019-10-25 04:46:42 -06:00
object Protocol {
2017-01-26 06:55:39 -07:00
2019-10-25 04:46:42 -06:00
object In {
2017-09-30 17:00:51 -06:00
2019-10-25 04:46:42 -06:00
object Data {
2019-12-07 17:43:22 -07:00
import lila.common.Json._
2019-10-25 04:46:42 -06:00
import play.api.libs.functional.syntax._
def reading[A](o: JsValue)(f: A => Unit)(implicit reader: Reads[A]): Unit =
2019-12-13 07:30:20 -07:00
o obj "d" flatMap { d =>
reader.reads(d).asOpt
} foreach f
2019-10-25 04:46:42 -06:00
case class AtPosition(path: String, chapterId: Chapter.Id) {
def ref = Position.Ref(chapterId, Path(path))
}
2019-12-13 07:30:20 -07:00
implicit val chapterIdReader: Reads[Chapter.Id] = stringIsoReader(Chapter.idIso)
2019-10-25 04:46:42 -06:00
implicit val chapterNameReader: Reads[Chapter.Name] = stringIsoReader(Chapter.nameIso)
implicit val atPositionReader: Reads[AtPosition] = (
(__ \ "path").read[String] and
2019-12-13 07:30:20 -07:00
(__ \ "ch").read[Chapter.Id]
2019-10-25 04:46:42 -06:00
)(AtPosition.apply _)
case class SetRole(userId: String, role: String)
2019-12-13 07:30:20 -07:00
implicit val SetRoleReader: Reads[SetRole] = Json.reads[SetRole]
implicit val ChapterDataReader: Reads[ChapterMaker.Data] = Json.reads[ChapterMaker.Data]
2019-10-25 04:46:42 -06:00
implicit val ChapterEditDataReader: Reads[ChapterMaker.EditData] = Json.reads[ChapterMaker.EditData]
implicit val ChapterDescDataReader: Reads[ChapterMaker.DescData] = Json.reads[ChapterMaker.DescData]
2019-12-13 07:30:20 -07:00
implicit val StudyDataReader: Reads[Study.Data] = Json.reads[Study.Data]
implicit val setTagReader: Reads[actorApi.SetTag] = Json.reads[actorApi.SetTag]
implicit val gamebookReader: Reads[Gamebook] = Json.reads[Gamebook]
implicit val explorerGame: Reads[actorApi.ExplorerGame] = Json.reads[actorApi.ExplorerGame]
2019-10-25 04:46:42 -06:00
}
2016-04-23 01:39:55 -06:00
}
2016-02-26 05:08:11 -07:00
2019-10-25 04:46:42 -06:00
object Out {
def getIsPresent(reqId: Int, studyId: Study.Id, userId: User.ID) =
s"room/present $reqId $studyId $userId"
2016-05-22 04:11:32 -06:00
}
}
2016-02-26 05:08:11 -07:00
}