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
|
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 {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in New Issue