Merge branch 'main' of https://github.com/dirtbags/moth into main

This commit is contained in:
Donaldson 2020-10-14 13:33:20 -05:00
commit c54a6dbb1a
20 changed files with 321 additions and 436 deletions

View File

@ -4,7 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v4.0.0] - Unreleased
## [v4.0.0-rc2] - Unreleased
### Fixed
- Multiple bugs preventing production server from working properly
- CI builds should be working now
- Team registration now correctly writes names to files
- Anonymized team names now only computed once per team
## [v4.0-rc1] - 2020-10-13
### Changed
- Major rewrite/refactor of `mothd`
- Clear separation of roles: State, Puzzles, and Theme
@ -37,14 +44,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security
## [Unreleased]
### Changed
- Endpoints `/points.json`, `/puzzles.json`, and `/messages.html` (optional theme file) combine into `/state`
- No more `__devel__` category for dev server: this is now `.config.devel` in the `/state` endpoint
- Development server no longer serves a static `/` with links: it now redirects you to a randomly-generated seed URL
- Default theme modifications to handle all this
- Default theme now automatically "logs you in" with Team ID if it's getting state from the devel server
## [v3.5.1] - 2020-03-16
### Fixed
- Support insta-checking for legacy puzzles

View File

@ -1,6 +1,7 @@
package main
import (
"bytes"
"log"
"net/http"
"strconv"
@ -109,7 +110,15 @@ func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter,
// 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 {
teamName = strings.TrimSpace(teamName)
if teamName == "" {
jsend.Sendf(w, jsend.Fail, "empty name", "Team name may not be empty")
return
}
if err := mh.Register(teamName); err == ErrAlreadyRegistered {
jsend.Sendf(w, jsend.Success, "already registered", "Team ID has already been registered")
} else if err != nil {
jsend.Sendf(w, jsend.Fail, "not registered", err.Error())
} else {
jsend.Sendf(w, jsend.Success, "registered", "Team ID registered")
@ -171,11 +180,12 @@ func (h *HTTPServer) MothballerHandler(mh MothRequestHandler, w http.ResponseWri
// parts[0] == "mothballer"
filename := parts[1]
cat := strings.TrimSuffix(filename, ".mb")
mothball, err := mh.Mothball(cat)
if err != nil {
mb := new(bytes.Buffer)
if err := mh.Mothball(cat, mb); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeContent(w, req, filename, time.Now(), mothball)
mbReader := bytes.NewReader(mb.Bytes())
http.ServeContent(w, req, filename, time.Now(), mbReader)
}

View File

@ -50,8 +50,8 @@ func TestHttpd(t *testing.T) {
if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 {
t.Error(r.Result())
} else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":""},"PointsLog":[],"Puzzles":{"pategory":[1]}}` {
t.Error("Unexpected state")
} else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{},"PointsLog":[],"Puzzles":{}}` {
t.Error("Unexpected state", r.Body.String())
}
if r := hs.TestRequest("/register", map[string]string{"id": "bad team id", "name": "GoTeam"}); r.Result().StatusCode != 200 {
@ -66,6 +66,12 @@ func TestHttpd(t *testing.T) {
t.Error("Register failed")
}
if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 {
t.Error(r.Result())
} else if r.Body.String() != `{"status":"success","data":{"short":"already registered","description":"Team ID has already been registered"}}` {
t.Error("Register failed", r.Body.String())
}
if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 {
t.Error(r.Result())
} else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":"GoTeam"},"PointsLog":[],"Puzzles":{"pategory":[1]}}` {
@ -108,6 +114,10 @@ func TestHttpd(t *testing.T) {
time.Sleep(TestMaintenanceInterval)
if r := hs.TestRequest("/content/pategory/2/puzzle.json", nil); r.Result().StatusCode != 200 {
t.Error(r.Result())
}
state := StateExport{}
if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 {
t.Error(r.Result())

View File

@ -3,7 +3,6 @@ package main
import (
"archive/zip"
"bufio"
"bytes"
"fmt"
"io"
"log"
@ -52,7 +51,7 @@ func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekClose
return nil, time.Time{}, fmt.Errorf("No such category: %s", cat)
}
f, err := zc.Open(fmt.Sprintf("content/%d/%s", points, filename))
f, err := zc.Open(fmt.Sprintf("%d/%s", points, filename))
if err != nil {
return nil, time.Time{}, err
}
@ -174,8 +173,8 @@ func (m *Mothballs) refresh() {
}
// Mothball just returns an error
func (m *Mothballs) Mothball(cat string) (*bytes.Reader, error) {
return nil, fmt.Errorf("Can't repackage a compiled mothball")
func (m *Mothballs) Mothball(cat string, w io.Writer) error {
return fmt.Errorf("Refusing to repackage a compiled mothball")
}
// Maintain performs housekeeping for Mothballs.

View File

@ -13,12 +13,12 @@ var testFiles = []struct {
}{
{"puzzles.txt", "1\n3\n2\n"},
{"answers.txt", "1 answer123\n1 answer456\n2 wat\n"},
{"content/1/puzzle.json", `{"name": "moo"}`},
{"content/1/moo.txt", `moo`},
{"content/2/puzzle.json", `{}`},
{"content/2/moo.txt", `moo`},
{"content/3/puzzle.json", `{}`},
{"content/3/moo.txt", `moo`},
{"1/puzzle.json", `{"name": "moo"}`},
{"1/moo.txt", `moo`},
{"2/puzzle.json", `{}`},
{"2/moo.txt", `moo`},
{"3/puzzle.json", `{}`},
{"3/moo.txt", `moo`},
}
func (m *Mothballs) createMothball(cat string) {

View File

@ -1,7 +1,6 @@
package main
import (
"bytes"
"fmt"
"io"
"strconv"
@ -42,7 +41,7 @@ type PuzzleProvider interface {
Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error)
Inventory() []Category
CheckAnswer(cat string, points int, answer string) (bool, error)
Mothball(cat string) (*bytes.Reader, error)
Mothball(cat string, w io.Writer) error
Maintainer
}
@ -108,7 +107,7 @@ type MothRequestHandler struct {
// PuzzlesOpen opens a file associated with a puzzle.
// BUG(neale): Multiple providers with the same category name are not detected or handled well.
func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (r ReadSeekCloser, ts time.Time, err error) {
export := mh.ExportState()
export := mh.exportStateIfRegistered(true)
found := false
for _, p := range export.Puzzles[cat] {
if p == points {
@ -116,7 +115,7 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (
}
}
if !found {
return nil, time.Time{}, fmt.Errorf("Category not found")
return nil, time.Time{}, fmt.Errorf("Puzzle does not exist or is locked")
}
// Try every provider until someone doesn't return an error
@ -173,27 +172,37 @@ func (mh *MothRequestHandler) Register(teamName string) error {
// the anonymized team name for this teamID has the special value "self".
// If not, the puzzles list is empty.
func (mh *MothRequestHandler) ExportState() *StateExport {
return mh.exportStateIfRegistered(false)
}
func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExport {
export := StateExport{}
export.Config = mh.Config
teamName, _ := mh.State.TeamName(mh.teamID)
teamName, err := mh.State.TeamName(mh.teamID)
registered := override || (err == nil)
export.Messages = mh.State.Messages()
export.TeamNames = map[string]string{"self": teamName}
export.TeamNames = make(map[string]string)
// Anonymize team IDs in points log, and write out team names
pointsLog := mh.State.PointsLog()
exportIDs := map[string]string{mh.teamID: "self"}
maxSolved := map[string]int{}
exportIDs := make(map[string]string)
maxSolved := make(map[string]int)
export.PointsLog = make(award.List, len(pointsLog))
if registered {
export.TeamNames["self"] = teamName
exportIDs[mh.teamID] = "self"
}
for logno, awd := range pointsLog {
if id, ok := exportIDs[awd.TeamID]; ok {
awd.TeamID = id
} else {
exportID := strconv.Itoa(logno)
name, _ := mh.State.TeamName(awd.TeamID)
exportIDs[awd.TeamID] = exportID
awd.TeamID = exportID
exportIDs[awd.TeamID] = awd.TeamID
export.TeamNames[exportID] = name
}
export.PointsLog[logno] = awd
@ -205,11 +214,10 @@ func (mh *MothRequestHandler) ExportState() *StateExport {
}
export.Puzzles = make(map[string][]int)
if _, ok := export.TeamNames["self"]; ok {
if registered {
// 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.
for _, provider := range mh.PuzzleProviders {
for _, category := range provider.Inventory() {
// Append sentry (end of puzzles)
@ -233,14 +241,16 @@ func (mh *MothRequestHandler) ExportState() *StateExport {
}
// Mothball generates a mothball for the given category.
func (mh *MothRequestHandler) Mothball(cat string) (r *bytes.Reader, err error) {
func (mh *MothRequestHandler) Mothball(cat string, w io.Writer) error {
var err error
if !mh.Config.Devel {
return nil, fmt.Errorf("Cannot mothball in production mode")
return fmt.Errorf("Cannot mothball in production mode")
}
for _, provider := range mh.PuzzleProviders {
if r, err = provider.Mothball(cat); err == nil {
return r, nil
if err = provider.Mothball(cat, w); err == nil {
return nil
}
}
return nil, err
return err
}

View File

@ -34,9 +34,28 @@ func TestServer(t *testing.T) {
server := NewTestServer()
handler := server.NewHandler(participantID, teamID)
anonHandler := server.NewHandler("badParticipantId", "badTeamId")
{
es := handler.ExportState()
if es.Config.Devel {
t.Error("Marked as development server", es.Config)
}
if len(es.Puzzles) != 0 {
t.Log("State", es)
t.Error("Unauthenticated state has non-empty puzzles list")
}
}
if err := handler.Register(teamName); err != nil {
t.Error(err)
}
if err := handler.Register(teamName); err == nil {
t.Error("Registering twice should have raised an error")
} else if err != ErrAlreadyRegistered {
t.Error("Wrong error for duplicate registration:", err)
}
if r, _, err := handler.ThemeOpen("/index.html"); err != nil {
t.Error(err)
} else if contents, err := ioutil.ReadAll(r); err != nil {
@ -45,24 +64,26 @@ func TestServer(t *testing.T) {
t.Error("index.html wrong contents", contents)
}
es := handler.ExportState()
if es.Config.Devel {
t.Error("Marked as development server", es.Config)
}
if len(es.Puzzles) != 1 {
t.Error("Puzzle categories wrong length")
}
if es.Messages != "messages.html" {
t.Error("Messages has wrong contents")
}
if len(es.PointsLog) != 0 {
t.Error("Points log not empty")
}
if len(es.TeamNames) != 1 {
t.Error("Wrong number of team names")
}
if es.TeamNames["self"] != teamName {
t.Error("TeamNames['self'] wrong")
{
es := handler.ExportState()
if es.Config.Devel {
t.Error("Marked as development server", es.Config)
}
if len(es.Puzzles) != 1 {
t.Error("Puzzle categories wrong length")
}
if es.Messages != "messages.html" {
t.Error("Messages has wrong contents")
}
if len(es.PointsLog) != 0 {
t.Error("Points log not empty")
}
if len(es.TeamNames) != 1 {
t.Error("Wrong number of team names")
}
if es.TeamNames["self"] != teamName {
t.Error("TeamNames['self'] wrong")
}
}
if r, _, err := handler.PuzzlesOpen("pategory", 1, "moo.txt"); err != nil {
@ -77,12 +98,12 @@ func TestServer(t *testing.T) {
r.Close()
}
if r, _, err := handler.PuzzlesOpen("pategory", 2, "puzzles.json"); err == nil {
if r, _, err := handler.PuzzlesOpen("pategory", 2, "puzzle.json"); err == nil {
t.Error("Opening locked puzzle shouldn't work")
r.Close()
}
if r, _, err := handler.PuzzlesOpen("pategory", 20, "puzzles.json"); err == nil {
if r, _, err := handler.PuzzlesOpen("pategory", 20, "puzzle.json"); err == nil {
t.Error("Opening non-existent puzzle shouldn't work")
r.Close()
}
@ -93,9 +114,53 @@ func TestServer(t *testing.T) {
time.Sleep(TestMaintenanceInterval)
es = handler.ExportState()
if len(es.PointsLog) != 1 {
t.Error("I didn't get my points!")
{
es := handler.ExportState()
if len(es.PointsLog) != 1 {
t.Error("I didn't get my points!")
}
if len(es.Puzzles["pategory"]) != 2 {
t.Error("The next puzzle didn't unlock!")
} else if es.Puzzles["pategory"][1] != 2 {
t.Error("The 2-point puzzle should have unlocked!")
}
}
if r, _, err := handler.PuzzlesOpen("pategory", 2, "puzzle.json"); err != nil {
t.Error("Opening unlocked puzzle should work")
} else {
r.Close()
}
if r, _, err := anonHandler.PuzzlesOpen("pategory", 2, "puzzle.json"); err != nil {
t.Error("Opening unlocked puzzle anonymously should work")
} else {
r.Close()
}
if err := handler.CheckAnswer("pategory", 2, "wat"); err != nil {
t.Error("Right answer marked wrong:", err)
}
time.Sleep(TestMaintenanceInterval)
{
es := anonHandler.ExportState()
if len(es.TeamNames) != 1 {
t.Error("Anonymous TeamNames is wrong:", es.TeamNames)
}
if len(es.PointsLog) != 2 {
t.Error("Points log wrong length")
}
if es.PointsLog[1].TeamID != "0" {
t.Error("Second point log didn't anonymize team ID correctly:", es.PointsLog[1])
}
}
{
es := handler.ExportState()
if len(es.TeamNames) != 1 {
t.Error("TeamNames is wrong:", es.TeamNames)
}
}
// BUG(neale): We aren't currently testing the various ways to disable the server

View File

@ -2,6 +2,7 @@ package main
import (
"bufio"
"errors"
"fmt"
"log"
"math/rand"
@ -23,6 +24,9 @@ const DistinguishableChars = "34678abcdefhikmnpqrtwxy="
// This is also a valid RFC3339 format.
const RFC3339Space = "2006-01-02 15:04:05Z07:00"
// ErrAlreadyRegistered means a team cannot be registered because it was registered previously.
var ErrAlreadyRegistered = errors.New("Team ID has already been registered")
// 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.
@ -127,7 +131,7 @@ func (s *State) TeamName(teamID string) (string, error) {
}
// SetTeamName writes out team name.
// This can only be done once.
// This can only be done once per team.
func (s *State) SetTeamName(teamID, teamName string) error {
idsFile, err := s.Open("teamids.txt")
if err != nil {
@ -147,14 +151,16 @@ func (s *State) SetTeamName(teamID, teamName string) error {
}
teamFilename := filepath.Join("teams", teamID)
teamFile, err := s.Fs.OpenFile(teamFilename, os.O_CREATE|os.O_EXCL, 0644)
teamFile, err := s.Fs.OpenFile(teamFilename, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
if os.IsExist(err) {
return fmt.Errorf("Team ID is already registered")
return ErrAlreadyRegistered
} else if err != nil {
return err
}
defer teamFile.Close()
log.Println("Setting team name to:", teamName, teamFilename, teamFile)
fmt.Fprintln(teamFile, teamName)
teamFile.Close()
return nil
}

View File

@ -55,12 +55,18 @@ func TestState(t *testing.T) {
t.Errorf("Setting bad team ID didn't raise an error")
}
if err := s.SetTeamName(teamID, "My Team"); err != nil {
t.Errorf("Setting team name: %v", err)
teamName := "My Team"
if err := s.SetTeamName(teamID, teamName); err != nil {
t.Errorf("Setting team name: %w", err)
}
if err := s.SetTeamName(teamID, "wat"); err == nil {
t.Errorf("Registering team a second time didn't fail")
}
if name, err := s.TeamName(teamID); err != nil {
t.Error(err)
} else if name != teamName {
t.Error("Incorrect team name:", name)
}
category := "poot"
points := 3928
@ -83,6 +89,10 @@ func TestState(t *testing.T) {
t.Error("Duplicate points award didn't fail")
}
if err := s.AwardPoints(teamID, category, points+1); err != nil {
t.Error("Awarding more points:", err)
}
pl = s.PointsLog()
if len(pl) != 1 {
t.Errorf("After awarding points, points log has length %d", len(pl))
@ -98,7 +108,7 @@ func TestState(t *testing.T) {
t.Error(err)
}
s.refresh()
if len(s.PointsLog()) != 1 {
if len(s.PointsLog()) != 2 {
t.Error("Intentional parse error screws up all parsing")
}

View File

@ -70,9 +70,9 @@ func (p TranspilerProvider) CheckAnswer(cat string, points int, answer string) (
}
// Mothball packages up a category into a mothball.
func (p TranspilerProvider) Mothball(cat string) (*bytes.Reader, error) {
func (p TranspilerProvider) Mothball(cat string, w io.Writer) error {
c := transpile.NewFsCategory(p.fs, cat)
return transpile.Mothball(c)
return transpile.Mothball(c, w)
}
// Maintain performs housekeeping.

View File

@ -16,11 +16,13 @@ import (
// T represents the state of things
type T struct {
Stdout io.Writer
Stderr io.Writer
Args []string
BaseFs afero.Fs
fs afero.Fs
Stdout io.Writer
Stderr io.Writer
Args []string
BaseFs afero.Fs
fs afero.Fs
// Arguments
filename string
answer string
}
@ -51,19 +53,21 @@ func (t *T) ParseArgs() (Command, error) {
}
flags := flag.NewFlagSet(t.Args[1], flag.ContinueOnError)
flags.SetOutput(t.Stderr)
directory := flags.String("dir", "", "Work directory")
switch t.Args[1] {
case "mothball":
cmd = t.DumpMothball
flags.StringVar(&t.filename, "out", "", "Path to create mothball (empty for stdout)")
case "inventory":
cmd = t.PrintInventory
case "open":
flags.StringVar(&t.filename, "file", "puzzle.json", "Filename to open")
cmd = t.DumpFile
flags.StringVar(&t.filename, "file", "puzzle.json", "Filename to open")
case "answer":
flags.StringVar(&t.answer, "answer", "", "Answer to check")
cmd = t.CheckAnswer
flags.StringVar(&t.answer, "answer", "", "Answer to check")
case "help":
usage(t.Stderr)
return nothing, nil
@ -73,12 +77,10 @@ func (t *T) ParseArgs() (Command, error) {
return nothing, fmt.Errorf("Invalid command")
}
flags.SetOutput(t.Stderr)
if err := flags.Parse(t.Args[2:]); err != nil {
return nothing, err
}
if *directory != "" {
log.Println(*directory)
t.fs = afero.NewBasePathFs(t.BaseFs, *directory)
} else {
t.fs = t.BaseFs
@ -140,14 +142,30 @@ func (t *T) DumpFile() error {
return nil
}
// DumpMothball writes a mothball to the writer.
// DumpMothball writes a mothball to the writer, or an output file if specified.
func (t *T) DumpMothball() error {
var w io.Writer
c := transpile.NewFsCategory(t.fs, "")
mb, err := transpile.Mothball(c)
if err != nil {
return err
removeOnError := false
switch t.filename {
case "", "-":
w = t.Stdout
default:
removeOnError = true
log.Println("Writing mothball to", t.filename)
outf, err := t.BaseFs.Create(t.filename)
if err != nil {
return err
}
defer outf.Close()
w = outf
}
if _, err := io.Copy(t.Stdout, mb); err != nil {
if err := transpile.Mothball(c, w); err != nil {
if removeOnError {
t.BaseFs.Remove(t.filename)
}
return err
}
return nil
@ -165,8 +183,6 @@ func (t *T) CheckAnswer() error {
}
func main() {
// XXX: Convert puzzle.py to standalone thingies
t := &T{
Stdout: os.Stdout,
Stderr: os.Stderr,

View File

@ -1,8 +1,10 @@
package main
import (
"archive/zip"
"bytes"
"encoding/json"
"io/ioutil"
"testing"
"github.com/dirtbags/moth/pkg/transpile"
@ -83,16 +85,72 @@ func TestEverything(t *testing.T) {
if stdout.String() != "Moo." {
t.Error("Wrong file pulled", stdout.String())
}
}
func TestMothballs(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
tp := T{
Stdout: stdout,
Stderr: stderr,
BaseFs: newTestFs(),
}
stdout.Reset()
if err := tp.Run("mothball", "-dir=unbroken"); err != nil {
t.Log(tp.fs)
if err := tp.Run("mothball", "-dir=unbroken", "-out=unbroken.mb"); err != nil {
t.Error(err)
return
}
// afero.WriteFile(tp.BaseFs, "unbroken.mb", []byte("moo"), 0644)
fis, err := afero.ReadDir(tp.BaseFs, "/")
if err != nil {
t.Error(err)
}
if stdout.Len() < 200 {
t.Error("That's way too short to be a mothball")
for _, fi := range fis {
t.Log(fi.Name())
}
if stdout.String()[:2] != "PK" {
t.Error("This mothball isn't a zip file!")
mb, err := tp.BaseFs.Open("unbroken.mb")
if err != nil {
t.Error(err)
return
}
defer mb.Close()
info, err := mb.Stat()
if err != nil {
t.Error(err)
return
}
zmb, err := zip.NewReader(mb, info.Size())
if err != nil {
t.Error(err)
return
}
for _, zf := range zmb.File {
f, err := zf.Open()
if err != nil {
t.Error(err)
continue
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
t.Error(err)
continue
}
switch zf.Name {
case "answers.txt":
if len(buf) == 0 {
t.Error("answers.txt empty")
}
case "puzzles.txt":
if len(buf) == 0 {
t.Error("puzzles.txt empty")
}
}
}
}

View File

@ -70,7 +70,7 @@ Adjusting scores
------------------
rm /srv/moth/state/enabled # Suspend scoring
nano /srv/moth/state/points.log
nano /srv/moth/state/points.log # Replace nano with your preferred editor
touch /srv/moth/state/enabled # Resume scoring
We don't warn participants before we do this:
@ -78,10 +78,8 @@ any points scored while scoring is suspended are queued up and processed as soon
It's very important to suspend scoring before mucking around with the points log.
The maintenance loop assumes it is the only thing writing to this file,
and any edits you make could blow aware points scored.
and any edits you make will remove points scored while you were editing.
No, I don't use nano.
None of us use nano.
Changing a team name

View File

@ -10,23 +10,16 @@ import (
)
// Mothball packages a Category up for a production server run.
func Mothball(c Category) (*bytes.Reader, error) {
buf := new(bytes.Buffer)
zf := zip.NewWriter(buf)
func Mothball(c Category, w io.Writer) error {
zf := zip.NewWriter(w)
inv, err := c.Inventory()
if err != nil {
return nil, err
return err
}
puzzlesTxt, err := zf.Create("puzzles.txt")
if err != nil {
return nil, err
}
answersTxt, err := zf.Create("answers.txt")
if err != nil {
return nil, err
}
puzzlesTxt := new(bytes.Buffer)
answersTxt := new(bytes.Buffer)
for _, points := range inv {
fmt.Fprintln(puzzlesTxt, points)
@ -34,11 +27,11 @@ func Mothball(c Category) (*bytes.Reader, error) {
puzzlePath := fmt.Sprintf("%d/puzzle.json", points)
pw, err := zf.Create(puzzlePath)
if err != nil {
return nil, err
return err
}
puzzle, err := c.Puzzle(points)
if err != nil {
return nil, fmt.Errorf("Puzzle %d: %s", points, err)
return fmt.Errorf("Puzzle %d: %s", points, err)
}
// Record answers in answers.txt
@ -55,7 +48,7 @@ func Mothball(c Category) (*bytes.Reader, error) {
// Write out Puzzle object
penc := json.NewEncoder(pw)
if err := penc.Encode(puzzle); err != nil {
return nil, fmt.Errorf("Puzzle %d: %s", points, err)
return fmt.Errorf("Puzzle %d: %s", points, err)
}
// Write out all attachments and scripts
@ -64,20 +57,33 @@ func Mothball(c Category) (*bytes.Reader, error) {
attPath := fmt.Sprintf("%d/%s", points, att)
aw, err := zf.Create(attPath)
if err != nil {
return nil, err
return err
}
ar, err := c.Open(points, att)
if exerr, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("Puzzle %d: %s: %s: %s", points, att, err, string(exerr.Stderr))
return fmt.Errorf("Puzzle %d: %s: %s: %s", points, att, err, string(exerr.Stderr))
} else if err != nil {
return nil, fmt.Errorf("Puzzle %d: %s: %s", points, att, err)
return fmt.Errorf("Puzzle %d: %s: %s", points, att, err)
}
if _, err := io.Copy(aw, ar); err != nil {
return nil, fmt.Errorf("Puzzle %d: %s: %s", points, att, err)
return fmt.Errorf("Puzzle %d: %s: %s", points, att, err)
}
}
}
pf, err := zf.Create("puzzles.txt")
if err != nil {
return err
}
puzzlesTxt.WriteTo(pf)
af, err := zf.Create("answers.txt")
if err != nil {
return err
}
answersTxt.WriteTo(af)
zf.Close()
return bytes.NewReader(buf.Bytes()), nil
return nil
}

View File

@ -2,6 +2,7 @@ package transpile
import (
"archive/zip"
"bytes"
"io/ioutil"
"os"
"path"
@ -14,7 +15,8 @@ import (
func TestMothballsMemFs(t *testing.T) {
static := NewFsCategory(newTestFs(), "cat1")
if _, err := Mothball(static); err != nil {
mb := new(bytes.Buffer)
if err := Mothball(static, mb); err != nil {
t.Error(err)
}
}
@ -25,13 +27,15 @@ func TestMothballsOsFs(t *testing.T) {
fs := NewRecursiveBasePathFs(afero.NewOsFs(), "testdata")
static := NewFsCategory(fs, "static")
mb, err := Mothball(static)
mb := new(bytes.Buffer)
err := Mothball(static, mb)
if err != nil {
t.Error(err)
return
}
mbr, err := zip.NewReader(mb, int64(mb.Len()))
mbReader := bytes.NewReader(mb.Bytes())
mbr, err := zip.NewReader(mbReader, int64(mb.Len()))
if err != nil {
t.Error(err)
}
@ -43,7 +47,7 @@ func TestMothballsOsFs(t *testing.T) {
defer f.Close()
if buf, err := ioutil.ReadAll(f); err != nil {
t.Error(err)
} else if string(buf) != "" {
} else if string(buf) != "1\n2\n3\n" {
t.Error("Bad puzzles.txt", string(buf))
}
}

View File

@ -59,7 +59,7 @@ function renderPuzzles(obj) {
pdiv.appendChild(l)
for (let puzzle of puzzles) {
let points = puzzle
let id = puzzle
let id = null
if (Array.isArray(puzzle)) {
points = puzzle[0]
@ -80,7 +80,7 @@ function renderPuzzles(obj) {
let url = new URL("puzzle.html", window.location)
url.searchParams.set("cat", cat)
url.searchParams.set("points", points)
url.searchParams.set("pid", id)
if (id) { url.searchParams.set("pid", id) }
a.href = url.toString()
}
}
@ -97,6 +97,7 @@ function renderPuzzles(obj) {
}
function renderState(obj) {
window.state = obj
devel = obj.Config.Devel
if (devel) {
let params = new URLSearchParams(window.location.search)

View File

@ -1,255 +0,0 @@
{
"__comment__": [
"This file is to help debug themes.",
"MOTHd will ignore it."
],
"teams": {
"0": "4HED Followers",
"1": "Dirtbags",
"17": "Eyeball",
"2": "Soup Giver!!!!!!!!!",
"24": "Dumb freshmans 3",
"25": "Winner",
"2d": "Cool team name",
"2f": "Dumm freshmans #1",
"4": "K19 the Widow Maker",
"5": "2T2",
"6": "Apples",
"7": "Top Minds",
"8": "DIRTBAGS",
"b": "Antiderivative of Pizza"
},
"points": [
[1573007086,"0","codebreaking",1],
[1573007096,"1","codebreaking",1],
[1573007114,"2","codebreaking",1],
[1573007153,"0","codebreaking",2],
[1573007159,"4","codebreaking",1],
[1573007169,"5","codebreaking",1],
[1573007181,"6","sequence",1],
[1573007184,"7","codebreaking",1],
[1573007209,"8","codebreaking",1],
[1573007212,"2","codebreaking",2],
[1573007240,"1","sequence",1],
[1573007244,"b","codebreaking",1],
[1573007246,"1","nocode",1],
[1573007258,"5","nocode",1],
[1573007271,"5","nocode",2],
[1573007284,"1","steg",1],
[1573007295,"7","codebreaking",2],
[1573007298,"2","codebreaking",4],
[1573007305,"5","nocode",3],
[1573007316,"7","codebreaking",4],
[1573007321,"0","codebreaking",4],
[1573007328,"5","nocode",4],
[1573007331,"7","nocode",1],
[1573007336,"17","codebreaking",1],
[1573007340,"7","nocode",2],
[1573007367,"0","nocode",10],
[1573007369,"7","nocode",3],
[1573007371,"b","nocode",1],
[1573007379,"7","nocode",4],
[1573007388,"b","nocode",2],
[1573007391,"6","sequence",2],
[1573007397,"4","codebreaking",2],
[1573007407,"b","nocode",3],
[1573007411,"7","nocode",10],
[1573007413,"5","nocode",10],
[1573007429,"b","nocode",4],
[1573007442,"24","codebreaking",2],
[1573007451,"25","codebreaking",1],
[1573007456,"7","sequence",1],
[1573007460,"b","nocode",10],
[1573007467,"5","sequence",1],
[1573007471,"7","sequence",2],
[1573007478,"5","sequence",2],
[1573007479,"17","codebreaking",2],
[1573007490,"2","codebreaking",5],
[1573007509,"2d","codebreaking",1],
[1573007536,"8","codebreaking",2],
[1573007544,"2f","codebreaking",1],
[1573007546,"b","sequence",1],
[1573007574,"24","codebreaking",4],
[1573007581,"b","sequence",2],
[1573007591,"25","codebreaking",2],
[1573007603,"8","codebreaking",4],
[1573007614,"0","nocode",20],
[1573007639,"4","codebreaking",4],
[1573007678,"6","codebreaking",1],
[1573007692,"8","nocode",1],
[1573007695,"24","codebreaking",5],
[1573007705,"7","codebreaking",5],
[1573007707,"8","nocode",2],
[1573007713,"17","nocode",1],
[1573007727,"17","nocode",2],
[1573007735,"8","nocode",3],
[1573007737,"b","steg",1],
[1573007739,"25","codebreaking",4],
[1573007749,"8","nocode",4],
[1573007757,"17","codebreaking",4],
[1573007768,"8","nocode",10],
[1573007795,"0","sequence",1],
[1573007799,"8","sequence",1],
[1573007816,"0","sequence",2],
[1573007822,"8","sequence",2],
[1573007834,"24","codebreaking",6],
[1573007853,"2d","codebreaking",2],
[1573007905,"1","codebreaking",2],
[1573007941,"4","codebreaking",5],
[1573007956,"1","codebreaking",4],
[1573007974,"6","codebreaking",2],
[1573007998,"17","sequence",1],
[1573008022,"b","codebreaking",4],
[1573008055,"24","sequence",2],
[1573008063,"6","codebreaking",4],
[1573008066,"2d","codebreaking",4],
[1573008074,"24","sequence",1],
[1573008099,"17","nocode",4],
[1573008101,"0","codebreaking",7],
[1573008108,"2d","nocode",1],
[1573008135,"24","nocode",30],
[1573008146,"1","codebreaking",5],
[1573008162,"2d","nocode",2],
[1573008174,"b","codebreaking",2],
[1573008191,"2","codebreaking",6],
[1573008234,"6","codebreaking",5],
[1573008240,"2","nocode",10],
[1573008291,"5","steg",1],
[1573008310,"6","nocode",1],
[1573008323,"2d","nocode",3],
[1573008327,"6","nocode",2],
[1573008330,"25","codebreaking",5],
[1573008334,"2f","codebreaking",2],
[1573008348,"6","nocode",3],
[1573008356,"2d","nocode",4],
[1573008362,"b","codebreaking",5],
[1573008364,"6","nocode",4],
[1573008364,"17","codebreaking",5],
[1573008371,"24","nocode",4],
[1573008385,"24","nocode",3],
[1573008390,"6","nocode",10],
[1573008397,"24","nocode",2],
[1573008400,"25","nocode",1],
[1573008402,"2d","nocode",10],
[1573008408,"24","nocode",1],
[1573008419,"25","nocode",2],
[1573008429,"24","steg",1],
[1573008437,"25","nocode",3],
[1573008451,"25","nocode",4],
[1573008479,"25","nocode",10],
[1573008502,"2d","sequence",1],
[1573008506,"17","codebreaking",6],
[1573008537,"2d","sequence",2],
[1573008649,"17","codebreaking",7],
[1573008668,"2f","codebreaking",4],
[1573008716,"1","codebreaking",6],
[1573008768,"8","steg",1],
[1573008808,"7","nocode",50],
[1573008817,"24","steg",2],
[1573008832,"2f","codebreaking",5],
[1573008890,"17","steg",1],
[1573008902,"b","steg",2],
[1573008932,"7","steg",1],
[1573008944,"24","steg",3],
[1573008978,"2","steg",1],
[1573009006,"24","steg",4],
[1573009032,"6","steg",1],
[1573009038,"b","steg",3],
[1573009052,"2d","codebreaking",5],
[1573009098,"b","steg",4],
[1573009122,"8","steg",2],
[1573009125,"4","nocode",1],
[1573009160,"24","nocode",10],
[1573009161,"4","nocode",2],
[1573009179,"2","steg",2],
[1573009180,"1","steg",2],
[1573009194,"24","nocode",20],
[1573009203,"0","nocode",50],
[1573009212,"2f","codebreaking",6],
[1573009240,"2f","nocode",1],
[1573009250,"4","nocode",4],
[1573009255,"2f","nocode",2],
[1573009258,"2","steg",4],
[1573009282,"4","nocode",10],
[1573009299,"25","sequence",1],
[1573009305,"6","steg",4],
[1573009308,"17","steg",3],
[1573009310,"1","steg",3],
[1573009334,"7","steg",4],
[1573009345,"1","steg",4],
[1573009345,"7","steg",3],
[1573009354,"8","steg",4],
[1573009357,"25","sequence",2],
[1573009402,"6","steg",3],
[1573009402,"b","sequence",8],
[1573009413,"2f","nocode",3],
[1573009437,"17","steg",2],
[1573009455,"2f","nocode",10],
[1573009481,"b","sequence",16],
[1573009502,"b","sequence",19],
[1573009520,"b","sequence",25],
[1573009525,"17","steg",4],
[1573009559,"7","steg",2],
[1573009561,"b","sequence",35],
[1573009571,"0","sequence",35],
[1573009588,"25","steg",1],
[1573009602,"24","sequence",8],
[1573009607,"2","steg",5],
[1573009614,"1","steg",5],
[1573009617,"17","sequence",35],
[1573009620,"7","sequence",50],
[1573009621,"6","steg",5],
[1573009629,"5","steg",3],
[1573009632,"7","sequence",35],
[1573009644,"17","sequence",25],
[1573009670,"6","steg",6],
[1573009698,"8","steg",6],
[1573009700,"17","sequence",19],
[1573009703,"24","steg",6],
[1573009703,"4","sequence",1],
[1573009707,"0","sequence",50],
[1573009710,"25","steg",2],
[1573009729,"2f","sequence",1],
[1573009768,"1","steg",6],
[1573009814,"2","codebreaking",8],
[1573009842,"0","steg",1],
[1573009844,"2f","sequence",2],
[1573009882,"4","steg",1],
[1573009896,"25","steg",3],
[1573009931,"1","sequence",2],
[1573009937,"25","steg",4],
[1573010066,"7","steg",6],
[1573010101,"25","steg",5],
[1573010114,"5","steg",4],
[1573010137,"25","steg",6],
[1573010185,"4","sequence",2],
[1573010229,"17","nocode",80],
[1573010256,"24","sequence",35],
[1573010281,"6","codebreaking",7],
[1573010336,"25","codebreaking",6],
[1573010390,"7","codebreaking",7],
[1573010468,"2f","steg",1],
[1573010712,"0","steg",2],
[1573010739,"0","steg",3],
[1573010754,"0","steg",4],
[1573010778,"0","steg",5],
[1573010784,"7","nocode",90],
[1573010792,"0","steg",6],
[1573011760,"7","sequence",60],
[1573056120,"0","sequence",100],
[1573056324,"0","sequence",200],
[1573056791,"0","sequence",300],
[1573057092,"0","sequence",400],
[1573076767,"25","sequence",400],
[1573076809,"25","sequence",300],
[1573076838,"25","sequence",200],
[1573076936,"25","nocode",20],
[1573077275,"25","nocode",50],
[1573078364,"0","sequence",19],
[1573078432,"0","sequence",25],
[1573078487,"25","sequence",35],
[1573078501,"25","sequence",50],
[1573079359,"0","nocode",90],
[1573079714,"25","nocode",9]
]
}

View File

@ -132,7 +132,7 @@ async function loadPuzzle(categoryName, points, puzzleId) {
document.getElementById("authors").textContent = window.puzzle.Pre.Authors.join(", ")
// If answers are provided, this is the devel server
if (window.puzzle.Answers) {
if (window.puzzle.Answers.length > 0) {
devel_addin(document.getElementById("devel"))
}
@ -204,8 +204,8 @@ function init() {
let points = params.get("points")
let puzzleId = params.get("pid")
if (categoryName && points && puzzleId) {
loadPuzzle(categoryName, points, puzzleId)
if (categoryName && points) {
loadPuzzle(categoryName, points, puzzleId || points)
}
let teamId = sessionStorage.getItem("id")

View File

@ -1,54 +0,0 @@
{
"__comment__": [
"This file is to help debug themes.",
"MOTHd will ignore it."
],
"codebreaking": [
[1,"37117e6b034696b86c6516477cc0bc60bc1e642e"],
[2,"546b586428979771b061608489327da4940086a7"],
[4,"6f2a33c93f56b4f29cc79e6576ba4d1000aa1756"],
[5,"c654fe263909b1940d7aad8c572363a0569c07c6"],
[6,"f30bd32bf940f2bb03506ec334d2d204efc4695b"],
[7,"128b119083b6ae70c380a8eb70ec6a518425e7af"],
[8,"edd4f57aeb565b3b053fa194f5e677cb77ef0285"],
[15,"9781863bca9f596972e2a10460932ec5ec6be3fe"]
],
"nocode": [
[1,"37117e6b034696b86c6516477cc0bc60bc1e642e"],
[2,"546b586428979771b061608489327da4940086a7"],
[3,"79c08697a1923da1118fd0c2e922b5d3899cabcc"],
[4,"6f2a33c93f56b4f29cc79e6576ba4d1000aa1756"],
[10,"bf4fae263bf6e4243b143f4ecd64e471f3ec75dd"],
[20,"9f374f6dac9f972fac4693099a7bfa7c535f7503"],
[30,"02de1196d43976b2d050c6c597f068623d2df201"],
[50,"9acb3af947cb4aa10a9c1221c04518f956cdc0d0"],
[80,"78f807ac44f3cbf537861e7cdf1ac53937e4ee47"],
[90,"6d537653aa599178c72528f7e1f2fbb36e6333f9"],
[100,"4f5982a3a7cc9b9af0320130132e8cab39a1fd2c"]
],
"sequence": [
[1,"37117e6b034696b86c6516477cc0bc60bc1e642e"],
[2,"546b586428979771b061608489327da4940086a7"],
[8,"edd4f57aeb565b3b053fa194f5e677cb77ef0285"],
[16,"a9ace4b773f045c422260edefaa8563dcd80ac59"],
[19,"f11ca0172451f37ba6f4d66ff9add80013480a49"],
[25,"0458533d28705548829e53d686215cc6fbeec8f5"],
[35,"91aac06bae090ae7d1699b5a78601ef8d29e9271"],
[50,"9acb3af947cb4aa10a9c1221c04518f956cdc0d0"],
[60,"bf84beed9e382268ab40d0113dfeb73c96aa919a"],
[100,"4f5982a3a7cc9b9af0320130132e8cab39a1fd2c"],
[200,"3b9b8993fe639cf0c19a58b39ebbf6077828887a"],
[300,"0f13c4d19bc5d2e10d43e8cd2e40f759e731cece"],
[400,"db7a59f313818fc9598969d2a0a04e21bd26697f"],
[500,"81c5389eb5406aa44053662f6482f246b8a12e0c"]
],
"steg": [
[1,"200e8cd902ba7304765c463f6ed1322bc25f3454"],
[2,"707328988c3986d450d8fe419eb49f078fb7998c"],
[3,"d0b336ad59cbcd4415ddf200c6c099db5c3fea1d"],
[4,"f071503b403ffee2b38e186e800bfd5dd28e8f0e"],
[5,"186f425fa5762ef37f874cc602fe0edc4325a5d2"],
[6,"c6527c3c30c4e6a33026192d358d83d259cd17a7"],
[10,"84973f77a1b14e4666f3d8a8bdeead7633c4ed56"]
]
}

View File

@ -13,6 +13,8 @@ function scoreboardInit() {
]
function update(state) {
window.state = state
for (let rotate of document.querySelectorAll(".rotate")) {
rotate.appendChild(rotate.firstElementChild)
}