/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
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 {

View File

@ -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")
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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)
}

View File

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

View File

@ -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() {
}
}
}

View File

@ -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, "<!-- messages.html: put client broadcast messages here. -->")
f.Close()
}
if f, err := s.Create("points.log"); err == nil {
f.Close()
}
}
func (s *State) Update() {

View File

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

View File

@ -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()

View File

@ -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)

View File

@ -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() {