2017-09-19 20:24:59 -06:00
|
|
|
package lila.relay
|
|
|
|
|
2017-10-05 15:48:14 -06:00
|
|
|
import org.joda.time.DateTime
|
|
|
|
|
2017-10-06 17:33:33 -06:00
|
|
|
import chess.format.pgn.{ Tag, Tags }
|
2019-07-13 12:02:50 -06:00
|
|
|
import lila.socket.Socket.Sri
|
2017-09-20 13:35:28 -06:00
|
|
|
import lila.study._
|
2017-09-19 20:24:59 -06:00
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
final private class RelaySync(
|
2017-09-20 13:35:28 -06:00
|
|
|
studyApi: StudyApi,
|
|
|
|
chapterRepo: ChapterRepo
|
2019-12-13 18:17:43 -07:00
|
|
|
)(implicit ec: scala.concurrent.ExecutionContext) {
|
2017-09-19 20:24:59 -06:00
|
|
|
|
2017-10-04 23:59:35 -06:00
|
|
|
private type NbMoves = Int
|
|
|
|
|
2017-10-09 07:36:33 -06:00
|
|
|
def apply(relay: Relay, games: RelayGames): Fu[SyncResult.Ok] =
|
2019-12-03 21:58:09 -07:00
|
|
|
studyApi byId relay.studyId orFail "Missing relay study!" flatMap { study =>
|
2017-10-09 07:36:33 -06:00
|
|
|
chapterRepo orderedByStudy study.id flatMap { chapters =>
|
2018-10-17 03:55:10 -06:00
|
|
|
RelayInputSanity(chapters, games) match {
|
2018-10-17 06:33:29 -06:00
|
|
|
case Some(fail) => fufail(fail.msg)
|
2019-12-13 07:30:20 -07:00
|
|
|
case None =>
|
|
|
|
lila.common.Future.traverseSequentially(games) { game =>
|
|
|
|
findCorrespondingChapter(game, chapters, games.size) match {
|
|
|
|
case Some(chapter) => updateChapter(study, chapter, game)
|
|
|
|
case None =>
|
|
|
|
createChapter(study, game) flatMap { chapter =>
|
|
|
|
chapters.find(_.isEmptyInitial).ifTrue(chapter.order == 2).?? { initial =>
|
|
|
|
studyApi.deleteChapter(study.id, initial.id) {
|
|
|
|
actorApi.Who(study.ownerId, sri)
|
|
|
|
}
|
|
|
|
} inject chapter.root.mainline.size
|
2019-10-25 04:46:42 -06:00
|
|
|
}
|
2018-10-17 03:55:10 -06:00
|
|
|
}
|
2019-12-13 18:17:43 -07:00
|
|
|
} map { _.foldLeft(0)(_ + _) } dmap { SyncResult.Ok(_, games) }
|
2018-10-17 03:55:10 -06:00
|
|
|
}
|
2017-10-09 07:36:33 -06:00
|
|
|
}
|
2017-09-20 13:35:28 -06:00
|
|
|
}
|
2017-09-19 20:24:59 -06:00
|
|
|
|
2017-10-14 21:52:43 -06:00
|
|
|
/*
|
|
|
|
* If the source contains several games, use their index to match them with the study chapter.
|
|
|
|
* If the source contains only one game, use the player tags to match with the study chapter.
|
|
|
|
* So the TCEC style - one game per file, reusing the file for all games - is supported.
|
|
|
|
* lichess will create a new chapter when the game player tags differ.
|
|
|
|
*/
|
2019-12-13 07:30:20 -07:00
|
|
|
private def findCorrespondingChapter(
|
|
|
|
game: RelayGame,
|
|
|
|
chapters: List[Chapter],
|
|
|
|
nbGames: Int
|
|
|
|
): Option[Chapter] =
|
2018-10-17 03:55:10 -06:00
|
|
|
if (nbGames == 1) chapters find game.staticTagsMatch
|
2017-10-14 21:52:43 -06:00
|
|
|
else chapters.find(_.relay.exists(_.index == game.index))
|
|
|
|
|
2017-10-05 19:13:29 -06:00
|
|
|
private def updateChapter(study: Study, chapter: Chapter, game: RelayGame): Fu[NbMoves] =
|
2017-10-06 17:33:33 -06:00
|
|
|
updateChapterTags(study, chapter, game) >>
|
2017-10-05 19:13:29 -06:00
|
|
|
updateChapterTree(study, chapter, game)
|
|
|
|
|
2019-10-25 04:46:42 -06:00
|
|
|
private def updateChapterTree(study: Study, chapter: Chapter, game: RelayGame): Fu[NbMoves] = {
|
|
|
|
val who = actorApi.Who(chapter.ownerId, sri)
|
2017-09-20 13:35:28 -06:00
|
|
|
game.root.mainline.foldLeft(Path.root -> none[Node]) {
|
|
|
|
case ((parentPath, None), gameNode) =>
|
|
|
|
val path = parentPath + gameNode
|
2017-09-20 13:53:52 -06:00
|
|
|
chapter.root.nodeAt(path) match {
|
|
|
|
case None => parentPath -> gameNode.some
|
|
|
|
case Some(existing) =>
|
|
|
|
gameNode.clock.filter(c => !existing.clock.has(c)) ?? { c =>
|
2017-10-09 12:36:36 -06:00
|
|
|
studyApi.setClock(
|
|
|
|
studyId = study.id,
|
|
|
|
position = Position(chapter, path).ref,
|
2019-10-25 04:46:42 -06:00
|
|
|
clock = c.some
|
|
|
|
)(who)
|
2017-09-20 13:53:52 -06:00
|
|
|
}
|
|
|
|
path -> none
|
|
|
|
}
|
2017-09-20 13:35:28 -06:00
|
|
|
case (found, _) => found
|
|
|
|
} match {
|
2017-10-09 10:59:17 -06:00
|
|
|
case (path, newNode) =>
|
|
|
|
!Path.isMainline(chapter.root, path) ?? {
|
2018-10-17 04:23:47 -06:00
|
|
|
logger.info(s"Change mainline ${showSC(study, chapter)} $path")
|
2017-10-09 10:59:17 -06:00
|
|
|
studyApi.promote(
|
2017-10-09 10:52:52 -06:00
|
|
|
studyId = study.id,
|
2017-10-09 10:59:17 -06:00
|
|
|
position = Position(chapter, path).ref,
|
2019-10-25 04:46:42 -06:00
|
|
|
toMainline = true
|
|
|
|
)(who) >> chapterRepo.setRelayPath(chapter.id, path)
|
2017-10-09 10:59:17 -06:00
|
|
|
} >> newNode.?? { node =>
|
|
|
|
lila.common.Future.fold(node.mainline)(Position(chapter, path).ref) {
|
2019-12-13 07:30:20 -07:00
|
|
|
case (position, n) =>
|
|
|
|
studyApi.addNode(
|
|
|
|
studyId = study.id,
|
|
|
|
position = position,
|
|
|
|
node = n,
|
|
|
|
opts = moveOpts.copy(clock = n.clock),
|
|
|
|
relay = Chapter
|
|
|
|
.Relay(
|
|
|
|
index = game.index,
|
|
|
|
path = position.path + n,
|
|
|
|
lastMoveAt = DateTime.now
|
|
|
|
)
|
|
|
|
.some
|
|
|
|
)(who) inject position + n
|
2017-10-09 10:59:17 -06:00
|
|
|
} inject node.mainline.size
|
|
|
|
}
|
2017-09-20 13:35:28 -06:00
|
|
|
}
|
2019-10-25 04:46:42 -06:00
|
|
|
}
|
2017-09-20 13:35:28 -06:00
|
|
|
|
2017-10-06 17:33:33 -06:00
|
|
|
private def updateChapterTags(study: Study, chapter: Chapter, game: RelayGame): Funit = {
|
|
|
|
val gameTags = game.tags.value.foldLeft(Tags(Nil)) {
|
|
|
|
case (newTags, tag) =>
|
2017-10-10 15:08:48 -06:00
|
|
|
if (!chapter.tags.value.exists(tag ==)) newTags + tag
|
2017-10-06 17:33:33 -06:00
|
|
|
else newTags
|
|
|
|
}
|
|
|
|
val tags = game.end
|
|
|
|
.ifFalse(gameTags(_.Result).isDefined)
|
|
|
|
.filterNot(end => chapter.tags(_.Result).??(end.resultText ==))
|
|
|
|
.fold(gameTags) { end =>
|
|
|
|
gameTags + Tag(_.Result, end.resultText)
|
|
|
|
}
|
2017-10-29 11:19:00 -06:00
|
|
|
val chapterNewTags = tags.value.foldLeft(chapter.tags) {
|
|
|
|
case (chapterTags, tag) => PgnTags(chapterTags + tag)
|
|
|
|
}
|
2018-03-08 18:30:14 -07:00
|
|
|
(chapterNewTags != chapter.tags) ?? {
|
2018-10-17 03:55:10 -06:00
|
|
|
if (vs(chapterNewTags) != vs(chapter.tags))
|
2018-10-17 04:23:47 -06:00
|
|
|
logger.info(s"Update ${showSC(study, chapter)} tags '${vs(chapter.tags)}' -> '${vs(chapterNewTags)}'")
|
2018-03-08 18:30:14 -07:00
|
|
|
studyApi.setTags(
|
|
|
|
studyId = study.id,
|
|
|
|
chapterId = chapter.id,
|
2019-10-25 04:46:42 -06:00
|
|
|
tags = chapterNewTags
|
|
|
|
)(actorApi.Who(chapter.ownerId, sri)) >> {
|
2018-03-13 09:08:48 -06:00
|
|
|
chapterNewTags.resultColor.isDefined ?? onChapterEnd(study.id, chapter.id)
|
2018-03-08 18:30:14 -07:00
|
|
|
}
|
|
|
|
}
|
2017-10-06 17:33:33 -06:00
|
|
|
}
|
|
|
|
|
2018-03-13 09:08:48 -06:00
|
|
|
private def onChapterEnd(studyId: Study.Id, chapterId: Chapter.Id): Funit =
|
|
|
|
chapterRepo.setRelayPath(chapterId, Path.root) >>
|
|
|
|
studyApi.analysisRequest(
|
|
|
|
studyId = studyId,
|
|
|
|
chapterId = chapterId,
|
|
|
|
userId = "lichess"
|
|
|
|
)
|
|
|
|
|
2017-10-01 17:25:44 -06:00
|
|
|
private def createChapter(study: Study, game: RelayGame): Fu[Chapter] =
|
2017-09-20 13:35:28 -06:00
|
|
|
chapterRepo.nextOrderByStudy(study.id) flatMap { order =>
|
2017-10-10 09:50:57 -06:00
|
|
|
val name = {
|
|
|
|
for {
|
|
|
|
w <- game.tags(_.White)
|
|
|
|
b <- game.tags(_.Black)
|
|
|
|
} yield s"$w - $b"
|
|
|
|
} orElse game.tags("board") getOrElse "?"
|
2017-09-20 13:35:28 -06:00
|
|
|
val chapter = Chapter.make(
|
|
|
|
studyId = study.id,
|
2017-10-10 09:50:57 -06:00
|
|
|
name = Chapter.Name(name),
|
2017-09-20 13:35:28 -06:00
|
|
|
setup = Chapter.Setup(
|
|
|
|
none,
|
2018-01-10 10:17:17 -07:00
|
|
|
game.variant,
|
2017-09-20 13:35:28 -06:00
|
|
|
chess.Color.White
|
|
|
|
),
|
|
|
|
root = game.root,
|
2017-10-05 11:28:07 -06:00
|
|
|
tags = game.tags,
|
2017-09-20 13:35:28 -06:00
|
|
|
order = order,
|
|
|
|
ownerId = study.ownerId,
|
|
|
|
practice = false,
|
|
|
|
gamebook = false,
|
2017-10-07 06:14:25 -06:00
|
|
|
conceal = none,
|
2019-12-13 07:30:20 -07:00
|
|
|
relay = Chapter
|
|
|
|
.Relay(
|
|
|
|
index = game.index,
|
|
|
|
path = game.root.mainlinePath,
|
|
|
|
lastMoveAt = DateTime.now
|
|
|
|
)
|
|
|
|
.some
|
2017-09-20 13:35:28 -06:00
|
|
|
)
|
2019-10-25 04:46:42 -06:00
|
|
|
studyApi.doAddChapter(study, chapter, sticky = false, actorApi.Who(study.ownerId, sri)) inject chapter
|
2017-09-20 13:35:28 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
private val moveOpts = MoveOpts(
|
|
|
|
write = true,
|
|
|
|
sticky = false,
|
|
|
|
promoteToMainline = true,
|
|
|
|
clock = none
|
|
|
|
)
|
|
|
|
|
2019-10-25 04:46:42 -06:00
|
|
|
private val sri = Sri("")
|
2018-10-17 03:55:10 -06:00
|
|
|
|
2018-10-17 04:23:47 -06:00
|
|
|
private def vs(tags: Tags) = s"${tags(_.White) | "?"} - ${tags(_.Black) | "?"}"
|
2018-10-17 03:55:10 -06:00
|
|
|
|
|
|
|
private def showSC(study: Study, chapter: Chapter) =
|
|
|
|
s"#${study.id} chapter[${chapter.relay.fold("?")(_.index.toString)}]"
|
2017-09-19 20:24:59 -06:00
|
|
|
}
|
2017-10-09 07:36:33 -06:00
|
|
|
|
2017-10-18 11:54:50 -06:00
|
|
|
sealed trait SyncResult {
|
|
|
|
val reportKey: String
|
|
|
|
}
|
2017-10-09 07:36:33 -06:00
|
|
|
object SyncResult {
|
2017-10-18 11:54:50 -06:00
|
|
|
case class Ok(moves: Int, games: RelayGames) extends SyncResult {
|
|
|
|
val reportKey = "ok"
|
|
|
|
}
|
|
|
|
case object Timeout extends Exception with SyncResult {
|
2019-12-13 07:30:20 -07:00
|
|
|
val reportKey = "timeout"
|
2017-10-09 07:36:33 -06:00
|
|
|
override def getMessage = "In progress..."
|
|
|
|
}
|
2018-10-17 06:33:29 -06:00
|
|
|
case class Error(msg: String) extends SyncResult {
|
2017-10-18 11:54:50 -06:00
|
|
|
val reportKey = "error"
|
|
|
|
}
|
2017-10-09 07:36:33 -06:00
|
|
|
}
|