broadcast tournaments WIP

broadcast-tournament
Thibault Duplessis 2021-04-23 08:50:15 +02:00
parent 7500455b92
commit 98fcc549a7
17 changed files with 267 additions and 212 deletions

View File

@ -7,7 +7,7 @@ import scala.annotation.nowarn
import lila.api.Context
import lila.app._
import lila.common.config.MaxPerSecond
import lila.relay.{ Relay => RelayModel, RelayForm }
import lila.relay.{ Relay => RelayModel, RelayTour => TourModel, RelayForm }
import lila.user.{ User => UserModel }
import views._
@ -27,47 +27,59 @@ final class Relay(
}
}
def form =
Auth { implicit ctx => _ =>
def relayForm(tourId: String) =
Auth { implicit ctx => me =>
NoLameOrBot {
Ok(html.relay.form.create(env.relay.forms.create)).fuccess
WithTour(tourId) { tour =>
(tour.owner == me.id) ?? {
Ok(html.relay.form.relayCreate(env.relay.forms.create, tour)).fuccess
}
}
}
}
def create =
def relayCreate(tourId: String) =
AuthOrScopedBody(_.Study.Write)(
auth = implicit ctx =>
me =>
NoLameOrBot {
env.relay.forms.create
.bindFromRequest()(ctx.body, formBinding)
.fold(
err => BadRequest(html.relay.form.create(err)).fuccess,
setup =>
env.relay.api.create(setup, me) map { relay =>
Redirect(showRoute(relay))
}
)
WithTour(tourId) { tour =>
(tour.owner == me.id) ?? {
env.relay.forms.create
.bindFromRequest()(ctx.body, formBinding)
.fold(
err => BadRequest(html.relay.form.relayCreate(err, tour)).fuccess,
setup =>
env.relay.api.create(setup, me, tour) map { relay =>
Redirect(showRoute(relay withTour tour))
}
)
}
}
},
scoped = req =>
me =>
!(me.isBot || me.lame) ??
env.relay.forms.create
.bindFromRequest()(req, formBinding)
.fold(
err => BadRequest(apiFormError(err)).fuccess,
setup => env.relay.api.create(setup, me) map env.relay.jsonView.admin map JsonOk
)
env.relay.api tourById TourModel.Id(tourId) flatMap {
_ ?? { tour =>
!(me.isBot || me.lame) ??
env.relay.forms.create
.bindFromRequest()(req, formBinding)
.fold(
err => BadRequest(apiFormError(err)).fuccess,
setup => env.relay.api.create(setup, me, tour) map env.relay.jsonView.admin map JsonOk
)
}
}
)
def edit(@nowarn("cat=unused") slug: String, id: String) =
def relayEdit(id: String) =
Auth { implicit ctx => me =>
OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { relay =>
Ok(html.relay.form.edit(relay, env.relay.forms.edit(relay))).fuccess
OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { rt =>
Ok(html.relay.form.relayEdit(rt, env.relay.forms.edit(rt.relay))).fuccess
}
}
def update(@nowarn slug: String, id: String) =
def relayUpdate(id: String) =
AuthOrScopedBody(_.Study.Write)(
auth = implicit ctx =>
me =>
@ -76,8 +88,8 @@ final class Relay(
case Some(res) =>
res
.fold(
{ case (old, err) => BadRequest(html.relay.form.edit(old, err)) },
relay => Redirect(showRoute(relay))
{ case (old, err) => BadRequest(html.relay.form.relayEdit(old, err)) },
rt => Redirect(showRoute(rt))
)
.fuccess
},
@ -88,81 +100,82 @@ final class Relay(
case Some(res) =>
res.fold(
{ case (_, err) => BadRequest(apiFormError(err)) },
relay => JsonOk(env.relay.jsonView.admin(relay))
rt => JsonOk(env.relay.jsonView.admin(rt.relay))
)
}
)
private def doUpdate(id: String, me: UserModel)(implicit
req: Request[_]
): Fu[Option[Either[(RelayModel, Form[RelayForm.Data]), RelayModel]]] =
): Fu[Option[Either[(RelayModel.WithTour, Form[RelayForm.Data]), RelayModel.WithTour]]] =
env.relay.api.byIdAndContributor(id, me) flatMap {
_ ?? { relay =>
_ ?? { rt =>
env.relay.forms
.edit(relay)
.edit(rt.relay)
.bindFromRequest()
.fold(
err => fuccess(Left(relay -> err)),
data => env.relay.api.update(relay) { data.update(_, me) } dmap Right.apply
err => fuccess(Left(rt -> err)),
data =>
env.relay.api.update(rt.relay) { data.update(_, me) }.dmap(_ withTour rt.tour) dmap Right.apply
) dmap some
}
}
def reset(@nowarn("cat=unused") slug: String, id: String) =
Auth { implicit ctx => me =>
OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { relay =>
env.relay.api.reset(relay, me) inject Redirect(showRoute(relay))
OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { rt =>
env.relay.api.reset(rt.relay, me) inject Redirect(showRoute(rt))
}
}
def show(slug: String, id: String) =
def showRelay(ts: String, rs: String, id: String) =
OpenOrScoped(_.Study.Read)(
open = implicit ctx => {
pageHit
WithRelay(slug, id) { relay =>
WithRelay(ts, rs, id) { rt =>
val sc =
if (relay.sync.ongoing)
env.study.chapterRepo relaysAndTagsByStudyId relay.studyId flatMap { chapters =>
if (rt.relay.sync.ongoing)
env.study.chapterRepo relaysAndTagsByStudyId rt.relay.studyId flatMap { chapters =>
chapters.find(_.looksAlive) orElse chapters.headOption match {
case Some(chapter) => env.study.api.byIdWithChapter(relay.studyId, chapter.id)
case None => env.study.api byIdWithChapter relay.studyId
case Some(chapter) => env.study.api.byIdWithChapter(rt.relay.studyId, chapter.id)
case None => env.study.api byIdWithChapter rt.relay.studyId
}
}
else env.study.api byIdWithChapter relay.studyId
sc flatMap { _ ?? { doShow(relay, _) } }
else env.study.api byIdWithChapter rt.relay.studyId
sc flatMap { _ ?? { doShow(rt, _) } }
}
},
scoped = _ =>
me =>
env.relay.api.byIdAndContributor(id, me) map {
case None => NotFound(jsonError("No such broadcast"))
case Some(relay) => JsonOk(env.relay.jsonView.admin(relay))
case None => NotFound(jsonError("No such broadcast"))
case Some(rt) => JsonOk(env.relay.jsonView.admin(rt.relay))
}
)
def chapter(slug: String, id: String, chapterId: String) =
def chapter(ts: String, rs: String, id: String, chapterId: String) =
Open { implicit ctx =>
WithRelay(slug, id) { relay =>
env.study.api.byIdWithChapter(relay.studyId, chapterId) flatMap {
_ ?? { doShow(relay, _) }
WithRelay(ts, rs, id) { rt =>
env.study.api.byIdWithChapter(rt.relay.studyId, chapterId) flatMap {
_ ?? { doShow(rt, _) }
}
}
}
def cloneRelay(@nowarn("cat=unused") slug: String, id: String) =
def cloneRelay(id: String) =
Auth { implicit ctx => me =>
OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { relay =>
env.relay.api.cloneRelay(relay, me) map { newRelay =>
Redirect(routes.Relay.edit(newRelay.slug, newRelay.id.value))
OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { rt =>
env.relay.api.cloneRelay(rt, me) map { newRelay =>
Redirect(routes.Relay.relayEdit(newRelay.id.value))
}
}
}
def push(@nowarn("cat=unused") slug: String, id: String) =
def push(id: String) =
ScopedBody(parse.tolerantText)(Seq(_.Study.Write)) { req => me =>
env.relay.api.byIdAndContributor(id, me) flatMap {
case None => notFoundJson()
case Some(relay) => env.relay.push(relay, req.body) inject jsonOkResult
case None => notFoundJson()
case Some(rt) => env.relay.push(rt, req.body) inject jsonOkResult
}
}
@ -173,28 +186,45 @@ final class Relay(
}.fuccess
}
private def WithRelay(slug: String, id: String)(
f: RelayModel => Fu[Result]
private def WithRelay(ts: String, rs: String, id: String)(
f: RelayModel.WithTour => Fu[Result]
)(implicit ctx: Context): Fu[Result] =
OptionFuResult(env.relay.api byId id) { relay =>
if (relay.slug != slug) Redirect(showRoute(relay)).fuccess
else f(relay)
OptionFuResult(env.relay.api byIdWithTour id) { rt =>
if (rt.tour.slug != ts) Redirect(showRoute(rt)).fuccess
if (rt.relay.slug != rs) Redirect(showRoute(rt)).fuccess
else f(rt)
}
private def doShow(relay: RelayModel, oldSc: lila.study.Study.WithChapter)(implicit
private def WithTour(slug: String, id: String)(
f: TourModel => Fu[Result]
)(implicit ctx: Context): Fu[Result] =
OptionFuResult(env.relay.api tourById TourModel.Id(id)) { tour =>
if (tour.slug != slug) Redirect(showRoute(tour)).fuccess
else f(tour)
}
private def WithTour(id: String)(
f: TourModel => Fu[Result]
)(implicit ctx: Context): Fu[Result] =
OptionFuResult(env.relay.api tourById TourModel.Id(id))(f)
private def doShow(rt: RelayModel.WithTour, oldSc: lila.study.Study.WithChapter)(implicit
ctx: Context
): Fu[Result] =
studyC.CanViewResult(oldSc.study) {
for {
(sc, studyData) <- studyC.getJsonData(oldSc)
data = env.relay.jsonView.makeData(relay, studyData, ctx.userId exists sc.study.canContribute)
data = env.relay.jsonView.makeData(rt.relay, studyData, ctx.userId exists sc.study.canContribute)
chat <- studyC.chatOf(sc.study)
sVersion <- env.study.version(sc.study.id)
streams <- studyC.streamsOf(sc.study)
} yield EnableSharedArrayBuffer(Ok(html.relay.show(relay, sc.study, data, chat, sVersion, streams)))
} yield EnableSharedArrayBuffer(Ok(html.relay.show(rt, sc.study, data, chat, sVersion, streams)))
}
private def showRoute(r: RelayModel) = routes.Relay.show(r.slug, r.id.value)
private def showRoute(rt: RelayModel.WithTour) =
routes.Relay.showRelay(rt.tour.slug, rt.relay.slug, rt.relay.id.value)
private def showRoute(t: TourModel) = routes.Relay.showTour(t.slug, t.id.value)
implicit private def makeRelayId(id: String): RelayModel.Id = RelayModel.Id(id)
implicit private def makeChapterId(id: String): lila.study.Chapter.Id = lila.study.Chapter.Id(id)

View File

@ -157,10 +157,10 @@ final class Study(
f: => Fu[Result]
)(implicit ctx: Context): Fu[Result] =
if (HTTPRequest isRedirectable ctx.req) env.relay.api.getOngoing(lila.relay.Relay.Id(id)) flatMap {
_.fold(f) { relay =>
_.fold(f) { rt =>
fuccess(Redirect {
chapterId.fold(routes.Relay.show(relay.slug, relay.id.value)) { c =>
routes.Relay.chapter(relay.slug, relay.id.value, c)
chapterId.fold(routes.Relay.showRelay(rt.tour.slug, rt.relay.slug, rt.relay.id.value)) { c =>
routes.Relay.chapter(rt.tour.slug, rt.relay.slug, rt.relay.id.value, c)
}
})
}

View File

@ -1,5 +1,6 @@
package views.html.relay
import controllers.routes
import play.api.data.Form
import lila.api.Context
@ -7,32 +8,31 @@ import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.relay.Relay.Sync.UpstreamUrl.LccRegex
import lila.relay.RelayForm.Data
import controllers.routes
import lila.relay.{ Relay, RelayTour }
object form {
import trans.broadcast._
def create(form: Form[Data])(implicit ctx: Context) =
def relayCreate(form: Form[Data], tour: RelayTour)(implicit ctx: Context) =
layout(newBroadcast.txt())(
h1(newBroadcast()),
inner(form, routes.Relay.create)
inner(form, routes.Relay.relayCreate(tour.id.value))
)
def edit(r: lila.relay.Relay, form: Form[Data])(implicit ctx: Context) =
layout(r.name)(
h1("Edit ", r.name),
inner(form, routes.Relay.update(r.slug, r.id.value)),
def relayEdit(rt: Relay.WithTour, form: Form[Data])(implicit ctx: Context) =
layout(rt.fullName)(
h1("Edit ", rt.fullName),
inner(form, routes.Relay.relayUpdate(rt.relay.id.value)),
hr,
postForm(action := routes.Relay.cloneRelay(r.slug, r.id.value))(
postForm(action := routes.Relay.cloneRelay(rt.relay.id.value))(
submitButton(
cls := "button button-empty confirm",
title := "Create an new identical broadcast, for another round or a similar tournament"
)(cloneBroadcast())
),
hr,
postForm(action := routes.Relay.reset(r.slug, r.id.value))(
postForm(action := routes.Relay.reset(rt.relay.id.value))(
submitButton(
cls := "button button-red button-empty confirm",
title := "The source will need to be active in order to re-create the chapters!"

View File

@ -36,7 +36,7 @@ object index {
div(cls := "box__top")(
h1(liveBroadcasts()),
a(
href := routes.Relay.form,
href := routes.Relay.tourForm,
cls := "new button button-empty",
title := newBroadcast.txt(),
dataIcon := "O"

View File

@ -13,15 +13,14 @@ import controllers.routes
object show {
def apply(
r: lila.relay.Relay,
s: lila.study.Study,
rt: lila.relay.Relay.WithTourAndStudy,
data: lila.relay.JsonView.JsData,
chatOption: Option[lila.chat.UserChat.Mine],
socketVersion: lila.socket.Socket.SocketVersion,
streams: List[lila.streamer.Stream]
)(implicit ctx: Context) =
views.html.base.layout(
title = r.name,
title = rt.fullName,
moreCss = cssTag("analyse.study"),
moreJs = frag(
analyseTag,
@ -39,17 +38,17 @@ object show {
c.chat,
name = trans.chatRoom.txt(),
timeout = c.timeout,
writeable = ctx.userId.??(s.canChat),
writeable = ctx.userId.??(rt.study.canChat),
public = false,
resourceId = lila.chat.Chat.ResourceId(s"relay/${c.chat.id}"),
localMod = ctx.userId.??(s.canContribute)
localMod = ctx.userId.??(rt.study.canContribute)
)
),
"explorer" -> Json.obj(
"endpoint" -> explorerEndpoint,
"tablebaseEndpoint" -> tablebaseEndpoint
),
"socketUrl" -> views.html.study.show.socketUrl(s.id.value),
"socketUrl" -> views.html.study.show.socketUrl(rt.study.id.value),
"socketVersion" -> socketVersion.value
)
)}""")
@ -59,9 +58,9 @@ object show {
csp = defaultCsp.withWebAssembly.some,
openGraph = lila.app.ui
.OpenGraph(
title = r.name,
url = s"$netBaseUrl${routes.Relay.show(r.slug, r.id.value).url}",
description = shorten(r.description, 152)
title = rt.fullName,
url = s"$netBaseUrl${routes.Relay.showRelay(rt.tour.slug, rt.relay.slug, rt.relay.id.value).url}",
description = shorten(rt.relay.description, 152)
)
.some
)(
@ -71,9 +70,9 @@ object show {
)
)
def widget(r: lila.relay.Relay.WithStudyAndLiked, extraCls: String = "") =
def widget(rt: lila.relay.Relay.WithTourAndStudy, extraCls: String = "") =
div(cls := s"relay-widget $extraCls", dataIcon := "")(
a(cls := "overlay", href := routes.Relay.show(r.relay.slug, r.relay.id.value)),
a(cls := "overlay", href := routes.Relay.showRelay(rt.tour.slug, rt.relay.slug, rt.relay.id.value)),
div(
h3(r.relay.name),
p(r.relay.description)

View File

@ -167,15 +167,18 @@ GET /study/topic/autocomplete controllers.Study.topicAutocomplete
# Relay
GET /broadcast controllers.Relay.index(page: Int ?= 1)
GET /broadcast/new controllers.Relay.form
POST /broadcast/new controllers.Relay.create
GET /broadcast/:slug/$id<\w{8}> controllers.Relay.show(slug: String, id: String)
GET /broadcast/:slug/$id<\w{8}>/$chapterId<\w{8}> controllers.Relay.chapter(slug: String, id: String, chapterId: String)
GET /broadcast/:slug/$id<\w{8}>/edit controllers.Relay.edit(slug: String, id: String)
POST /broadcast/:slug/$id<\w{8}>/edit controllers.Relay.update(slug: String, id: String)
POST /broadcast/:slug/$id<\w{8}>/reset controllers.Relay.reset(slug: String, id: String)
POST /broadcast/:slug/$id<\w{8}>/clone controllers.Relay.cloneRelay(slug: String, id: String)
POST /broadcast/:slug/$id<\w{8}>/push controllers.Relay.push(slug: String, id: String)
GET /broadcast/new controllers.Relay.tourForm
POST /broadcast/new controllers.Relay.tourCreate
GET /broadcast/:ts/:tourId controllers.Relay.showTour(ts: String, tourId: String)
GET /broadcast/:tourId/new controllers.Relay.relayForm(tourId: String)
POST /broadcast/:tourId/new controllers.Relay.relayCreate(tourId: String)
GET /broadcast/:ts/:rs/$id<\w{8}> controllers.Relay.showRelay(ts: String, rs: String, id: String)
GET /broadcast/:ts/:rs/$id<\w{8}>/$chapterId<\w{8}> controllers.Relay.chapter(ts: String, rs: String, id: String, chapterId: String)
GET /broadcast/$id<\w{8}>/edit controllers.Relay.relayEdit(id: String)
POST /broadcast/$id<\w{8}>/edit controllers.Relay.relayUpdate(id: String)
POST /broadcast/$id<\w{8}>/reset controllers.Relay.reset(id: String)
POST /broadcast/$id<\w{8}>/clone controllers.Relay.cloneRelay(id: String)
POST /broadcast/$id<\w{8}>/push controllers.Relay.push(rs: String, id: String)
# Learn
GET /learn controllers.Learn.index

View File

@ -343,6 +343,21 @@ trait dsl {
val updatedDesc = desc("updatedAt")
}
object $lookup {
def simple(from: String, as: String, local: String, foreign: String): Bdoc = $doc(
"$lookup" -> $doc(
"from" -> from,
"as" -> as,
"localField" -> local,
"foreignField" -> foreign
)
)
def simple(from: Coll, as: String, local: String, foreign: String): Bdoc =
simple(from.name, as, local, foreign)
def simple(from: AsyncColl, as: String, local: String, foreign: String): Bdoc =
simple(from.name.value, as, local, foreign)
}
implicit class ElementBuilderLike(val field: String)
extends ElementBuilder
with ComparisonOperators

View File

@ -11,6 +11,7 @@ final class Env(
ws: StandaloneWSClient,
db: lila.db.Db,
studyApi: lila.study.StudyApi,
studyRepo: lila.study.StudyRepo,
chapterRepo: lila.study.ChapterRepo,
gameRepo: lila.game.GameRepo,
pgnDump: lila.game.PgnDump,
@ -29,8 +30,6 @@ final class Env(
private lazy val tourRepo = new RelayTourRepo(db(CollName("relay_tour")))
private lazy val withStudy = wire[RelayWithStudy]
lazy val jsonView = wire[JsonView]
lazy val api: RelayApi = wire[RelayApi]

View File

@ -59,6 +59,8 @@ case class Relay(
def withSync(f: Relay.Sync => Relay.Sync) = copy(sync = f(sync))
def withTour(tour: RelayTour) = Relay.WithTour(this, tour)
override def toString = s"""relay #$id "$name" $sync"""
}
@ -68,8 +70,6 @@ object Relay {
def makeId = Id(lila.common.ThreadLocalRandom nextString 8)
case class WithTour(relay: Relay, tour: RelayTour)
case class Sync(
upstream: Option[Sync.Upstream], // if empty, needs a client to push PGN
until: Option[DateTime], // sync until then; resets on move
@ -133,12 +133,13 @@ object Relay {
case class UpstreamIds(ids: List[lila.game.Game.ID]) extends Upstream
}
case class WithTour(relay: Relay, tour: RelayTour) {
def fullName = s"${tour.name}${relay.name}"
}
case class WithStudy(relay: Relay, study: Study)
case class WithStudyAndLiked(relay: Relay, study: Study, liked: Boolean)
case class WithTourAndStudy(relay: Relay, tour: RelayTour, study: Study)
case class Fresh(
created: Seq[WithStudyAndLiked],
started: Seq[WithStudyAndLiked]
)
case class Fresh(created: Seq[WithTourAndStudy], started: Seq[WithTourAndStudy])
}

View File

@ -11,33 +11,60 @@ import scala.util.chaining._
import lila.common.config.MaxPerSecond
import lila.db.dsl._
import lila.study.{ Settings, Study, StudyApi, StudyMaker }
import lila.study.{ Settings, Study, StudyApi, StudyMaker, StudyRepo }
import lila.user.User
final class RelayApi(
relayRepo: RelayRepo,
tourRepo: RelayTourRepo,
studyApi: StudyApi,
withStudy: RelayWithStudy,
studyRepo: StudyRepo,
jsonView: JsonView,
formatApi: RelayFormatApi
)(implicit ec: scala.concurrent.ExecutionContext, mat: akka.stream.Materializer) {
import BSONHandlers._
import lila.study.BSONHandlers.StudyBSONHandler
def byId(id: Relay.Id) = relayRepo.coll.byId[Relay](id.value)
def byIdWithTour(id: Relay.Id): Fu[Option[Relay.WithTour]] =
relayRepo.coll
.aggregateOne() { framework =>
import framework._
Match($id(id)) -> List(
PipelineOperator(tourRepo lookup "tourId"),
UnwindField("tour")
)
}
.map { docO =>
for {
doc <- docO
relay <- doc.asOpt[Relay]
tour <- doc.getAsOpt[RelayTour]("tour")
} yield Relay.WithTour(relay, tour)
}
def byIdAndContributor(id: Relay.Id, me: User) =
byIdWithStudy(id) map {
_ collect {
case Relay.WithStudy(relay, study) if study.canContribute(me.id) => relay
case Relay.WithTourAndStudy(relay, tour, study) if study.canContribute(me.id) => relay withTour tour
}
}
def byIdWithStudy(id: Relay.Id): Fu[Option[Relay.WithStudy]] =
WithRelay(id) { relay =>
studyApi.byId(relay.studyId) dmap2 {
Relay.WithStudy(relay, _)
def byIdWithStudy(id: Relay.Id): Fu[Option[Relay.WithTourAndStudy]] =
byIdWithTour(id) flatMap {
_ ?? { case Relay.WithTour(relay, tour) =>
studyApi.byId(relay.studyId) dmap2 {
Relay.WithTourAndStudy(relay, tour, _)
}
}
}
def apply(relays: List[Relay]): Fu[List[Relay.WithStudy]] =
studyApi byIds relays.map(_.studyId) map { studies =>
relays.flatMap { relay =>
studies.find(_.id == relay.studyId) map { Relay.WithStudy(relay, _) }
}
}
@ -47,7 +74,9 @@ final class RelayApi(
Relay.Fresh(c, s)
}
private[relay] def toSync =
def tourById(id: RelayTour.Id) = tourRepo.coll.byId[RelayTour](id.value)
private[relay] def toSync: Fu[List[Relay.WithTour]] =
fetchWithTours(
$doc(
"sync.until" $exists true,
@ -61,16 +90,7 @@ final class RelayApi(
.aggregateList(maxDocs, readPreference) { framework =>
import framework._
Match(query) -> List(
PipelineOperator(
$doc(
"$lookup" -> $doc(
"from" -> tourRepo.coll.name,
"as" -> "tour",
"localField" -> "tourId",
"foreignField" -> "_id"
)
)
),
PipelineOperator(tourRepo lookup "tourId"),
UnwindField("tour")
)
}
@ -131,15 +151,19 @@ final class RelayApi(
studyApi.deleteAllChapters(relay.studyId, by) >>
requestPlay(relay.id, v = true)
def cloneRelay(relay: Relay, by: User, tour: RelayTour): Fu[Relay] =
def cloneRelay(rt: Relay.WithTour, by: User): Fu[Relay] =
create(
RelayForm.Data make relay.copy(name = s"${relay.name} (clone)"),
RelayForm.Data make rt.relay.copy(name = s"${rt.relay.name} (clone)"),
by,
tour
rt.tour
)
def getOngoing(id: Relay.Id): Fu[Option[Relay]] =
relayRepo.coll.one[Relay]($doc("_id" -> id, "finished" -> false))
def getOngoing(id: Relay.Id): Fu[Option[Relay.WithTour]] =
relayRepo.coll.one[Relay]($doc("_id" -> id, "finished" -> false)) flatMap {
_ ?? { relay =>
tourById(relay.tourId) map2 relay.withTour
}
}
def officialStream(perSecond: MaxPerSecond, nb: Int): Source[JsObject, _] =
relayRepo

View File

@ -54,78 +54,78 @@ final private class RelayFetch(
List(true, false) foreach { official =>
lila.mon.relay.ongoing(official).update(relays.count(_.tour.official == official))
}
relays.map { case Relay.WithTour(relay, tour) =>
if (relay.sync.ongoing) processRelay(relay, tour) flatMap { newRelay =>
api.update(relay)(_ => newRelay)
relays.map { rt =>
if (rt.relay.sync.ongoing) processRelay(rt) flatMap { newRelay =>
api.update(rt.relay)(_ => newRelay)
}
else if (relay.hasStarted) {
logger.info(s"Finish by lack of activity $relay")
api.update(relay)(_.finish)
} else if (relay.shouldGiveUp) {
logger.info(s"Finish for lack of start $relay")
api.update(relay)(_.finish)
} else fuccess(relay)
else if (rt.relay.hasStarted) {
logger.info(s"Finish by lack of activity ${rt.relay}")
api.update(rt.relay)(_.finish)
} else if (rt.relay.shouldGiveUp) {
logger.info(s"Finish for lack of start ${rt.relay}")
api.update(rt.relay)(_.finish)
} else fuccess(rt.relay)
}.sequenceFu addEffectAnyway scheduleNext()
}.unit
}
// no writing the relay; only reading!
def processRelay(relay: Relay, tour: RelayTour): Fu[Relay] =
if (!relay.sync.playing) fuccess(relay.withSync(_.play))
def processRelay(rt: Relay.WithTour): Fu[Relay] =
if (!rt.relay.sync.playing) fuccess(rt.relay.withSync(_.play))
else
fetchGames(relay, tour)
.mon(_.relay.fetchTime(tour.official, relay.slug))
.addEffect(gs => lila.mon.relay.games(tour.official, relay.slug).update(gs.size).unit)
fetchGames(rt)
.mon(_.relay.fetchTime(rt.tour.official, rt.relay.slug))
.addEffect(gs => lila.mon.relay.games(rt.tour.official, rt.relay.slug).update(gs.size).unit)
.flatMap { games =>
sync(relay, tour, games)
sync(rt, games)
.withTimeout(7 seconds, SyncResult.Timeout)
.mon(_.relay.syncTime(tour.official, relay.slug))
.mon(_.relay.syncTime(rt.tour.official, rt.relay.slug))
.map { res =>
res -> relay.withSync(_ addLog SyncLog.event(res.moves, none))
res -> rt.relay.withSync(_ addLog SyncLog.event(res.moves, none))
}
}
.recover { case e: Exception =>
(e match {
case SyncResult.Timeout =>
if (tour.official) logger.info(s"Sync timeout $relay")
if (rt.tour.official) logger.info(s"Sync timeout ${rt.relay}")
SyncResult.Timeout
case _ =>
if (tour.official) logger.info(s"Sync error $relay ${e.getMessage take 80}")
if (rt.tour.official) logger.info(s"Sync error ${rt.relay} ${e.getMessage take 80}")
SyncResult.Error(e.getMessage)
}) -> relay.withSync(_ addLog SyncLog.event(0, e.some))
}) -> rt.relay.withSync(_ addLog SyncLog.event(0, e.some))
}
.map { case (result, newRelay) =>
afterSync(result, newRelay, tour)
afterSync(result, newRelay withTour rt.tour)
}
def afterSync(result: SyncResult, relay: Relay, tour: RelayTour): Relay =
def afterSync(result: SyncResult, rt: Relay.WithTour): Relay =
result match {
case SyncResult.Ok(0, _) => continueRelay(relay, tour)
case SyncResult.Ok(0, _) => continueRelay(rt)
case SyncResult.Ok(nbMoves, _) =>
lila.mon.relay.moves(tour.official, relay.slug).increment(nbMoves)
continueRelay(relay.ensureStarted.resume, tour)
case _ => continueRelay(relay, tour)
lila.mon.relay.moves(rt.tour.official, rt.relay.slug).increment(nbMoves)
continueRelay(rt.relay.ensureStarted.resume withTour rt.tour)
case _ => continueRelay(rt)
}
def continueRelay(r: Relay, tour: RelayTour): Relay =
r.sync.upstream.fold(r) { upstream =>
def continueRelay(rt: Relay.WithTour): Relay =
rt.relay.sync.upstream.fold(rt.relay) { upstream =>
val seconds =
if (r.sync.log.alwaysFails && !upstream.local) {
r.sync.log.events.lastOption
if (rt.relay.sync.log.alwaysFails && !upstream.local) {
rt.relay.sync.log.events.lastOption
.filterNot(_.isTimeout)
.flatMap(_.error)
.ifTrue(tour.official && r.hasStarted) foreach { error =>
slackApi.broadcastError(r.id.value, r.name, error)
.ifTrue(rt.tour.official && rt.relay.hasStarted) foreach { error =>
slackApi.broadcastError(rt.relay.id.value, rt.relay.name, error)
}
60
} else
r.sync.delay getOrElse {
rt.relay.sync.delay getOrElse {
if (upstream.local) 3 else 6
}
r.withSync {
rt.relay.withSync {
_.copy(
nextAt = DateTime.now plusSeconds {
seconds atLeast { if (r.sync.log.justTimedOut) 10 else 2 }
seconds atLeast { if (rt.relay.sync.log.justTimedOut) 10 else 2 }
} some
)
}
@ -145,8 +145,8 @@ final private class RelayFetch(
delayMoves = true
)
private def fetchGames(relay: Relay, tour: RelayTour): Fu[RelayGames] =
relay.sync.upstream ?? {
private def fetchGames(rt: Relay.WithTour): Fu[RelayGames] =
rt.relay.sync.upstream ?? {
case UpstreamIds(ids) =>
gameRepo.gamesFromSecondary(ids) flatMap
gameProxy.upgradeIfPresent flatMap
@ -161,10 +161,10 @@ final private class RelayFetch(
url,
(_, v) =>
Option(v) match {
case Some(GamesSeenBy(games, seenBy)) if !seenBy(relay.id) =>
GamesSeenBy(games, seenBy + relay.id)
case Some(GamesSeenBy(games, seenBy)) if !seenBy(rt.relay.id) =>
GamesSeenBy(games, seenBy + rt.relay.id)
case _ =>
GamesSeenBy(doFetchUrl(url, RelayFetch.maxChapters(tour)), Set(relay.id))
GamesSeenBy(doFetchUrl(url, RelayFetch.maxChapters(rt.tour)), Set(rt.relay.id))
}
)
.games

View File

@ -13,24 +13,24 @@ final class RelayPush(sync: RelaySync, api: RelayApi)(implicit
private val throttler = system.actorOf(Props(new EarlyMultiThrottler(logger = logger)))
def apply(relay: Relay, tour: RelayTour, pgn: String): Fu[Option[String]] =
if (relay.sync.hasUpstream)
def apply(rt: Relay.WithTour, pgn: String): Fu[Option[String]] =
if (rt.relay.sync.hasUpstream)
fuccess("The relay has an upstream URL, and cannot be pushed to.".some)
else
fuccess {
throttler ! EarlyMultiThrottler.Work(
id = relay.id.value,
run = () => pushNow(relay, tour, pgn),
cooldown = if (tour.official) 3.seconds else 7.seconds
id = rt.relay.id.value,
run = () => pushNow(rt, pgn),
cooldown = if (rt.tour.official) 3.seconds else 7.seconds
)
none
}
private def pushNow(relay: Relay, tour: RelayTour, pgn: String): Funit =
private def pushNow(rt: Relay.WithTour, pgn: String): Funit =
RelayFetch
.multiPgnToGames(MultiPgn.split(pgn, RelayFetch.maxChapters(tour)))
.multiPgnToGames(MultiPgn.split(pgn, RelayFetch.maxChapters(rt.tour)))
.flatMap {
sync(relay, tour, _)
sync(rt, _)
}
.map { res =>
SyncLog.event(res.moves, none)
@ -39,6 +39,6 @@ final class RelayPush(sync: RelaySync, api: RelayApi)(implicit
SyncLog.event(0, e.some)
}
.flatMap { event =>
api.update(relay)(_.withSync(_ addLog event)).void
api.update(rt.relay)(_.withSync(_ addLog event)).void
}
}

View File

@ -13,15 +13,15 @@ final private class RelaySync(
private type NbMoves = Int
def apply(relay: Relay, tour: RelayTour, games: RelayGames): Fu[SyncResult.Ok] =
studyApi byId relay.studyId orFail "Missing relay study!" flatMap { study =>
def apply(rt: Relay.WithTour, games: RelayGames): Fu[SyncResult.Ok] =
studyApi byId rt.relay.studyId orFail "Missing relay study!" flatMap { study =>
chapterRepo orderedByStudy study.id flatMap { chapters =>
RelayInputSanity(chapters, games) match {
case Some(fail) => fufail(fail.msg)
case None =>
lila.common.Future.linear(games) { game =>
findCorrespondingChapter(game, chapters, games.size) match {
case Some(chapter) => updateChapter(relay, tour, study, chapter, game)
case Some(chapter) => updateChapter(rt.tour, study, chapter, game)
case None =>
createChapter(study, game) flatMap { chapter =>
chapters.find(_.isEmptyInitial).ifTrue(chapter.order == 2).?? { initial =>
@ -51,13 +51,12 @@ final private class RelaySync(
else chapters.find(_.relay.exists(_.index == game.index))
private def updateChapter(
relay: Relay,
tour: RelayTour,
study: Study,
chapter: Chapter,
game: RelayGame
): Fu[NbMoves] =
updateChapterTags(relay, tour, study, chapter, game) >>
updateChapterTags(tour, study, chapter, game) >>
updateChapterTree(study, chapter, game)
private def updateChapterTree(study: Study, chapter: Chapter, game: RelayGame): Fu[NbMoves] = {
@ -112,7 +111,6 @@ final private class RelaySync(
}
private def updateChapterTags(
relay: Relay,
tour: RelayTour,
study: Study,
chapter: Chapter,
@ -139,12 +137,12 @@ final private class RelaySync(
tags = chapterNewTags
)(actorApi.Who(chapter.ownerId, sri)) >> {
val newEnd = chapter.tags.resultColor.isEmpty && tags.resultColor.isDefined
newEnd ?? onChapterEnd(relay, tour, study, chapter)
newEnd ?? onChapterEnd(tour, study, chapter)
}
}
}
private def onChapterEnd(relay: Relay, tour: RelayTour, study: Study, chapter: Chapter): Funit =
private def onChapterEnd(tour: RelayTour, study: Study, chapter: Chapter): Funit =
chapterRepo.setRelayPath(chapter.id, Path.root) >> {
(tour.official && chapter.root.mainline.sizeIs > 10) ?? studyApi.analysisRequest(
studyId = study.id,

View File

@ -14,6 +14,11 @@ case class RelayTour(
createdAt: DateTime
) {
def id = _id
def slug = {
val s = lila.common.String slugify name
if (s.isEmpty) "-" else s
}
}
object RelayTour {

View File

@ -9,4 +9,6 @@ import lila.db.dsl._
final private class RelayTourRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionContext) {
import BSONHandlers._
def lookup(local: String) = $lookup.simple(coll, "tour", local, "_id")
}

View File

@ -1,23 +0,0 @@
package lila.relay
import lila.study.{ Study, StudyApi }
import lila.user.User
final private class RelayWithStudy(studyApi: StudyApi)(implicit ec: scala.concurrent.ExecutionContext) {
def apply(relays: List[Relay]): Fu[List[Relay.WithStudy]] =
studyApi byIds relays.map(_.studyId) map { studies =>
relays.flatMap { relay =>
studies.find(_.id == relay.studyId) map { Relay.WithStudy(relay, _) }
}
}
def andLiked(me: Option[User])(relays: Seq[Relay]): Fu[Seq[Relay.WithStudyAndLiked]] =
studyApi byIds relays.map(_.studyId) flatMap studyApi.withLiked(me) map { s =>
relays.flatMap { relay =>
s.find(_.study.id == relay.studyId) map { case Study.WithLiked(study, liked) =>
Relay.WithStudyAndLiked(relay, study, liked)
}
}
}
}

View File

@ -53,6 +53,8 @@ final class StudyRepo(private[study] val coll: AsyncColl)(implicit
def exists(id: Study.Id) = coll(_.exists($id(id)))
def lookup(local: String) = $lookup.simple(coll, "study", local, "_id")
private[study] def selectOwnerId(ownerId: User.ID) = $doc("ownerId" -> ownerId)
private[study] def selectMemberId(memberId: User.ID) = $doc(F.uids -> memberId)
private[study] val selectPublic = $doc(