/state and /register working

This commit is contained in:
Neale Pickett 2020-02-29 22:37:22 -07:00
parent 5e5592b0d4
commit 5fbc4753de
12 changed files with 142 additions and 180 deletions

View File

@ -1,9 +1,9 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"strings" "strings"
"encoding/json"
) )
type Award struct { type Award struct {
@ -34,17 +34,17 @@ func (a *Award) String() string {
} }
func (a *Award) MarshalJSON() ([]byte, error) { func (a *Award) MarshalJSON() ([]byte, error) {
if a == nil { if a == nil {
return []byte("null"), nil return []byte("null"), nil
} }
ao := []interface{}{ ao := []interface{}{
a.When, a.When,
a.TeamId, a.TeamId,
a.Category, a.Category,
a.Points, a.Points,
} }
return json.Marshal(ao) return json.Marshal(ao)
} }
func (a *Award) Same(o *Award) bool { func (a *Award) Same(o *Award) bool {

View File

@ -30,8 +30,7 @@ func TestAward(t *testing.T) {
} else if string(ja) != `[1536958399,"1a2b3c4d","counting",1]` { } else if string(ja) != `[1536958399,"1a2b3c4d","counting",1]` {
t.Error("JSON wrong") t.Error("JSON wrong")
} }
if _, err := ParseAward("bad bad bad 1"); err == nil { if _, err := ParseAward("bad bad bad 1"); err == nil {
t.Error("Not throwing error on bad timestamp") t.Error("Not throwing error on bad timestamp")
} }

View File

@ -6,7 +6,7 @@ import (
) )
type Category struct { type Category struct {
Name string Name string
Puzzles []int Puzzles []int
} }
@ -27,8 +27,15 @@ type ThemeProvider interface {
ModTime(path string) (time.Time, error) ModTime(path string) (time.Time, error)
} }
type StateExport struct {
Messages string
TeamNames map[string]string
PointsLog []Award
}
type StateProvider interface { type StateProvider interface {
Export(teamId string) *StateExport
SetTeamName(teamId, teamName string) error
} }
type Component interface { type Component interface {

View File

@ -1,24 +1,24 @@
package main package main
import ( import (
"net/http"
"log" "log"
"net/http"
"strings" "strings"
) )
type HTTPServer struct { type HTTPServer struct {
PuzzleProvider
ThemeProvider
StateProvider
*http.ServeMux *http.ServeMux
Puzzles PuzzleProvider
Theme ThemeProvider
State StateProvider
} }
func NewHTTPServer(base string, theme ThemeProvider, state StateProvider, puzzles PuzzleProvider) (*HTTPServer) { func NewHTTPServer(base string, theme ThemeProvider, state StateProvider, puzzles PuzzleProvider) *HTTPServer {
h := &HTTPServer{ h := &HTTPServer{
ThemeProvider: theme,
StateProvider: state,
PuzzleProvider: puzzles,
ServeMux: http.NewServeMux(), ServeMux: http.NewServeMux(),
Puzzles: puzzles,
Theme: theme,
State: state,
} }
base = strings.TrimRight(base, "/") base = strings.TrimRight(base, "/")
h.HandleFunc(base+"/", h.ThemeHandler) h.HandleFunc(base+"/", h.ThemeHandler)
@ -47,7 +47,7 @@ func (w MothResponseWriter) WriteHeader(statusCode int) {
// This gives Instances the signature of http.Handler // This gives Instances the signature of http.Handler
func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
w := MothResponseWriter{ w := MothResponseWriter{
statusCode: new(int), statusCode: new(int),
ResponseWriter: wOrig, ResponseWriter: wOrig,
} }
h.ServeMux.ServeHTTP(w, r) h.ServeMux.ServeHTTP(w, r)
@ -66,37 +66,48 @@ func (h *HTTPServer) ThemeHandler(w http.ResponseWriter, req *http.Request) {
path = "/index.html" path = "/index.html"
} }
f, err := h.ThemeProvider.Open(path) f, err := h.Theme.Open(path)
if err != nil { if err != nil {
http.NotFound(w, req) http.NotFound(w, req)
return return
} }
defer f.Close() defer f.Close()
mtime, _ := h.ThemeProvider.ModTime(path) mtime, _ := h.Theme.ModTime(path)
http.ServeContent(w, req, path, mtime, f) http.ServeContent(w, req, path, mtime, f)
} }
func (h *HTTPServer) StateHandler(w http.ResponseWriter, req *http.Request) { func (h *HTTPServer) StateHandler(w http.ResponseWriter, req *http.Request) {
var state struct { var state struct {
Config struct { Config struct {
Devel bool `json:"devel"` Devel bool
} `json:"config"` }
Messages string `json:"messages"` Messages string
Teams []string `json:"teams"` TeamNames map[string]string
Points []Award `json:"points"` PointsLog []Award
Puzzles map[string][]int `json:"puzzles"` Puzzles map[string][]int
} }
state.Messages = "Hello world"
state.Teams = []string{"goobers"} teamId := req.FormValue("id")
state.Points = []Award{{0, "0", "sequence", 1}} export := h.State.Export(teamId)
state.Messages = export.Messages
state.TeamNames = export.TeamNames
state.PointsLog = export.PointsLog
// XXX: unstub this
state.Puzzles = map[string][]int{"sequence": {1}} state.Puzzles = map[string][]int{"sequence": {1}}
JSONWrite(w, state) JSONWrite(w, state)
} }
func (h *HTTPServer) RegisterHandler(w http.ResponseWriter, req *http.Request) { func (h *HTTPServer) RegisterHandler(w http.ResponseWriter, req *http.Request) {
JSendf(w, JSendFail, "unimplemented", "I haven't written this yet") teamId := req.FormValue("id")
teamName := req.FormValue("name")
if err := h.State.SetTeamName(teamId, teamName); err != nil {
JSendf(w, JSendFail, "not registered", err.Error())
} else {
JSendf(w, JSendSuccess, "registered", "Team ID registered")
}
} }
func (h *HTTPServer) AnswerHandler(w http.ResponseWriter, req *http.Request) { func (h *HTTPServer) AnswerHandler(w http.ResponseWriter, req *http.Request) {

View File

@ -21,7 +21,7 @@ func JSONWrite(w http.ResponseWriter, data interface{}) {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(respBytes))) w.Header().Set("Content-Length", fmt.Sprintf("%d", len(respBytes)))
w.WriteHeader(http.StatusOK) // RFC2616 makes it pretty clear that 4xx codes are for the user-agent w.WriteHeader(http.StatusOK) // RFC2616 makes it pretty clear that 4xx codes are for the user-agent
@ -29,8 +29,8 @@ func JSONWrite(w http.ResponseWriter, data interface{}) {
} }
func JSend(w http.ResponseWriter, status string, data interface{}) { func JSend(w http.ResponseWriter, status string, data interface{}) {
resp := struct{ resp := struct {
Status string `json:"status"` Status string `json:"status"`
Data interface{} `json:"data"` Data interface{} `json:"data"`
}{} }{}
resp.Status = status resp.Status = status
@ -40,12 +40,12 @@ func JSend(w http.ResponseWriter, status string, data interface{}) {
} }
func JSendf(w http.ResponseWriter, status, short string, format string, a ...interface{}) { func JSendf(w http.ResponseWriter, status, short string, format string, a ...interface{}) {
data := struct{ data := struct {
Short string `json:"short"` Short string `json:"short"`
Description string `json:"description"` Description string `json:"description"`
}{} }{}
data.Short = short data.Short = short
data.Description = fmt.Sprintf(format, a...) data.Description = fmt.Sprintf(format, a...)
JSend(w, status, data) JSend(w, status, data)
} }

View File

@ -14,7 +14,7 @@ func custodian(updateInterval time.Duration, components []Component) {
c.Update() c.Update()
} }
} }
ticker := time.NewTicker(updateInterval) ticker := time.NewTicker(updateInterval)
update() update()
for _ = range ticker.C { for _ = range ticker.C {

View File

@ -2,8 +2,8 @@ package main
import ( import (
"github.com/spf13/afero" "github.com/spf13/afero"
"log"
"io" "io"
"log"
"strings" "strings"
) )
@ -14,7 +14,7 @@ type Mothballs struct {
func NewMothballs(fs afero.Fs) *Mothballs { func NewMothballs(fs afero.Fs) *Mothballs {
return &Mothballs{ return &Mothballs{
Fs: fs, Fs: fs,
categories: make(map[string]*Zipfs), categories: make(map[string]*Zipfs),
} }
} }
@ -33,7 +33,6 @@ func (m *Mothballs) Inventory() []Category {
return []Category{} return []Category{}
} }
func (m *Mothballs) Update() { func (m *Mothballs) Update() {
// Any new categories? // Any new categories?
files, err := afero.ReadDir(m.Fs, "/") files, err := afero.ReadDir(m.Fs, "/")
@ -59,4 +58,3 @@ func (m *Mothballs) Update() {
} }
} }
} }

