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.
47 lines
1.4 KiB
Go
47 lines
1.4 KiB
Go
package web
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"io/fs"
|
|
"net/http"
|
|
"sync"
|
|
)
|
|
|
|
// The pages, scripts and styles are embedded, so they only change with a new build. They're served
|
|
// with Cache-Control: no-cache and an ETag (a hash of the file): browsers, the Android app and
|
|
// Cloudflare check each time and get a quick 304 while the file is unchanged, and a new deploy
|
|
// shows up at once. Without these headers Cloudflare tells browsers to keep them for 4 hours.
|
|
|
|
var assetTags = sync.OnceValue(func() map[string]string {
|
|
tags := map[string]string{}
|
|
static, _ := fs.Sub(assets, "assets")
|
|
_ = fs.WalkDir(static, ".", func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return err
|
|
}
|
|
data, err := fs.ReadFile(static, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sum := sha256.Sum256(data)
|
|
tags[path] = `"` + hex.EncodeToString(sum[:8]) + `"`
|
|
return nil
|
|
})
|
|
return tags
|
|
})
|
|
|
|
// revalidate sets the caching headers for an embedded file. http.ServeContent answers a matching
|
|
// If-None-Match with 304 from the ETag set here.
|
|
func revalidate(w http.ResponseWriter, name string) {
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
if tag, ok := assetTags()[name]; ok {
|
|
w.Header().Set("ETag", tag)
|
|
}
|
|
}
|
|
|
|
// serveAsset serves one embedded file, see revalidate.
|
|
func serveAsset(w http.ResponseWriter, r *http.Request, static fs.FS, name string) {
|
|
revalidate(w, name)
|
|
http.ServeFileFS(w, r, static, name)
|
|
}
|