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
|
### Fixed
|
||||||
- Clear Debug.summary field when making mothballs
|
- 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
|
### Added
|
||||||
- More log events
|
- More log events
|
||||||
- [Log channels document](docs/logs.md)
|
- [Log channels document](docs/logs.md)
|
||||||
|
|
|
@ -4,6 +4,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -13,6 +14,8 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dirtbags/moth/pkg/transpile"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProviderCommand specifies a command to run for the puzzle API
|
// 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 {
|
} else if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
result := strings.TrimSpace(string(stdout))
|
|
||||||
|
|
||||||
if result != "correct" {
|
ans := transpile.AnswerResponse{}
|
||||||
log.Printf("WARNING: %s: Nothing written to stdout", pc.Path)
|
if err := json.Unmarshal(stdout, &ans); err != nil {
|
||||||
return false, nil
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return ans.Correct, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mothball just returns an error
|
// Mothball just returns an error
|
||||||
|
|
|
@ -185,7 +185,7 @@ func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExpor
|
||||||
export.Config = mh.Config
|
export.Config = mh.Config
|
||||||
|
|
||||||
teamName, err := mh.State.TeamName(mh.teamID)
|
teamName, err := mh.State.TeamName(mh.teamID)
|
||||||
registered := override || (err == nil)
|
registered := override || mh.Config.Devel || (err == nil)
|
||||||
|
|
||||||
export.Messages = mh.State.Messages()
|
export.Messages = mh.State.Messages()
|
||||||
export.TeamNames = make(map[string]string)
|
export.TeamNames = make(map[string]string)
|
||||||
|
|
|
@ -27,7 +27,23 @@ func NewTestServer() *MothServer {
|
||||||
return NewMothServer(Configuration{}, theme, state, puzzles)
|
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"
|
teamName := "OurTeam"
|
||||||
participantID := "participantID"
|
participantID := "participantID"
|
||||||
teamID := TestTeamID
|
teamID := TestTeamID
|
||||||
|
|
|
@ -26,9 +26,9 @@ EOT
|
||||||
;;
|
;;
|
||||||
answer:pategory:1)
|
answer:pategory:1)
|
||||||
if [ "$ANSWER" = "answer" ]; then
|
if [ "$ANSWER" = "answer" ]; then
|
||||||
echo "correct"
|
echo '{"Correct":true}'
|
||||||
else
|
else
|
||||||
echo "Sorry, wrong answer."
|
echo '{"Correct":false}'
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
answer:pategory:2)
|
answer:pategory:2)
|
||||||
|
|
|
@ -45,12 +45,14 @@ def open_file(filename):
|
||||||
shutil.copyfileobj(f, sys.stdout.buffer)
|
shutil.copyfileobj(f, sys.stdout.buffer)
|
||||||
|
|
||||||
def check_answer(check):
|
def check_answer(check):
|
||||||
if answer == check:
|
obj = {
|
||||||
print("correct")
|
"Correct": (answer == check)
|
||||||
else:
|
}
|
||||||
print("incorrect")
|
json.dump(obj, sys.stdout)
|
||||||
|
|
||||||
if len(sys.argv) == 1:
|
if len(sys.argv) == 1:
|
||||||
|
raise RuntimeError("Command not provided")
|
||||||
|
elif sys.argv[1] == "puzzle":
|
||||||
puzzle()
|
puzzle()
|
||||||
elif sys.argv[1] == "file":
|
elif sys.argv[1] == "file":
|
||||||
open_file(sys.argv[2])
|
open_file(sys.argv[2])
|
||||||
|
|
|
@ -9,12 +9,16 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"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.
|
// Category defines the functionality required to be a puzzle category.
|
||||||
type Category interface {
|
type Category interface {
|
||||||
// Inventory lists every puzzle in the category.
|
// Inventory lists every puzzle in the category.
|
||||||
|
@ -141,12 +145,12 @@ func (c FsCommandCategory) Inventory() ([]int, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ret := make([]int, 0)
|
inv := InventoryResponse{}
|
||||||
if err := json.Unmarshal(stdout, &ret); err != nil {
|
if err := json.Unmarshal(stdout, &inv); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret, nil
|
return inv.Puzzles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Puzzle returns a Puzzle structure for the given point value.
|
// 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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
switch strings.TrimSpace(string(stdout)) {
|
ans := AnswerResponse{}
|
||||||
case "correct":
|
if err := json.Unmarshal(stdout, &ans); err != nil {
|
||||||
return true
|
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"
|
"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.
|
// Puzzle contains everything about a puzzle that a client would see.
|
||||||
type Puzzle struct {
|
type Puzzle struct {
|
||||||
Pre struct {
|
Pre struct {
|
||||||
|
@ -425,9 +430,11 @@ func (fp FsCommandPuzzle) Answer(answer string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
switch strings.TrimSpace(string(stdout)) {
|
ans := AnswerResponse{}
|
||||||
case "correct":
|
if err := json.Unmarshal(stdout, &ans); err != nil {
|
||||||
return true
|
log.Printf("ERROR: checking answer: %s", err)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
return ans.Correct
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,12 @@ fail () {
|
||||||
|
|
||||||
case $1:$2:$3 in
|
case $1:$2:$3 in
|
||||||
inventory::)
|
inventory::)
|
||||||
echo "[1,2,3,"
|
cat <<EOT
|
||||||
echo "4,5]"
|
{
|
||||||
|
"Puzzles": [1, 2, 3,
|
||||||
|
4, 5]
|
||||||
|
}
|
||||||
|
EOT
|
||||||
;;
|
;;
|
||||||
puzzle:1:)
|
puzzle:1:)
|
||||||
cat <<EOT
|
cat <<EOT
|
||||||
|
@ -31,10 +35,10 @@ EOT
|
||||||
fail "No such file: $2"
|
fail "No such file: $2"
|
||||||
;;
|
;;
|
||||||
answer:1:answer1.0)
|
answer:1:answer1.0)
|
||||||
echo "correct"
|
echo -n '{"Correct":true}'
|
||||||
;;
|
;;
|
||||||
answer:1:*)
|
answer:1:*)
|
||||||
echo "incorrect"
|
echo '{"Correct":false}'
|
||||||
;;
|
;;
|
||||||
answer:*:*)
|
answer:*:*)
|
||||||
fail "Fail answer"
|
fail "Fail answer"
|
||||||
|
|
|
@ -24,16 +24,15 @@ EOT
|
||||||
fail "no such file: $1"
|
fail "no such file: $1"
|
||||||
;;
|
;;
|
||||||
answer:moo)
|
answer:moo)
|
||||||
echo "correct"
|
echo '{"Correct":true}'
|
||||||
;;
|
;;
|
||||||
answer:error)
|
answer:error)
|
||||||
fail "you requested an error"
|
fail "you requested an error"
|
||||||
;;
|
;;
|
||||||
answer:*)
|
answer:*)
|
||||||
echo "incorrect"
|
echo '{"Correct":false}'
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
fail "What is $1"
|
fail "What is $1"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
@ -30,6 +30,9 @@ function renderPuzzles(obj) {
|
||||||
// Create a sorted list of category names
|
// Create a sorted list of category names
|
||||||
let cats = Object.keys(obj)
|
let cats = Object.keys(obj)
|
||||||
cats.sort()
|
cats.sort()
|
||||||
|
if (cats.length == 0) {
|
||||||
|
toast("No categories to serve!")
|
||||||
|
}
|
||||||
for (let cat of cats) {
|
for (let cat of cats) {
|
||||||
if (cat.startsWith("__")) {
|
if (cat.startsWith("__")) {
|
||||||
// Skip metadata
|
// Skip metadata
|
||||||
|
@ -103,8 +106,8 @@ function renderState(obj) {
|
||||||
let params = new URLSearchParams(window.location.search)
|
let params = new URLSearchParams(window.location.search)
|
||||||
sessionStorage.id = "1"
|
sessionStorage.id = "1"
|
||||||
sessionStorage.pid = "rodney"
|
sessionStorage.pid = "rodney"
|
||||||
}
|
renderPuzzles(obj.Puzzles)
|
||||||
if (Object.keys(obj.Puzzles).length > 0) {
|
} else if (Object.keys(obj.Puzzles).length > 0) {
|
||||||
renderPuzzles(obj.Puzzles)
|
renderPuzzles(obj.Puzzles)
|
||||||
if (obj.Config.Detachable) {
|
if (obj.Config.Detachable) {
|
||||||
fetchAll(obj.Puzzles)
|
fetchAll(obj.Puzzles)
|
||||||
|
|
Loading…
Reference in New Issue