View File

@ -14,34 +14,19 @@ import (
) )
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift // Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
const distinguishableChars = "234678abcdefhijkmnpqrtwxyz=" const DistinguishableChars = "234678abcdefhikmnpqrtwxyz="
func mktoken() string {
a := make([]byte, 8)
for i := range a {
char := rand.Intn(len(distinguishableChars))
a[i] = distinguishableChars[char]
}
return string(a)
}
type StateExport struct {
TeamNames map[string]string
PointsLog []Award
Messages []string
}
// We use the filesystem for synchronization between threads. // We use the filesystem for synchronization between threads.
// The only thing State methods need to know is the path to the state directory. // The only thing State methods need to know is the path to the state directory.
type State struct { type State struct {
Enabled bool
afero.Fs afero.Fs
Enabled bool
} }
func NewState(fs afero.Fs) *State { func NewState(fs afero.Fs) *State {
return &State{ return &State{
Enabled: true,
Fs: fs, Fs: fs,
Enabled: true,
} }
} }
@ -49,7 +34,7 @@ func NewState(fs afero.Fs) *State {
func (s *State) UpdateEnabled() { func (s *State) UpdateEnabled() {
if _, err := s.Stat("enabled"); os.IsNotExist(err) { if _, err := s.Stat("enabled"); os.IsNotExist(err) {
s.Enabled = false s.Enabled = false
log.Print("Suspended: enabled file missing") log.Println("Suspended: enabled file missing")
return return
} }
@ -78,12 +63,12 @@ func (s *State) UpdateEnabled() {
case '#': case '#':
continue continue
default: default:
log.Printf("Misformatted line in hours file") log.Println("Misformatted line in hours file")
} }
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
until, err := time.Parse(time.RFC3339, line) until, err := time.Parse(time.RFC3339, line)
if err != nil { if err != nil {
log.Printf("Suspended: Unparseable until date: %s", line) log.Println("Suspended: Unparseable until date:", line)
continue continue
} }
if until.Before(time.Now()) { if until.Before(time.Now()) {
@ -112,7 +97,7 @@ func (s *State) TeamName(teamId string) (string, error) {
} }
// Write out team name. This can only be done once. // Write out team name. This can only be done once.
func (s *State) SetTeamName(teamId string, teamName string) error { func (s *State) SetTeamName(teamId, teamName string) error {
if f, err := s.Open("teamids.txt"); err != nil { if f, err := s.Open("teamids.txt"); err != nil {
return fmt.Errorf("Team IDs file does not exist") return fmt.Errorf("Team IDs file does not exist")
} else { } else {
@ -161,53 +146,33 @@ func (s *State) PointsLog() []*Award {
return pointsLog return pointsLog
} }
// Return an exportable points log, // Return an exportable points log for a client
// This anonymizes teamId with either an integer, or the string "self"
// for the requesting teamId.
func (s *State) Export(teamId string) *StateExport { func (s *State) Export(teamId string) *StateExport {
var export StateExport
bMessages, _ := afero.ReadFile(s, "messages.html")
export.Messages = string(bMessages)
teamName, _ := s.TeamName(teamId) teamName, _ := s.TeamName(teamId)
export.TeamNames = map[string]string{"self": teamName}
pointsLog := s.PointsLog() pointsLog := s.PointsLog()
export.PointsLog = make([]Award, len(pointsLog))
export := StateExport{
PointsLog: make([]Award, len(pointsLog)),
Messages: make([]string, 0, 10),
TeamNames: map[string]string{"self": teamName},
}
// Read in messages
if f, err := s.Open("messages.txt"); err != nil {
log.Print(err)
} else {
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
message := scanner.Text()
if strings.HasPrefix(message, "#") {
continue
}
export.Messages = append(export.Messages, message)
}
}
// Read in points // Read in points
exportIds := map[string]string{teamId: "self"} exportIds := map[string]string{teamId: "self"}
for logno, award := range pointsLog { for logno, award := range pointsLog {
exportAward := award exportAward := *award
if id, ok := exportIds[award.TeamId]; ok { if id, ok := exportIds[award.TeamId]; ok {
exportAward.TeamId = id exportAward.TeamId = id
} else { } else {
exportId := strconv.Itoa(logno) exportId := strconv.Itoa(logno)
name, _ := s.TeamName(award.TeamId)
exportAward.TeamId = exportId exportAward.TeamId = exportId
exportIds[award.TeamId] = exportAward.TeamId exportIds[award.TeamId] = exportAward.TeamId
name, err := s.TeamName(award.TeamId)
if err != nil {
name = "Rodney" // https://en.wikipedia.org/wiki/Rogue_(video_game)#Gameplay
}
export.TeamNames[exportId] = name export.TeamNames[exportId] = name
} }
export.PointsLog[logno] = *exportAward export.PointsLog[logno] = exportAward
} }
return &export return &export
@ -308,13 +273,14 @@ func (s *State) maybeInitialize() {
return return
} }
now := time.Now().UTC().Format(time.RFC3339)
log.Print("initialized file missing, re-initializing") log.Print("initialized file missing, re-initializing")
// Remove any extant control and state files // Remove any extant control and state files
s.Remove("enabled") s.Remove("enabled")
s.Remove("hours") s.Remove("hours")
s.Remove("points.log") s.Remove("points.log")
s.Remove("messages.txt") s.Remove("messages.html")
s.RemoveAll("points.tmp") s.RemoveAll("points.tmp")
s.RemoveAll("points.new") s.RemoveAll("points.new")
s.RemoveAll("teams") s.RemoveAll("teams")
@ -326,49 +292,54 @@ func (s *State) maybeInitialize() {
// Preseed available team ids if file doesn't exist // Preseed available team ids if file doesn't exist
if f, err := s.OpenFile("teamids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { if f, err := s.OpenFile("teamids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil {
defer f.Close() id := make([]byte, 8)
for i := 0; i < 100; i += 1 { for i := 0; i < 100; i += 1 {
fmt.Fprintln(f, mktoken()) for i := range id {
char := rand.Intn(len(DistinguishableChars))
id[i] = DistinguishableChars[char]
}
fmt.Fprintln(f, string(id))
} }
f.Close()
} }
// Create some files // Create some files
afero.WriteFile( if f, err := s.Create("initialized"); err == nil {
s, fmt.Fprintln(f, "initialized: remove to re-initialize the contest.")
"initialized", fmt.Fprintln(f)
[]byte("state/initialized: remove to re-initialize the contest\n"), fmt.Fprintln(f, "This instance was initaliazed at", now)
0644, f.Close()
) }
afero.WriteFile(
s, if f, err := s.Create("enabled"); err == nil {
"enabled", fmt.Fprintln(f, "enabled: remove or rename to suspend the contest.")
[]byte("state/enabled: remove to suspend the contest\n"), f.Close()
0644, }
)
afero.WriteFile( if f, err := s.Create("hours"); err == nil {
s, fmt.Fprintln(f, "# hours: when the contest is enabled")
"hours", fmt.Fprintln(f, "#")
[]byte( fmt.Fprintln(f, "# Enable: + timestamp")
"# state/hours: when the contest is enabled\n"+ fmt.Fprintln(f, "# Disable: - timestamp")
"# Lines starting with + enable, with - disable.\n"+ fmt.Fprintln(f, "#")
"\n"+ fmt.Fprintln(f, "# You can have multiple start/stop times.")
"+ 1970-01-01T00:00:00Z\n"+ fmt.Fprintln(f, "# Whatever time is the most recent, wins.")
"- 3019-10-31T00:00:00Z\n", fmt.Fprintln(f, "# Times in the future are ignored.")
), fmt.Fprintln(f)
0644, fmt.Fprintln(f, "+", now)
) fmt.Fprintln(f, "- 3019-10-31T00:00:00Z")
afero.WriteFile( f.Close()
s, }
"messages.txt",
[]byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))), if f, err := s.Create("messages.html"); err == nil {
0644, fmt.Fprintln(f, "<!-- messages.html: put client broadcast messages here. -->")
) f.Close()
afero.WriteFile( }
s,
"points.log", if f, err := s.Create("points.log"); err == nil {
[]byte(""), f.Close()
0644, }
)
} }
func (s *State) Update() { func (s *State) Update() {

View File

@ -2,8 +2,8 @@ package main
import ( import (
"github.com/spf13/afero" "github.com/spf13/afero"
"testing"
"io/ioutil" "io/ioutil"
"testing"
) )
func TestTheme(t *testing.T) { func TestTheme(t *testing.T) {

View File

@ -3,9 +3,9 @@ package main
import ( import (
"archive/zip" "archive/zip"
"fmt" "fmt"
"github.com/spf13/afero"
"io" "io"
"io/ioutil" "io/ioutil"
"github.com/spf13/afero"
"strings" "strings"
"time" "time"
) )
@ -146,7 +146,7 @@ func (zfs *Zipfs) Refresh() error {
if err != nil { if err != nil {
return err return err
} }
zf, err := zip.NewReader(f, info.Size()) zf, err := zip.NewReader(f, info.Size())
if err != nil { if err != nil {
f.Close() f.Close()

View File

@ -3,15 +3,14 @@ package main
import ( import (
"archive/zip" "archive/zip"
"fmt" "fmt"
"io"
"github.com/spf13/afero" "github.com/spf13/afero"
"io"
"testing" "testing"
) )
func TestZipfs(t *testing.T) { func TestZipfs(t *testing.T) {
fs := new(afero.MemMapFs) fs := new(afero.MemMapFs)
tf, err := fs.Create("/test.zip") tf, err := fs.Create("/test.zip")
if err != nil { if err != nil {
t.Error(err) t.Error(err)

View File

@ -57,8 +57,13 @@ function renderPuzzles(obj) {
let l = document.createElement('ul') let l = document.createElement('ul')
pdiv.appendChild(l) pdiv.appendChild(l)
for (let puzzle of puzzles) { for (let puzzle of puzzles) {
let points = puzzle[0] let points = puzzle
let id = puzzle[1] let id = puzzle
if (Array.isArray(puzzle)) {
points = puzzle[0]
id = puzzle[1]
}
let i = document.createElement('li') let i = document.createElement('li')
l.appendChild(i) l.appendChild(i)
@ -91,15 +96,15 @@ function renderPuzzles(obj) {
} }
function renderState(obj) { function renderState(obj) {
devel = obj.config.devel devel = obj.Config.Devel
if (devel) { if (devel) {
sessionStorage.id = "1234" sessionStorage.id = "1234"
sessionStorage.pid = "rodney" sessionStorage.pid = "rodney"
} }
if (Object.keys(obj.puzzles).length > 0) { if (Object.keys(obj.Puzzles).length > 0) {
renderPuzzles(obj.puzzles) renderPuzzles(obj.Puzzles)
} }
renderNotices(obj.messages) renderNotices(obj.Messages)
} }
function heartbeat() { function heartbeat() {
@ -136,34 +141,6 @@ function showPuzzles() {
document.getElementById("login").style.display = "none" document.getElementById("login").style.display = "none"
document.getElementById("puzzles").appendChild(spinner) document.getElementById("puzzles").appendChild(spinner)
heartbeat() heartbeat()
drawCacheButton()
}
function drawCacheButton() {
let teamId = sessionStorage.id
let cacher = document.querySelector("#cacheButton")
function updateCacheButton() {
let headers = new Headers()
headers.append("pragma", "no-cache")
headers.append("cache-control", "no-cache")
let url = new URL("current_manifest.json", window.location)
url.searchParams.set("id", teamId)
fetch(url, {method: "HEAD", headers: headers})
.then( resp => {
if (resp.ok) {
cacher.classList.remove("disabled")
} else {
cacher.classList.add("disabled")
}
})
.catch(ex => {
cacher.classList.add("disabled")
})
}
setInterval (updateCacheButton , 30000)
updateCacheButton()
} }
async function fetchAll() { async function fetchAll() {