From 5fbc4753de60a6391615c3fe6cf95c89676a3a89 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 29 Feb 2020 22:37:22 -0700 Subject: [PATCH] /state and /register working --- cmd/mothd/award.go | 24 +++---- cmd/mothd/award_test.go | 3 +- cmd/mothd/common.go | 11 ++- cmd/mothd/httpd.go | 55 ++++++++------ cmd/mothd/jsend.go | 10 +-- cmd/mothd/main.go | 2 +- cmd/mothd/mothballs.go | 6 +- cmd/mothd/state.go | 155 ++++++++++++++++------------------------ cmd/mothd/theme_test.go | 2 +- cmd/mothd/zipfs.go | 4 +- cmd/mothd/zipfs_test.go | 5 +- theme/moth.js | 45 +++--------- 12 files changed, 142 insertions(+), 180 deletions(-) diff --git a/cmd/mothd/award.go b/cmd/mothd/award.go index b07add6..39c2459 100644 --- a/cmd/mothd/award.go +++ b/cmd/mothd/award.go @@ -1,9 +1,9 @@ package main import ( + "encoding/json" "fmt" "strings" - "encoding/json" ) type Award struct { @@ -34,17 +34,17 @@ func (a *Award) String() string { } func (a *Award) MarshalJSON() ([]byte, error) { - if a == nil { - return []byte("null"), nil - } - ao := []interface{}{ - a.When, - a.TeamId, - a.Category, - a.Points, - } - - return json.Marshal(ao) + if a == nil { + return []byte("null"), nil + } + ao := []interface{}{ + a.When, + a.TeamId, + a.Category, + a.Points, + } + + return json.Marshal(ao) } func (a *Award) Same(o *Award) bool { diff --git a/cmd/mothd/award_test.go b/cmd/mothd/award_test.go index 6c24e21..7239156 100644 --- a/cmd/mothd/award_test.go +++ b/cmd/mothd/award_test.go @@ -30,8 +30,7 @@ func TestAward(t *testing.T) { } else if string(ja) != `[1536958399,"1a2b3c4d","counting",1]` { t.Error("JSON wrong") } - - + if _, err := ParseAward("bad bad bad 1"); err == nil { t.Error("Not throwing error on bad timestamp") } diff --git a/cmd/mothd/common.go b/cmd/mothd/common.go index 2e07ea1..16cf575 100644 --- a/cmd/mothd/common.go +++ b/cmd/mothd/common.go @@ -6,7 +6,7 @@ import ( ) type Category struct { - Name string + Name string Puzzles []int } @@ -27,8 +27,15 @@ type ThemeProvider interface { ModTime(path string) (time.Time, error) } +type StateExport struct { + Messages string + TeamNames map[string]string + PointsLog []Award +} + type StateProvider interface { - + Export(teamId string) *StateExport + SetTeamName(teamId, teamName string) error } type Component interface { diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index 29cdaa5..38e3d76 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -1,24 +1,24 @@ package main import ( - "net/http" "log" + "net/http" "strings" ) type HTTPServer struct { - PuzzleProvider - ThemeProvider - StateProvider *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{ - ThemeProvider: theme, - StateProvider: state, - PuzzleProvider: puzzles, ServeMux: http.NewServeMux(), + Puzzles: puzzles, + Theme: theme, + State: state, } base = strings.TrimRight(base, "/") h.HandleFunc(base+"/", h.ThemeHandler) @@ -47,7 +47,7 @@ func (w MothResponseWriter) WriteHeader(statusCode int) { // This gives Instances the signature of http.Handler func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { w := MothResponseWriter{ - statusCode: new(int), + statusCode: new(int), ResponseWriter: wOrig, } h.ServeMux.ServeHTTP(w, r) @@ -66,37 +66,48 @@ func (h *HTTPServer) ThemeHandler(w http.ResponseWriter, req *http.Request) { path = "/index.html" } - f, err := h.ThemeProvider.Open(path) + f, err := h.Theme.Open(path) if err != nil { http.NotFound(w, req) return } defer f.Close() - mtime, _ := h.ThemeProvider.ModTime(path) + mtime, _ := h.Theme.ModTime(path) http.ServeContent(w, req, path, mtime, f) } - func (h *HTTPServer) StateHandler(w http.ResponseWriter, req *http.Request) { var state struct { Config struct { - Devel bool `json:"devel"` - } `json:"config"` - Messages string `json:"messages"` - Teams []string `json:"teams"` - Points []Award `json:"points"` - Puzzles map[string][]int `json:"puzzles"` + Devel bool + } + Messages string + TeamNames map[string]string + PointsLog []Award + Puzzles map[string][]int } - state.Messages = "Hello world" - state.Teams = []string{"goobers"} - state.Points = []Award{{0, "0", "sequence", 1}} + + teamId := req.FormValue("id") + 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}} JSONWrite(w, state) } 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) { diff --git a/cmd/mothd/jsend.go b/cmd/mothd/jsend.go index d5036d6..3c461aa 100644 --- a/cmd/mothd/jsend.go +++ b/cmd/mothd/jsend.go @@ -21,7 +21,7 @@ func JSONWrite(w http.ResponseWriter, data interface{}) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - + w.Header().Set("Content-Type", "application/json") 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 @@ -29,8 +29,8 @@ func JSONWrite(w http.ResponseWriter, data interface{}) { } func JSend(w http.ResponseWriter, status string, data interface{}) { - resp := struct{ - Status string `json:"status"` + resp := struct { + Status string `json:"status"` Data interface{} `json:"data"` }{} 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{}) { - data := struct{ + data := struct { Short string `json:"short"` Description string `json:"description"` }{} data.Short = short data.Description = fmt.Sprintf(format, a...) - + JSend(w, status, data) } diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go index 2ffb1d1..413ad77 100644 --- a/cmd/mothd/main.go +++ b/cmd/mothd/main.go @@ -14,7 +14,7 @@ func custodian(updateInterval time.Duration, components []Component) { c.Update() } } - + ticker := time.NewTicker(updateInterval) update() for _ = range ticker.C { diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index 4877f26..7af6ae3 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -2,8 +2,8 @@ package main import ( "github.com/spf13/afero" - "log" "io" + "log" "strings" ) @@ -14,7 +14,7 @@ type Mothballs struct { func NewMothballs(fs afero.Fs) *Mothballs { return &Mothballs{ - Fs: fs, + Fs: fs, categories: make(map[string]*Zipfs), } } @@ -33,7 +33,6 @@ func (m *Mothballs) Inventory() []Category { return []Category{} } - func (m *Mothballs) Update() { // Any new categories? files, err := afero.ReadDir(m.Fs, "/") @@ -59,4 +58,3 @@ func (m *Mothballs) Update() { } } } - diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 081b644..ff3db54 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -14,34 +14,19 @@ import ( ) // Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift -const distinguishableChars = "234678abcdefhijkmnpqrtwxyz=" - -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 -} +const DistinguishableChars = "234678abcdefhikmnpqrtwxyz=" // We use the filesystem for synchronization between threads. // The only thing State methods need to know is the path to the state directory. type State struct { - Enabled bool afero.Fs + Enabled bool } func NewState(fs afero.Fs) *State { return &State{ - Enabled: true, Fs: fs, + Enabled: true, } } @@ -49,7 +34,7 @@ func NewState(fs afero.Fs) *State { func (s *State) UpdateEnabled() { if _, err := s.Stat("enabled"); os.IsNotExist(err) { s.Enabled = false - log.Print("Suspended: enabled file missing") + log.Println("Suspended: enabled file missing") return } @@ -78,12 +63,12 @@ func (s *State) UpdateEnabled() { case '#': continue default: - log.Printf("Misformatted line in hours file") + log.Println("Misformatted line in hours file") } line = strings.TrimSpace(line) until, err := time.Parse(time.RFC3339, line) if err != nil { - log.Printf("Suspended: Unparseable until date: %s", line) + log.Println("Suspended: Unparseable until date:", line) continue } 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. -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 { return fmt.Errorf("Team IDs file does not exist") } else { @@ -161,53 +146,33 @@ func (s *State) PointsLog() []*Award { return pointsLog } -// Return an exportable points log, -// This anonymizes teamId with either an integer, or the string "self" -// for the requesting teamId. +// Return an exportable points log for a client func (s *State) Export(teamId string) *StateExport { + var export StateExport + + bMessages, _ := afero.ReadFile(s, "messages.html") + export.Messages = string(bMessages) + teamName, _ := s.TeamName(teamId) + export.TeamNames = map[string]string{"self": teamName} pointsLog := s.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) - } - } + export.PointsLog = make([]Award, len(pointsLog)) // Read in points exportIds := map[string]string{teamId: "self"} for logno, award := range pointsLog { - exportAward := award + exportAward := *award if id, ok := exportIds[award.TeamId]; ok { exportAward.TeamId = id } else { exportId := strconv.Itoa(logno) + name, _ := s.TeamName(award.TeamId) exportAward.TeamId = exportId 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.PointsLog[logno] = *exportAward + export.PointsLog[logno] = exportAward } return &export @@ -308,13 +273,14 @@ func (s *State) maybeInitialize() { return } + now := time.Now().UTC().Format(time.RFC3339) log.Print("initialized file missing, re-initializing") // Remove any extant control and state files s.Remove("enabled") s.Remove("hours") s.Remove("points.log") - s.Remove("messages.txt") + s.Remove("messages.html") s.RemoveAll("points.tmp") s.RemoveAll("points.new") s.RemoveAll("teams") @@ -326,49 +292,54 @@ func (s *State) maybeInitialize() { // 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 { - defer f.Close() + id := make([]byte, 8) 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 - afero.WriteFile( - s, - "initialized", - []byte("state/initialized: remove to re-initialize the contest\n"), - 0644, - ) - afero.WriteFile( - s, - "enabled", - []byte("state/enabled: remove to suspend the contest\n"), - 0644, - ) - afero.WriteFile( - s, - "hours", - []byte( - "# state/hours: when the contest is enabled\n"+ - "# Lines starting with + enable, with - disable.\n"+ - "\n"+ - "+ 1970-01-01T00:00:00Z\n"+ - "- 3019-10-31T00:00:00Z\n", - ), - 0644, - ) - afero.WriteFile( - s, - "messages.txt", - []byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))), - 0644, - ) - afero.WriteFile( - s, - "points.log", - []byte(""), - 0644, - ) + if f, err := s.Create("initialized"); err == nil { + fmt.Fprintln(f, "initialized: remove to re-initialize the contest.") + fmt.Fprintln(f) + fmt.Fprintln(f, "This instance was initaliazed at", now) + f.Close() + } + + if f, err := s.Create("enabled"); err == nil { + fmt.Fprintln(f, "enabled: remove or rename to suspend the contest.") + f.Close() + } + + if f, err := s.Create("hours"); err == nil { + fmt.Fprintln(f, "# hours: when the contest is enabled") + fmt.Fprintln(f, "#") + fmt.Fprintln(f, "# Enable: + timestamp") + fmt.Fprintln(f, "# Disable: - timestamp") + fmt.Fprintln(f, "#") + fmt.Fprintln(f, "# You can have multiple start/stop times.") + fmt.Fprintln(f, "# Whatever time is the most recent, wins.") + fmt.Fprintln(f, "# Times in the future are ignored.") + fmt.Fprintln(f) + fmt.Fprintln(f, "+", now) + fmt.Fprintln(f, "- 3019-10-31T00:00:00Z") + f.Close() + } + + if f, err := s.Create("messages.html"); err == nil { + fmt.Fprintln(f, "") + f.Close() + } + + if f, err := s.Create("points.log"); err == nil { + f.Close() + } + } func (s *State) Update() { diff --git a/cmd/mothd/theme_test.go b/cmd/mothd/theme_test.go index 28f3fe9..ff2653d 100644 --- a/cmd/mothd/theme_test.go +++ b/cmd/mothd/theme_test.go @@ -2,8 +2,8 @@ package main import ( "github.com/spf13/afero" - "testing" "io/ioutil" + "testing" ) func TestTheme(t *testing.T) { diff --git a/cmd/mothd/zipfs.go b/cmd/mothd/zipfs.go index a955e35..ed26d4a 100644 --- a/cmd/mothd/zipfs.go +++ b/cmd/mothd/zipfs.go @@ -3,9 +3,9 @@ package main import ( "archive/zip" "fmt" + "github.com/spf13/afero" "io" "io/ioutil" - "github.com/spf13/afero" "strings" "time" ) @@ -146,7 +146,7 @@ func (zfs *Zipfs) Refresh() error { if err != nil { return err } - + zf, err := zip.NewReader(f, info.Size()) if err != nil { f.Close() diff --git a/cmd/mothd/zipfs_test.go b/cmd/mothd/zipfs_test.go index 8b42169..1a9e0fa 100644 --- a/cmd/mothd/zipfs_test.go +++ b/cmd/mothd/zipfs_test.go @@ -3,15 +3,14 @@ package main import ( "archive/zip" "fmt" - "io" "github.com/spf13/afero" + "io" "testing" ) - func TestZipfs(t *testing.T) { fs := new(afero.MemMapFs) - + tf, err := fs.Create("/test.zip") if err != nil { t.Error(err) diff --git a/theme/moth.js b/theme/moth.js index b29ac42..2e9d3b8 100644 --- a/theme/moth.js +++ b/theme/moth.js @@ -57,8 +57,13 @@ function renderPuzzles(obj) { let l = document.createElement('ul') pdiv.appendChild(l) for (let puzzle of puzzles) { - let points = puzzle[0] - let id = puzzle[1] + let points = puzzle + let id = puzzle + + if (Array.isArray(puzzle)) { + points = puzzle[0] + id = puzzle[1] + } let i = document.createElement('li') l.appendChild(i) @@ -91,15 +96,15 @@ function renderPuzzles(obj) { } function renderState(obj) { - devel = obj.config.devel + devel = obj.Config.Devel if (devel) { sessionStorage.id = "1234" sessionStorage.pid = "rodney" } - if (Object.keys(obj.puzzles).length > 0) { - renderPuzzles(obj.puzzles) + if (Object.keys(obj.Puzzles).length > 0) { + renderPuzzles(obj.Puzzles) } - renderNotices(obj.messages) + renderNotices(obj.Messages) } function heartbeat() { @@ -136,34 +141,6 @@ function showPuzzles() { document.getElementById("login").style.display = "none" document.getElementById("puzzles").appendChild(spinner) 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() {