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

747 lines
30 KiB
Scala
Raw Normal View History

2016-02-26 05:08:11 -07:00
package lila.study
2018-12-07 05:18:57 -07:00
import akka.actor.ActorSelection
import scala.concurrent.duration._
2016-02-26 05:08:11 -07:00
2019-10-24 11:37:19 -06:00
import actorApi.Who
2017-07-01 08:44:24 -06:00
import chess.Centis
import chess.format.pgn.{ Tags, Glyph }
import lila.chat.Chat
2016-02-26 05:08:11 -07:00
import lila.hub.actorApi.map.Tell
2016-05-31 18:14:02 -06:00
import lila.hub.actorApi.timeline.{ Propagate, StudyCreate, StudyLike }
2019-07-13 12:02:50 -06:00
import lila.socket.Socket.Sri
import lila.tree.Eval
import lila.tree.Node.{ Shapes, Comment, Gamebook }
2017-10-21 14:01:50 -06:00
import lila.user.User
2016-02-26 05:08:11 -07:00
final class StudyApi(
studyRepo: StudyRepo,
chapterRepo: ChapterRepo,
sequencer: StudySequencer,
2016-05-22 08:50:55 -06:00
studyMaker: StudyMaker,
2016-04-25 03:09:26 -06:00
chapterMaker: ChapterMaker,
2017-05-06 04:30:58 -06:00
inviter: StudyInvite,
2017-09-19 07:29:43 -06:00
explorerGameHandler: ExplorerGame,
lightUser: lila.common.LightUser.GetterSync,
scheduler: akka.actor.Scheduler,
chat: ActorSelection,
bus: lila.common.Bus,
timeline: ActorSelection,
2018-12-07 05:18:57 -07:00
socketMap: SocketMap,
serverEvalRequester: ServerEval.Requester,
lightStudyCache: LightStudyCache
) {
2016-02-26 05:08:11 -07:00
import sequencer._
def byId = studyRepo byId _
2016-02-27 04:30:38 -07:00
def byIds = studyRepo byOrderedIds _
2017-07-21 04:51:56 -06:00
def publicIdNames = studyRepo publicIdNames _
2017-07-21 04:49:54 -06:00
def publicByIds(ids: Seq[Study.Id]) = byIds(ids) map { _.filter(_.isPublic) }
def byIdAndOwner(id: Study.Id, owner: User) = byId(id) map {
_.filter(_ isOwner owner.id)
}
def isOwner(id: Study.Id, owner: User) = byIdAndOwner(id, owner).map(_.isDefined)
def byIdWithChapter(id: Study.Id): Fu[Option[Study.WithChapter]] = byId(id) flatMap {
_ ?? { study =>
2019-08-20 02:34:05 -06:00
chapterRepo byId study.position.chapterId flatMap {
case None => chapterRepo firstByStudy study.id flatMap {
2016-07-26 03:47:15 -06:00
case None => fuccess(none)
case Some(chapter) =>
val fixed = study withChapter chapter
2019-08-20 02:34:05 -06:00
studyRepo updateSomeFields fixed inject
2016-07-26 03:47:15 -06:00
Study.WithChapter(fixed, chapter).some
}
case Some(chapter) => fuccess(Study.WithChapter(study, chapter).some)
}
}
}
2017-01-20 06:00:42 -07:00
def byIdWithChapter(id: Study.Id, chapterId: Chapter.Id): Fu[Option[Study.WithChapter]] = byId(id) flatMap {
2016-04-26 02:49:39 -06:00
_ ?? { study =>
2019-08-20 02:34:05 -06:00
chapterRepo byId chapterId map {
2016-10-08 12:49:51 -06:00
_.filter(_.studyId == study.id) map { Study.WithChapter(study, _) }
2016-04-26 02:49:39 -06:00
}
}
}
def byIdWithFirstChapter(id: Study.Id): Fu[Option[Study.WithChapter]] = byId(id) flatMap {
_ ?? { study =>
chapterRepo.firstByStudy(study.id) map {
_ ?? { Study.WithChapter(study, _).some }
}
}
}
2017-07-18 15:18:12 -06:00
def studyIdOf = chapterRepo.studyIdOf _
def members(id: Study.Id): Fu[Option[StudyMembers]] = studyRepo membersById id
2018-04-26 09:56:14 -06:00
def importGame(data: StudyMaker.ImportGame, user: User): Fu[Option[Study.WithChapter]] = (data.form.as match {
case DataForm.importGame.AsNewStudy =>
2017-09-29 12:33:37 -06:00
studyMaker(data, user) flatMap { res =>
studyRepo.insert(res.study) >>
chapterRepo.insert(res.chapter) >>-
indexStudy(res.study) >>-
scheduleTimeline(res.study.id) inject res.some
}
2018-04-26 09:56:14 -06:00
case DataForm.importGame.AsChapterOf(studyId) => byId(studyId) flatMap {
case Some(study) if study.canContribute(user.id) =>
import akka.pattern.ask
import makeTimeout.short
2018-12-07 05:18:57 -07:00
addChapter(
studyId = study.id,
data = data.form.toChapterData,
2019-10-24 11:37:19 -06:00
sticky = study.settings.sticky
)(Who(user.id, Sri(""))) >> byIdWithChapter(studyId)
case _ => fuccess(none)
2018-04-26 09:56:14 -06:00
} orElse importGame(data.copy(form = data.form.copy(asStr = none)), user)
2017-07-21 04:49:54 -06:00
}) addEffect {
_ ?? { sc =>
bus.publish(actorApi.StartStudy(sc.study.id), 'startStudy)
}
}
2016-04-16 07:26:01 -06:00
2016-10-12 07:37:40 -06:00
def clone(me: User, prev: Study): Fu[Option[Study]] =
Settings.UserSelection.allows(prev.settings.cloneable, prev, me.id.some) ?? {
chapterRepo.orderedByStudy(prev.id).flatMap { chapters =>
val study1 = prev.cloneFor(me)
val newChapters = chapters.map(_ cloneFor study1)
2016-10-21 08:49:28 -06:00
newChapters.headOption.map(study1.rewindTo) ?? { study =>
2016-10-12 07:37:40 -06:00
studyRepo.insert(study) >>
newChapters.map(chapterRepo.insert).sequenceFu >>- {
chat ! lila.chat.actorApi.SystemTalk(
2017-08-17 14:25:50 -06:00
Chat.Id(study.id.value),
s"Cloned from lichess.org/study/${prev.id}"
)
2016-10-12 07:37:40 -06:00
} inject study.some
}
}
2016-08-31 10:20:25 -06:00
}
2016-08-31 10:00:45 -06:00
2017-06-09 18:47:38 -06:00
def resetIfOld(study: Study, chapters: List[Chapter.Metadata]): Fu[(Study, Option[Chapter])] =
chapters.headOption match {
case Some(c) if study.isOld && study.position != c.initialPosition =>
2016-10-21 08:49:28 -06:00
val newStudy = study rewindTo c
2017-06-09 18:47:38 -06:00
studyRepo.updateSomeFields(newStudy) zip chapterRepo.byId(c.id) map {
case (_, chapter) => newStudy -> chapter
}
case _ => fuccess(study -> none)
}
private def scheduleTimeline(studyId: Study.Id) = scheduler.scheduleOnce(1 minute) {
byId(studyId) foreach {
_.filter(_.isPublic) foreach { study =>
2017-01-20 06:05:09 -07:00
timeline ! (Propagate(StudyCreate(study.ownerId, study.id.value, study.name.value)) toFollowersOf study.ownerId)
}
}
}
2018-12-07 05:18:57 -07:00
def talk(userId: User.ID, studyId: Study.Id, text: String) = byId(studyId) foreach {
2016-05-10 23:42:03 -06:00
_ foreach { study =>
2016-06-13 05:29:00 -06:00
(study canChat userId) ?? {
chat ! lila.chat.actorApi.UserTalk(
Chat.Id(studyId.value),
userId = userId,
text = text,
publicSource = lila.hub.actorApi.shutup.PublicSource.Study(studyId.value).some
)
2016-05-10 23:42:03 -06:00
}
}
}
2019-10-24 11:37:19 -06:00
def setPath(studyId: Study.Id, position: Position.Ref)(who: Who): Funit = sequenceStudy(studyId) { study =>
2019-10-23 09:31:31 -06:00
Contribute(who.u, study) {
2016-05-18 08:15:02 -06:00
chapterRepo.byId(position.chapterId).map {
_ filter { c =>
c.root.pathExists(position.path) && study.position.chapterId == c.id
2016-04-22 23:52:09 -06:00
}
2016-05-18 08:15:02 -06:00
} flatMap {
2019-10-23 09:31:31 -06:00
case None => funit >>- reloadSri(study, who.sri)
2016-05-18 08:15:02 -06:00
case Some(chapter) if study.position.path != position.path =>
studyRepo.setPosition(study.id, position) >>
updateConceal(study, chapter, position) >>-
2019-10-23 09:31:31 -06:00
sendToNew(study.id, _.SetPath(position, who))
2016-05-18 08:15:02 -06:00
case _ => funit
2016-04-22 23:52:09 -06:00
}
2016-04-20 22:42:49 -06:00
}
}
2016-02-27 04:30:38 -07:00
2019-10-24 11:37:19 -06:00
def addNode(studyId: Study.Id, position: Position.Ref, node: Node, opts: MoveOpts, relay: Option[Chapter.Relay] = None)(who: Who) =
sequenceStudyWithChapter(studyId, position.chapterId) {
2019-10-24 11:37:19 -06:00
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
doAddNode(study, Position(chapter, position.path), node, opts, relay)(who).void
}
2016-04-18 04:51:48 -06:00
}
2016-02-28 17:28:35 -07:00
2019-10-24 11:37:19 -06:00
private def doAddNode(study: Study, position: Position, rawNode: Node, opts: MoveOpts, relay: Option[Chapter.Relay])(who: Who): Funit = {
2017-10-05 11:28:27 -06:00
val node = rawNode.withoutChildren
2019-10-24 11:37:19 -06:00
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)
} else position.chapter.addNode(node, position.path, relay) match {
2017-09-20 13:25:05 -06:00
case None =>
failReload
fufail(s"Invalid addNode ${study.id} ${position.ref} $node")
2017-09-20 13:25:05 -06:00
case Some(chapter) =>
chapter.root.nodeAt(position.path) ?? { parent =>
val newPosition = position.ref + node
2019-09-26 13:31:27 -06:00
chapterRepo.setChildren(parent.children)(chapter, position.path) >>
(relay ?? { chapterRepo.setRelay(chapter.id, _) }) >>
2017-09-20 13:25:05 -06:00
(opts.sticky ?? studyRepo.setPosition(study.id, newPosition)) >>
updateConceal(study, chapter, newPosition) >>- {
2018-12-07 05:18:57 -07:00
sendTo(study, StudySocket.AddNode(
position.ref,
node,
chapter.setup.variant,
2019-10-24 11:37:19 -06:00
who.sri,
sticky = opts.sticky,
relay = relay
))
2019-10-24 11:37:19 -06:00
sendStudyEnters(study, who.u)
2017-09-20 13:25:05 -06:00
if (opts.promoteToMainline && !Path.isMainline(chapter.root, newPosition.path))
2019-10-24 11:37:19 -06:00
promote(study.id, position.ref + node, toMainline = true)(who)
}
2017-09-20 13:25:05 -06:00
}
}
}
2017-09-20 13:25:05 -06:00
private def updateConceal(study: Study, chapter: Chapter, position: Position.Ref) =
chapter.conceal ?? { conceal =>
chapter.root.lastMainlinePlyOf(position.path).some.filter(_ > conceal) ?? { newConceal =>
if (newConceal >= chapter.root.lastMainlinePly)
chapterRepo.removeConceal(chapter.id) >>-
2018-12-07 05:18:57 -07:00
sendTo(study, StudySocket.SetConceal(position, none))
else
chapterRepo.setConceal(chapter.id, newConceal) >>-
2018-12-07 05:18:57 -07:00
sendTo(study, StudySocket.SetConceal(position, newConceal.some))
}
}
2019-10-24 11:37:19 -06:00
def deleteNodeAt(studyId: Study.Id, position: Position.Ref)(who: Who) = sequenceStudyWithChapter(studyId, position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
2016-04-27 01:54:56 -06:00
chapter.updateRoot { root =>
root.withChildren(_.deleteNodeAt(position.path))
} match {
case Some(newChapter) =>
chapterRepo.update(newChapter) >>-
2019-10-24 11:37:19 -06:00
sendTo(study, StudySocket.DeleteNode(position, who.sri))
case None =>
fufail(s"Invalid delNode $studyId $position") >>-
2019-10-24 11:37:19 -06:00
reloadSriBecauseOf(study, who.sri, chapter.id)
2016-04-27 01:54:56 -06:00
}
}
}
2019-10-24 11:37:19 -06:00
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)
2019-10-24 11:37:19 -06:00
} | chapter) >>- sendTo(study, StudySocket.UpdateChapter(who.sri, chapter.id))
}
}
2019-10-24 11:37:19 -06:00
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) {
2016-04-27 22:43:50 -06:00
chapter.updateRoot { root =>
root.withChildren { children =>
if (toMainline) children.promoteToMainlineAt(position.path)
else children.promoteUpAt(position.path).map(_._1)
}
2016-04-27 22:43:50 -06:00
} match {
case Some(newChapter) =>
chapterRepo.update(newChapter) >>-
2019-10-24 11:37:19 -06:00
sendTo(study, StudySocket.Promote(position, toMainline, who.sri)) >>
newChapter.root.children.nodesOn {
newChapter.root.mainlinePath.intersect(position.path)
}.collect {
case (node, path) if node.forceVariation =>
2019-07-13 12:02:50 -06:00
doForceVariation(Study.WithChapter(study, newChapter), path, false, Sri(""))
}.sequenceFu.void
case None =>
fufail(s"Invalid promoteToMainline $studyId $position") >>-
2019-10-24 11:37:19 -06:00
reloadSriBecauseOf(study, who.sri, chapter.id)
2016-04-27 22:43:50 -06:00
}
}
}
2019-10-24 11:37:19 -06:00
def forceVariation(studyId: Study.Id, position: Position.Ref, force: Boolean)(who: Who): Funit =
sequenceStudyWithChapter(studyId, position.chapterId) { sc =>
2019-10-24 11:37:19 -06:00
Contribute(who.u, sc.study) {
doForceVariation(sc, position.path, force, who.sri)
}
}
2019-07-13 12:02:50 -06:00
private def doForceVariation(sc: Study.WithChapter, path: Path, force: Boolean, sri: Sri): Funit =
sc.chapter.forceVariation(force, path) match {
case Some(newChapter) =>
2019-09-26 13:31:27 -06:00
chapterRepo.forceVariation(force)(newChapter, path) >>-
2019-07-13 12:02:50 -06:00
sendTo(sc.study, StudySocket.ForceVariation(Position(newChapter, path).ref, force, sri))
case None =>
fufail(s"Invalid forceVariation ${Position(sc.chapter, path)} $force") >>-
2019-07-13 12:02:50 -06:00
reloadSriBecauseOf(sc.study, sri, sc.chapter.id)
}
2019-10-24 11:37:19 -06:00
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 =>
2017-02-14 06:41:32 -07:00
if (!member.role.canWrite && role.canWrite)
bus.publish(lila.hub.actorApi.study.StudyMemberGotWriteAccess(userId, studyId.value), 'study)
2017-02-14 06:41:32 -07:00
else if (member.role.canWrite && !role.canWrite)
bus.publish(lila.hub.actorApi.study.StudyMemberLostWriteAccess(userId, studyId.value), 'study)
}
2017-02-14 06:02:42 -07:00
studyRepo.setRole(study, userId, role) >>-
onMembersChange(study)
}
}
2018-12-07 05:18:57 -07:00
def invite(byUserId: User.ID, studyId: Study.Id, username: String, socket: StudySocket, onError: String => Unit) = sequenceStudy(studyId) { study =>
inviter(byUserId, study, username, socket).addEffects(
err => onError(err.getMessage),
_ => onMembersChange(study)
)
}
2019-10-24 11:37:19 -06:00
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)
} >>- onMembersChange(study)
}
2017-06-10 11:48:04 -06:00
def isContributor = studyRepo.isContributor _
2018-08-12 11:21:55 -06:00
def isMember = studyRepo.isMember _
2017-06-10 11:48:04 -06:00
private def onMembersChange(study: Study) = {
lightStudyCache.refresh(study.id)
2017-06-10 12:21:56 -06:00
studyRepo.membersById(study.id).foreach {
_ foreach { members =>
2018-12-07 05:18:57 -07:00
sendTo(study, StudySocket.ReloadMembers(members))
2017-06-10 12:21:56 -06:00
}
}
indexStudy(study)
2016-04-20 22:42:49 -06:00
}
2019-10-24 11:37:19 -06:00
def setShapes(studyId: Study.Id, position: Position.Ref, shapes: Shapes)(who: Who) = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
2016-04-25 21:51:05 -06:00
chapterRepo.byIdAndStudy(position.chapterId, study.id) flatMap {
_ ?? { chapter =>
2016-04-25 22:28:35 -06:00
chapter.setShapes(shapes, position.path) match {
2016-04-25 21:51:05 -06:00
case Some(newChapter) =>
2016-05-27 04:04:16 -06:00
studyRepo.updateNow(study)
2019-09-26 13:31:27 -06:00
chapterRepo.setShapes(shapes)(newChapter, position.path) >>-
2019-10-24 11:37:19 -06:00
sendTo(study, StudySocket.SetShapes(position, shapes, who.sri))
case None =>
fufail(s"Invalid setShapes $position $shapes") >>-
2019-10-24 11:37:19 -06:00
reloadSriBecauseOf(study, who.sri, chapter.id)
2016-04-25 21:51:05 -06:00
}
}
}
}
2016-04-21 20:53:16 -06:00
}
2019-07-13 12:02:50 -06:00
def setClock(studyId: Study.Id, position: Position.Ref, clock: Option[Centis], sri: Sri): Funit =
2017-10-09 12:36:36 -06:00
sequenceStudyWithChapter(studyId, position.chapterId) { sc =>
sc.chapter.setClock(clock, position.path) match {
case Some(newChapter) =>
studyRepo.updateNow(sc.study)
2019-09-26 13:31:27 -06:00
chapterRepo.setClock(clock)(newChapter, position.path) >>-
2019-07-13 12:02:50 -06:00
sendTo(sc.study, StudySocket.SetClock(position, clock, sri))
2017-10-09 12:36:36 -06:00
case None =>
fufail(s"Invalid setClock $position $clock") >>-
2019-07-13 12:02:50 -06:00
reloadSriBecauseOf(sc.study, sri, position.chapterId)
2017-10-09 12:36:36 -06:00
}
}
2019-10-24 11:37:19 -06:00
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)
2016-12-21 11:18:38 -07:00
}
}
2019-10-24 11:37:19 -06:00
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)
}
}
2019-07-13 12:02:50 -06:00
private def doSetTags(study: Study, oldChapter: Chapter, tags: Tags, sri: Sri): Funit = {
val chapter = oldChapter.copy(tags = tags)
(chapter.tags != oldChapter.tags) ?? {
chapterRepo.setTagsFor(chapter) >> {
PgnTags.setRootClockFromTags(chapter) ?? { c =>
2019-07-13 12:02:50 -06:00
setClock(study.id, Position(c, Path.root).ref, c.root.clock, sri)
}
} >>-
2019-07-13 12:02:50 -06:00
sendTo(study, StudySocket.SetTags(chapter.id, chapter.tags, sri))
} >>- indexStudy(study)
}
2019-10-24 11:37:19 -06:00
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 =>
2017-09-19 07:29:43 -06:00
val comment = Comment(
id = Comment.Id.make,
text = text,
by = Comment.Author.User(author.id, author.titleName)
)
2019-10-24 11:37:19 -06:00
doSetComment(who.u, study, Position(chapter, position.path), comment, who.sri)
}
}
}
2019-07-13 12:02:50 -06:00
private def doSetComment(userId: User.ID, study: Study, position: Position, comment: Comment, sri: Sri): Funit =
2017-09-19 10:22:42 -06:00
position.chapter.setComment(comment, position.path) match {
case Some(newChapter) =>
studyRepo.updateNow(study)
newChapter.root.nodeAt(position.path) ?? { node =>
node.comments.findBy(comment.by) ?? { c =>
2019-09-26 13:31:27 -06:00
chapterRepo.setComments(node.comments.filterEmpty)(newChapter, position.path) >>- {
2019-07-13 12:02:50 -06:00
sendTo(study, StudySocket.SetComment(position.ref, c, sri))
2017-09-19 10:22:42 -06:00
indexStudy(study)
sendStudyEnters(study, userId)
}
}
}
case None =>
fufail(s"Invalid setComment ${study.id} $position") >>-
2019-07-13 12:02:50 -06:00
reloadSriBecauseOf(study, sri, position.chapter.id)
2017-09-19 10:22:42 -06:00
}
2019-10-24 11:37:19 -06:00
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) {
2017-09-19 07:29:43 -06:00
chapter.deleteComment(id, position.path) match {
case Some(newChapter) =>
chapterRepo.update(newChapter) >>-
2019-10-24 11:37:19 -06:00
sendTo(study, StudySocket.DeleteComment(position, id, who.sri)) >>-
2017-09-19 07:29:43 -06:00
indexStudy(study)
case None =>
fufail(s"Invalid deleteComment $studyId $position $id") >>-
2019-10-24 11:37:19 -06:00
reloadSriBecauseOf(study, who.sri, chapter.id)
2016-05-17 08:23:09 -06:00
}
}
}
2019-10-24 11:37:19 -06:00
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) {
2017-09-19 07:29:43 -06:00
chapter.toggleGlyph(glyph, position.path) match {
case Some(newChapter) =>
studyRepo.updateNow(study)
newChapter.root.nodeAt(position.path) ?? { node =>
2019-09-26 13:31:27 -06:00
chapterRepo.setGlyphs(node.glyphs)(newChapter, position.path) >>-
2017-09-19 07:29:43 -06:00
newChapter.root.nodeAt(position.path).foreach { node =>
2019-10-24 11:37:19 -06:00
sendTo(study, StudySocket.SetGlyphs(position, node.glyphs, who.sri))
2017-09-19 07:29:43 -06:00
}
}
case None =>
fufail(s"Invalid toggleGlyph $studyId $position $glyph") >>-
2019-10-24 11:37:19 -06:00
reloadSriBecauseOf(study, who.sri, chapter.id)
2016-04-28 00:18:45 -06:00
}
}
}
2019-10-24 11:37:19 -06:00
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) {
2017-08-15 10:14:12 -06:00
chapter.setGamebook(gamebook, position.path) match {
2017-08-14 18:44:04 -06:00
case Some(newChapter) =>
studyRepo.updateNow(study)
2019-09-26 13:31:27 -06:00
chapterRepo.setGamebook(gamebook)(newChapter, position.path) >>- {
2017-08-14 18:44:04 -06:00
indexStudy(study)
2019-10-24 11:37:19 -06:00
sendStudyEnters(study, who.u)
2017-08-14 18:44:04 -06:00
}
case None =>
fufail(s"Invalid setGamebook $studyId $position") >>-
2019-10-24 11:37:19 -06:00
reloadSriBecauseOf(study, who.sri, chapter.id)
2017-08-14 18:44:04 -06:00
}
}
}
2019-10-24 11:37:19 -06:00
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") >>-
2019-10-24 11:37:19 -06:00
reloadSriBecauseOf(study, who.sri, chapter.id)
case Some((chapter, path)) =>
studyRepo.updateNow(study)
chapter.root.nodeAt(path) ?? { parent =>
2019-09-26 13:31:27 -06:00
chapterRepo.setChildren(parent.children)(chapter, path) >>- {
2019-10-24 11:37:19 -06:00
sendStudyEnters(study, who.u)
2018-12-07 05:18:57 -07:00
sendTo(study, StudySocket.ReloadAll)
}
}
}
2017-09-19 10:22:42 -06:00
else explorerGameHandler.quote(data.gameId) flatMap {
_ ?? {
2019-10-24 11:37:19 -06:00
doSetComment(who.u, study, Position(chapter, data.position.path), _, who.sri)
2017-09-19 07:29:43 -06:00
}
}
}
}
2019-10-24 11:37:19 -06:00
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 =>
2019-10-24 11:37:19 -06:00
chapterMaker(study, data, order, who.u) flatMap { chapter =>
data.initial ?? {
chapterRepo.firstByStudy(study.id) flatMap {
_.filter(_.isEmptyInitial) ?? chapterRepo.delete
}
2019-10-24 11:37:19 -06:00
} >> doAddChapter(study, chapter, sticky, who.sri)
2019-08-25 02:14:37 -06:00
} addFailureEffect {
case ChapterMaker.ValidationException(error) =>
2019-10-24 11:37:19 -06:00
sendTo(study, StudySocket.ValidationError(who.sri, error))
2019-08-25 02:14:37 -06:00
case u => println(u)
2016-04-25 03:09:26 -06:00
}
}
2016-04-24 03:15:18 -06:00
}
}
}
2019-10-24 11:37:19 -06:00
def importPgns(studyId: Study.Id, datas: List[ChapterMaker.Data], sticky: Boolean)(who: Who) =
2018-04-26 09:56:14 -06:00
lila.common.Future.applySequentially(datas) { data =>
2019-10-24 11:37:19 -06:00
addChapter(studyId, data, sticky)(who)
2018-04-26 09:56:14 -06:00
}
2019-07-13 12:02:50 -06:00
def doAddChapter(study: Study, chapter: Chapter, sticky: Boolean, sri: Sri) =
2017-09-20 13:25:05 -06:00
chapterRepo.insert(chapter) >> {
val newStudy = study withChapter chapter
(sticky ?? studyRepo.updateSomeFields(newStudy)) >>-
2019-07-13 12:02:50 -06:00
sendTo(study, StudySocket.AddChapter(sri, newStudy.position, sticky))
2017-09-20 13:25:05 -06:00
} >>-
studyRepo.updateNow(study) >>-
indexStudy(study)
2019-10-24 11:37:19 -06:00
def setChapter(studyId: Study.Id, chapterId: Chapter.Id)(who: Who) = sequenceStudy(studyId) { study =>
study.canContribute(who.u) ?? doSetChapter(study, chapterId, who.sri)
2016-04-25 03:09:26 -06:00
}
2019-07-13 12:02:50 -06:00
private def doSetChapter(study: Study, chapterId: Chapter.Id, sri: Sri) =
2016-07-26 03:47:15 -06:00
(study.position.chapterId != chapterId) ?? {
2016-04-25 03:09:26 -06:00
chapterRepo.byIdAndStudy(chapterId, study.id) flatMap {
2016-04-24 04:45:34 -06:00
_ ?? { chapter =>
2017-06-14 00:50:51 -06:00
val newStudy = study withChapter chapter
studyRepo.updateSomeFields(newStudy) >>-
2019-07-13 12:02:50 -06:00
sendTo(study, StudySocket.ChangeChapter(sri, newStudy.position))
}
}
}
2019-10-24 11:37:19 -06:00
def editChapter(studyId: Study.Id, data: ChapterMaker.EditData)(who: Who) = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
2016-05-19 09:27:32 -06:00
chapterRepo.byIdAndStudy(data.id, studyId) flatMap {
2016-05-16 04:01:03 -06:00
_ ?? { chapter =>
2017-01-20 06:05:09 -07:00
val name = Chapter fixName data.name
2016-05-19 09:27:32 -06:00
val newChapter = chapter.copy(
name = name,
2017-01-18 09:49:46 -07:00
practice = data.isPractice option true,
2017-08-10 20:05:03 -06:00
gamebook = data.isGamebook option true,
2017-01-18 09:49:46 -07:00
conceal = (chapter.conceal, data.isConceal) match {
case (None, true) => Chapter.Ply(chapter.root.ply).some
2016-05-19 09:27:32 -06:00
case (Some(_), false) => None
case _ => chapter.conceal
},
2017-08-19 17:05:41 -06:00
setup = chapter.setup.copy(orientation = data.realOrientation),
description = data.hasDescription option {
chapter.description | "-"
}
)
2016-05-19 09:27:32 -06:00
if (chapter == newChapter) funit
else chapterRepo.update(newChapter) >> {
if (chapter.conceal != newChapter.conceal) {
(newChapter.conceal.isDefined && study.position.chapterId == chapter.id).?? {
val newPosition = study.position.withPath(Path.root)
studyRepo.setPosition(study.id, newPosition)
} >>-
2018-12-07 05:18:57 -07:00
sendTo(study, StudySocket.ReloadAll)
2017-08-23 17:56:39 -06:00
} else fuccess {
val shouldReload =
(newChapter.setup.orientation != chapter.setup.orientation) ||
2017-08-15 14:28:16 -06:00
(newChapter.practice != chapter.practice) ||
(newChapter.gamebook != chapter.gamebook) ||
(newChapter.description != chapter.description)
2019-10-24 11:37:19 -06:00
if (shouldReload) sendTo(study, StudySocket.UpdateChapter(who.sri, chapter.id))
2017-08-22 16:45:35 -06:00
else reloadChapters(study)
2016-05-19 09:27:32 -06:00
}
2016-05-16 04:01:03 -06:00
}
2016-07-25 14:34:15 -06:00
} >>- indexStudy(study)
2016-04-24 04:45:34 -06:00
}
}
}
2019-10-24 11:37:19 -06:00
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(
description = data.desc.nonEmpty option data.desc
)
(chapter != newChapter) ?? {
chapterRepo.update(newChapter) >>- {
2019-10-24 11:37:19 -06:00
sendTo(study, StudySocket.DescChapter(who.sri, newChapter.id, newChapter.description))
indexStudy(study)
}
}
}
}
}
}
2019-10-24 11:37:19 -06:00
def deleteChapter(studyId: Study.Id, chapterId: Chapter.Id)(who: Who) = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
2016-05-16 04:01:03 -06:00
chapterRepo.byIdAndStudy(chapterId, studyId) flatMap {
_ ?? { chapter =>
chapterRepo.orderedMetadataByStudy(studyId).flatMap { chaps =>
// deleting the only chapter? Automatically create an empty one
if (chaps.size < 2) {
2019-10-24 11:37:19 -06:00
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).?? {
2016-05-16 04:01:03 -06:00
chaps.find(_.id != chapterId) ?? { newChap =>
2019-10-24 11:37:19 -06:00
doSetChapter(study, newChap.id, who.sri)
2016-05-16 04:01:03 -06:00
}
}
} >> chapterRepo.delete(chapter.id) >>- reloadChapters(study)
2016-07-25 14:34:15 -06:00
} >>- indexStudy(study)
2016-04-24 23:23:13 -06:00
}
}
}
2019-10-24 11:37:19 -06:00
def sortChapters(studyId: Study.Id, chapterIds: List[Chapter.Id])(who: Who) = sequenceStudy(studyId) { study =>
Contribute(who.u, study) {
2016-05-16 04:01:03 -06:00
chapterRepo.sort(study, chapterIds) >>- reloadChapters(study)
}
}
2019-10-24 11:37:19 -06:00
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) >>-
2019-10-24 11:37:19 -06:00
sendTo(study, StudySocket.DescStudy(who.sri, newStudy.description)) >>-
indexStudy(study)
}
}
}
2019-10-24 11:37:19 -06:00
def editStudy(studyId: Study.Id, data: Study.Data)(who: Who) = sequenceStudy(studyId) { study =>
data.settings.ifTrue(study isOwner who.u) ?? { settings =>
2016-05-19 11:43:19 -06:00
val newStudy = study.copy(
name = Study toName data.name,
settings = settings,
visibility = data.vis,
description = settings.description option {
study.description.filter(_.nonEmpty) | "-"
}
)
if (!study.isPublic && newStudy.isPublic) {
bus.publish(lila.hub.actorApi.study.StudyBecamePublic(studyId.value, study.members.contributorIds), 'study)
2017-08-23 17:56:39 -06:00
} else if (study.isPublic && !newStudy.isPublic) {
bus.publish(lila.hub.actorApi.study.StudyBecamePrivate(studyId.value, study.members.contributorIds), 'study)
}
2016-04-26 21:12:53 -06:00
(newStudy != study) ?? {
2016-07-25 14:34:15 -06:00
studyRepo.updateSomeFields(newStudy) >>-
2018-12-07 05:18:57 -07:00
sendTo(study, StudySocket.ReloadAll) >>-
indexStudy(study) >>-
2017-02-14 06:09:05 -07:00
lightStudyCache.put(studyId, newStudy.light.some)
2016-04-26 21:12:53 -06:00
}
}
}
2016-05-12 08:21:25 -06:00
def delete(study: Study) = sequenceStudy(study.id) { study =>
2016-07-25 14:34:15 -06:00
studyRepo.delete(study) >>
chapterRepo.deleteByStudy(study) >>-
bus.publish(lila.hub.actorApi.study.RemoveStudy(study.id.value, study.members.contributorIds), 'study) >>-
2017-02-14 06:02:42 -07:00
lightStudyCache.put(study.id, none)
2016-05-12 08:21:25 -06:00
}
2019-10-24 11:37:19 -06:00
def like(studyId: Study.Id, v: Boolean)(who: Who): Funit =
2019-10-23 09:31:31 -06:00
studyRepo.like(studyId, who.u, v) map { likes =>
sendToNew(studyId, _.SetLiking(Study.Liking(likes, v), who))
2017-10-06 18:56:06 -06:00
bus.publish(actorApi.StudyLikes(studyId, likes), 'studyLikes)
if (v) studyRepo byId studyId foreach {
_ foreach { study =>
2019-10-23 09:31:31 -06:00
if (who.u != study.ownerId && study.isPublic)
timeline ! (Propagate(StudyLike(who.u, study.id.value, study.name.value)) toFollowersOf who.u)
2016-05-31 18:14:02 -06:00
}
}
2016-05-26 14:01:43 -06:00
}
def resetAllRanks = studyRepo.resetAllRanks
2017-01-21 06:22:51 -07:00
def chapterIdNames(studyIds: List[Study.Id]): Fu[Map[Study.Id, Vector[Chapter.IdName]]] =
2019-07-08 17:35:27 -06:00
chapterRepo.idNamesByStudyIds(studyIds, Study.maxChapters)
2017-01-21 06:22:51 -07:00
def chapterMetadatas = chapterRepo.orderedMetadataByStudy _
2017-10-06 18:56:06 -06:00
def withLiked(me: Option[User])(studies: Seq[Study]): Fu[Seq[Study.WithLiked]] =
me.?? { u => studyRepo.filterLiked(u, studies.map(_.id)) } map { liked =>
studies.map { study =>
Study.WithLiked(study, liked(study.id))
}
}
2019-10-24 11:37:19 -06:00
def analysisRequest(studyId: Study.Id, chapterId: Chapter.Id)(who: Who): Funit =
2018-01-15 21:12:10 -07:00
sequenceStudyWithChapter(studyId, chapterId) {
2019-10-24 11:37:19 -06:00
case Study.WithChapter(study, chapter) => Contribute(who.u, study) {
serverEvalRequester(study, chapter, who.u)
2018-01-15 21:12:10 -07:00
}
}
def erase(user: User) = studyRepo.allIdsByOwner(user.id) flatMap { ids =>
chat ! lila.chat.actorApi.RemoveAll(ids.map(id => Chat.Id(id.value)))
studyRepo.deleteByIds(ids) >>
chapterRepo.deleteByStudyIds(ids)
}
private def sendStudyEnters(study: Study, userId: User.ID) = bus.publish(
lila.hub.actorApi.study.StudyDoor(
userId = userId,
studyId = study.id.value,
contributor = study canContribute userId,
public = study.isPublic,
enters = true
),
'study
)
private def indexStudy(study: Study) =
bus.publish(actorApi.SaveStudy(study), 'study)
2016-07-25 14:34:15 -06:00
2019-07-13 12:02:50 -06:00
private def reloadSri(study: Study, sri: Sri, becauseOf: Option[Chapter.Id] = None) =
sendTo(study, StudySocket.ReloadSri(sri))
2019-07-13 12:02:50 -06:00
private def reloadSriBecauseOf(study: Study, sri: Sri, chapterId: Chapter.Id) =
sendTo(study, StudySocket.ReloadSriBecauseOf(sri, chapterId))
2016-04-24 03:15:18 -06:00
private def reloadChapters(study: Study) =
chapterRepo.orderedMetadataByStudy(study.id).foreach { chapters =>
2018-12-07 05:18:57 -07:00
sendTo(study, StudySocket.ReloadChapters(chapters))
2016-04-24 03:15:18 -06:00
}
import ornicar.scalalib.Zero
private def Contribute[A](userId: User.ID, study: Study)(f: => A)(implicit default: Zero[A]): A =
if (study canContribute userId) f else default.zero
2016-05-26 14:01:43 -06:00
private def sendTo(study: Study, msg: Any): Unit = sendTo(study.id, msg)
private def sendTo(studyId: Study.Id, msg: Any): Unit =
2018-12-07 05:18:57 -07:00
socketMap.tell(studyId.value, msg)
2019-10-23 09:31:31 -06:00
private def sendToNew(studyId: Study.Id, msg: StudyRemoteSocket.Out.type => StudyRemoteSocket.Out): Unit =
bus.publish(StudyRemoteSocket.Send(studyId, msg(StudyRemoteSocket.Out)), 'studySocket)
2016-02-26 05:08:11 -07:00
}