diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d1b23a..3839421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Clear Debug.summary field when making mothballs +### Changed +- Regulated category/puzzle provider API: now everything returns a JSON dictionary (or octet stream for files) + ### Added - More log events - [Log channels document](docs/logs.md) diff --git a/cmd/mothd/providercommand.go b/cmd/mothd/providercommand.go index fc4bfd5..ef0ca0b 100644 --- a/cmd/mothd/providercommand.go +++ b/cmd/mothd/providercommand.go @@ -4,6 +4,7 @@ package main import ( "bytes" "context" + "encoding/json" "fmt" "io" "log" @@ -13,6 +14,8 @@ import ( "strconv" "strings" "time" + + "github.com/dirtbags/moth/pkg/transpile" ) // ProviderCommand specifies a command to run for the puzzle API @@ -111,14 +114,13 @@ func (pc ProviderCommand) CheckAnswer(cat string, points int, answer string) (bo } else if err != nil { return false, err } - result := strings.TrimSpace(string(stdout)) - if result != "correct" { - log.Printf("WARNING: %s: Nothing written to stdout", pc.Path) - return false, nil + ans := transpile.AnswerResponse{} + if err := json.Unmarshal(stdout, &ans); err != nil { + return false, err } - return true, nil + return ans.Correct, nil } // Mothball just returns an error diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index 822edc3..60aa8ae 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -185,7 +185,7 @@ func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExpor export.Config = mh.Config teamName, err := mh.State.TeamName(mh.teamID) - registered := override || (err == nil) + registered := override || mh.Config.Devel || (err == nil) export.Messages = mh.State.Messages() export.TeamNames = make(map[string]string) diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 4b0df96..c3d19c4 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -27,7 +27,23 @@ func NewTestServer() *MothServer { return NewMothServer(Configuration{}, theme, state, puzzles) } -func TestServer(t *testing.T) { +func TestDevelServer(t *testing.T) { + server := NewTestServer() + server.Config.Devel = true + anonHandler := server.NewHandler("badParticipantId", "badTeamId") + + { + es := anonHandler.ExportState() + if !es.Config.Devel { + t.Error("Not marked as development server") + } + if len(es.Puzzles) != 1 { + t.Error("Wrong puzzles for anonymous state on devel server:", es.Puzzles) + } + } +} + +func TestProdServer(t *testing.T) { teamName := "OurTeam" participantID := "participantID" teamID := TestTeamID diff --git a/cmd/mothd/testdata/testpiler.sh b/cmd/mothd/testdata/testpiler.sh index 60da536..2207379 100755 --- a/cmd/mothd/testdata/testpiler.sh +++ b/cmd/mothd/testdata/testpiler.sh @@ -26,9 +26,9 @@ EOT ;; answer:pategory:1) if [ "$ANSWER" = "answer" ]; then - echo "correct" + echo '{"Correct":true}' else - echo "Sorry, wrong answer." + echo '{"Correct":false}' fi ;; answer:pategory:2) diff --git a/example-puzzles/example/3/mkpuzzle b/example-puzzles/example/3/mkpuzzle index 5863e13..bfe1854 100755 --- a/example-puzzles/example/3/mkpuzzle +++ b/example-puzzles/example/3/mkpuzzle @@ -45,12 +45,14 @@ def open_file(filename): shutil.copyfileobj(f, sys.stdout.buffer) def check_answer(check): - if answer == check: - print("correct") - else: - print("incorrect") + obj = { + "Correct": (answer == check) + } + json.dump(obj, sys.stdout) if len(sys.argv) == 1: + raise RuntimeError("Command not provided") +elif sys.argv[1] == "puzzle": puzzle() elif sys.argv[1] == "file": open_file(sys.argv[2]) diff --git a/pkg/transpile/category.go b/pkg/transpile/category.go index 887ae61..915a872 100644 --- a/pkg/transpile/category.go +++ b/pkg/transpile/category.go @@ -9,12 +9,16 @@ import ( "os/exec" "path" "strconv" - "strings" "time" "github.com/spf13/afero" ) +// InventoryResponse is what's handed back when we ask for an inventory. +type InventoryResponse struct { + Puzzles []int +} + // Category defines the functionality required to be a puzzle category. type Category interface { // Inventory lists every puzzle in the category. @@ -141,12 +145,12 @@ func (c FsCommandCategory) Inventory() ([]int, error) { return nil, err } - ret := make([]int, 0) - if err := json.Unmarshal(stdout, &ret); err != nil { + inv := InventoryResponse{} + if err := json.Unmarshal(stdout, &inv); err != nil { return nil, err } - return ret, nil + return inv.Puzzles, nil } // Puzzle returns a Puzzle structure for the given point value. @@ -181,10 +185,11 @@ func (c FsCommandCategory) Answer(points int, answer string) bool { return false } - switch strings.TrimSpace(string(stdout)) { - case "correct": - return true + ans := AnswerResponse{} + if err := json.Unmarshal(stdout, &ans); err != nil { + log.Printf("ERROR: Answering %d points: %s", points, err) + return false } - return false + return ans.Correct } diff --git a/pkg/transpile/puzzle.go b/pkg/transpile/puzzle.go index ff9c2e2..77a9ed1 100644 --- a/pkg/transpile/puzzle.go +++ b/pkg/transpile/puzzle.go @@ -22,6 +22,11 @@ import ( "gopkg.in/yaml.v2" ) +// AnswerResponse is handed back when we ask for an answer to be checked. +type AnswerResponse struct { + Correct bool +} + // Puzzle contains everything about a puzzle that a client would see. type Puzzle struct { Pre struct { @@ -425,9 +430,11 @@ func (fp FsCommandPuzzle) Answer(answer string) bool { return false } - switch strings.TrimSpace(string(stdout)) { - case "correct": - return true + ans := AnswerResponse{} + if err := json.Unmarshal(stdout, &ans); err != nil { + log.Printf("ERROR: checking answer: %s", err) + return false } - return false + + return ans.Correct } diff --git a/pkg/transpile/testdata/generated/mkcategory b/pkg/transpile/testdata/generated/mkcategory index 67d96f9..04e5eb2 100755 --- a/pkg/transpile/testdata/generated/mkcategory +++ b/pkg/transpile/testdata/generated/mkcategory @@ -7,8 +7,12 @@ fail () { case $1:$2:$3 in inventory::) - echo "[1,2,3," - echo "4,5]" + cat < 0) { + renderPuzzles(obj.Puzzles) + } else if (Object.keys(obj.Puzzles).length > 0) { renderPuzzles(obj.Puzzles) if (obj.Config.Detachable) { fetchAll(obj.Puzzles)