Skip to content

Commit

Permalink
Merge branch 'master' into streamer-decline-with-reason
Browse files Browse the repository at this point in the history
  • Loading branch information
ornicar authored Dec 22, 2024
2 parents 4c4ae24 + cd27d58 commit 4a57778
Show file tree
Hide file tree
Showing 51 changed files with 494 additions and 306 deletions.
6 changes: 2 additions & 4 deletions app/controllers/Recap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ final class Recap(env: Env) extends LilaController(env):
res <- f(using ctx.updatePref(_.forceDarkBg))(user)(av)
yield res
if me.is(username) then proceed(me)
else
Found(env.user.api.byId(username)): user =>
val canView = isGranted(_.SeeInsight) || !env.net.isProd
canView.so(proceed(user))
else if isGranted(_.SeeInsight) || !env.net.isProd then Found(env.user.api.byId(username))(proceed)
else Redirect(routes.Recap.home).toFuccess
}
17 changes: 9 additions & 8 deletions app/controllers/RelayRound.scala
Original file line number Diff line number Diff line change
Expand Up @@ -268,20 +268,21 @@ final class RelayRound(
isSubscribed <- ctx.me.soFu: me =>
env.relay.api.isSubscribed(rt.tour.id, me.userId)
videoUrls <- embed match
case VideoEmbed.Auto =>
fuccess:
rt.tour.pinnedStream
.ifFalse(rt.round.isFinished)
.flatMap(_.upstream)
.map(_.urls(netDomain).toPair)
case VideoEmbed.No => fuccess(none)
case VideoEmbed.Stream(userId) =>
env.streamer.api
.find(userId)
.flatMapz(s => env.streamer.liveStreamApi.of(s).dmap(some))
.map:
_.flatMap(_.stream).map(_.urls(netDomain).toPair)
crossSiteIsolation = videoUrls.isEmpty
case VideoEmbed.PinnedStream =>
fuccess:
rt.tour.pinnedStream
.ifFalse(rt.round.isFinished)
.flatMap(_.upstream)
.map(_.urls(netDomain).toPair)
case _ => fuccess(none)
crossSiteIsolation = videoUrls.isEmpty || (rt.tour.pinnedStream.isDefined && crossOriginPolicy
.supportsCredentiallessIFrames(ctx.req))
data = env.relay.jsonView.makeData(
rt.tour.withRounds(rounds.map(_.round)),
rt.round.id,
Expand Down
2 changes: 1 addition & 1 deletion conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ GET /tutor/:username/:perf/phase controllers.Tutor.phases(username: UserSt
GET /tutor/:username/:perf/time controllers.Tutor.time(username: UserStr, perf: PerfKey)

# Recap
GET /recap controllers.Recap.home()
GET /recap controllers.Recap.home
GET /recap/:username controllers.Recap.user(username: UserStr)

# OAuth
Expand Down
8 changes: 6 additions & 2 deletions modules/relay/src/main/JsonView.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,12 @@ final class JsonView(baseUrl: BaseUrl, markup: RelayMarkup, picfitUrl: PicfitUrl
.add("lcc", trs.rounds.find(_.id == currentRoundId).map(_.sync.upstream.exists(_.hasLcc)))
.add("isSubscribed" -> isSubscribed)
.add("videoUrls" -> videoUrls)
.add("pinnedStream" -> pinned)
.add("note" -> trs.tour.note.ifTrue(canContribute)),
.add("note" -> trs.tour.note.ifTrue(canContribute))
.add("pinned" -> pinned.map: p =>
Json
.obj("name" -> p.name)
.add("redirect" -> p.upstream.map(_.urls(lila.core.config.NetDomain("")).redirect))
.add("text" -> p.text)),
study = studyData.study,
analysis = studyData.analysis,
group = group.map(_.group.name)
Expand Down
101 changes: 48 additions & 53 deletions modules/relay/src/main/RelayFetch.scala
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ final private class RelayFetch(

private def dynamicPeriod(tour: RelayTour, round: RelayRound, upstream: Sync.Upstream) = Seconds:
val base =
if upstream.hasLcc then 6
if upstream.hasLcc then 5
else if upstream.isRound then 10 // uses push so no need to pull often
else 2
base * {
Expand Down Expand Up @@ -301,33 +301,28 @@ final private class RelayFetch(
.map { MultiPgn.split(_, RelayFetch.maxGamesToRead(rt.tour.official)) }
.flatMap(multiPgnToGames.future)
case RelayFormat.LccWithGames(lcc) =>
lccRoundJsonWithEtag
.fetch(lcc.indexUrl)
.flatMap: round =>
val lookForStart: Boolean =
rt.round.startsAtTime
.map(_.minusSeconds(rt.round.sync.delay.so(_.value) + 5 * 60))
.forall(_.isBeforeNow)
round.pairings
.mapWithIndex: (pairing, i) =>
val game = i + 1
val tags = pairing.tags(lcc.round, game, round.date)
lccCache(lcc, game, tags, lookForStart): () =>
lccGameJsonWithEtag
.fetch(lcc.gameUrl(game))
.recover:
case _: Exception => GameJson(moves = Nil, result = none)
.map { _.toPgn(tags) }
.recover: _ =>
PgnStr(s"${tags}\n\n${pairing.result}")
.map(game -> _)
.parallel
.map: pgns =>
MultiPgn(pgns.sortBy(_._1).map(_._2))
.flatMap(multiPgnToGames.future)
lccRoundJsonWithEtag(lcc.indexUrl).flatMap: round =>
val lookForStart: Boolean =
rt.round.startsAtTime
.map(_.minusSeconds(rt.round.sync.delay.so(_.value) + 5 * 60))
.forall(_.isBeforeNow)
round.pairings
.mapWithIndex: (pairing, i) =>
val game = i + 1
val tags = pairing.tags(lcc.round, game, round.date)
lccCache(lcc, game, tags, lookForStart): () =>
lccGameJsonWithEtag(lcc.gameUrl(game)).recover:
case _: Exception => GameJson(moves = Nil, result = none)
.map { _.toPgn(tags) }
.recover: _ =>
PgnStr(s"${tags}\n\n${pairing.result}")
.map(game -> _)
.parallel
.map: pgns =>
MultiPgn(pgns.sortBy(_._1).map(_._2))
.flatMap(multiPgnToGames.future)
case RelayFormat.LccWithoutGames(lcc) =>
lccRoundJsonWithEtag
.fetch(lcc.indexUrl)
lccRoundJsonWithEtag(lcc.indexUrl)
.map: round =>
MultiPgn:
round.pairings.mapWithIndex: (pairing, i) =>
Expand All @@ -343,34 +338,34 @@ final private class RelayFetch(
data <- summon[Reads[A]].reads(json).fold(err => fufail(s"Invalid JSON from $url: $err"), fuccess)
yield data

private final class JsonWithEtag[A: Reads](initialCapacity: Int):
// lcc supports https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
private def fetchJsonWithEtag[A: Reads](initialCapacity: Int): URL => CanProxy ?=> Fu[A] =
import RelayFormat.Etag
// lcc uses https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
private val cache = cacheApi.notLoadingSync[URL, (Etag, A)](initialCapacity, "relay.fetch.jsonWithEtag"):
val cache = cacheApi.notLoadingSync[URL, (Etag, A)](initialCapacity, "relay.fetch.jsonWithEtag"):
_.expireAfterWrite(5 minutes).build()

def fetch(url: URL)(using CanProxy): Fu[A] =
cache
.getIfPresent(url)
.match
case None =>
for
(body, newEtag) <- formatApi.httpGetWithEtag(url, none)
data <- readAsJson[A](url)(~body)
yield (data, newEtag)
case Some((etag, prev)) =>
for
(body, newEtag) <- formatApi.httpGetWithEtag(url, etag.some)
isHit = body.isEmpty && newEtag.contains(etag)
_ = lila.mon.relay.etag(isHit).increment()
data <- if isHit then fuccess(prev) else readAsJson[A](url)(~body)
yield (data, newEtag)
.map: (data, newEtag) =>
newEtag.foreach(e => cache.put(url, e -> data))
data

private val lccGameJsonWithEtag = new JsonWithEtag[DgtJson.GameJson](512)
private val lccRoundJsonWithEtag = new JsonWithEtag[DgtJson.RoundJson](32)
url =>
CanProxy ?=>
cache
.getIfPresent(url)
.match
case None =>
for
(body, newEtag) <- formatApi.httpGetWithEtag(url, none)
data <- readAsJson[A](url)(~body)
yield (data, newEtag)
case Some((etag, prev)) =>
for
(body, newEtag) <- formatApi.httpGetWithEtag(url, etag.some)
isHit = body.isEmpty && newEtag.forall(_ == etag) // on 304 response, Etag might be empty
_ = lila.mon.relay.etag(isHit).increment()
data <- if isHit then fuccess(prev) else readAsJson[A](url)(~body)
yield (data, newEtag.orElse(etag.some))
.map: (data, newEtag) =>
newEtag.foreach(e => cache.put(url, e -> data))
data

private val lccGameJsonWithEtag = fetchJsonWithEtag[DgtJson.GameJson](512)
private val lccRoundJsonWithEtag = fetchJsonWithEtag[DgtJson.RoundJson](32)

private object RelayFetch:

Expand Down
62 changes: 28 additions & 34 deletions modules/relay/src/main/RelayFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import play.api.libs.ws.{
StandaloneWSRequest,
StandaloneWSResponse
}

import scala.util.matching.Regex

import lila.core.config.{ Credentials, HostPort }
Expand Down Expand Up @@ -70,54 +69,50 @@ final private class RelayFormatApi(
roundRepo.exists(id).map(_.option(RelayFormat.Round(id)))

def httpGet(url: URL)(using CanProxy): Fu[String] =
httpGetResponse(url).map(_.body)
httpGetResponse(toRequest(url)).map(_.body)

def httpGetAndGuessCharset(url: URL)(using CanProxy): Fu[String] =
httpGetResponse(url).map: res =>
httpGetResponse(toRequest(url)).map: res =>
responseHeaderCharset(res) match
case None => lila.common.String.charset.guessAndDecode(res.bodyAsBytes)
case Some(known) => res.bodyAsBytes.decodeString(known)

def httpGetWithEtag(url: URL, etag: Option[Etag])(using
CanProxy
): Fu[(Option[String], Option[Etag])] =
val (req, proxy) = configure(url)
etag
.fold(req)(etag => req.addHttpHeaders("If-None-Match" -> etag))
.get()
def httpGetWithEtag(url: URL, etag: Option[Etag])(using CanProxy): Fu[(Option[String], Option[Etag])] =
val req = etag
.foldLeft(toRequest(url))((req, etag) => req.addHttpHeaders("If-None-Match" -> etag))
httpGetResponse(req)
.flatMap: res =>
val newEtag = res.header("Etag")
if res.status == 304 then fuccess(none -> newEtag.orElse(etag))
else if res.status == 200 then fuccess((res.body: String).some -> newEtag)
else if res.status == 404 then fufail(NotFound(url))
else fufail(s"[${res.status}] $url")
.monSuccess(_.relay.httpGet(url.host.toString, proxy))

private def httpGetResponse(url: URL)(using CanProxy): Future[StandaloneWSResponse] =
val (req, proxy) = configure(url)
req
.get()
.flatMap: res =>
if res.status == 200 then fuccess(res)
else if res.status == 404 then fufail(NotFound(url))
else fufail(s"[${res.status}] $url")
.monSuccess(_.relay.httpGet(url.host.toString, proxy))
else fuccess((res.body: String).some -> newEtag)
.monSuccess(_.relay.httpGet(url.host.toString, req.proxyServer.map(_.host)))

private def httpGetResponse(req: StandaloneWSRequest)(using CanProxy): Future[StandaloneWSResponse] =
Future
.fromTry(lila.common.url.parse(req.url))
.flatMap: url =>
req
.get()
.flatMap: res =>
if res.status == 200 || res.status == 304 then fuccess(res)
else if res.status == 404 then fufail(NotFound(url))
else fufail(s"[${res.status}] ${req.url}")
.monSuccess(_.relay.httpGet(url.host.toString, req.proxyServer.map(_.host)))

private def responseHeaderCharset(res: StandaloneWSResponse): Option[java.nio.charset.Charset] =
import play.shaded.ahc.org.asynchttpclient.util.HttpUtils
Option(HttpUtils.extractContentTypeCharsetAttribute(res.contentType)).orElse:
res.contentType.startsWith("text/").option(java.nio.charset.StandardCharsets.ISO_8859_1)

private def configure(url: URL)(using CanProxy): (StandaloneWSRequest, Option[String]) =
addProxy(url):
ws.url(url.toString)
.withRequestTimeout(5.seconds)
.withFollowRedirects(false)
private def toRequest(url: URL)(using CanProxy): StandaloneWSRequest =
val req = ws
.url(url.toString)
.withRequestTimeout(5.seconds)
.withFollowRedirects(false)
proxyServerFor(url).foldLeft(req)(_ withProxyServer _)

private def addProxy(url: URL)(ws: StandaloneWSRequest)(using
allowed: CanProxy
): (StandaloneWSRequest, Option[String]) =
def server = for
private def proxyServerFor(url: URL)(using allowed: CanProxy): Option[DefaultWSProxyServer] =
for
hostPort <- proxyHostPort.get()
if allowed.yes
proxyRegex = proxyDomainRegex.get()
Expand All @@ -130,7 +125,6 @@ final private class RelayFormatApi(
principal = creds.map(_.user),
password = creds.map(_.password.value)
)
server.foldLeft(ws)(_ withProxyServer _) -> server.map(_.host)

private def looksLikePgn(body: String)(using CanProxy): Boolean =
MultiPgn
Expand Down
19 changes: 8 additions & 11 deletions modules/relay/src/main/RelayPinnedStream.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,19 @@ import io.mola.galimatias.URL
import scala.jdk.CollectionConverters.*
import lila.core.config.NetDomain

case class RelayPinnedStream(name: String, url: URL):
case class RelayPinnedStream(name: String, url: URL, text: Option[String]):

import RelayPinnedStream.*

def upstream: Option[RelayPinnedStream.Upstream] =
parseYoutube.orElse(parseTwitch)

// https://www.youtube.com/live/Lg0askmGqvo
// https://www.youtube.com/live/Lg0askmGqvo?si=KKOexnmA2xPcyStZ
def parseYoutube: Option[YouTube] =
url.host.toString
.endsWith("youtube.com")
.so:
url.pathSegments.asScala.toList match
case List("live", id) => YouTube(id).some
case _ => none
if List("www.youtube.com", "youtube.com", "youtu.be").contains(url.host.toString) then
url.pathSegments.asScala.toList match
case List("live", id) => Some(YouTube(id))
case _ => Option(url.queryParameter("v")).map(YouTube.apply)
else None

// https://www.twitch.tv/tcec_chess_tv
def parseTwitch: Option[Twitch] =
Expand All @@ -37,11 +34,11 @@ object RelayPinnedStream:
def urls(parent: NetDomain): Urls
case class YouTube(id: String) extends Upstream:
def urls(parent: NetDomain) = Urls(
s"https://www.youtube.com/embed/${id}?disablekb=1&modestbranding=1",
s"https://www.youtube.com/embed/${id}?disablekb=1&modestbranding=1&autoplay=1",
s"https://www.youtube.com/watch?v=${id}"
)
case class Twitch(id: String) extends Upstream:
def urls(parent: NetDomain) = Urls(
s"https://player.twitch.tv/?channel=${id}&parent=${parent}",
s"https://player.twitch.tv/?channel=${id}&parent=${parent}&autoplay=true",
s"https://www.twitch.tv/${id}"
)
4 changes: 3 additions & 1 deletion modules/relay/src/main/RelayTourForm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ final class RelayTourForm(langList: lila.core.i18n.LangList):

private val pinnedStreamMapping = mapping(
"name" -> cleanNonEmptyText(maxLength = 100),
"url" -> url.field.verifying("Invalid stream URL", url => RelayPinnedStream("", url).upstream.isDefined)
"url" -> url.field
.verifying("Invalid stream URL", url => RelayPinnedStream("", url, None).upstream.isDefined),
"text" -> optional(cleanText(maxLength = 100))
)(RelayPinnedStream.apply)(unapply)

private given Formatter[RelayTour.Tier] =
Expand Down
10 changes: 5 additions & 5 deletions modules/relay/src/main/RelayVideoEmbed.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import play.api.mvc.Result
enum RelayVideoEmbed:
case No
case Auto
case PinnedStream
case Stream(userId: UserId)
override def toString = this match
case No => "no"
case Auto => ""
case Stream(u) => u.toString
case No => "no"
case _ => ""

final class RelayVideoEmbedStore(baker: LilaCookie):

Expand All @@ -21,11 +21,11 @@ final class RelayVideoEmbedStore(baker: LilaCookie):
def read(using req: RequestHeader): RelayVideoEmbed =
def fromCookie = req.cookies.get(cookieName).map(_.value).filter(_.nonEmpty) match
case Some("no") => No
case Some("ps") => PinnedStream
case _ => Auto
req.queryString.get("embed") match
case Some(Nil) => fromCookie
case Some(Seq("")) => Auto
case Some(Seq("no")) => No
case Some(Seq("ps")) => PinnedStream
case Some(Seq(name)) => UserStr.read(name).fold(Auto)(u => Stream(u.id))
case _ => fromCookie

Expand Down
11 changes: 11 additions & 0 deletions modules/relay/src/main/ui/RelayFormUi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,17 @@ Team Dogs ; Scooby Doo"""),
"Stream name",
half = true
)(form3.input(_))
),
form3.split(
form3.group(
form("pinnedStream.text"),
"Stream link label",
help = frag(
"Optional. Show a label on the image link to your live stream.",
br,
"Example: 'Watch us live on YouTube!'"
).some
)(form3.input(_))
)
)
)
Expand Down
3 changes: 3 additions & 0 deletions translation/dest/activity/zh-CN.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
<plurals name="completedNbGames">
<item quantity="other">完成了 %s 盘通讯棋</item>
</plurals>
<plurals name="completedNbVariantGames">
<item quantity="other">下完了%1$s%2$s局通信棋局</item>
</plurals>
<plurals name="followedNbPlayers">
<item quantity="other">新关注 %s 个用户</item>
</plurals>
Expand Down
Loading

0 comments on commit 4a57778

Please sign in to comment.