Notifications: staff turn them on per device from a new Notifications page, pick each type (bans, gags, mutes and silences added or lifted, server down and back up) and quiet hours. A watcher polls SimpleAdmin's tables, so in-game penalties are sent too. Web Push with VAPID and RFC 8291 encryption in internal/push, standard library only. Tables in sql/push.sql, created at startup; the VAPID key is made on first start and kept in saw_settings. Penalty notifications link to the public list searching for the player (#bans?q=<steamid64>). The Android app delegates notifications (versionCode 3). Caching: pages, scripts and styles are served with Cache-Control: no-cache and an ETag, so a deploy shows up at once instead of after Cloudflare's default 4-hour browser cache.
99 lines
3.2 KiB
Go
99 lines
3.2 KiB
Go
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) {
|
|
serveAsset(w, r, static, "sw.js")
|
|
}
|
|
}
|