diff --git a/app/controllers/Importer.scala b/app/controllers/Importer.scala index 92bd879b53ea..be76da9befe6 100644 --- a/app/controllers/Importer.scala +++ b/app/controllers/Importer.scala @@ -44,16 +44,13 @@ object Importer extends LilaController { ) } - import lila.game.GameRepo - import org.joda.time.DateTime - private val masterGameEncodingFixedAt = new DateTime(2016, 3, 9, 0, 0) - def masterGame(id: String, orientation: String) = Open { implicit ctx => - def redirectAtFen(game: lila.game.Game) = Redirect { - val url = routes.Round.watcher(game.id, orientation).url - val fenParam = get("fen").??(f => s"?fen=$f") - s"$url$fenParam" + Env.explorer.importer(id) map { + _ ?? { game => + val url = routes.Round.watcher(game.id, orientation).url + val fenParam = get("fen").??(f => s"?fen=$f") + Redirect(s"$url$fenParam") + } } - Env.explorer.importer(id) map2 redirectAtFen } } diff --git a/modules/explorer/src/main/Env.scala b/modules/explorer/src/main/Env.scala index a1dfc1136ecd..d61fd4b7f4a7 100644 --- a/modules/explorer/src/main/Env.scala +++ b/modules/explorer/src/main/Env.scala @@ -6,7 +6,7 @@ import com.typesafe.config.Config final class Env( config: Config, gameColl: lila.db.dsl.Coll, - importer: lila.importer.Importer, + gameImporter: lila.importer.Importer, system: ActorSystem ) { @@ -18,6 +18,11 @@ final class Env( internalEndpoint = InternalEndpoint ) + lazy val importer = new ExplorerImporter( + endpoint = InternalEndpoint, + gameImporter = gameImporter + ) + def cli = new lila.common.Cli { def process = { case "explorer" :: "index" :: since :: Nil => indexer(since) inject "done" @@ -36,7 +41,7 @@ object Env { lazy val current = "explorer" boot new Env( config = lila.common.PlayApp loadConfig "explorer", gameColl = lila.game.Env.current.gameColl, - importer = lila.importer.Env.current.importer, + gameImporter = lila.importer.Env.current.importer, system = lila.common.PlayApp.system ) } diff --git a/modules/explorer/src/main/ExplorerImport.scala b/modules/explorer/src/main/ExplorerImporter.scala similarity index 90% rename from modules/explorer/src/main/ExplorerImport.scala rename to modules/explorer/src/main/ExplorerImporter.scala index 250a4ba5116d..2d5343fccc21 100644 --- a/modules/explorer/src/main/ExplorerImport.scala +++ b/modules/explorer/src/main/ExplorerImporter.scala @@ -5,9 +5,9 @@ import org.joda.time.DateTime import lila.game.{ Game, GameRepo } import lila.importer.{ Importer, ImportData } -final class ExplorerImport( +final class ExplorerImporter( endpoint: String, - importer: Importer + gameImporter: Importer ) { private val masterGameEncodingFixedAt = new DateTime(2016, 3, 9, 0, 0) @@ -17,7 +17,7 @@ final class ExplorerImport( case Some(game) if game.createdAt.isAfter(masterGameEncodingFixedAt) => fuccess(game.some) case _ => (GameRepo remove id) >> fetchPgn(id) flatMap { case None => fuccess(none) - case Some(pgn) => importer( + case Some(pgn) => gameImporter( ImportData(pgn, none), user = "lichess".some, forceId = id.some diff --git a/modules/importer/src/main/DataForm.scala b/modules/importer/src/main/DataForm.scala index 29db49e87c0b..21e91cec996c 100644 --- a/modules/importer/src/main/DataForm.scala +++ b/modules/importer/src/main/DataForm.scala @@ -39,26 +39,23 @@ case class ImportData(pgn: String, analyse: Option[String]) { case ParsedPgn(_, _, sans) if sans.size > maxPlies => !!("Replay is too long") case parsed @ ParsedPgn(_, tags, sans) => Reader.full(pgn) map { case replay @ Replay(setup, _, game) => - def tag(which: Tag.type => TagType): Option[String] = - tags find (_.name == which(Tag)) map (_.value) - - val initBoard = tag(_.FEN) flatMap Forsyth.<< map (_.board) - val fromPosition = initBoard.nonEmpty && tag(_.FEN) != Forsyth.initial.some + val initBoard = parsed.tag(_.FEN) flatMap Forsyth.<< map (_.board) + val fromPosition = initBoard.nonEmpty && parsed.tag(_.FEN) != Forsyth.initial.some val variant = { - tag(_.Variant).map(Chess960.fixVariantName).flatMap(chess.variant.Variant.byName) | { + parsed.tag(_.Variant).map(Chess960.fixVariantName).flatMap(chess.variant.Variant.byName) | { if (fromPosition) chess.variant.FromPosition else chess.variant.Standard } } match { case chess.variant.Chess960 if !Chess960.isStartPosition(setup.board) => chess.variant.FromPosition - case chess.variant.FromPosition if tag(_.FEN).isEmpty => chess.variant.Standard + case chess.variant.FromPosition if parsed.tag(_.FEN).isEmpty => chess.variant.Standard case v => v } - val initialFen = tag(_.FEN) flatMap { + val initialFen = parsed.tag(_.FEN) flatMap { Forsyth.<<<@(variant, _) } map Forsyth.>> map FEN.apply - val status = tag(_.Termination).map(_.toLowerCase) match { + val status = parsed.tag(_.Termination).map(_.toLowerCase) match { case Some("normal") | None => Status.Resign case Some("abandoned") => Status.Aborted case Some("time forfeit") => Status.Outoftime @@ -66,7 +63,7 @@ case class ImportData(pgn: String, analyse: Option[String]) { case Some(_) => Status.UnknownFinish } - val result = tag(_.Result) ifFalse game.situation.end collect { + val result = parsed.tag(_.Result) ifFalse game.situation.end collect { case "1-0" => Result(status, Color.White.some) case "0-1" => Result(status, Color.Black.some) case "*" => Result(Status.Started, none) @@ -74,10 +71,10 @@ case class ImportData(pgn: String, analyse: Option[String]) { case "1/2-1/2" => Result(Status.Draw, none) } - val date = tag(_.Date) + val date = parsed.tag(_.Date) - def name(whichName: TagPicker, whichRating: TagPicker): String = tag(whichName).fold("?") { n => - n + ~tag(whichRating).map(e => s" (${e take 8})") + def name(whichName: TagPicker, whichRating: TagPicker): String = parsed.tag(whichName).fold("?") { n => + n + ~parsed.tag(whichRating).map(e => s" (${e take 8})") } val dbGame = Game.make( diff --git a/modules/study/src/main/Env.scala b/modules/study/src/main/Env.scala index 61fb9a48800a..a07024e69979 100644 --- a/modules/study/src/main/Env.scala +++ b/modules/study/src/main/Env.scala @@ -18,6 +18,7 @@ final class Env( lightUserApi: lila.user.LightUserApi, gamePgnDump: lila.game.PgnDump, importer: lila.importer.Importer, + explorerImporter: lila.explorer.ExplorerImporter, evalCacheHandler: lila.evalCache.EvalCacheSocketHandler, notifyApi: lila.notify.NotifyApi, getPref: User => Fu[lila.pref.Pref], @@ -84,6 +85,11 @@ final class Env( domain = NetDomain ) + private lazy val explorerGame = new ExplorerGame( + importer = explorerImporter, + lightUser = lightUserApi.sync + ) + private lazy val studyMaker = new StudyMaker( lightUser = lightUserApi.sync, chapterMaker = chapterMaker @@ -104,6 +110,7 @@ final class Env( studyMaker = studyMaker, inviter = studyInvite, tagsFixer = new ChapterTagsFixer(chapterRepo, gamePgnDump), + explorerGameHandler = explorerGame, lightUser = lightUserApi.sync, scheduler = system.scheduler, chat = hub.actor.chat, @@ -154,6 +161,7 @@ object Env { lightUserApi = lila.user.Env.current.lightUserApi, gamePgnDump = lila.game.Env.current.pgnDump, importer = lila.importer.Env.current.importer, + explorerImporter = lila.explorer.Env.current.importer, evalCacheHandler = lila.evalCache.Env.current.socketHandler, notifyApi = lila.notify.Env.current.api, getPref = lila.pref.Env.current.api.getPref, diff --git a/modules/study/src/main/ExplorerGame.scala b/modules/study/src/main/ExplorerGame.scala index 85f86bea572e..d9d10ebb793b 100644 --- a/modules/study/src/main/ExplorerGame.scala +++ b/modules/study/src/main/ExplorerGame.scala @@ -1,37 +1,45 @@ package lila.study -import lila.game.{ Game, Namer, PgnImport } -import lila.user.User +import org.joda.time.DateTime +import scala.util.Try + import chess.format.pgn.{ Parser, Reader, ParsedPgn, Tag, TagType } +import lila.common.LightUser +import lila.game.{ Game, Namer } +import lila.tree.Node.Comment +import lila.user.User private final class ExplorerGame( - importer: lila.explorer.ExplorerImport + importer: lila.explorer.ExplorerImporter, + lightUser: LightUser.GetterSync ) { - def quote(userId: User.ID, study: Study, chapter: Chapter, path: Path, gameId: Game.ID): Fu[Comment] = - importer(gameId) flatMap { - _.fold(false) { game => - - val comment = Comment( + def quote(userId: User.ID, gameId: Game.ID): Fu[Option[Comment]] = + lightUser(userId) ?? { author => + importer(gameId) map { + _ ?? { game => + Comment( id = Comment.Id.make, - text = game.pgnImport.fold(lichessTitle)(importTitle(game)), + text = Comment.Text(gameTitle(game)), by = Comment.Author.User(author.id, author.titleName) - ) + ).some + } } } - private def importTitle(g: Game)(pgnImport: PgnImport): String = - Parser.full(pgnImport.pgn) flatMap { - case ParsedPgn(_, tags, _) => - def tag(which: Tag.type => TagType): Option[String] = - tags find (_.name == which(Tag)) map (_.value) + private def gameYear(pgn: Option[ParsedPgn], g: Game): Int = pgn.flatMap { p => + p.tag(_.UTCDate) orElse p.tag(_.Date) + }.flatMap { pgnDate => + Try(DateTime.parse(pgnDate, Tag.UTCDate.format)).toOption map (_.getYear) + } | g.createdAt.getYear - ImportData(pgnImport.pgn, none).preprocess(none).fold( - _ => lichessTitle(g), - processed => - val players = Namer.vsText(game, withRatings = true) - val result = chess.Color.showResult(game.winnerColor) - val text = s"$players, $result + private def gameTitle(g: Game): String = { + val pgn = g.pgnImport.flatMap(pgnImport => Parser.full(pgnImport.pgn).toOption) + val white = pgn.flatMap(_.tag(_.White)) | Namer.playerText(g.whitePlayer)(lightUser) + val black = pgn.flatMap(_.tag(_.Black)) | Namer.playerText(g.blackPlayer)(lightUser) + val result = chess.Color.showResult(g.winnerColor) + val event = pgn.flatMap(_.tag(_.Event)) | gameYear(pgn, g).toString + s"$white - $black, $result, $event" } def insert(userId: User.ID, study: Study, chapter: Chapter, gameId: Game.ID) = ??? diff --git a/modules/study/src/main/StudyApi.scala b/modules/study/src/main/StudyApi.scala index 0a754af84cd2..4ac72663530d 100644 --- a/modules/study/src/main/StudyApi.scala +++ b/modules/study/src/main/StudyApi.scala @@ -21,6 +21,7 @@ final class StudyApi( chapterMaker: ChapterMaker, inviter: StudyInvite, tagsFixer: ChapterTagsFixer, + explorerGameHandler: ExplorerGame, lightUser: lila.common.LightUser.GetterSync, scheduler: akka.actor.Scheduler, chat: ActorSelection, @@ -332,29 +333,27 @@ final class StudyApi( def setComment(userId: User.ID, studyId: Study.Id, position: Position.Ref, text: Comment.Text, uid: Uid) = sequenceStudyWithChapter(studyId, position.chapterId) { case Study.WithChapter(study, chapter) => Contribute(userId, study) { - (study.members get userId) ?? { byMember => - lightUser(userId) ?? { author => - val comment = Comment( - id = Comment.Id.make, - text = text, - by = Comment.Author.User(author.id, author.titleName) - ) - chapter.setComment(comment, position.path) match { - case Some(newChapter) => - studyRepo.updateNow(study) - newChapter.root.nodeAt(position.path) ?? { node => - node.comments.findBy(comment.by) ?? { c => - chapterRepo.setComments(newChapter, position.path, node.comments.filterEmpty) >>- { - sendTo(study, Socket.SetComment(position, c, uid)) - indexStudy(study) - sendStudyEnters(study, userId) - } + lightUser(userId) ?? { author => + val comment = Comment( + id = Comment.Id.make, + text = text, + by = Comment.Author.User(author.id, author.titleName) + ) + chapter.setComment(comment, position.path) match { + case Some(newChapter) => + studyRepo.updateNow(study) + newChapter.root.nodeAt(position.path) ?? { node => + node.comments.findBy(comment.by) ?? { c => + chapterRepo.setComments(newChapter, position.path, node.comments.filterEmpty) >>- { + sendTo(study, Socket.SetComment(position, c, uid)) + indexStudy(study) + sendStudyEnters(study, userId) } } - case None => - fufail(s"Invalid setComment $studyId $position") >>- - reloadUidBecauseOf(study, uid, chapter.id) - } + } + case None => + fufail(s"Invalid setComment $studyId $position") >>- + reloadUidBecauseOf(study, uid, chapter.id) } } } @@ -362,36 +361,32 @@ final class StudyApi( def deleteComment(userId: User.ID, studyId: Study.Id, position: Position.Ref, id: Comment.Id, uid: Uid) = sequenceStudyWithChapter(studyId, position.chapterId) { case Study.WithChapter(study, chapter) => Contribute(userId, study) { - (study.members get userId) ?? { byMember => - chapter.deleteComment(id, position.path) match { - case Some(newChapter) => - chapterRepo.update(newChapter) >>- - sendTo(study, Socket.DeleteComment(position, id, uid)) >>- - indexStudy(study) - case None => - fufail(s"Invalid deleteComment $studyId $position $id") >>- - reloadUidBecauseOf(study, uid, chapter.id) - } + chapter.deleteComment(id, position.path) match { + case Some(newChapter) => + chapterRepo.update(newChapter) >>- + sendTo(study, Socket.DeleteComment(position, id, uid)) >>- + indexStudy(study) + case None => + fufail(s"Invalid deleteComment $studyId $position $id") >>- + reloadUidBecauseOf(study, uid, chapter.id) } } } def toggleGlyph(userId: User.ID, studyId: Study.Id, position: Position.Ref, glyph: Glyph, uid: Uid) = sequenceStudyWithChapter(studyId, position.chapterId) { case Study.WithChapter(study, chapter) => Contribute(userId, study) { - (study.members get userId) ?? { byMember => - chapter.toggleGlyph(glyph, position.path) match { - case Some(newChapter) => - studyRepo.updateNow(study) - newChapter.root.nodeAt(position.path) ?? { node => - chapterRepo.setGlyphs(newChapter, position.path, node.glyphs) >>- - newChapter.root.nodeAt(position.path).foreach { node => - sendTo(study, Socket.SetGlyphs(position, node.glyphs, uid)) - } - } - case None => - fufail(s"Invalid toggleGlyph $studyId $position $glyph") >>- - reloadUidBecauseOf(study, uid, chapter.id) - } + chapter.toggleGlyph(glyph, position.path) match { + case Some(newChapter) => + studyRepo.updateNow(study) + newChapter.root.nodeAt(position.path) ?? { node => + chapterRepo.setGlyphs(newChapter, position.path, node.glyphs) >>- + newChapter.root.nodeAt(position.path).foreach { node => + sendTo(study, Socket.SetGlyphs(position, node.glyphs, uid)) + } + } + case None => + fufail(s"Invalid toggleGlyph $studyId $position $glyph") >>- + reloadUidBecauseOf(study, uid, chapter.id) } } } @@ -412,6 +407,32 @@ final class StudyApi( } } + def explorerGame(userId: User.ID, studyId: Study.Id, data: actorApi.ExplorerGame, uid: Uid) = sequenceStudyWithChapter(studyId, data.position.chapterId) { + case Study.WithChapter(study, chapter) => Contribute(userId, study) { + if (data.insert) ??? + else explorerGameHandler.quote(userId, data.gameId) flatMap { + _ ?? { comment => + chapter.setComment(comment, data.position.path) match { + case Some(newChapter) => + studyRepo.updateNow(study) + newChapter.root.nodeAt(data.position.path) ?? { node => + node.comments.findBy(comment.by) ?? { c => + chapterRepo.setComments(newChapter, data.position.path, node.comments.filterEmpty) >>- { + sendTo(study, Socket.SetComment(data.position, c, uid)) + indexStudy(study) + sendStudyEnters(study, userId) + } + } + } + case None => + fufail(s"Invalid explorerGame quote $studyId $data") >>- + reloadUidBecauseOf(study, uid, chapter.id) + } + } + } + } + } + def addChapter(byUserId: User.ID, studyId: Study.Id, data: ChapterMaker.Data, sticky: Boolean, socket: ActorRef, uid: Uid) = sequenceStudy(studyId) { study => Contribute(byUserId, study) { chapterRepo.nextOrderByStudy(study.id) flatMap { order => diff --git a/modules/study/src/main/StudyCommenter.scala b/modules/study/src/main/StudyCommenter.scala deleted file mode 100644 index 8ea40ed32bc4..000000000000 --- a/modules/study/src/main/StudyCommenter.scala +++ /dev/null @@ -1,19 +0,0 @@ -package lila.study - -private final class StudyCommenter() { - - def apply(chapter: Chapteer, position: Position, comment: Comment) = - chapter.setComment(comment, position.path) match { - case Some(newChapter) => - studyRepo.updateNow(study) - newChapter.root.nodeAt(position.path) ?? { node => - node.comments.findBy(comment.by) ?? { c => - chapterRepo.setComments(newChapter, position.path, node.comments.filterEmpty) >>- { - sendTo(study, Socket.SetComment(position, c, uid)) - indexStudy(study) - sendStudyEnters(study, userId) - } - } - } - } -} diff --git a/modules/study/src/main/actorApi.scala b/modules/study/src/main/actorApi.scala index fd6fdba3b500..85df80a91a36 100644 --- a/modules/study/src/main/actorApi.scala +++ b/modules/study/src/main/actorApi.scala @@ -6,4 +6,7 @@ case class SaveStudy(study: Study) case class SetTag(chapterId: Chapter.Id, name: String, value: String) { def tag = chess.format.pgn.Tag(name, value take 140) } -case class ExplorerGame(chapterId: Chapter.Id, path: String, gameId: String, insert: Boolean) +case class ExplorerGame(ch: Chapter.Id, path: String, gameId: String, insert: Boolean) { + def chapterId = ch + val position = Position.Ref(chapterId, Path(path)) +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 79c7119fe19b..a6e885337c37 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -29,7 +29,7 @@ object Dependencies { val hasher = "com.roundeights" %% "hasher" % "1.2.0" val jodaTime = "joda-time" % "joda-time" % "2.9.9" - val chess = "org.lichess" %% "scalachess" % "6.12" + val chess = "org.lichess" %% "scalachess" % "6.13" val maxmind = "com.sanoma.cda" %% "maxmind-geoip2-scala" % "1.2.3-THIB" val prismic = "io.prismic" %% "scala-kit" % "1.2.11-THIB" val java8compat = "org.scala-lang.modules" %% "scala-java8-compat" % "0.8.0" diff --git a/ui/analyse/src/explorer/explorerView.ts b/ui/analyse/src/explorer/explorerView.ts index 986011471db7..12aab29511be 100644 --- a/ui/analyse/src/explorer/explorerView.ts +++ b/ui/analyse/src/explorer/explorerView.ts @@ -110,16 +110,23 @@ function showGameTable(ctrl: AnalyseCtrl, title: string, games): VNode | null { else window.open('/import/master/' + openedId + '/' + orientation + fenParam, '_blank'); }) }, 'View'), - ...(ctrl.study ? [ - h('a.text', { - attrs: dataIcon('c'), - hook: bind('click', _ => ctrl.study!.explorerGame(openedId, false), ctrl.redraw) - }, 'Quote'), - h('a.text', { - attrs: dataIcon('O'), - hook: bind('click', _ => ctrl.study!.explorerGame(openedId, true), ctrl.redraw) - }, 'Insert') - ] : []), + ...(ctrl.study ? (function() { + function send(insert: boolean) { + ctrl.study!.explorerGame(openedId!, insert); + ctrl.explorer.gameMenu(null); + ctrl.redraw(); + } + return [ + h('a.text', { + attrs: dataIcon('c'), + hook: bind('click', _ => send(false), ctrl.redraw) + }, 'Quote'), + h('a.text', { + attrs: dataIcon('O'), + hook: bind('click', _ => send(true), ctrl.redraw) + }, 'Insert') + ]; + })() : []), h('a.text', { attrs: dataIcon('L'), hook: bind('click', _ => ctrl.explorer.gameMenu(null), ctrl.redraw)