mirror of https://github.com/dirtbags/moth.git
/state and /register working
This commit is contained in:
parent
5e5592b0d4
commit
5fbc4753de
|
@ -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,
|
||||
}
|
||||
if a == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
ao := []interface{}{
|
||||
a.When,
|
||||
a.TeamId,
|
||||
a.Category,
|
||||
a.Points,
|
||||
}
|
||||
|
||||
return json.Marshal(ao)
|
||||
return json.Marshal(ao)
|
||||
}
|
||||
|
||||
func (a *Award) Same(o *Award) bool {
|
||||
|
|
|
@ -31,7 +31,6 @@ func TestAward(t *testing.T) {
|
|||
t.Error("JSON wrong")
|
||||
}
|
||||
|
||||
|
||||
if _, err := ParseAward("bad bad bad 1"); err == nil {
|
||||
t.Error("Not throwing error on bad timestamp")
|
||||
}
|
||||
|
|
|
@ -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 StateProvider interface {
|
||||
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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,7 +40,7 @@ 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"`
|
||||
}{}
|
||||
|
|
|
@ -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() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -2,8 +2,8 @@ package main
|
|||
|
||||
import (
|
||||
"github.com/spf13/afero"
|
||||
"testing"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTheme(t *testing.T) {
|
||||
|
|
|
@ -3,9 +3,9 @@ package main
|
|||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"github.com/spf13/afero"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"github.com/spf13/afero"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
|
|
@ -3,12 +3,11 @@ package main
|
|||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"github.com/spf13/afero"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
||||
func TestZipfs(t *testing.T) {
|
||||
fs := new(afero.MemMapFs)
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue