mirror of https://github.com/dirtbags/moth.git
Running again, fix scoreboard.js
This commit is contained in:
parent
cedc8521ff
commit
ab0488ba14
|
@ -9,7 +9,7 @@ import (
|
||||||
type Award struct {
|
type Award struct {
|
||||||
// Unix epoch time of this event
|
// Unix epoch time of this event
|
||||||
When int64
|
When int64
|
||||||
TeamId string
|
TeamID string
|
||||||
Category string
|
Category string
|
||||||
Points int
|
Points int
|
||||||
}
|
}
|
||||||
|
@ -18,26 +18,25 @@ type AwardList []*Award
|
||||||
|
|
||||||
// Implement sort.Interface on AwardList
|
// Implement sort.Interface on AwardList
|
||||||
func (awards AwardList) Len() int {
|
func (awards AwardList) Len() int {
|
||||||
return len(awards)
|
return len(awards)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (awards AwardList) Less(i, j int) bool {
|
func (awards AwardList) Less(i, j int) bool {
|
||||||
return awards[i].When.Before(awards[j].When)
|
return awards[i].When < awards[j].When
|
||||||
}
|
}
|
||||||
|
|
||||||
func (awards AwardList) Swap(i, j int) {
|
func (awards AwardList) Swap(i, j int) {
|
||||||
tmp := awards[i]
|
tmp := awards[i]
|
||||||
awards[i] = awards[j]
|
awards[i] = awards[j]
|
||||||
awards[j] = tmp
|
awards[j] = tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func ParseAward(s string) (*Award, error) {
|
func ParseAward(s string) (*Award, error) {
|
||||||
ret := Award{}
|
ret := Award{}
|
||||||
|
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
|
|
||||||
n, err := fmt.Sscanf(s, "%d %s %s %d", &ret.When, &ret.TeamId, &ret.Category, &ret.Points)
|
n, err := fmt.Sscanf(s, "%d %s %s %d", &ret.When, &ret.TeamID, &ret.Category, &ret.Points)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if n != 4 {
|
} else if n != 4 {
|
||||||
|
@ -48,7 +47,7 @@ func ParseAward(s string) (*Award, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Award) String() string {
|
func (a *Award) String() string {
|
||||||
return fmt.Sprintf("%d %s %s %d", a.When, a.TeamId, a.Category, a.Points)
|
return fmt.Sprintf("%d %s %s %d", a.When, a.TeamID, a.Category, a.Points)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Award) MarshalJSON() ([]byte, error) {
|
func (a *Award) MarshalJSON() ([]byte, error) {
|
||||||
|
@ -57,7 +56,7 @@ func (a *Award) MarshalJSON() ([]byte, error) {
|
||||||
}
|
}
|
||||||
ao := []interface{}{
|
ao := []interface{}{
|
||||||
a.When,
|
a.When,
|
||||||
a.TeamId,
|
a.TeamID,
|
||||||
a.Category,
|
a.Category,
|
||||||
a.Points,
|
a.Points,
|
||||||
}
|
}
|
||||||
|
@ -67,7 +66,7 @@ func (a *Award) MarshalJSON() ([]byte, error) {
|
||||||
|
|
||||||
func (a *Award) Same(o *Award) bool {
|
func (a *Award) Same(o *Award) bool {
|
||||||
switch {
|
switch {
|
||||||
case a.TeamId != o.TeamId:
|
case a.TeamID != o.TeamID:
|
||||||
return false
|
return false
|
||||||
case a.Category != o.Category:
|
case a.Category != o.Category:
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAward(t *testing.T) {
|
func TestAward(t *testing.T) {
|
||||||
|
@ -12,7 +12,7 @@ func TestAward(t *testing.T) {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if a.TeamId != "1a2b3c4d" {
|
if a.TeamID != "1a2b3c4d" {
|
||||||
t.Error("TeamID parsed wrong")
|
t.Error("TeamID parsed wrong")
|
||||||
}
|
}
|
||||||
if a.Category != "counting" {
|
if a.Category != "counting" {
|
||||||
|
@ -41,21 +41,21 @@ func TestAward(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAwardList(t *testing.T) {
|
func TestAwardList(t *testing.T) {
|
||||||
a, _ := ParseAward("1536958399 1a2b3c4d counting 1")
|
a, _ := ParseAward("1536958399 1a2b3c4d counting 1")
|
||||||
b, _ := ParseAward("1536958400 1a2b3c4d counting 1")
|
b, _ := ParseAward("1536958400 1a2b3c4d counting 1")
|
||||||
c, _ := ParseAward("1536958300 1a2b3c4d counting 1")
|
c, _ := ParseAward("1536958300 1a2b3c4d counting 1")
|
||||||
list := AwardList{a, b, c}
|
list := AwardList{a, b, c}
|
||||||
|
|
||||||
if sort.IsSorted(list) {
|
if sort.IsSorted(list) {
|
||||||
t.Error("Unsorted list thinks it's sorted")
|
t.Error("Unsorted list thinks it's sorted")
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Stable(list)
|
sort.Stable(list)
|
||||||
if (list[0] != c) || (list[1] != a) || (list[2] != b) {
|
if (list[0] != c) || (list[1] != a) || (list[2] != b) {
|
||||||
t.Error("Sorting didn't")
|
t.Error("Sorting didn't")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ! sort.IsSorted(list) {
|
if !sort.IsSorted(list) {
|
||||||
t.Error("Sorted list thinks it isn't")
|
t.Error("Sorted list thinks it isn't")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,16 +3,18 @@ package main
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HTTPServer is a MOTH HTTP server
|
||||||
type HTTPServer struct {
|
type HTTPServer struct {
|
||||||
*http.ServeMux
|
*http.ServeMux
|
||||||
server *MothServer
|
server *MothServer
|
||||||
base string
|
base string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewHTTPServer creates a MOTH HTTP server, with handler functions registered
|
||||||
func NewHTTPServer(base string, server *MothServer) *HTTPServer {
|
func NewHTTPServer(base string, server *MothServer) *HTTPServer {
|
||||||
base = strings.TrimRight(base, "/")
|
base = strings.TrimRight(base, "/")
|
||||||
h := &HTTPServer{
|
h := &HTTPServer{
|
||||||
|
@ -28,6 +30,7 @@ func NewHTTPServer(base string, server *MothServer) *HTTPServer {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleMothFunc binds a new handler function which creates a new MothServer with every request
|
||||||
func (h *HTTPServer) HandleMothFunc(
|
func (h *HTTPServer) HandleMothFunc(
|
||||||
pattern string,
|
pattern string,
|
||||||
mothHandler func(MothRequestHandler, http.ResponseWriter, *http.Request),
|
mothHandler func(MothRequestHandler, http.ResponseWriter, *http.Request),
|
||||||
|
@ -36,10 +39,10 @@ func (h *HTTPServer) HandleMothFunc(
|
||||||
mh := h.server.NewHandler(req.FormValue("id"))
|
mh := h.server.NewHandler(req.FormValue("id"))
|
||||||
mothHandler(mh, w, req)
|
mothHandler(mh, w, req)
|
||||||
}
|
}
|
||||||
h.HandleFunc(h.base + pattern, handler)
|
h.HandleFunc(h.base+pattern, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This gives Instances the signature of http.Handler
|
// ServeHTTP provides the http.Handler interface
|
||||||
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),
|
||||||
|
@ -55,21 +58,25 @@ func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MothResponseWriter provides a ResponseWriter that remembers what the status code was
|
||||||
type MothResponseWriter struct {
|
type MothResponseWriter struct {
|
||||||
statusCode *int
|
statusCode *int
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteHeader sends an HTTP response header with the provided status code
|
||||||
func (w MothResponseWriter) WriteHeader(statusCode int) {
|
func (w MothResponseWriter) WriteHeader(statusCode int) {
|
||||||
*w.statusCode = statusCode
|
*w.statusCode = statusCode
|
||||||
w.ResponseWriter.WriteHeader(statusCode)
|
w.ResponseWriter.WriteHeader(statusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run binds to the provided bindStr, and serves incoming requests until failure
|
||||||
func (h *HTTPServer) Run(bindStr string) {
|
func (h *HTTPServer) Run(bindStr string) {
|
||||||
log.Printf("Listening on %s", bindStr)
|
log.Printf("Listening on %s", bindStr)
|
||||||
log.Fatal(http.ListenAndServe(bindStr, h))
|
log.Fatal(http.ListenAndServe(bindStr, h))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ThemeHandler serves up static content from the theme directory
|
||||||
func (h *HTTPServer) ThemeHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
func (h *HTTPServer) ThemeHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
||||||
path := req.URL.Path
|
path := req.URL.Path
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
|
@ -85,10 +92,12 @@ func (h *HTTPServer) ThemeHandler(mh MothRequestHandler, w http.ResponseWriter,
|
||||||
http.ServeContent(w, req, path, mtime, f)
|
http.ServeContent(w, req, path, mtime, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StateHandler returns the full JSON-encoded state of the event
|
||||||
func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
||||||
JSONWrite(w, mh.ExportState())
|
JSONWrite(w, mh.ExportState())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterHandler handles attempts to register a team
|
||||||
func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
||||||
teamName := req.FormValue("name")
|
teamName := req.FormValue("name")
|
||||||
if err := mh.Register(teamName); err != nil {
|
if err := mh.Register(teamName); err != nil {
|
||||||
|
@ -98,6 +107,7 @@ func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWrite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AnswerHandler checks answer correctness and awards points
|
||||||
func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
||||||
cat := req.FormValue("cat")
|
cat := req.FormValue("cat")
|
||||||
pointstr := req.FormValue("points")
|
pointstr := req.FormValue("points")
|
||||||
|
@ -112,19 +122,20 @@ func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContentHandler returns static content from a given puzzle
|
||||||
func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
||||||
trimLen := len(h.base) + len("/content/")
|
trimLen := len(h.base) + len("/content/")
|
||||||
parts := strings.SplitN(req.URL.Path[trimLen:], "/", 3)
|
parts := strings.SplitN(req.URL.Path[trimLen:], "/", 3)
|
||||||
if len(parts) < 3 {
|
if len(parts) < 3 {
|
||||||
http.Error(w, "Not Found", http.StatusNotFound)
|
http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cat := parts[0]
|
cat := parts[0]
|
||||||
pointsStr := parts[1]
|
pointsStr := parts[1]
|
||||||
filename := parts[2]
|
filename := parts[2]
|
||||||
|
|
||||||
if (filename == "") {
|
if filename == "" {
|
||||||
filename = "puzzles.json"
|
filename = "puzzles.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,5 +148,5 @@ func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter
|
||||||
}
|
}
|
||||||
defer mf.Close()
|
defer mf.Close()
|
||||||
|
|
||||||
http.ServeContent(w, req, filename, mtime, mf)
|
http.ServeContent(w, req, filename, mtime, mf)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,11 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"github.com/spf13/afero"
|
|
||||||
"log"
|
"log"
|
||||||
"mime"
|
"mime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
func custodian(updateInterval time.Duration, components []Component) {
|
func custodian(updateInterval time.Duration, components []Component) {
|
||||||
|
@ -17,7 +18,7 @@ func custodian(updateInterval time.Duration, components []Component) {
|
||||||
|
|
||||||
ticker := time.NewTicker(updateInterval)
|
ticker := time.NewTicker(updateInterval)
|
||||||
update()
|
update()
|
||||||
for _ = range ticker.C {
|
for range ticker.C {
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Category struct {
|
type Category struct {
|
||||||
|
@ -25,7 +25,7 @@ type StateExport struct {
|
||||||
Messages string
|
Messages string
|
||||||
TeamNames map[string]string
|
TeamNames map[string]string
|
||||||
PointsLog []Award
|
PointsLog []Award
|
||||||
Puzzles map[string][]int
|
Puzzles map[string][]int
|
||||||
}
|
}
|
||||||
|
|
||||||
type PuzzleProvider interface {
|
type PuzzleProvider interface {
|
||||||
|
@ -49,30 +49,28 @@ type StateProvider interface {
|
||||||
Component
|
Component
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Component interface {
|
type Component interface {
|
||||||
Update()
|
Update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type MothServer struct {
|
type MothServer struct {
|
||||||
Puzzles PuzzleProvider
|
Puzzles PuzzleProvider
|
||||||
Theme ThemeProvider
|
Theme ThemeProvider
|
||||||
State StateProvider
|
State StateProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMothServer(puzzles PuzzleProvider, theme ThemeProvider, state StateProvider) *MothServer {
|
func NewMothServer(puzzles PuzzleProvider, theme ThemeProvider, state StateProvider) *MothServer {
|
||||||
return &MothServer{
|
return &MothServer{
|
||||||
Puzzles: puzzles,
|
Puzzles: puzzles,
|
||||||
Theme: theme,
|
Theme: theme,
|
||||||
State: state,
|
State: state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MothServer) NewHandler(teamId string) MothRequestHandler {
|
func (s *MothServer) NewHandler(teamId string) MothRequestHandler {
|
||||||
return MothRequestHandler{
|
return MothRequestHandler{
|
||||||
MothServer: s,
|
MothServer: s,
|
||||||
teamId: teamId,
|
teamId: teamId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,13 +133,13 @@ func (mh *MothRequestHandler) ExportAllState() *StateExport {
|
||||||
export.PointsLog = make([]Award, len(pointsLog))
|
export.PointsLog = make([]Award, len(pointsLog))
|
||||||
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, _ := mh.State.TeamName(award.TeamId)
|
name, _ := mh.State.TeamName(award.TeamID)
|
||||||
exportAward.TeamId = exportId
|
exportAward.TeamID = exportId
|
||||||
exportIds[award.TeamId] = exportAward.TeamId
|
exportIds[award.TeamID] = exportAward.TeamID
|
||||||
export.TeamNames[exportId] = name
|
export.TeamNames[exportId] = name
|
||||||
}
|
}
|
||||||
export.PointsLog[logno] = exportAward
|
export.PointsLog[logno] = exportAward
|
||||||
|
@ -152,7 +150,6 @@ func (mh *MothRequestHandler) ExportAllState() *StateExport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export.Puzzles = make(map[string][]int)
|
export.Puzzles = make(map[string][]int)
|
||||||
for _, category := range mh.Puzzles.Inventory() {
|
for _, category := range mh.Puzzles.Inventory() {
|
||||||
// Append sentry (end of puzzles)
|
// Append sentry (end of puzzles)
|
||||||
|
|
|
@ -3,13 +3,14 @@ package main
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/afero"
|
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -81,30 +82,30 @@ func (s *State) UpdateEnabled() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns team name given a team ID.
|
// Returns team name given a team ID.
|
||||||
func (s *State) TeamName(teamId string) (string, error) {
|
func (s *State) TeamName(teamID string) (string, error) {
|
||||||
// XXX: directory traversal
|
// XXX: directory traversal
|
||||||
teamFile := filepath.Join("teams", teamId)
|
teamFile := filepath.Join("teams", teamID)
|
||||||
teamNameBytes, err := afero.ReadFile(s, teamFile)
|
teamNameBytes, err := afero.ReadFile(s, teamFile)
|
||||||
teamName := strings.TrimSpace(string(teamNameBytes))
|
teamName := strings.TrimSpace(string(teamNameBytes))
|
||||||
|
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return "", fmt.Errorf("Unregistered team ID: %s", teamId)
|
return "", fmt.Errorf("Unregistered team ID: %s", teamID)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamId, err)
|
return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return teamName, nil
|
return teamName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, 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 {
|
||||||
found := false
|
found := false
|
||||||
scanner := bufio.NewScanner(f)
|
scanner := bufio.NewScanner(f)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
if scanner.Text() == teamId {
|
if scanner.Text() == teamID {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -115,7 +116,7 @@ func (s *State) SetTeamName(teamId, teamName string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
teamFile := filepath.Join("teams", teamId)
|
teamFile := filepath.Join("teams", teamID)
|
||||||
err := afero.WriteFile(s, teamFile, []byte(teamName), os.ModeExclusive|0644)
|
err := afero.WriteFile(s, teamFile, []byte(teamName), os.ModeExclusive|0644)
|
||||||
if os.IsExist(err) {
|
if os.IsExist(err) {
|
||||||
return fmt.Errorf("Team ID is already registered")
|
return fmt.Errorf("Team ID is already registered")
|
||||||
|
@ -152,20 +153,20 @@ func (s *State) Messages() string {
|
||||||
return string(bMessages)
|
return string(bMessages)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AwardPoints gives points to teamId in category.
|
// AwardPoints gives points to teamID in category.
|
||||||
// It first checks to make sure these are not duplicate points.
|
// It first checks to make sure these are not duplicate points.
|
||||||
// This is not a perfect check, you can trigger a race condition here.
|
// This is not a perfect check, you can trigger a race condition here.
|
||||||
// It's just a courtesy to the user.
|
// It's just a courtesy to the user.
|
||||||
// The update task makes sure we never have duplicate points in the log.
|
// The update task makes sure we never have duplicate points in the log.
|
||||||
func (s *State) AwardPoints(teamId, category string, points int) error {
|
func (s *State) AwardPoints(teamID, category string, points int) error {
|
||||||
a := Award{
|
a := Award{
|
||||||
When: time.Now().Unix(),
|
When: time.Now().Unix(),
|
||||||
TeamId: teamId,
|
TeamID: teamID,
|
||||||
Category: category,
|
Category: category,
|
||||||
Points: points,
|
Points: points,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.TeamName(teamId)
|
_, err := s.TeamName(teamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -176,7 +177,7 @@ func (s *State) AwardPoints(teamId, category string, points int) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn := fmt.Sprintf("%s-%s-%d", teamId, category, points)
|
fn := fmt.Sprintf("%s-%s-%d", teamID, category, points)
|
||||||
tmpfn := filepath.Join("points.tmp", fn)
|
tmpfn := filepath.Join("points.tmp", fn)
|
||||||
newfn := filepath.Join("points.new", fn)
|
newfn := filepath.Join("points.new", fn)
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"github.com/spf13/afero"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestState(t *testing.T) {
|
func TestState(t *testing.T) {
|
||||||
|
@ -56,7 +57,7 @@ func TestState(t *testing.T) {
|
||||||
pl = s.PointsLog()
|
pl = s.PointsLog()
|
||||||
if len(pl) != 1 {
|
if len(pl) != 1 {
|
||||||
t.Errorf("After awarding points, points log has length %d", len(pl))
|
t.Errorf("After awarding points, points log has length %d", len(pl))
|
||||||
} else if (pl[0].TeamId != teamId) || (pl[0].Category != category) || (pl[0].Points != points) {
|
} else if (pl[0].TeamID != teamId) || (pl[0].Category != category) || (pl[0].Points != points) {
|
||||||
t.Errorf("Incorrect logged award %v", pl)
|
t.Errorf("Incorrect logged award %v", pl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,31 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/spf13/afero"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTheme(t *testing.T) {
|
func TestTheme(t *testing.T) {
|
||||||
|
filename := "/index.html"
|
||||||
fs := new(afero.MemMapFs)
|
fs := new(afero.MemMapFs)
|
||||||
index := "this is the index"
|
index := "this is the index"
|
||||||
afero.WriteFile(fs, "/index.html", []byte(index), 0644)
|
afero.WriteFile(fs, filename, []byte(index), 0644)
|
||||||
|
fileInfo, err := fs.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
s := NewTheme(fs)
|
s := NewTheme(fs)
|
||||||
|
|
||||||
if f, err := s.Open("/index.html"); err != nil {
|
if f, timestamp, err := s.Open("/index.html"); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
} else if buf, err := ioutil.ReadAll(f); err != nil {
|
} else if buf, err := ioutil.ReadAll(f); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
} else if string(buf) != index {
|
} else if string(buf) != index {
|
||||||
t.Error("Read wrong value from index")
|
t.Error("Read wrong value from index")
|
||||||
|
} else if !timestamp.Equal(fileInfo.ModTime()) {
|
||||||
|
t.Error("Timestamp compared wrong")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
module github.com/russross/blackfriday/v2
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/russross/blackfriday v2.0.0+incompatible
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.3.0
|
||||||
|
)
|
|
@ -0,0 +1,7 @@
|
||||||
|
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk=
|
||||||
|
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@ -4,12 +4,13 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gopkg.in/russross/blackfriday.v2"
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/russross/blackfriday"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Attachment struct {
|
type Attachment struct {
|
||||||
|
@ -55,7 +56,7 @@ func YamlParser(input []byte) (*Puzzle, error) {
|
||||||
return puzzle, nil
|
return puzzle, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AttachmentParser(val []string) ([]Attachment) {
|
func AttachmentParser(val []string) []Attachment {
|
||||||
ret := make([]Attachment, len(val))
|
ret := make([]Attachment, len(val))
|
||||||
for idx, txt := range val {
|
for idx, txt := range val {
|
||||||
parts := strings.SplitN(txt, " ", 3)
|
parts := strings.SplitN(txt, " ", 3)
|
||||||
|
|
5
go.mod
5
go.mod
|
@ -4,5 +4,10 @@ go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/namsral/flag v1.7.4-pre
|
github.com/namsral/flag v1.7.4-pre
|
||||||
|
github.com/pkg/sftp v1.11.0 // indirect
|
||||||
|
github.com/russross/blackfriday v2.0.0+incompatible // indirect
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
github.com/spf13/afero v1.2.2
|
github.com/spf13/afero v1.2.2
|
||||||
|
golang.org/x/tools v0.0.0-20200814172026-c4923e618c08 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.3.0
|
||||||
)
|
)
|
||||||
|
|
50
go.sum
50
go.sum
|
@ -1,6 +1,56 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs=
|
github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs=
|
||||||
github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo=
|
github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/sftp v1.11.0 h1:4Zv0OGbpkg4yNuUtH0s8rvoYxRCNyT29NVUo6pgPmxI=
|
||||||
|
github.com/pkg/sftp v1.11.0/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk=
|
||||||
|
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
|
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/yuin/goldmark v1.1.32 h1:5tjfNdR2ki3yYQ842+eX2sQHeiwpKJ0RnHO4IYOc4V8=
|
||||||
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200814172026-c4923e618c08 h1:sfBQLM20fzeXhOixVQirwEbuW4PGStP773EXQpsBB6E=
|
||||||
|
golang.org/x/tools v0.0.0-20200814172026-c4923e618c08/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|
Binary file not shown.
|
@ -18,20 +18,20 @@ function scoreboardInit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let element = document.getElementById("rankings")
|
let element = document.getElementById("rankings")
|
||||||
let teamNames = state.teams
|
let teamNames = state.TeamNames
|
||||||
let pointsLog = state.points
|
let pointsLog = state.PointsLog
|
||||||
|
|
||||||
// Every machine that's displaying the scoreboard helpfully stores the last 20 values of
|
// Every machine that's displaying the scoreboard helpfully stores the last 20 values of
|
||||||
// points.json for us, in case of catastrophe. Thanks, y'all!
|
// points.json for us, in case of catastrophe. Thanks, y'all!
|
||||||
//
|
//
|
||||||
// We have been doing some variation on this "everybody backs up the server state" trick since 2009.
|
// We have been doing some variation on this "everybody backs up the server state" trick since 2009.
|
||||||
// We have needed it 0 times.
|
// We have needed it 0 times.
|
||||||
let pointsHistory = JSON.parse(localStorage.getItem("pointsHistory")) || []
|
let stateHistory = JSON.parse(localStorage.getItem("stateHistory")) || []
|
||||||
if (pointsHistory.length >= 20) {
|
if (stateHistory.length >= 20) {
|
||||||
pointsHistory.shift()
|
stateHistory.shift()
|
||||||
}
|
}
|
||||||
pointsHistory.push(pointsLog)
|
stateHistory.push(state)
|
||||||
localStorage.setItem("pointsHistory", JSON.stringify(pointsHistory))
|
localStorage.setItem("stateHistory", JSON.stringify(stateHistory))
|
||||||
|
|
||||||
let teams = {}
|
let teams = {}
|
||||||
let highestCategoryScore = {} // map[string]int
|
let highestCategoryScore = {} // map[string]int
|
||||||
|
@ -216,7 +216,7 @@ function scoreboardInit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
fetch("points.json")
|
fetch("state")
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
return resp.json()
|
return resp.json()
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue