package web import ( "encoding/json" "io/fs" "net/http" "strings" ) // The site installs as an app (a PWA): a web app manifest, plus the service worker in // assets/sw.js. The public site and the staff panel are separate apps, so each opens on its own // page; both cover the whole site, since signing in goes through /auth. type manifest struct { ID string `json:"id"` Name string `json:"name"` ShortName string `json:"short_name"` Description string `json:"description"` StartURL string `json:"start_url"` Scope string `json:"scope"` Display string `json:"display"` BackgroundColor string `json:"background_color"` ThemeColor string `json:"theme_color"` Icons []manifestIcon `json:"icons"` } type manifestIcon struct { Src string `json:"src"` Sizes string `json:"sizes"` Type string `json:"type"` Purpose string `json:"purpose,omitempty"` } func (s *Server) manifest(staff bool) http.HandlerFunc { m := manifest{ ID: "/", Name: s.site.Title, ShortName: s.site.Title, Description: "Live server, bans and staff.", StartURL: "/", Scope: "/", Display: "standalone", BackgroundColor: "#14161b", ThemeColor: "#1b1e25", Icons: []manifestIcon{ {Src: "/static/icon-192.png", Sizes: "192x192", Type: "image/png"}, {Src: "/static/icon-512.png", Sizes: "512x512", Type: "image/png"}, {Src: "/static/icon-maskable-512.png", Sizes: "512x512", Type: "image/png", Purpose: "maskable"}, }, } if staff { m.ID = "/admin" m.Name = s.site.Title + " staff panel" m.ShortName = s.site.Title + " staff" m.Description = "Bans, mutes, ranks and the live server, for staff." m.StartURL = "/admin" } body, _ := json.Marshal(m) return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/manifest+json") w.Header().Set("Cache-Control", "no-cache") w.Write(body) } } // assetLinks tells Chrome the Android app belongs to this site, so it opens the site full screen // (a Trusted Web Activity) instead of with its URL bar showing. func (s *Server) assetLinks(w http.ResponseWriter, r *http.Request) { var certs []string for _, c := range s.cfg.AndroidCerts { if c = strings.ToUpper(strings.TrimSpace(c)); c != "" { certs = append(certs, c) } } if s.cfg.AndroidPackage == "" || len(certs) == 0 { http.NotFound(w, r) return } type target struct { Namespace string `json:"namespace"` PackageName string `json:"package_name"` Fingerprints []string `json:"sha256_cert_fingerprints"` } type statement struct { Relation []string `json:"relation"` Target target `json:"target"` } writeJSON(w, http.StatusOK, []statement{{ Relation: []string{"delegate_permission/common.handle_all_urls"}, Target: target{Namespace: "android_app", PackageName: s.cfg.AndroidPackage, Fingerprints: certs}, }}) } // serviceWorker serves sw.js from the root, since a worker only controls pages under its own path. func serviceWorker(static fs.FS) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache") http.ServeFileFS(w, r, static, "sw.js") } }