0.3.01
This commit is contained in:
@@ -6,13 +6,29 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/config"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
// handleRemoteConfig serves one app-scoped, immutable-at-runtime document. It is public
|
||||
// handleRemoteConfig serves one app-scoped, versioned document. It is public
|
||||
// for the same reason the update verdict is public: a fresh install and a signed-out TV
|
||||
// must be able to warm the next launch. No viewer or session data belongs in this answer.
|
||||
func (s *Server) handleRemoteConfig(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := json.Marshal(s.cfg.RemoteConfig)
|
||||
document := s.cfg.RemoteConfig
|
||||
clientSchema := parseClientSchema(r)
|
||||
components := parseCapabilities(r.Header.Get("X-Memby-Components"))
|
||||
if clientSchema > 0 && clientSchema < document.SchemaVersion {
|
||||
document.SchemaVersion = clientSchema
|
||||
}
|
||||
policy := s.currentFeaturePolicy(r.Context())
|
||||
applyGlobalConfiguration(&document, policy)
|
||||
negotiateRemoteSections(&document, components)
|
||||
if s.log != nil {
|
||||
s.loggerFor(r.Context()).Info("remote configuration delivered", "client_version", r.Header.Get("X-Memby-Version"), "schema", clientSchema, "delivered_schema", document.SchemaVersion, "components", components, "config_version", document.ConfigVersion)
|
||||
}
|
||||
body, err := json.Marshal(document)
|
||||
if err != nil {
|
||||
// Config is validated during start-up, so this is defensive rather than an expected
|
||||
// operational failure.
|
||||
@@ -23,7 +39,9 @@ func (s *Server) handleRemoteConfig(w http.ResponseWriter, r *http.Request) {
|
||||
etag := `"rc-` + hex.EncodeToString(digest[:12]) + `"`
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("ETag", etag)
|
||||
w.Header().Set("X-Memby-Config-Version", configVersionHeader(s.cfg.RemoteConfig.ConfigVersion))
|
||||
w.Header().Set("X-Memby-Config-Version", configVersionHeader(document.ConfigVersion))
|
||||
w.Header().Set("X-Memby-Delivered-Schema", strconv.Itoa(document.SchemaVersion))
|
||||
w.Header().Set("X-Memby-Delivered-Components", strings.Join(components, ","))
|
||||
if r.Header.Get("If-None-Match") == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
@@ -31,6 +49,113 @@ func (s *Server) handleRemoteConfig(w http.ResponseWriter, r *http.Request) {
|
||||
writeRaw(w, http.StatusOK, body)
|
||||
}
|
||||
|
||||
func parseClientSchema(r *http.Request) int {
|
||||
value, _ := strconv.Atoi(strings.TrimSpace(r.Header.Get("X-Memby-Config-Schema")))
|
||||
return value
|
||||
}
|
||||
|
||||
func negotiateRemoteSections(document *config.RemoteConfig, components []string) {
|
||||
if len(components) == 0 {
|
||||
return
|
||||
}
|
||||
supported := make(map[string]bool, len(components))
|
||||
for _, component := range components {
|
||||
supported[component] = true
|
||||
}
|
||||
for _, sections := range []*[]config.RemoteSectionDefinition{&document.Home.SectionDefinitions, &document.Movies.SectionDefinitions, &document.TV.SectionDefinitions} {
|
||||
for index := range *sections {
|
||||
section := &(*sections)[index]
|
||||
if supported[section.Component] {
|
||||
continue
|
||||
}
|
||||
if section.Component == "landscapeMediaCardV2" && supported["mediaRow"] {
|
||||
section.Component = "mediaRow"
|
||||
continue
|
||||
}
|
||||
section.Enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// applyGlobalConfiguration is intentionally small and typed. The public startup
|
||||
// document has no viewer identity, so user/device assignments are resolved by the
|
||||
// authenticated feature endpoint rather than leaking into this cacheable response.
|
||||
func applyGlobalConfiguration(document *config.RemoteConfig, policy store.FeaturePolicy) {
|
||||
for key, raw := range policy.Values {
|
||||
var value any
|
||||
if json.Unmarshal(raw, &value) != nil {
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "forYou.enabled":
|
||||
if v, ok := value.(bool); ok {
|
||||
document.ForYou.Enabled = v
|
||||
document.Features.Flags["forYou.enabled"] = v
|
||||
document.Features.Flags["for_you"] = v
|
||||
}
|
||||
case "continueWatching.enabled":
|
||||
if v, ok := value.(bool); ok {
|
||||
document.ContinueWatching.Enabled = v
|
||||
document.Features.Flags["continueWatching.enabled"] = v
|
||||
document.Features.Flags["continue_watching"] = v
|
||||
}
|
||||
case "continueWatching.showNextUp":
|
||||
if v, ok := value.(bool); ok {
|
||||
document.ContinueWatching.IncludeNextUp = v
|
||||
}
|
||||
case "continueWatching.progressColour":
|
||||
if v, ok := value.(string); ok {
|
||||
document.ContinueWatching.ProgressColour = v
|
||||
}
|
||||
case "home.heroRefreshSeconds":
|
||||
if v, ok := value.(float64); ok {
|
||||
document.Home.HeroRefreshSeconds = int(v)
|
||||
}
|
||||
case "home.maxItemsPerRow":
|
||||
if v, ok := value.(float64); ok {
|
||||
document.Home.MaxItemsPerRow = int(v)
|
||||
}
|
||||
case "home.sectionDefinitions":
|
||||
if err := json.Unmarshal(raw, &document.Home.SectionDefinitions); err == nil {
|
||||
document.Home.Sections = sectionIDs(document.Home.SectionDefinitions)
|
||||
}
|
||||
case "movies.sectionDefinitions":
|
||||
if err := json.Unmarshal(raw, &document.Movies.SectionDefinitions); err == nil {
|
||||
document.Movies.Sections = sectionIDs(document.Movies.SectionDefinitions)
|
||||
}
|
||||
case "tv.sectionDefinitions":
|
||||
if err := json.Unmarshal(raw, &document.TV.SectionDefinitions); err == nil {
|
||||
document.TV.Sections = sectionIDs(document.TV.SectionDefinitions)
|
||||
}
|
||||
case "hero.enabled":
|
||||
if v, ok := value.(bool); ok {
|
||||
document.Features.Flags["hero.enabled"] = v
|
||||
}
|
||||
case "branding.markUrl":
|
||||
if v, ok := value.(string); ok {
|
||||
document.Branding.MarkURL = v
|
||||
}
|
||||
case "branding.markVersion":
|
||||
if v, ok := value.(string); ok {
|
||||
document.Branding.MarkVersion = v
|
||||
}
|
||||
}
|
||||
if v, ok := value.(bool); ok {
|
||||
document.Features.Flags[key] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sectionIDs(definitions []config.RemoteSectionDefinition) []string {
|
||||
ids := make([]string, 0, len(definitions))
|
||||
for _, definition := range definitions {
|
||||
if definition.Enabled {
|
||||
ids = append(ids, definition.ID)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func configVersionHeader(version int64) string {
|
||||
return strconv.FormatInt(version, 10)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user