broadcast tournaments WIP
parent
7500455b92
commit
98fcc549a7
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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!"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
21
conf/routes
21
conf/routes
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue