study remote socket WIP

pull/5629/head
Thibault Duplessis 2019-10-24 19:37:19 +02:00
parent 0ab7df8b1d
commit 8739f079b3
4 changed files with 274 additions and 318 deletions

View File

@ -4,6 +4,7 @@ import chess.Centis
import io.lettuce.core._
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.ConcurrentHashMap
import ornicar.scalalib.Zero
import play.api.libs.json._
import scala.concurrent.duration._
@ -112,6 +113,13 @@ final class RemoteSocket(
conn.async.subscribe(channel)
}
// final class Ask[R] {
// def apply[R, A](find: R => A): Future[A] = {
// promise = Promise[R]
// }
// val handler: Protocol.In => Option[R]
lifecycle.addStopHook { () =>
logger.info("Stopping the Redis pool...")
Future {

View File

@ -39,30 +39,6 @@ final class SocketHandler(
key = "study_invite.user"
)
private def moveOrDrop(studyId: Study.Id, m: AnaAny, opts: MoveOpts, sri: Sri, member: StudySocket.Member) =
AnaRateLimit(sri, member) {
m.branch match {
case scalaz.Success(branch) if branch.ply < Node.MAX_PLIES =>
member push makeMessage("node", m json branch)
for {
userId <- member.userId
chapterId <- m.chapterId
if opts.write
} api.addNode(
userId,
studyId,
Position.Ref(Chapter.Id(chapterId), Path(m.path)),
Node.fromBranch(branch) withClock opts.clock,
sri,
opts
)
case scalaz.Success(branch) =>
member push makeMessage("stepFailure", s"ply ${branch.ply}/${Node.MAX_PLIES}")
case scalaz.Failure(err) =>
member push makeMessage("stepFailure", err.toString)
}
}
def makeController(
socket: StudySocket,
studyId: Study.Id,
@ -70,47 +46,6 @@ final class SocketHandler(
member: StudySocket.Member,
user: Option[User]
): Handler.Controller = ({
case ("talk", o) => o str "d" foreach { text =>
member.userId foreach { api.talk(_, studyId, text) }
}
case ("anaMove", o) => AnaMove parse o foreach {
moveOrDrop(studyId, _, MoveOpts parse o, sri, member)
}
case ("anaDrop", o) => AnaDrop parse o foreach {
moveOrDrop(studyId, _, MoveOpts parse o, sri, member)
}
case ("deleteNode", o) => AnaRateLimit(sri, member) {
reading[AtPosition](o) { position =>
for {
jumpTo <- (o \ "d" \ "jumpTo").asOpt[String] map Path.apply
userId <- member.userId
} api.setPath(Who(userId, sri), studyId, position.ref.withPath(jumpTo)) >>
api.deleteNodeAt(userId, studyId, position.ref, sri)
}
}
case ("promote", o) => AnaRateLimit(sri, member) {
reading[AtPosition](o) { position =>
for {
toMainline <- (o \ "d" \ "toMainline").asOpt[Boolean]
userId <- member.userId
} api.promote(userId, studyId, position.ref, toMainline, sri)
}
}
case ("forceVariation", o) => AnaRateLimit(sri, member) {
reading[AtPosition](o) { position =>
for {
force <- (o \ "d" \ "force").asOpt[Boolean]
userId <- member.userId
} api.forceVariation(userId, studyId, position.ref, force, sri)
}
}
case ("setRole", o) => AnaRateLimit(sri, member) {
reading[SetRole](o) { d =>
member.userId foreach { userId =>
api.setRole(userId, studyId, d.userId, d.role)
}
}
}
case ("invite", o) => for {
byUserId <- member.userId
username <- o str "d"
@ -119,126 +54,6 @@ final class SocketHandler(
onError = err => member push makeMessage("error", err))
}
case ("kick", o) => for {
byUserId <- member.userId
username <- o str "d"
} api.kick(byUserId, studyId, username)
case ("leave", _) => member.userId foreach { userId =>
api.kick(userId, studyId, userId)
}
case ("shapes", o) =>
reading[AtPosition](o) { position =>
for {
shapes <- (o \ "d" \ "shapes").asOpt[List[Shape]]
userId <- member.userId
} api.setShapes(userId, studyId, position.ref, Shapes(shapes take 32), sri)
}
case ("addChapter", o) =>
reading[ChapterMaker.Data](o) { data =>
member.userId foreach { byUserId =>
val sticky = o.obj("d").flatMap(_.boolean("sticky")) | true
api.addChapter(byUserId, studyId, data, sticky = sticky, sri)
}
}
case ("setChapter", o) => for {
byUserId <- member.userId
chapterId <- o.get[Chapter.Id]("d")
} api.setChapter(byUserId, studyId, chapterId, sri)
case ("editChapter", o) =>
reading[ChapterMaker.EditData](o) { data =>
member.userId foreach {
api.editChapter(_, studyId, data, sri)
}
}
case ("descStudy", o) => for {
desc <- o str "d"
userId <- member.userId
} api.descStudy(userId, studyId, desc, sri)
case ("descChapter", o) =>
reading[ChapterMaker.DescData](o) { data =>
member.userId foreach {
api.descChapter(_, studyId, data, sri)
}
}
case ("deleteChapter", o) => for {
byUserId <- member.userId
id <- o.get[Chapter.Id]("d")
} api.deleteChapter(byUserId, studyId, id, sri)
case ("clearAnnotations", o) => for {
byUserId <- member.userId
id <- o.get[Chapter.Id]("d")
} api.clearAnnotations(byUserId, studyId, id, sri)
case ("sortChapters", o) => for {
byUserId <- member.userId
ids <- o.get[List[Chapter.Id]]("d")
} api.sortChapters(byUserId, studyId, ids, sri)
case ("editStudy", o) => for {
byUserId <- member.userId
data <- (o \ "d").asOpt[Study.Data]
} api.editStudy(byUserId, studyId, data)
case ("setTag", o) =>
reading[actorApi.SetTag](o) { setTag =>
member.userId foreach { byUserId =>
api.setTag(byUserId, studyId, setTag, sri)
}
}
case ("setComment", o) =>
reading[AtPosition](o) { position =>
for {
userId <- member.userId
text <- (o \ "d" \ "text").asOpt[String]
} api.setComment(userId, studyId, position.ref, Comment sanitize text, sri)
}
case ("deleteComment", o) =>
reading[AtPosition](o) { position =>
for {
userId <- member.userId
id <- (o \ "d" \ "id").asOpt[String]
} api.deleteComment(userId, studyId, position.ref, Comment.Id(id), sri)
}
case ("setGamebook", o) =>
reading[AtPosition](o) { position =>
for {
userId <- member.userId
gamebook <- (o \ "d" \ "gamebook").asOpt[Gamebook].map(_.cleanUp)
} api.setGamebook(userId, studyId, position.ref, gamebook, sri)
}
case ("toggleGlyph", o) =>
reading[AtPosition](o) { position =>
for {
userId <- member.userId
glyph <- (o \ "d" \ "id").asOpt[Int] flatMap Glyph.find
} api.toggleGlyph(userId, studyId, position.ref, glyph, sri)
}
case ("explorerGame", o) =>
reading[actorApi.ExplorerGame](o) { data =>
member.userId foreach { byUserId =>
api.explorerGame(byUserId, studyId, data, sri)
}
}
case ("requestAnalysis", o) => for {
byUserId <- member.userId
chapterId <- o.get[Chapter.Id]("d")
} api.analysisRequest(studyId, chapterId, byUserId)
}: Handler.Controller) orElse evalCacheHandler(sri, member, user) orElse lila.chat.Socket.in(
chatId = Chat.Id(studyId.value),
member = member,

View File

@ -3,6 +3,7 @@ package lila.study
import akka.actor.ActorSelection
import scala.concurrent.duration._
import actorApi.Who
import chess.Centis
import chess.format.pgn.{ Tags, Glyph }
import lila.chat.Chat
@ -12,7 +13,6 @@ import lila.socket.Socket.Sri
import lila.tree.Eval
import lila.tree.Node.{ Shapes, Comment, Gamebook }
import lila.user.User
import actorApi.Who
final class StudyApi(
studyRepo: StudyRepo,
@ -96,12 +96,10 @@ final class StudyApi(
import akka.pattern.ask
import makeTimeout.short
addChapter(
byUserId = user.id,
studyId = study.id,
data = data.form.toChapterData,
sticky = study.settings.sticky,
sri = Sri("") // the user is not in the study yet
) >> byIdWithChapter(studyId)
sticky = study.settings.sticky
)(Who(user.id, Sri(""))) >> byIdWithChapter(studyId)
case _ => fuccess(none)
} orElse importGame(data.copy(form = data.form.copy(asStr = none)), user)
}) addEffect {
@ -158,7 +156,7 @@ final class StudyApi(
}
}
def setPath(who: Who, studyId: Study.Id, position: Position.Ref) = sequenceStudy(studyId) { study =>
def setPath(studyId: Study.Id, position: Position.Ref)(who: Who): Funit = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
chapterRepo.byId(position.chapterId).map {
_ filter { c =>
@ -175,16 +173,16 @@ final class StudyApi(
}
}
def addNode(userId: User.ID, studyId: Study.Id, position: Position.Ref, node: Node, sri: Sri, opts: MoveOpts, relay: Option[Chapter.Relay] = None) =
def addNode(studyId: Study.Id, position: Position.Ref, node: Node, opts: MoveOpts, relay: Option[Chapter.Relay] = None)(who: Who) =
sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
doAddNode(userId, study, Position(chapter, position.path), node, sri, opts, relay).void
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
doAddNode(study, Position(chapter, position.path), node, opts, relay)(who).void
}
}
private def doAddNode(userId: User.ID, study: Study, position: Position, rawNode: Node, sri: Sri, opts: MoveOpts, relay: Option[Chapter.Relay]): Funit = {
private def doAddNode(study: Study, position: Position, rawNode: Node, opts: MoveOpts, relay: Option[Chapter.Relay])(who: Who): Funit = {
val node = rawNode.withoutChildren
def failReload = reloadSriBecauseOf(study, sri, position.chapter.id)
def failReload = reloadSriBecauseOf(study, who.sri, position.chapter.id)
if (position.chapter.isOverweight) {
logger.info(s"Overweight chapter ${study.id}/${position.chapter.id}")
fuccess(failReload)
@ -203,13 +201,13 @@ final class StudyApi(
position.ref,
node,
chapter.setup.variant,
sri,
who.sri,
sticky = opts.sticky,
relay = relay
))
sendStudyEnters(study, userId)
sendStudyEnters(study, who.u)
if (opts.promoteToMainline && !Path.isMainline(chapter.root, newPosition.path))
promote(userId, study.id, position.ref + node, toMainline = true, sri)
promote(study.id, position.ref + node, toMainline = true)(who)
}
}
}
@ -227,31 +225,31 @@ final class StudyApi(
}
}
def deleteNodeAt(userId: User.ID, studyId: Study.Id, position: Position.Ref, sri: Sri) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
def deleteNodeAt(studyId: Study.Id, position: Position.Ref)(who: Who) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
chapter.updateRoot { root =>
root.withChildren(_.deleteNodeAt(position.path))
} match {
case Some(newChapter) =>
chapterRepo.update(newChapter) >>-
sendTo(study, StudySocket.DeleteNode(position, sri))
sendTo(study, StudySocket.DeleteNode(position, who.sri))
case None =>
fufail(s"Invalid delNode $studyId $position") >>-
reloadSriBecauseOf(study, sri, chapter.id)
reloadSriBecauseOf(study, who.sri, chapter.id)
}
}
}
def clearAnnotations(userId: User.ID, studyId: Study.Id, chapterId: Chapter.Id, sri: Sri) = sequenceStudyWithChapter(studyId, chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
def clearAnnotations(studyId: Study.Id, chapterId: Chapter.Id)(who: Who) = sequenceStudyWithChapter(studyId, chapterId) {
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
chapterRepo.update(chapter.updateRoot { root =>
root.withChildren(_.updateAllWith(_.clearAnnotations).some)
} | chapter) >>- sendTo(study, StudySocket.UpdateChapter(sri, chapter.id))
} | chapter) >>- sendTo(study, StudySocket.UpdateChapter(who.sri, chapter.id))
}
}
def promote(userId: User.ID, studyId: Study.Id, position: Position.Ref, toMainline: Boolean, sri: Sri) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
def promote(studyId: Study.Id, position: Position.Ref, toMainline: Boolean)(who: Who) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
chapter.updateRoot { root =>
root.withChildren { children =>
if (toMainline) children.promoteToMainlineAt(position.path)
@ -260,7 +258,7 @@ final class StudyApi(
} match {
case Some(newChapter) =>
chapterRepo.update(newChapter) >>-
sendTo(study, StudySocket.Promote(position, toMainline, sri)) >>
sendTo(study, StudySocket.Promote(position, toMainline, who.sri)) >>
newChapter.root.children.nodesOn {
newChapter.root.mainlinePath.intersect(position.path)
}.collect {
@ -269,15 +267,15 @@ final class StudyApi(
}.sequenceFu.void
case None =>
fufail(s"Invalid promoteToMainline $studyId $position") >>-
reloadSriBecauseOf(study, sri, chapter.id)
reloadSriBecauseOf(study, who.sri, chapter.id)
}
}
}
def forceVariation(userId: User.ID, studyId: Study.Id, position: Position.Ref, force: Boolean, sri: Sri): Funit =
def forceVariation(studyId: Study.Id, position: Position.Ref, force: Boolean)(who: Who): Funit =
sequenceStudyWithChapter(studyId, position.chapterId) { sc =>
Contribute(userId, sc.study) {
doForceVariation(sc, position.path, force, sri)
Contribute(who.u, sc.study) {
doForceVariation(sc, position.path, force, who.sri)
}
}
@ -291,8 +289,8 @@ final class StudyApi(
reloadSriBecauseOf(sc.study, sri, sc.chapter.id)
}
def setRole(byUserId: User.ID, studyId: Study.Id, userId: User.ID, roleStr: String) = sequenceStudy(studyId) { study =>
(study isOwner byUserId) ?? {
def setRole(studyId: Study.Id, userId: User.ID, roleStr: String)(who: Who) = sequenceStudy(studyId) { study =>
(study isOwner who.u) ?? {
val role = StudyMember.Role.byId.getOrElse(roleStr, StudyMember.Role.Read)
study.members.get(userId) ifTrue study.isPublic foreach { member =>
if (!member.role.canWrite && role.canWrite)
@ -312,8 +310,8 @@ final class StudyApi(
)
}
def kick(byUserId: User.ID, studyId: Study.Id, userId: User.ID) = sequenceStudy(studyId) { study =>
(study.isMember(userId) && (study.isOwner(byUserId) ^ (byUserId == userId))) ?? {
def kick(studyId: Study.Id, userId: User.ID)(who: Who) = sequenceStudy(studyId) { study =>
(study.isMember(userId) && (study.isOwner(who.u) ^ (who.u == userId))) ?? {
if (study.isPublic && study.canContribute(userId))
bus.publish(lila.hub.actorApi.study.StudyMemberLostWriteAccess(userId, studyId.value), 'study)
studyRepo.removeMember(study, userId)
@ -333,18 +331,18 @@ final class StudyApi(
indexStudy(study)
}
def setShapes(userId: User.ID, studyId: Study.Id, position: Position.Ref, shapes: Shapes, sri: Sri) = sequenceStudy(studyId) { study =>
Contribute(userId, study) {
def setShapes(studyId: Study.Id, position: Position.Ref, shapes: Shapes)(who: Who) = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
chapterRepo.byIdAndStudy(position.chapterId, study.id) flatMap {
_ ?? { chapter =>
chapter.setShapes(shapes, position.path) match {
case Some(newChapter) =>
studyRepo.updateNow(study)
chapterRepo.setShapes(shapes)(newChapter, position.path) >>-
sendTo(study, StudySocket.SetShapes(position, shapes, sri))
sendTo(study, StudySocket.SetShapes(position, shapes, who.sri))
case None =>
fufail(s"Invalid setShapes $position $shapes") >>-
reloadSriBecauseOf(study, sri, chapter.id)
reloadSriBecauseOf(study, who.sri, chapter.id)
}
}
}
@ -364,15 +362,15 @@ final class StudyApi(
}
}
def setTag(userId: User.ID, studyId: Study.Id, setTag: actorApi.SetTag, sri: Sri) = sequenceStudyWithChapter(studyId, setTag.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
doSetTags(study, chapter, PgnTags(chapter.tags + setTag.tag), sri)
def setTag(studyId: Study.Id, setTag: actorApi.SetTag)(who: Who) = sequenceStudyWithChapter(studyId, setTag.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
doSetTags(study, chapter, PgnTags(chapter.tags + setTag.tag), who.sri)
}
}
def setTags(userId: User.ID, studyId: Study.Id, chapterId: Chapter.Id, tags: Tags, sri: Sri) = sequenceStudyWithChapter(studyId, chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
doSetTags(study, chapter, tags, sri)
def setTags(studyId: Study.Id, chapterId: Chapter.Id, tags: Tags, sri: Sri)(who: Who) = sequenceStudyWithChapter(studyId, chapterId) {
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
doSetTags(study, chapter, tags, who.sri)
}
}
@ -388,15 +386,15 @@ final class StudyApi(
} >>- indexStudy(study)
}
def setComment(userId: User.ID, studyId: Study.Id, position: Position.Ref, text: Comment.Text, sri: Sri) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
lightUser(userId) ?? { author =>
def setComment(studyId: Study.Id, position: Position.Ref, text: Comment.Text)(who: Who) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
lightUser(who.u) ?? { author =>
val comment = Comment(
id = Comment.Id.make,
text = text,
by = Comment.Author.User(author.id, author.titleName)
)
doSetComment(userId, study, Position(chapter, position.path), comment, sri)
doSetComment(who.u, study, Position(chapter, position.path), comment, who.sri)
}
}
}
@ -419,91 +417,91 @@ final class StudyApi(
reloadSriBecauseOf(study, sri, position.chapter.id)
}
def deleteComment(userId: User.ID, studyId: Study.Id, position: Position.Ref, id: Comment.Id, sri: Sri) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
def deleteComment(studyId: Study.Id, position: Position.Ref, id: Comment.Id)(who: Who) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
chapter.deleteComment(id, position.path) match {
case Some(newChapter) =>
chapterRepo.update(newChapter) >>-
sendTo(study, StudySocket.DeleteComment(position, id, sri)) >>-
sendTo(study, StudySocket.DeleteComment(position, id, who.sri)) >>-
indexStudy(study)
case None =>
fufail(s"Invalid deleteComment $studyId $position $id") >>-
reloadSriBecauseOf(study, sri, chapter.id)
reloadSriBecauseOf(study, who.sri, chapter.id)
}
}
}
def toggleGlyph(userId: User.ID, studyId: Study.Id, position: Position.Ref, glyph: Glyph, sri: Sri) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
def toggleGlyph(studyId: Study.Id, position: Position.Ref, glyph: Glyph)(who: Who) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
chapter.toggleGlyph(glyph, position.path) match {
case Some(newChapter) =>
studyRepo.updateNow(study)
newChapter.root.nodeAt(position.path) ?? { node =>
chapterRepo.setGlyphs(node.glyphs)(newChapter, position.path) >>-
newChapter.root.nodeAt(position.path).foreach { node =>
sendTo(study, StudySocket.SetGlyphs(position, node.glyphs, sri))
sendTo(study, StudySocket.SetGlyphs(position, node.glyphs, who.sri))
}
}
case None =>
fufail(s"Invalid toggleGlyph $studyId $position $glyph") >>-
reloadSriBecauseOf(study, sri, chapter.id)
reloadSriBecauseOf(study, who.sri, chapter.id)
}
}
}
def setGamebook(userId: User.ID, studyId: Study.Id, position: Position.Ref, gamebook: Gamebook, sri: Sri) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
def setGamebook(studyId: Study.Id, position: Position.Ref, gamebook: Gamebook)(who: Who) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
chapter.setGamebook(gamebook, position.path) match {
case Some(newChapter) =>
studyRepo.updateNow(study)
chapterRepo.setGamebook(gamebook)(newChapter, position.path) >>- {
indexStudy(study)
sendStudyEnters(study, userId)
sendStudyEnters(study, who.u)
}
case None =>
fufail(s"Invalid setGamebook $studyId $position") >>-
reloadSriBecauseOf(study, sri, chapter.id)
reloadSriBecauseOf(study, who.sri, chapter.id)
}
}
}
def explorerGame(userId: User.ID, studyId: Study.Id, data: actorApi.ExplorerGame, sri: Sri) = sequenceStudyWithChapter(studyId, data.position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
if (data.insert) explorerGameHandler.insert(userId, study, Position(chapter, data.position.path), data.gameId) flatMap {
def explorerGame(studyId: Study.Id, data: actorApi.ExplorerGame)(who: Who) = sequenceStudyWithChapter(studyId, data.position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
if (data.insert) explorerGameHandler.insert(who.u, study, Position(chapter, data.position.path), data.gameId) flatMap {
case None =>
fufail(s"Invalid explorerGame insert $studyId $data") >>-
reloadSriBecauseOf(study, sri, chapter.id)
reloadSriBecauseOf(study, who.sri, chapter.id)
case Some((chapter, path)) =>
studyRepo.updateNow(study)
chapter.root.nodeAt(path) ?? { parent =>
chapterRepo.setChildren(parent.children)(chapter, path) >>- {
sendStudyEnters(study, userId)
sendStudyEnters(study, who.u)
sendTo(study, StudySocket.ReloadAll)
}
}
}
else explorerGameHandler.quote(data.gameId) flatMap {
_ ?? {
doSetComment(userId, study, Position(chapter, data.position.path), _, sri)
doSetComment(who.u, study, Position(chapter, data.position.path), _, who.sri)
}
}
}
}
def addChapter(byUserId: User.ID, studyId: Study.Id, data: ChapterMaker.Data, sticky: Boolean, sri: Sri) = sequenceStudy(studyId) { study =>
Contribute(byUserId, study) {
def addChapter(studyId: Study.Id, data: ChapterMaker.Data, sticky: Boolean)(who: Who) = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
chapterRepo.countByStudyId(study.id) flatMap { count =>
if (count >= Study.maxChapters) funit
else chapterRepo.nextOrderByStudy(study.id) flatMap { order =>
chapterMaker(study, data, order, byUserId) flatMap { chapter =>
chapterMaker(study, data, order, who.u) flatMap { chapter =>
data.initial ?? {
chapterRepo.firstByStudy(study.id) flatMap {
_.filter(_.isEmptyInitial) ?? chapterRepo.delete
}
} >> doAddChapter(study, chapter, sticky, sri)
} >> doAddChapter(study, chapter, sticky, who.sri)
} addFailureEffect {
case ChapterMaker.ValidationException(error) =>
sendTo(study, StudySocket.ValidationError(sri, error))
sendTo(study, StudySocket.ValidationError(who.sri, error))
case u => println(u)
}
}
@ -511,9 +509,9 @@ final class StudyApi(
}
}
def importPgns(byUser: User, studyId: Study.Id, datas: List[ChapterMaker.Data], sticky: Boolean, sri: Sri) =
def importPgns(studyId: Study.Id, datas: List[ChapterMaker.Data], sticky: Boolean)(who: Who) =
lila.common.Future.applySequentially(datas) { data =>
addChapter(byUser.id, studyId, data, sticky, sri)
addChapter(studyId, data, sticky)(who)
}
def doAddChapter(study: Study, chapter: Chapter, sticky: Boolean, sri: Sri) =
@ -525,8 +523,8 @@ final class StudyApi(
studyRepo.updateNow(study) >>-
indexStudy(study)
def setChapter(byUserId: User.ID, studyId: Study.Id, chapterId: Chapter.Id, sri: Sri) = sequenceStudy(studyId) { study =>
study.canContribute(byUserId) ?? doSetChapter(study, chapterId, sri)
def setChapter(studyId: Study.Id, chapterId: Chapter.Id)(who: Who) = sequenceStudy(studyId) { study =>
study.canContribute(who.u) ?? doSetChapter(study, chapterId, who.sri)
}
private def doSetChapter(study: Study, chapterId: Chapter.Id, sri: Sri) =
@ -540,8 +538,8 @@ final class StudyApi(
}
}
def editChapter(byUserId: User.ID, studyId: Study.Id, data: ChapterMaker.EditData, sri: Sri) = sequenceStudy(studyId) { study =>
Contribute(byUserId, study) {
def editChapter(studyId: Study.Id, data: ChapterMaker.EditData)(who: Who) = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
chapterRepo.byIdAndStudy(data.id, studyId) flatMap {
_ ?? { chapter =>
val name = Chapter fixName data.name
@ -573,7 +571,7 @@ final class StudyApi(
(newChapter.practice != chapter.practice) ||
(newChapter.gamebook != chapter.gamebook) ||
(newChapter.description != chapter.description)
if (shouldReload) sendTo(study, StudySocket.UpdateChapter(sri, chapter.id))
if (shouldReload) sendTo(study, StudySocket.UpdateChapter(who.sri, chapter.id))
else reloadChapters(study)
}
}
@ -582,8 +580,8 @@ final class StudyApi(
}
}
def descChapter(byUserId: User.ID, studyId: Study.Id, data: ChapterMaker.DescData, sri: Sri) = sequenceStudy(studyId) { study =>
Contribute(byUserId, study) {
def descChapter(studyId: Study.Id, data: ChapterMaker.DescData)(who: Who) = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
chapterRepo.byIdAndStudy(data.id, studyId) flatMap {
_ ?? { chapter =>
val newChapter = chapter.copy(
@ -591,7 +589,7 @@ final class StudyApi(
)
(chapter != newChapter) ?? {
chapterRepo.update(newChapter) >>- {
sendTo(study, StudySocket.DescChapter(sri, newChapter.id, newChapter.description))
sendTo(study, StudySocket.DescChapter(who.sri, newChapter.id, newChapter.description))
indexStudy(study)
}
}
@ -600,20 +598,20 @@ final class StudyApi(
}
}
def deleteChapter(byUserId: User.ID, studyId: Study.Id, chapterId: Chapter.Id, sri: Sri) = sequenceStudy(studyId) { study =>
Contribute(byUserId, study) {
def deleteChapter(studyId: Study.Id, chapterId: Chapter.Id)(who: Who) = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
chapterRepo.byIdAndStudy(chapterId, studyId) flatMap {
_ ?? { chapter =>
chapterRepo.orderedMetadataByStudy(studyId).flatMap { chaps =>
// deleting the only chapter? Automatically create an empty one
if (chaps.size < 2) {
chapterMaker(study, ChapterMaker.Data(Chapter.Name("Chapter 1")), 1, byUserId) flatMap { c =>
doAddChapter(study, c, sticky = true, sri) >> doSetChapter(study, c.id, sri)
chapterMaker(study, ChapterMaker.Data(Chapter.Name("Chapter 1")), 1, who.u) flatMap { c =>
doAddChapter(study, c, sticky = true, who.sri) >> doSetChapter(study, c.id, who.sri)
}
} // deleting the current chapter? Automatically move to another one
else (study.position.chapterId == chapterId).?? {
chaps.find(_.id != chapterId) ?? { newChap =>
doSetChapter(study, newChap.id, sri)
doSetChapter(study, newChap.id, who.sri)
}
}
} >> chapterRepo.delete(chapter.id) >>- reloadChapters(study)
@ -622,25 +620,25 @@ final class StudyApi(
}
}
def sortChapters(byUserId: User.ID, studyId: Study.Id, chapterIds: List[Chapter.Id], sri: Sri) = sequenceStudy(studyId) { study =>
Contribute(byUserId, study) {
def sortChapters(studyId: Study.Id, chapterIds: List[Chapter.Id])(who: Who) = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
chapterRepo.sort(study, chapterIds) >>- reloadChapters(study)
}
}
def descStudy(byUserId: User.ID, studyId: Study.Id, desc: String, sri: Sri) = sequenceStudy(studyId) { study =>
Contribute(byUserId, study) {
def descStudy(studyId: Study.Id, desc: String)(who: Who) = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
val newStudy = study.copy(description = desc.nonEmpty option desc)
(study != newStudy) ?? {
studyRepo.updateSomeFields(newStudy) >>-
sendTo(study, StudySocket.DescStudy(sri, newStudy.description)) >>-
sendTo(study, StudySocket.DescStudy(who.sri, newStudy.description)) >>-
indexStudy(study)
}
}
}
def editStudy(byUserId: User.ID, studyId: Study.Id, data: Study.Data) = sequenceStudy(studyId) { study =>
data.settings.ifTrue(study isOwner byUserId) ?? { settings =>
def editStudy(studyId: Study.Id, data: Study.Data)(who: Who) = sequenceStudy(studyId) { study =>
data.settings.ifTrue(study isOwner who.u) ?? { settings =>
val newStudy = study.copy(
name = Study toName data.name,
settings = settings,
@ -670,7 +668,7 @@ final class StudyApi(
lightStudyCache.put(study.id, none)
}
def like(who: Who, studyId: Study.Id, v: Boolean): Funit =
def like(studyId: Study.Id, v: Boolean)(who: Who): Funit =
studyRepo.like(studyId, who.u, v) map { likes =>
sendToNew(studyId, _.SetLiking(Study.Liking(likes, v), who))
bus.publish(actorApi.StudyLikes(studyId, likes), 'studyLikes)
@ -696,10 +694,10 @@ final class StudyApi(
}
}
def analysisRequest(studyId: Study.Id, chapterId: Chapter.Id, userId: User.ID): Funit =
def analysisRequest(studyId: Study.Id, chapterId: Chapter.Id)(who: Who): Funit =
sequenceStudyWithChapter(studyId, chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
serverEvalRequester(study, chapter, userId)
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
serverEvalRequester(study, chapter, who.u)
}
}

View File

@ -6,9 +6,12 @@ import scala.concurrent.duration._
import scala.concurrent.Promise
import actorApi.Who
import chess.format.pgn.Glyph
import lila.room.RoomSocket.{ Protocol => RP, _ }
import lila.socket.RemoteSocket.{ Protocol => P, _ }
import lila.socket.Socket.{ Sri, makeMessage }
import lila.socket.{ AnaMove, AnaDrop, AnaAny }
import lila.tree.Node.{ Shape, Shapes, Comment, Gamebook }
import lila.user.User
private final class StudyRemoteSocket(
@ -19,7 +22,18 @@ private final class StudyRemoteSocket(
system: ActorSystem
) {
import StudyRemoteSocket._
implicit def roomIdToStudyId(roomId: RoomId) = Study.Id(roomId.value)
implicit def studyIdToRoomId(studyId: Study.Id) = RoomId(studyId.value)
// private val isPresent = ask[IsPresent](Protocol.In.isPresentReader)
// def isPresent(studyId: Study.Id, userId: User.ID): Fu[Boolean] = {
// val promise = isPresent(studyId, userId)
// send(Protocol.Out.isPresent(studyId, userId))
// promise.future
// }
lazy val rooms = makeRoomMap(send, system.lilaBus)
@ -27,22 +41,130 @@ private final class StudyRemoteSocket(
roomId => _.Study(roomId.value).some)
private lazy val studyHandler: Handler = {
case m @ RP.In.TellRoomSri(roomId, P.In.TellSri(sri, user, tpe, msg)) if messagesHandled(tpe) =>
case Protocol.In.TellStudySri(studyId, P.In.TellSri(sri, user, tpe, o)) =>
import Protocol.In.Data._
def who = user map { Who(_, sri) }
(tpe -> msg) match {
case ("setPath", o) => reading[AtPosition](o) { position =>
who foreach { api.setPath(_, roomId, position.ref) }
tpe match {
case "setPath" => reading[AtPosition](o) { position =>
who foreach api.setPath(studyId, position.ref)
}
case ("like", o) => for {
w <- who
v <- (o \ "d" \ "liked").asOpt[Boolean]
} api.like(w, roomId, v)
case "like" => (o \ "d" \ "liked").asOpt[Boolean] foreach { v =>
who foreach api.like(studyId, v)
}
case "anaMove" => AnaMove parse o foreach { move =>
who foreach moveOrDrop(studyId, move, MoveOpts parse o)
}
case "anaDrop" => AnaDrop parse o foreach { drop =>
who foreach moveOrDrop(studyId, drop, MoveOpts parse o)
}
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)
}
}
case "promote" => reading[AtPosition](o) { position =>
(o \ "d" \ "toMainline").asOpt[Boolean] foreach { toMainline =>
who foreach api.promote(studyId, position.ref, toMainline)
}
}
case "forceVariation" => reading[AtPosition](o) { position =>
(o \ "d" \ "force").asOpt[Boolean] foreach { force =>
who foreach api.forceVariation(studyId, position.ref, force)
}
}
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 =>
import JsonView.shapeReader
(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)
}
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)
}
}
case "explorerGame" => reading[actorApi.ExplorerGame](o) { data =>
who foreach api.explorerGame(studyId, data)
}
case "requestAnalysis" => o.get[Chapter.Id]("d") foreach { chapterId =>
who foreach api.analysisRequest(studyId, chapterId)
}
case t => logger.warn(s"Unhandled study socket message: $t")
}
}
private val messagesHandled: Set[String] =
Set("like", "setPath")
private def moveOrDrop(studyId: Study.Id, m: AnaAny, opts: MoveOpts)(who: Who) = m.branch match {
case scalaz.Success(branch) if branch.ply < Node.MAX_PLIES =>
send(P.Out.tellSri(who.sri, makeMessage("node", m json branch)))
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 scalaz.Success(branch) =>
send(P.Out.tellSri(who.sri, makeMessage("stepFailure", s"ply ${branch.ply}/${Node.MAX_PLIES}")))
case scalaz.Failure(err) =>
send(P.Out.tellSri(who.sri, makeMessage("stepFailure", err.toString)))
}
private lazy val send: String => Unit = remoteSocketApi.makeSender("study-out").apply _
@ -56,30 +178,39 @@ private final class StudyRemoteSocket(
import JsonView._
import jsonView.membersWrites
import lila.tree.Node.{ openingWriter, commentWriter, glyphsWriter, shapesWrites, clockWrites }
def version(tpe: String, data: JsObject) = rooms.tell(studyId.value, NotifyVersion(tpe, data))
def notify(tpe: String, data: JsObject) = rooms.tell(studyId.value, makeMessage(tpe, data))
msg match {
case SetPath(pos, who) => tellRoom(studyId, NotifyVersion("path", Json.obj(
"p" -> pos,
"w" -> who
)))
case SetLiking(liking, who) => tellRoom(studyId, makeMessage("liking", Json.obj(
"l" -> liking,
"w" -> who
)))
case SetPath(pos, who) => version("path", Json.obj("p" -> pos, "w" -> who))
case SetLiking(liking, who) => notify("liking", Json.obj("l" -> liking, "w" -> who))
}
}
private def tellRoom(studyId: Study.Id, msg: Any): Unit =
rooms.tell(studyId.value, msg)
private val InviteLimitPerUser = new lila.memo.RateLimit[User.ID](
credits = 50,
duration = 24 hour,
name = "study invites per user",
key = "study_invite.user"
)
}
object StudyRemoteSocket {
object Protocol {
object In {
case class TellStudySri(studyId: Study.Id, tellSri: P.In.TellSri) extends P.In
val reader: P.In.Reader = raw => RP.In.reader(raw)
// val studyReader: P.In.Reader = raw => raw.path match {
// case _ => none
// }
val studyReader: P.In.Reader = raw => raw.path match {
case "tell/study/sri" => raw.get(4) {
case arr @ Array(studyId, _, _, _) => P.In.tellSriMapper.lift(arr drop 1).flatten map {
TellStudySri(Study.Id(studyId), _)
}
}
}
object Data {
import lila.common.PimpedJson._
@ -91,30 +222,34 @@ private final class StudyRemoteSocket(
case class AtPosition(path: String, chapterId: Chapter.Id) {
def ref = Position.Ref(chapterId, Path(path))
}
implicit val chapterIdReader = stringIsoReader(Chapter.idIso)
implicit val chapterNameReader = stringIsoReader(Chapter.nameIso)
implicit val chapterIdReader: Reads[Chapter.Id] = stringIsoReader(Chapter.idIso)
implicit val chapterNameReader: Reads[Chapter.Name] = stringIsoReader(Chapter.nameIso)
implicit val atPositionReader: Reads[AtPosition] = (
(__ \ "path").read[String] and
(__ \ "ch").read[Chapter.Id]
)(AtPosition.apply _)
case class SetRole(userId: String, role: String)
implicit val SetRoleReader = Json.reads[SetRole]
implicit val ChapterDataReader = Json.reads[ChapterMaker.Data]
implicit val ChapterEditDataReader = Json.reads[ChapterMaker.EditData]
implicit val ChapterDescDataReader = Json.reads[ChapterMaker.DescData]
implicit val StudyDataReader = Json.reads[Study.Data]
implicit val setTagReader = Json.reads[actorApi.SetTag]
// implicit val gamebookReader = Json.reads[Gamebook]
implicit val explorerGame = Json.reads[actorApi.ExplorerGame]
implicit val SetRoleReader: Reads[SetRole] = Json.reads[SetRole]
implicit val ChapterDataReader: Reads[ChapterMaker.Data] = Json.reads[ChapterMaker.Data]
implicit val ChapterEditDataReader: Reads[ChapterMaker.EditData] = Json.reads[ChapterMaker.EditData]
implicit val ChapterDescDataReader: Reads[ChapterMaker.DescData] = Json.reads[ChapterMaker.DescData]
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]
}
}
object Out {
}
}
}
// case class IsPresent(roomId: RoomId, userId: User.ID, present: Boolean) extends P.In
object StudyRemoteSocket {
// val isPresentReader: P.In.Reader = raw => raw.path match {
// case "room/present" => raw.args.split(" ", 3) match {
// case Array(roomId, userId, present) => IsPresent(RoomId(roomId), userId, present).some
// case _ => none
// }
// case _ => none
// }
}
sealed trait Out