mirror of https://github.com/dirtbags/moth.git
JSON dicts for back-end API
This commit is contained in:
parent
682a6a7f86
commit
bb2565b06d
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -7,8 +7,12 @@ fail () {
|
|||
|
||||
case $1:$2:$3 in
|
||||
inventory::)
|
||||
echo "[1,2,3,"
|
||||
echo "4,5]"
|
||||
cat <<EOT
|
||||
{
|
||||
"Puzzles": [1, 2, 3,
|
||||
4, 5]
|
||||
}
|
||||
EOT
|
||||
;;
|
||||
puzzle:1:)
|
||||
cat <<EOT
|
||||
|
@ -31,10 +35,10 @@ EOT
|
|||
fail "No such file: $2"
|
||||
;;
|
||||
answer:1:answer1.0)
|
||||
echo "correct"
|
||||
echo -n '{"Correct":true}'
|
||||
;;
|
||||
answer:1:*)
|
||||
echo "incorrect"
|
||||
echo '{"Correct":false}'
|
||||
;;
|
||||
answer:*:*)
|
||||
fail "Fail answer"
|
||||
|
|
|
@ -24,16 +24,15 @@ EOT
|
|||
fail "no such file: $1"
|
||||
;;
|
||||
answer:moo)
|
||||
echo "correct"
|
||||
echo '{"Correct":true}'
|
||||
;;
|
||||
answer:error)
|
||||
fail "you requested an error"
|
||||
;;
|
||||
answer:*)
|
||||
echo "incorrect"
|
||||
echo '{"Correct":false}'
|
||||
;;
|
||||
*)
|
||||
fail "What is $1"
|
||||
;;
|
||||
esac
|
||||
|
|
@ -30,6 +30,9 @@ function renderPuzzles(obj) {
|
|||
// Create a sorted list of category names
|
||||
let cats = Object.keys(obj)
|
||||
cats.sort()
|
||||
if (cats.length == 0) {
|
||||
toast("No categories to serve!")
|
||||
}
|
||||
for (let cat of cats) {
|
||||
if (cat.startsWith("__")) {
|
||||
// Skip metadata
|
||||
|
@ -103,8 +106,8 @@ function renderState(obj) {
|
|||
let params = new URLSearchParams(window.location.search)
|
||||
sessionStorage.id = "1"
|
||||
sessionStorage.pid = "rodney"
|
||||
}
|
||||
if (Object.keys(obj.Puzzles).length > 0) {
|
||||
renderPuzzles(obj.Puzzles)
|
||||
} else if (Object.keys(obj.Puzzles).length > 0) {
|
||||
renderPuzzles(obj.Puzzles)
|
||||
if (obj.Config.Detachable) {
|
||||
fetchAll(obj.Puzzles)
|
||||
|
|
Loading…
Reference in New Issue