diff --git a/docs/api.md b/docs/api.md
index 25a6536..dbb34b1 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -48,7 +48,7 @@ RegisterTeam(teamId, teamName)
Register a team name with a team hash.
-Parameters:
+### Parameters
* teamId: Team's unique identifier (usually a hex value)
* teamName: Team's human-readable name
@@ -56,13 +56,13 @@ Parameters:
On success, no data is returned.
On failure, message contains an English explanation of why.
-Example:
+### Example
https://server/RegisterTeam?teamId=8b1292ca
{
status: "success",
- data: nil
+ data: null
}
@@ -71,12 +71,12 @@ GetPuzzleList()
Return all currently-open puzzles.
-Return data:
+### Return data
* puzzles: dictionary mapping from category to a list of point values.
-Example:
+### Example
https://server/GetPuzzleList
@@ -96,12 +96,12 @@ GetPuzzle(category, points)
Return a puzzle.
-Parameters:
+### Parameters
* category: name of category to fetch from
* points: point value of the puzzle to fetch
-Return data:
+### Return data
* authors: List of puzzle authors
* hashes: list of djbhash values of acceptable answers
@@ -109,7 +109,7 @@ Return data:
* body: HTML body of the puzzle
-Example:
+### Example
https://server/GetPuzzle?category=sequence&points=1
@@ -130,7 +130,7 @@ GetPointsLog()
Return the entire points log, and team names.
-Return data:
+### Return data
* teams: mapping from team number (int) to team name
* log: list of (timestamp, team number, category, points)
@@ -138,7 +138,7 @@ Return data:
Note: team number may change between calls.
-Example:
+### Example
https://server/GetEventsLog
@@ -163,20 +163,27 @@ SubmitAnswer(teamId, category, points, answer)
Submit an answer to a puzzle.
-Parameters:
+### Parameters
* teamId: Team ID (optional: if ommitted, answer is verified but no points are awarded)
* category: category name of puzzle
* points: point value of puzzle
* answer: attempted answer
-Example:
+
+### Return Data
+
+* epilogue: HTML to display as an "epilogue" to the puzzle
+
+### Example
https://server/SubmitAnswer?teamId=8b1292ca&category=sequence&points=1&answer=6
{
status: "success",
- data: null
+ data: {
+ epilogue: "That's right: in base 10, 5 + 1 = 6."
+ }
}
SubmitToken(teamId, token)
@@ -184,18 +191,18 @@ SubmitToken(teamId, token)
Submit a token for points
-Parameters:
+### Parameters
* teamId: Team ID
* token: Token being submitted
-Return data:
+### Return data
* category: category for which this token awarded points
* points: number of points awarded
-Example:
+### Example
https://server/SubmitToken?teamId=8b1292ca&token=wat:30:xylep-radar-nanox
diff --git a/handlers.go b/handlers.go
deleted file mode 100644
index a1edddd..0000000
--- a/handlers.go
+++ /dev/null
@@ -1,152 +0,0 @@
-package main
-
-import (
- "fmt"
- "net/http"
- "os"
- "regexp"
- "strings"
- "strconv"
-)
-
-func registerHandler(w http.ResponseWriter, req *http.Request) {
- teamname := req.FormValue("n")
- teamid := req.FormValue("h")
-
- if matched, _ := regexp.MatchString("[^0-9a-z]", teamid); matched {
- teamid = ""
- }
-
- if (teamid == "") || (teamname == "") {
- showPage(w, "Invalid Entry", "Oops! Are you sure you got that right?")
- return
- }
-
- if ! anchoredSearch(statePath("teamids.txt"), teamid, 0) {
- showPage(w, "Invalid Team ID", "I don't have a record of that team ID. Maybe you used capital letters accidentally?")
- return
- }
-
- f, err := os.OpenFile(statePath("state", teamid), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
- if err != nil {
- showPage(
- w,
- "Registration failed",
- "Unable to register. Perhaps a teammate has already registered?",
- )
- return
- }
- defer f.Close()
- fmt.Fprintln(f, teamname)
- showPage(w, "Success", "Okay, your team has been named and you may begin using your team ID!")
-}
-
-func tokenHandler(w http.ResponseWriter, req *http.Request) {
- teamid := req.FormValue("t")
- token := req.FormValue("k")
-
- // Check answer
- if ! anchoredSearch(token, statePath("tokens.txt"), 0) {
- showPage(w, "Unrecognized token", "I don't recognize that token. Did you type in the whole thing?")
- return
- }
-
- parts := strings.Split(token, ":")
- category := ""
- pointstr := ""
- if len(parts) >= 2 {
- category = parts[0]
- pointstr = parts[1]
- }
- points, err := strconv.Atoi(pointstr)
- if err != nil {
- points = 0
- }
- // Defang category name; prevent directory traversal
- if matched, _ := regexp.MatchString("^[A-Za-z0-9_-]", category); matched {
- category = ""
- }
-
- if (category == "") || (points == 0) {
- showPage(w, "Unrecognized token", "Something doesn't look right about that token")
- return
- }
-
- if err := awardPoints(teamid, category, points); err != nil {
- showPage(w, "Error awarding points", err.Error())
- return
- }
- showPage(w, "Points awarded", fmt.Sprintf("%d points for %s!", points, teamid))
-}
-
-func answerHandler(w http.ResponseWriter, req *http.Request) {
- teamid := req.FormValue("t")
- category := req.FormValue("c")
- pointstr := req.FormValue("p")
- answer := req.FormValue("a")
-
- points, err := strconv.Atoi(pointstr)
- if err != nil {
- points = 0
- }
-
- // Defang category name; prevent directory traversal
- if matched, _ := regexp.MatchString("^[A-Za-z0-9_-]", category); matched {
- category = ""
- }
-
- // Check answer
- needle := fmt.Sprintf("%s %s", points, answer)
- haystack := cachePath(category, "answers.txt")
- if ! anchoredSearch(haystack, needle, 0) {
- showPage(w, "Wrong answer", err.Error())
- }
-
- if err := awardPoints(teamid, category, points); err != nil {
- showPage(w, "Error awarding points", err.Error())
- return
- }
- showPage(w, "Points awarded", fmt.Sprintf("%d points for %s!", points, teamid))
-}
-
-func puzzlesHandler(w http.ResponseWriter, req *http.Request) {
- puzzles := map[string][]interface{}{}
- // v := map[string][]interface{}{"Moo": {1, "0177f85ae895a33e2e7c5030c3dc484e8173e55c"}}
- // j, _ := json.Marshal(v)
-
- for _, category := range categories {
-
- }
-}
-
-func pointsHandler(w http.ResponseWriter, req *http.Request) {
-
-}
-
-// staticHandler serves up static files.
-func rootHandler(w http.ResponseWriter, req *http.Request) {
- if req.URL.Path == "/" {
- showPage(
- w,
- "Welcome",
- `
-
Register your team
-
-
-
-
- If someone on your team has already registered,
- proceed to the
-
puzzles overview.
-
- `,
- )
- return
- }
-
- http.NotFound(w, req)
-}
diff --git a/maintenance.go b/maintenance.go
deleted file mode 100644
index f352c33..0000000
--- a/maintenance.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package main
-
-import (
- "log"
- "io/ioutil"
- "time"
- "os"
- "strings"
-)
-
-func cacheMothball(filepath string, categoryName string) {
- log.Printf("I'm exploding a mothball %s %s", filepath, categoryName)
-}
-
-// maintenance runs
-func tidy() {
- // Skip if we've been disabled
- if _, err := os.Stat(statePath("disabled")); err == nil {
- log.Print("disabled file found, suspending maintenance")
- return
- }
-
- // Skip if we've expired
- untilspec, err := ioutil.ReadFile(statePath("until"))
- if err == nil {
- until, err := time.Parse(time.RFC3339, string(untilspec))
- if err != nil {
- log.Print("Unparseable date in until file: %s", until)
- } else {
- if until.Before(time.Now()) {
- log.Print("until file time reached, suspending maintenance")
- return
- }
- }
- }
-
- // Get current list of categories
- newCategories := []string{}
- files, err := ioutil.ReadDir(modulesPath())
- if err != nil {
- log.Printf("Error reading packages: %s", err)
- }
- for _, f := range files {
- filename := f.Name()
- filepath := modulesPath(filename)
- if ! strings.HasSuffix(filename, ".mb") {
- continue
- }
-
- categoryName := strings.TrimSuffix(filename, ".mb")
- newCategories = append(newCategories, categoryName)
-
- // Uncompress into cache directory
- cacheMothball(filepath, categoryName)
- }
- categories = newCategories
-
- collectPoints()
-}
-
-// maintenance is the goroutine that runs a periodic maintenance task
-func maintenance(maintenanceInterval time.Duration) {
- for ;; time.Sleep(maintenanceInterval) {
- tidy()
- }
-}
diff --git a/mothd.go b/mothd.go
deleted file mode 100644
index 18f42a6..0000000
--- a/mothd.go
+++ /dev/null
@@ -1,167 +0,0 @@
-package main
-
-import (
- "bufio"
- "github.com/namsral/flag"
- "fmt"
- "log"
- "net/http"
- "os"
- "path"
- "strings"
- "time"
-)
-
-var moduleDir string
-var stateDir string
-var cacheDir string
-var categories = []string{}
-
-// anchoredSearch looks for needle in filename,
-// skipping the first skip space-delimited words
-func anchoredSearch(filename string, needle string, skip int) bool {
- f, err := os.Open(filename)
- if err != nil {
- log.Print("Can't open %s: %s", filename, err)
- return false
- }
- defer f.Close()
-
- scanner := bufio.NewScanner(f)
- for scanner.Scan() {
- line := scanner.Text()
- parts := strings.SplitN(" ", line, skip+1)
- if parts[skip+1] == needle {
- return true
- }
- }
-
- return false
-}
-
-func showPage(w http.ResponseWriter, title string, body string) {
- w.WriteHeader(http.StatusOK)
-
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "%s", title)
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "%s
", title)
- fmt.Fprintf(w, "", body)
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "")
-}
-
-func modulesPath(parts ...string) string {
- tail := path.Join(parts...)
- return path.Join(moduleDir, tail)
-}
-
-func statePath(parts ...string) string {
- tail := path.Join(parts...)
- return path.Join(stateDir, tail)
-}
-
-func cachePath(parts ...string) string {
- tail := path.Join(parts...)
- return path.Join(cacheDir, tail)
-}
-
-func logRequest(handler http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- log.Printf("HTTP %s %s %s\n", r.RemoteAddr, r.Method, r.URL)
- handler.ServeHTTP(w, r)
- })
-}
-
-func setup() error {
- // Roll over and die if directories aren't even set up
- if _, err := os.Stat(modulesPath()); os.IsNotExist(err) {
- return err
- }
- if _, err := os.Stat(statePath()); os.IsNotExist(err) {
- return err
- }
- if _, err := os.Stat(cachePath()); os.IsNotExist(err) {
- return err
- }
-
- // Make sure points directories exist
- os.Mkdir(statePath("points.tmp"), 0755)
- os.Mkdir(statePath("points.new"), 0755)
-
- // Preseed available team ids if file doesn't exist
- if f, err := os.OpenFile(statePath("teamids.txt"), os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0644); err == nil {
- defer f.Close()
- for i := 0; i <= 9999; i += 1 {
- fmt.Fprintf(f, "%04d\n", i)
- }
- }
-
- return nil
-}
-
-func main() {
- var maintenanceInterval time.Duration
- var listen string
-
- fs := flag.NewFlagSetWithEnvPrefix(os.Args[0], "MOTH", flag.ExitOnError)
- fs.StringVar(
- &moduleDir,
- "modules",
- "/moth/modules",
- "Path where your moth modules live",
- )
- fs.StringVar(
- &stateDir,
- "state",
- "/moth/state",
- "Path where state should be written",
- )
- fs.StringVar(
- &cacheDir,
- "cache",
- "/moth/cache",
- "Path for ephemeral cache",
- )
- fs.DurationVar(
- &maintenanceInterval,
- "maint",
- 20 * time.Second,
- "Maintenance interval",
- )
- fs.StringVar(
- &listen,
- "listen",
- ":8080",
- "[host]:port to bind and listen",
- )
- fs.Parse(os.Args[1:])
-
- if err := setup(); err != nil {
- log.Fatal(err)
- }
- go maintenance(maintenanceInterval)
-
- fileserver := http.FileServer(http.Dir(cacheDir))
- http.HandleFunc("/", rootHandler)
- http.Handle("/static/", http.StripPrefix("/static", fileserver))
-
- http.HandleFunc("/register", registerHandler)
- http.HandleFunc("/token", tokenHandler)
- http.HandleFunc("/answer", answerHandler)
-
- http.HandleFunc("/puzzles.json", puzzlesHandler)
- http.HandleFunc("/points.json", pointsHandler)
-
- log.Printf("Listening on %s", listen)
- log.Fatal(http.ListenAndServe(listen, logRequest(http.DefaultServeMux)))
-}
diff --git a/points.go b/points.go
deleted file mode 100644
index f6f16c0..0000000
--- a/points.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package main
-
-import (
- "bufio"
- "fmt"
- "io/ioutil"
- "log"
- "os"
- "strconv"
- "strings"
- "time"
-)
-
-type Award struct {
- when time.Time
- teamid string
- category string
- points int
-}
-
-func ParseAward(s string) (*Award, error) {
- ret := Award{}
-
- parts := strings.SplitN(s, " ", 5)
- if len(parts) < 4 {
- return nil, fmt.Errorf("Malformed award string")
- }
-
- whenEpoch, err := strconv.ParseInt(parts[0], 10, 64)
- if (err != nil) {
- return nil, fmt.Errorf("Malformed timestamp: %s", parts[0])
- }
- ret.when = time.Unix(whenEpoch, 0)
-
- ret.teamid = parts[1]
- ret.category = parts[2]
-
- points, err := strconv.Atoi(parts[3])
- if (err != nil) {
- return nil, fmt.Errorf("Malformed points: %s", parts[3])
- }
- ret.points = points
-
- return &ret, nil
-}
-
-func (a *Award) String() string {
- return fmt.Sprintf("%d %s %s %d", a.when.Unix(), a.teamid, a.category, a.points)
-}
-
-func pointsLog() []Award {
- var ret []Award
-
- fn := statePath("points.log")
- f, err := os.Open(fn)
- if err != nil {
- log.Printf("Unable to open %s: %s", fn, err)
- return ret
- }
- defer f.Close()
-
- scanner := bufio.NewScanner(f)
- for scanner.Scan() {
- line := scanner.Text()
- cur, err := ParseAward(line)
- if err != nil {
- log.Printf("Skipping malformed award line %s: %s", line, err)
- continue
- }
- ret = append(ret, *cur)
- }
-
- return ret
-}
-
-// awardPoints gives points points to team teamid in category category
-func awardPoints(teamid string, category string, points int) error {
- fn := fmt.Sprintf("%s-%s-%d", teamid, category, points)
- tmpfn := statePath("points.tmp", fn)
- newfn := statePath("points.new", fn)
-
- contents := fmt.Sprintf("%d %s %s %d\n", time.Now().Unix(), teamid, points)
-
- if err := ioutil.WriteFile(tmpfn, []byte(contents), 0644); err != nil {
- return err
- }
-
- if err := os.Rename(tmpfn, newfn); err != nil {
- return err
- }
-
- log.Printf("Award %s %s %d", teamid, category, points)
- return nil
-}
-
-// collectPoints gathers up files in points.new/ and appends their contents to points.log,
-// removing each points.new/ file as it goes.
-func collectPoints() {
- logf, err := os.OpenFile(statePath("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if err != nil {
- log.Printf("Can't append to points log: %s", err)
- return
- }
- defer logf.Close()
-
- files, err := ioutil.ReadDir(statePath("points.new"))
- if err != nil {
- log.Printf("Error reading packages: %s", err)
- }
- for _, f := range files {
- filename := statePath("points.new", f.Name())
- s, err := ioutil.ReadFile(filename)
- if err != nil {
- log.Printf("Can't read points file %s: %s", filename, err)
- continue
- }
- award, err := ParseAward(string(s))
- if err != nil {
- log.Printf("Can't parse award file %s: %s", filename, err)
- continue
- }
- fmt.Fprintf(logf, "%s\n", award.String())
- log.Print(award.String())
- logf.Sync()
- if err := os.Remove(filename); err != nil {
- log.Printf("Unable to remove %s: %s", filename, err)
- }
- }
-}
\ No newline at end of file