mirror of https://github.com/dirtbags/moth.git
Mostly documentation in Go files
This commit is contained in:
parent
a46ae95cb6
commit
2a3826e2b3
|
@ -1,77 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Award struct {
|
||||
// Unix epoch time of this event
|
||||
When int64
|
||||
TeamID string
|
||||
Category string
|
||||
Points int
|
||||
}
|
||||
|
||||
type AwardList []*Award
|
||||
|
||||
// Implement sort.Interface on AwardList
|
||||
func (awards AwardList) Len() int {
|
||||
return len(awards)
|
||||
}
|
||||
|
||||
func (awards AwardList) Less(i, j int) bool {
|
||||
return awards[i].When < awards[j].When
|
||||
}
|
||||
|
||||
func (awards AwardList) Swap(i, j int) {
|
||||
tmp := awards[i]
|
||||
awards[i] = awards[j]
|
||||
awards[j] = tmp
|
||||
}
|
||||
|
||||
func ParseAward(s string) (*Award, error) {
|
||||
ret := Award{}
|
||||
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
n, err := fmt.Sscanf(s, "%d %s %s %d", &ret.When, &ret.TeamID, &ret.Category, &ret.Points)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if n != 4 {
|
||||
return nil, fmt.Errorf("Malformed award string: only parsed %d fields", n)
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (a *Award) String() string {
|
||||
return fmt.Sprintf("%d %s %s %d", a.When, a.TeamID, a.Category, a.Points)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (a *Award) Same(o *Award) bool {
|
||||
switch {
|
||||
case a.TeamID != o.TeamID:
|
||||
return false
|
||||
case a.Category != o.Category:
|
||||
return false
|
||||
case a.Points != o.Points:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAward(t *testing.T) {
|
||||
entry := "1536958399 1a2b3c4d counting 1"
|
||||
a, err := ParseAward(entry)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if a.TeamID != "1a2b3c4d" {
|
||||
t.Error("TeamID parsed wrong")
|
||||
}
|
||||
if a.Category != "counting" {
|
||||
t.Error("Category parsed wrong")
|
||||
}
|
||||
if a.Points != 1 {
|
||||
t.Error("Points parsed wrong")
|
||||
}
|
||||
|
||||
if a.String() != entry {
|
||||
t.Error("String conversion wonky")
|
||||
}
|
||||
|
||||
if ja, err := a.MarshalJSON(); err != nil {
|
||||
t.Error(err)
|
||||
} 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")
|
||||
}
|
||||
if _, err := ParseAward("1 bad bad bad"); err == nil {
|
||||
t.Error("Not throwing error on bad points")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwardList(t *testing.T) {
|
||||
a, _ := ParseAward("1536958399 1a2b3c4d counting 1")
|
||||
b, _ := ParseAward("1536958400 1a2b3c4d counting 1")
|
||||
c, _ := ParseAward("1536958300 1a2b3c4d counting 1")
|
||||
list := AwardList{a, b, c}
|
||||
|
||||
if sort.IsSorted(list) {
|
||||
t.Error("Unsorted list thinks it's sorted")
|
||||
}
|
||||
|
||||
sort.Stable(list)
|
||||
if (list[0] != c) || (list[1] != a) || (list[2] != b) {
|
||||
t.Error("Sorting didn't")
|
||||
}
|
||||
|
||||
if !sort.IsSorted(list) {
|
||||
t.Error("Sorted list thinks it isn't")
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dirtbags/moth/pkg/jsend"
|
||||
)
|
||||
|
||||
// HTTPServer is a MOTH HTTP server
|
||||
|
@ -44,7 +46,7 @@ func (h *HTTPServer) HandleMothFunc(
|
|||
|
||||
// ServeHTTP provides the http.Handler interface
|
||||
func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
|
||||
w := MothResponseWriter{
|
||||
w := StatusResponseWriter{
|
||||
statusCode: new(int),
|
||||
ResponseWriter: wOrig,
|
||||
}
|
||||
|
@ -58,14 +60,14 @@ func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
|
|||
)
|
||||
}
|
||||
|
||||
// MothResponseWriter provides a ResponseWriter that remembers what the status code was
|
||||
type MothResponseWriter struct {
|
||||
// StatusResponseWriter provides a ResponseWriter that remembers what the status code was
|
||||
type StatusResponseWriter struct {
|
||||
statusCode *int
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
// WriteHeader sends an HTTP response header with the provided status code
|
||||
func (w MothResponseWriter) WriteHeader(statusCode int) {
|
||||
func (w StatusResponseWriter) WriteHeader(statusCode int) {
|
||||
*w.statusCode = statusCode
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
@ -94,16 +96,16 @@ func (h *HTTPServer) ThemeHandler(mh MothRequestHandler, w http.ResponseWriter,
|
|||
|
||||
// StateHandler returns the full JSON-encoded state of the event
|
||||
func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
||||
JSONWrite(w, mh.ExportState())
|
||||
jsend.JSONWrite(w, mh.ExportState())
|
||||
}
|
||||
|
||||
// RegisterHandler handles attempts to register a team
|
||||
func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
||||
teamName := req.FormValue("name")
|
||||
if err := mh.Register(teamName); err != nil {
|
||||
JSendf(w, JSendFail, "not registered", err.Error())
|
||||
jsend.Sendf(w, jsend.Fail, "not registered", err.Error())
|
||||
} else {
|
||||
JSendf(w, JSendSuccess, "registered", "Team ID registered")
|
||||
jsend.Sendf(w, jsend.Success, "registered", "Team ID registered")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,9 +118,9 @@ func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter,
|
|||
points, _ := strconv.Atoi(pointstr)
|
||||
|
||||
if err := mh.CheckAnswer(cat, points, answer); err != nil {
|
||||
JSendf(w, JSendFail, "not accepted", err.Error())
|
||||
jsend.Sendf(w, jsend.Fail, "not accepted", err.Error())
|
||||
} else {
|
||||
JSendf(w, JSendSuccess, "accepted", "%d points awarded in %s", points, cat)
|
||||
jsend.Sendf(w, jsend.Success, "accepted", "%d points awarded in %s", points, cat)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func custodian(updateInterval time.Duration, components []Component) {
|
||||
func custodian(updateInterval time.Duration, components []Provider) {
|
||||
update := func() {
|
||||
for _, c := range components {
|
||||
c.Update()
|
||||
|
@ -39,7 +39,7 @@ func main() {
|
|||
mothballPath := flag.String(
|
||||
"mothballs",
|
||||
"mothballs",
|
||||
"Path to mothballs to host",
|
||||
"Path to mothball files",
|
||||
)
|
||||
refreshInterval := flag.Duration(
|
||||
"refresh",
|
||||
|
@ -67,7 +67,7 @@ func main() {
|
|||
mime.AddExtensionType(".json", "application/json")
|
||||
mime.AddExtensionType(".zip", "application/zip")
|
||||
|
||||
go custodian(*refreshInterval, []Component{theme, state, puzzles})
|
||||
go custodian(*refreshInterval, []Provider{theme, state, puzzles})
|
||||
|
||||
server := NewMothServer(puzzles, theme, state)
|
||||
httpd := NewHTTPServer(*base, server)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEverything(t *testing.T) {
|
||||
state := NewTestState()
|
||||
t.Error("No test")
|
||||
|
||||
state.Update()
|
||||
}
|
|
@ -1,20 +1,23 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/afero"
|
||||
"log"
|
||||
"strings"
|
||||
"bufio"
|
||||
"strconv"
|
||||
"time"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Mothballs provides a collection of active mothball files (puzzle categories)
|
||||
type Mothballs struct {
|
||||
categories map[string]*Zipfs
|
||||
afero.Fs
|
||||
}
|
||||
|
||||
// NewMothballs returns a new Mothballs structure backed by the provided directory
|
||||
func NewMothballs(fs afero.Fs) *Mothballs {
|
||||
return &Mothballs{
|
||||
Fs: fs,
|
||||
|
@ -22,9 +25,10 @@ func NewMothballs(fs afero.Fs) *Mothballs {
|
|||
}
|
||||
}
|
||||
|
||||
// Open returns a ReadSeekCloser corresponding to the filename in a puzzle's category and points
|
||||
func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekCloser, time.Time, error) {
|
||||
mb, ok := m.categories[cat]
|
||||
if ! ok {
|
||||
if !ok {
|
||||
return nil, time.Time{}, fmt.Errorf("No such category: %s", cat)
|
||||
}
|
||||
|
||||
|
@ -32,6 +36,7 @@ func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekClose
|
|||
return f, mb.ModTime(), err
|
||||
}
|
||||
|
||||
// Inventory returns the list of current categories
|
||||
func (m *Mothballs) Inventory() []Category {
|
||||
categories := make([]Category, 0, 20)
|
||||
for cat, zfs := range m.categories {
|
||||
|
@ -55,9 +60,10 @@ func (m *Mothballs) Inventory() []Category {
|
|||
return categories
|
||||
}
|
||||
|
||||
// CheckAnswer returns an error if the provided answer is in any way incorrect for the given category and points
|
||||
func (m *Mothballs) CheckAnswer(cat string, points int, answer string) error {
|
||||
zfs, ok := m.categories[cat]
|
||||
if ! ok {
|
||||
if !ok {
|
||||
return fmt.Errorf("No such category: %s", cat)
|
||||
}
|
||||
|
||||
|
@ -78,6 +84,8 @@ func (m *Mothballs) CheckAnswer(cat string, points int, answer string) error {
|
|||
return fmt.Errorf("Invalid answer")
|
||||
}
|
||||
|
||||
// Update refreshes internal state.
|
||||
// It looks for changes to the directory listing, and caches any new mothballs.
|
||||
func (m *Mothballs) Update() {
|
||||
// Any new categories?
|
||||
files, err := afero.ReadDir(m.Fs, "/")
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMothballs(t *testing.T) {
|
||||
t.Error("moo")
|
||||
}
|
|
@ -5,60 +5,71 @@ import (
|
|||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/dirtbags/moth/pkg/award"
|
||||
)
|
||||
|
||||
// Category represents a puzzle category.
|
||||
type Category struct {
|
||||
Name string
|
||||
Puzzles []int
|
||||
}
|
||||
|
||||
// ReadSeekCloser defines a struct that can read, seek, and close.
|
||||
type ReadSeekCloser interface {
|
||||
io.Reader
|
||||
io.Seeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// StateExport is given to clients requesting the current state.
|
||||
type StateExport struct {
|
||||
Config struct {
|
||||
Devel bool
|
||||
}
|
||||
Messages string
|
||||
TeamNames map[string]string
|
||||
PointsLog []Award
|
||||
PointsLog award.List
|
||||
Puzzles map[string][]int
|
||||
}
|
||||
|
||||
// PuzzleProvider defines what's required to provide puzzles.
|
||||
type PuzzleProvider interface {
|
||||
Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error)
|
||||
Inventory() []Category
|
||||
CheckAnswer(cat string, points int, answer string) error
|
||||
Component
|
||||
Provider
|
||||
}
|
||||
|
||||
// ThemeProvider defines what's required to provide a theme.
|
||||
type ThemeProvider interface {
|
||||
Open(path string) (ReadSeekCloser, time.Time, error)
|
||||
Component
|
||||
Provider
|
||||
}
|
||||
|
||||
// StateProvider defines what's required to provide MOTH state.
|
||||
type StateProvider interface {
|
||||
Messages() string
|
||||
PointsLog() []*Award
|
||||
TeamName(teamId string) (string, error)
|
||||
SetTeamName(teamId, teamName string) error
|
||||
AwardPoints(teamId string, cat string, points int) error
|
||||
Component
|
||||
PointsLog() award.List
|
||||
TeamName(teamID string) (string, error)
|
||||
SetTeamName(teamID, teamName string) error
|
||||
AwardPoints(teamID string, cat string, points int) error
|
||||
Provider
|
||||
}
|
||||
|
||||
type Component interface {
|
||||
// Provider defines providers that can be updated.
|
||||
type Provider interface {
|
||||
Update()
|
||||
}
|
||||
|
||||
// MothServer gathers together the providers that make up a MOTH server.
|
||||
type MothServer struct {
|
||||
Puzzles PuzzleProvider
|
||||
Theme ThemeProvider
|
||||
State StateProvider
|
||||
}
|
||||
|
||||
// NewMothServer returns a new MothServer.
|
||||
func NewMothServer(puzzles PuzzleProvider, theme ThemeProvider, state StateProvider) *MothServer {
|
||||
return &MothServer{
|
||||
Puzzles: puzzles,
|
||||
|
@ -67,21 +78,23 @@ func NewMothServer(puzzles PuzzleProvider, theme ThemeProvider, state StateProvi
|
|||
}
|
||||
}
|
||||
|
||||
func (s *MothServer) NewHandler(teamId string) MothRequestHandler {
|
||||
// NewHandler returns a new http.RequestHandler for the provided teamID.
|
||||
func (s *MothServer) NewHandler(teamID string) MothRequestHandler {
|
||||
return MothRequestHandler{
|
||||
MothServer: s,
|
||||
teamId: teamId,
|
||||
teamID: teamID,
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Come up with a better name for this.
|
||||
// MothRequestHandler provides http.RequestHandler for a MothServer.
|
||||
type MothRequestHandler struct {
|
||||
*MothServer
|
||||
teamId string
|
||||
teamID string
|
||||
}
|
||||
|
||||
// PuzzlesOpen opens a file associated with a puzzle.
|
||||
func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (ReadSeekCloser, time.Time, error) {
|
||||
export := mh.ExportAllState()
|
||||
export := mh.ExportState()
|
||||
fmt.Println(export.Puzzles)
|
||||
for _, p := range export.Puzzles[cat] {
|
||||
fmt.Println(points, p)
|
||||
|
@ -93,93 +106,91 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (
|
|||
return nil, time.Time{}, fmt.Errorf("Puzzle locked")
|
||||
}
|
||||
|
||||
// ThemeOpen opens a file from a theme.
|
||||
func (mh *MothRequestHandler) ThemeOpen(path string) (ReadSeekCloser, time.Time, error) {
|
||||
return mh.Theme.Open(path)
|
||||
}
|
||||
|
||||
// Register associates a team name with a team ID.
|
||||
func (mh *MothRequestHandler) Register(teamName string) error {
|
||||
// XXX: Should we just return success if the team is already registered?
|
||||
// XXX: Should this function be renamed to Login?
|
||||
if teamName == "" {
|
||||
return fmt.Errorf("Empty team name")
|
||||
}
|
||||
return mh.State.SetTeamName(mh.teamId, teamName)
|
||||
return mh.State.SetTeamName(mh.teamID, teamName)
|
||||
}
|
||||
|
||||
// CheckAnswer returns an error if answer is not a correct answer for puzzle points in category cat
|
||||
func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string) error {
|
||||
if err := mh.Puzzles.CheckAnswer(cat, points, answer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := mh.State.AwardPoints(mh.teamId, cat, points); err != nil {
|
||||
if err := mh.State.AwardPoints(mh.teamID, cat, points); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mh *MothRequestHandler) ExportAllState() *StateExport {
|
||||
// ExportState anonymizes team IDs and returns StateExport.
|
||||
// If a teamID has been specified for this MothRequestHandler,
|
||||
// the anonymized team name for this teamID has the special value "self".
|
||||
// If not, the puzzles list is empty.
|
||||
func (mh *MothRequestHandler) ExportState() *StateExport {
|
||||
export := StateExport{}
|
||||
|
||||
teamName, _ := mh.State.TeamName(mh.teamId)
|
||||
teamName, _ := mh.State.TeamName(mh.teamID)
|
||||
|
||||
export.Messages = mh.State.Messages()
|
||||
export.TeamNames = map[string]string{"self": teamName}
|
||||
|
||||
// Anonymize team IDs in points log, and write out team names
|
||||
pointsLog := mh.State.PointsLog()
|
||||
exportIds := map[string]string{mh.teamId: "self"}
|
||||
exportIDs := map[string]string{mh.teamID: "self"}
|
||||
maxSolved := map[string]int{}
|
||||
export.PointsLog = make([]Award, len(pointsLog))
|
||||
for logno, award := range pointsLog {
|
||||
exportAward := *award
|
||||
if id, ok := exportIds[award.TeamID]; ok {
|
||||
exportAward.TeamID = id
|
||||
export.PointsLog = make(award.List, len(pointsLog))
|
||||
for logno, awd := range pointsLog {
|
||||
if id, ok := exportIDs[awd.TeamID]; ok {
|
||||
awd.TeamID = id
|
||||
} else {
|
||||
exportId := strconv.Itoa(logno)
|
||||
name, _ := mh.State.TeamName(award.TeamID)
|
||||
exportAward.TeamID = exportId
|
||||
exportIds[award.TeamID] = exportAward.TeamID
|
||||
export.TeamNames[exportId] = name
|
||||
exportID := strconv.Itoa(logno)
|
||||
name, _ := mh.State.TeamName(awd.TeamID)
|
||||
awd.TeamID = exportID
|
||||
exportIDs[awd.TeamID] = awd.TeamID
|
||||
export.TeamNames[exportID] = name
|
||||
}
|
||||
export.PointsLog[logno] = exportAward
|
||||
export.PointsLog[logno] = awd
|
||||
|
||||
// Record the highest-value unlocked puzzle in each category
|
||||
if award.Points > maxSolved[award.Category] {
|
||||
maxSolved[award.Category] = award.Points
|
||||
if awd.Points > maxSolved[awd.Category] {
|
||||
maxSolved[awd.Category] = awd.Points
|
||||
}
|
||||
}
|
||||
|
||||
export.Puzzles = make(map[string][]int)
|
||||
for _, category := range mh.Puzzles.Inventory() {
|
||||
// Append sentry (end of puzzles)
|
||||
allPuzzles := append(category.Puzzles, 0)
|
||||
if _, ok := export.TeamNames["self"]; ok {
|
||||
// We used to hand this out to everyone,
|
||||
// but then we got a bad reputation on some secretive blacklist,
|
||||
// and now the Navy can't register for events.
|
||||
|
||||
max := maxSolved[category.Name]
|
||||
for _, category := range mh.Puzzles.Inventory() {
|
||||
// Append sentry (end of puzzles)
|
||||
allPuzzles := append(category.Puzzles, 0)
|
||||
|
||||
puzzles := make([]int, 0, len(allPuzzles))
|
||||
for i, val := range allPuzzles {
|
||||
puzzles = allPuzzles[:i+1]
|
||||
if val > max {
|
||||
break
|
||||
max := maxSolved[category.Name]
|
||||
|
||||
puzzles := make([]int, 0, len(allPuzzles))
|
||||
for i, val := range allPuzzles {
|
||||
puzzles = allPuzzles[:i+1]
|
||||
if val > max {
|
||||
break
|
||||
}
|
||||
}
|
||||
export.Puzzles[category.Name] = puzzles
|
||||
}
|
||||
export.Puzzles[category.Name] = puzzles
|
||||
}
|
||||
|
||||
return &export
|
||||
}
|
||||
|
||||
func (mh *MothRequestHandler) ExportState() *StateExport {
|
||||
export := mh.ExportAllState()
|
||||
|
||||
// We don't give this out to just anybody,
|
||||
// because back when we did,
|
||||
// we got a bad reputation on some secretive blacklist,
|
||||
// and now the Navy can't register for events.
|
||||
if export.TeamNames["self"] == "" {
|
||||
export.Puzzles = map[string][]int{}
|
||||
}
|
||||
|
||||
return export
|
||||
}
|
||||
|
|
|
@ -10,12 +10,16 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dirtbags/moth/pkg/award"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
|
||||
// DistinguishableChars are visually unambiguous glyphs.
|
||||
// People with mediocre handwriting could write these down unambiguously,
|
||||
// and they can be entered without holding down shift.
|
||||
const DistinguishableChars = "234678abcdefhikmnpqrtwxyz="
|
||||
|
||||
// State defines the current state of a MOTH instance.
|
||||
// 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 {
|
||||
|
@ -23,6 +27,7 @@ type State struct {
|
|||
Enabled bool
|
||||
}
|
||||
|
||||
// NewState returns a new State struct backed by the given Fs
|
||||
func NewState(fs afero.Fs) *State {
|
||||
return &State{
|
||||
Fs: fs,
|
||||
|
@ -30,7 +35,7 @@ func NewState(fs afero.Fs) *State {
|
|||
}
|
||||
}
|
||||
|
||||
// Check a few things to see if this state directory is "enabled".
|
||||
// UpdateEnabled checks a few things to see if this state directory is "enabled".
|
||||
func (s *State) UpdateEnabled() {
|
||||
if _, err := s.Stat("enabled"); os.IsNotExist(err) {
|
||||
s.Enabled = false
|
||||
|
@ -81,7 +86,7 @@ func (s *State) UpdateEnabled() {
|
|||
}
|
||||
}
|
||||
|
||||
// Returns team name given a team ID.
|
||||
// TeamName returns team name given a team ID.
|
||||
func (s *State) TeamName(teamID string) (string, error) {
|
||||
// XXX: directory traversal
|
||||
teamFile := filepath.Join("teams", teamID)
|
||||
|
@ -97,35 +102,35 @@ func (s *State) TeamName(teamID string) (string, error) {
|
|||
return teamName, nil
|
||||
}
|
||||
|
||||
// Write out team name. This can only be done once.
|
||||
// SetTeamName writes out team name.
|
||||
// This can only be done once.
|
||||
func (s *State) SetTeamName(teamID, teamName string) error {
|
||||
if f, err := s.Open("teamids.txt"); err != nil {
|
||||
f, err := s.Open("teamids.txt")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Team IDs file does not exist")
|
||||
} else {
|
||||
found := false
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
if scanner.Text() == teamID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
if !found {
|
||||
return fmt.Errorf("Team ID not found in list of valid Team IDs")
|
||||
}
|
||||
found := false
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
if scanner.Text() == teamID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
if !found {
|
||||
return fmt.Errorf("Team ID not found in list of valid Team IDs")
|
||||
}
|
||||
|
||||
teamFile := filepath.Join("teams", teamID)
|
||||
err := afero.WriteFile(s, teamFile, []byte(teamName), os.ModeExclusive|0644)
|
||||
if os.IsExist(err) {
|
||||
if err := afero.WriteFile(s, teamFile, []byte(teamName), os.ModeExclusive|0644); os.IsExist(err) {
|
||||
return fmt.Errorf("Team ID is already registered")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Retrieve the current points log
|
||||
func (s *State) PointsLog() []*Award {
|
||||
// PointsLog retrieves the current points log.
|
||||
func (s *State) PointsLog() award.List {
|
||||
f, err := s.Open("points.log")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
|
@ -133,11 +138,11 @@ func (s *State) PointsLog() []*Award {
|
|||
}
|
||||
defer f.Close()
|
||||
|
||||
pointsLog := make([]*Award, 0, 200)
|
||||
pointsLog := make(award.List, 0, 200)
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
cur, err := ParseAward(line)
|
||||
cur, err := award.Parse(line)
|
||||
if err != nil {
|
||||
log.Printf("Skipping malformed award line %s: %s", line, err)
|
||||
continue
|
||||
|
@ -147,7 +152,7 @@ func (s *State) PointsLog() []*Award {
|
|||
return pointsLog
|
||||
}
|
||||
|
||||
// Retrieve current messages
|
||||
// Messages retrieves the current messages.
|
||||
func (s *State) Messages() string {
|
||||
bMessages, _ := afero.ReadFile(s, "messages.html")
|
||||
return string(bMessages)
|
||||
|
@ -159,7 +164,7 @@ func (s *State) Messages() string {
|
|||
// It's just a courtesy to the user.
|
||||
// The update task makes sure we never have duplicate points in the log.
|
||||
func (s *State) AwardPoints(teamID, category string, points int) error {
|
||||
a := Award{
|
||||
a := award.T{
|
||||
When: time.Now().Unix(),
|
||||
TeamID: teamID,
|
||||
Category: category,
|
||||
|
@ -172,7 +177,7 @@ func (s *State) AwardPoints(teamID, category string, points int) error {
|
|||
}
|
||||
|
||||
for _, e := range s.PointsLog() {
|
||||
if a.Same(e) {
|
||||
if a.Equal(e) {
|
||||
return fmt.Errorf("Points already awarded to this team in this category")
|
||||
}
|
||||
}
|
||||
|
@ -208,7 +213,7 @@ func (s *State) collectPoints() {
|
|||
log.Print("Opening new points: ", err)
|
||||
continue
|
||||
}
|
||||
award, err := ParseAward(string(awardstr))
|
||||
awd, err := award.Parse(string(awardstr))
|
||||
if err != nil {
|
||||
log.Print("Can't parse award file ", filename, ": ", err)
|
||||
continue
|
||||
|
@ -216,23 +221,23 @@ func (s *State) collectPoints() {
|
|||
|
||||
duplicate := false
|
||||
for _, e := range s.PointsLog() {
|
||||
if award.Same(e) {
|
||||
if awd.Equal(e) {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if duplicate {
|
||||
log.Print("Skipping duplicate points: ", award.String())
|
||||
log.Print("Skipping duplicate points: ", awd.String())
|
||||
} else {
|
||||
log.Print("Award: ", award.String())
|
||||
log.Print("Award: ", awd.String())
|
||||
|
||||
logf, err := s.OpenFile("points.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Print("Can't append to points log: ", err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(logf, award.String())
|
||||
fmt.Fprintln(logf, awd.String())
|
||||
logf.Close()
|
||||
}
|
||||
|
||||
|
@ -268,7 +273,7 @@ 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 {
|
||||
id := make([]byte, 8)
|
||||
for i := 0; i < 100; i += 1 {
|
||||
for i := 0; i < 100; i++ {
|
||||
for i := range id {
|
||||
char := rand.Intn(len(DistinguishableChars))
|
||||
id[i] = DistinguishableChars[char]
|
||||
|
@ -317,6 +322,7 @@ func (s *State) maybeInitialize() {
|
|||
|
||||
}
|
||||
|
||||
// Update performs housekeeping on a State struct.
|
||||
func (s *State) Update() {
|
||||
s.maybeInitialize()
|
||||
s.UpdateEnabled()
|
||||
|
|
|
@ -8,19 +8,22 @@ import (
|
|||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func NewTestState() *State {
|
||||
s := NewState(new(afero.MemMapFs))
|
||||
s.Update()
|
||||
return s
|
||||
}
|
||||
|
||||
func TestState(t *testing.T) {
|
||||
fs := new(afero.MemMapFs)
|
||||
s := NewTestState()
|
||||
|
||||
mustExist := func(path string) {
|
||||
_, err := fs.Stat(path)
|
||||
_, err := s.Fs.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
t.Errorf("File %s does not exist", path)
|
||||
}
|
||||
}
|
||||
|
||||
s := NewState(fs)
|
||||
s.Update()
|
||||
|
||||
pl := s.PointsLog()
|
||||
if len(pl) != 0 {
|
||||
t.Errorf("Empty points log is not empty")
|
||||
|
@ -30,38 +33,38 @@ func TestState(t *testing.T) {
|
|||
mustExist("enabled")
|
||||
mustExist("hours")
|
||||
|
||||
teamidsBuf, err := afero.ReadFile(fs, "teamids.txt")
|
||||
teamIDsBuf, err := afero.ReadFile(s.Fs, "teamids.txt")
|
||||
if err != nil {
|
||||
t.Errorf("Reading teamids.txt: %v", err)
|
||||
}
|
||||
|
||||
teamids := bytes.Split(teamidsBuf, []byte("\n"))
|
||||
if (len(teamids) != 101) || (len(teamids[100]) > 0) {
|
||||
t.Errorf("There weren't 100 teamids, there were %d", len(teamids))
|
||||
teamIDs := bytes.Split(teamIDsBuf, []byte("\n"))
|
||||
if (len(teamIDs) != 101) || (len(teamIDs[100]) > 0) {
|
||||
t.Errorf("There weren't 100 teamIDs, there were %d", len(teamIDs))
|
||||
}
|
||||
teamId := string(teamids[0])
|
||||
teamID := string(teamIDs[0])
|
||||
|
||||
if err := s.SetTeamName("bad team ID", "bad team name"); err == nil {
|
||||
t.Errorf("Setting bad team ID didn't raise an error")
|
||||
}
|
||||
|
||||
if err := s.SetTeamName(teamId, "My Team"); err != nil {
|
||||
if err := s.SetTeamName(teamID, "My Team"); err != nil {
|
||||
t.Errorf("Setting team name: %v", err)
|
||||
}
|
||||
|
||||
category := "poot"
|
||||
points := 3928
|
||||
s.AwardPoints(teamId, category, points)
|
||||
s.AwardPoints(teamID, category, points)
|
||||
s.Update()
|
||||
|
||||
pl = s.PointsLog()
|
||||
if len(pl) != 1 {
|
||||
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)
|
||||
}
|
||||
|
||||
fs.Remove("initialized")
|
||||
s.Fs.Remove("initialized")
|
||||
s.Update()
|
||||
|
||||
pl = s.PointsLog()
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/afero"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Theme defines a filesystem-backed ThemeProvider.
|
||||
type Theme struct {
|
||||
afero.Fs
|
||||
}
|
||||
|
||||
// NewTheme returns a new Theme, backed by Fs.
|
||||
func NewTheme(fs afero.Fs) *Theme {
|
||||
return &Theme{
|
||||
Fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
// I don't understand why I need this. The type checking system is weird here.
|
||||
// Open returns a new opened file.
|
||||
func (t *Theme) Open(name string) (ReadSeekCloser, time.Time, error) {
|
||||
f, err := t.Fs.Open(name)
|
||||
if err != nil {
|
||||
|
@ -31,6 +34,7 @@ func (t *Theme) Open(name string) (ReadSeekCloser, time.Time, error) {
|
|||
return f, fi.ModTime(), nil
|
||||
}
|
||||
|
||||
// Update performs housekeeping for a Theme.
|
||||
func (t *Theme) Update() {
|
||||
// No periodic tasks for a theme
|
||||
}
|
||||
|
|
|
@ -7,18 +7,21 @@ import (
|
|||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func NewTestTheme() *Theme {
|
||||
return NewTheme(new(afero.MemMapFs))
|
||||
}
|
||||
|
||||
func TestTheme(t *testing.T) {
|
||||
s := NewTestTheme()
|
||||
|
||||
filename := "/index.html"
|
||||
fs := new(afero.MemMapFs)
|
||||
index := "this is the index"
|
||||
afero.WriteFile(fs, filename, []byte(index), 0644)
|
||||
fileInfo, err := fs.Stat(filename)
|
||||
afero.WriteFile(s.Fs, filename, []byte(index), 0644)
|
||||
fileInfo, err := s.Fs.Stat(filename)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
s := NewTheme(fs)
|
||||
|
||||
if f, timestamp, err := s.Open("/index.html"); err != nil {
|
||||
t.Error(err)
|
||||
} else if buf, err := ioutil.ReadAll(f); err != nil {
|
||||
|
|
|
@ -3,13 +3,15 @@ package main
|
|||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"github.com/spf13/afero"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Zipfs defines a Zip Filesystem structure
|
||||
type Zipfs struct {
|
||||
f io.Closer
|
||||
zf *zip.Reader
|
||||
|
@ -164,7 +166,7 @@ func (zfs *Zipfs) Refresh() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (zfs *Zipfs) ModTime() (time.Time) {
|
||||
func (zfs *Zipfs) ModTime() time.Time {
|
||||
return zfs.mtime
|
||||
}
|
||||
|
||||
|
|
|
@ -30,3 +30,8 @@ This pretty much set the entire design:
|
|||
* It should be easy to remember in your head everything it does
|
||||
* Server is also compiled
|
||||
* Static type-checking helps assure no run-time errors
|
||||
* Server only tracks who scored how many points at what time
|
||||
* This means the scoreboard program determines rankings
|
||||
* Want to provide a time bonus for quick answers? I don't, but if you do, you can just modify the scoreboard to do so.
|
||||
* Maybe you want to show a graph of team rankings over time: just replay the event log.
|
||||
* Want to do some analysis of what puzzles take the longest to answer? It's all there.
|
||||
|
|
8
go.mod
8
go.mod
|
@ -3,11 +3,11 @@ module github.com/dirtbags/moth
|
|||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/namsral/flag v1.7.4-pre
|
||||
github.com/namsral/flag v1.7.4-pre // indirect
|
||||
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
|
||||
golang.org/x/tools v0.0.0-20200814172026-c4923e618c08 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
github.com/spf13/afero v1.3.4
|
||||
golang.org/x/tools v0.0.0-20200817190302-118ac038d721 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
)
|
||||
|
|
10
go.sum
10
go.sum
|
@ -6,6 +6,7 @@ github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZ
|
|||
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.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
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=
|
||||
|
@ -16,6 +17,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I
|
|||
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/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/afero v1.3.4 h1:8q6vk3hthlpb2SouZcnBVKboxWQWMDNF38bwholZrJc=
|
||||
github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
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=
|
||||
|
@ -42,9 +45,16 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20u
|
|||
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/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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/tools v0.0.0-20200817023811-d00afeaade8f h1:33yHANSyO/TeglgY9rBhUpX43wtonTXoFOsMRtNB6qE=
|
||||
golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200817190302-118ac038d721 h1:ZMR6guGpa1BJujpPXaMYw3au+XbIfCsRWe68e6KqBKo=
|
||||
golang.org/x/tools v0.0.0-20200817190302-118ac038d721/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=
|
||||
|
|
|
@ -17,7 +17,7 @@ type T struct {
|
|||
}
|
||||
|
||||
// List is a collection of award events.
|
||||
type List []*T
|
||||
type List []T
|
||||
|
||||
// Len implements sort.Interface.
|
||||
func (awards List) Len() int {
|
||||
|
@ -37,31 +37,28 @@ func (awards List) Swap(i, j int) {
|
|||
}
|
||||
|
||||
// Parse parses a string log entry into an award.T.
|
||||
func Parse(s string) (*T, error) {
|
||||
func Parse(s string) (T, error) {
|
||||
ret := T{}
|
||||
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
n, err := fmt.Sscanf(s, "%d %s %s %d", &ret.When, &ret.TeamID, &ret.Category, &ret.Points)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return ret, err
|
||||
} else if n != 4 {
|
||||
return nil, fmt.Errorf("Malformed award string: only parsed %d fields", n)
|
||||
return ret, fmt.Errorf("Malformed award string: only parsed %d fields", n)
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// String returns a log entry string for an award.T.
|
||||
func (a *T) String() string {
|
||||
func (a T) String() string {
|
||||
return fmt.Sprintf("%d %s %s %d", a.When, a.TeamID, a.Category, a.Points)
|
||||
}
|
||||
|
||||
// MarshalJSON returns the award event, encoded as a list.
|
||||
func (a *T) MarshalJSON() ([]byte, error) {
|
||||
if a == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
func (a T) MarshalJSON() ([]byte, error) {
|
||||
ao := []interface{}{
|
||||
a.When,
|
||||
a.TeamID,
|
||||
|
@ -74,7 +71,7 @@ func (a *T) MarshalJSON() ([]byte, error) {
|
|||
|
||||
// Equal returns true if two award events represent the same award.
|
||||
// Timestamps are ignored in this comparison!
|
||||
func (a *T) Equal(o *T) bool {
|
||||
func (a T) Equal(o T) bool {
|
||||
switch {
|
||||
case a.TeamID != o.TeamID:
|
||||
return false
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package jsend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
@ -10,11 +10,17 @@ import (
|
|||
// https://github.com/omniti-labs/jsend
|
||||
|
||||
const (
|
||||
JSendSuccess = "success"
|
||||
JSendFail = "fail"
|
||||
JSendError = "error"
|
||||
// Success is the return code indicating "All went well, and (usually) some data was returned".
|
||||
Success = "success"
|
||||
|
||||
// Fail is the return code indicating "There was a problem with the data submitted, or some pre-condition of the API call wasn't satisfied".
|
||||
Fail = "fail"
|
||||
|
||||
// Error is the return code indicating "An error occurred in processing the request, i.e. an exception was thrown".
|
||||
Error = "error"
|
||||
)
|
||||
|
||||
// JSONWrite writes out data as JSON, sending headers and content length
|
||||
func JSONWrite(w http.ResponseWriter, data interface{}) {
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
|
@ -28,7 +34,8 @@ func JSONWrite(w http.ResponseWriter, data interface{}) {
|
|||
w.Write(respBytes)
|
||||
}
|
||||
|
||||
func JSend(w http.ResponseWriter, status string, data interface{}) {
|
||||
// Send sends arbitrary data as a JSend response
|
||||
func Send(w http.ResponseWriter, status string, data interface{}) {
|
||||
resp := struct {
|
||||
Status string `json:"status"`
|
||||
Data interface{} `json:"data"`
|
||||
|
@ -39,7 +46,8 @@ func JSend(w http.ResponseWriter, status string, data interface{}) {
|
|||
JSONWrite(w, resp)
|
||||
}
|
||||
|
||||
func JSendf(w http.ResponseWriter, status, short string, format string, a ...interface{}) {
|
||||
// Sendf sends a Sprintf()-formatted string as a JSend response
|
||||
func Sendf(w http.ResponseWriter, status, short string, format string, a ...interface{}) {
|
||||
data := struct {
|
||||
Short string `json:"short"`
|
||||
Description string `json:"description"`
|
||||
|
@ -47,5 +55,5 @@ func JSendf(w http.ResponseWriter, status, short string, format string, a ...int
|
|||
data.Short = short
|
||||
data.Description = fmt.Sprintf(format, a...)
|
||||
|
||||
JSend(w, status, data)
|
||||
Send(w, status, data)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package jsend
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEverything(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
Sendf(w, Success, "You have cows", "You have %d cows", 12)
|
||||
if w.Result().StatusCode != 200 {
|
||||
t.Errorf("HTTP Status code: %d", w.Result().StatusCode)
|
||||
}
|
||||
if w.Body.String() != `{"status":"success","data":{"short":"You have cows","description":"You have 12 cows"}}` {
|
||||
t.Errorf("HTTP Body %s", w.Body.Bytes())
|
||||
}
|
||||
}
|
|
@ -176,7 +176,7 @@ function login(e) {
|
|||
let name = document.querySelector("[name=name]").value
|
||||
let teamId = document.querySelector("[name=id]").value
|
||||
let pide = document.querySelector("[name=pid]")
|
||||
let participantId = pide?pide.value:""
|
||||
let participantId = pide?pide.value:Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
|
||||
|
||||
fetch("register", {
|
||||
method: "POST",
|
||||
|
|
Loading…
Reference in New Issue