From ffab4d37b6a5fa43c849f2ecee401dafdf7c6b42 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Sat, 6 Jul 2019 00:53:46 +0100 Subject: [PATCH 01/96] Adding YAML support to Moth files --- Dockerfile.moth-devel | 3 +- devel/moth.py | 119 ++++++++++++++++++++++++++++-------------- 2 files changed, 82 insertions(+), 40 deletions(-) diff --git a/Dockerfile.moth-devel b/Dockerfile.moth-devel index 6e8466a..a667a8e 100644 --- a/Dockerfile.moth-devel +++ b/Dockerfile.moth-devel @@ -9,7 +9,8 @@ RUN apk --no-cache add \ && \ pip3 install \ scapy==2.4.2 \ - pillow==5.4.1 + pillow==5.4.1 \ + PyYAML==5.1.1 COPY devel /app/ diff --git a/devel/moth.py b/devel/moth.py index 612d371..25bc3cf 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -13,6 +13,7 @@ import random import string import tempfile import shlex +import yaml messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' @@ -91,47 +92,87 @@ class Puzzle: def read_stream(self, stream): header = True + line = "" + if stream.read(3) == "---": + header = "yaml" + else: + header = "moth" + + stream.seek(0) + + if header == "yaml": + self.read_yaml_header(stream) + elif header == "moth": + self.read_moth_header(stream) + for line in stream: - if header: - line = line.strip() - if not line: - header = False - continue - key, val = line.split(':', 1) - key = key.lower() - val = val.strip() - if key == 'author': - self.authors.append(val) - elif key == 'summary': - self.summary = val - elif key == 'answer': - self.answers.append(val) - elif key == 'pattern': - self.pattern = val - elif key == 'hint': - self.hint = val - elif key == 'name': - pass - elif key == 'file': - parts = shlex.split(val) - name = parts[0] - hidden = False - stream = open(name, 'rb') - try: - name = parts[1] - hidden = (parts[2].lower() == "hidden") - except IndexError: - pass - self.files[name] = PuzzleFile(stream, name, not hidden) - elif key == 'script': - stream = open(val, 'rb') - # Make sure this shows up in the header block of the HTML output. - self.files[val] = PuzzleFile(stream, val, visible=False) - self.scripts.append(val) - else: - raise ValueError("Unrecognized header field: {}".format(key)) + self.body.write(line) + + def read_yaml_header(self, stream): + contents = "" + header = False + for line in stream: + if line.strip() == "---" and header: # Handle last line + break + elif line.strip() == "---": # Handle first line + header = True + continue else: - self.body.write(line) + contents += line + + config = yaml.safe_load(contents) + for key, value in config.items(): + key = key.lower() + self.handle_header_key(key, value) + + + def read_moth_header(self, stream): + for line in stream: + line = line.strip() + if not line: + break + + key, val = line.split(':', 1) + key = key.lower() + val = val.strip() + self.handle_header_key(key, val) + + def handle_header_key(self, key, val): + if key == 'author': + self.authors.append(val) + elif key == 'summary': + self.summary = val + elif key == 'answer': + self.answers.append(val) + elif key == "answers": + for answer in val: + answer = str(answer) + self.answers.append(answer) + elif key == 'pattern': + self.pattern = val + elif key == 'hint': + self.hint = val + elif key == 'name': + pass + elif key == 'file': + parts = shlex.split(val) + name = parts[0] + hidden = False + stream = open(name, 'rb') + try: + name = parts[1] + hidden = (parts[2].lower() == "hidden") + except IndexError: + pass + self.files[name] = PuzzleFile(stream, name, not hidden) + elif key == 'script': + stream = open(val, 'rb') + # Make sure this shows up in the header block of the HTML output. + self.files[val] = PuzzleFile(stream, val, visible=False) + self.scripts.append(val) + else: + raise ValueError("Unrecognized header field: {}".format(key)) + def read_directory(self, path): try: From 97f808804ec12e14edff5725a4a73799fae47da6 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Tue, 9 Jul 2019 18:59:57 +0100 Subject: [PATCH 02/96] Force answers to be provided as strings --- devel/moth.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/devel/moth.py b/devel/moth.py index 25bc3cf..92e2ecb 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -143,10 +143,13 @@ class Puzzle: elif key == 'summary': self.summary = val elif key == 'answer': + if not isinstance(val, str): + raise ValueError("Answers must be strings, got %s, instead" % (type(val),)) self.answers.append(val) elif key == "answers": for answer in val: - answer = str(answer) + if not isinstance(answer, str): + raise ValueError("Answers must be strings, got %s, instead" % (type(answer),)) self.answers.append(answer) elif key == 'pattern': self.pattern = val From 9dbafa276b10c2b3e16d8155a6eaf63d02ad73cd Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 15 Aug 2019 19:46:49 -0600 Subject: [PATCH 03/96] distinguish between a zipfs (which is a zip with files in it) and a mothball (which is a zipfs with certain layout) --- src/instance.go | 4 +- src/maintenance.go | 2 +- src/mothball.go | 191 ------------------------ src/zipfs.go | 191 ++++++++++++++++++++++++ src/{mothball_test.go => zipfs_test.go} | 6 +- 5 files changed, 197 insertions(+), 197 deletions(-) delete mode 100644 src/mothball.go create mode 100644 src/zipfs.go rename src/{mothball_test.go => zipfs_test.go} (87%) diff --git a/src/instance.go b/src/instance.go index 1d3a26f..0741ca0 100644 --- a/src/instance.go +++ b/src/instance.go @@ -22,7 +22,7 @@ type Instance struct { ThemeDir string AttemptInterval time.Duration - categories map[string]*Mothball + categories map[string]*Zipfs update chan bool jPuzzleList []byte jPointsLog []byte @@ -41,7 +41,7 @@ func (ctx *Instance) Initialize() error { } ctx.Base = strings.TrimRight(ctx.Base, "/") - ctx.categories = map[string]*Mothball{} + ctx.categories = map[string]*Zipfs{} ctx.update = make(chan bool, 10) ctx.nextAttempt = map[string]time.Time{} ctx.nextAttemptMutex = new(sync.RWMutex) diff --git a/src/maintenance.go b/src/maintenance.go index abd51e0..751271b 100644 --- a/src/maintenance.go +++ b/src/maintenance.go @@ -148,7 +148,7 @@ func (ctx *Instance) tidy() { categoryName := strings.TrimSuffix(filename, ".mb") if _, ok := ctx.categories[categoryName]; !ok { - mb, err := OpenMothball(filepath) + mb, err := OpenZipfs(filepath) if err != nil { log.Printf("Error opening %s: %s", filepath, err) continue diff --git a/src/mothball.go b/src/mothball.go deleted file mode 100644 index 149dbf5..0000000 --- a/src/mothball.go +++ /dev/null @@ -1,191 +0,0 @@ -package main - -import ( - "archive/zip" - "fmt" - "io" - "io/ioutil" - "os" - "strings" - "time" -) - -type Mothball struct { - zf *zip.ReadCloser - filename string - mtime time.Time -} - -type MothballFile struct { - f io.ReadCloser - pos int64 - zf *zip.File - io.Reader - io.Seeker - io.Closer -} - -func NewMothballFile(zf *zip.File) (*MothballFile, error) { - mf := &MothballFile{ - zf: zf, - pos: 0, - f: nil, - } - if err := mf.reopen(); err != nil { - return nil, err - } - return mf, nil -} - -func (mf *MothballFile) reopen() error { - if mf.f != nil { - if err := mf.f.Close(); err != nil { - return err - } - } - f, err := mf.zf.Open() - if err != nil { - return err - } - mf.f = f - mf.pos = 0 - return nil -} - -func (mf *MothballFile) ModTime() time.Time { - return mf.zf.Modified -} - -func (mf *MothballFile) Read(p []byte) (int, error) { - n, err := mf.f.Read(p) - mf.pos += int64(n) - return n, err -} - -func (mf *MothballFile) Seek(offset int64, whence int) (int64, error) { - var pos int64 - switch whence { - case io.SeekStart: - pos = offset - case io.SeekCurrent: - pos = mf.pos + int64(offset) - case io.SeekEnd: - pos = int64(mf.zf.UncompressedSize64) - int64(offset) - } - - if pos < 0 { - return mf.pos, fmt.Errorf("Tried to seek %d before start of file", pos) - } - if pos >= int64(mf.zf.UncompressedSize64) { - // We don't need to decompress anything, we're at the end of the file - mf.f.Close() - mf.f = ioutil.NopCloser(strings.NewReader("")) - mf.pos = int64(mf.zf.UncompressedSize64) - return mf.pos, nil - } - if pos < mf.pos { - if err := mf.reopen(); err != nil { - return mf.pos, err - } - } - - buf := make([]byte, 32*1024) - for pos > mf.pos { - l := pos - mf.pos - if l > int64(cap(buf)) { - l = int64(cap(buf)) - 1 - } - p := buf[0:int(l)] - n, err := mf.Read(p) - if err != nil { - return mf.pos, err - } else if n <= 0 { - return mf.pos, fmt.Errorf("Short read (%d bytes)", n) - } - } - - return mf.pos, nil -} - -func (mf *MothballFile) Close() error { - return mf.f.Close() -} - -func OpenMothball(filename string) (*Mothball, error) { - var m Mothball - - m.filename = filename - - err := m.Refresh() - if err != nil { - return nil, err - } - - return &m, nil -} - -func (m *Mothball) Close() error { - return m.zf.Close() -} - -func (m *Mothball) Refresh() error { - info, err := os.Stat(m.filename) - if err != nil { - return err - } - mtime := info.ModTime() - - if !mtime.After(m.mtime) { - return nil - } - - zf, err := zip.OpenReader(m.filename) - if err != nil { - return err - } - - if m.zf != nil { - m.zf.Close() - } - m.zf = zf - m.mtime = mtime - - return nil -} - -func (m *Mothball) get(filename string) (*zip.File, error) { - for _, f := range m.zf.File { - if filename == f.Name { - return f, nil - } - } - return nil, fmt.Errorf("File not found: %s %s", m.filename, filename) -} - -func (m *Mothball) Header(filename string) (*zip.FileHeader, error) { - f, err := m.get(filename) - if err != nil { - return nil, err - } - return &f.FileHeader, nil -} - -func (m *Mothball) Open(filename string) (*MothballFile, error) { - f, err := m.get(filename) - if err != nil { - return nil, err - } - mf, err := NewMothballFile(f) - return mf, err -} - -func (m *Mothball) ReadFile(filename string) ([]byte, error) { - f, err := m.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - - bytes, err := ioutil.ReadAll(f) - return bytes, err -} diff --git a/src/zipfs.go b/src/zipfs.go new file mode 100644 index 0000000..1b0f00a --- /dev/null +++ b/src/zipfs.go @@ -0,0 +1,191 @@ +package main + +import ( + "archive/zip" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "time" +) + +type Zipfs struct { + zf *zip.ReadCloser + filename string + mtime time.Time +} + +type ZipfsFile struct { + f io.ReadCloser + pos int64 + zf *zip.File + io.Reader + io.Seeker + io.Closer +} + +func NewZipfsFile(zf *zip.File) (*ZipfsFile, error) { + zfsf := &ZipfsFile{ + zf: zf, + pos: 0, + f: nil, + } + if err := zfsf.reopen(); err != nil { + return nil, err + } + return zfsf, nil +} + +func (zfsf *ZipfsFile) reopen() error { + if zfsf.f != nil { + if err := zfsf.f.Close(); err != nil { + return err + } + } + f, err := zfsf.zf.Open() + if err != nil { + return err + } + zfsf.f = f + zfsf.pos = 0 + return nil +} + +func (zfsf *ZipfsFile) ModTime() time.Time { + return zfsf.zf.Modified +} + +func (zfsf *ZipfsFile) Read(p []byte) (int, error) { + n, err := zfsf.f.Read(p) + zfsf.pos += int64(n) + return n, err +} + +func (zfsf *ZipfsFile) Seek(offset int64, whence int) (int64, error) { + var pos int64 + switch whence { + case io.SeekStart: + pos = offset + case io.SeekCurrent: + pos = zfsf.pos + int64(offset) + case io.SeekEnd: + pos = int64(zfsf.zf.UncompressedSize64) - int64(offset) + } + + if pos < 0 { + return zfsf.pos, fmt.Errorf("Tried to seek %d before start of file", pos) + } + if pos >= int64(zfsf.zf.UncompressedSize64) { + // We don't need to decompress anything, we're at the end of the file + zfsf.f.Close() + zfsf.f = ioutil.NopCloser(strings.NewReader("")) + zfsf.pos = int64(zfsf.zf.UncompressedSize64) + return zfsf.pos, nil + } + if pos < zfsf.pos { + if err := zfsf.reopen(); err != nil { + return zfsf.pos, err + } + } + + buf := make([]byte, 32*1024) + for pos > zfsf.pos { + l := pos - zfsf.pos + if l > int64(cap(buf)) { + l = int64(cap(buf)) - 1 + } + p := buf[0:int(l)] + n, err := zfsf.Read(p) + if err != nil { + return zfsf.pos, err + } else if n <= 0 { + return zfsf.pos, fmt.Errorf("Short read (%d bytes)", n) + } + } + + return zfsf.pos, nil +} + +func (zfsf *ZipfsFile) Close() error { + return zfsf.f.Close() +} + +func OpenZipfs(filename string) (*Zipfs, error) { + var zfs Zipfs + + zfs.filename = filename + + err := zfs.Refresh() + if err != nil { + return nil, err + } + + return &zfs, nil +} + +func (zfs *Zipfs) Close() error { + return zfs.zf.Close() +} + +func (zfs *Zipfs) Refresh() error { + info, err := os.Stat(zfs.filename) + if err != nil { + return err + } + mtime := info.ModTime() + + if !mtime.After(zfs.mtime) { + return nil + } + + zf, err := zip.OpenReader(zfs.filename) + if err != nil { + return err + } + + if zfs.zf != nil { + zfs.zf.Close() + } + zfs.zf = zf + zfs.mtime = mtime + + return nil +} + +func (zfs *Zipfs) get(filename string) (*zip.File, error) { + for _, f := range zfs.zf.File { + if filename == f.Name { + return f, nil + } + } + return nil, fmt.Errorf("File not found: %s %s", zfs.filename, filename) +} + +func (zfs *Zipfs) Header(filename string) (*zip.FileHeader, error) { + f, err := zfs.get(filename) + if err != nil { + return nil, err + } + return &f.FileHeader, nil +} + +func (zfs *Zipfs) Open(filename string) (*ZipfsFile, error) { + f, err := zfs.get(filename) + if err != nil { + return nil, err + } + zfsf, err := NewZipfsFile(f) + return zfsf, err +} + +func (zfs *Zipfs) ReadFile(filename string) ([]byte, error) { + f, err := zfs.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + + bytes, err := ioutil.ReadAll(f) + return bytes, err +} diff --git a/src/mothball_test.go b/src/zipfs_test.go similarity index 87% rename from src/mothball_test.go rename to src/zipfs_test.go index 8115809..22d9386 100644 --- a/src/mothball_test.go +++ b/src/zipfs_test.go @@ -9,8 +9,8 @@ import ( "testing" ) -func TestMothball(t *testing.T) { - tf, err := ioutil.TempFile("", "mothball") +func TestZipfs(t *testing.T) { + tf, err := ioutil.TempFile("", "zipfs") if err != nil { t.Error(err) return @@ -35,7 +35,7 @@ func TestMothball(t *testing.T) { tf.Close() // Now read it in - mb, err := OpenMothball(tf.Name()) + mb, err := OpenZipfs(tf.Name()) if err != nil { t.Error(err) return From b5424fb005d9bf20ec0e284c28d509094984641f Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 17 Aug 2019 11:01:26 -0600 Subject: [PATCH 04/96] transpiler handling rfc822 and yaml metadata --- {src => cmd/mothd}/award.go | 0 {src => cmd/mothd}/award_test.go | 0 {src => cmd/mothd}/handlers.go | 0 {src => cmd/mothd}/instance.go | 0 {src => cmd/mothd}/instance_test.go | 0 {src => cmd/mothd}/maintenance.go | 0 {src => cmd/mothd}/mothd.go | 0 {src => cmd/mothd}/zipfs.go | 0 {src => cmd/mothd}/zipfs_test.go | 0 cmd/transpile/main.go | 136 ++++++++++++++++++++++++++++ 10 files changed, 136 insertions(+) rename {src => cmd/mothd}/award.go (100%) rename {src => cmd/mothd}/award_test.go (100%) rename {src => cmd/mothd}/handlers.go (100%) rename {src => cmd/mothd}/instance.go (100%) rename {src => cmd/mothd}/instance_test.go (100%) rename {src => cmd/mothd}/maintenance.go (100%) rename {src => cmd/mothd}/mothd.go (100%) rename {src => cmd/mothd}/zipfs.go (100%) rename {src => cmd/mothd}/zipfs_test.go (100%) create mode 100644 cmd/transpile/main.go diff --git a/src/award.go b/cmd/mothd/award.go similarity index 100% rename from src/award.go rename to cmd/mothd/award.go diff --git a/src/award_test.go b/cmd/mothd/award_test.go similarity index 100% rename from src/award_test.go rename to cmd/mothd/award_test.go diff --git a/src/handlers.go b/cmd/mothd/handlers.go similarity index 100% rename from src/handlers.go rename to cmd/mothd/handlers.go diff --git a/src/instance.go b/cmd/mothd/instance.go similarity index 100% rename from src/instance.go rename to cmd/mothd/instance.go diff --git a/src/instance_test.go b/cmd/mothd/instance_test.go similarity index 100% rename from src/instance_test.go rename to cmd/mothd/instance_test.go diff --git a/src/maintenance.go b/cmd/mothd/maintenance.go similarity index 100% rename from src/maintenance.go rename to cmd/mothd/maintenance.go diff --git a/src/mothd.go b/cmd/mothd/mothd.go similarity index 100% rename from src/mothd.go rename to cmd/mothd/mothd.go diff --git a/src/zipfs.go b/cmd/mothd/zipfs.go similarity index 100% rename from src/zipfs.go rename to cmd/mothd/zipfs.go diff --git a/src/zipfs_test.go b/cmd/mothd/zipfs_test.go similarity index 100% rename from src/zipfs_test.go rename to cmd/mothd/zipfs_test.go diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go new file mode 100644 index 0000000..3f8b4ea --- /dev/null +++ b/cmd/transpile/main.go @@ -0,0 +1,136 @@ +package main + +import ( + "gopkg.in/russross/blackfriday.v2" + "gopkg.in/yaml.v2" + "bufio" + "bytes" + "flag" + "fmt" + "log" + "io" + "os" + "net/mail" + "strings" +) + +type Header struct { + Pre struct { + Authors []string + } + Answers []string + Post struct { + Objective string + } + Debug struct { + Log []string + Error string + } +} + +type HeaderParser func([]byte) (*Header, error) + +func YamlParser(input []byte) (*Header, error) { + header := new(Header) + + err := yaml.Unmarshal(input, header) + if err != nil { + return nil, err + } + return header, nil +} + +func Rfc822Parser(input []byte) (*Header, error) { + msgBytes := append(input, '\n') + r := bytes.NewReader(msgBytes) + m, err := mail.ReadMessage(r) + if err != nil { + return nil, err + } + + header := new(Header) + for key, val := range m.Header { + key = strings.ToLower(key) + switch key { + case "author": + header.Pre.Authors = val + case "answer": + header.Answers = val + default: + return nil, fmt.Errorf("Unknown header field: %s", key) + } + } + + return header, nil +} + + +func parse(r io.Reader) (error) { + headerEnd := "" + headerBuf := new(bytes.Buffer) + headerParser := Rfc822Parser + + scanner := bufio.NewScanner(r) + lineNo := 0 + for scanner.Scan() { + line := scanner.Text() + lineNo += 1 + if lineNo == 1 { + if line == "---" { + headerParser = YamlParser + headerEnd = "---" + continue + } else { + headerParser = Rfc822Parser + } + } + if line == headerEnd { + break + } + headerBuf.WriteString(line) + headerBuf.WriteRune('\n') + } + + bodyBuf := new(bytes.Buffer) + for scanner.Scan() { + line := scanner.Text() + lineNo += 1 + bodyBuf.WriteString(line) + bodyBuf.WriteRune('\n') + } + + header, err := headerParser(headerBuf.Bytes()) + if err != nil { + return err + } + + headerB, _ := yaml.Marshal(header) + bodyB := blackfriday.Run(bodyBuf.Bytes()) + fmt.Println(string(headerB)) + fmt.Println("") + fmt.Println(string(bodyB)) + + return nil +} + +func main() { + flag.Parse() + + if flag.NArg() < 1 { + fmt.Fprintf(flag.CommandLine.Output(), "Error: no files to parse\n\n") + flag.PrintDefaults() + os.Exit(1) + } + + for _,filename := range flag.Args() { + f, err := os.Open(filename) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + if err := parse(f); err != nil { + log.Fatal(err) + } + } +} From 711ae36fb21eb5539e95e435530b264c55be1422 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 17 Aug 2019 13:09:09 -0600 Subject: [PATCH 05/96] Transpiling everything in netarch now --- cmd/transpile/main.go | 139 ++++++++++++++++++++++++++++-------------- 1 file changed, 93 insertions(+), 46 deletions(-) diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go index 3f8b4ea..8d615e5 100644 --- a/cmd/transpile/main.go +++ b/cmd/transpile/main.go @@ -1,71 +1,113 @@ package main import ( - "gopkg.in/russross/blackfriday.v2" - "gopkg.in/yaml.v2" "bufio" "bytes" + "encoding/json" "flag" "fmt" - "log" + "gopkg.in/russross/blackfriday.v2" + "gopkg.in/yaml.v2" "io" - "os" + "log" "net/mail" + "os" "strings" ) -type Header struct { - Pre struct { - Authors []string - } - Answers []string - Post struct { - Objective string - } - Debug struct { - Log []string - Error string - } +type Attachment struct { + Filename string // Filename presented as part of puzzle + FilesystemPath string // Filename in backing FS (URL, mothball, or local FS) + Listed bool // Whether this file is listed as an attachment } -type HeaderParser func([]byte) (*Header, error) +type Puzzle struct { + Pre struct { + Authors []string + Attachments []Attachment + AnswerPattern string + Body string + } + Post struct { + Objective string + Success struct { + Acceptable string + Mastery string + } + KSAs []string + } + Debug struct { + Log []string + Errors []string + Hints []string + Summary string + } + Answers []string +} -func YamlParser(input []byte) (*Header, error) { - header := new(Header) - - err := yaml.Unmarshal(input, header) +type HeaderParser func([]byte) (*Puzzle, error) + +func YamlParser(input []byte) (*Puzzle, error) { + puzzle := new(Puzzle) + + err := yaml.Unmarshal(input, puzzle) if err != nil { return nil, err } - return header, nil + return puzzle, nil } -func Rfc822Parser(input []byte) (*Header, error) { +func Rfc822Parser(input []byte) (*Puzzle, error) { msgBytes := append(input, '\n') r := bytes.NewReader(msgBytes) m, err := mail.ReadMessage(r) if err != nil { return nil, err } - - header := new(Header) + + puzzle := new(Puzzle) for key, val := range m.Header { key = strings.ToLower(key) switch key { - case "author": - header.Pre.Authors = val - case "answer": - header.Answers = val - default: - return nil, fmt.Errorf("Unknown header field: %s", key) + case "author": + puzzle.Pre.Authors = val + case "pattern": + puzzle.Pre.AnswerPattern = val[0] + case "answer": + puzzle.Answers = val + case "summary": + puzzle.Debug.Summary = val[0] + case "hint": + puzzle.Debug.Hints = val + case "ksa": + puzzle.Post.KSAs = val + case "file": + for _, txt := range val { + parts := strings.SplitN(txt, " ", 3) + attachment := Attachment{} + attachment.FilesystemPath = parts[0] + if len(parts) > 1 { + attachment.Filename = parts[1] + } else { + attachment.Filename = attachment.FilesystemPath + } + if (len(parts) > 2) && (parts[2] == "hidden") { + attachment.Listed = false + } else { + attachment.Listed = true + } + + puzzle.Pre.Attachments = append(puzzle.Pre.Attachments, attachment) + } + default: + return nil, fmt.Errorf("Unknown header field: %s", key) } } - return header, nil + return puzzle, nil } - -func parse(r io.Reader) (error) { +func parse(r io.Reader) error { headerEnd := "" headerBuf := new(bytes.Buffer) headerParser := Rfc822Parser @@ -90,7 +132,7 @@ func parse(r io.Reader) (error) { headerBuf.WriteString(line) headerBuf.WriteRune('\n') } - + bodyBuf := new(bytes.Buffer) for scanner.Scan() { line := scanner.Text() @@ -98,37 +140,42 @@ func parse(r io.Reader) (error) { bodyBuf.WriteString(line) bodyBuf.WriteRune('\n') } - - header, err := headerParser(headerBuf.Bytes()) + + puzzle, err := headerParser(headerBuf.Bytes()) if err != nil { return err } - - headerB, _ := yaml.Marshal(header) + bodyB := blackfriday.Run(bodyBuf.Bytes()) - fmt.Println(string(headerB)) - fmt.Println("") - fmt.Println(string(bodyB)) + + if (puzzle.Pre.Body != "") && (len(bodyB) > 0) { + log.Print("Body specified in header; overwriting...") + } + puzzle.Pre.Body = string(bodyB) + + puzzleB, _ := json.MarshalIndent(puzzle, "", " ") + + fmt.Println(string(puzzleB)) return nil } func main() { flag.Parse() - + if flag.NArg() < 1 { fmt.Fprintf(flag.CommandLine.Output(), "Error: no files to parse\n\n") flag.PrintDefaults() os.Exit(1) } - - for _,filename := range flag.Args() { + + for _, filename := range flag.Args() { f, err := os.Open(filename) if err != nil { log.Fatal(err) } defer f.Close() - + if err := parse(f); err != nil { log.Fatal(err) } From 447bb3158f0c9d48bd1cd411eab0585b9912bf11 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 17 Aug 2019 16:00:15 -0600 Subject: [PATCH 06/96] Start implementing a category doodad --- cmd/transpile/category.go | 18 ++++++ cmd/transpile/{main.go => puzzle.go} | 83 +++++++++++----------------- 2 files changed, 49 insertions(+), 52 deletions(-) create mode 100644 cmd/transpile/category.go rename cmd/transpile/{main.go => puzzle.go} (71%) diff --git a/cmd/transpile/category.go b/cmd/transpile/category.go new file mode 100644 index 0000000..2310862 --- /dev/null +++ b/cmd/transpile/category.go @@ -0,0 +1,18 @@ +package main + +import ( + "os" + "flag" + "fmt" +) + +func main() { + flag.Parse() + + for _, dirname := range flag.Args() { + f, _ := os.Open(dirname) + defer f.Close() + names, _ := f.Readdirnames(0) + fmt.Print(names) + } +} \ No newline at end of file diff --git a/cmd/transpile/main.go b/cmd/transpile/puzzle.go similarity index 71% rename from cmd/transpile/main.go rename to cmd/transpile/puzzle.go index 8d615e5..4b197a7 100644 --- a/cmd/transpile/main.go +++ b/cmd/transpile/puzzle.go @@ -3,15 +3,12 @@ package main import ( "bufio" "bytes" - "encoding/json" - "flag" "fmt" "gopkg.in/russross/blackfriday.v2" "gopkg.in/yaml.v2" "io" "log" "net/mail" - "os" "strings" ) @@ -25,6 +22,7 @@ type Puzzle struct { Pre struct { Authors []string Attachments []Attachment + Scripts []Attachment AnswerPattern string Body string } @@ -57,6 +55,27 @@ func YamlParser(input []byte) (*Puzzle, error) { return puzzle, nil } +func AttachmentParser(val []string) ([]Attachment) { + ret := make([]Attachment, len(val)) + for idx, txt := range val { + parts := strings.SplitN(txt, " ", 3) + cur := Attachment{} + cur.FilesystemPath = parts[0] + if len(parts) > 1 { + cur.Filename = parts[1] + } else { + cur.Filename = cur.FilesystemPath + } + if (len(parts) > 2) && (parts[2] == "hidden") { + cur.Listed = false + } else { + cur.Listed = true + } + ret[idx] = cur + } + return ret +} + func Rfc822Parser(input []byte) (*Puzzle, error) { msgBytes := append(input, '\n') r := bytes.NewReader(msgBytes) @@ -73,6 +92,10 @@ func Rfc822Parser(input []byte) (*Puzzle, error) { puzzle.Pre.Authors = val case "pattern": puzzle.Pre.AnswerPattern = val[0] + case "script": + puzzle.Pre.Scripts = AttachmentParser(val) + case "file": + puzzle.Pre.Attachments = AttachmentParser(val) case "answer": puzzle.Answers = val case "summary": @@ -81,24 +104,6 @@ func Rfc822Parser(input []byte) (*Puzzle, error) { puzzle.Debug.Hints = val case "ksa": puzzle.Post.KSAs = val - case "file": - for _, txt := range val { - parts := strings.SplitN(txt, " ", 3) - attachment := Attachment{} - attachment.FilesystemPath = parts[0] - if len(parts) > 1 { - attachment.Filename = parts[1] - } else { - attachment.Filename = attachment.FilesystemPath - } - if (len(parts) > 2) && (parts[2] == "hidden") { - attachment.Listed = false - } else { - attachment.Listed = true - } - - puzzle.Pre.Attachments = append(puzzle.Pre.Attachments, attachment) - } default: return nil, fmt.Errorf("Unknown header field: %s", key) } @@ -107,7 +112,7 @@ func Rfc822Parser(input []byte) (*Puzzle, error) { return puzzle, nil } -func parse(r io.Reader) error { +func ParseMoth(r io.Reader) (*Puzzle, error) { headerEnd := "" headerBuf := new(bytes.Buffer) headerParser := Rfc822Parser @@ -143,41 +148,15 @@ func parse(r io.Reader) error { puzzle, err := headerParser(headerBuf.Bytes()) if err != nil { - return err + return nil, err } - + + // Markdownify the body bodyB := blackfriday.Run(bodyBuf.Bytes()) - if (puzzle.Pre.Body != "") && (len(bodyB) > 0) { log.Print("Body specified in header; overwriting...") } puzzle.Pre.Body = string(bodyB) - puzzleB, _ := json.MarshalIndent(puzzle, "", " ") - - fmt.Println(string(puzzleB)) - - return nil -} - -func main() { - flag.Parse() - - if flag.NArg() < 1 { - fmt.Fprintf(flag.CommandLine.Output(), "Error: no files to parse\n\n") - flag.PrintDefaults() - os.Exit(1) - } - - for _, filename := range flag.Args() { - f, err := os.Open(filename) - if err != nil { - log.Fatal(err) - } - defer f.Close() - - if err := parse(f); err != nil { - log.Fatal(err) - } - } + return puzzle, nil } From fc79db677d41033639f858635f7fc8bb96b161a6 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 18 Aug 2019 21:59:06 -0600 Subject: [PATCH 07/96] Generating whole categories now --- cmd/transpile/category.go | 121 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 5 deletions(-) diff --git a/cmd/transpile/category.go b/cmd/transpile/category.go index 2310862..450f5bc 100644 --- a/cmd/transpile/category.go +++ b/cmd/transpile/category.go @@ -4,15 +4,126 @@ import ( "os" "flag" "fmt" + "log" + "path/filepath" + "hash/fnv" + "encoding/binary" + "encoding/json" + "encoding/hex" + "strconv" + "math/rand" ) + +type PuzzleEntry struct { + Id string + Points int + Puzzle Puzzle +} + + +func HashHex(input ...string) (string) { + hasher := fnv.New64() + for _, s := range input { + fmt.Fprintln(hasher, s) + } + return hex.EncodeToString(hasher.Sum(nil)) +} + + +func PrngOfStrings(input ...string) (*rand.Rand) { + hasher := fnv.New64() + for _, s := range input { + fmt.Fprint(hasher, s, "\n") + } + seed := binary.BigEndian.Uint64(hasher.Sum(nil)) + source := rand.NewSource(int64(seed)) + return rand.New(source) +} + + +func ParsePuzzle(puzzlePath string, seed string) (*Puzzle, error) { + puzzleFd, err := os.Open(puzzlePath) + if err != nil { + return nil, err + } + defer puzzleFd.Close() + + puzzle, err := ParseMoth(puzzleFd) + if err != nil { + return nil, err + } + + return puzzle, nil +} + + +func ParseCategory(categoryPath string, seed string) ([]PuzzleEntry, error) { + categoryFd, err := os.Open(categoryPath) + if err != nil { + return nil, err + } + defer categoryFd.Close() + + puzzleDirs, err := categoryFd.Readdirnames(0) + if err != nil { + return nil, err + } + + puzzleEntries := make([]PuzzleEntry, 0, len(puzzleDirs)) + for _, puzzleDir := range puzzleDirs { + puzzlePath := filepath.Join(categoryPath, puzzleDir, "puzzle.moth") + puzzleSeed := fmt.Sprintf("%s/%s", seed, puzzleDir) + + points, err := strconv.Atoi(puzzleDir) + if err != nil { + log.Printf("Skipping %s: %v", puzzlePath, err) + continue + } + + puzzle, err := ParsePuzzle(puzzlePath, puzzleSeed) + if err != nil { + log.Printf("Skipping %s: %v", puzzlePath, err) + continue + } + + prng := PrngOfStrings(puzzleSeed) + idBytes := make([]byte, 16) + prng.Read(idBytes) + id := hex.EncodeToString(idBytes) + puzzleEntry := PuzzleEntry{ + Id: id, + Puzzle: *puzzle, + Points: points, + } + puzzleEntries = append(puzzleEntries, puzzleEntry) + } + + return puzzleEntries, nil +} + + func main() { + // XXX: We need a way to pass in "only run this one point value puzzle" + flag.Parse() + baseSeedString := os.Getenv("SEED") for _, dirname := range flag.Args() { - f, _ := os.Open(dirname) - defer f.Close() - names, _ := f.Readdirnames(0) - fmt.Print(names) + categoryName := filepath.Base(dirname) + categorySeed := fmt.Sprintf("%s/%s", baseSeedString, categoryName) + puzzles, err := ParseCategory(dirname, categorySeed) + if err != nil { + log.Print(err) + continue + } + + jpuzzles, err := json.MarshalIndent(puzzles, "", " ") + if err != nil { + log.Print(err) + continue + } + + fmt.Println(string(jpuzzles)) } -} \ No newline at end of file +} From edfa895b584217a8eb4e7be07b6d6256472bccd9 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 18 Aug 2019 21:59:59 -0600 Subject: [PATCH 08/96] More documentation on what to do next --- cmd/transpile/category.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/transpile/category.go b/cmd/transpile/category.go index 450f5bc..d2be4c7 100644 --- a/cmd/transpile/category.go +++ b/cmd/transpile/category.go @@ -105,7 +105,7 @@ func ParseCategory(categoryPath string, seed string) ([]PuzzleEntry, error) { func main() { // XXX: We need a way to pass in "only run this one point value puzzle" - + // XXX: Convert puzzle.py to standalone thingies flag.Parse() baseSeedString := os.Getenv("SEED") From 921cc86c42b7ef794c40003360f9906ae1a6ca9a Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 18 Aug 2019 22:01:36 -0600 Subject: [PATCH 09/96] Remove unused function --- cmd/transpile/category.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/cmd/transpile/category.go b/cmd/transpile/category.go index d2be4c7..d48289c 100644 --- a/cmd/transpile/category.go +++ b/cmd/transpile/category.go @@ -21,16 +21,6 @@ type PuzzleEntry struct { Puzzle Puzzle } - -func HashHex(input ...string) (string) { - hasher := fnv.New64() - for _, s := range input { - fmt.Fprintln(hasher, s) - } - return hex.EncodeToString(hasher.Sum(nil)) -} - - func PrngOfStrings(input ...string) (*rand.Rand) { hasher := fnv.New64() for _, s := range input { From de8f6cbeb8060c87526b04a18b1e454bdbf3a2f9 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 25 Aug 2019 07:10:32 -0600 Subject: [PATCH 10/96] Bit of reorg, add missing files --- LICENSE.md | 9 +++++++-- cmd/mothd/instance.go | 2 ++ cmd/mothd/mothd.go | 6 ++++++ cmd/transpile/rfc822.md | 15 +++++++++++++++ cmd/transpile/yaml.md | 22 ++++++++++++++++++++++ {devel => contrib}/mothd.service | 0 {devel => lib/python}/answer_words.txt | 0 {devel => lib/python}/devel-server.py | 0 {devel => lib/python}/mistune.py | 0 {devel => lib/python}/moth.py | 0 {devel => lib/python}/mothballer.py | 0 {devel => lib/python}/package-puzzles.py | 0 {devel => lib/python}/parse.py | 0 {devel => lib/python}/setup.cfg | 0 {devel => lib/python}/update-words.sh | 0 {devel => lib/python}/validate.py | 0 16 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 cmd/transpile/rfc822.md create mode 100644 cmd/transpile/yaml.md rename {devel => contrib}/mothd.service (100%) rename {devel => lib/python}/answer_words.txt (100%) rename {devel => lib/python}/devel-server.py (100%) rename {devel => lib/python}/mistune.py (100%) rename {devel => lib/python}/moth.py (100%) rename {devel => lib/python}/mothballer.py (100%) rename {devel => lib/python}/package-puzzles.py (100%) rename {devel => lib/python}/parse.py (100%) rename {devel => lib/python}/setup.cfg (100%) rename {devel => lib/python}/update-words.sh (100%) rename {devel => lib/python}/validate.py (100%) diff --git a/LICENSE.md b/LICENSE.md index d77eb2a..5810db0 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -7,10 +7,15 @@ Copyright © 2015-2016 Neale Pickett > publish, distribute, sublicense, and/or sell copies of the Software, > and to permit persons to whom the Software is furnished to do so, > subject to the following conditions: - +> > The above copyright notice and this permission notice shall be > included in all copies or substantial portions of the Software. - +> +> Except as contained in this notice, the name(s) of the above +> copyright holders shall not be used in advertising or otherwise +> to promote the sale, use or other dealings in this Software +> without prior written authorization. +> > The software is provided "as is", without warranty of any kind, > express or implied, including but not limited to the warranties of > merchantability, fitness for a particular purpose and diff --git a/cmd/mothd/instance.go b/cmd/mothd/instance.go index 0741ca0..162f9ef 100644 --- a/cmd/mothd/instance.go +++ b/cmd/mothd/instance.go @@ -18,9 +18,11 @@ import ( type Instance struct { Base string MothballDir string + PuzzlesDir string StateDir string ThemeDir string AttemptInterval time.Duration + Debug bool categories map[string]*Zipfs update chan bool diff --git a/cmd/mothd/mothd.go b/cmd/mothd/mothd.go index abf02cb..a1a5a0b 100644 --- a/cmd/mothd/mothd.go +++ b/cmd/mothd/mothd.go @@ -29,6 +29,12 @@ func main() { "/mothballs", "Path to read mothballs", ) + flag.StringVar( + &ctx.PuzzlesDir, + "puzzles", + "", + "Path to read puzzle source trees", + ) flag.StringVar( &ctx.StateDir, "state", diff --git a/cmd/transpile/rfc822.md b/cmd/transpile/rfc822.md new file mode 100644 index 0000000..e972932 --- /dev/null +++ b/cmd/transpile/rfc822.md @@ -0,0 +1,15 @@ +Author: neale +Answer: moo + +A MOTH file +=========== + +This is a moth file, woo wo! + +# A MOTH file + +* moo +* moo +* moo +* squeak + diff --git a/cmd/transpile/yaml.md b/cmd/transpile/yaml.md new file mode 100644 index 0000000..3fdaeee --- /dev/null +++ b/cmd/transpile/yaml.md @@ -0,0 +1,22 @@ +--- +pre: + authors: + - neale +answers: + - moo +--- + +A YAML MOTH file +=========== + +This is a moth file, woo wo! + +With YAML metadata! + +# A MOTH file + +* moo +* moo +* moo +* + diff --git a/devel/mothd.service b/contrib/mothd.service similarity index 100% rename from devel/mothd.service rename to contrib/mothd.service diff --git a/devel/answer_words.txt b/lib/python/answer_words.txt similarity index 100% rename from devel/answer_words.txt rename to lib/python/answer_words.txt diff --git a/devel/devel-server.py b/lib/python/devel-server.py similarity index 100% rename from devel/devel-server.py rename to lib/python/devel-server.py diff --git a/devel/mistune.py b/lib/python/mistune.py similarity index 100% rename from devel/mistune.py rename to lib/python/mistune.py diff --git a/devel/moth.py b/lib/python/moth.py similarity index 100% rename from devel/moth.py rename to lib/python/moth.py diff --git a/devel/mothballer.py b/lib/python/mothballer.py similarity index 100% rename from devel/mothballer.py rename to lib/python/mothballer.py diff --git a/devel/package-puzzles.py b/lib/python/package-puzzles.py similarity index 100% rename from devel/package-puzzles.py rename to lib/python/package-puzzles.py diff --git a/devel/parse.py b/lib/python/parse.py similarity index 100% rename from devel/parse.py rename to lib/python/parse.py diff --git a/devel/setup.cfg b/lib/python/setup.cfg similarity index 100% rename from devel/setup.cfg rename to lib/python/setup.cfg diff --git a/devel/update-words.sh b/lib/python/update-words.sh similarity index 100% rename from devel/update-words.sh rename to lib/python/update-words.sh diff --git a/devel/validate.py b/lib/python/validate.py similarity index 100% rename from devel/validate.py rename to lib/python/validate.py From dbcc6544929b4a3cbc7277685e5fba0cbcccafba Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 25 Aug 2019 07:18:23 -0600 Subject: [PATCH 11/96] Remove ncurses clause from MIT license --- LICENSE.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 5810db0..e17c0a9 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -11,11 +11,6 @@ Copyright © 2015-2016 Neale Pickett > The above copyright notice and this permission notice shall be > included in all copies or substantial portions of the Software. > -> Except as contained in this notice, the name(s) of the above -> copyright holders shall not be used in advertising or otherwise -> to promote the sale, use or other dealings in this Software -> without prior written authorization. -> > The software is provided "as is", without warranty of any kind, > express or implied, including but not limited to the warranties of > merchantability, fitness for a particular purpose and From c020fb18c2ad3b384c703f6619dbf87e13378c31 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 25 Aug 2019 07:20:17 -0600 Subject: [PATCH 12/96] License formatting --- LICENSE.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index e17c0a9..82e110c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,24 +1,24 @@ Copyright © 2015-2016 Neale Pickett -> Permission is hereby granted, free of charge, to any person -> obtaining a copy of this software and associated documentation files -> (the "Software"), to deal in the Software without restriction, -> including without limitation the rights to use, copy, modify, merge, -> publish, distribute, sublicense, and/or sell copies of the Software, -> and to permit persons to whom the Software is furnished to do so, -> subject to the following conditions: -> -> The above copyright notice and this permission notice shall be -> included in all copies or substantial portions of the Software. -> -> The software is provided "as is", without warranty of any kind, -> express or implied, including but not limited to the warranties of -> merchantability, fitness for a particular purpose and -> noninfringement. In no event shall the authors or copyright holders -> be liable for any claim, damages or other liability, whether in an -> action of contract, tort or otherwise, arising from, out of or in -> connection with the software or the use or other dealings in the -> software. +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +The software is provided "as is", without warranty of any kind, +express or implied, including but not limited to the warranties of +merchantability, fitness for a particular purpose and +noninfringement. In no event shall the authors or copyright holders +be liable for any claim, damages or other liability, whether in an +action of contract, tort or otherwise, arising from, out of or in +connection with the software or the use or other dealings in the +software. Font Licenses From 890718e8bb5f2322bcf4d33f16e6f7216ff7b8b1 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 25 Aug 2019 17:30:32 -0600 Subject: [PATCH 13/96] Transpiler now runs commands with JSON output --- cmd/transpile/category.go | 74 ++++++++++++++++++++------- example-puzzles/example/6/puzzle.moth | 17 ++++++ example-puzzles/example/7/mkpuzzle | 24 +++++++++ 3 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 example-puzzles/example/6/puzzle.moth create mode 100755 example-puzzles/example/7/mkpuzzle diff --git a/cmd/transpile/category.go b/cmd/transpile/category.go index d48289c..80aaaa6 100644 --- a/cmd/transpile/category.go +++ b/cmd/transpile/category.go @@ -12,6 +12,10 @@ import ( "encoding/hex" "strconv" "math/rand" + "context" + "time" + "os/exec" + "bytes" ) @@ -32,18 +36,28 @@ func PrngOfStrings(input ...string) (*rand.Rand) { } -func ParsePuzzle(puzzlePath string, seed string) (*Puzzle, error) { - puzzleFd, err := os.Open(puzzlePath) - if err != nil { - return nil, err - } - defer puzzleFd.Close() +func runPuzzleGen(puzzlePath string, seed string) (*Puzzle, error) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() - puzzle, err := ParseMoth(puzzleFd) + cmd := exec.CommandContext(ctx, puzzlePath) + cmd.Env = append( + os.Environ(), + fmt.Sprintf("MOTH_PUZZLE_SEED=%s", seed), + ) + stdout, err := cmd.Output() if err != nil { return nil, err } - + + jsdec := json.NewDecoder(bytes.NewReader(stdout)) + jsdec.DisallowUnknownFields() + puzzle := new(Puzzle) + err = jsdec.Decode(puzzle) + if err != nil { + return nil, err + } + return puzzle, nil } @@ -62,22 +76,43 @@ func ParseCategory(categoryPath string, seed string) ([]PuzzleEntry, error) { puzzleEntries := make([]PuzzleEntry, 0, len(puzzleDirs)) for _, puzzleDir := range puzzleDirs { - puzzlePath := filepath.Join(categoryPath, puzzleDir, "puzzle.moth") - puzzleSeed := fmt.Sprintf("%s/%s", seed, puzzleDir) + var puzzle *Puzzle + puzzlePath := filepath.Join(categoryPath, puzzleDir) + + // Determine point value from directory name points, err := strconv.Atoi(puzzleDir) if err != nil { log.Printf("Skipping %s: %v", puzzlePath, err) continue } - - puzzle, err := ParsePuzzle(puzzlePath, puzzleSeed) - if err != nil { - log.Printf("Skipping %s: %v", puzzlePath, err) + + // Try the .moth file first + puzzleMothPath := filepath.Join(puzzlePath, "puzzle.moth") + puzzleFd, err := os.Open(puzzleMothPath) + if err == nil { + defer puzzleFd.Close() + puzzle, err = ParseMoth(puzzleFd) + if err != nil { + log.Printf("Skipping %s: %v", puzzleMothPath, err) + continue + } + } else if os.IsNotExist(err) { + var genErr error + puzzleGenPath := filepath.Join(puzzlePath, "mkpuzzle") + puzzle, genErr = runPuzzleGen(puzzleGenPath, puzzlePath) + if genErr != nil { + log.Printf("Skipping %20s: %v", puzzleMothPath, err) + log.Printf("Skipping %20s: %v", puzzleGenPath, genErr) + continue + } + } else { + log.Printf("Skipping %s: %v", puzzleMothPath, err) continue } - prng := PrngOfStrings(puzzleSeed) + // Create a category entry for this + prng := PrngOfStrings(puzzlePath) idBytes := make([]byte, 16) prng.Read(idBytes) id := hex.EncodeToString(idBytes) @@ -99,6 +134,10 @@ func main() { flag.Parse() baseSeedString := os.Getenv("SEED") + jsenc := json.NewEncoder(os.Stdout) + jsenc.SetEscapeHTML(false) + jsenc.SetIndent("", " ") + for _, dirname := range flag.Args() { categoryName := filepath.Base(dirname) categorySeed := fmt.Sprintf("%s/%s", baseSeedString, categoryName) @@ -108,12 +147,9 @@ func main() { continue } - jpuzzles, err := json.MarshalIndent(puzzles, "", " ") - if err != nil { + if err := jsenc.Encode(puzzles); err != nil { log.Print(err) continue } - - fmt.Println(string(jpuzzles)) } } diff --git a/example-puzzles/example/6/puzzle.moth b/example-puzzles/example/6/puzzle.moth new file mode 100644 index 0000000..f5425d0 --- /dev/null +++ b/example-puzzles/example/6/puzzle.moth @@ -0,0 +1,17 @@ +--- +Summary: YAML Metadata +Author: neale +Answer: YAML +Answer: yaml +Objective: | + Understand how YAML metadata can be used in a `.moth` file +Success: + Acceptable: | + Enter the answer and move on + Mastery: | + Create a `.moth` file using YAML metadata and serve it + through the devel server. +--- + +You can also provide metadata in YAML format. +This puzzle's `.moth` file serves as an example. diff --git a/example-puzzles/example/7/mkpuzzle b/example-puzzles/example/7/mkpuzzle new file mode 100755 index 0000000..6919d6b --- /dev/null +++ b/example-puzzles/example/7/mkpuzzle @@ -0,0 +1,24 @@ +#! /bin/sh + +cat <Writing Your Own Generator

\ +

\ + You can output the JSON puzzle format if you want.\ + See the source of this puzzle for an example!\ +

\ + " + }, + "debug": { + "log": [ + "There are many fields you can specify, aside from those in this file.", + "See puzzle.moth for the full spec.", + "MOTH_PUZZLE_SEED=$MOTH_PUZZLE_SEED" + ] + }, + "answers": ["JSON"] +} +EOD From 506e0ace6bbc85a112972e9afcca2bec2b7a0c5d Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 25 Aug 2019 18:50:38 -0600 Subject: [PATCH 14/96] Transpiler can now render individual puzles --- cmd/transpile/category.go | 99 ++++++++++++++++----------------------- cmd/transpile/main.go | 69 +++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 58 deletions(-) create mode 100644 cmd/transpile/main.go diff --git a/cmd/transpile/category.go b/cmd/transpile/category.go index 80aaaa6..3f303c3 100644 --- a/cmd/transpile/category.go +++ b/cmd/transpile/category.go @@ -2,7 +2,6 @@ package main import ( "os" - "flag" "fmt" "log" "path/filepath" @@ -37,7 +36,7 @@ func PrngOfStrings(input ...string) (*rand.Rand) { func runPuzzleGen(puzzlePath string, seed string) (*Puzzle, error) { - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() cmd := exec.CommandContext(ctx, puzzlePath) @@ -61,6 +60,38 @@ func runPuzzleGen(puzzlePath string, seed string) (*Puzzle, error) { return puzzle, nil } +func ParsePuzzle(puzzlePath string, puzzleSeed string) (*Puzzle, error) { + var puzzle *Puzzle + + // Try the .moth file first + puzzleMothPath := filepath.Join(puzzlePath, "puzzle.moth") + puzzleFd, err := os.Open(puzzleMothPath) + if err == nil { + defer puzzleFd.Close() + puzzle, err = ParseMoth(puzzleFd) + if err != nil { + return nil, err + } + } else if os.IsNotExist(err) { + var genErr error + + puzzleGenPath := filepath.Join(puzzlePath, "mkpuzzle") + puzzle, genErr = runPuzzleGen(puzzleGenPath, puzzlePath) + if genErr != nil { + bigErr := fmt.Errorf( + "%v; (%s: %v)", + genErr, + filepath.Base(puzzleMothPath), err, + ) + return nil, bigErr + } + } else { + return nil, err + } + + return puzzle, nil +} + func ParseCategory(categoryPath string, seed string) ([]PuzzleEntry, error) { categoryFd, err := os.Open(categoryPath) @@ -76,41 +107,20 @@ func ParseCategory(categoryPath string, seed string) ([]PuzzleEntry, error) { puzzleEntries := make([]PuzzleEntry, 0, len(puzzleDirs)) for _, puzzleDir := range puzzleDirs { - var puzzle *Puzzle - puzzlePath := filepath.Join(categoryPath, puzzleDir) - - // Determine point value from directory name - points, err := strconv.Atoi(puzzleDir) + puzzleSeed := fmt.Sprintf("%s/%s", seed, puzzleDir) + puzzle, err := ParsePuzzle(puzzlePath, puzzleSeed) if err != nil { log.Printf("Skipping %s: %v", puzzlePath, err) continue } - - // Try the .moth file first - puzzleMothPath := filepath.Join(puzzlePath, "puzzle.moth") - puzzleFd, err := os.Open(puzzleMothPath) - if err == nil { - defer puzzleFd.Close() - puzzle, err = ParseMoth(puzzleFd) - if err != nil { - log.Printf("Skipping %s: %v", puzzleMothPath, err) - continue - } - } else if os.IsNotExist(err) { - var genErr error - puzzleGenPath := filepath.Join(puzzlePath, "mkpuzzle") - puzzle, genErr = runPuzzleGen(puzzleGenPath, puzzlePath) - if genErr != nil { - log.Printf("Skipping %20s: %v", puzzleMothPath, err) - log.Printf("Skipping %20s: %v", puzzleGenPath, genErr) - continue - } - } else { - log.Printf("Skipping %s: %v", puzzleMothPath, err) - continue - } + // Determine point value from directory name + points, err := strconv.Atoi(puzzleDir) + if err != nil { + return nil, err + } + // Create a category entry for this prng := PrngOfStrings(puzzlePath) idBytes := make([]byte, 16) @@ -126,30 +136,3 @@ func ParseCategory(categoryPath string, seed string) ([]PuzzleEntry, error) { return puzzleEntries, nil } - - -func main() { - // XXX: We need a way to pass in "only run this one point value puzzle" - // XXX: Convert puzzle.py to standalone thingies - flag.Parse() - baseSeedString := os.Getenv("SEED") - - jsenc := json.NewEncoder(os.Stdout) - jsenc.SetEscapeHTML(false) - jsenc.SetIndent("", " ") - - for _, dirname := range flag.Args() { - categoryName := filepath.Base(dirname) - categorySeed := fmt.Sprintf("%s/%s", baseSeedString, categoryName) - puzzles, err := ParseCategory(dirname, categorySeed) - if err != nil { - log.Print(err) - continue - } - - if err := jsenc.Encode(puzzles); err != nil { - log.Print(err) - continue - } - } -} diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go new file mode 100644 index 0000000..e589c71 --- /dev/null +++ b/cmd/transpile/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "flag" + "encoding/json" + "path/filepath" + "strconv" + "strings" + "os" + "log" + "fmt" +) + +func seedJoin(parts ...string) string { + return strings.Join(parts, "::") +} + +func usage() { + out := flag.CommandLine.Output() + name := flag.CommandLine.Name() + fmt.Fprintf(out, "Usage: %s [OPTIONS] CATEGORY [CATEGORY ...]\n", name) + flag.PrintDefaults() +} + +func main() { + // XXX: We need a way to pass in "only run this one point value puzzle" + // XXX: Convert puzzle.py to standalone thingies + + flag.Usage = usage + points := flag.Int("points", 0, "Transpile only this point value puzzle") + flag.Parse() + + baseSeedString := os.Getenv("MOTH_SEED") + + jsenc := json.NewEncoder(os.Stdout) + jsenc.SetEscapeHTML(false) + jsenc.SetIndent("", " ") + + for _, categoryPath := range flag.Args() { + categoryName := filepath.Base(categoryPath) + categorySeed := seedJoin(baseSeedString, categoryName) + + if *points > 0 { + puzzleDir := strconv.Itoa(*points) + puzzleSeed := seedJoin(categorySeed, puzzleDir) + puzzlePath := filepath.Join(categoryPath, puzzleDir) + puzzle, err := ParsePuzzle(puzzlePath, puzzleSeed) + if err != nil { + log.Print(err) + continue + } + + if err := jsenc.Encode(puzzle); err != nil { + log.Fatal(err) + } + } else { + puzzles, err := ParseCategory(categoryPath, categorySeed) + if err != nil { + log.Print(err) + continue + } + + if err := jsenc.Encode(puzzles); err != nil { + log.Print(err) + continue + } + } + } +} From c6f8e87bb6d0b3377ff11091756985f6c0079b18 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 31 Aug 2019 16:09:19 -0600 Subject: [PATCH 15/96] Start coding up a WebDAV server --- cmd/mothdav/main.go | 82 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 cmd/mothdav/main.go diff --git a/cmd/mothdav/main.go b/cmd/mothdav/main.go new file mode 100644 index 0000000..d8bb201 --- /dev/null +++ b/cmd/mothdav/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "time" + "context" + + "golang.org/x/net/webdav" +) + +type StubLockSystem struct { +} + +func (ls *StubLockSystem) Confirm(now time.Time, name0, name1 string, conditions ...webdav.Condition) (release func(), err error) { + return nil, webdav.ErrConfirmationFailed +} + +func (ls *StubLockSystem) Create(now time.Time, details webdav.LockDetails) (token string, err error) { + return "", webdav.ErrLocked +} + +func (ls *StubLockSystem) Refresh(now time.Time, token string, duration time.Duration) (webdav.LockDetails, error) { + return webdav.LockDetails{}, webdav.ErrNoSuchLock +} + +func (ls *StubLockSystem) Unlock(now time.Time, token string) error { + return webdav.ErrNoSuchLock +} + + +type MothFS struct { +} + +func (fs *MothFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error { + return os.ErrPermission +} + +func (fs *MothFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { + f, err := os.Open("hello.txt") + return f, err +} + +func (fs *MothFS) RemoveAll(ctx context.Context, name string) error { + return os.ErrPermission +} + +func (fs *MothFS) Rename(ctx context.Context, oldName, newName string) error { + return os.ErrPermission +} + +func (fs *MothFS) Stat(ctx context.Context, name string) (os.FileInfo, error) { + info, err := os.Stat("hello.txt") + return info, err +} + +func main() { + //dirFlag := flag.String("d", "./", "Directory to serve from. Default is CWD") + httpPort := flag.Int("p", 80, "Port to serve on (Plain HTTP)") + + flag.Parse() + + srv := &webdav.Handler{ + FileSystem: new(MothFS), + LockSystem: new(StubLockSystem), + Logger: func(r *http.Request, err error) { + if err != nil { + log.Printf("WEBDAV [%s]: %s, ERROR: %s\n", r.Method, r.URL, err) + } else { + log.Printf("WEBDAV [%s]: %s \n", r.Method, r.URL) + } + }, + } + http.Handle("/", srv) + if err := http.ListenAndServe(fmt.Sprintf(":%d", *httpPort), nil); err != nil { + log.Fatalf("Error with WebDAV server: %v", err) + } + +} From 657a02e773f1cf2fa3ffac468c886c21496cfbca Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 2 Sep 2019 18:13:37 -0600 Subject: [PATCH 16/96] Refactor state functions into their own doodad --- cmd/mothd/award.go | 35 +-- cmd/mothd/state.go | 356 ++++++++++++++++++++++++ cmd/{mothd => mothdv3}/handlers.go | 0 cmd/{mothd => mothdv3}/instance.go | 0 cmd/{mothd => mothdv3}/instance_test.go | 0 cmd/{mothd => mothdv3}/maintenance.go | 0 cmd/{mothd => mothdv3}/mothd.go | 0 cmd/{mothd => mothdv3}/zipfs.go | 0 cmd/{mothd => mothdv3}/zipfs_test.go | 0 {docs => doc}/CREDITS.md | 0 {docs => doc}/devel-server.md | 0 {docs => doc}/dirtbags.svg | 0 {docs => doc}/overview.md | 0 {docs => doc}/philosophy.md | 0 {docs => doc}/tokens.md | 0 {docs => doc}/writing-puzzles.md | 0 16 files changed, 360 insertions(+), 31 deletions(-) create mode 100644 cmd/mothd/state.go rename cmd/{mothd => mothdv3}/handlers.go (100%) rename cmd/{mothd => mothdv3}/instance.go (100%) rename cmd/{mothd => mothdv3}/instance_test.go (100%) rename cmd/{mothd => mothdv3}/maintenance.go (100%) rename cmd/{mothd => mothdv3}/mothd.go (100%) rename cmd/{mothd => mothdv3}/zipfs.go (100%) rename cmd/{mothd => mothdv3}/zipfs_test.go (100%) rename {docs => doc}/CREDITS.md (100%) rename {docs => doc}/devel-server.md (100%) rename {docs => doc}/dirtbags.svg (100%) rename {docs => doc}/overview.md (100%) rename {docs => doc}/philosophy.md (100%) rename {docs => doc}/tokens.md (100%) rename {docs => doc}/writing-puzzles.md (100%) diff --git a/cmd/mothd/award.go b/cmd/mothd/award.go index 4a8ba75..8fa7772 100644 --- a/cmd/mothd/award.go +++ b/cmd/mothd/award.go @@ -1,14 +1,13 @@ package main import ( - "encoding/json" "fmt" "strings" - "time" ) type Award struct { - When time.Time + // Unix epoch time of this event + When int64 TeamId string Category string Points int @@ -19,44 +18,18 @@ func ParseAward(s string) (*Award, error) { s = strings.TrimSpace(s) - var whenEpoch int64 - - n, err := fmt.Sscanf(s, "%d %s %s %d", &whenEpoch, &ret.TeamId, &ret.Category, &ret.Points) + n, err := fmt.Sscanf(s, "%d %s %s %d", &ret.When, &ret.TeamId, &ret.Category, &ret.Points) if err != nil { return nil, err } else if n != 4 { return nil, fmt.Errorf("Malformed award string: only parsed %d fields", n) } - ret.When = time.Unix(whenEpoch, 0) - 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 (a *Award) MarshalJSON() ([]byte, error) { - if a == nil { - return []byte("null"), nil - } - jTeamId, err := json.Marshal(a.TeamId) - if err != nil { - return nil, err - } - jCategory, err := json.Marshal(a.Category) - if err != nil { - return nil, err - } - ret := fmt.Sprintf( - "[%d,%s,%s,%d]", - a.When.Unix(), - jTeamId, - jCategory, - a.Points, - ) - return []byte(ret), nil + return fmt.Sprintf("%d %s %s %d", a.When, a.TeamId, a.Category, a.Points) } func (a *Award) Same(o *Award) bool { diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go new file mode 100644 index 0000000..43cf3b1 --- /dev/null +++ b/cmd/mothd/state.go @@ -0,0 +1,356 @@ +package main + +import ( + "fmt" + "strconv" + "log" + "path/filepath" + "strings" + "os" + "io/ioutil" + "bufio" + "time" + "math/rand" +) + +// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift +const distinguishableChars = "234678abcdefhijkmnpqrtwxyz=" + +func mktoken() string { + a := make([]byte, 8) + for i := range a { + char := rand.Intn(len(distinguishableChars)) + a[i] = distinguishableChars[char] + } + return string(a) +} + +type StateExport struct { + TeamNames map[string]string + PointsLog []Award + Messages []string +} + +// We use the filesystem for synchronization between threads. +// The only thing State methods need to know is the path to the state directory. +type State struct { + StateDir string + update chan bool +} + +func NewState(stateDir string) (*State) { + return &State{ + StateDir: stateDir, + update: make(chan bool, 10), + } +} + +// Returns a cleaned up join of path parts relative to +func (s *State) path(parts ...string) string { + rel := filepath.Clean(filepath.Join(parts...)) + parts = filepath.SplitList(rel) + for i, part := range parts { + part = strings.TrimLeft(part, "./\\:") + parts[i] = part + } + rel = filepath.Join(parts...) + return filepath.Join(s.StateDir, rel) +} + +// Check a few things to see if this state directory is "enabled". +func (s *State) Enabled() bool { + if _, err := os.Stat(s.path("enabled")); os.IsNotExist(err) { + log.Print("Suspended: enabled file missing") + return false + } + + untilspec, err := ioutil.ReadFile(s.path("until")) + if err == nil { + untilspecs := strings.TrimSpace(string(untilspec)) + until, err := time.Parse(time.RFC3339, untilspecs) + if err != nil { + log.Printf("Suspended: Unparseable until date: %s", untilspec) + return false + } + if until.Before(time.Now()) { + log.Print("Suspended: until time reached, suspending maintenance") + return false + } + } + + return true +} + +// Returns team name given a team ID. +func (s *State) TeamName(teamId string) (string, error) { + teamFile := s.path("teams", teamId) + teamNameBytes, err := ioutil.ReadFile(teamFile) + teamName := strings.TrimSpace(string(teamNameBytes)) + + if os.IsNotExist(err) { + return "", fmt.Errorf("Unregistered team ID: %s", teamId) + } else if err != nil { + return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamId, err) + } + + return teamName, nil +} + +// Write out team name. This can only be done once. +func (s *State) SetTeamName(teamId string, teamName string) error { + teamFile := s.path("teams", teamId) + err := ioutil.WriteFile(teamFile, []byte(teamName), os.ModeExclusive | 0644) + return err +} + +// Retrieve the current points log +func (s *State) PointsLog() ([]*Award) { + pointsFile := s.path("points.log") + f, err := os.Open(pointsFile) + if err != nil { + log.Println(err) + return nil + } + defer f.Close() + + pointsLog := make([]*Award, 0, 200) + 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 + } + pointsLog = append(pointsLog, cur) + } + return pointsLog +} + +// Return an exportable points log, +// This anonymizes teamId with either an integer, or the string "self" +// for the requesting teamId. +func (s *State) Export(teamId string) (*StateExport) { + teamName, _ := s.TeamName(teamId) + + pointsLog := s.PointsLog() + + export := StateExport{ + PointsLog: make([]Award, len(pointsLog)), + Messages: make([]string, 0, 10), + TeamNames: map[string]string{"self": teamName}, + } + + // Read in messages + messagesFile := s.path("messages.txt") + if f, err := os.Open(messagesFile); err != nil { + log.Print(err) + } else { + defer f.Close() + scanner := bufio.NewScanner(f) + for scanner.Scan() { + message := scanner.Text() + if strings.HasPrefix(message, "#") { + continue + } + export.Messages = append(export.Messages, message) + } + } + + // Read in points + exportIds := map[string]string{teamId: "self"} + for logno, award := range pointsLog { + exportAward := award + if id, ok := exportIds[award.TeamId]; ok { + exportAward.TeamId = id + } else { + exportId := strconv.Itoa(logno) + exportAward.TeamId = exportId + exportIds[award.TeamId] = exportAward.TeamId + + name, err := s.TeamName(award.TeamId) + if err != nil { + name = "Rodney" // https://en.wikipedia.org/wiki/Rogue_(video_game)#Gameplay + } + export.TeamNames[exportId] = name + } + export.PointsLog[logno] = *exportAward + } + + return &export +} + +// AwardPoints gives points to teamId in category. +// It first checks to make sure these are not duplicate points. +// This is not a perfect check, you can trigger a race condition here. +// It's just a courtesy to the user. +// The update task makes sure we never have duplicate points in the log. +func (s *State) AwardPoints(teamId, category string, points int) error { + a := Award{ + When: time.Now().Unix(), + TeamId: teamId, + Category: category, + Points: points, + } + + _, err := s.TeamName(teamId) + if err != nil { + return err + } + + for _, e := range s.PointsLog() { + if a.Same(e) { + return fmt.Errorf("Points already awarded to this team in this category") + } + } + + fn := fmt.Sprintf("%s-%s-%d", teamId, category, points) + tmpfn := s.path("points.tmp", fn) + newfn := s.path("points.new", fn) + + if err := ioutil.WriteFile(tmpfn, []byte(a.String()), 0644); err != nil { + return err + } + + if err := os.Rename(tmpfn, newfn); err != nil { + return err + } + + s.update <- true + 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 (s *State) collectPoints() { + files, err := ioutil.ReadDir(s.path("points.new")) + if err != nil { + log.Print(err) + return + } + for _, f := range files { + filename := s.path("points.new", f.Name()) + awardstr, err := ioutil.ReadFile(filename) + if err != nil { + log.Print("Opening new points: ", err) + continue + } + award, err := ParseAward(string(awardstr)) + if err != nil { + log.Print("Can't parse award file ", filename, ": ", err) + continue + } + + duplicate := false + for _, e := range s.PointsLog() { + if award.Same(e) { + duplicate = true + break + } + } + + if duplicate { + log.Print("Skipping duplicate points: ", award.String()) + } else { + log.Print("Award: ", award.String()) + + logf, err := os.OpenFile(s.path("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Print("Can't append to points log: ", err) + return + } + fmt.Fprintln(logf, award.String()) + logf.Close() + } + + if err := os.Remove(filename); err != nil { + log.Print("Unable to remove new points file: ", err) + } + } +} + + +func (s *State) maybeInitialize() { + // Are we supposed to re-initialize? + if _, err := os.Stat(s.path("initialized")); ! os.IsNotExist(err) { + return + } + + log.Print("initialized file missing, re-initializing") + + // Remove any extant control and state files + os.Remove(s.path("enabled")) + os.Remove(s.path("until")) + os.Remove(s.path("points.log")) + os.Remove(s.path("messages.txt")) + os.RemoveAll(s.path("points.tmp")) + os.RemoveAll(s.path("points.new")) + os.RemoveAll(s.path("teams")) + + // Make sure various subdirectories exist + os.Mkdir(s.path("points.tmp"), 0755) + os.Mkdir(s.path("points.new"), 0755) + os.Mkdir(s.path("teams"), 0755) + + // Preseed available team ids if file doesn't exist + if f, err := os.OpenFile(s.path("teamids.txt"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { + defer f.Close() + for i := 0; i <= 100; i += 1 { + fmt.Fprintln(f, mktoken()) + } + } + + // Create some files + ioutil.WriteFile( + s.path("initialized"), + []byte("Remove this file to re-initialized the contest\n"), + 0644, + ) + ioutil.WriteFile( + s.path("enabled"), + []byte("Remove this file to suspend the contest\n"), + 0644, + ) + ioutil.WriteFile( + s.path("until"), + []byte("3009-10-31T00:00:00Z\n"), + 0644, + ) + ioutil.WriteFile( + s.path("messages.txt"), + []byte(fmt.Sprintf("[%s] Initialized.\n", time.Now())), + 0644, + ) + ioutil.WriteFile( + s.path("points.log"), + []byte(""), + 0644, + ) +} + +func (s *State) Run(updateInterval time.Duration) { + for { + s.maybeInitialize() + if s.Enabled() { + s.collectPoints() + } + + select { + case <-s.update: + case <-time.After(updateInterval): + } + } +} + + +func main() { + s := NewState("./state") + go s.Run(2 * time.Second) + for { + select { + case <-time.After(5 * time.Second): + } + + fmt.Println(s.Export("")) + } +} diff --git a/cmd/mothd/handlers.go b/cmd/mothdv3/handlers.go similarity index 100% rename from cmd/mothd/handlers.go rename to cmd/mothdv3/handlers.go diff --git a/cmd/mothd/instance.go b/cmd/mothdv3/instance.go similarity index 100% rename from cmd/mothd/instance.go rename to cmd/mothdv3/instance.go diff --git a/cmd/mothd/instance_test.go b/cmd/mothdv3/instance_test.go similarity index 100% rename from cmd/mothd/instance_test.go rename to cmd/mothdv3/instance_test.go diff --git a/cmd/mothd/maintenance.go b/cmd/mothdv3/maintenance.go similarity index 100% rename from cmd/mothd/maintenance.go rename to cmd/mothdv3/maintenance.go diff --git a/cmd/mothd/mothd.go b/cmd/mothdv3/mothd.go similarity index 100% rename from cmd/mothd/mothd.go rename to cmd/mothdv3/mothd.go diff --git a/cmd/mothd/zipfs.go b/cmd/mothdv3/zipfs.go similarity index 100% rename from cmd/mothd/zipfs.go rename to cmd/mothdv3/zipfs.go diff --git a/cmd/mothd/zipfs_test.go b/cmd/mothdv3/zipfs_test.go similarity index 100% rename from cmd/mothd/zipfs_test.go rename to cmd/mothdv3/zipfs_test.go diff --git a/docs/CREDITS.md b/doc/CREDITS.md similarity index 100% rename from docs/CREDITS.md rename to doc/CREDITS.md diff --git a/docs/devel-server.md b/doc/devel-server.md similarity index 100% rename from docs/devel-server.md rename to doc/devel-server.md diff --git a/docs/dirtbags.svg b/doc/dirtbags.svg similarity index 100% rename from docs/dirtbags.svg rename to doc/dirtbags.svg diff --git a/docs/overview.md b/doc/overview.md similarity index 100% rename from docs/overview.md rename to doc/overview.md diff --git a/docs/philosophy.md b/doc/philosophy.md similarity index 100% rename from docs/philosophy.md rename to doc/philosophy.md diff --git a/docs/tokens.md b/doc/tokens.md similarity index 100% rename from docs/tokens.md rename to doc/tokens.md diff --git a/docs/writing-puzzles.md b/doc/writing-puzzles.md similarity index 100% rename from docs/writing-puzzles.md rename to doc/writing-puzzles.md From 54ea337447d743f40e2a11fd2887175f08dbe8c3 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 2 Sep 2019 18:42:27 -0600 Subject: [PATCH 17/96] Add common routines and theme dojigger --- cmd/mothd/common.go | 19 ++++++++++++++++++ cmd/mothd/theme.go | 47 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 cmd/mothd/common.go create mode 100644 cmd/mothd/theme.go diff --git a/cmd/mothd/common.go b/cmd/mothd/common.go new file mode 100644 index 0000000..ca04486 --- /dev/null +++ b/cmd/mothd/common.go @@ -0,0 +1,19 @@ +package main + +import ( + "path/filepath" + "strings" +) + +func MothPath(base string, parts ...string) string { + path := filepath.Clean(filepath.Join(parts...)) + parts = filepath.SplitList(path) + for i, part := range parts { + part = strings.TrimLeft(part, "./\\:") + parts[i] = part + } + parts = append([]string{base}, parts...) + path = filepath.Join(parts...) + path = filepath.Clean(path) + return path +} diff --git a/cmd/mothd/theme.go b/cmd/mothd/theme.go new file mode 100644 index 0000000..76eeb83 --- /dev/null +++ b/cmd/mothd/theme.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/http" + "os" + "strings" +) + +type Theme struct { + ThemeDir string +} + +func NewTheme(themeDir string) *Theme { + return &Theme{ + ThemeDir: themeDir, + } +} + +func (t *Theme) path(parts ...string) string { + return MothPath(t.ThemeDir, parts...) +} + +func (t *Theme) staticHandler(w http.ResponseWriter, req *http.Request) { + path := req.URL.Path + if strings.Contains(path, "/.") { + http.Error(w, "Invalid URL path", http.StatusBadRequest) + return + } + if path == "/" { + path = "/index.html" + } + + f, err := os.Open(t.path(path)) + if err != nil { + http.NotFound(w, req) + return + } + defer f.Close() + + d, err := f.Stat() + if err != nil { + http.NotFound(w, req) + return + } + + http.ServeContent(w, req, path, d.ModTime(), f) +} From 309432d05cfa066776cce4ec67c2361a99cbaff1 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 2 Sep 2019 19:47:24 -0600 Subject: [PATCH 18/96] Mostly refactored --- README.md | 6 +- cmd/mothd/common.go | 13 ++- cmd/mothd/main.go | 24 ++++++ cmd/mothd/mothballs.go | 61 ++++++++++++++ cmd/mothd/state.go | 83 +++++++------------ cmd/mothd/theme.go | 14 ++-- cmd/{mothdv3 => mothd}/zipfs.go | 0 cmd/{mothdv3 => mothd}/zipfs_test.go | 0 cmd/mothdav/main.go | 82 ------------------ cmd/transpile/main.go | 13 ++- .../mothball.go} | 1 + 11 files changed, 144 insertions(+), 153 deletions(-) create mode 100644 cmd/mothd/main.go create mode 100644 cmd/mothd/mothballs.go rename cmd/{mothdv3 => mothd}/zipfs.go (100%) rename cmd/{mothdv3 => mothd}/zipfs_test.go (100%) delete mode 100644 cmd/mothdav/main.go rename cmd/{mothdv3/instance_test.go => transpile/mothball.go} (92%) diff --git a/README.md b/README.md index 9d9d9c9..6b97eb6 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ And point a browser to http://localhost:8080/ (or whatever host is running the s The development server includes a number of Python libraries that we have found useful in writing puzzles. When you're ready to create your own puzzles, -read [the devel server documentation](docs/devel-server.md). +read [the devel server documentation](doc/devel-server.md). Click the `[mb]` link by a puzzle category to compile and download a mothball that the production server can read. @@ -49,7 +49,7 @@ Running a Production Server You can be more fine-grained about directories, if you like. Inside the container, you need the following paths: -* `/state` (rw) Where state is stored. Read [the overview](docs/overview.md) to learn what's what in here. +* `/state` (rw) Where state is stored. Read [the overview](doc/overview.md) to learn what's what in here. * `/mothballs` (ro) Mothballs (puzzle bundles) as provided by the development server. * `/resources` (ro) Overrides for built-in HTML/CSS resources. @@ -73,7 +73,7 @@ Point a web browser at http://localhost:8080/ and start hacking on things in your `puzzles` directory. More on how the devel sever works in -[the devel server documentation](docs/devel-server.md) +[the devel server documentation](doc/devel-server.md) Running A Production Server diff --git a/cmd/mothd/common.go b/cmd/mothd/common.go index ca04486..9da809b 100644 --- a/cmd/mothd/common.go +++ b/cmd/mothd/common.go @@ -3,17 +3,26 @@ package main import ( "path/filepath" "strings" + "time" ) -func MothPath(base string, parts ...string) string { +type Component struct { + baseDir string +} + +func (c *Component) path(parts ...string) string { path := filepath.Clean(filepath.Join(parts...)) parts = filepath.SplitList(path) for i, part := range parts { part = strings.TrimLeft(part, "./\\:") parts[i] = part } - parts = append([]string{base}, parts...) + parts = append([]string{c.baseDir}, parts...) path = filepath.Join(parts...) path = filepath.Clean(path) return path } + +func (c *Component) Run(updateInterval time.Duration) { + // Stub! +} \ No newline at end of file diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go new file mode 100644 index 0000000..5aa11d3 --- /dev/null +++ b/cmd/mothd/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "time" + "log" +) + +func main() { + log.Print("Started") + + theme := NewTheme("../../theme") + state := NewState("../../state") + puzzles := NewMothballs("../../mothballs") + + + interval := 2 * time.Second + go theme.Run(interval) + go state.Run(interval) + go puzzles.Run(interval) + + time.Sleep(1 * time.Second) + log.Print(state.Export("")) + time.Sleep(19 * time.Second) +} \ No newline at end of file diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go new file mode 100644 index 0000000..08ee41c --- /dev/null +++ b/cmd/mothd/mothballs.go @@ -0,0 +1,61 @@ +package main + +import ( + "time" + "io/ioutil" + "strings" + "log" +) + +type Mothballs struct { + Component + categories map[string]*Zipfs +} + +func NewMothballs(baseDir string) *Mothballs { + return &Mothballs{ + Component: Component{ + baseDir: baseDir, + }, + categories: make(map[string]*Zipfs), + } +} + +func (m *Mothballs) update() { + // Any new categories? + files, err := ioutil.ReadDir(m.path()) + if err != nil { + log.Print("Error listing mothballs: ", err) + return + } + for _, f := range files { + filename := f.Name() + filepath := m.path(filename) + if !strings.HasSuffix(filename, ".mb") { + continue + } + categoryName := strings.TrimSuffix(filename, ".mb") + + if _, ok := m.categories[categoryName]; !ok { + zfs, err := OpenZipfs(filepath) + if err != nil { + log.Print("Error opening ", filepath, ": ", err) + continue + } + log.Print("New mothball: ", filename) + m.categories[categoryName] = zfs + } + } +} + +func (m *Mothballs) Run(updateInterval time.Duration) { + ticker := time.NewTicker(updateInterval) + m.update() + for { + select { + case when := <-ticker.C: + log.Print("Tick: ", when) + m.update() + } + } +} diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 43cf3b1..ff3bb62 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -1,16 +1,15 @@ package main import ( - "fmt" - "strconv" - "log" - "path/filepath" - "strings" - "os" - "io/ioutil" "bufio" - "time" + "fmt" + "io/ioutil" + "log" "math/rand" + "os" + "strconv" + "strings" + "time" ) // Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift @@ -34,29 +33,19 @@ type StateExport struct { // We use the filesystem for synchronization between threads. // The only thing State methods need to know is the path to the state directory. type State struct { - StateDir string - update chan bool + Component + update chan bool } -func NewState(stateDir string) (*State) { +func NewState(baseDir string) *State { return &State{ - StateDir: stateDir, - update: make(chan bool, 10), + Component: Component{ + baseDir: baseDir, + }, + update: make(chan bool, 10), } } -// Returns a cleaned up join of path parts relative to -func (s *State) path(parts ...string) string { - rel := filepath.Clean(filepath.Join(parts...)) - parts = filepath.SplitList(rel) - for i, part := range parts { - part = strings.TrimLeft(part, "./\\:") - parts[i] = part - } - rel = filepath.Join(parts...) - return filepath.Join(s.StateDir, rel) -} - // Check a few things to see if this state directory is "enabled". func (s *State) Enabled() bool { if _, err := os.Stat(s.path("enabled")); os.IsNotExist(err) { @@ -92,19 +81,19 @@ func (s *State) TeamName(teamId string) (string, error) { } else if err != nil { return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamId, err) } - + return teamName, nil } // Write out team name. This can only be done once. func (s *State) SetTeamName(teamId string, teamName string) error { teamFile := s.path("teams", teamId) - err := ioutil.WriteFile(teamFile, []byte(teamName), os.ModeExclusive | 0644) + err := ioutil.WriteFile(teamFile, []byte(teamName), os.ModeExclusive|0644) return err } // Retrieve the current points log -func (s *State) PointsLog() ([]*Award) { +func (s *State) PointsLog() []*Award { pointsFile := s.path("points.log") f, err := os.Open(pointsFile) if err != nil { @@ -112,7 +101,7 @@ func (s *State) PointsLog() ([]*Award) { return nil } defer f.Close() - + pointsLog := make([]*Award, 0, 200) scanner := bufio.NewScanner(f) for scanner.Scan() { @@ -130,17 +119,17 @@ func (s *State) PointsLog() ([]*Award) { // Return an exportable points log, // This anonymizes teamId with either an integer, or the string "self" // for the requesting teamId. -func (s *State) Export(teamId string) (*StateExport) { +func (s *State) Export(teamId string) *StateExport { teamName, _ := s.TeamName(teamId) pointsLog := s.PointsLog() export := StateExport{ PointsLog: make([]Award, len(pointsLog)), - Messages: make([]string, 0, 10), + Messages: make([]string, 0, 10), TeamNames: map[string]string{"self": teamName}, } - + // Read in messages messagesFile := s.path("messages.txt") if f, err := os.Open(messagesFile); err != nil { @@ -156,7 +145,7 @@ func (s *State) Export(teamId string) (*StateExport) { export.Messages = append(export.Messages, message) } } - + // Read in points exportIds := map[string]string{teamId: "self"} for logno, award := range pointsLog { @@ -167,7 +156,7 @@ func (s *State) Export(teamId string) (*StateExport) { exportId := strconv.Itoa(logno) exportAward.TeamId = exportId exportIds[award.TeamId] = exportAward.TeamId - + name, err := s.TeamName(award.TeamId) if err != nil { name = "Rodney" // https://en.wikipedia.org/wiki/Rogue_(video_game)#Gameplay @@ -176,7 +165,7 @@ func (s *State) Export(teamId string) (*StateExport) { } export.PointsLog[logno] = *exportAward } - + return &export } @@ -269,10 +258,9 @@ func (s *State) collectPoints() { } } - func (s *State) maybeInitialize() { // Are we supposed to re-initialize? - if _, err := os.Stat(s.path("initialized")); ! os.IsNotExist(err) { + if _, err := os.Stat(s.path("initialized")); !os.IsNotExist(err) { return } @@ -318,7 +306,7 @@ func (s *State) maybeInitialize() { ) ioutil.WriteFile( s.path("messages.txt"), - []byte(fmt.Sprintf("[%s] Initialized.\n", time.Now())), + []byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))), 0644, ) ioutil.WriteFile( @@ -334,23 +322,10 @@ func (s *State) Run(updateInterval time.Duration) { if s.Enabled() { s.collectPoints() } - + select { - case <-s.update: - case <-time.After(updateInterval): + case <-s.update: + case <-time.After(updateInterval): } } } - - -func main() { - s := NewState("./state") - go s.Run(2 * time.Second) - for { - select { - case <-time.After(5 * time.Second): - } - - fmt.Println(s.Export("")) - } -} diff --git a/cmd/mothd/theme.go b/cmd/mothd/theme.go index 76eeb83..895f294 100644 --- a/cmd/mothd/theme.go +++ b/cmd/mothd/theme.go @@ -7,23 +7,21 @@ import ( ) type Theme struct { - ThemeDir string + Component } -func NewTheme(themeDir string) *Theme { +func NewTheme(baseDir string) *Theme { return &Theme{ - ThemeDir: themeDir, + Component: Component{ + baseDir: baseDir, + }, } } -func (t *Theme) path(parts ...string) string { - return MothPath(t.ThemeDir, parts...) -} - func (t *Theme) staticHandler(w http.ResponseWriter, req *http.Request) { path := req.URL.Path if strings.Contains(path, "/.") { - http.Error(w, "Invalid URL path", http.StatusBadRequest) + http.Error(w, "Invalid path", http.StatusBadRequest) return } if path == "/" { diff --git a/cmd/mothdv3/zipfs.go b/cmd/mothd/zipfs.go similarity index 100% rename from cmd/mothdv3/zipfs.go rename to cmd/mothd/zipfs.go diff --git a/cmd/mothdv3/zipfs_test.go b/cmd/mothd/zipfs_test.go similarity index 100% rename from cmd/mothdv3/zipfs_test.go rename to cmd/mothd/zipfs_test.go diff --git a/cmd/mothdav/main.go b/cmd/mothdav/main.go deleted file mode 100644 index d8bb201..0000000 --- a/cmd/mothdav/main.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "net/http" - "os" - "time" - "context" - - "golang.org/x/net/webdav" -) - -type StubLockSystem struct { -} - -func (ls *StubLockSystem) Confirm(now time.Time, name0, name1 string, conditions ...webdav.Condition) (release func(), err error) { - return nil, webdav.ErrConfirmationFailed -} - -func (ls *StubLockSystem) Create(now time.Time, details webdav.LockDetails) (token string, err error) { - return "", webdav.ErrLocked -} - -func (ls *StubLockSystem) Refresh(now time.Time, token string, duration time.Duration) (webdav.LockDetails, error) { - return webdav.LockDetails{}, webdav.ErrNoSuchLock -} - -func (ls *StubLockSystem) Unlock(now time.Time, token string) error { - return webdav.ErrNoSuchLock -} - - -type MothFS struct { -} - -func (fs *MothFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error { - return os.ErrPermission -} - -func (fs *MothFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { - f, err := os.Open("hello.txt") - return f, err -} - -func (fs *MothFS) RemoveAll(ctx context.Context, name string) error { - return os.ErrPermission -} - -func (fs *MothFS) Rename(ctx context.Context, oldName, newName string) error { - return os.ErrPermission -} - -func (fs *MothFS) Stat(ctx context.Context, name string) (os.FileInfo, error) { - info, err := os.Stat("hello.txt") - return info, err -} - -func main() { - //dirFlag := flag.String("d", "./", "Directory to serve from. Default is CWD") - httpPort := flag.Int("p", 80, "Port to serve on (Plain HTTP)") - - flag.Parse() - - srv := &webdav.Handler{ - FileSystem: new(MothFS), - LockSystem: new(StubLockSystem), - Logger: func(r *http.Request, err error) { - if err != nil { - log.Printf("WEBDAV [%s]: %s, ERROR: %s\n", r.Method, r.URL, err) - } else { - log.Printf("WEBDAV [%s]: %s \n", r.Method, r.URL) - } - }, - } - http.Handle("/", srv) - if err := http.ListenAndServe(fmt.Sprintf(":%d", *httpPort), nil); err != nil { - log.Fatalf("Error with WebDAV server: %v", err) - } - -} diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go index e589c71..44d05fa 100644 --- a/cmd/transpile/main.go +++ b/cmd/transpile/main.go @@ -18,18 +18,23 @@ func seedJoin(parts ...string) string { func usage() { out := flag.CommandLine.Output() name := flag.CommandLine.Name() - fmt.Fprintf(out, "Usage: %s [OPTIONS] CATEGORY [CATEGORY ...]\n", name) + fmt.Fprintf(out, "Usage: %s [OPTION]... CATEGORY [PUZZLE [FILENAME]]\n", name) + fmt.Fprintf(out, "\n") + fmt.Fprintf(out, "Transpile CATEGORY, or provide individual category components.\n") + fmt.Fprintf(out, "If PUZZLE is provided, only transpile the given puzzle.\n") + fmt.Fprintf(out, "If FILENAME is provided, output provided file.\n") flag.PrintDefaults() } func main() { - // XXX: We need a way to pass in "only run this one point value puzzle" // XXX: Convert puzzle.py to standalone thingies flag.Usage = usage - points := flag.Int("points", 0, "Transpile only this point value puzzle") - flag.Parse() + points := flag.Int("points", 0, "Transpile only this point value puzzle") + mothball := flag.Bool("mothball", false, "Generate a mothball") + flag.Parse() + baseSeedString := os.Getenv("MOTH_SEED") jsenc := json.NewEncoder(os.Stdout) diff --git a/cmd/mothdv3/instance_test.go b/cmd/transpile/mothball.go similarity index 92% rename from cmd/mothdv3/instance_test.go rename to cmd/transpile/mothball.go index 06ab7d0..c9ecbf5 100644 --- a/cmd/mothdv3/instance_test.go +++ b/cmd/transpile/mothball.go @@ -1 +1,2 @@ package main + From 65f810539a79fb903647b5e923b751d8db5339a4 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 24 Nov 2019 15:58:43 -0700 Subject: [PATCH 19/96] Add flags and more robust functionality than "until" provided --- cmd/mothd/common.go | 2 +- cmd/mothd/main.go | 52 +++++++++++++++++++++++-------- cmd/mothd/mothballs.go | 4 +-- cmd/mothd/state.go | 71 ++++++++++++++++++++++++++++++++---------- 4 files changed, 96 insertions(+), 33 deletions(-) diff --git a/cmd/mothd/common.go b/cmd/mothd/common.go index 9da809b..2217258 100644 --- a/cmd/mothd/common.go +++ b/cmd/mothd/common.go @@ -25,4 +25,4 @@ func (c *Component) path(parts ...string) string { func (c *Component) Run(updateInterval time.Duration) { // Stub! -} \ No newline at end of file +} diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go index 5aa11d3..04394bf 100644 --- a/cmd/mothd/main.go +++ b/cmd/mothd/main.go @@ -1,24 +1,50 @@ package main import ( - "time" + "github.com/namsral/flag" "log" + "time" ) func main() { log.Print("Started") - - theme := NewTheme("../../theme") - state := NewState("../../state") - puzzles := NewMothballs("../../mothballs") - - - interval := 2 * time.Second - go theme.Run(interval) - go state.Run(interval) - go puzzles.Run(interval) - + + themePath := flag.String( + "theme", + "theme", + "Path to theme files", + ) + statePath := flag.String( + "state", + "state", + "Path to state files", + ) + puzzlePath := flag.String( + "mothballs", + "mothballs", + "Path to mothballs to host", + ) + refreshInterval := flag.Duration( + "refresh", + 2*time.Second, + "Duration between maintenance tasks", + ) + bindStr := flag.String( + "bind", + ":8000", + "Bind [host]:port for HTTP service", + ) + + theme := NewTheme(*themePath) + state := NewState(*statePath) + puzzles := NewMothballs(*puzzlePath) + + go theme.Run(*refreshInterval) + go state.Run(*refreshInterval) + go puzzles.Run(*refreshInterval) + + log.Println("I would be binding to", *bindStr) time.Sleep(1 * time.Second) log.Print(state.Export("")) time.Sleep(19 * time.Second) -} \ No newline at end of file +} diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index 08ee41c..1ceaac6 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -1,10 +1,10 @@ package main import ( - "time" "io/ioutil" - "strings" "log" + "strings" + "time" ) type Mothballs struct { diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index ff3bb62..95580a8 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -34,6 +34,7 @@ type StateExport struct { // The only thing State methods need to know is the path to the state directory. type State struct { Component + Enabled bool update chan bool } @@ -42,32 +43,60 @@ func NewState(baseDir string) *State { Component: Component{ baseDir: baseDir, }, + Enabled: true, update: make(chan bool, 10), } } // Check a few things to see if this state directory is "enabled". -func (s *State) Enabled() bool { +func (s *State) UpdateEnabled() { if _, err := os.Stat(s.path("enabled")); os.IsNotExist(err) { + s.Enabled = false log.Print("Suspended: enabled file missing") - return false + return } - untilspec, err := ioutil.ReadFile(s.path("until")) - if err == nil { - untilspecs := strings.TrimSpace(string(untilspec)) - until, err := time.Parse(time.RFC3339, untilspecs) + nextEnabled := true + untilFile, err := os.Open(s.path("hours")) + if err != nil { + return + } + defer untilFile.Close() + + scanner := bufio.NewScanner(untilFile) + for scanner.Scan() { + line := scanner.Text() + if len(line) < 1 { + continue + } + + thisEnabled := true + switch line[0] { + case '+': + thisEnabled = true + line = line[1:] + case '-': + thisEnabled = false + line = line[1:] + case '#': + continue + default: + log.Printf("Misformatted line in hours file") + } + line = strings.TrimSpace(line) + until, err := time.Parse(time.RFC3339, line) if err != nil { - log.Printf("Suspended: Unparseable until date: %s", untilspec) - return false + log.Printf("Suspended: Unparseable until date: %s", line) + continue } if until.Before(time.Now()) { - log.Print("Suspended: until time reached, suspending maintenance") - return false + nextEnabled = thisEnabled } } - - return true + if nextEnabled != s.Enabled { + s.Enabled = nextEnabled + log.Println("Setting enabled to", s.Enabled, "based on hours file") + } } // Returns team name given a team ID. @@ -291,17 +320,23 @@ func (s *State) maybeInitialize() { // Create some files ioutil.WriteFile( s.path("initialized"), - []byte("Remove this file to re-initialized the contest\n"), + []byte("state/initialized: remove to re-initialize the contest\n"), 0644, ) ioutil.WriteFile( s.path("enabled"), - []byte("Remove this file to suspend the contest\n"), + []byte("state/enabled: remove to suspend the contest\n"), 0644, ) ioutil.WriteFile( - s.path("until"), - []byte("3009-10-31T00:00:00Z\n"), + s.path("hours"), + []byte( + "# state/hours: when the contest is enabled\n"+ + "# Lines starting with + enable, with - disable.\n"+ + "\n"+ + "+ 1970-01-01T00:00:00Z\n"+ + "- 3019-10-31T00:00:00Z\n", + ), 0644, ) ioutil.WriteFile( @@ -319,10 +354,12 @@ func (s *State) maybeInitialize() { func (s *State) Run(updateInterval time.Duration) { for { s.maybeInitialize() - if s.Enabled() { + s.UpdateEnabled() + if s.Enabled { s.collectPoints() } + // Wait for something to happen select { case <-s.update: case <-time.After(updateInterval): From eb0a7fb357dd4cfaa6db2e88e96b820a2a181fe3 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 26 Nov 2019 19:09:40 +0000 Subject: [PATCH 20/96] Add a Changelog --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ea2248e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog +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). + +## [Unreleased] +### Added +- New `transpile` command to replace some functionality of devel server + +### Changed +- Major rewrite/refactor of `mothd` +- `state/until` is now `state/hours` and can specify multiple begin/end hours +- `state/disabled` is now `state/enabled` +- Mothball structure has changed substantially + +### Deprecated + +### Removed +- Development server is gone now; use `mothd` directly with a flag to transpile on the fly + +### Fixed + +### Security + From 7825f196be5a4377f6b891dad2dcf0f4013f8fe7 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 1 Dec 2019 18:58:09 -0700 Subject: [PATCH 21/96] Start doing more tests --- cmd/mothd/main.go | 7 ++- cmd/mothd/state.go | 112 +++++++++++++++++++++------------------- cmd/mothd/state_test.go | 25 +++++++++ 3 files changed, 88 insertions(+), 56 deletions(-) create mode 100644 cmd/mothd/state_test.go diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go index 04394bf..9461906 100644 --- a/cmd/mothd/main.go +++ b/cmd/mothd/main.go @@ -2,6 +2,7 @@ package main import ( "github.com/namsral/flag" + "github.com/spf13/afero" "log" "time" ) @@ -35,15 +36,17 @@ func main() { "Bind [host]:port for HTTP service", ) + stateFs := afero.NewBasePathFs(afero.NewOsFs(), *statePath) + theme := NewTheme(*themePath) - state := NewState(*statePath) + state := NewState(stateFs) puzzles := NewMothballs(*puzzlePath) go theme.Run(*refreshInterval) go state.Run(*refreshInterval) go puzzles.Run(*refreshInterval) - log.Println("I would be binding to", *bindStr) + log.Println("I would be binding to", *bindStr) time.Sleep(1 * time.Second) log.Print(state.Export("")) time.Sleep(19 * time.Second) diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 95580a8..852028c 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -3,10 +3,11 @@ package main import ( "bufio" "fmt" - "io/ioutil" + "github.com/spf13/afero" "log" "math/rand" "os" + "path/filepath" "strconv" "strings" "time" @@ -33,31 +34,29 @@ type StateExport struct { // We use the filesystem for synchronization between threads. // The only thing State methods need to know is the path to the state directory. type State struct { - Component Enabled bool update chan bool + fs afero.Fs } -func NewState(baseDir string) *State { +func NewState(fs afero.Fs) *State { return &State{ - Component: Component{ - baseDir: baseDir, - }, Enabled: true, update: make(chan bool, 10), + fs: fs, } } // Check a few things to see if this state directory is "enabled". func (s *State) UpdateEnabled() { - if _, err := os.Stat(s.path("enabled")); os.IsNotExist(err) { + if _, err := s.fs.Stat("enabled"); os.IsNotExist(err) { s.Enabled = false log.Print("Suspended: enabled file missing") return } nextEnabled := true - untilFile, err := os.Open(s.path("hours")) + untilFile, err := s.fs.Open("hours") if err != nil { return } @@ -101,8 +100,8 @@ func (s *State) UpdateEnabled() { // Returns team name given a team ID. func (s *State) TeamName(teamId string) (string, error) { - teamFile := s.path("teams", teamId) - teamNameBytes, err := ioutil.ReadFile(teamFile) + teamFile := filepath.Join("teams", teamId) + teamNameBytes, err := afero.ReadFile(s.fs, teamFile) teamName := strings.TrimSpace(string(teamNameBytes)) if os.IsNotExist(err) { @@ -116,15 +115,14 @@ func (s *State) TeamName(teamId string) (string, error) { // Write out team name. This can only be done once. func (s *State) SetTeamName(teamId string, teamName string) error { - teamFile := s.path("teams", teamId) - err := ioutil.WriteFile(teamFile, []byte(teamName), os.ModeExclusive|0644) + teamFile := filepath.Join("teams", teamId) + err := afero.WriteFile(s.fs, teamFile, []byte(teamName), os.ModeExclusive|0644) return err } // Retrieve the current points log func (s *State) PointsLog() []*Award { - pointsFile := s.path("points.log") - f, err := os.Open(pointsFile) + f, err := s.fs.Open("points.log") if err != nil { log.Println(err) return nil @@ -160,8 +158,7 @@ func (s *State) Export(teamId string) *StateExport { } // Read in messages - messagesFile := s.path("messages.txt") - if f, err := os.Open(messagesFile); err != nil { + if f, err := s.fs.Open("messages.txt"); err != nil { log.Print(err) } else { defer f.Close() @@ -223,14 +220,14 @@ func (s *State) AwardPoints(teamId, category string, points int) error { } fn := fmt.Sprintf("%s-%s-%d", teamId, category, points) - tmpfn := s.path("points.tmp", fn) - newfn := s.path("points.new", fn) + tmpfn := filepath.Join("points.tmp", fn) + newfn := filepath.Join("points.new", fn) - if err := ioutil.WriteFile(tmpfn, []byte(a.String()), 0644); err != nil { + if err := afero.WriteFile(s.fs, tmpfn, []byte(a.String()), 0644); err != nil { return err } - if err := os.Rename(tmpfn, newfn); err != nil { + if err := s.fs.Rename(tmpfn, newfn); err != nil { return err } @@ -241,14 +238,14 @@ func (s *State) AwardPoints(teamId, category string, points int) error { // collectPoints gathers up files in points.new/ and appends their contents to points.log, // removing each points.new/ file as it goes. func (s *State) collectPoints() { - files, err := ioutil.ReadDir(s.path("points.new")) + files, err := afero.ReadDir(s.fs, "points.new") if err != nil { log.Print(err) return } for _, f := range files { - filename := s.path("points.new", f.Name()) - awardstr, err := ioutil.ReadFile(filename) + filename := filepath.Join("points.new", f.Name()) + awardstr, err := afero.ReadFile(s.fs, filename) if err != nil { log.Print("Opening new points: ", err) continue @@ -272,7 +269,7 @@ func (s *State) collectPoints() { } else { log.Print("Award: ", award.String()) - logf, err := os.OpenFile(s.path("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + logf, err := s.fs.OpenFile("points.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Print("Can't append to points log: ", err) return @@ -281,7 +278,7 @@ func (s *State) collectPoints() { logf.Close() } - if err := os.Remove(filename); err != nil { + if err := s.fs.Remove(filename); err != nil { log.Print("Unable to remove new points file: ", err) } } @@ -289,28 +286,28 @@ func (s *State) collectPoints() { func (s *State) maybeInitialize() { // Are we supposed to re-initialize? - if _, err := os.Stat(s.path("initialized")); !os.IsNotExist(err) { + if _, err := s.fs.Stat("initialized"); !os.IsNotExist(err) { return } log.Print("initialized file missing, re-initializing") // Remove any extant control and state files - os.Remove(s.path("enabled")) - os.Remove(s.path("until")) - os.Remove(s.path("points.log")) - os.Remove(s.path("messages.txt")) - os.RemoveAll(s.path("points.tmp")) - os.RemoveAll(s.path("points.new")) - os.RemoveAll(s.path("teams")) + s.fs.Remove("enabled") + s.fs.Remove("hours") + s.fs.Remove("points.log") + s.fs.Remove("messages.txt") + s.fs.RemoveAll("points.tmp") + s.fs.RemoveAll("points.new") + s.fs.RemoveAll("teams") // Make sure various subdirectories exist - os.Mkdir(s.path("points.tmp"), 0755) - os.Mkdir(s.path("points.new"), 0755) - os.Mkdir(s.path("teams"), 0755) + s.fs.Mkdir("points.tmp", 0755) + s.fs.Mkdir("points.new", 0755) + s.fs.Mkdir("teams", 0755) // Preseed available team ids if file doesn't exist - if f, err := os.OpenFile(s.path("teamids.txt"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { + if f, err := s.fs.OpenFile("teamids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { defer f.Close() for i := 0; i <= 100; i += 1 { fmt.Fprintln(f, mktoken()) @@ -318,18 +315,21 @@ func (s *State) maybeInitialize() { } // Create some files - ioutil.WriteFile( - s.path("initialized"), + afero.WriteFile( + s.fs, + "initialized", []byte("state/initialized: remove to re-initialize the contest\n"), 0644, ) - ioutil.WriteFile( - s.path("enabled"), + afero.WriteFile( + s.fs, + "enabled", []byte("state/enabled: remove to suspend the contest\n"), 0644, ) - ioutil.WriteFile( - s.path("hours"), + afero.WriteFile( + s.fs, + "hours", []byte( "# state/hours: when the contest is enabled\n"+ "# Lines starting with + enable, with - disable.\n"+ @@ -339,27 +339,31 @@ func (s *State) maybeInitialize() { ), 0644, ) - ioutil.WriteFile( - s.path("messages.txt"), + afero.WriteFile( + s.fs, + "messages.txt", []byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))), 0644, ) - ioutil.WriteFile( - s.path("points.log"), + afero.WriteFile( + s.fs, + "points.log", []byte(""), 0644, ) } +func (s *State) Cleanup() { + s.maybeInitialize() + s.UpdateEnabled() + if s.Enabled { + s.collectPoints() + } +} + func (s *State) Run(updateInterval time.Duration) { for { - s.maybeInitialize() - s.UpdateEnabled() - if s.Enabled { - s.collectPoints() - } - - // Wait for something to happen + s.Cleanup() select { case <-s.update: case <-time.After(updateInterval): diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go new file mode 100644 index 0000000..2d8c6f0 --- /dev/null +++ b/cmd/mothd/state_test.go @@ -0,0 +1,25 @@ +package main + +import ( + "github.com/spf13/afero" + "os" + "testing" +) + +func TestState(t *testing.T) { + fs := new(afero.MemMapFs) + + mustExist := func(path string) { + _, err := fs.Stat(path) + if os.IsNotExist(err) { + t.Errorf("File %s does not exist", path) + } + } + + s := NewState(fs) + s.Cleanup() + + mustExist("initialized") + mustExist("enabled") + mustExist("hours") +} From 5dccc6431dec00661113be72ef210d7d9b43629a Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 1 Dec 2019 20:47:46 -0700 Subject: [PATCH 22/96] More tests, and a bug fix! --- cmd/mothd/state.go | 19 ++++++++++++++++++- cmd/mothd/state_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 852028c..551a89d 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -115,6 +115,23 @@ func (s *State) TeamName(teamId string) (string, error) { // Write out team name. This can only be done once. func (s *State) SetTeamName(teamId string, teamName string) error { + if f, err := s.fs.Open("teamids.txt"); err != nil { + return fmt.Errorf("Team IDs file does not exist") + } else { + ok := false + scanner := bufio.NewScanner(f) + for scanner.Scan() { + if scanner.Text() == teamId { + ok = true + break + } + } + f.Close() + if !ok { + return fmt.Errorf("Team ID not found in list of valid Team IDs") + } + } + teamFile := filepath.Join("teams", teamId) err := afero.WriteFile(s.fs, teamFile, []byte(teamName), os.ModeExclusive|0644) return err @@ -309,7 +326,7 @@ func (s *State) maybeInitialize() { // Preseed available team ids if file doesn't exist if f, err := s.fs.OpenFile("teamids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { defer f.Close() - for i := 0; i <= 100; i += 1 { + for i := 0; i < 100; i += 1 { fmt.Fprintln(f, mktoken()) } } diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 2d8c6f0..90defdc 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "github.com/spf13/afero" "os" "testing" @@ -19,7 +20,31 @@ func TestState(t *testing.T) { s := NewState(fs) s.Cleanup() + pl := s.PointsLog() + if len(pl) != 0 { + t.Errorf("Empty points log is not empty") + } + mustExist("initialized") mustExist("enabled") mustExist("hours") + + teamidsBuf, err := afero.ReadFile(fs, "teamids.txt") + if err != nil { + t.Errorf("Reading teamids.txt: %v", err) + } + + teamids := bytes.Split(teamidsBuf, []byte("\n")) + if (len(teamids) != 101) || (len(teamids[100]) > 0) { + t.Errorf("There weren't 100 teamids, there were %d", len(teamids)) + } + myTeam := string(teamids[0]) + + if err := s.SetTeamName("bad team ID", "bad team name"); err == nil { + t.Errorf("Setting bad team ID didn't raise an error") + } + + if err := s.SetTeamName(myTeam, "My Team"); err != nil { + t.Errorf("Setting team name: %v", err) + } } From 97da227777c9bbbf5e93e8e21072642c7ba6463f Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 1 Dec 2019 20:53:13 -0700 Subject: [PATCH 23/96] go fmt --- cmd/mothd/state.go | 26 +++++++++++++------------- cmd/mothd/state_test.go | 26 +++++++++++++------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 551a89d..c855264 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -116,20 +116,20 @@ func (s *State) TeamName(teamId string) (string, error) { // Write out team name. This can only be done once. func (s *State) SetTeamName(teamId string, teamName string) error { if f, err := s.fs.Open("teamids.txt"); err != nil { - return fmt.Errorf("Team IDs file does not exist") + return fmt.Errorf("Team IDs file does not exist") } else { - ok := false - scanner := bufio.NewScanner(f) - for scanner.Scan() { - if scanner.Text() == teamId { - ok = true - break - } - } - f.Close() - if !ok { - return fmt.Errorf("Team ID not found in list of valid Team IDs") - } + found := false + scanner := bufio.NewScanner(f) + for scanner.Scan() { + if scanner.Text() == teamId { + found = true + break + } + } + f.Close() + if !found { + return fmt.Errorf("Team ID not found in list of valid Team IDs") + } } teamFile := filepath.Join("teams", teamId) diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 90defdc..9efd4c1 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -1,7 +1,7 @@ package main import ( - "bytes" + "bytes" "github.com/spf13/afero" "os" "testing" @@ -20,31 +20,31 @@ func TestState(t *testing.T) { s := NewState(fs) s.Cleanup() - pl := s.PointsLog() - if len(pl) != 0 { - t.Errorf("Empty points log is not empty") - } + pl := s.PointsLog() + if len(pl) != 0 { + t.Errorf("Empty points log is not empty") + } mustExist("initialized") mustExist("enabled") mustExist("hours") - + teamidsBuf, err := afero.ReadFile(fs, "teamids.txt") if err != nil { - t.Errorf("Reading teamids.txt: %v", err) + t.Errorf("Reading teamids.txt: %v", err) } - + teamids := bytes.Split(teamidsBuf, []byte("\n")) if (len(teamids) != 101) || (len(teamids[100]) > 0) { - t.Errorf("There weren't 100 teamids, there were %d", len(teamids)) + t.Errorf("There weren't 100 teamids, there were %d", len(teamids)) } myTeam := string(teamids[0]) - + if err := s.SetTeamName("bad team ID", "bad team name"); err == nil { - t.Errorf("Setting bad team ID didn't raise an error") + t.Errorf("Setting bad team ID didn't raise an error") } - + if err := s.SetTeamName(myTeam, "My Team"); err != nil { - t.Errorf("Setting team name: %v", err) + t.Errorf("Setting team name: %v", err) } } From dc352178400e215bee40bce66c3cc43669ca0d54 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 5 Dec 2019 21:50:43 -0700 Subject: [PATCH 24/96] A couple more state tests --- cmd/mothd/state_test.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 9efd4c1..668ab7e 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -38,13 +38,25 @@ func TestState(t *testing.T) { if (len(teamids) != 101) || (len(teamids[100]) > 0) { t.Errorf("There weren't 100 teamids, there were %d", len(teamids)) } - myTeam := string(teamids[0]) + teamId := string(teamids[0]) if err := s.SetTeamName("bad team ID", "bad team name"); err == nil { t.Errorf("Setting bad team ID didn't raise an error") } - if err := s.SetTeamName(myTeam, "My Team"); err != nil { + if err := s.SetTeamName(teamId, "My Team"); err != nil { t.Errorf("Setting team name: %v", err) } + + category := "poot" + points := 3928 + s.AwardPoints(teamId, category, points) + s.Cleanup() + + pl = s.PointsLog() + if len(pl) != 1 { + t.Errorf("After awarding points, points log has length %d", len(pl)) + } else if (pl[0].TeamId != teamId) || (pl[0].Category != category) || (pl[0].Points != points) { + t.Errorf("Incorrect logged award %v", pl) + } } From 3eac94c70d8d3f544aa168d07984d056b682b1b3 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 5 Dec 2019 21:54:32 -0700 Subject: [PATCH 25/96] One more state test --- cmd/mothd/state_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 668ab7e..1849b07 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -59,4 +59,12 @@ func TestState(t *testing.T) { } else if (pl[0].TeamId != teamId) || (pl[0].Category != category) || (pl[0].Points != points) { t.Errorf("Incorrect logged award %v", pl) } + + fs.Remove("initialized") + s.Cleanup() + + pl = s.PointsLog() + if len(pl) != 0 { + t.Errorf("After reinitialization, points log has length %d", len(pl)) + } } From 430e44ce8774f024bb6748ba31f87041ece58ad4 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 5 Dec 2019 22:25:03 -0700 Subject: [PATCH 26/96] Add unit test for themes --- cmd/mothd/main.go | 19 +++++++++++++------ cmd/mothd/theme.go | 12 +++++------- cmd/mothd/theme_test.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 cmd/mothd/theme_test.go diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go index 9461906..7704f65 100644 --- a/cmd/mothd/main.go +++ b/cmd/mothd/main.go @@ -4,6 +4,8 @@ import ( "github.com/namsral/flag" "github.com/spf13/afero" "log" + "mime" + "net/http" "time" ) @@ -37,17 +39,22 @@ func main() { ) stateFs := afero.NewBasePathFs(afero.NewOsFs(), *statePath) + themeFs := afero.NewBasePathFs(afero.NewOsFs(), *themePath) - theme := NewTheme(*themePath) + theme := NewTheme(themeFs) state := NewState(stateFs) puzzles := NewMothballs(*puzzlePath) - go theme.Run(*refreshInterval) go state.Run(*refreshInterval) go puzzles.Run(*refreshInterval) - log.Println("I would be binding to", *bindStr) - time.Sleep(1 * time.Second) - log.Print(state.Export("")) - time.Sleep(19 * time.Second) + // Add some MIME extensions + // Doing this avoids decompressing a mothball entry twice per request + mime.AddExtensionType(".json", "application/json") + mime.AddExtensionType(".zip", "application/zip") + + http.HandleFunc("/", theme.staticHandler) + + log.Printf("Listening on %s", *bindStr) + log.Fatal(http.ListenAndServe(*bindStr, nil)) } diff --git a/cmd/mothd/theme.go b/cmd/mothd/theme.go index 895f294..b7a24ee 100644 --- a/cmd/mothd/theme.go +++ b/cmd/mothd/theme.go @@ -1,20 +1,18 @@ package main import ( + "github.com/spf13/afero" "net/http" - "os" "strings" ) type Theme struct { - Component + fs afero.Fs } -func NewTheme(baseDir string) *Theme { +func NewTheme(fs afero.Fs) *Theme { return &Theme{ - Component: Component{ - baseDir: baseDir, - }, + fs: fs, } } @@ -28,7 +26,7 @@ func (t *Theme) staticHandler(w http.ResponseWriter, req *http.Request) { path = "/index.html" } - f, err := os.Open(t.path(path)) + f, err := t.fs.Open(path) if err != nil { http.NotFound(w, req) return diff --git a/cmd/mothd/theme_test.go b/cmd/mothd/theme_test.go new file mode 100644 index 0000000..bf667a9 --- /dev/null +++ b/cmd/mothd/theme_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/spf13/afero" + "net/http" + "net/http/httptest" + "testing" +) + +func TestTheme(t *testing.T) { + fs := new(afero.MemMapFs) + afero.WriteFile(fs, "/index.html", []byte("index"), 0644) + afero.WriteFile(fs, "/moo.html", []byte("moo"), 0644) + + s := NewTheme(fs) + + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(s.staticHandler) + handler.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Errorf("Handler returned wrong code: %v", rr.Code) + } + + if rr.Body.String() != "index" { + t.Errorf("Handler returned wrong content: %v", rr.Body.String()) + } +} From 431e1f00a7be36655ccf64a2a944b3b404d17c24 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 7 Dec 2019 21:17:13 -0700 Subject: [PATCH 27/96] Add zipfile performance testing --- cmd/mothd/main.go | 18 +++++------ cmd/mothd/state_test.go | 8 ++--- cmd/mothd/theme_test.go | 4 +-- cmd/mothd/zipfs_test.go | 70 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go index 7704f65..df1bea4 100644 --- a/cmd/mothd/main.go +++ b/cmd/mothd/main.go @@ -48,13 +48,13 @@ func main() { go state.Run(*refreshInterval) go puzzles.Run(*refreshInterval) - // Add some MIME extensions - // Doing this avoids decompressing a mothball entry twice per request - mime.AddExtensionType(".json", "application/json") - mime.AddExtensionType(".zip", "application/zip") - - http.HandleFunc("/", theme.staticHandler) - - log.Printf("Listening on %s", *bindStr) - log.Fatal(http.ListenAndServe(*bindStr, nil)) + // Add some MIME extensions + // Doing this avoids decompressing a mothball entry twice per request + mime.AddExtensionType(".json", "application/json") + mime.AddExtensionType(".zip", "application/zip") + + http.HandleFunc("/", theme.staticHandler) + + log.Printf("Listening on %s", *bindStr) + log.Fatal(http.ListenAndServe(*bindStr, nil)) } diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 1849b07..e9f74c4 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -47,22 +47,22 @@ func TestState(t *testing.T) { if err := s.SetTeamName(teamId, "My Team"); err != nil { t.Errorf("Setting team name: %v", err) } - + category := "poot" points := 3928 s.AwardPoints(teamId, category, points) s.Cleanup() - + pl = s.PointsLog() if len(pl) != 1 { t.Errorf("After awarding points, points log has length %d", len(pl)) } else if (pl[0].TeamId != teamId) || (pl[0].Category != category) || (pl[0].Points != points) { t.Errorf("Incorrect logged award %v", pl) } - + fs.Remove("initialized") s.Cleanup() - + pl = s.PointsLog() if len(pl) != 0 { t.Errorf("After reinitialization, points log has length %d", len(pl)) diff --git a/cmd/mothd/theme_test.go b/cmd/mothd/theme_test.go index bf667a9..b64a2fe 100644 --- a/cmd/mothd/theme_test.go +++ b/cmd/mothd/theme_test.go @@ -18,14 +18,14 @@ func TestTheme(t *testing.T) { if err != nil { t.Fatal(err) } - + rr := httptest.NewRecorder() handler := http.HandlerFunc(s.staticHandler) handler.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("Handler returned wrong code: %v", rr.Code) } - + if rr.Body.String() != "index" { t.Errorf("Handler returned wrong content: %v", rr.Body.String()) } diff --git a/cmd/mothd/zipfs_test.go b/cmd/mothd/zipfs_test.go index 22d9386..b6898f0 100644 --- a/cmd/mothd/zipfs_test.go +++ b/cmd/mothd/zipfs_test.go @@ -5,10 +5,80 @@ import ( "fmt" "io" "io/ioutil" + "math/rand" "os" "testing" + "time" ) +func TestZipPerformance(t *testing.T) { + // I get 4.8s for 10,000 reads + if os.Getenv("BENCHMARK") == "" { + return + } + + rng := rand.New(rand.NewSource(rand.Int63())) + + tf, err := ioutil.TempFile("", "zipfs") + if err != nil { + t.Error(err) + return + } + defer os.Remove(tf.Name()) + + w := zip.NewWriter(tf) + for i := 0; i < 100; i += 1 { + fsize := 1000 + switch { + case i % 10 == 0: + fsize = 400000 + case i % 20 == 6: + fsize = 5000000 + case i == 80: + fsize = 1000000000 + } + + f, err := w.Create(fmt.Sprintf("%d.bin", i)) + if err != nil { + t.Fatal(err) + return + } + if _, err := io.CopyN(f, rng, int64(fsize)); err != nil { + t.Error(err) + } + } + w.Close() + + tfsize, err := tf.Seek(0, 2) + if err != nil { + t.Fatal(err) + } + + startTime := time.Now() + nReads := 10000 + for i := 0; i < 10000; i += 1 { + r, err := zip.NewReader(tf, tfsize) + if err != nil { + t.Error(err) + return + } + filenum := rng.Intn(len(r.File)) + f, err := r.File[filenum].Open() + if err != nil { + t.Error(err) + continue + } + buf, err := ioutil.ReadAll(f) + if err != nil { + t.Error(err) + } + t.Log("Read file of size", len(buf)) + f.Close() + } + t.Log(nReads, "reads took", time.Since(startTime)) + t.Error("moo") +} + func TestZipfs(t *testing.T) { tf, err := ioutil.TempFile("", "zipfs") if err != nil { From 5f221b466eb5a264ee52c15c3d77b3266c544724 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 12 Dec 2019 20:02:57 -0700 Subject: [PATCH 28/96] Report if the team ID is already registered --- cmd/mothd/state.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index c855264..775bf85 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -134,6 +134,9 @@ func (s *State) SetTeamName(teamId string, teamName string) error { teamFile := filepath.Join("teams", teamId) err := afero.WriteFile(s.fs, teamFile, []byte(teamName), os.ModeExclusive|0644) + if os.IsExist(err) { + return fmt.Errorf("Team ID is already registered") + } return err } From cbc69f5647327d38191532a307955b3e45a3222f Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 22 Feb 2020 16:49:58 -0600 Subject: [PATCH 29/96] Some stuff on my laptop --- cmd/mothd/main.go | 3 ++- cmd/mothd/mothballs.go | 11 +++++------ cmd/mothd/zipfs.go | 11 +++++++++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go index df1bea4..98ea8b1 100644 --- a/cmd/mothd/main.go +++ b/cmd/mothd/main.go @@ -40,10 +40,11 @@ func main() { stateFs := afero.NewBasePathFs(afero.NewOsFs(), *statePath) themeFs := afero.NewBasePathFs(afero.NewOsFs(), *themePath) + mothballFs := afero.NewBasePathFs(afero.NewOsFs(), *mothballPath) theme := NewTheme(themeFs) state := NewState(stateFs) - puzzles := NewMothballs(*puzzlePath) + puzzles := NewMothballs(mothballFs) go state.Run(*refreshInterval) go puzzles.Run(*refreshInterval) diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index 1ceaac6..e4c5d51 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -1,6 +1,7 @@ package main import ( + "github.com/spf13/afero" "io/ioutil" "log" "strings" @@ -8,22 +9,20 @@ import ( ) type Mothballs struct { - Component + fs afero.Fs categories map[string]*Zipfs } -func NewMothballs(baseDir string) *Mothballs { +func NewMothballs(fs afero.Fs) *Mothballs { return &Mothballs{ - Component: Component{ - baseDir: baseDir, - }, + fs: fs, categories: make(map[string]*Zipfs), } } func (m *Mothballs) update() { // Any new categories? - files, err := ioutil.ReadDir(m.path()) + files, err := afero.ReadDir(m.fs, "/") if err != nil { log.Print("Error listing mothballs: ", err) return diff --git a/cmd/mothd/zipfs.go b/cmd/mothd/zipfs.go index 1b0f00a..9c27e62 100644 --- a/cmd/mothd/zipfs.go +++ b/cmd/mothd/zipfs.go @@ -12,6 +12,7 @@ import ( type Zipfs struct { zf *zip.ReadCloser + fs afero.Fs filename string mtime time.Time } @@ -111,9 +112,10 @@ func (zfsf *ZipfsFile) Close() error { return zfsf.f.Close() } -func OpenZipfs(filename string) (*Zipfs, error) { +func OpenZipfs(fs afero.fs, filename string) (*Zipfs, error) { var zfs Zipfs + zfs.fs = fs zfs.filename = filename err := zfs.Refresh() @@ -129,7 +131,7 @@ func (zfs *Zipfs) Close() error { } func (zfs *Zipfs) Refresh() error { - info, err := os.Stat(zfs.filename) + info, err := zfs.fs.Stat(zfs.filename) if err != nil { return err } @@ -139,6 +141,11 @@ func (zfs *Zipfs) Refresh() error { return nil } + f, err := zfs.fs.Open(zfs.filename) + if err != nil { + return err + } + zf, err := zip.OpenReader(zfs.filename) if err != nil { return err From 1d307c71a8758d63f7fac23d50457af4e1d03093 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 22 Feb 2020 17:25:46 -0600 Subject: [PATCH 30/96] Now you can add scripts from puzzle.py --- devel/moth.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/devel/moth.py b/devel/moth.py index d228b72..1e834f6 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -235,9 +235,7 @@ class Puzzle: self.files[name] = PuzzleFile(stream, name, not hidden) elif key == 'script': stream = open(val, 'rb') - # Make sure this shows up in the header block of the HTML output. - self.files[val] = PuzzleFile(stream, val, visible=False) - self.scripts.append(val) + self.add_script_stream(stream, val) elif key == "objective": self.objective = val elif key == "success": @@ -290,6 +288,11 @@ class Puzzle: self.add_stream(stream, name, visible) return stream + def add_script_stream(self, stream, name): + # Make sure this shows up in the header block of the HTML output. + self.files[name] = PuzzleFile(stream, name, visible=False) + self.scripts.append(name) + def add_stream(self, stream, name=None, visible=True): if name is None: name = self.random_hash() From bdfbe527de3645fa69962f3e1a4219e13de392e3 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 25 Feb 2020 17:22:39 -0700 Subject: [PATCH 31/96] Add a few things for new CyFi theme --- devel/devel-server.py | 108 +++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/devel/devel-server.py b/devel/devel-server.py index 0d775dc..5ec8b68 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -61,7 +61,27 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): cat = moth.Category(catpath, self.seed) puzzle = cat.puzzle(points) return puzzle + + + def send_json(self, obj): + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(obj).encode("utf-8")) + + def handle_register(self): + # Everybody eats when they come to my house + ret = { + "status": "success", + "data": { + "short": "You win", + "description": "Welcome to the development server, you wily hacker you" + } + } + self.send_json(ret) + endpoints.append(('/{seed}/register', handle_register)) + def handle_answer(self): for f in ("cat", "points", "answer"): @@ -77,17 +97,12 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): if self.req.get("answer") in puzzle.answers: ret["data"]["description"] = "Answer is correct" - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.end_headers() - self.wfile.write(json.dumps(ret).encode("utf-8")) + self.send_json(ret) endpoints.append(('/{seed}/answer', handle_answer)) - - def handle_puzzlelist(self): - puzzles = { - "__devel__": [[0, ""]], - } + + def puzzlelist(self): + puzzles = {} for p in self.server.args["puzzles_dir"].glob("*"): if not p.is_dir() or p.match(".*"): continue @@ -97,13 +112,28 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): puzzles[catName].append([0, ""]) if len(puzzles) <= 1: logging.warning("No directories found matching {}/*".format(self.server.args["puzzles_dir"])) - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.end_headers() - self.wfile.write(json.dumps(puzzles).encode("utf-8")) + + return puzzles + + + # XXX: Remove this (redundant) when we've upgraded the bundled theme (probably v3.6 and beyond) + def handle_puzzlelist(self): + self.send_json(self.puzzlelist()) endpoints.append(('/{seed}/puzzles.json', handle_puzzlelist)) + def handle_state(self): + resp = { + "config": { + "devel": True, + }, + "puzzles": self.puzzlelist(), + "messages": "

[MOTH Development Server] Participant broadcast messages would go here.

", + } + self.send_json(resp) + endpoints.append(('/{seed}/state', handle_state)) + + def handle_puzzle(self): puzzle = self.get_puzzle() @@ -113,10 +143,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): obj["summary"] = puzzle.summary obj["logs"] = puzzle.logs - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.end_headers() - self.wfile.write(json.dumps(obj).encode("utf-8")) + self.send_json(obj) endpoints.append(('/{seed}/content/{cat}/{points}/puzzle.json', handle_puzzle)) @@ -144,7 +171,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): try: catdir = self.server.args["puzzles_dir"].joinpath(category) - mb = mothballer.package(category, catdir, self.seed) + mb = mothballer.package(category, str(catdir), self.seed) except Exception as ex: logging.exception(ex) self.send_response(200) @@ -162,48 +189,11 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): def handle_index(self): seed = random.getrandbits(32) - body = """ - - - Dev Server - - - -

Dev Server

- -

- Pick a seed: -

-
    -
  • {seed}: a special seed I made just for you!
  • -
  • random: will use a different seed every time you load a page (could be useful for debugging)
  • -
  • You can also hack your own seed into the URL, if you want to.
  • -
- -

- Puzzles can be generated from Python code: these puzzles can use a random number generator if they want. - The seed is used to create these random numbers. -

- -

- We like to make a new seed for every contest, - and re-use that seed whenever we regenerate a category during an event - (say to fix a bug). - By using the same seed, - we make sure that all the dynamically-generated puzzles have the same values - in any new packages we build. -

- - -""".format(seed=seed) - - self.send_response(200) - self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_response(307) + self.send_header("Location", "%s/" % seed) + self.send_header("Content-Type", "text/html") self.end_headers() - self.wfile.write(body.encode('utf-8')) + self.wfile.write("Your browser was supposed to redirect you to here." % seed) endpoints.append((r"/", handle_index)) From 3d0e73d0e907c870f4f4f5038f97e93764f48556 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 27 Feb 2020 18:10:09 -0700 Subject: [PATCH 32/96] Hand out the right content-type for .mjs files --- devel/devel-server.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/devel/devel-server.py b/devel/devel-server.py index 5ec8b68..0534b32 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -7,7 +7,6 @@ import cgi import http.server import io import json -import mimetypes import moth import logging import os @@ -43,7 +42,6 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): except TypeError: super().__init__(request, client_address, server) - # Backport from Python 3.7 def translate_path(self, path): # I guess we just hope that some other thread doesn't call getcwd @@ -160,7 +158,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): return self.send_response(200) - self.send_header("Content-Type", mimetypes.guess_type(file.name)) + self.send_header("Content-Type", self.guess_type(file.name)) self.end_headers() shutil.copyfileobj(file.stream, self.wfile) endpoints.append(("/{seed}/content/{cat}/{points}/{filename}", handle_puzzlefile)) @@ -194,7 +192,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write("Your browser was supposed to redirect you to here." % seed) - endpoints.append((r"/", handle_index)) + endpoints.append(("/", handle_index)) def handle_theme_file(self): @@ -235,6 +233,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): HTTPStatus.NOT_IMPLEMENTED, "Unsupported method (%r)" % self.command, ) +MothRequestHandler.extensions_map[".mjs"] = "application/ecmascript" if __name__ == '__main__': @@ -279,6 +278,6 @@ if __name__ == '__main__': server.args["base_url"] = args.base server.args["puzzles_dir"] = pathlib.Path(args.puzzles) server.args["theme_dir"] = args.theme - + logging.info("Listening on %s:%d", addr, port) server.serve_forever() From 618100d7b7e250486a13dddf3f6de8049e11fec0 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 28 Feb 2020 19:05:24 +0000 Subject: [PATCH 33/96] Revert "Hand out the right content-type for .mjs files" This reverts commit 3d0e73d0e907c870f4f4f5038f97e93764f48556. --- devel/devel-server.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/devel/devel-server.py b/devel/devel-server.py index 0534b32..5ec8b68 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -7,6 +7,7 @@ import cgi import http.server import io import json +import mimetypes import moth import logging import os @@ -42,6 +43,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): except TypeError: super().__init__(request, client_address, server) + # Backport from Python 3.7 def translate_path(self, path): # I guess we just hope that some other thread doesn't call getcwd @@ -158,7 +160,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): return self.send_response(200) - self.send_header("Content-Type", self.guess_type(file.name)) + self.send_header("Content-Type", mimetypes.guess_type(file.name)) self.end_headers() shutil.copyfileobj(file.stream, self.wfile) endpoints.append(("/{seed}/content/{cat}/{points}/{filename}", handle_puzzlefile)) @@ -192,7 +194,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write("Your browser was supposed to redirect you to here." % seed) - endpoints.append(("/", handle_index)) + endpoints.append((r"/", handle_index)) def handle_theme_file(self): @@ -233,7 +235,6 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): HTTPStatus.NOT_IMPLEMENTED, "Unsupported method (%r)" % self.command, ) -MothRequestHandler.extensions_map[".mjs"] = "application/ecmascript" if __name__ == '__main__': @@ -278,6 +279,6 @@ if __name__ == '__main__': server.args["base_url"] = args.base server.args["puzzles_dir"] = pathlib.Path(args.puzzles) server.args["theme_dir"] = args.theme - + logging.info("Listening on %s:%d", addr, port) server.serve_forever() From 6d2c65f9c0a5ee04d2e98b9e151193c2338e03d5 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 28 Feb 2020 15:24:23 -0700 Subject: [PATCH 34/96] Fix some junk --- devel/devel-server.py | 4 +++- devel/moth.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/devel/devel-server.py b/devel/devel-server.py index 0534b32..4203e0d 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -153,7 +153,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): except KeyError: self.send_error( HTTPStatus.NOT_FOUND, - "File Not Found", + "File Not Found: %s" % self.req["filename"], ) return @@ -233,6 +233,8 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): HTTPStatus.NOT_IMPLEMENTED, "Unsupported method (%r)" % self.command, ) + +# I don't fully understand why you can't do this inside the class definition. MothRequestHandler.extensions_map[".mjs"] = "application/ecmascript" diff --git a/devel/moth.py b/devel/moth.py index 1e834f6..595ba9e 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -233,6 +233,12 @@ class Puzzle: except IndexError: pass self.files[name] = PuzzleFile(stream, name, not hidden) + elif key == 'files': + for file in val: + path = file["path"] + stream = open(path, "rb") + name = file.get("name") or path + self.files[name] = PuzzleFile(stream, name, not file.get("hidden")) elif key == 'script': stream = open(val, 'rb') self.add_script_stream(stream, val) @@ -400,10 +406,12 @@ class Puzzle: """Return a dict packaging of the puzzle.""" files = [fn for fn,f in self.files.items() if f.visible] + hidden = [fn for fn,f in self.files.items() if not f.visible] return { 'authors': self.get_authors(), 'hashes': self.hashes(), 'files': files, + 'hidden': hidden, 'scripts': self.scripts, 'pattern': self.pattern, 'body': self.html_body(), From 94acae13ca5ad97a9f608726a94b7faa17236ce4 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 28 Feb 2020 15:42:55 -0700 Subject: [PATCH 35/96] Theme doing devel detection properly with new state --- devel/devel-server.py | 2 +- theme/moth.js | 25 +++++++++++-------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/devel/devel-server.py b/devel/devel-server.py index bc35765..2acc7f3 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -193,7 +193,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): self.send_header("Location", "%s/" % seed) self.send_header("Content-Type", "text/html") self.end_headers() - self.wfile.write("Your browser was supposed to redirect you to here." % seed) + self.wfile.write(b"Your browser was supposed to redirect you to here." % seed) endpoints.append((r"/", handle_index)) diff --git a/theme/moth.js b/theme/moth.js index 41ed728..7383388 100644 --- a/theme/moth.js +++ b/theme/moth.js @@ -1,5 +1,6 @@ // jshint asi:true +var devel = false var teamId var heartbeatInterval = 40000 @@ -42,7 +43,7 @@ function renderPuzzles(obj) { h.textContent = cat // Extras if we're running a devel server - if (obj.__devel__) { + if (devel) { let a = document.createElement('a') h.insertBefore(a, h.firstChild) a.textContent = "⬇️" @@ -88,20 +89,16 @@ function renderPuzzles(obj) { container.appendChild(puzzlesElement) } +function renderState(obj) { + devel = obj.config.devel + console.log(obj) + renderPuzzles(obj.puzzles) + renderNotices(obj.messages) +} + function heartbeat(teamId, participantId) { - let noticesUrl = new URL("notices.html", window.location) - fetch(noticesUrl) - .then(resp => { - if (resp.ok) { - resp.text() - .then(renderNotices) - .catch(err => console.log) - } - }) - .catch(err => console.log) - - let url = new URL("puzzles.json", window.location) + let url = new URL("state", window.location) url.searchParams.set("id", teamId) if (participantId) { url.searchParams.set("pid", participantId) @@ -112,7 +109,7 @@ function heartbeat(teamId, participantId) { .then(resp => { if (resp.ok) { resp.json() - .then(renderPuzzles) + .then(renderState) .catch(err => { toast("Error fetching recent puzzles. I'll try again in a moment.") console.log(err) From 4a73bb09100c6a4fe4ce6d94732a93f0e2bd5342 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 28 Feb 2020 16:01:35 -0700 Subject: [PATCH 36/96] Theme back to previous functionality with dev server --- theme/moth.js | 73 +++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/theme/moth.js b/theme/moth.js index 7383388..3c81546 100644 --- a/theme/moth.js +++ b/theme/moth.js @@ -25,6 +25,8 @@ function renderNotices(obj) { function renderPuzzles(obj) { let puzzlesElement = document.createElement('div') + document.getElementById("login").style.display = "none" + // Create a sorted list of category names let cats = Object.keys(obj) cats.sort() @@ -91,13 +93,20 @@ function renderPuzzles(obj) { function renderState(obj) { devel = obj.config.devel - console.log(obj) - renderPuzzles(obj.puzzles) + if (devel) { + sessionStorage.id = "1234" + sessionStorage.pid = "rodney" + } + if (Object.keys(obj.puzzles).length > 0) { + renderPuzzles(obj.puzzles) + } renderNotices(obj.messages) } -function heartbeat(teamId, participantId) { +function heartbeat() { + let teamId = sessionStorage.id + let participantId = sessionStorage.pid let url = new URL("state", window.location) url.searchParams.set("id", teamId) if (participantId) { @@ -111,34 +120,29 @@ function heartbeat(teamId, participantId) { resp.json() .then(renderState) .catch(err => { - toast("Error fetching recent puzzles. I'll try again in a moment.") + toast("Error fetching recent state. I'll try again in a moment.") console.log(err) }) } }) .catch(err => { - toast("Error fetching recent puzzles. I'll try again in a moment.") + toast("Error fetching recent state. I'll try again in a moment.") console.log(err) }) } -function showPuzzles(teamId, participantId) { +function showPuzzles() { let spinner = document.createElement("span") spinner.classList.add("spinner") - sessionStorage.setItem("id", teamId) - if (participantId) { - sessionStorage.setItem("pid", participantId) - } - document.getElementById("login").style.display = "none" document.getElementById("puzzles").appendChild(spinner) - heartbeat(teamId, participantId) - setInterval(e => { heartbeat(teamId) }, 40000) - drawCacheButton(teamId) + heartbeat() + drawCacheButton() } -function drawCacheButton(teamId) { +function drawCacheButton() { + let teamId = sessionStorage.id let cacher = document.querySelector("#cacheButton") function updateCacheButton() { @@ -165,7 +169,7 @@ function drawCacheButton(teamId) { } async function fetchAll() { - let teamId = sessionStorage.getItem("id") + let teamId = sessionStorage.id let headers = new Headers() headers.append("pragma", "no-cache") headers.append("cache-control", "no-cache") @@ -206,14 +210,16 @@ async function fetchAll() { } let puzzles = categories[cat_name] for (let puzzle of puzzles) { - let url = new URL("puzzle.html", window.location) - url.searchParams.set("cat", cat_name) - url.searchParams.set("points", puzzle[0]) - url.searchParams.set("pid", puzzle[1]) - requests.push( fetch(url) - .then(e => { - console.log("Fetched " + url) - })) + let url = new URL("puzzle.html", window.location) + url.searchParams.set("cat", cat_name) + url.searchParams.set("points", puzzle[0]) + url.searchParams.set("pid", puzzle[1]) + requests.push( + fetch(url) + .then(e => { + console.log("Fetched " + url) + }) + ) } } } @@ -236,12 +242,11 @@ function login(e) { if (resp.ok) { resp.json() .then(obj => { - if (obj.status == "success") { - toast("Team registered") - showPuzzles(teamId, participantId) - } else if (obj.data.short == "Already registered") { - toast("Logged in with previously-registered team name") - showPuzzles(teamId, participantId) + if ((obj.status == "success") || (obj.data.short == "Already registered")) { + toast("Logged in") + sessionStorage.id = teamId + sessionStorage.pid = participantId + showPuzzles() } else { toast(obj.data.description) } @@ -263,11 +268,11 @@ function login(e) { function init() { // Already signed in? - let teamId = sessionStorage.getItem("id") - let participantId = sessionStorage.getItem("pid") - if (teamId) { - showPuzzles(teamId, participantId) + if (sessionStorage.id) { + showPuzzles() } + heartbeat() + setInterval(e => heartbeat(), 40000) document.getElementById("login").addEventListener("submit", login) } From d22478fc7ad8d95bbcae78031e98776203b84724 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 28 Feb 2020 16:30:01 -0700 Subject: [PATCH 37/96] Changelog updates --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c99e9f3..a260324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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). ## [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 `state.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 ## [3.4.3] - 2019-11-20 ### Fixed From b30860726a728a035101cfc29f877d34cc9efd1a Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 28 Feb 2020 16:32:31 -0700 Subject: [PATCH 38/96] Clarify changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a260324..dd8fc66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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 `state.config.devel` in the `/state` endpoint + - 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 From 0bc8faa5ece3755815c5d00886d12f7d407fe44d Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 29 Feb 2020 16:51:32 -0700 Subject: [PATCH 39/96] OMG it runs, haven't tested yet but exciting stuff here folks --- cmd/mothd/common.go | 42 +++++++++++++--------- cmd/mothd/main.go | 41 +++++++++++++-------- cmd/mothd/mothballs.go | 42 +++++++++++----------- cmd/mothd/state.go | 80 ++++++++++++++++++----------------------- cmd/mothd/theme.go | 46 ++++++++++-------------- cmd/mothd/zipfs.go | 18 ++++++---- cmd/mothdv3/handlers.go | 32 +---------------- 7 files changed, 137 insertions(+), 164 deletions(-) diff --git a/cmd/mothd/common.go b/cmd/mothd/common.go index 2217258..2e07ea1 100644 --- a/cmd/mothd/common.go +++ b/cmd/mothd/common.go @@ -1,28 +1,36 @@ package main import ( - "path/filepath" - "strings" + "io" "time" ) -type Component struct { - baseDir string +type Category struct { + Name string + Puzzles []int } -func (c *Component) path(parts ...string) string { - path := filepath.Clean(filepath.Join(parts...)) - parts = filepath.SplitList(path) - for i, part := range parts { - part = strings.TrimLeft(part, "./\\:") - parts[i] = part - } - parts = append([]string{c.baseDir}, parts...) - path = filepath.Join(parts...) - path = filepath.Clean(path) - return path +type ReadSeekCloser interface { + io.Reader + io.Seeker + io.Closer } -func (c *Component) Run(updateInterval time.Duration) { - // Stub! +type PuzzleProvider interface { + Metadata(cat string, points int) (io.ReadCloser, error) + Open(cat string, points int, path string) (io.ReadCloser, error) + Inventory() []Category +} + +type ThemeProvider interface { + Open(path string) (ReadSeekCloser, error) + ModTime(path string) (time.Time, error) +} + +type StateProvider interface { + +} + +type Component interface { + Update() } diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go index 98ea8b1..b377526 100644 --- a/cmd/mothd/main.go +++ b/cmd/mothd/main.go @@ -5,10 +5,23 @@ import ( "github.com/spf13/afero" "log" "mime" - "net/http" "time" ) +func custodian(updateInterval time.Duration, components []Component) { + update := func() { + for _, c := range components { + c.Update() + } + } + + ticker := time.NewTicker(updateInterval) + update() + for _ = range ticker.C { + update() + } +} + func main() { log.Print("Started") @@ -22,7 +35,7 @@ func main() { "state", "Path to state files", ) - puzzlePath := flag.String( + mothballPath := flag.String( "mothballs", "mothballs", "Path to mothballs to host", @@ -37,25 +50,23 @@ func main() { ":8000", "Bind [host]:port for HTTP service", ) + base := flag.String( + "base", + "/", + "Base URL of this instance", + ) - stateFs := afero.NewBasePathFs(afero.NewOsFs(), *statePath) - themeFs := afero.NewBasePathFs(afero.NewOsFs(), *themePath) - mothballFs := afero.NewBasePathFs(afero.NewOsFs(), *mothballPath) - - theme := NewTheme(themeFs) - state := NewState(stateFs) - puzzles := NewMothballs(mothballFs) - - go state.Run(*refreshInterval) - go puzzles.Run(*refreshInterval) + theme := NewTheme(afero.NewBasePathFs(afero.NewOsFs(), *themePath)) + state := NewState(afero.NewBasePathFs(afero.NewOsFs(), *statePath)) + puzzles := NewMothballs(afero.NewBasePathFs(afero.NewOsFs(), *mothballPath)) // Add some MIME extensions // Doing this avoids decompressing a mothball entry twice per request mime.AddExtensionType(".json", "application/json") mime.AddExtensionType(".zip", "application/zip") - http.HandleFunc("/", theme.staticHandler) + go custodian(*refreshInterval, []Component{theme, state, puzzles}) - log.Printf("Listening on %s", *bindStr) - log.Fatal(http.ListenAndServe(*bindStr, nil)) + httpd := NewHTTPServer(*base, theme, state, puzzles) + httpd.Run(*bindStr) } diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index e4c5d51..4877f26 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -2,43 +2,56 @@ package main import ( "github.com/spf13/afero" - "io/ioutil" "log" + "io" "strings" - "time" ) type Mothballs struct { - fs afero.Fs categories map[string]*Zipfs + afero.Fs } func NewMothballs(fs afero.Fs) *Mothballs { return &Mothballs{ - fs: fs, + Fs: fs, categories: make(map[string]*Zipfs), } } -func (m *Mothballs) update() { +func (m *Mothballs) Metadata(cat string, points int) (io.ReadCloser, error) { + f, err := m.Fs.Open("/dev/null") + return f, err +} + +func (m *Mothballs) Open(cat string, points int, filename string) (io.ReadCloser, error) { + f, err := m.Fs.Open("/dev/null") + return f, err +} + +func (m *Mothballs) Inventory() []Category { + return []Category{} +} + + +func (m *Mothballs) Update() { // Any new categories? - files, err := afero.ReadDir(m.fs, "/") + files, err := afero.ReadDir(m.Fs, "/") if err != nil { log.Print("Error listing mothballs: ", err) return } for _, f := range files { filename := f.Name() - filepath := m.path(filename) if !strings.HasSuffix(filename, ".mb") { continue } categoryName := strings.TrimSuffix(filename, ".mb") if _, ok := m.categories[categoryName]; !ok { - zfs, err := OpenZipfs(filepath) + zfs, err := OpenZipfs(m.Fs, filename) if err != nil { - log.Print("Error opening ", filepath, ": ", err) + log.Print("Error opening ", filename, ": ", err) continue } log.Print("New mothball: ", filename) @@ -47,14 +60,3 @@ func (m *Mothballs) update() { } } -func (m *Mothballs) Run(updateInterval time.Duration) { - ticker := time.NewTicker(updateInterval) - m.update() - for { - select { - case when := <-ticker.C: - log.Print("Tick: ", when) - m.update() - } - } -} diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 775bf85..081b644 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -35,28 +35,26 @@ type StateExport struct { // The only thing State methods need to know is the path to the state directory. type State struct { Enabled bool - update chan bool - fs afero.Fs + afero.Fs } func NewState(fs afero.Fs) *State { return &State{ Enabled: true, - update: make(chan bool, 10), - fs: fs, + Fs: fs, } } // Check a few things to see if this state directory is "enabled". func (s *State) UpdateEnabled() { - if _, err := s.fs.Stat("enabled"); os.IsNotExist(err) { + if _, err := s.Stat("enabled"); os.IsNotExist(err) { s.Enabled = false log.Print("Suspended: enabled file missing") return } nextEnabled := true - untilFile, err := s.fs.Open("hours") + untilFile, err := s.Open("hours") if err != nil { return } @@ -101,7 +99,7 @@ func (s *State) UpdateEnabled() { // Returns team name given a team ID. func (s *State) TeamName(teamId string) (string, error) { teamFile := filepath.Join("teams", teamId) - teamNameBytes, err := afero.ReadFile(s.fs, teamFile) + teamNameBytes, err := afero.ReadFile(s, teamFile) teamName := strings.TrimSpace(string(teamNameBytes)) if os.IsNotExist(err) { @@ -115,7 +113,7 @@ func (s *State) TeamName(teamId string) (string, error) { // Write out team name. This can only be done once. func (s *State) SetTeamName(teamId string, teamName string) error { - if f, err := s.fs.Open("teamids.txt"); err != nil { + if f, err := s.Open("teamids.txt"); err != nil { return fmt.Errorf("Team IDs file does not exist") } else { found := false @@ -133,7 +131,7 @@ func (s *State) SetTeamName(teamId string, teamName string) error { } teamFile := filepath.Join("teams", teamId) - err := afero.WriteFile(s.fs, teamFile, []byte(teamName), os.ModeExclusive|0644) + err := afero.WriteFile(s, teamFile, []byte(teamName), os.ModeExclusive|0644) if os.IsExist(err) { return fmt.Errorf("Team ID is already registered") } @@ -142,7 +140,7 @@ func (s *State) SetTeamName(teamId string, teamName string) error { // Retrieve the current points log func (s *State) PointsLog() []*Award { - f, err := s.fs.Open("points.log") + f, err := s.Open("points.log") if err != nil { log.Println(err) return nil @@ -178,7 +176,7 @@ func (s *State) Export(teamId string) *StateExport { } // Read in messages - if f, err := s.fs.Open("messages.txt"); err != nil { + if f, err := s.Open("messages.txt"); err != nil { log.Print(err) } else { defer f.Close() @@ -243,29 +241,29 @@ func (s *State) AwardPoints(teamId, category string, points int) error { tmpfn := filepath.Join("points.tmp", fn) newfn := filepath.Join("points.new", fn) - if err := afero.WriteFile(s.fs, tmpfn, []byte(a.String()), 0644); err != nil { + if err := afero.WriteFile(s, tmpfn, []byte(a.String()), 0644); err != nil { return err } - if err := s.fs.Rename(tmpfn, newfn); err != nil { + if err := s.Rename(tmpfn, newfn); err != nil { return err } - s.update <- true + // XXX: update everything 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 (s *State) collectPoints() { - files, err := afero.ReadDir(s.fs, "points.new") + files, err := afero.ReadDir(s, "points.new") if err != nil { log.Print(err) return } for _, f := range files { filename := filepath.Join("points.new", f.Name()) - awardstr, err := afero.ReadFile(s.fs, filename) + awardstr, err := afero.ReadFile(s, filename) if err != nil { log.Print("Opening new points: ", err) continue @@ -289,7 +287,7 @@ func (s *State) collectPoints() { } else { log.Print("Award: ", award.String()) - logf, err := s.fs.OpenFile("points.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + logf, err := s.OpenFile("points.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Print("Can't append to points log: ", err) return @@ -298,7 +296,7 @@ func (s *State) collectPoints() { logf.Close() } - if err := s.fs.Remove(filename); err != nil { + if err := s.Remove(filename); err != nil { log.Print("Unable to remove new points file: ", err) } } @@ -306,28 +304,28 @@ func (s *State) collectPoints() { func (s *State) maybeInitialize() { // Are we supposed to re-initialize? - if _, err := s.fs.Stat("initialized"); !os.IsNotExist(err) { + if _, err := s.Stat("initialized"); !os.IsNotExist(err) { return } log.Print("initialized file missing, re-initializing") // Remove any extant control and state files - s.fs.Remove("enabled") - s.fs.Remove("hours") - s.fs.Remove("points.log") - s.fs.Remove("messages.txt") - s.fs.RemoveAll("points.tmp") - s.fs.RemoveAll("points.new") - s.fs.RemoveAll("teams") + s.Remove("enabled") + s.Remove("hours") + s.Remove("points.log") + s.Remove("messages.txt") + s.RemoveAll("points.tmp") + s.RemoveAll("points.new") + s.RemoveAll("teams") // Make sure various subdirectories exist - s.fs.Mkdir("points.tmp", 0755) - s.fs.Mkdir("points.new", 0755) - s.fs.Mkdir("teams", 0755) + s.Mkdir("points.tmp", 0755) + s.Mkdir("points.new", 0755) + s.Mkdir("teams", 0755) // Preseed available team ids if file doesn't exist - if f, err := s.fs.OpenFile("teamids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { + if f, err := s.OpenFile("teamids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { defer f.Close() for i := 0; i < 100; i += 1 { fmt.Fprintln(f, mktoken()) @@ -336,19 +334,19 @@ func (s *State) maybeInitialize() { // Create some files afero.WriteFile( - s.fs, + s, "initialized", []byte("state/initialized: remove to re-initialize the contest\n"), 0644, ) afero.WriteFile( - s.fs, + s, "enabled", []byte("state/enabled: remove to suspend the contest\n"), 0644, ) afero.WriteFile( - s.fs, + s, "hours", []byte( "# state/hours: when the contest is enabled\n"+ @@ -360,33 +358,23 @@ func (s *State) maybeInitialize() { 0644, ) afero.WriteFile( - s.fs, + s, "messages.txt", []byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))), 0644, ) afero.WriteFile( - s.fs, + s, "points.log", []byte(""), 0644, ) } -func (s *State) Cleanup() { +func (s *State) Update() { s.maybeInitialize() s.UpdateEnabled() if s.Enabled { s.collectPoints() } } - -func (s *State) Run(updateInterval time.Duration) { - for { - s.Cleanup() - select { - case <-s.update: - case <-time.After(updateInterval): - } - } -} diff --git a/cmd/mothd/theme.go b/cmd/mothd/theme.go index b7a24ee..afb7e7e 100644 --- a/cmd/mothd/theme.go +++ b/cmd/mothd/theme.go @@ -2,42 +2,32 @@ package main import ( "github.com/spf13/afero" - "net/http" - "strings" + "time" ) type Theme struct { - fs afero.Fs + afero.Fs } func NewTheme(fs afero.Fs) *Theme { return &Theme{ - fs: fs, + Fs: fs, } } -func (t *Theme) staticHandler(w http.ResponseWriter, req *http.Request) { - path := req.URL.Path - if strings.Contains(path, "/.") { - http.Error(w, "Invalid path", http.StatusBadRequest) - return - } - if path == "/" { - path = "/index.html" - } - - f, err := t.fs.Open(path) - if err != nil { - http.NotFound(w, req) - return - } - defer f.Close() - - d, err := f.Stat() - if err != nil { - http.NotFound(w, req) - return - } - - http.ServeContent(w, req, path, d.ModTime(), f) +// I don't understand why I need this. The type checking system is weird here. +func (t *Theme) Open(name string) (ReadSeekCloser, error) { + return t.Fs.Open(name) +} + +func (t *Theme) ModTime(name string) (mt time.Time, err error) { + fi, err := t.Fs.Stat(name) + if err == nil { + mt = fi.ModTime() + } + return +} + +func (t *Theme) Update() { + // No periodic tasks for a theme } diff --git a/cmd/mothd/zipfs.go b/cmd/mothd/zipfs.go index 9c27e62..a955e35 100644 --- a/cmd/mothd/zipfs.go +++ b/cmd/mothd/zipfs.go @@ -5,16 +5,17 @@ import ( "fmt" "io" "io/ioutil" - "os" + "github.com/spf13/afero" "strings" "time" ) type Zipfs struct { - zf *zip.ReadCloser - fs afero.Fs + f io.Closer + zf *zip.Reader filename string mtime time.Time + fs afero.Fs } type ZipfsFile struct { @@ -112,7 +113,7 @@ func (zfsf *ZipfsFile) Close() error { return zfsf.f.Close() } -func OpenZipfs(fs afero.fs, filename string) (*Zipfs, error) { +func OpenZipfs(fs afero.Fs, filename string) (*Zipfs, error) { var zfs Zipfs zfs.fs = fs @@ -127,7 +128,7 @@ func OpenZipfs(fs afero.fs, filename string) (*Zipfs, error) { } func (zfs *Zipfs) Close() error { - return zfs.zf.Close() + return zfs.f.Close() } func (zfs *Zipfs) Refresh() error { @@ -146,15 +147,18 @@ func (zfs *Zipfs) Refresh() error { return err } - zf, err := zip.OpenReader(zfs.filename) + zf, err := zip.NewReader(f, info.Size()) if err != nil { + f.Close() return err } + // Clean up the last one if zfs.zf != nil { - zfs.zf.Close() + zfs.f.Close() } zfs.zf = zf + zfs.f = f zfs.mtime = mtime return nil diff --git a/cmd/mothdv3/handlers.go b/cmd/mothdv3/handlers.go index a699223..29197bd 100644 --- a/cmd/mothdv3/handlers.go +++ b/cmd/mothdv3/handlers.go @@ -12,37 +12,7 @@ import ( "strings" ) -// https://github.com/omniti-labs/jsend -type JSend struct { - Status string `json:"status"` - Data struct { - Short string `json:"short"` - Description string `json:"description"` - } `json:"data"` -} -const ( - JSendSuccess = "success" - JSendFail = "fail" - JSendError = "error" -) - -func respond(w http.ResponseWriter, req *http.Request, status string, short string, format string, a ...interface{}) { - resp := JSend{} - resp.Status = status - resp.Data.Short = short - resp.Data.Description = fmt.Sprintf(format, a...) - - respBytes, err := json.Marshal(resp) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) // RFC2616 makes it pretty clear that 4xx codes are for the user-agent - w.Write(respBytes) -} // hasLine returns true if line appears in r. // The entire line must match. @@ -120,9 +90,9 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) { return } - points, err := strconv.Atoi(pointstr) if err != nil { respond( + points, err := strconv.Atoi(pointstr) w, req, JSendFail, "Cannot parse point value", "This doesn't look like an integer: %s", pointstr, From ffc705ec8ba99c9986becff9b2cd9690207a31e3 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 29 Feb 2020 16:53:08 -0700 Subject: [PATCH 40/96] check in other important files --- cmd/mothd/httpd.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 8 ++++ go.sum | 6 +++ jsend/jsend.go | 39 ++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 cmd/mothd/httpd.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 jsend/jsend.go diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go new file mode 100644 index 0000000..b2315db --- /dev/null +++ b/cmd/mothd/httpd.go @@ -0,0 +1,99 @@ +package main + +import ( + "net/http" + "log" + "strings" + "github.com/dirtbags/moth/jsend" +) + +type HTTPServer struct { + PuzzleProvider + ThemeProvider + StateProvider + *http.ServeMux +} + +func NewHTTPServer(base string, theme ThemeProvider, state StateProvider, puzzles PuzzleProvider) (*HTTPServer) { + h := &HTTPServer{ + ThemeProvider: theme, + StateProvider: state, + PuzzleProvider: puzzles, + ServeMux: http.NewServeMux(), + } + h.HandleFunc(base+"/", h.ThemeHandler) + h.HandleFunc(base+"/state", h.StateHandler) + h.HandleFunc(base+"/register", h.RegisterHandler) + h.HandleFunc(base+"/answer", h.AnswerHandler) + h.HandleFunc(base+"/content/", h.ContentHandler) + return h +} + + +func (h *HTTPServer) Run(bindStr string) { + log.Printf("Listening on %s", bindStr) + log.Fatal(http.ListenAndServe(bindStr, h)) +} + +type MothResponseWriter struct { + statusCode *int + http.ResponseWriter +} + +func (w MothResponseWriter) WriteHeader(statusCode int) { + *w.statusCode = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} + +// This gives Instances the signature of http.Handler +func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { + w := MothResponseWriter{ + statusCode: new(int), + ResponseWriter: wOrig, + } + h.ServeMux.ServeHTTP(w, r) + log.Printf( + "%s %s %s %d\n", + r.RemoteAddr, + r.Method, + r.URL, + *w.statusCode, + ) +} + +func (h *HTTPServer) ThemeHandler(w http.ResponseWriter, req *http.Request) { + path := req.URL.Path + if strings.Contains(path, "..") { + http.Error(w, "Invalid URL path", http.StatusBadRequest) + return + } + if path == "/" { + path = "/index.html" + } + + f, err := h.ThemeProvider.Open(path) + if err != nil { + http.NotFound(w, req) + return + } + defer f.Close() + mtime, _ := h.ThemeProvider.ModTime(path) + http.ServeContent(w, req, path, mtime, f) +} + + +func (h *HTTPServer) StateHandler(w http.ResponseWriter, req *http.Request) { + jsend.Write(w, jsend.Fail, "unimplemented", "I haven't written this yet") +} + +func (h *HTTPServer) RegisterHandler(w http.ResponseWriter, req *http.Request) { + jsend.Write(w, jsend.Fail, "unimplemented", "I haven't written this yet") +} + +func (h *HTTPServer) AnswerHandler(w http.ResponseWriter, req *http.Request) { + jsend.Write(w, jsend.Fail, "unimplemented", "I haven't written this yet") +} + +func (h *HTTPServer) ContentHandler(w http.ResponseWriter, req *http.Request) { + jsend.Write(w, jsend.Fail, "unimplemented", "I haven't written this yet") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fe6b061 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/dirtbags/moth + +go 1.13 + +require ( + github.com/namsral/flag v1.7.4-pre + github.com/spf13/afero v1.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f0f321c --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs= +github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/jsend/jsend.go b/jsend/jsend.go new file mode 100644 index 0000000..6027d7f --- /dev/null +++ b/jsend/jsend.go @@ -0,0 +1,39 @@ +package jsend + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// This provides a JSend function for MOTH +// https://github.com/omniti-labs/jsend + +const ( + Success = "success" + Fail = "fail" + Error = "error" +) + +func Write(w http.ResponseWriter, status, short string, format string, a ...interface{}) { + resp := struct{ + Status string `json:"status"` + Data struct { + Short string `json:"short"` + Description string `json:"description"` + } `json:"data"` + }{} + resp.Status = status + resp.Data.Short = short + resp.Data.Description = fmt.Sprintf(format, a...) + + respBytes, err := json.Marshal(resp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) // RFC2616 makes it pretty clear that 4xx codes are for the user-agent + w.Write(respBytes) +} From 53947e2d64f88e216c7a98aec9662e28d861995b Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 29 Feb 2020 16:59:44 -0700 Subject: [PATCH 41/96] Serving theme files --- cmd/mothd/main.go | 5 +++-- cmd/mothd/theme.go | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go index b377526..2ffb1d1 100644 --- a/cmd/mothd/main.go +++ b/cmd/mothd/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/namsral/flag" + "flag" "github.com/spf13/afero" "log" "mime" @@ -47,7 +47,7 @@ func main() { ) bindStr := flag.String( "bind", - ":8000", + ":8080", "Bind [host]:port for HTTP service", ) base := flag.String( @@ -55,6 +55,7 @@ func main() { "/", "Base URL of this instance", ) + flag.Parse() theme := NewTheme(afero.NewBasePathFs(afero.NewOsFs(), *themePath)) state := NewState(afero.NewBasePathFs(afero.NewOsFs(), *statePath)) diff --git a/cmd/mothd/theme.go b/cmd/mothd/theme.go index afb7e7e..c1d728b 100644 --- a/cmd/mothd/theme.go +++ b/cmd/mothd/theme.go @@ -3,6 +3,7 @@ package main import ( "github.com/spf13/afero" "time" + "log" ) type Theme struct { @@ -17,6 +18,7 @@ func NewTheme(fs afero.Fs) *Theme { // I don't understand why I need this. The type checking system is weird here. func (t *Theme) Open(name string) (ReadSeekCloser, error) { + log.Println(name) return t.Fs.Open(name) } From 464261cd57a17de9fda6196c0242312812b72f17 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 29 Feb 2020 17:12:19 -0700 Subject: [PATCH 42/96] Oops --- cmd/mothd/httpd.go | 1 + cmd/mothd/theme.go | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index b2315db..26faf4f 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -21,6 +21,7 @@ func NewHTTPServer(base string, theme ThemeProvider, state StateProvider, puzzle PuzzleProvider: puzzles, ServeMux: http.NewServeMux(), } + base = strings.TrimRight(base, "/") h.HandleFunc(base+"/", h.ThemeHandler) h.HandleFunc(base+"/state", h.StateHandler) h.HandleFunc(base+"/register", h.RegisterHandler) diff --git a/cmd/mothd/theme.go b/cmd/mothd/theme.go index c1d728b..afb7e7e 100644 --- a/cmd/mothd/theme.go +++ b/cmd/mothd/theme.go @@ -3,7 +3,6 @@ package main import ( "github.com/spf13/afero" "time" - "log" ) type Theme struct { @@ -18,7 +17,6 @@ func NewTheme(fs afero.Fs) *Theme { // I don't understand why I need this. The type checking system is weird here. func (t *Theme) Open(name string) (ReadSeekCloser, error) { - log.Println(name) return t.Fs.Open(name) } From 8ed07e2e29bf5f291ded6960b212a014237c2c67 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 29 Feb 2020 17:12:35 -0700 Subject: [PATCH 43/96] Bring in theme from devel --- theme/Chart.min.js | 7 ++ theme/basic.css | 32 ++++- theme/index.html | 16 ++- theme/manifest.json | 9 ++ theme/moment.min.js | 1 + theme/moth-pwa.js | 17 +++ theme/moth.js | 175 +++++++++++++++++++++++----- theme/notices.html | 1 + theme/points.json | 264 ++++++++++++++++++++++++++++++++++++++++-- theme/puzzle.html | 1 + theme/puzzles.json | 54 +++++++++ theme/scoreboard.html | 131 ++------------------- theme/scoreboard.js | 249 +++++++++++++++++++++++++++++++++++++++ theme/sw.js | 49 ++++++++ 14 files changed, 838 insertions(+), 168 deletions(-) create mode 100644 theme/Chart.min.js create mode 100644 theme/manifest.json create mode 100644 theme/moment.min.js create mode 100644 theme/moth-pwa.js create mode 100644 theme/notices.html create mode 100644 theme/puzzles.json create mode 100644 theme/scoreboard.js create mode 100644 theme/sw.js diff --git a/theme/Chart.min.js b/theme/Chart.min.js new file mode 100644 index 0000000..c74a791 --- /dev/null +++ b/theme/Chart.min.js @@ -0,0 +1,7 @@ +/*! + * Chart.js v2.8.0 + * https://www.chartjs.org + * (c) 2019 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(function(){try{return require("moment")}catch(t){}}()):"function"==typeof define&&define.amd?define(["require"],function(t){return e(function(){try{return t("moment")}catch(t){}}())}):t.Chart=e(t.moment)}(this,function(t){"use strict";t=t&&t.hasOwnProperty("default")?t.default:t;var e={rgb2hsl:i,rgb2hsv:n,rgb2hwb:a,rgb2cmyk:o,rgb2keyword:s,rgb2xyz:l,rgb2lab:d,rgb2lch:function(t){return x(d(t))},hsl2rgb:u,hsl2hsv:function(t){var e=t[0],i=t[1]/100,n=t[2]/100;if(0===n)return[0,0,0];return[e,100*(2*(i*=(n*=2)<=1?n:2-n)/(n+i)),100*((n+i)/2)]},hsl2hwb:function(t){return a(u(t))},hsl2cmyk:function(t){return o(u(t))},hsl2keyword:function(t){return s(u(t))},hsv2rgb:h,hsv2hsl:function(t){var e,i,n=t[0],a=t[1]/100,o=t[2]/100;return e=a*o,[n,100*(e=(e/=(i=(2-a)*o)<=1?i:2-i)||0),100*(i/=2)]},hsv2hwb:function(t){return a(h(t))},hsv2cmyk:function(t){return o(h(t))},hsv2keyword:function(t){return s(h(t))},hwb2rgb:c,hwb2hsl:function(t){return i(c(t))},hwb2hsv:function(t){return n(c(t))},hwb2cmyk:function(t){return o(c(t))},hwb2keyword:function(t){return s(c(t))},cmyk2rgb:f,cmyk2hsl:function(t){return i(f(t))},cmyk2hsv:function(t){return n(f(t))},cmyk2hwb:function(t){return a(f(t))},cmyk2keyword:function(t){return s(f(t))},keyword2rgb:w,keyword2hsl:function(t){return i(w(t))},keyword2hsv:function(t){return n(w(t))},keyword2hwb:function(t){return a(w(t))},keyword2cmyk:function(t){return o(w(t))},keyword2lab:function(t){return d(w(t))},keyword2xyz:function(t){return l(w(t))},xyz2rgb:p,xyz2lab:m,xyz2lch:function(t){return x(m(t))},lab2xyz:v,lab2rgb:y,lab2lch:x,lch2lab:k,lch2xyz:function(t){return v(k(t))},lch2rgb:function(t){return y(k(t))}};function i(t){var e,i,n=t[0]/255,a=t[1]/255,o=t[2]/255,r=Math.min(n,a,o),s=Math.max(n,a,o),l=s-r;return s==r?e=0:n==s?e=(a-o)/l:a==s?e=2+(o-n)/l:o==s&&(e=4+(n-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),i=(r+s)/2,[e,100*(s==r?0:i<=.5?l/(s+r):l/(2-s-r)),100*i]}function n(t){var e,i,n=t[0],a=t[1],o=t[2],r=Math.min(n,a,o),s=Math.max(n,a,o),l=s-r;return i=0==s?0:l/s*1e3/10,s==r?e=0:n==s?e=(a-o)/l:a==s?e=2+(o-n)/l:o==s&&(e=4+(n-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),[e,i,s/255*1e3/10]}function a(t){var e=t[0],n=t[1],a=t[2];return[i(t)[0],100*(1/255*Math.min(e,Math.min(n,a))),100*(a=1-1/255*Math.max(e,Math.max(n,a)))]}function o(t){var e,i=t[0]/255,n=t[1]/255,a=t[2]/255;return[100*((1-i-(e=Math.min(1-i,1-n,1-a)))/(1-e)||0),100*((1-n-e)/(1-e)||0),100*((1-a-e)/(1-e)||0),100*e]}function s(t){return _[JSON.stringify(t)]}function l(t){var e=t[0]/255,i=t[1]/255,n=t[2]/255;return[100*(.4124*(e=e>.04045?Math.pow((e+.055)/1.055,2.4):e/12.92)+.3576*(i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92)+.1805*(n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92)),100*(.2126*e+.7152*i+.0722*n),100*(.0193*e+.1192*i+.9505*n)]}function d(t){var e=l(t),i=e[0],n=e[1],a=e[2];return n/=100,a/=108.883,i=(i/=95.047)>.008856?Math.pow(i,1/3):7.787*i+16/116,[116*(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116)-16,500*(i-n),200*(n-(a=a>.008856?Math.pow(a,1/3):7.787*a+16/116))]}function u(t){var e,i,n,a,o,r=t[0]/360,s=t[1]/100,l=t[2]/100;if(0==s)return[o=255*l,o,o];e=2*l-(i=l<.5?l*(1+s):l+s-l*s),a=[0,0,0];for(var d=0;d<3;d++)(n=r+1/3*-(d-1))<0&&n++,n>1&&n--,o=6*n<1?e+6*(i-e)*n:2*n<1?i:3*n<2?e+(i-e)*(2/3-n)*6:e,a[d]=255*o;return a}function h(t){var e=t[0]/60,i=t[1]/100,n=t[2]/100,a=Math.floor(e)%6,o=e-Math.floor(e),r=255*n*(1-i),s=255*n*(1-i*o),l=255*n*(1-i*(1-o));n*=255;switch(a){case 0:return[n,l,r];case 1:return[s,n,r];case 2:return[r,n,l];case 3:return[r,s,n];case 4:return[l,r,n];case 5:return[n,r,s]}}function c(t){var e,i,n,a,o=t[0]/360,s=t[1]/100,l=t[2]/100,d=s+l;switch(d>1&&(s/=d,l/=d),n=6*o-(e=Math.floor(6*o)),0!=(1&e)&&(n=1-n),a=s+n*((i=1-l)-s),e){default:case 6:case 0:r=i,g=a,b=s;break;case 1:r=a,g=i,b=s;break;case 2:r=s,g=i,b=a;break;case 3:r=s,g=a,b=i;break;case 4:r=a,g=s,b=i;break;case 5:r=i,g=s,b=a}return[255*r,255*g,255*b]}function f(t){var e=t[0]/100,i=t[1]/100,n=t[2]/100,a=t[3]/100;return[255*(1-Math.min(1,e*(1-a)+a)),255*(1-Math.min(1,i*(1-a)+a)),255*(1-Math.min(1,n*(1-a)+a))]}function p(t){var e,i,n,a=t[0]/100,o=t[1]/100,r=t[2]/100;return i=-.9689*a+1.8758*o+.0415*r,n=.0557*a+-.204*o+1.057*r,e=(e=3.2406*a+-1.5372*o+-.4986*r)>.0031308?1.055*Math.pow(e,1/2.4)-.055:e*=12.92,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:i*=12.92,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:n*=12.92,[255*(e=Math.min(Math.max(0,e),1)),255*(i=Math.min(Math.max(0,i),1)),255*(n=Math.min(Math.max(0,n),1))]}function m(t){var e=t[0],i=t[1],n=t[2];return i/=100,n/=108.883,e=(e/=95.047)>.008856?Math.pow(e,1/3):7.787*e+16/116,[116*(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116)-16,500*(e-i),200*(i-(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116))]}function v(t){var e,i,n,a,o=t[0],r=t[1],s=t[2];return o<=8?a=(i=100*o/903.3)/100*7.787+16/116:(i=100*Math.pow((o+16)/116,3),a=Math.pow(i/100,1/3)),[e=e/95.047<=.008856?e=95.047*(r/500+a-16/116)/7.787:95.047*Math.pow(r/500+a,3),i,n=n/108.883<=.008859?n=108.883*(a-s/200-16/116)/7.787:108.883*Math.pow(a-s/200,3)]}function x(t){var e,i=t[0],n=t[1],a=t[2];return(e=360*Math.atan2(a,n)/2/Math.PI)<0&&(e+=360),[i,Math.sqrt(n*n+a*a),e]}function y(t){return p(v(t))}function k(t){var e,i=t[0],n=t[1];return e=t[2]/360*2*Math.PI,[i,n*Math.cos(e),n*Math.sin(e)]}function w(t){return M[t]}var M={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},_={};for(var C in M)_[JSON.stringify(M[C])]=C;var S=function(){return new T};for(var P in e){S[P+"Raw"]=function(t){return function(i){return"number"==typeof i&&(i=Array.prototype.slice.call(arguments)),e[t](i)}}(P);var I=/(\w+)2(\w+)/.exec(P),A=I[1],D=I[2];(S[A]=S[A]||{})[D]=S[P]=function(t){return function(i){"number"==typeof i&&(i=Array.prototype.slice.call(arguments));var n=e[t](i);if("string"==typeof n||void 0===n)return n;for(var a=0;a=0&&e<1?H(Math.round(255*e)):"")},rgbString:function(t,e){if(e<1||t[3]&&t[3]<1)return N(t,e);return"rgb("+t[0]+", "+t[1]+", "+t[2]+")"},rgbaString:N,percentString:function(t,e){if(e<1||t[3]&&t[3]<1)return W(t,e);var i=Math.round(t[0]/255*100),n=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgb("+i+"%, "+n+"%, "+a+"%)"},percentaString:W,hslString:function(t,e){if(e<1||t[3]&&t[3]<1)return V(t,e);return"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"},hslaString:V,hwbString:function(t,e){void 0===e&&(e=void 0!==t[3]?t[3]:1);return"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"},keyword:function(t){return j[t.slice(0,3)]}};function O(t){if(t){var e=[0,0,0],i=1,n=t.match(/^#([a-fA-F0-9]{3,4})$/i),a="";if(n){a=(n=n[1])[3];for(var o=0;oi?(e+.05)/(i+.05):(i+.05)/(e+.05)},level:function(t){var e=this.contrast(t);return e>=7.1?"AAA":e>=4.5?"AA":""},dark:function(){var t=this.values.rgb;return(299*t[0]+587*t[1]+114*t[2])/1e3<128},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;e<3;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){var e=this.values.hsl;return e[2]+=e[2]*t,this.setValues("hsl",e),this},darken:function(t){var e=this.values.hsl;return e[2]-=e[2]*t,this.setValues("hsl",e),this},saturate:function(t){var e=this.values.hsl;return e[1]+=e[1]*t,this.setValues("hsl",e),this},desaturate:function(t){var e=this.values.hsl;return e[1]-=e[1]*t,this.setValues("hsl",e),this},whiten:function(t){var e=this.values.hwb;return e[1]+=e[1]*t,this.setValues("hwb",e),this},blacken:function(t){var e=this.values.hwb;return e[2]+=e[2]*t,this.setValues("hwb",e),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){var e=this.values.alpha;return this.setValues("alpha",e-e*t),this},opaquer:function(t){var e=this.values.alpha;return this.setValues("alpha",e+e*t),this},rotate:function(t){var e=this.values.hsl,i=(e[0]+t)%360;return e[0]=i<0?360+i:i,this.setValues("hsl",e),this},mix:function(t,e){var i=t,n=void 0===e?.5:e,a=2*n-1,o=this.alpha()-i.alpha(),r=((a*o==-1?a:(a+o)/(1+a*o))+1)/2,s=1-r;return this.rgb(r*this.red()+s*i.red(),r*this.green()+s*i.green(),r*this.blue()+s*i.blue()).alpha(this.alpha()*n+i.alpha()*(1-n))},toJSON:function(){return this.rgb()},clone:function(){var t,e,i=new Y,n=this.values,a=i.values;for(var o in n)n.hasOwnProperty(o)&&(t=n[o],"[object Array]"===(e={}.toString.call(t))?a[o]=t.slice(0):"[object Number]"===e?a[o]=t:console.error("unexpected color value:",t));return i}},Y.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},Y.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},Y.prototype.getValues=function(t){for(var e=this.values,i={},n=0;n=0;a--)e.call(i,t[a],a);else for(a=0;a=1?t:-(Math.sqrt(1-t*t)-1)},easeOutCirc:function(t){return Math.sqrt(1-(t-=1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:1===t?1:(i||(i=.3),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),-n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i))},easeOutElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:1===t?1:(i||(i=.3),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),n*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/i)+1)},easeInOutElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:2==(t/=.5)?1:(i||(i=.45),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),t<1?n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*-.5:n*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*.5+1)},easeInBack:function(t){var e=1.70158;return t*t*((e+1)*t-e)},easeOutBack:function(t){var e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:function(t){return 1-Z.easeOutBounce(1-t)},easeOutBounce:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},easeInOutBounce:function(t){return t<.5?.5*Z.easeInBounce(2*t):.5*Z.easeOutBounce(2*t-1)+.5}},$={effects:Z};G.easingEffects=Z;var J=Math.PI,Q=J/180,tt=2*J,et=J/2,it=J/4,nt=2*J/3,at={clear:function(t){t.ctx.clearRect(0,0,t.width,t.height)},roundedRect:function(t,e,i,n,a,o){if(o){var r=Math.min(o,a/2,n/2),s=e+r,l=i+r,d=e+n-r,u=i+a-r;t.moveTo(e,l),se.left-1e-6&&t.xe.top-1e-6&&t.y0&&this.requestAnimationFrame()},advance:function(){for(var t,e,i,n,a=this.animations,o=0;o=i?(ut.callback(t.onAnimationComplete,[t],e),e.animating=!1,a.splice(o,1)):++o}},xt=ut.options.resolve,yt=["push","pop","shift","splice","unshift"];function kt(t,e){var i=t._chartjs;if(i){var n=i.listeners,a=n.indexOf(e);-1!==a&&n.splice(a,1),n.length>0||(yt.forEach(function(e){delete t[e]}),delete t._chartjs)}}var wt=function(t,e){this.initialize(t,e)};ut.extend(wt.prototype,{datasetElementType:null,dataElementType:null,initialize:function(t,e){this.chart=t,this.index=e,this.linkScales(),this.addElements()},updateIndex:function(t){this.index=t},linkScales:function(){var t=this,e=t.getMeta(),i=t.getDataset();null!==e.xAxisID&&e.xAxisID in t.chart.scales||(e.xAxisID=i.xAxisID||t.chart.options.scales.xAxes[0].id),null!==e.yAxisID&&e.yAxisID in t.chart.scales||(e.yAxisID=i.yAxisID||t.chart.options.scales.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(t){return this.chart.scales[t]},_getValueScaleId:function(){return this.getMeta().yAxisID},_getIndexScaleId:function(){return this.getMeta().xAxisID},_getValueScale:function(){return this.getScaleForId(this._getValueScaleId())},_getIndexScale:function(){return this.getScaleForId(this._getIndexScaleId())},reset:function(){this.update(!0)},destroy:function(){this._data&&kt(this._data,this)},createMetaDataset:function(){var t=this.datasetElementType;return t&&new t({_chart:this.chart,_datasetIndex:this.index})},createMetaData:function(t){var e=this.dataElementType;return e&&new e({_chart:this.chart,_datasetIndex:this.index,_index:t})},addElements:function(){var t,e,i=this.getMeta(),n=this.getDataset().data||[],a=i.data;for(t=0,e=n.length;ti&&this.insertElements(i,n-i)},insertElements:function(t,e){for(var i=0;is;)a-=2*Math.PI;for(;a=r&&a<=s,d=o>=i.innerRadius&&o<=i.outerRadius;return l&&d}return!1},getCenterPoint:function(){var t=this._view,e=(t.startAngle+t.endAngle)/2,i=(t.innerRadius+t.outerRadius)/2;return{x:t.x+Math.cos(e)*i,y:t.y+Math.sin(e)*i}},getArea:function(){var t=this._view;return Math.PI*((t.endAngle-t.startAngle)/(2*Math.PI))*(Math.pow(t.outerRadius,2)-Math.pow(t.innerRadius,2))},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,i=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*i,y:t.y+Math.sin(e)*i}},draw:function(){var t,e=this._chart.ctx,i=this._view,n=i.startAngle,a=i.endAngle,o="inner"===i.borderAlign?.33:0;e.save(),e.beginPath(),e.arc(i.x,i.y,Math.max(i.outerRadius-o,0),n,a),e.arc(i.x,i.y,i.innerRadius,a,n,!0),e.closePath(),e.fillStyle=i.backgroundColor,e.fill(),i.borderWidth&&("inner"===i.borderAlign?(e.beginPath(),t=o/i.outerRadius,e.arc(i.x,i.y,i.outerRadius,n-t,a+t),i.innerRadius>o?(t=o/i.innerRadius,e.arc(i.x,i.y,i.innerRadius-o,a+t,n-t,!0)):e.arc(i.x,i.y,o,a+Math.PI/2,n-Math.PI/2),e.closePath(),e.clip(),e.beginPath(),e.arc(i.x,i.y,i.outerRadius,n,a),e.arc(i.x,i.y,i.innerRadius,a,n,!0),e.closePath(),e.lineWidth=2*i.borderWidth,e.lineJoin="round"):(e.lineWidth=i.borderWidth,e.lineJoin="bevel"),e.strokeStyle=i.borderColor,e.stroke()),e.restore()}}),Ct=ut.valueOrDefault,St=st.global.defaultColor;st._set("global",{elements:{line:{tension:.4,backgroundColor:St,borderWidth:3,borderColor:St,borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",capBezierPoints:!0,fill:!0}}});var Pt=pt.extend({draw:function(){var t,e,i,n,a=this._view,o=this._chart.ctx,r=a.spanGaps,s=this._children.slice(),l=st.global,d=l.elements.line,u=-1;for(this._loop&&s.length&&s.push(s[0]),o.save(),o.lineCap=a.borderCapStyle||d.borderCapStyle,o.setLineDash&&o.setLineDash(a.borderDash||d.borderDash),o.lineDashOffset=Ct(a.borderDashOffset,d.borderDashOffset),o.lineJoin=a.borderJoinStyle||d.borderJoinStyle,o.lineWidth=Ct(a.borderWidth,d.borderWidth),o.strokeStyle=a.borderColor||l.defaultColor,o.beginPath(),u=-1,t=0;tt.x&&(e=Ot(e,"left","right")):t.basei?i:n,r:l.right||a<0?0:a>e?e:a,b:l.bottom||o<0?0:o>i?i:o,l:l.left||r<0?0:r>e?e:r}}function Bt(t,e,i){var n=null===e,a=null===i,o=!(!t||n&&a)&&Rt(t);return o&&(n||e>=o.left&&e<=o.right)&&(a||i>=o.top&&i<=o.bottom)}st._set("global",{elements:{rectangle:{backgroundColor:Ft,borderColor:Ft,borderSkipped:"bottom",borderWidth:0}}});var Nt=pt.extend({draw:function(){var t=this._chart.ctx,e=this._view,i=function(t){var e=Rt(t),i=e.right-e.left,n=e.bottom-e.top,a=zt(t,i/2,n/2);return{outer:{x:e.left,y:e.top,w:i,h:n},inner:{x:e.left+a.l,y:e.top+a.t,w:i-a.l-a.r,h:n-a.t-a.b}}}(e),n=i.outer,a=i.inner;t.fillStyle=e.backgroundColor,t.fillRect(n.x,n.y,n.w,n.h),n.w===a.w&&n.h===a.h||(t.save(),t.beginPath(),t.rect(n.x,n.y,n.w,n.h),t.clip(),t.fillStyle=e.borderColor,t.rect(a.x,a.y,a.w,a.h),t.fill("evenodd"),t.restore())},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){return Bt(this._view,t,e)},inLabelRange:function(t,e){var i=this._view;return Lt(i)?Bt(i,t,null):Bt(i,null,e)},inXRange:function(t){return Bt(this._view,t,null)},inYRange:function(t){return Bt(this._view,null,t)},getCenterPoint:function(){var t,e,i=this._view;return Lt(i)?(t=i.x,e=(i.y+i.base)/2):(t=(i.x+i.base)/2,e=i.y),{x:t,y:e}},getArea:function(){var t=this._view;return Lt(t)?t.width*Math.abs(t.y-t.base):t.height*Math.abs(t.x-t.base)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}}),Wt={},Vt=_t,Et=Pt,Ht=Tt,jt=Nt;Wt.Arc=Vt,Wt.Line=Et,Wt.Point=Ht,Wt.Rectangle=jt;var qt=ut.options.resolve;st._set("bar",{hover:{mode:"label"},scales:{xAxes:[{type:"category",categoryPercentage:.8,barPercentage:.9,offset:!0,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}});var Yt=Mt.extend({dataElementType:Wt.Rectangle,initialize:function(){var t;Mt.prototype.initialize.apply(this,arguments),(t=this.getMeta()).stack=this.getDataset().stack,t.bar=!0},update:function(t){var e,i,n=this.getMeta().data;for(this._ruler=this.getRuler(),e=0,i=n.length;e0?Math.min(r,n-i):r,i=n;return r}(i,l):-1,pixels:l,start:r,end:s,stackCount:n,scale:i}},calculateBarValuePixels:function(t,e){var i,n,a,o,r,s,l=this.chart,d=this.getMeta(),u=this._getValueScale(),h=u.isHorizontal(),c=l.data.datasets,f=+u.getRightValue(c[t].data[e]),g=u.options.minBarLength,p=u.options.stacked,m=d.stack,v=0;if(p||void 0===p&&void 0!==m)for(i=0;i=0&&a>0)&&(v+=a));return o=u.getPixelForValue(v),s=(r=u.getPixelForValue(v+f))-o,void 0!==g&&Math.abs(s)=0&&!h||f<0&&h?o-g:o+g),{size:s,base:o,head:r,center:r+s/2}},calculateBarIndexPixels:function(t,e,i){var n=i.scale.options,a="flex"===n.barThickness?function(t,e,i){var n,a=e.pixels,o=a[t],r=t>0?a[t-1]:null,s=t');var i=t.data,n=i.datasets,a=i.labels;if(n.length)for(var o=0;o'),a[o]&&e.push(a[o]),e.push("");return e.push(""),e.join("")},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map(function(i,n){var a=t.getDatasetMeta(0),o=e.datasets[0],r=a.data[n],s=r&&r.custom||{},l=t.options.elements.arc;return{text:i,fillStyle:Gt([s.backgroundColor,o.backgroundColor,l.backgroundColor],void 0,n),strokeStyle:Gt([s.borderColor,o.borderColor,l.borderColor],void 0,n),lineWidth:Gt([s.borderWidth,o.borderWidth,l.borderWidth],void 0,n),hidden:isNaN(o.data[n])||a.data[n].hidden,index:n}}):[]}},onClick:function(t,e){var i,n,a,o=e.index,r=this.chart;for(i=0,n=(r.data.datasets||[]).length;i=Math.PI?-1:m<-Math.PI?1:0))+g,b={x:Math.cos(m),y:Math.sin(m)},x={x:Math.cos(v),y:Math.sin(v)},y=m<=0&&v>=0||m<=2*Math.PI&&2*Math.PI<=v,k=m<=.5*Math.PI&&.5*Math.PI<=v||m<=2.5*Math.PI&&2.5*Math.PI<=v,w=m<=-Math.PI&&-Math.PI<=v||m<=Math.PI&&Math.PI<=v,M=m<=.5*-Math.PI&&.5*-Math.PI<=v||m<=1.5*Math.PI&&1.5*Math.PI<=v,_=f/100,C={x:w?-1:Math.min(b.x*(b.x<0?1:_),x.x*(x.x<0?1:_)),y:M?-1:Math.min(b.y*(b.y<0?1:_),x.y*(x.y<0?1:_))},S={x:y?1:Math.max(b.x*(b.x>0?1:_),x.x*(x.x>0?1:_)),y:k?1:Math.max(b.y*(b.y>0?1:_),x.y*(x.y>0?1:_))},P={width:.5*(S.x-C.x),height:.5*(S.y-C.y)};d=Math.min(s/P.width,l/P.height),u={x:-.5*(S.x+C.x),y:-.5*(S.y+C.y)}}for(e=0,i=c.length;e0&&!isNaN(t)?2*Math.PI*(Math.abs(t)/e):0},getMaxBorderWidth:function(t){var e,i,n,a,o,r,s,l,d=0,u=this.chart;if(!t)for(e=0,i=u.data.datasets.length;e(d=s>d?s:d)?l:d);return d},setHoverStyle:function(t){var e=t._model,i=t._options,n=ut.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth},e.backgroundColor=Zt(i.hoverBackgroundColor,n(i.backgroundColor)),e.borderColor=Zt(i.hoverBorderColor,n(i.borderColor)),e.borderWidth=Zt(i.hoverBorderWidth,i.borderWidth)},_resolveElementOptions:function(t,e){var i,n,a,o=this.chart,r=this.getDataset(),s=t.custom||{},l=o.options.elements.arc,d={},u={chart:o,dataIndex:e,dataset:r,datasetIndex:this.index},h=["backgroundColor","borderColor","borderWidth","borderAlign","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth"];for(i=0,n=h.length;i0&&ee(l[t-1]._model,s)&&(i.controlPointPreviousX=d(i.controlPointPreviousX,s.left,s.right),i.controlPointPreviousY=d(i.controlPointPreviousY,s.top,s.bottom)),t');var i=t.data,n=i.datasets,a=i.labels;if(n.length)for(var o=0;o'),a[o]&&e.push(a[o]),e.push("");return e.push(""),e.join("")},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map(function(i,n){var a=t.getDatasetMeta(0),o=e.datasets[0],r=a.data[n].custom||{},s=t.options.elements.arc;return{text:i,fillStyle:ae([r.backgroundColor,o.backgroundColor,s.backgroundColor],void 0,n),strokeStyle:ae([r.borderColor,o.borderColor,s.borderColor],void 0,n),lineWidth:ae([r.borderWidth,o.borderWidth,s.borderWidth],void 0,n),hidden:isNaN(o.data[n])||a.data[n].hidden,index:n}}):[]}},onClick:function(t,e){var i,n,a,o=e.index,r=this.chart;for(i=0,n=(r.data.datasets||[]).length;i0&&(o=t.getDatasetMeta(o[0]._datasetIndex).data),o},"x-axis":function(t,e){return me(t,e,{intersect:!1})},point:function(t,e){return fe(t,he(e,t))},nearest:function(t,e,i){var n=he(e,t);i.axis=i.axis||"xy";var a=pe(i.axis);return ge(t,n,i.intersect,a)},x:function(t,e,i){var n=he(e,t),a=[],o=!1;return ce(t,function(t){t.inXRange(n.x)&&a.push(t),t.inRange(n.x,n.y)&&(o=!0)}),i.intersect&&!o&&(a=[]),a},y:function(t,e,i){var n=he(e,t),a=[],o=!1;return ce(t,function(t){t.inYRange(n.y)&&a.push(t),t.inRange(n.x,n.y)&&(o=!0)}),i.intersect&&!o&&(a=[]),a}}};function be(t,e){return ut.where(t,function(t){return t.position===e})}function xe(t,e){t.forEach(function(t,e){return t._tmpIndex_=e,t}),t.sort(function(t,i){var n=e?i:t,a=e?t:i;return n.weight===a.weight?n._tmpIndex_-a._tmpIndex_:n.weight-a.weight}),t.forEach(function(t){delete t._tmpIndex_})}function ye(t,e){ut.each(t,function(t){e[t.position]+=t.isHorizontal()?t.height:t.width})}st._set("global",{layout:{padding:{top:0,right:0,bottom:0,left:0}}});var ke={defaults:{},addBox:function(t,e){t.boxes||(t.boxes=[]),e.fullWidth=e.fullWidth||!1,e.position=e.position||"top",e.weight=e.weight||0,t.boxes.push(e)},removeBox:function(t,e){var i=t.boxes?t.boxes.indexOf(e):-1;-1!==i&&t.boxes.splice(i,1)},configure:function(t,e,i){for(var n,a=["fullWidth","position","weight"],o=a.length,r=0;rdiv{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}"}))&&we.default||we,_e="$chartjs",Ce="chartjs-size-monitor",Se="chartjs-render-monitor",Pe="chartjs-render-animation",Ie=["animationstart","webkitAnimationStart"],Ae={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};function De(t,e){var i=ut.getStyle(t,e),n=i&&i.match(/^(\d+)(\.\d+)?px$/);return n?Number(n[1]):void 0}var Te=!!function(){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("e",null,e)}catch(t){}return t}()&&{passive:!0};function Fe(t,e,i){t.addEventListener(e,i,Te)}function Le(t,e,i){t.removeEventListener(e,i,Te)}function Re(t,e,i,n,a){return{type:t,chart:e,native:a||null,x:void 0!==i?i:null,y:void 0!==n?n:null}}function Oe(t){var e=document.createElement("div");return e.className=t||"",e}function ze(t,e,i){var n,a,o,r,s=t[_e]||(t[_e]={}),l=s.resizer=function(t){var e=Oe(Ce),i=Oe(Ce+"-expand"),n=Oe(Ce+"-shrink");i.appendChild(Oe()),n.appendChild(Oe()),e.appendChild(i),e.appendChild(n),e._reset=function(){i.scrollLeft=1e6,i.scrollTop=1e6,n.scrollLeft=1e6,n.scrollTop=1e6};var a=function(){e._reset(),t()};return Fe(i,"scroll",a.bind(i,"expand")),Fe(n,"scroll",a.bind(n,"shrink")),e}((n=function(){if(s.resizer){var n=i.options.maintainAspectRatio&&t.parentNode,a=n?n.clientWidth:0;e(Re("resize",i)),n&&n.clientWidth0){var o=t[0];o.label?i=o.label:o.xLabel?i=o.xLabel:a>0&&o.index-1?t.split("\n"):t}function Xe(t){var e=st.global;return{xPadding:t.xPadding,yPadding:t.yPadding,xAlign:t.xAlign,yAlign:t.yAlign,bodyFontColor:t.bodyFontColor,_bodyFontFamily:je(t.bodyFontFamily,e.defaultFontFamily),_bodyFontStyle:je(t.bodyFontStyle,e.defaultFontStyle),_bodyAlign:t.bodyAlign,bodyFontSize:je(t.bodyFontSize,e.defaultFontSize),bodySpacing:t.bodySpacing,titleFontColor:t.titleFontColor,_titleFontFamily:je(t.titleFontFamily,e.defaultFontFamily),_titleFontStyle:je(t.titleFontStyle,e.defaultFontStyle),titleFontSize:je(t.titleFontSize,e.defaultFontSize),_titleAlign:t.titleAlign,titleSpacing:t.titleSpacing,titleMarginBottom:t.titleMarginBottom,footerFontColor:t.footerFontColor,_footerFontFamily:je(t.footerFontFamily,e.defaultFontFamily),_footerFontStyle:je(t.footerFontStyle,e.defaultFontStyle),footerFontSize:je(t.footerFontSize,e.defaultFontSize),_footerAlign:t.footerAlign,footerSpacing:t.footerSpacing,footerMarginTop:t.footerMarginTop,caretSize:t.caretSize,cornerRadius:t.cornerRadius,backgroundColor:t.backgroundColor,opacity:0,legendColorBackground:t.multiKeyBackground,displayColors:t.displayColors,borderColor:t.borderColor,borderWidth:t.borderWidth}}function Ke(t,e){return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-t.xPadding:t.x+t.xPadding}function Ge(t){return Ye([],Ue(t))}var Ze=pt.extend({initialize:function(){this._model=Xe(this._options),this._lastActive=[]},getTitle:function(){var t=this._options.callbacks,e=t.beforeTitle.apply(this,arguments),i=t.title.apply(this,arguments),n=t.afterTitle.apply(this,arguments),a=[];return a=Ye(a,Ue(e)),a=Ye(a,Ue(i)),a=Ye(a,Ue(n))},getBeforeBody:function(){return Ge(this._options.callbacks.beforeBody.apply(this,arguments))},getBody:function(t,e){var i=this,n=i._options.callbacks,a=[];return ut.each(t,function(t){var o={before:[],lines:[],after:[]};Ye(o.before,Ue(n.beforeLabel.call(i,t,e))),Ye(o.lines,n.label.call(i,t,e)),Ye(o.after,Ue(n.afterLabel.call(i,t,e))),a.push(o)}),a},getAfterBody:function(){return Ge(this._options.callbacks.afterBody.apply(this,arguments))},getFooter:function(){var t=this._options.callbacks,e=t.beforeFooter.apply(this,arguments),i=t.footer.apply(this,arguments),n=t.afterFooter.apply(this,arguments),a=[];return a=Ye(a,Ue(e)),a=Ye(a,Ue(i)),a=Ye(a,Ue(n))},update:function(t){var e,i,n,a,o,r,s,l,d,u,h=this,c=h._options,f=h._model,g=h._model=Xe(c),p=h._active,m=h._data,v={xAlign:f.xAlign,yAlign:f.yAlign},b={x:f.x,y:f.y},x={width:f.width,height:f.height},y={x:f.caretX,y:f.caretY};if(p.length){g.opacity=1;var k=[],w=[];y=qe[c.position].call(h,p,h._eventPosition);var M=[];for(e=0,i=p.length;en.width&&(a=n.width-e.width),a<0&&(a=0)),"top"===u?o+=h:o-="bottom"===u?e.height+h:e.height/2,"center"===u?"left"===d?a+=h:"right"===d&&(a-=h):"left"===d?a-=c:"right"===d&&(a+=c),{x:a,y:o}}(g,x,v=function(t,e){var i,n,a,o,r,s=t._model,l=t._chart,d=t._chart.chartArea,u="center",h="center";s.yl.height-e.height&&(h="bottom");var c=(d.left+d.right)/2,f=(d.top+d.bottom)/2;"center"===h?(i=function(t){return t<=c},n=function(t){return t>c}):(i=function(t){return t<=e.width/2},n=function(t){return t>=l.width-e.width/2}),a=function(t){return t+e.width+s.caretSize+s.caretPadding>l.width},o=function(t){return t-e.width-s.caretSize-s.caretPadding<0},r=function(t){return t<=f?"top":"bottom"},i(s.x)?(u="left",a(s.x)&&(u="center",h=r(s.y))):n(s.x)&&(u="right",o(s.x)&&(u="center",h=r(s.y)));var g=t._options;return{xAlign:g.xAlign?g.xAlign:u,yAlign:g.yAlign?g.yAlign:h}}(this,x),h._chart)}else g.opacity=0;return g.xAlign=v.xAlign,g.yAlign=v.yAlign,g.x=b.x,g.y=b.y,g.width=x.width,g.height=x.height,g.caretX=y.x,g.caretY=y.y,h._model=g,t&&c.custom&&c.custom.call(h,g),h},drawCaret:function(t,e){var i=this._chart.ctx,n=this._view,a=this.getCaretPosition(t,e,n);i.lineTo(a.x1,a.y1),i.lineTo(a.x2,a.y2),i.lineTo(a.x3,a.y3)},getCaretPosition:function(t,e,i){var n,a,o,r,s,l,d=i.caretSize,u=i.cornerRadius,h=i.xAlign,c=i.yAlign,f=t.x,g=t.y,p=e.width,m=e.height;if("center"===c)s=g+m/2,"left"===h?(a=(n=f)-d,o=n,r=s+d,l=s-d):(a=(n=f+p)+d,o=n,r=s-d,l=s+d);else if("left"===h?(n=(a=f+u+d)-d,o=a+d):"right"===h?(n=(a=f+p-u-d)-d,o=a+d):(n=(a=i.caretX)-d,o=a+d),"top"===c)s=(r=g)-d,l=r;else{s=(r=g+m)+d,l=r;var v=o;o=n,n=v}return{x1:n,x2:a,x3:o,y1:r,y2:s,y3:l}},drawTitle:function(t,e,i){var n=e.title;if(n.length){t.x=Ke(e,e._titleAlign),i.textAlign=e._titleAlign,i.textBaseline="top";var a,o,r=e.titleFontSize,s=e.titleSpacing;for(i.fillStyle=e.titleFontColor,i.font=ut.fontString(r,e._titleFontStyle,e._titleFontFamily),a=0,o=n.length;a0&&i.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var i={width:e.width,height:e.height},n={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,o=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&o&&(t.save(),t.globalAlpha=a,this.drawBackground(n,e,t,i),n.y+=e.yPadding,this.drawTitle(n,e,t),this.drawBody(n,e,t),this.drawFooter(n,e,t),t.restore())}},handleEvent:function(t){var e,i=this,n=i._options;return i._lastActive=i._lastActive||[],"mouseout"===t.type?i._active=[]:i._active=i._chart.getElementsAtEventForMode(t,n.mode,n),(e=!ut.arrayEquals(i._active,i._lastActive))&&(i._lastActive=i._active,(n.enabled||n.custom)&&(i._eventPosition={x:t.x,y:t.y},i.update(!0),i.pivot())),e}}),$e=qe,Je=Ze;Je.positioners=$e;var Qe=ut.valueOrDefault;function ti(){return ut.merge({},[].slice.call(arguments),{merger:function(t,e,i,n){if("xAxes"===t||"yAxes"===t){var a,o,r,s=i[t].length;for(e[t]||(e[t]=[]),a=0;a=e[t].length&&e[t].push({}),!e[t][a].type||r.type&&r.type!==e[t][a].type?ut.merge(e[t][a],[He.getScaleDefaults(o),r]):ut.merge(e[t][a],r)}else ut._merger(t,e,i,n)}})}function ei(){return ut.merge({},[].slice.call(arguments),{merger:function(t,e,i,n){var a=e[t]||{},o=i[t];"scales"===t?e[t]=ti(a,o):"scale"===t?e[t]=ut.merge(a,[He.getScaleDefaults(o.type),o]):ut._merger(t,e,i,n)}})}function ii(t){return"top"===t||"bottom"===t}st._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var ni=function(t,e){return this.construct(t,e),this};ut.extend(ni.prototype,{construct:function(t,e){var i=this;e=function(t){var e=(t=t||{}).data=t.data||{};return e.datasets=e.datasets||[],e.labels=e.labels||[],t.options=ei(st.global,st[t.type],t.options||{}),t}(e);var n=Ve.acquireContext(t,e),a=n&&n.canvas,o=a&&a.height,r=a&&a.width;i.id=ut.uid(),i.ctx=n,i.canvas=a,i.config=e,i.width=r,i.height=o,i.aspectRatio=o?r/o:null,i.options=e.options,i._bufferedRender=!1,i.chart=i,i.controller=i,ni.instances[i.id]=i,Object.defineProperty(i,"data",{get:function(){return i.config.data},set:function(t){i.config.data=t}}),n&&a?(i.initialize(),i.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return Ee.notify(t,"beforeInit"),ut.retinaScale(t,t.options.devicePixelRatio),t.bindEvents(),t.options.responsive&&t.resize(!0),t.ensureScalesHaveIDs(),t.buildOrUpdateScales(),t.initToolTip(),Ee.notify(t,"afterInit"),t},clear:function(){return ut.canvas.clear(this),this},stop:function(){return bt.cancelAnimation(this),this},resize:function(t){var e=this,i=e.options,n=e.canvas,a=i.maintainAspectRatio&&e.aspectRatio||null,o=Math.max(0,Math.floor(ut.getMaximumWidth(n))),r=Math.max(0,Math.floor(a?o/a:ut.getMaximumHeight(n)));if((e.width!==o||e.height!==r)&&(n.width=e.width=o,n.height=e.height=r,n.style.width=o+"px",n.style.height=r+"px",ut.retinaScale(e,i.devicePixelRatio),!t)){var s={width:o,height:r};Ee.notify(e,"resize",[s]),i.onResize&&i.onResize(e,s),e.stop(),e.update({duration:i.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},i=t.scale;ut.each(e.xAxes,function(t,e){t.id=t.id||"x-axis-"+e}),ut.each(e.yAxes,function(t,e){t.id=t.id||"y-axis-"+e}),i&&(i.id=i.id||"scale")},buildOrUpdateScales:function(){var t=this,e=t.options,i=t.scales||{},n=[],a=Object.keys(i).reduce(function(t,e){return t[e]=!1,t},{});e.scales&&(n=n.concat((e.scales.xAxes||[]).map(function(t){return{options:t,dtype:"category",dposition:"bottom"}}),(e.scales.yAxes||[]).map(function(t){return{options:t,dtype:"linear",dposition:"left"}}))),e.scale&&n.push({options:e.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),ut.each(n,function(e){var n=e.options,o=n.id,r=Qe(n.type,e.dtype);ii(n.position)!==ii(e.dposition)&&(n.position=e.dposition),a[o]=!0;var s=null;if(o in i&&i[o].type===r)(s=i[o]).options=n,s.ctx=t.ctx,s.chart=t;else{var l=He.getScaleConstructor(r);if(!l)return;s=new l({id:o,type:r,options:n,ctx:t.ctx,chart:t}),i[s.id]=s}s.mergeTicksOptions(),e.isDefault&&(t.scale=s)}),ut.each(a,function(t,e){t||delete i[e]}),t.scales=i,He.addScalesToLayout(this)},buildOrUpdateControllers:function(){var t=this,e=[];return ut.each(t.data.datasets,function(i,n){var a=t.getDatasetMeta(n),o=i.type||t.config.type;if(a.type&&a.type!==o&&(t.destroyDatasetMeta(n),a=t.getDatasetMeta(n)),a.type=o,a.controller)a.controller.updateIndex(n),a.controller.linkScales();else{var r=ue[a.type];if(void 0===r)throw new Error('"'+a.type+'" is not a chart type.');a.controller=new r(t,n),e.push(a.controller)}},t),e},resetElements:function(){var t=this;ut.each(t.data.datasets,function(e,i){t.getDatasetMeta(i).controller.reset()},t)},reset:function(){this.resetElements(),this.tooltip.initialize()},update:function(t){var e,i,n=this;if(t&&"object"==typeof t||(t={duration:t,lazy:arguments[1]}),i=(e=n).options,ut.each(e.scales,function(t){ke.removeBox(e,t)}),i=ei(st.global,st[e.config.type],i),e.options=e.config.options=i,e.ensureScalesHaveIDs(),e.buildOrUpdateScales(),e.tooltip._options=i.tooltips,e.tooltip.initialize(),Ee._invalidate(n),!1!==Ee.notify(n,"beforeUpdate")){n.tooltip._data=n.data;var a=n.buildOrUpdateControllers();ut.each(n.data.datasets,function(t,e){n.getDatasetMeta(e).controller.buildOrUpdateElements()},n),n.updateLayout(),n.options.animation&&n.options.animation.duration&&ut.each(a,function(t){t.reset()}),n.updateDatasets(),n.tooltip.initialize(),n.lastActive=[],Ee.notify(n,"afterUpdate"),n._bufferedRender?n._bufferedRequest={duration:t.duration,easing:t.easing,lazy:t.lazy}:n.render(t)}},updateLayout:function(){!1!==Ee.notify(this,"beforeLayout")&&(ke.update(this,this.width,this.height),Ee.notify(this,"afterScaleUpdate"),Ee.notify(this,"afterLayout"))},updateDatasets:function(){if(!1!==Ee.notify(this,"beforeDatasetsUpdate")){for(var t=0,e=this.data.datasets.length;t=0;--i)e.isDatasetVisible(i)&&e.drawDataset(i,t);Ee.notify(e,"afterDatasetsDraw",[t])}},drawDataset:function(t,e){var i=this.getDatasetMeta(t),n={meta:i,index:t,easingValue:e};!1!==Ee.notify(this,"beforeDatasetDraw",[n])&&(i.controller.draw(e),Ee.notify(this,"afterDatasetDraw",[n]))},_drawTooltip:function(t){var e=this.tooltip,i={tooltip:e,easingValue:t};!1!==Ee.notify(this,"beforeTooltipDraw",[i])&&(e.draw(),Ee.notify(this,"afterTooltipDraw",[i]))},getElementAtEvent:function(t){return ve.modes.single(this,t)},getElementsAtEvent:function(t){return ve.modes.label(this,t,{intersect:!0})},getElementsAtXAxis:function(t){return ve.modes["x-axis"](this,t,{intersect:!0})},getElementsAtEventForMode:function(t,e,i){var n=ve.modes[e];return"function"==typeof n?n(this,t,i):[]},getDatasetAtEvent:function(t){return ve.modes.dataset(this,t,{intersect:!0})},getDatasetMeta:function(t){var e=this.data.datasets[t];e._meta||(e._meta={});var i=e._meta[this.id];return i||(i=e._meta[this.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null}),i},getVisibleDatasetCount:function(){for(var t=0,e=0,i=this.data.datasets.length;e3?i[2]-i[1]:i[1]-i[0];Math.abs(n)>1&&t!==Math.floor(t)&&(n=t-Math.floor(t));var a=ut.log10(Math.abs(n)),o="";if(0!==t)if(Math.max(Math.abs(i[0]),Math.abs(i[i.length-1]))<1e-4){var r=ut.log10(Math.abs(t));o=t.toExponential(Math.floor(r)-Math.floor(a))}else{var s=-1*Math.floor(a);s=Math.max(Math.min(s,20),0),o=t.toFixed(s)}else o="0";return o},logarithmic:function(t,e,i){var n=t/Math.pow(10,Math.floor(ut.log10(t)));return 0===t?"0":1===n||2===n||5===n||0===e||e===i.length-1?t.toExponential():""}}},di=ut.valueOrDefault,ui=ut.valueAtIndexOrDefault;function hi(t){var e,i,n=[];for(e=0,i=t.length;ed&&ot.maxHeight){o--;break}o++,l=r*s}t.labelRotation=o},afterCalculateTickRotation:function(){ut.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){ut.callback(this.options.beforeFit,[this])},fit:function(){var t=this,e=t.minSize={width:0,height:0},i=hi(t._ticks),n=t.options,a=n.ticks,o=n.scaleLabel,r=n.gridLines,s=t._isVisible(),l=n.position,d=t.isHorizontal(),u=ut.options._parseFont,h=u(a),c=n.gridLines.tickMarkLength;if(e.width=d?t.isFullWidth()?t.maxWidth-t.margins.left-t.margins.right:t.maxWidth:s&&r.drawTicks?c:0,e.height=d?s&&r.drawTicks?c:0:t.maxHeight,o.display&&s){var f=u(o),g=ut.options.toPadding(o.padding),p=f.lineHeight+g.height;d?e.height+=p:e.width+=p}if(a.display&&s){var m=ut.longestText(t.ctx,h.string,i,t.longestTextCache),v=ut.numberOfLabelLines(i),b=.5*h.size,x=t.options.ticks.padding;if(t._maxLabelLines=v,t.longestLabelWidth=m,d){var y=ut.toRadians(t.labelRotation),k=Math.cos(y),w=Math.sin(y)*m+h.lineHeight*v+b;e.height=Math.min(t.maxHeight,e.height+w+x),t.ctx.font=h.string;var M,_,C=ci(t.ctx,i[0],h.string),S=ci(t.ctx,i[i.length-1],h.string),P=t.getPixelForTick(0)-t.left,I=t.right-t.getPixelForTick(i.length-1);0!==t.labelRotation?(M="bottom"===l?k*C:k*b,_="bottom"===l?k*b:k*S):(M=C/2,_=S/2),t.paddingLeft=Math.max(M-P,0)+3,t.paddingRight=Math.max(_-I,0)+3}else a.mirror?m=0:m+=x+b,e.width=Math.min(t.maxWidth,e.width+m),t.paddingTop=h.size/2,t.paddingBottom=h.size/2}t.handleMargins(),t.width=e.width,t.height=e.height},handleMargins:function(){var t=this;t.margins&&(t.paddingLeft=Math.max(t.paddingLeft-t.margins.left,0),t.paddingTop=Math.max(t.paddingTop-t.margins.top,0),t.paddingRight=Math.max(t.paddingRight-t.margins.right,0),t.paddingBottom=Math.max(t.paddingBottom-t.margins.bottom,0))},afterFit:function(){ut.callback(this.options.afterFit,[this])},isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(t){if(ut.isNullOrUndef(t))return NaN;if(("number"==typeof t||t instanceof Number)&&!isFinite(t))return NaN;if(t)if(this.isHorizontal()){if(void 0!==t.x)return this.getRightValue(t.x)}else if(void 0!==t.y)return this.getRightValue(t.y);return t},getLabelForIndex:ut.noop,getPixelForValue:ut.noop,getValueForPixel:ut.noop,getPixelForTick:function(t){var e=this,i=e.options.offset;if(e.isHorizontal()){var n=(e.width-(e.paddingLeft+e.paddingRight))/Math.max(e._ticks.length-(i?0:1),1),a=n*t+e.paddingLeft;i&&(a+=n/2);var o=e.left+a;return o+=e.isFullWidth()?e.margins.left:0}var r=e.height-(e.paddingTop+e.paddingBottom);return e.top+t*(r/(e._ticks.length-1))},getPixelForDecimal:function(t){var e=this;if(e.isHorizontal()){var i=(e.width-(e.paddingLeft+e.paddingRight))*t+e.paddingLeft,n=e.left+i;return n+=e.isFullWidth()?e.margins.left:0}return e.top+t*e.height},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var t=this.min,e=this.max;return this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0},_autoSkip:function(t){var e,i,n=this,a=n.isHorizontal(),o=n.options.ticks.minor,r=t.length,s=!1,l=o.maxTicksLimit,d=n._tickSize()*(r-1),u=a?n.width-(n.paddingLeft+n.paddingRight):n.height-(n.paddingTop+n.PaddingBottom),h=[];for(d>u&&(s=1+Math.floor(d/u)),r>l&&(s=Math.max(s,1+Math.floor(r/l))),e=0;e1&&e%s>0&&delete i.label,h.push(i);return h},_tickSize:function(){var t=this,e=t.isHorizontal(),i=t.options.ticks.minor,n=ut.toRadians(t.labelRotation),a=Math.abs(Math.cos(n)),o=Math.abs(Math.sin(n)),r=i.autoSkipPadding||0,s=t.longestLabelWidth+r||0,l=ut.options._parseFont(i),d=t._maxLabelLines*l.lineHeight+r||0;return e?d*a>s*o?s/a:d/o:d*o0&&n>0&&(t.min=0)}var a=void 0!==e.min||void 0!==e.suggestedMin,o=void 0!==e.max||void 0!==e.suggestedMax;void 0!==e.min?t.min=e.min:void 0!==e.suggestedMin&&(null===t.min?t.min=e.suggestedMin:t.min=Math.min(t.min,e.suggestedMin)),void 0!==e.max?t.max=e.max:void 0!==e.suggestedMax&&(null===t.max?t.max=e.suggestedMax:t.max=Math.max(t.max,e.suggestedMax)),a!==o&&t.min>=t.max&&(a?t.max=t.min+1:t.min=t.max-1),t.min===t.max&&(t.max++,e.beginAtZero||t.min--)},getTickLimit:function(){var t,e=this.options.ticks,i=e.stepSize,n=e.maxTicksLimit;return i?t=Math.ceil(this.max/i)-Math.floor(this.min/i)+1:(t=this._computeTickLimit(),n=n||11),n&&(t=Math.min(n,t)),t},_computeTickLimit:function(){return Number.POSITIVE_INFINITY},handleDirectionalChanges:mi,buildTicks:function(){var t=this,e=t.options.ticks,i=t.getTickLimit(),n={maxTicks:i=Math.max(2,i),min:e.min,max:e.max,precision:e.precision,stepSize:ut.valueOrDefault(e.fixedStepSize,e.stepSize)},a=t.ticks=function(t,e){var i,n,a,o,r=[],s=t.stepSize,l=s||1,d=t.maxTicks-1,u=t.min,h=t.max,c=t.precision,f=e.min,g=e.max,p=ut.niceNum((g-f)/d/l)*l;if(p<1e-14&&vi(u)&&vi(h))return[f,g];(o=Math.ceil(g/p)-Math.floor(f/p))>d&&(p=ut.niceNum(o*p/d/l)*l),s||vi(c)?i=Math.pow(10,ut._decimalPlaces(p)):(i=Math.pow(10,c),p=Math.ceil(p*i)/i),n=Math.floor(f/p)*p,a=Math.ceil(g/p)*p,s&&(!vi(u)&&ut.almostWhole(u/p,p/1e3)&&(n=u),!vi(h)&&ut.almostWhole(h/p,p/1e3)&&(a=h)),o=(a-n)/p,o=ut.almostEquals(o,Math.round(o),p/1e3)?Math.round(o):Math.ceil(o),n=Math.round(n*i)/i,a=Math.round(a*i)/i,r.push(vi(u)?n:u);for(var m=1;mt.max&&(t.max=n))})});t.min=isFinite(t.min)&&!isNaN(t.min)?t.min:0,t.max=isFinite(t.max)&&!isNaN(t.max)?t.max:1,this.handleTickRangeOptions()},_computeTickLimit:function(){var t;return this.isHorizontal()?Math.ceil(this.width/40):(t=ut.options._parseFont(this.options.ticks),Math.ceil(this.height/t.lineHeight))},handleDirectionalChanges:function(){this.isHorizontal()||this.ticks.reverse()},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},getPixelForValue:function(t){var e=this,i=e.start,n=+e.getRightValue(t),a=e.end-i;return e.isHorizontal()?e.left+e.width/a*(n-i):e.bottom-e.height/a*(n-i)},getValueForPixel:function(t){var e=this,i=e.isHorizontal(),n=i?e.width:e.height,a=(i?t-e.left:e.bottom-t)/n;return e.start+(e.end-e.start)*a},getPixelForTick:function(t){return this.getPixelForValue(this.ticksAsNumbers[t])}}),ki=xi;yi._defaults=ki;var wi=ut.valueOrDefault;var Mi={position:"left",ticks:{callback:li.formatters.logarithmic}};function _i(t,e){return ut.isFinite(t)&&t>=0?t:e}var Ci=fi.extend({determineDataLimits:function(){var t=this,e=t.options,i=t.chart,n=i.data.datasets,a=t.isHorizontal();function o(e){return a?e.xAxisID===t.id:e.yAxisID===t.id}t.min=null,t.max=null,t.minNotZero=null;var r=e.stacked;if(void 0===r&&ut.each(n,function(t,e){if(!r){var n=i.getDatasetMeta(e);i.isDatasetVisible(e)&&o(n)&&void 0!==n.stack&&(r=!0)}}),e.stacked||r){var s={};ut.each(n,function(n,a){var r=i.getDatasetMeta(a),l=[r.type,void 0===e.stacked&&void 0===r.stack?a:"",r.stack].join(".");i.isDatasetVisible(a)&&o(r)&&(void 0===s[l]&&(s[l]=[]),ut.each(n.data,function(e,i){var n=s[l],a=+t.getRightValue(e);isNaN(a)||r.data[i].hidden||a<0||(n[i]=n[i]||0,n[i]+=a)}))}),ut.each(s,function(e){if(e.length>0){var i=ut.min(e),n=ut.max(e);t.min=null===t.min?i:Math.min(t.min,i),t.max=null===t.max?n:Math.max(t.max,n)}})}else ut.each(n,function(e,n){var a=i.getDatasetMeta(n);i.isDatasetVisible(n)&&o(a)&&ut.each(e.data,function(e,i){var n=+t.getRightValue(e);isNaN(n)||a.data[i].hidden||n<0||(null===t.min?t.min=n:nt.max&&(t.max=n),0!==n&&(null===t.minNotZero||n0?t.minNotZero=t.min:t.max<1?t.minNotZero=Math.pow(10,Math.floor(ut.log10(t.max))):t.minNotZero=1)},buildTicks:function(){var t=this,e=t.options.ticks,i=!t.isHorizontal(),n={min:_i(e.min),max:_i(e.max)},a=t.ticks=function(t,e){var i,n,a=[],o=wi(t.min,Math.pow(10,Math.floor(ut.log10(e.min)))),r=Math.floor(ut.log10(e.max)),s=Math.ceil(e.max/Math.pow(10,r));0===o?(i=Math.floor(ut.log10(e.minNotZero)),n=Math.floor(e.minNotZero/Math.pow(10,i)),a.push(o),o=n*Math.pow(10,i)):(i=Math.floor(ut.log10(o)),n=Math.floor(o/Math.pow(10,i)));var l=i<0?Math.pow(10,Math.abs(i)):1;do{a.push(o),10==++n&&(n=1,l=++i>=0?1:l),o=Math.round(n*Math.pow(10,i)*l)/l}while(ia?{start:e-i,end:e}:{start:e,end:e+i}}function Ri(t){return 0===t||180===t?"center":t<180?"left":"right"}function Oi(t,e,i,n){var a,o,r=i.y+n/2;if(ut.isArray(e))for(a=0,o=e.length;a270||t<90)&&(i.y-=e.h)}function Bi(t){return ut.isNumber(t)?t:0}var Ni=bi.extend({setDimensions:function(){var t=this;t.width=t.maxWidth,t.height=t.maxHeight,t.paddingTop=Fi(t.options)/2,t.xCenter=Math.floor(t.width/2),t.yCenter=Math.floor((t.height-t.paddingTop)/2),t.drawingArea=Math.min(t.height-t.paddingTop,t.width)/2},determineDataLimits:function(){var t=this,e=t.chart,i=Number.POSITIVE_INFINITY,n=Number.NEGATIVE_INFINITY;ut.each(e.data.datasets,function(a,o){if(e.isDatasetVisible(o)){var r=e.getDatasetMeta(o);ut.each(a.data,function(e,a){var o=+t.getRightValue(e);isNaN(o)||r.data[a].hidden||(i=Math.min(o,i),n=Math.max(o,n))})}}),t.min=i===Number.POSITIVE_INFINITY?0:i,t.max=n===Number.NEGATIVE_INFINITY?0:n,t.handleTickRangeOptions()},_computeTickLimit:function(){return Math.ceil(this.drawingArea/Fi(this.options))},convertTicksToLabels:function(){var t=this;bi.prototype.convertTicksToLabels.call(t),t.pointLabels=t.chart.data.labels.map(t.options.pointLabels.callback,t)},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var t=this.options;t.display&&t.pointLabels.display?function(t){var e,i,n,a=ut.options._parseFont(t.options.pointLabels),o={l:0,r:t.width,t:0,b:t.height-t.paddingTop},r={};t.ctx.font=a.string,t._pointLabelSizes=[];var s,l,d,u=Ti(t);for(e=0;eo.r&&(o.r=f.end,r.r=h),g.starto.b&&(o.b=g.end,r.b=h)}t.setReductions(t.drawingArea,o,r)}(this):this.setCenterPoint(0,0,0,0)},setReductions:function(t,e,i){var n=this,a=e.l/Math.sin(i.l),o=Math.max(e.r-n.width,0)/Math.sin(i.r),r=-e.t/Math.cos(i.t),s=-Math.max(e.b-(n.height-n.paddingTop),0)/Math.cos(i.b);a=Bi(a),o=Bi(o),r=Bi(r),s=Bi(s),n.drawingArea=Math.min(Math.floor(t-(a+o)/2),Math.floor(t-(r+s)/2)),n.setCenterPoint(a,o,r,s)},setCenterPoint:function(t,e,i,n){var a=this,o=a.width-e-a.drawingArea,r=t+a.drawingArea,s=i+a.drawingArea,l=a.height-a.paddingTop-n-a.drawingArea;a.xCenter=Math.floor((r+o)/2+a.left),a.yCenter=Math.floor((s+l)/2+a.top+a.paddingTop)},getIndexAngle:function(t){return t*(2*Math.PI/Ti(this))+(this.chart.options&&this.chart.options.startAngle?this.chart.options.startAngle:0)*Math.PI*2/360},getDistanceFromCenterForValue:function(t){var e=this;if(null===t)return 0;var i=e.drawingArea/(e.max-e.min);return e.options.ticks.reverse?(e.max-t)*i:(t-e.min)*i},getPointPosition:function(t,e){var i=this.getIndexAngle(t)-Math.PI/2;return{x:Math.cos(i)*e+this.xCenter,y:Math.sin(i)*e+this.yCenter}},getPointPositionForValue:function(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))},getBasePosition:function(){var t=this.min,e=this.max;return this.getPointPositionForValue(0,this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0)},draw:function(){var t=this,e=t.options,i=e.gridLines,n=e.ticks;if(e.display){var a=t.ctx,o=this.getIndexAngle(0),r=ut.options._parseFont(n);(e.angleLines.display||e.pointLabels.display)&&function(t){var e=t.ctx,i=t.options,n=i.angleLines,a=i.gridLines,o=i.pointLabels,r=Pi(n.lineWidth,a.lineWidth),s=Pi(n.color,a.color),l=Fi(i);e.save(),e.lineWidth=r,e.strokeStyle=s,e.setLineDash&&(e.setLineDash(Ai([n.borderDash,a.borderDash,[]])),e.lineDashOffset=Ai([n.borderDashOffset,a.borderDashOffset,0]));var d=t.getDistanceFromCenterForValue(i.ticks.reverse?t.min:t.max),u=ut.options._parseFont(o);e.font=u.string,e.textBaseline="middle";for(var h=Ti(t)-1;h>=0;h--){if(n.display&&r&&s){var c=t.getPointPosition(h,d);e.beginPath(),e.moveTo(t.xCenter,t.yCenter),e.lineTo(c.x,c.y),e.stroke()}if(o.display){var f=0===h?l/2:0,g=t.getPointPosition(h,d+f+5),p=Ii(o.fontColor,h,st.global.defaultFontColor);e.fillStyle=p;var m=t.getIndexAngle(h),v=ut.toDegrees(m);e.textAlign=Ri(v),zi(v,t._pointLabelSizes[h],g),Oi(e,t.pointLabels[h]||"",g,u.lineHeight)}}e.restore()}(t),ut.each(t.ticks,function(e,s){if(s>0||n.reverse){var l=t.getDistanceFromCenterForValue(t.ticksAsNumbers[s]);if(i.display&&0!==s&&function(t,e,i,n){var a,o=t.ctx,r=e.circular,s=Ti(t),l=Ii(e.color,n-1),d=Ii(e.lineWidth,n-1);if((r||s)&&l&&d){if(o.save(),o.strokeStyle=l,o.lineWidth=d,o.setLineDash&&(o.setLineDash(e.borderDash||[]),o.lineDashOffset=e.borderDashOffset||0),o.beginPath(),r)o.arc(t.xCenter,t.yCenter,i,0,2*Math.PI);else{a=t.getPointPosition(0,i),o.moveTo(a.x,a.y);for(var u=1;u=0&&r<=s;){if(a=t[(n=r+s>>1)-1]||null,o=t[n],!a)return{lo:null,hi:o};if(o[e]i))return{lo:a,hi:o};s=n-1}}return{lo:o,hi:null}}(t,e,i),o=a.lo?a.hi?a.lo:t[t.length-2]:t[0],r=a.lo?a.hi?a.hi:t[t.length-1]:t[1],s=r[e]-o[e],l=s?(i-o[e])/s:0,d=(r[n]-o[n])*l;return o[n]+d}function Ki(t,e){var i=t._adapter,n=t.options.time,a=n.parser,o=a||n.format,r=e;return"function"==typeof a&&(r=a(r)),ut.isFinite(r)||(r="string"==typeof o?i.parse(r,o):i.parse(r)),null!==r?+r:(a||"function"!=typeof o||(r=o(e),ut.isFinite(r)||(r=i.parse(r))),r)}function Gi(t,e){if(ut.isNullOrUndef(e))return null;var i=t.options.time,n=Ki(t,t.getRightValue(e));return null===n?n:(i.round&&(n=+t._adapter.startOf(n,i.round)),n)}function Zi(t){for(var e=qi.indexOf(t)+1,i=qi.length;e=a&&i<=o&&d.push(i);return n.min=a,n.max=o,n._unit=s.unit||function(t,e,i,n,a){var o,r;for(o=qi.length-1;o>=qi.indexOf(i);o--)if(r=qi[o],ji[r].common&&t._adapter.diff(a,n,r)>=e.length)return r;return qi[i?qi.indexOf(i):0]}(n,d,s.minUnit,n.min,n.max),n._majorUnit=Zi(n._unit),n._table=function(t,e,i,n){if("linear"===n||!t.length)return[{time:e,pos:0},{time:i,pos:1}];var a,o,r,s,l,d=[],u=[e];for(a=0,o=t.length;ae&&s=0&&t0?r:1}}),Qi={position:"bottom",distribution:"linear",bounds:"data",adapters:{},time:{parser:!1,format:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{autoSkip:!1,source:"auto",major:{enabled:!1}}};Ji._defaults=Qi;var tn={category:gi,linear:yi,logarithmic:Ci,radialLinear:Ni,time:Ji},en={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};si._date.override("function"==typeof t?{_id:"moment",formats:function(){return en},parse:function(e,i){return"string"==typeof e&&"string"==typeof i?e=t(e,i):e instanceof t||(e=t(e)),e.isValid()?e.valueOf():null},format:function(e,i){return t(e).format(i)},add:function(e,i,n){return t(e).add(i,n).valueOf()},diff:function(e,i,n){return t.duration(t(e).diff(t(i))).as(n)},startOf:function(e,i,n){return e=t(e),"isoWeek"===i?e.isoWeekday(n).valueOf():e.startOf(i).valueOf()},endOf:function(e,i){return t(e).endOf(i).valueOf()},_create:function(e){return t(e)}}:{}),st._set("global",{plugins:{filler:{propagate:!0}}});var nn={dataset:function(t){var e=t.fill,i=t.chart,n=i.getDatasetMeta(e),a=n&&i.isDatasetVisible(e)&&n.dataset._children||[],o=a.length||0;return o?function(t,e){return e=i)&&n;switch(o){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return o;default:return!1}}function on(t){var e,i=t.el._model||{},n=t.el._scale||{},a=t.fill,o=null;if(isFinite(a))return null;if("start"===a?o=void 0===i.scaleBottom?n.bottom:i.scaleBottom:"end"===a?o=void 0===i.scaleTop?n.top:i.scaleTop:void 0!==i.scaleZero?o=i.scaleZero:n.getBasePosition?o=n.getBasePosition():n.getBasePixel&&(o=n.getBasePixel()),null!=o){if(void 0!==o.x&&void 0!==o.y)return o;if(ut.isFinite(o))return{x:(e=n.isHorizontal())?o:null,y:e?null:o}}return null}function rn(t,e,i){var n,a=t[e].fill,o=[e];if(!i)return a;for(;!1!==a&&-1===o.indexOf(a);){if(!isFinite(a))return a;if(!(n=t[a]))return!1;if(n.visible)return a;o.push(a),a=n.fill}return!1}function sn(t){var e=t.fill,i="dataset";return!1===e?null:(isFinite(e)||(i="boundary"),nn[i](t))}function ln(t){return t&&!t.skip}function dn(t,e,i,n,a){var o;if(n&&a){for(t.moveTo(e[0].x,e[0].y),o=1;o0;--o)ut.canvas.lineTo(t,i[o],i[o-1],!0)}}var un={id:"filler",afterDatasetsUpdate:function(t,e){var i,n,a,o,r=(t.data.datasets||[]).length,s=e.propagate,l=[];for(n=0;ne?e:t.boxWidth}st._set("global",{legend:{display:!0,position:"top",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(t,e){var i=e.datasetIndex,n=this.chart,a=n.getDatasetMeta(i);a.hidden=null===a.hidden?!n.data.datasets[i].hidden:null,n.update()},onHover:null,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data;return ut.isArray(e.datasets)?e.datasets.map(function(e,i){return{text:e.label,fillStyle:ut.isArray(e.backgroundColor)?e.backgroundColor[0]:e.backgroundColor,hidden:!t.isDatasetVisible(i),lineCap:e.borderCapStyle,lineDash:e.borderDash,lineDashOffset:e.borderDashOffset,lineJoin:e.borderJoinStyle,lineWidth:e.borderWidth,strokeStyle:e.borderColor,pointStyle:e.pointStyle,datasetIndex:i}},this):[]}}},legendCallback:function(t){var e=[];e.push('
    ');for(var i=0;i'),t.data.datasets[i].label&&e.push(t.data.datasets[i].label),e.push("");return e.push("
"),e.join("")}});var gn=pt.extend({initialize:function(t){ut.extend(this,t),this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1},beforeUpdate:hn,update:function(t,e,i){var n=this;return n.beforeUpdate(),n.maxWidth=t,n.maxHeight=e,n.margins=i,n.beforeSetDimensions(),n.setDimensions(),n.afterSetDimensions(),n.beforeBuildLabels(),n.buildLabels(),n.afterBuildLabels(),n.beforeFit(),n.fit(),n.afterFit(),n.afterUpdate(),n.minSize},afterUpdate:hn,beforeSetDimensions:hn,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:hn,beforeBuildLabels:hn,buildLabels:function(){var t=this,e=t.options.labels||{},i=ut.callback(e.generateLabels,[t.chart],t)||[];e.filter&&(i=i.filter(function(i){return e.filter(i,t.chart.data)})),t.options.reverse&&i.reverse(),t.legendItems=i},afterBuildLabels:hn,beforeFit:hn,fit:function(){var t=this,e=t.options,i=e.labels,n=e.display,a=t.ctx,o=ut.options._parseFont(i),r=o.size,s=t.legendHitBoxes=[],l=t.minSize,d=t.isHorizontal();if(d?(l.width=t.maxWidth,l.height=n?10:0):(l.width=n?10:0,l.height=t.maxHeight),n)if(a.font=o.string,d){var u=t.lineWidths=[0],h=0;a.textAlign="left",a.textBaseline="top",ut.each(t.legendItems,function(t,e){var n=fn(i,r)+r/2+a.measureText(t.text).width;(0===e||u[u.length-1]+n+i.padding>l.width)&&(h+=r+i.padding,u[u.length-(e>0?0:1)]=i.padding),s[e]={left:0,top:0,width:n,height:r},u[u.length-1]+=n+i.padding}),l.height+=h}else{var c=i.padding,f=t.columnWidths=[],g=i.padding,p=0,m=0,v=r+c;ut.each(t.legendItems,function(t,e){var n=fn(i,r)+r/2+a.measureText(t.text).width;e>0&&m+v>l.height-c&&(g+=p+i.padding,f.push(p),p=0,m=0),p=Math.max(p,n),m+=v,s[e]={left:0,top:0,width:n,height:r}}),g+=p,f.push(p),l.width+=g}t.width=l.width,t.height=l.height},afterFit:hn,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var t=this,e=t.options,i=e.labels,n=st.global,a=n.defaultColor,o=n.elements.line,r=t.width,s=t.lineWidths;if(e.display){var l,d=t.ctx,u=cn(i.fontColor,n.defaultFontColor),h=ut.options._parseFont(i),c=h.size;d.textAlign="left",d.textBaseline="middle",d.lineWidth=.5,d.strokeStyle=u,d.fillStyle=u,d.font=h.string;var f=fn(i,c),g=t.legendHitBoxes,p=t.isHorizontal();l=p?{x:t.left+(r-s[0])/2+i.padding,y:t.top+i.padding,line:0}:{x:t.left+i.padding,y:t.top+i.padding,line:0};var m=c+i.padding;ut.each(t.legendItems,function(n,u){var h=d.measureText(n.text).width,v=f+c/2+h,b=l.x,x=l.y;p?u>0&&b+v+i.padding>t.left+t.minSize.width&&(x=l.y+=m,l.line++,b=l.x=t.left+(r-s[l.line])/2+i.padding):u>0&&x+m>t.top+t.minSize.height&&(b=l.x=b+t.columnWidths[l.line]+i.padding,x=l.y=t.top+i.padding,l.line++),function(t,i,n){if(!(isNaN(f)||f<=0)){d.save();var r=cn(n.lineWidth,o.borderWidth);if(d.fillStyle=cn(n.fillStyle,a),d.lineCap=cn(n.lineCap,o.borderCapStyle),d.lineDashOffset=cn(n.lineDashOffset,o.borderDashOffset),d.lineJoin=cn(n.lineJoin,o.borderJoinStyle),d.lineWidth=r,d.strokeStyle=cn(n.strokeStyle,a),d.setLineDash&&d.setLineDash(cn(n.lineDash,o.borderDash)),e.labels&&e.labels.usePointStyle){var s=f*Math.SQRT2/2,l=t+f/2,u=i+c/2;ut.canvas.drawPoint(d,n.pointStyle,s,l,u)}else 0!==r&&d.strokeRect(t,i,f,c),d.fillRect(t,i,f,c);d.restore()}}(b,x,n),g[u].left=b,g[u].top=x,function(t,e,i,n){var a=c/2,o=f+a+t,r=e+a;d.fillText(i.text,o,r),i.hidden&&(d.beginPath(),d.lineWidth=2,d.moveTo(o,r),d.lineTo(o+n,r),d.stroke())}(b,x,n,h),p?l.x+=v+i.padding:l.y+=m})}},_getLegendItemAt:function(t,e){var i,n,a,o=this;if(t>=o.left&&t<=o.right&&e>=o.top&&e<=o.bottom)for(a=o.legendHitBoxes,i=0;i=(n=a[i]).left&&t<=n.left+n.width&&e>=n.top&&e<=n.top+n.height)return o.legendItems[i];return null},handleEvent:function(t){var e,i=this,n=i.options,a="mouseup"===t.type?"click":t.type;if("mousemove"===a){if(!n.onHover&&!n.onLeave)return}else{if("click"!==a)return;if(!n.onClick)return}e=i._getLegendItemAt(t.x,t.y),"click"===a?e&&n.onClick&&n.onClick.call(i,t.native,e):(n.onLeave&&e!==i._hoveredItem&&(i._hoveredItem&&n.onLeave.call(i,t.native,i._hoveredItem),i._hoveredItem=e),n.onHover&&e&&n.onHover.call(i,t.native,e))}});function pn(t,e){var i=new gn({ctx:t.ctx,options:e,chart:t});ke.configure(t,i,e),ke.addBox(t,i),t.legend=i}var mn={id:"legend",_element:gn,beforeInit:function(t){var e=t.options.legend;e&&pn(t,e)},beforeUpdate:function(t){var e=t.options.legend,i=t.legend;e?(ut.mergeIf(e,st.global.legend),i?(ke.configure(t,i,e),i.options=e):pn(t,e)):i&&(ke.removeBox(t,i),delete t.legend)},afterEvent:function(t,e){var i=t.legend;i&&i.handleEvent(e)}},vn=ut.noop;st._set("global",{title:{display:!1,fontStyle:"bold",fullWidth:!0,padding:10,position:"top",text:"",weight:2e3}});var bn=pt.extend({initialize:function(t){ut.extend(this,t),this.legendHitBoxes=[]},beforeUpdate:vn,update:function(t,e,i){var n=this;return n.beforeUpdate(),n.maxWidth=t,n.maxHeight=e,n.margins=i,n.beforeSetDimensions(),n.setDimensions(),n.afterSetDimensions(),n.beforeBuildLabels(),n.buildLabels(),n.afterBuildLabels(),n.beforeFit(),n.fit(),n.afterFit(),n.afterUpdate(),n.minSize},afterUpdate:vn,beforeSetDimensions:vn,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:vn,beforeBuildLabels:vn,buildLabels:vn,afterBuildLabels:vn,beforeFit:vn,fit:function(){var t=this,e=t.options,i=e.display,n=t.minSize,a=ut.isArray(e.text)?e.text.length:1,o=ut.options._parseFont(e),r=i?a*o.lineHeight+2*e.padding:0;t.isHorizontal()?(n.width=t.maxWidth,n.height=r):(n.width=r,n.height=t.maxHeight),t.width=n.width,t.height=n.height},afterFit:vn,isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},draw:function(){var t=this,e=t.ctx,i=t.options;if(i.display){var n,a,o,r=ut.options._parseFont(i),s=r.lineHeight,l=s/2+i.padding,d=0,u=t.top,h=t.left,c=t.bottom,f=t.right;e.fillStyle=ut.valueOrDefault(i.fontColor,st.global.defaultFontColor),e.font=r.string,t.isHorizontal()?(a=h+(f-h)/2,o=u+l,n=f-h):(a="left"===i.position?h+l:f-l,o=u+(c-u)/2,n=c-u,d=Math.PI*("left"===i.position?-.5:.5)),e.save(),e.translate(a,o),e.rotate(d),e.textAlign="center",e.textBaseline="middle";var g=i.text;if(ut.isArray(g))for(var p=0,m=0;m=0;n--){var a=t[n];if(e(a))return a}},ut.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},ut.almostEquals=function(t,e,i){return Math.abs(t-e)t},ut.max=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.max(t,e)},Number.NEGATIVE_INFINITY)},ut.min=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.min(t,e)},Number.POSITIVE_INFINITY)},ut.sign=Math.sign?function(t){return Math.sign(t)}:function(t){return 0==(t=+t)||isNaN(t)?t:t>0?1:-1},ut.log10=Math.log10?function(t){return Math.log10(t)}:function(t){var e=Math.log(t)*Math.LOG10E,i=Math.round(e);return t===Math.pow(10,i)?i:e},ut.toRadians=function(t){return t*(Math.PI/180)},ut.toDegrees=function(t){return t*(180/Math.PI)},ut._decimalPlaces=function(t){if(ut.isFinite(t)){for(var e=1,i=0;Math.round(t*e)/e!==t;)e*=10,i++;return i}},ut.getAngleFromPoint=function(t,e){var i=e.x-t.x,n=e.y-t.y,a=Math.sqrt(i*i+n*n),o=Math.atan2(n,i);return o<-.5*Math.PI&&(o+=2*Math.PI),{angle:o,distance:a}},ut.distanceBetweenPoints=function(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))},ut.aliasPixel=function(t){return t%2==0?0:.5},ut._alignPixel=function(t,e,i){var n=t.currentDevicePixelRatio,a=i/2;return Math.round((e-a)*n)/n+a},ut.splineCurve=function(t,e,i,n){var a=t.skip?e:t,o=e,r=i.skip?e:i,s=Math.sqrt(Math.pow(o.x-a.x,2)+Math.pow(o.y-a.y,2)),l=Math.sqrt(Math.pow(r.x-o.x,2)+Math.pow(r.y-o.y,2)),d=s/(s+l),u=l/(s+l),h=n*(d=isNaN(d)?0:d),c=n*(u=isNaN(u)?0:u);return{previous:{x:o.x-h*(r.x-a.x),y:o.y-h*(r.y-a.y)},next:{x:o.x+c*(r.x-a.x),y:o.y+c*(r.y-a.y)}}},ut.EPSILON=Number.EPSILON||1e-14,ut.splineCurveMonotone=function(t){var e,i,n,a,o,r,s,l,d,u=(t||[]).map(function(t){return{model:t._model,deltaK:0,mK:0}}),h=u.length;for(e=0;e0?u[e-1]:null,(a=e0?u[e-1]:null,a=e=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},ut.previousItem=function(t,e,i){return i?e<=0?t[t.length-1]:t[e-1]:e<=0?t[0]:t[e-1]},ut.niceNum=function(t,e){var i=Math.floor(ut.log10(t)),n=t/Math.pow(10,i);return(e?n<1.5?1:n<3?2:n<7?5:10:n<=1?1:n<=2?2:n<=5?5:10)*Math.pow(10,i)},ut.requestAnimFrame="undefined"==typeof window?function(t){t()}:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)},ut.getRelativePosition=function(t,e){var i,n,a=t.originalEvent||t,o=t.target||t.srcElement,r=o.getBoundingClientRect(),s=a.touches;s&&s.length>0?(i=s[0].clientX,n=s[0].clientY):(i=a.clientX,n=a.clientY);var l=parseFloat(ut.getStyle(o,"padding-left")),d=parseFloat(ut.getStyle(o,"padding-top")),u=parseFloat(ut.getStyle(o,"padding-right")),h=parseFloat(ut.getStyle(o,"padding-bottom")),c=r.right-r.left-l-u,f=r.bottom-r.top-d-h;return{x:i=Math.round((i-r.left-l)/c*o.width/e.currentDevicePixelRatio),y:n=Math.round((n-r.top-d)/f*o.height/e.currentDevicePixelRatio)}},ut.getConstraintWidth=function(t){return i(t,"max-width","clientWidth")},ut.getConstraintHeight=function(t){return i(t,"max-height","clientHeight")},ut._calculatePadding=function(t,e,i){return(e=ut.getStyle(t,e)).indexOf("%")>-1?i*parseInt(e,10)/100:parseInt(e,10)},ut._getParentNode=function(t){var e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e},ut.getMaximumWidth=function(t){var e=ut._getParentNode(t);if(!e)return t.clientWidth;var i=e.clientWidth,n=i-ut._calculatePadding(e,"padding-left",i)-ut._calculatePadding(e,"padding-right",i),a=ut.getConstraintWidth(t);return isNaN(a)?n:Math.min(n,a)},ut.getMaximumHeight=function(t){var e=ut._getParentNode(t);if(!e)return t.clientHeight;var i=e.clientHeight,n=i-ut._calculatePadding(e,"padding-top",i)-ut._calculatePadding(e,"padding-bottom",i),a=ut.getConstraintHeight(t);return isNaN(a)?n:Math.min(n,a)},ut.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},ut.retinaScale=function(t,e){var i=t.currentDevicePixelRatio=e||"undefined"!=typeof window&&window.devicePixelRatio||1;if(1!==i){var n=t.canvas,a=t.height,o=t.width;n.height=a*i,n.width=o*i,t.ctx.scale(i,i),n.style.height||n.style.width||(n.style.height=a+"px",n.style.width=o+"px")}},ut.fontString=function(t,e,i){return e+" "+t+"px "+i},ut.longestText=function(t,e,i,n){var a=(n=n||{}).data=n.data||{},o=n.garbageCollect=n.garbageCollect||[];n.font!==e&&(a=n.data={},o=n.garbageCollect=[],n.font=e),t.font=e;var r=0;ut.each(i,function(e){null!=e&&!0!==ut.isArray(e)?r=ut.measureText(t,a,o,r,e):ut.isArray(e)&&ut.each(e,function(e){null==e||ut.isArray(e)||(r=ut.measureText(t,a,o,r,e))})});var s=o.length/2;if(s>i.length){for(var l=0;ln&&(n=o),n},ut.numberOfLabelLines=function(t){var e=1;return ut.each(t,function(t){ut.isArray(t)&&t.length>e&&(e=t.length)}),e},ut.color=X?function(t){return t instanceof CanvasGradient&&(t=st.global.defaultColor),X(t)}:function(t){return console.error("Color.js not found!"),t},ut.getHoverColor=function(t){return t instanceof CanvasPattern||t instanceof CanvasGradient?t:ut.color(t).saturate(.5).darken(.1).rgbString()}}(),ai._adapters=si,ai.Animation=vt,ai.animationService=bt,ai.controllers=ue,ai.DatasetController=Mt,ai.defaults=st,ai.Element=pt,ai.elements=Wt,ai.Interaction=ve,ai.layouts=ke,ai.platform=Ve,ai.plugins=Ee,ai.Scale=fi,ai.scaleService=He,ai.Ticks=li,ai.Tooltip=Je,ai.helpers.each(tn,function(t,e){ai.scaleService.registerScaleType(e,t,t._defaults)}),yn)yn.hasOwnProperty(_n)&&ai.plugins.register(yn[_n]);ai.platform.initialize();var Cn=ai;return"undefined"!=typeof window&&(window.Chart=ai),ai.Chart=ai,ai.Legend=yn.legend._element,ai.Title=yn.title._element,ai.pluginService=ai.plugins,ai.PluginBase=ai.Element.extend({}),ai.canvasHelpers=ai.helpers.canvas,ai.layoutService=ai.layouts,ai.LinearScaleBase=bi,ai.helpers.each(["Bar","Bubble","Doughnut","Line","PolarArea","Radar","Scatter"],function(t){ai[t]=function(e,i){return new ai(e,ai.helpers.merge(i||{},{type:t.charAt(0).toLowerCase()+t.slice(1)}))}}),Cn}); diff --git a/theme/basic.css b/theme/basic.css index 9614028..e7bb81e 100644 --- a/theme/basic.css +++ b/theme/basic.css @@ -5,6 +5,9 @@ body { background: #282a33; color: #f6efdc; } +body.wide { + max-width: 100%; +} a:any-link { color: #8b969a; } @@ -57,18 +60,18 @@ input:invalid { min-height: 3em; border: solid black 2px; } -#scoreboard { +#rankings { width: 100%; position: relative; } -#scoreboard span { +#rankings span { font-size: 75%; display: inline-block; overflow: hidden; height: 1.7em; } -#scoreboard span.teamname { +#rankings span.teamname { font-size: inherit; color: white; text-shadow: 0 0 3px black; @@ -76,7 +79,7 @@ input:invalid { position: absolute; right: 0.2em; } -#scoreboard div * {white-space: nowrap;} +#rankings div * {white-space: nowrap;} .cat0, .cat8, .cat16 {background-color: #a6cee3; color: black;} .cat1, .cat9, .cat17 {background-color: #1f78b4; color: white;} .cat2, .cat10, .cat18 {background-color: #b2df8a; color: black;} @@ -116,3 +119,24 @@ input:invalid { transform: rotate(360deg); } } + +li[draggable]::before { + content: "↕"; + padding: 0.5em; + cursor: move; +} +li[draggable] { + list-style: none; +} + +[draggable].moving { + opacity: 0.4; +} + +[draggable].over { + border: 1px white dashed; +} + +#cacheButton.disabled { + display: none; +} diff --git a/theme/index.html b/theme/index.html index 89bdf48..f72f5e1 100644 --- a/theme/index.html +++ b/theme/index.html @@ -5,26 +5,36 @@ + +

MOTH

-
+
+
+
- Team name: + Team ID:
+ Team name:
-
+ diff --git a/theme/manifest.json b/theme/manifest.json new file mode 100644 index 0000000..acc15b6 --- /dev/null +++ b/theme/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "Monarch of the Hill", + "short_name": "MOTH", + "start_url": ".", + "display": "standalone", + "background_color": "#282a33", + "theme_color": "#ECB", + "description": "The MOTH CTF engine" +} diff --git a/theme/moment.min.js b/theme/moment.min.js new file mode 100644 index 0000000..5787a40 --- /dev/null +++ b/theme/moment.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function c(){return e.apply(null,arguments)}function o(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function u(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function l(e){return void 0===e}function h(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function d(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function f(e,t){var n,s=[];for(n=0;n>>0,s=0;sSe(e)?(r=e+1,o-Se(e)):(r=e,o),{year:r,dayOfYear:a}}function Ie(e,t,n){var s,i,r=Ve(e.year(),t,n),a=Math.floor((e.dayOfYear()-r-1)/7)+1;return a<1?s=a+Ae(i=e.year()-1,t,n):a>Ae(e.year(),t,n)?(s=a-Ae(e.year(),t,n),i=e.year()+1):(i=e.year(),s=a),{week:s,year:i}}function Ae(e,t,n){var s=Ve(e,t,n),i=Ve(e+1,t,n);return(Se(e)-s+i)/7}I("w",["ww",2],"wo","week"),I("W",["WW",2],"Wo","isoWeek"),C("week","w"),C("isoWeek","W"),F("week",5),F("isoWeek",5),ue("w",B),ue("ww",B,z),ue("W",B),ue("WW",B,z),fe(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=D(e)});function je(e,t){return e.slice(t,7).concat(e.slice(0,t))}I("d",0,"do","day"),I("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),I("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),I("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),I("e",0,0,"weekday"),I("E",0,0,"isoWeekday"),C("day","d"),C("weekday","e"),C("isoWeekday","E"),F("day",11),F("weekday",11),F("isoWeekday",11),ue("d",B),ue("e",B),ue("E",B),ue("dd",function(e,t){return t.weekdaysMinRegex(e)}),ue("ddd",function(e,t){return t.weekdaysShortRegex(e)}),ue("dddd",function(e,t){return t.weekdaysRegex(e)}),fe(["dd","ddd","dddd"],function(e,t,n,s){var i=n._locale.weekdaysParse(e,s,n._strict);null!=i?t.d=i:g(n).invalidWeekday=e}),fe(["d","e","E"],function(e,t,n,s){t[s]=D(e)});var Ze="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var $e="Su_Mo_Tu_We_Th_Fr_Sa".split("_");var qe=ae;var Je=ae;var Be=ae;function Qe(){function e(e,t){return t.length-e.length}var t,n,s,i,r,a=[],o=[],u=[],l=[];for(t=0;t<7;t++)n=y([2e3,1]).day(t),s=this.weekdaysMin(n,""),i=this.weekdaysShort(n,""),r=this.weekdays(n,""),a.push(s),o.push(i),u.push(r),l.push(s),l.push(i),l.push(r);for(a.sort(e),o.sort(e),u.sort(e),l.sort(e),t=0;t<7;t++)o[t]=he(o[t]),u[t]=he(u[t]),l[t]=he(l[t]);this._weekdaysRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+a.join("|")+")","i")}function Xe(){return this.hours()%12||12}function Ke(e,t){I(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function et(e,t){return t._meridiemParse}I("H",["HH",2],0,"hour"),I("h",["hh",2],0,Xe),I("k",["kk",2],0,function(){return this.hours()||24}),I("hmm",0,0,function(){return""+Xe.apply(this)+L(this.minutes(),2)}),I("hmmss",0,0,function(){return""+Xe.apply(this)+L(this.minutes(),2)+L(this.seconds(),2)}),I("Hmm",0,0,function(){return""+this.hours()+L(this.minutes(),2)}),I("Hmmss",0,0,function(){return""+this.hours()+L(this.minutes(),2)+L(this.seconds(),2)}),Ke("a",!0),Ke("A",!1),C("hour","h"),F("hour",13),ue("a",et),ue("A",et),ue("H",B),ue("h",B),ue("k",B),ue("HH",B,z),ue("hh",B,z),ue("kk",B,z),ue("hmm",Q),ue("hmmss",X),ue("Hmm",Q),ue("Hmmss",X),ce(["H","HH"],ge),ce(["k","kk"],function(e,t,n){var s=D(e);t[ge]=24===s?0:s}),ce(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),ce(["h","hh"],function(e,t,n){t[ge]=D(e),g(n).bigHour=!0}),ce("hmm",function(e,t,n){var s=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s)),g(n).bigHour=!0}),ce("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s,2)),t[pe]=D(e.substr(i)),g(n).bigHour=!0}),ce("Hmm",function(e,t,n){var s=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s))}),ce("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s,2)),t[pe]=D(e.substr(i))});var tt,nt=Te("Hours",!0),st={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Ce,monthsShort:He,week:{dow:0,doy:6},weekdays:Ze,weekdaysMin:$e,weekdaysShort:ze,meridiemParse:/[ap]\.?m?\.?/i},it={},rt={};function at(e){return e?e.toLowerCase().replace("_","-"):e}function ot(e){var t=null;if(!it[e]&&"undefined"!=typeof module&&module&&module.exports)try{t=tt._abbr,require("./locale/"+e),ut(t)}catch(e){}return it[e]}function ut(e,t){var n;return e&&((n=l(t)?ht(e):lt(e,t))?tt=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),tt._abbr}function lt(e,t){if(null===t)return delete it[e],null;var n,s=st;if(t.abbr=e,null!=it[e])T("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=it[e]._config;else if(null!=t.parentLocale)if(null!=it[t.parentLocale])s=it[t.parentLocale]._config;else{if(null==(n=ot(t.parentLocale)))return rt[t.parentLocale]||(rt[t.parentLocale]=[]),rt[t.parentLocale].push({name:e,config:t}),null;s=n._config}return it[e]=new P(x(s,t)),rt[e]&&rt[e].forEach(function(e){lt(e.name,e.config)}),ut(e),it[e]}function ht(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return tt;if(!o(e)){if(t=ot(e))return t;e=[e]}return function(e){for(var t,n,s,i,r=0;r=t&&a(i,n,!0)>=t-1)break;t--}r++}return tt}(e)}function dt(e){var t,n=e._a;return n&&-2===g(e).overflow&&(t=n[_e]<0||11Pe(n[me],n[_e])?ye:n[ge]<0||24Ae(n,r,a)?g(e)._overflowWeeks=!0:null!=u?g(e)._overflowWeekday=!0:(o=Ee(n,s,i,r,a),e._a[me]=o.year,e._dayOfYear=o.dayOfYear)}(e),null!=e._dayOfYear&&(r=ct(e._a[me],s[me]),(e._dayOfYear>Se(r)||0===e._dayOfYear)&&(g(e)._overflowDayOfYear=!0),n=Ge(r,0,e._dayOfYear),e._a[_e]=n.getUTCMonth(),e._a[ye]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=a[t]=s[t];for(;t<7;t++)e._a[t]=a[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[ge]&&0===e._a[ve]&&0===e._a[pe]&&0===e._a[we]&&(e._nextDay=!0,e._a[ge]=0),e._d=(e._useUTC?Ge:function(e,t,n,s,i,r,a){var o;return e<100&&0<=e?(o=new Date(e+400,t,n,s,i,r,a),isFinite(o.getFullYear())&&o.setFullYear(e)):o=new Date(e,t,n,s,i,r,a),o}).apply(null,a),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[ge]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(g(e).weekdayMismatch=!0)}}var mt=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,_t=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,yt=/Z|[+-]\d\d(?::?\d\d)?/,gt=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],vt=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],pt=/^\/?Date\((\-?\d+)/i;function wt(e){var t,n,s,i,r,a,o=e._i,u=mt.exec(o)||_t.exec(o);if(u){for(g(e).iso=!0,t=0,n=gt.length;tn.valueOf():n.valueOf()this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},mn.isLocal=function(){return!!this.isValid()&&!this._isUTC},mn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},mn.isUtc=Et,mn.isUTC=Et,mn.zoneAbbr=function(){return this._isUTC?"UTC":""},mn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},mn.dates=n("dates accessor is deprecated. Use date instead.",un),mn.months=n("months accessor is deprecated. Use month instead",Ue),mn.years=n("years accessor is deprecated. Use year instead",Oe),mn.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),mn.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!l(this._isDSTShifted))return this._isDSTShifted;var e={};if(w(e,this),(e=Ot(e))._a){var t=e._isUTC?y(e._a):bt(e._a);this._isDSTShifted=this.isValid()&&0 { + console.warn("Error while registering service worker", err) + }) + } else { + console.log("Service workers not supported. Some offline functionality may not work") + } +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", pwa_init) +} else { + pwa_init() +} diff --git a/theme/moth.js b/theme/moth.js index f6f99ec..8f143b8 100644 --- a/theme/moth.js +++ b/theme/moth.js @@ -1,6 +1,6 @@ // jshint asi:true -var teamId +var devel = false var heartbeatInterval = 40000 function toast(message, timeout=5000) { @@ -14,9 +14,18 @@ function toast(message, timeout=5000) { ) } +function renderNotices(obj) { + let ne = document.getElementById("notices") + if (ne) { + ne.innerHTML = obj + } +} + function renderPuzzles(obj) { let puzzlesElement = document.createElement('div') + document.getElementById("login").style.display = "none" + // Create a sorted list of category names let cats = Object.keys(obj) cats.sort() @@ -35,7 +44,7 @@ function renderPuzzles(obj) { h.textContent = cat // Extras if we're running a devel server - if (obj.__devel__) { + if (devel) { let a = document.createElement('a') h.insertBefore(a, h.firstChild) a.textContent = "⬇️" @@ -62,7 +71,11 @@ function renderPuzzles(obj) { let a = document.createElement('a') i.appendChild(a) a.textContent = points - a.href = "puzzle.html?cat=" + cat + "&points=" + points + "&pid=" + id + let url = new URL("puzzle.html", window.location) + url.searchParams.set("cat", cat) + url.searchParams.set("points", points) + url.searchParams.set("pid", id) + a.href = url.toString() } } @@ -77,45 +90,148 @@ function renderPuzzles(obj) { container.appendChild(puzzlesElement) } -function heartbeat(teamId) { +function renderState(obj) { + devel = obj.config.devel + if (devel) { + sessionStorage.id = "1234" + sessionStorage.pid = "rodney" + } + if (Object.keys(obj.puzzles).length > 0) { + renderPuzzles(obj.puzzles) + } + renderNotices(obj.messages) +} + + +function heartbeat() { + let teamId = sessionStorage.id || "" + let participantId = sessionStorage.pid + let url = new URL("state", window.location) + url.searchParams.set("id", teamId) + if (participantId) { + url.searchParams.set("pid", participantId) + } let fd = new FormData() fd.append("id", teamId) - fetch("puzzles.json", { - method: "POST", - body: fd, - }) + fetch(url) .then(resp => { if (resp.ok) { resp.json() - .then(renderPuzzles) + .then(renderState) .catch(err => { - toast("Error fetching recent puzzles. I'll try again in a moment.") + toast("Error fetching recent state. I'll try again in a moment.") console.log(err) }) } }) .catch(err => { - toast("Error fetching recent puzzles. I'll try again in a moment.") + toast("Error fetching recent state. I'll try again in a moment.") console.log(err) }) } -function showPuzzles(teamId) { +function showPuzzles() { let spinner = document.createElement("span") spinner.classList.add("spinner") - sessionStorage.setItem("id", teamId) - document.getElementById("login").style.display = "none" document.getElementById("puzzles").appendChild(spinner) - heartbeat(teamId) - setInterval(e => { heartbeat(teamId) }, 40000) + heartbeat() + drawCacheButton() +} + +function drawCacheButton() { + let teamId = sessionStorage.id + let cacher = document.querySelector("#cacheButton") + + function updateCacheButton() { + let headers = new Headers() + headers.append("pragma", "no-cache") + headers.append("cache-control", "no-cache") + let url = new URL("current_manifest.json", window.location) + url.searchParams.set("id", teamId) + fetch(url, {method: "HEAD", headers: headers}) + .then( resp => { + if (resp.ok) { + cacher.classList.remove("disabled") + } else { + cacher.classList.add("disabled") + } + }) + .catch(ex => { + cacher.classList.add("disabled") + }) + } + + setInterval (updateCacheButton , 30000) + updateCacheButton() +} + +async function fetchAll() { + let teamId = sessionStorage.id + let headers = new Headers() + headers.append("pragma", "no-cache") + headers.append("cache-control", "no-cache") + requests = [] + let url = new URL("current_manifest.json", window.location) + url.searchParams.set("id", teamId) + + toast("Caching all currently-open content") + requests.push( fetch(url, {headers: headers}) + .then( resp => { + if (resp.ok) { + resp.json() + .then(contents => { + console.log("Processing manifest") + for (let resource of contents) { + if (resource == "puzzles.json") { + continue + } + fetch(resource) + .then(e => { + console.log("Fetched " + resource) + }) + } + }) + } + })) + + let resp = await fetch("puzzles.json?id=" + teamId, {headers: headers}) + + if (resp.ok) { + let categories = await resp.json() + let cat_names = Object.keys(categories) + cat_names.sort() + for (let cat_name of cat_names) { + if (cat_name.startsWith("__")) { + // Skip metadata + continue + } + let puzzles = categories[cat_name] + for (let puzzle of puzzles) { + let url = new URL("puzzle.html", window.location) + url.searchParams.set("cat", cat_name) + url.searchParams.set("points", puzzle[0]) + url.searchParams.set("pid", puzzle[1]) + requests.push( + fetch(url) + .then(e => { + console.log("Fetched " + url) + }) + ) + } + } + } + await Promise.all(requests) + toast("Done caching content") } function login(e) { e.preventDefault() let name = document.querySelector("[name=name]").value - let id = document.querySelector("[name=id]").value + let teamId = document.querySelector("[name=id]").value + let pide = document.querySelector("[name=pid]") + let participantId = pide?pide.value:"" fetch("register", { method: "POST", @@ -125,12 +241,11 @@ function login(e) { if (resp.ok) { resp.json() .then(obj => { - if (obj.status == "success") { - toast("Team registered") - showPuzzles(id) - } else if (obj.data.short == "Already registered") { - toast("Logged in with previously-registered team name") - showPuzzles(id) + if ((obj.status == "success") || (obj.data.short == "Already registered")) { + toast("Logged in") + sessionStorage.id = teamId + sessionStorage.pid = participantId + showPuzzles() } else { toast(obj.data.description) } @@ -152,16 +267,18 @@ function login(e) { function init() { // Already signed in? - let id = sessionStorage.getItem("id") - if (id) { - showPuzzles(id) + if (sessionStorage.id) { + showPuzzles() } - + heartbeat() + setInterval(e => heartbeat(), 40000) + document.getElementById("login").addEventListener("submit", login) } if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", init); + document.addEventListener("DOMContentLoaded", init) } else { - init(); + init() } + diff --git a/theme/notices.html b/theme/notices.html new file mode 100644 index 0000000..7b76fa5 --- /dev/null +++ b/theme/notices.html @@ -0,0 +1 @@ + diff --git a/theme/points.json b/theme/points.json index 5440553..36e0aef 100644 --- a/theme/points.json +++ b/theme/points.json @@ -1,9 +1,255 @@ -{"__comment__": - " - This is some sample data from a Cyber Fire Puzzles event in 2018. - You don't need to include this file in any custom theme: - It's here to help debug scoreboard work. - The devel server will serve this up as static content; - MOTHd will ignore it. - ", -"teams":{"0":"PPP11","1":"Ashley King","103":"sparkles","10c":"OpsElite","11d":"ECU","11f":"hellokitty","127":"dirtbags","12f":"C","17c":"gouda","187":"student-d","19e":"Mit1306","1d0":"ssamson","1e":"Zen","1e0":"Moises Carrillo","1e2":"Cyber_Defender","1fb":"Carnivorous Robot","2":"NasarioS","20d":"Gnaneshwar","23":"CyberTia","24":"Cyberfighter","3":"csus2018","315":"TaterBots","344":"Starstruck Traveller","34e":"team202","35d":"Giga_mpact","38":"KTF","39":"#SINA","3f8":"cebcn","3f9":"WeAreGamers","410":"AWCTeam","42":"cyberfire","44e":"sam","4a":"rBurke","4d8":"Fiji","4e5":"Viet","5":"AART","517":"hazard.bay","52":"Prismriver","53":"Prajna","55":"Aggie1","577":"345keenan","5e":"App_U","64":"yau.marie","65":"cyberfight","673":"intel0pe","674":"ccat","6b":"Slackers 🐶","7":"hpu","73":"Haris","77":"tutuvous","81":"n0v","8d":"RemindMeMidtermOnMonday","9c":"Jest","a1":"pmcclanahan","a7":"Shadow03","ab":"TopTierMemeEngineer","af":"Tacere","b":"GREEN","b1":"Slackers 🐸","c":"SaengP","d0":"Penny1","d2":"Yashkumar Ukani","d5":"Unintended Consequences","d6":"JoelSwansonNYPD","da":"pmcclanahan","db":"worm","dd":"1","f9":"silversky"},"points":[[1538154043,"0","base",1],[1538154052,"1","base",1],[1538154090,"2","base",1],[1538154090,"3","base",1],[1538154118,"2","nocode",1],[1538154182,"5","nocode",1],[1538154199,"2","nocode",2],[1538154219,"7","base",1],[1538154243,"2","nocode",3],[1538154269,"3","base",2],[1538154274,"2","nocode",4],[1538154292,"b","base",1],[1538154318,"c","base",1],[1538154318,"b","nocode",1],[1538154329,"b","nocode",2],[1538154334,"2","nocode",10],[1538154348,"0","netarch",1],[1538154350,"5","nocode",3],[1538154355,"1","base",2],[1538154358,"0","nocode",1],[1538154362,"3","nocode",1],[1538154372,"0","nocode",2],[1538154386,"0","nocode",3],[1538154393,"b","nocode",4],[1538154395,"0","nocode",4],[1538154418,"b","nocode",10],[1538154428,"0","nocode",10],[1538154458,"7","netarch",1],[1538154498,"0","netarch",2],[1538154540,"7","netarch",2],[1538154581,"1e","base",1],[1538154632,"1","netarch",1],[1538154633,"2","netarch",1],[1538154694,"1e","base",2],[1538154754,"5","base",2],[1538154762,"23","base",1],[1538154796,"24","base",1],[1538154848,"23","nocode",1],[1538154857,"c","netarch",1],[1538154884,"23","nocode",2],[1538154886,"1e","netarch",1],[1538154908,"1","netarch",2],[1538154922,"1","nocode",1],[1538154927,"1e","netarch",2],[1538154933,"1","nocode",2],[1538154951,"1","nocode",3],[1538154962,"1","nocode",4],[1538154972,"5","nocode",2],[1538155005,"1","nocode",10],[1538155006,"7","base",2],[1538155016,"3","nocode",10],[1538155031,"5","nocode",4],[1538155059,"b","netarch",1],[1538155104,"7","nocode",1],[1538155132,"23","nocode",4],[1538155142,"7","nocode",2],[1538155149,"38","netarch",1],[1538155170,"39","base",1],[1538155219,"23","nocode",3],[1538155256,"38","netarch",2],[1538155281,"3","nocode",2],[1538155335,"1e","nocode",1],[1538155367,"7","nocode",4],[1538155384,"1e","nocode",2],[1538155429,"7","nocode",10],[1538155458,"1e","nocode",3],[1538155490,"42","base",1],[1538155509,"3","netarch",1],[1538155515,"1e","nocode",4],[1538155516,"38","nocode",10],[1538155551,"5","nocode",10],[1538155585,"39","netarch",1],[1538155600,"5","netarch",1],[1538155611,"39","nocode",1],[1538155615,"4a","nocode",1],[1538155626,"38","nocode",4],[1538155637,"4a","nocode",2],[1538155645,"38","nocode",3],[1538155650,"39","nocode",2],[1538155659,"38","nocode",2],[1538155661,"5","netarch",2],[1538155666,"38","nocode",1],[1538155687,"52","base",1],[1538155723,"53","netarch",1],[1538155745,"1e","nocode",10],[1538155779,"55","nocode",10],[1538155856,"55","base",1],[1538155893,"39","nocode",4],[1538155915,"3","nocode",4],[1538155916,"4a","nocode",3],[1538155934,"4a","nocode",4],[1538155978,"3","nocode",3],[1538155982,"4a","nocode",10],[1538156084,"52","base",2],[1538156161,"5e","base",1],[1538156229,"5e","nocode",1],[1538156261,"7","nocode",20],[1538156269,"4a","base",1],[1538156301,"5e","nocode",2],[1538156348,"7","nocode",3],[1538156537,"64","base",1],[1538156548,"65","base",1],[1538156608,"64","base",2],[1538156738,"39","nocode",10],[1538156823,"2","netarch",2],[1538157096,"3","netarch",2],[1538157358,"5e","nocode",3],[1538157404,"6b","base",1],[1538157417,"5e","nocode",4],[1538157422,"64","netarch",1],[1538157450,"5e","nocode",10],[1538157503,"64","netarch",2],[1538157587,"64","nocode",1],[1538157609,"64","nocode",2],[1538157684,"52","netarch",1],[1538157697,"73","base",1],[1538157700,"64","nocode",3],[1538157713,"64","nocode",4],[1538157792,"c","netarch",2],[1538157840,"77","base",1],[1538157886,"c","nocode",1],[1538157913,"c","nocode",2],[1538157926,"0","base",2],[1538157961,"6b","base",2],[1538157971,"c","nocode",3],[1538157989,"c","nocode",4],[1538158009,"38","base",1],[1538158082,"c","nocode",10],[1538158097,"0","base",3],[1538158118,"81","base",1],[1538158285,"0","base",4],[1538158354,"0","base",5],[1538158433,"0","base",6],[1538158470,"0","base",7],[1538158635,"6b","netarch",1],[1538158695,"3","base",4],[1538158715,"5","nocode",20],[1538158752,"6b","netarch",2],[1538158864,"3","nocode",20],[1538158874,"6b","nocode",1],[1538158890,"6b","nocode",2],[1538158939,"8d","base",1],[1538159105,"42","base",2],[1538159302,"42","nocode",2],[1538159579,"6b","nocode",3],[1538159714,"6b","nocode",4],[1538159801,"6b","nocode",10],[1538159920,"52","nocode",1],[1538159929,"52","nocode",2],[1538159987,"52","nocode",3],[1538159998,"52","nocode",4],[1538160019,"52","nocode",10],[1538160168,"8d","base",2],[1538160250,"8d","base",3],[1538160302,"8d","base",4],[1538160306,"4a","netarch",1],[1538160320,"9c","base",1],[1538160354,"8d","base",5],[1538160403,"8d","base",6],[1538160458,"8d","base",7],[1538160711,"52","base",4],[1538160886,"a1","netarch",1],[1538161028,"7","base",3],[1538161093,"5","base",1],[1538161171,"7","base",4],[1538161423,"7","base",5],[1538161534,"7","base",6],[1538161913,"a7","base",2],[1538161970,"3","base",5],[1538162163,"38","base",2],[1538162248,"55","base",2],[1538162423,"ab","base",1],[1538162486,"7","base",7],[1538162580,"ab","base",2],[1538162806,"73","nocode",1],[1538162926,"af","base",1],[1538162995,"3","base",6],[1538163002,"b1","base",1],[1538163024,"55","nocode",1],[1538163025,"b1","nocode",2],[1538163035,"1","base",3],[1538163036,"b1","nocode",3],[1538163054,"55","nocode",2],[1538163063,"b1","nocode",4],[1538163082,"b1","base",2],[1538163096,"b1","netarch",1],[1538163109,"b1","netarch",2],[1538163149,"b1","nocode",1],[1538163174,"55","netarch",1],[1538163179,"b1","nocode",10],[1538163275,"55","netarch",2],[1538163527,"a7","netarch",1],[1538163529,"3","base",7],[1538163553,"1","base",4],[1538163563,"55","nocode",3],[1538163629,"55","nocode",4],[1538163637,"a7","netarch",2],[1538163845,"55","nocode",20],[1538163865,"af","base",2],[1538163950,"a7","nocode",1],[1538164019,"a7","nocode",2],[1538164084,"7","base",8],[1538164125,"1","base",5],[1538164138,"5","nocode",30],[1538164213,"a7","nocode",3],[1538164246,"9c","base",2],[1538164246,"a7","nocode",4],[1538164311,"a7","nocode",10],[1538164593,"d0","base",1],[1538164674,"52","base",8],[1538164694,"d2","base",1],[1538164711,"d2","nocode",1],[1538164859,"d2","netarch",1],[1538165026,"d5","base",1],[1538165148,"d6","base",1],[1538165165,"52","nocode",20],[1538165199,"af","base",3],[1538165208,"73","base",2],[1538165212,"da","base",1],[1538165359,"db","base",1],[1538165387,"d6","base",2],[1538165394,"dd","base",1],[1538165398,"7","base",9],[1538165582,"dd","base",2],[1538165747,"db","base",2],[1538166077,"7","base",10],[1538166100,"3","base",3],[1538166106,"af","base",4],[1538166208,"d5","base",2],[1538166211,"8d","netarch",1],[1538166224,"d0","base",2],[1538166290,"af","base",5],[1538166328,"52","nocode",50],[1538166358,"af","base",6],[1538166495,"af","base",7],[1538166803,"7","nocode",50],[1538166910,"d0","base",10],[1538167306,"7","base",11],[1538167411,"8d","netarch",2],[1538167436,"d5","base",3],[1538167482,"55","nocode",80],[1538167511,"7","base",12],[1538167524,"d5","base",4],[1538167577,"d5","base",5],[1538167625,"d5","base",6],[1538167702,"55","nocode",90],[1538167807,"d5","base",7],[1538167974,"d0","netarch",1],[1538168034,"d0","netarch",2],[1538168066,"f9","nocode",1],[1538168086,"f9","nocode",2],[1538168101,"f9","nocode",3],[1538168111,"f9","nocode",4],[1538168226,"d6","base",3],[1538168238,"52","netarch",2],[1538168289,"f9","nocode",10],[1538168349,"d6","netarch",1],[1538168387,"af","base",9],[1538168469,"af","base",10],[1538168511,"103","netarch",1],[1538168538,"af","base",11],[1538168589,"d6","nocode",1],[1538168596,"5","nocode",90],[1538168635,"d6","nocode",2],[1538168664,"d6","nocode",3],[1538168683,"af","base",12],[1538168704,"d0","nocode",1],[1538168726,"d0","nocode",2],[1538168732,"10c","base",1],[1538168744,"d6","nocode",4],[1538168746,"d0","nocode",3],[1538168750,"af","base",13],[1538168768,"d0","nocode",4],[1538168841,"af","base",14],[1538168856,"d0","nocode",10],[1538168915,"af","base",15],[1538168936,"7","nocode",90],[1538168973,"af","base",16],[1538169126,"10c","base",2],[1538169149,"3","nocode",90],[1538169311,"af","netarch",1],[1538169499,"af","nocode",1],[1538169522,"af","nocode",2],[1538169543,"d6","nocode",90],[1538169651,"af","nocode",3],[1538169702,"11d","base",1],[1538169834,"d5","base",9],[1538169870,"11f","base",1],[1538169879,"d5","base",10],[1538169896,"9c","nocode",1],[1538169908,"11d","base",2],[1538169919,"d5","base",11],[1538169948,"9c","nocode",2],[1538169959,"d5","base",12],[1538169985,"af","nocode",4],[1538170018,"127","netarch",3],[1538170020,"d6","netarch",2],[1538170025,"d5","base",13],[1538170033,"af","nocode",10],[1538170043,"9c","nocode",4],[1538170069,"d5","base",14],[1538170110,"d5","base",15],[1538170111,"11f","base",2],[1538170117,"12f","base",1],[1538170182,"d5","base",16],[1538170223,"d5","base",17],[1538170325,"52","base",16],[1538170360,"3","netarch",4],[1538170361,"f9","netarch",1],[1538170435,"0","netarch",4],[1538170450,"2","base",2],[1538170515,"d5","netarch",1],[1538170770,"0","nocode",80],[1538170830,"0","base",9],[1538170880,"0","base",10],[1538170916,"0","base",11],[1538170929,"2","nocode",20],[1538170942,"103","nocode",1],[1538170964,"0","base",12],[1538170964,"103","nocode",2],[1538170995,"0","base",13],[1538170999,"103","nocode",3],[1538171026,"0","base",14],[1538171045,"103","nocode",4],[1538171055,"0","base",15],[1538171071,"103","nocode",10],[1538171085,"0","base",16],[1538171111,"0","base",17],[1538171136,"0","base",18],[1538171147,"103","nocode",20],[1538171165,"0","base",19],[1538171254,"0","base",20],[1538171276,"0","base",21],[1538171290,"12f","base",2],[1538171304,"0","base",22],[1538171328,"0","base",23],[1538171355,"0","base",24],[1538171380,"0","base",25],[1538171404,"0","base",26],[1538171430,"0","base",27],[1538171461,"0","base",28],[1538171484,"0","base",29],[1538171510,"0","base",30],[1538171541,"0","base",31],[1538171566,"7","netarch",4],[1538171614,"0","base",32],[1538171698,"12f","nocode",1],[1538172076,"0","base",33],[1538172186,"0","base",34],[1538172243,"0","base",35],[1538172258,"52","base",3],[1538172273,"0","base",36],[1538172308,"52","base",5],[1538172331,"52","base",6],[1538172354,"52","base",7],[1538172446,"52","base",9],[1538172468,"52","base",10],[1538172487,"52","base",11],[1538172507,"52","base",12],[1538172533,"52","base",13],[1538172545,"af","base",17],[1538172561,"12f","nocode",2],[1538172581,"52","base",14],[1538172603,"52","base",15],[1538172650,"12f","netarch",1],[1538172652,"52","base",17],[1538172672,"52","base",18],[1538172700,"52","base",19],[1538172710,"0","base",37],[1538172718,"52","base",20],[1538172735,"52","base",21],[1538172753,"52","base",22],[1538172771,"52","base",23],[1538172790,"52","base",24],[1538172806,"52","base",25],[1538172823,"52","base",26],[1538172840,"52","base",27],[1538172859,"52","base",28],[1538172867,"12f","nocode",3],[1538172878,"52","base",29],[1538172879,"17c","nocode",1],[1538172891,"17c","nocode",2],[1538172898,"52","base",30],[1538172917,"52","base",31],[1538172922,"12f","nocode",4],[1538172956,"17c","base",1],[1538172967,"12f","nocode",10],[1538172993,"17c","nocode",4],[1538173049,"52","nocode",90],[1538173049,"17c","nocode",10],[1538173116,"0","base",38],[1538173138,"187","base",1],[1538173167,"0","base",39],[1538173195,"0","base",40],[1538173306,"187","base",2],[1538173349,"103","nocode",90],[1538173393,"0","base",8],[1538173460,"12f","nocode",20],[1538173482,"d5","netarch",2],[1538173807,"0","base",41],[1538173831,"52","netarch",4],[1538173957,"0","base",42],[1538173976,"7","base",13],[1538173995,"0","base",43],[1538174032,"0","base",44],[1538174045,"7","base",14],[1538174079,"10c","netarch",1],[1538174089,"0","base",45],[1538174101,"7","base",15],[1538174127,"0","base",46],[1538174164,"0","base",47],[1538174197,"3","base",20],[1538174224,"7","base",16],[1538174247,"d5","nocode",1],[1538174253,"19e","base",1],[1538174264,"d5","nocode",2],[1538174327,"19e","netarch",1],[1538174343,"19e","nocode",1],[1538174350,"0","base",48],[1538174438,"0","base",49],[1538174459,"19e","nocode",2],[1538174499,"19e","nocode",10],[1538174556,"0","nocode",90],[1538174671,"11f","netarch",1],[1538174727,"7","base",32],[1538174737,"8d","base",9],[1538174756,"11f","netarch",2],[1538174757,"0","base",50],[1538174775,"8d","base",10],[1538174788,"db","nocode",1],[1538174800,"db","nocode",2],[1538174811,"8d","base",11],[1538174813,"11f","nocode",1],[1538174813,"db","nocode",3],[1538174826,"db","nocode",4],[1538174835,"11f","nocode",2],[1538174843,"8d","base",12],[1538174856,"11f","nocode",3],[1538174867,"11f","nocode",4],[1538174870,"db","nocode",10],[1538174875,"8d","base",13],[1538174893,"11f","nocode",10],[1538174907,"8d","base",14],[1538174940,"8d","base",15],[1538174987,"8d","base",16],[1538175021,"8d","base",17],[1538175040,"11f","nocode",90],[1538175055,"8d","base",18],[1538175121,"8d","base",28],[1538175137,"db","netarch",1],[1538175197,"8d","base",19],[1538175229,"8d","base",20],[1538175261,"8d","base",21],[1538175293,"8d","base",22],[1538175326,"8d","base",23],[1538175358,"8d","base",24],[1538175390,"8d","base",25],[1538175419,"8d","base",26],[1538175450,"8d","base",27],[1538175490,"8d","base",29],[1538175525,"8d","base",30],[1538175565,"8d","base",31],[1538175597,"db","netarch",2],[1538176001,"8d","netarch",4],[1538176052,"1d0","base",1],[1538176502,"db","netarch",4],[1538176516,"10c","netarch",2],[1538176523,"17c","nocode",3],[1538176639,"0","reverse",200],[1538176677,"1d0","nocode",1],[1538176694,"1d0","nocode",2],[1538176713,"38","nocode",90],[1538176715,"1d0","nocode",3],[1538176748,"1d0","nocode",4],[1538176768,"103","netarch",2],[1538176910,"1d0","nocode",10],[1538176982,"17c","nocode",20],[1538177189,"17c","nocode",90],[1538177408,"8d","netarch",5],[1538177507,"4a","base",2],[1538177526,"1e0","base",1],[1538177636,"1e0","nocode",1],[1538177789,"1e2","base",1],[1538177826,"0","netarch",5],[1538177879,"0","netarch",6],[1538178068,"0","netarch",7],[1538178085,"1e2","nocode",1],[1538178262,"8d","netarch",6],[1538178265,"10c","nocode",1],[1538178281,"10c","nocode",2],[1538178453,"10c","nocode",3],[1538178565,"10c","nocode",4],[1538178608,"10c","nocode",10],[1538178653,"64","nocode",10],[1538178837,"187","netarch",1],[1538178947,"103","netarch",7],[1538178964,"8d","netarch",7],[1538178968,"0","netarch",8],[1538179087,"0","netarch",10],[1538179177,"0","netarch",20],[1538179356,"64","base",16],[1538179376,"0","netarch",30],[1538179429,"7","netarch",10],[1538179455,"0","netarch",100],[1538179506,"0","netarch",200],[1538179581,"0","netarch",250],[1538179730,"0","netarch",300],[1538179738,"1fb","base",1],[1538179771,"187","netarch",2],[1538179848,"4a","nocode",90],[1538179915,"1fb","base",2],[1538180372,"4a","nocode",20],[1538180381,"8d","netarch",8],[1538180386,"b1","netarch",250],[1538180569,"81","base",2],[1538180674,"1fb","base",16],[1538180846,"64","base",3],[1538180864,"103","base",2],[1538181098,"1fb","base",32],[1538181237,"187","nocode",1],[1538181252,"8d","netarch",10],[1538181272,"187","nocode",2],[1538181272,"55","reverse",200],[1538181363,"8d","netarch",20],[1538181363,"187","nocode",3],[1538181380,"20d","base",1],[1538181422,"187","nocode",4],[1538181555,"20d","nocode",1],[1538181576,"17c","netarch",1],[1538182000,"3","nocode",50],[1538182028,"64","base",4],[1538182179,"81","nocode",1],[1538182199,"81","sequence",1],[1538182265,"4a","netarch",2],[1538182289,"3","sequence",1],[1538182324,"4a","network-fundamentals",1],[1538182328,"3","sequence",2],[1538182331,"81","network-fundamentals",1],[1538182358,"4a","sequence",1],[1538182360,"17c","sequence",1],[1538182361,"b1","nocode",90],[1538182374,"17c","sequence",2],[1538182378,"4a","sequence",2],[1538182382,"3","sequence",8],[1538182387,"7","sequence",1],[1538182403,"3","sequence",16],[1538182409,"81","network-fundamentals",10],[1538182410,"7","sequence",2],[1538182416,"4a","sequence",8],[1538182427,"81","network-fundamentals",11],[1538182434,"3","sequence",19],[1538182441,"81","network-fundamentals",12],[1538182443,"4a","sequence",16],[1538182444,"3","sequence",25],[1538182457,"4a","sequence",19],[1538182462,"3","sequence",35],[1538182467,"4a","sequence",25],[1538182482,"3","sequence",50],[1538182485,"4a","sequence",35],[1538182522,"81","network-fundamentals",13],[1538182560,"81","network-fundamentals",14],[1538182563,"17c","sequence",8],[1538182587,"5","sequence",1],[1538182627,"17c","sequence",16],[1538182653,"20d","netarch",2],[1538182657,"10c","nocode",90],[1538182684,"5","sequence",2],[1538182687,"17c","sequence",19],[1538182706,"17c","sequence",25],[1538182741,"17c","sequence",35],[1538182799,"7","network-fundamentals",10],[1538182811,"7","network-fundamentals",11],[1538182822,"7","network-fundamentals",12],[1538182825,"8d","netarch",100],[1538183054,"5","sequence",16],[1538183065,"7","sequence",19],[1538183073,"5","sequence",19],[1538183093,"5","sequence",25],[1538183098,"7","sequence",25],[1538183128,"5","sequence",35],[1538183131,"81","netarch",1],[1538183181,"7","sequence",35],[1538183190,"3","netarch",5],[1538183202,"0","sequence",1],[1538183212,"52","sequence",1],[1538183215,"0","sequence",2],[1538183216,"5","sequence",50],[1538183226,"52","sequence",2],[1538183230,"0","sequence",8],[1538183239,"52","sequence",8],[1538183242,"0","sequence",16],[1538183247,"64","network-fundamentals",1],[1538183253,"0","sequence",19],[1538183256,"52","sequence",16],[1538183263,"0","sequence",25],[1538183279,"0","sequence",35],[1538183280,"8d","netarch",250],[1538183382,"0","sequence",50],[1538183418,"52","sequence",25],[1538183423,"0","sequence",60],[1538183429,"52","sequence",35],[1538183458,"20d","network-fundamentals",1],[1538183463,"3","netarch",6],[1538183489,"5","sequence",60],[1538183514,"20d","sequence",1],[1538183550,"0","network-fundamentals",1],[1538183609,"0","network-fundamentals",10],[1538183617,"10c","network-fundamentals",1],[1538183629,"4a","sequence",100],[1538183631,"0","network-fundamentals",11],[1538183632,"64","sequence",1],[1538183639,"0","network-fundamentals",12],[1538183643,"3","sequence",60],[1538183651,"64","sequence",2],[1538183653,"0","network-fundamentals",13],[1538183732,"0","network-fundamentals",14],[1538183788,"7","network-fundamentals",13],[1538183789,"52","sequence",100],[1538183818,"5","network-fundamentals",1],[1538183832,"3","netarch",7],[1538183862,"52","network-fundamentals",1],[1538183879,"0","sequence",200],[1538183890,"2","sequence",1],[1538183944,"2","sequence",2],[1538183946,"17c","network-fundamentals",1],[1538183966,"3","sequence",100],[1538183970,"52","sequence",200],[1538184064,"2","sequence",8],[1538184065,"5","base",32],[1538184072,"20d","netarch",1],[1538184112,"2","sequence",16],[1538184165,"2","sequence",19],[1538184180,"2","sequence",25],[1538184202,"2","sequence",35],[1538184704,"b1","sequence",1],[1538184799,"17c","sequence",300],[1538184805,"4a","sequence",300],[1538184844,"b1","sequence",2],[1538185161,"5","netarch",200],[1538185263,"3","netarch",8],[1538185288,"b1","sequence",25],[1538185393,"3","netarch",10],[1538185440,"2","base",16],[1538185451,"b1","sequence",19],[1538185477,"10c","network-fundamentals",10],[1538185537,"10c","network-fundamentals",11],[1538185546,"b1","sequence",35],[1538185557,"10c","network-fundamentals",12],[1538185566,"d0","sequence",1],[1538185583,"d0","sequence",2],[1538185617,"0","sequence",400],[1538185638,"5","netarch",250],[1538185655,"10c","network-fundamentals",13],[1538185703,"5","network-fundamentals",12],[1538185780,"0","sequence",300],[1538185787,"d0","sequence",19],[1538185881,"0","sequence",100],[1538185961,"5","network-fundamentals",10],[1538186003,"0","sequence",500],[1538186020,"5","network-fundamentals",11],[1538186070,"5","network-fundamentals",13],[1538186122,"5","netarch",300],[1538186124,"b1","sequence",16],[1538186315,"5","sequence",100],[1538186352,"3","netarch",20],[1538186542,"9c","sequence",1],[1538186544,"17c","netarch",2],[1538186601,"d6","network-fundamentals",1],[1538186661,"9c","sequence",2],[1538186670,"1fb","nocode",1],[1538186730,"1fb","nocode",2],[1538186815,"d6","sequence",1],[1538186823,"d0","sequence",25],[1538186831,"d6","sequence",2],[1538186843,"d0","sequence",35],[1538186858,"d6","sequence",8],[1538186874,"d6","sequence",16],[1538186889,"d0","sequence",50],[1538186893,"d6","sequence",19],[1538186895,"1fb","nocode",3],[1538186922,"d6","sequence",25],[1538186935,"0","sequence",600],[1538186951,"d6","sequence",35],[1538187018,"1fb","nocode",4],[1538187074,"5","sequence",200],[1538187095,"2","network-fundamentals",1],[1538187099,"1fb","nocode",10],[1538187162,"b1","netarch",10],[1538187366,"f9","sequence",1],[1538187366,"0","reverse",500],[1538187379,"2","network-fundamentals",10],[1538187478,"d6","sequence",300],[1538187501,"81","sequence",2],[1538187547,"3","network-fundamentals",1],[1538187577,"2","network-fundamentals",11],[1538187601,"2","network-fundamentals",12],[1538187642,"3","network-fundamentals",10],[1538187695,"3","network-fundamentals",11],[1538187720,"10c","sequence",1],[1538187742,"81","nocode",2],[1538187769,"3","network-fundamentals",12],[1538187796,"3","network-fundamentals",13],[1538187803,"9c","sequence",8],[1538187817,"3","network-fundamentals",14],[1538187968,"3","network-fundamentals",15],[1538187983,"9c","sequence",16],[1538187988,"1fb","netarch",1],[1538188051,"3","network-fundamentals",16],[1538188147,"10c","sequence",19],[1538188245,"65","network-fundamentals",1],[1538188260,"10c","sequence",25],[1538188284,"65","nocode",1],[1538188306,"10c","sequence",35],[1538188311,"0","nocode",50],[1538188395,"2","network-fundamentals",14],[1538188402,"65","network-fundamentals",11],[1538188416,"2","network-fundamentals",15],[1538188431,"1fb","sequence",1],[1538188439,"2","network-fundamentals",16],[1538188462,"65","network-fundamentals",12],[1538188466,"1fb","sequence",2],[1538188503,"0","network-fundamentals",15],[1538188513,"0","network-fundamentals",16],[1538188514,"65","network-fundamentals",13],[1538188539,"0","network-fundamentals",30],[1538188588,"65","network-fundamentals",15],[1538188590,"1fb","sequence",8],[1538188611,"f9","nocode",90],[1538188611,"3","sequence",300],[1538188617,"65","network-fundamentals",16],[1538188629,"1fb","sequence",16],[1538188652,"65","network-fundamentals",30],[1538188881,"5","network-fundamentals",30],[1538189024,"0","base",51],[1538189032,"65","sequence",2],[1538189062,"0","base",52],[1538189114,"0","base",53],[1538189138,"53","base",1],[1538189193,"55","sequence",1],[1538189213,"55","sequence",2],[1538189238,"55","sequence",8],[1538189239,"3","sequence",400],[1538189251,"d0","sequence",60],[1538189252,"55","sequence",16],[1538189277,"10c","sequence",100],[1538189296,"53","base",2],[1538189351,"55","sequence",25],[1538189354,"0","base",54],[1538189369,"55","sequence",35],[1538189380,"0","base",55],[1538189391,"d0","sequence",100],[1538189409,"0","base",56],[1538189438,"3","sequence",500],[1538189439,"0","base",57],[1538189484,"55","network-fundamentals",1],[1538189548,"55","network-fundamentals",10],[1538189643,"10c","netarch",100],[1538190021,"3","sequence",600],[1538190258,"24","network-fundamentals",1],[1538190398,"5","base",3],[1538190563,"1fb","netarch",2],[1538190729,"b1","netarch",200],[1538190898,"b1","netarch",100],[1538191003,"9c","nocode",3],[1538191016,"9c","sequence",19],[1538191063,"10c","netarch",200],[1538191084,"5","base",4],[1538191100,"73","netarch",2],[1538191342,"1d0","sequence",1],[1538191371,"1d0","sequence",2],[1538191418,"5","base",5],[1538191515,"5","base",6],[1538191617,"1d0","sequence",8],[1538191625,"5","network-fundamentals",14],[1538191738,"5","base",7],[1538191790,"73","nocode",2],[1538191800,"1d0","sequence",16],[1538191819,"1d0","sequence",19],[1538191867,"1d0","sequence",25],[1538192020,"5","base",9],[1538192144,"5","base",10],[1538192214,"1d0","sequence",35],[1538192326,"73","sequence",1],[1538192450,"2","nocode",90],[1538192711,"3","sequence",200],[1538192726,"1d0","sequence",50],[1538192845,"5","network-fundamentals",16],[1538193142,"1","sequence",1],[1538193153,"1","sequence",2],[1538193201,"315","nocode",1],[1538193246,"315","base",1],[1538193250,"1","sequence",25],[1538193264,"1","sequence",35],[1538193285,"10c","base",4],[1538193288,"55","network-fundamentals",11],[1538193311,"55","network-fundamentals",12],[1538193368,"1d0","network-fundamentals",1],[1538193825,"3","netarch",30],[1538193930,"1","nocode",20],[1538194028,"9c","network-fundamentals",1],[1538194076,"9c","network-fundamentals",10],[1538194100,"9c","network-fundamentals",11],[1538194149,"9c","network-fundamentals",12],[1538194447,"9c","network-fundamentals",13],[1538194527,"5","netarch",20],[1538194610,"5","netarch",10],[1538194652,"9c","network-fundamentals",14],[1538194670,"1d0","network-fundamentals",12],[1538194712,"1d0","network-fundamentals",10],[1538194816,"9c","network-fundamentals",15],[1538194836,"9c","network-fundamentals",16],[1538195026,"5","netarch",400],[1538195039,"9c","netarch",100],[1538195501,"0","reverse",650],[1538195720,"5","sequence",8],[1538196349,"9c","netarch",1],[1538196784,"55","netarch",4],[1538196838,"52","netarch",7],[1538196892,"0","netarch",400],[1538196923,"9c","netarch",2],[1538197199,"53","network-fundamentals",1],[1538197267,"55","network-fundamentals",13],[1538197296,"55","network-fundamentals",14],[1538197320,"53","netarch",2],[1538197361,"55","network-fundamentals",15],[1538197400,"55","netarch",6],[1538197462,"55","network-fundamentals",16],[1538197607,"55","network-fundamentals",30],[1538197709,"53","network-fundamentals",11],[1538197775,"53","network-fundamentals",12],[1538197934,"55","sequence",50],[1538198026,"1fb","sequence",600],[1538198039,"53","network-fundamentals",13],[1538198122,"55","sequence",200],[1538198198,"55","sequence",300],[1538198296,"55","sequence",100],[1538198949,"344","base",1],[1538199218,"187","sequence",1],[1538199246,"187","sequence",2],[1538199519,"8d","base",32],[1538199628,"53","nocode",1],[1538199659,"53","nocode",2],[1538199725,"53","nocode",3],[1538199746,"53","nocode",4],[1538199761,"1fb","sequence",700],[1538199871,"53","nocode",10],[1538199979,"34e","base",1],[1538200015,"55","netarch",100],[1538200046,"55","netarch",200],[1538200101,"5","reverse",200],[1538200205,"34e","base",2],[1538200655,"0","sequence",740],[1538201242,"5","reverse",500],[1538201529,"42","sequence",1],[1538201540,"24","network-fundamentals",12],[1538201556,"42","sequence",2],[1538201595,"53","nocode",90],[1538201626,"42","sequence",25],[1538201648,"42","sequence",35],[1538201972,"42","sequence",19],[1538202003,"24","network-fundamentals",14],[1538203046,"35d","nocode",1],[1538203095,"35d","nocode",2],[1538203139,"35d","nocode",3],[1538203158,"35d","nocode",4],[1538203163,"f9","network-fundamentals",1],[1538203213,"35d","nocode",10],[1538203284,"53","sequence",1],[1538203330,"53","sequence",2],[1538203413,"35d","sequence",1],[1538203428,"35d","sequence",2],[1538203458,"53","sequence",8],[1538203514,"f9","sequence",2],[1538203546,"35d","sequence",16],[1538203566,"f9","sequence",8],[1538203575,"53","sequence",16],[1538203578,"35d","sequence",19],[1538203584,"f9","sequence",16],[1538203591,"35d","sequence",25],[1538203605,"f9","sequence",19],[1538203609,"35d","sequence",35],[1538203616,"f9","sequence",25],[1538203638,"f9","sequence",35],[1538203651,"53","sequence",19],[1538203679,"53","sequence",25],[1538203748,"53","sequence",35],[1538203752,"35d","base",1],[1538203902,"0","reverse",1000],[1538204061,"35d","network-fundamentals",1],[1538204277,"f9","sequence",50],[1538204423,"35d","network-fundamentals",10],[1538204468,"10c","netarch",5],[1538204604,"315","network-fundamentals",1],[1538204666,"f9","sequence",60],[1538204734,"10c","netarch",10],[1538204748,"35d","netarch",1],[1538204800,"315","network-fundamentals",11],[1538204821,"3","sequence",740],[1538204835,"315","network-fundamentals",12],[1538204866,"315","network-fundamentals",13],[1538204916,"315","network-fundamentals",14],[1538204934,"10c","netarch",20],[1538205088,"315","nocode",2],[1538205100,"53","sequence",100],[1538205130,"315","nocode",3],[1538205144,"315","nocode",4],[1538205190,"0","network-fundamentals",40],[1538205193,"f9","sequence",100],[1538205240,"0","network-fundamentals",41],[1538205266,"315","nocode",10],[1538205278,"0","network-fundamentals",42],[1538205306,"12f","base",3],[1538205322,"24","nocode",1],[1538205466,"f9","sequence",200],[1538205490,"0","network-fundamentals",43],[1538205496,"24","nocode",2],[1538205552,"1fb","base",3],[1538205602,"0","network-fundamentals",44],[1538205603,"1fb","base",4],[1538205608,"315","nocode",90],[1538205626,"24","nocode",3],[1538205632,"53","sequence",300],[1538205642,"1fb","base",5],[1538205660,"0","network-fundamentals",46],[1538205660,"24","nocode",4],[1538205688,"1fb","base",6],[1538205742,"1fb","base",7],[1538205813,"0","network-fundamentals",50],[1538205824,"315","netarch",2],[1538205902,"0","network-fundamentals",51],[1538205953,"1fb","base",9],[1538205992,"1fb","base",10],[1538206019,"81","netarch",2],[1538206025,"1fb","base",11],[1538206105,"0","network-fundamentals",52],[1538206105,"8d","base",33],[1538206124,"24","nocode",10],[1538206125,"1fb","base",12],[1538206153,"1fb","base",13],[1538206183,"0","network-fundamentals",53],[1538206185,"1fb","base",14],[1538206213,"1fb","base",15],[1538206274,"8d","base",34],[1538206282,"1fb","base",17],[1538206283,"0","network-fundamentals",54],[1538206358,"0","network-fundamentals",55],[1538206416,"315","sequence",1],[1538206432,"315","sequence",2],[1538206473,"315","sequence",8],[1538206490,"8d","base",35],[1538206503,"315","sequence",16],[1538206526,"315","sequence",19],[1538206559,"315","sequence",25],[1538206664,"1fb","base",18],[1538206695,"0","network-fundamentals",56],[1538206704,"1fb","base",19],[1538206730,"1fb","base",20],[1538206748,"8d","base",36],[1538206757,"1fb","base",21],[1538206788,"0","network-fundamentals",57],[1538206821,"1fb","base",22],[1538206834,"0","network-fundamentals",58],[1538206844,"1fb","base",23],[1538206899,"1fb","base",24],[1538206926,"0","network-fundamentals",59],[1538206928,"1fb","base",25],[1538206955,"1fb","base",26],[1538206986,"1fb","base",27],[1538207017,"0","network-fundamentals",60],[1538207018,"1fb","base",28],[1538207057,"1fb","base",29],[1538207059,"0","network-fundamentals",61],[1538207092,"1fb","base",30],[1538207112,"12f","base",4],[1538207127,"1fb","base",31],[1538207145,"0","network-fundamentals",62],[1538207188,"315","sequence",400],[1538207212,"0","network-fundamentals",63],[1538207250,"0","network-fundamentals",64],[1538207265,"0","network-fundamentals",70],[1538207275,"0","network-fundamentals",71],[1538207278,"12f","base",5],[1538207296,"0","network-fundamentals",72],[1538207406,"12f","base",6],[1538207467,"0","network-fundamentals",200],[1538207587,"12f","base",7],[1538207631,"f9","sequence",300],[1538208185,"1fb","network-fundamentals",1],[1538208564,"81","network-fundamentals",200],[1538208581,"1fb","sequence",19],[1538208685,"1fb","sequence",25],[1538208944,"3","nocode",80],[1538209362,"24","netarch",1],[1538209605,"1fb","sequence",35],[1538209672,"3","netarch",100],[1538209925,"1fb","sequence",50],[1538210291,"12f","base",9],[1538210534,"12f","base",10],[1538210647,"f9","network-fundamentals",12],[1538210676,"f9","network-fundamentals",13],[1538210738,"24","sequence",1],[1538210792,"24","sequence",2],[1538211005,"3","netarch",200],[1538211035,"24","sequence",8],[1538211081,"24","sequence",16],[1538211154,"12f","sequence",1],[1538211160,"1fb","sequence",100],[1538211217,"12f","sequence",2],[1538211491,"1fb","sequence",300],[1538211533,"24","sequence",19],[1538211568,"24","sequence",25],[1538212107,"3","netarch",250],[1538212117,"81","netarch",250],[1538212155,"24","sequence",35],[1538212335,"24","sequence",50],[1538212545,"1fb","sequence",400],[1538212935,"3f8","netarch",1],[1538213499,"3f9","nocode",1],[1538214048,"24","sequence",100],[1538214537,"24","network-fundamentals",72],[1538214657,"3f8","netarch",2],[1538214819,"24","network-fundamentals",70],[1538214898,"24","network-fundamentals",71],[1538216910,"1fb","sequence",740],[1538217532,"3f8","netarch",4],[1538217991,"1fb","network-fundamentals",72],[1538219894,"3f8","netarch",5],[1538232378,"f9","network-fundamentals",10],[1538232631,"f9","network-fundamentals",11],[1538233724,"8d","netarch",300],[1538234792,"42","network-fundamentals",10],[1538234809,"42","network-fundamentals",11],[1538234821,"42","network-fundamentals",12],[1538235061,"42","network-fundamentals",13],[1538235137,"0","nocode",20],[1538235206,"42","nocode",1],[1538236318,"7","sequence",50],[1538237155,"8d","netarch",400],[1538237600,"7","base",17],[1538237945,"55","sequence",19],[1538237981,"410","base",1],[1538238252,"42","codebreaking",1],[1538238344,"55","codebreaking",1],[1538238435,"55","codebreaking",2],[1538238462,"55","network-fundamentals",72],[1538238505,"7","codebreaking",1],[1538238532,"315","network-fundamentals",10],[1538238658,"7","codebreaking",2],[1538238815,"315","network-fundamentals",15],[1538238878,"8d","netarch",700],[1538238940,"5","codebreaking",1],[1538239044,"0","codebreaking",1],[1538239155,"81","codebreaking",1],[1538239291,"55","codebreaking",4],[1538239310,"315","sequence",35],[1538239338,"55","netarch",250],[1538239449,"5","codebreaking",4],[1538239523,"5","codebreaking",2],[1538239555,"315","sequence",100],[1538239589,"410","nocode",1],[1538239607,"410","nocode",2],[1538239674,"410","base",16],[1538239817,"410","base",2],[1538239845,"55","codebreaking",5],[1538239927,"410","nocode",3],[1538239967,"315","codebreaking",1],[1538240094,"315","codebreaking",2],[1538240253,"410","nocode",4],[1538240391,"410","nocode",10],[1538240457,"410","codebreaking",2],[1538240472,"410","codebreaking",1],[1538240555,"410","nocode",20],[1538240579,"d0","codebreaking",1],[1538240633,"410","codebreaking",4],[1538240746,"53","base",4],[1538240799,"5","sequence",740],[1538241016,"410","base",10],[1538241071,"d0","codebreaking",2],[1538241096,"410","sequence",1],[1538241097,"53","base",3],[1538241143,"53","base",5],[1538241150,"410","sequence",2],[1538241188,"53","base",6],[1538241237,"53","base",7],[1538241370,"7","codebreaking",4],[1538241370,"55","netarch",300],[1538241503,"53","base",9],[1538241545,"0","codebreaking",2],[1538241549,"53","base",10],[1538241564,"0","codebreaking",4],[1538241576,"7","codebreaking",5],[1538241591,"53","base",11],[1538241636,"53","base",12],[1538241652,"5","network-fundamentals",61],[1538242188,"410","netarch",1],[1538242237,"0","codebreaking",5],[1538242554,"410","sequence",16],[1538242592,"0","codebreaking",6],[1538242720,"0","codebreaking",7],[1538242908,"7","codebreaking",6],[1538243063,"a7","network-fundamentals",1],[1538243067,"8d","netarch",800],[1538243190,"44e","base",1],[1538243272,"410","sequence",25],[1538243276,"5","sequence",300],[1538243371,"a7","network-fundamentals",10],[1538243424,"44e","codebreaking",1],[1538243445,"410","sequence",35],[1538243527,"a7","network-fundamentals",11],[1538243581,"a7","network-fundamentals",12],[1538243623,"44e","netarch",1],[1538243646,"a7","network-fundamentals",13],[1538243675,"44e","network-fundamentals",1],[1538243689,"44e","nocode",1],[1538243799,"44e","sequence",1],[1538243818,"44e","sequence",2],[1538243863,"0","netarch",800],[1538243976,"a7","network-fundamentals",30],[1538244036,"410","base",32],[1538244145,"44e","sequence",16],[1538244173,"44e","sequence",25],[1538244201,"44e","sequence",35],[1538244418,"44e","sequence",50],[1538244447,"8d","network-fundamentals",1],[1538244481,"44e","sequence",60],[1538244518,"410","sequence",100],[1538244692,"44e","sequence",200],[1538244743,"53","base",13],[1538244800,"53","base",14],[1538244861,"53","base",15],[1538244914,"53","base",16],[1538244960,"8d","network-fundamentals",10],[1538244968,"53","base",17],[1538244974,"8d","network-fundamentals",11],[1538244979,"44e","sequence",300],[1538244987,"8d","network-fundamentals",12],[1538245130,"53","base",18],[1538245194,"53","base",19],[1538245234,"53","base",20],[1538245280,"53","base",21],[1538245294,"44e","nocode",2],[1538245322,"53","base",22],[1538245361,"53","base",23],[1538245368,"8d","network-fundamentals",13],[1538245401,"a7","network-fundamentals",72],[1538245401,"53","base",24],[1538245403,"2","codebreaking",1],[1538245485,"44e","nocode",4],[1538245509,"44e","nocode",3],[1538245537,"44e","nocode",10],[1538245564,"f9","network-fundamentals",14],[1538245654,"53","base",25],[1538245657,"410","nocode",90],[1538245708,"53","base",26],[1538245761,"53","base",27],[1538245766,"f9","network-fundamentals",15],[1538245826,"53","base",28],[1538245863,"410","network-fundamentals",1],[1538245945,"410","network-fundamentals",10],[1538245954,"53","base",29],[1538245986,"410","network-fundamentals",11],[1538246000,"53","base",30],[1538246000,"410","network-fundamentals",12],[1538246021,"410","network-fundamentals",13],[1538246036,"410","network-fundamentals",14],[1538246050,"53","base",31],[1538246099,"52","codebreaking",1],[1538246115,"8d","network-fundamentals",14],[1538246159,"44e","codebreaking",2],[1538246198,"8d","network-fundamentals",15],[1538246224,"8d","network-fundamentals",16],[1538246262,"2","codebreaking",2],[1538246272,"53","codebreaking",1],[1538246306,"f9","codebreaking",1],[1538246349,"52","codebreaking",2],[1538246404,"2","codebreaking",4],[1538246502,"53","codebreaking",2],[1538246566,"53","codebreaking",4],[1538246659,"24","codebreaking",1],[1538246671,"5","network-fundamentals",72],[1538246952,"410","sequence",400],[1538246991,"f9","network-fundamentals",16],[1538247200,"53","codebreaking",7],[1538247216,"f9","codebreaking",2],[1538247304,"f9","codebreaking",5],[1538247461,"5","network-fundamentals",71],[1538247530,"5","network-fundamentals",70],[1538247550,"7","base",18],[1538247745,"f9","codebreaking",4],[1538247773,"f9","base",1],[1538247812,"8d","codebreaking",1],[1538247951,"7","base",19],[1538248006,"7","base",20],[1538248036,"7","base",21],[1538248084,"7","base",22],[1538248127,"7","base",23],[1538248161,"7","base",24],[1538248186,"8d","codebreaking",2],[1538248205,"7","base",26],[1538248239,"7","base",25],[1538248278,"7","base",27],[1538248365,"7","base",28],[1538248400,"7","base",29],[1538248435,"7","base",30],[1538248464,"a7","base",32],[1538248470,"8d","codebreaking",4],[1538248585,"f9","base",2],[1538248745,"8d","codebreaking",5],[1538248952,"a7","base",16],[1538248982,"44e","codebreaking",4],[1538249105,"1fb","codebreaking",1],[1538249153,"1fb","codebreaking",2],[1538249178,"7","base",31],[1538249197,"81","codebreaking",5],[1538249267,"315","base",2],[1538249286,"7","sequence",200],[1538249389,"53","netarch",4],[1538249417,"7","sequence",300],[1538249477,"f9","network-fundamentals",61],[1538249552,"1fb","codebreaking",4],[1538249594,"410","network-fundamentals",200],[1538249630,"44e","codebreaking",5],[1538249962,"315","base",16],[1538250003,"53","netarch",5],[1538250037,"4a","codebreaking",1],[1538250101,"4a","codebreaking",2],[1538250491,"53","netarch",10],[1538250524,"44e","codebreaking",6],[1538250530,"4a","codebreaking",4],[1538250561,"81","network-fundamentals",72],[1538250744,"315","codebreaking",4],[1538250817,"7","network-fundamentals",14],[1538250913,"1fb","netarch",4],[1538251155,"7","network-fundamentals",72],[1538251456,"53","netarch",200],[1538251472,"44e","codebreaking",7],[1538251960,"7","netarch",250],[1538252500,"315","codebreaking",5],[1538253409,"a7","netarch",250],[1538253591,"7","sequence",700],[1538253711,"4d8","codebreaking",1],[1538253761,"4d8","base",1],[1538254573,"7","sequence",740],[1538254577,"5","codebreaking",8],[1538254686,"5","codebreaking",7],[1538254702,"1fb","netarch",5],[1538255622,"a7","sequence",19],[1538255694,"4d8","netarch",1],[1538255745,"4d8","nocode",1],[1538255757,"1fb","netarch",6],[1538255940,"f9","network-fundamentals",70],[1538256054,"1fb","netarch",7],[1538256186,"5","codebreaking",5],[1538256197,"4e5","base",1],[1538256299,"f9","network-fundamentals",72],[1538256305,"4d8","sequence",1],[1538256560,"4d8","network-fundamentals",1],[1538256789,"315","sequence",600],[1538256971,"4e5","base",2],[1538257007,"4d8","sequence",2],[1538257412,"4e5","codebreaking",1],[1538257439,"3","nocode",30],[1538257730,"4e5","netarch",1],[1538257755,"4e5","nocode",1],[1538257884,"4e5","sequence",1],[1538257952,"4e5","sequence",2],[1538257984,"315","sequence",740],[1538258021,"4e5","sequence",8],[1538258134,"4e5","sequence",16],[1538258161,"4e5","sequence",19],[1538258180,"4e5","sequence",25],[1538258238,"4e5","sequence",35],[1538258294,"4e5","sequence",50],[1538258597,"af","codebreaking",1],[1538258660,"55","base",16],[1538258802,"af","codebreaking",2],[1538258856,"b1","codebreaking",1],[1538259414,"af","codebreaking",4],[1538259458,"8d","codebreaking",15],[1538259621,"4e5","nocode",2],[1538259634,"4e5","nocode",3],[1538259645,"4e5","nocode",4],[1538259676,"4e5","nocode",10],[1538259787,"af","codebreaking",5],[1538260383,"0","codebreaking",50],[1538260397,"8d","codebreaking",50],[1538260415,"3","codebreaking",1],[1538260521,"4e5","base",3],[1538260553,"4e5","base",4],[1538260579,"4e5","base",5],[1538260609,"4e5","base",6],[1538260611,"3","codebreaking",2],[1538260646,"af","codebreaking",7],[1538260653,"4e5","base",7],[1538260902,"af","codebreaking",8],[1538261403,"3","codebreaking",4],[1538261903,"d0","codebreaking",5],[1538261905,"0","codebreaking",15],[1538262527,"b1","codebreaking",2],[1538262558,"3","codebreaking",50],[1538262594,"b1","sequence",8],[1538262706,"b1","sequence",50],[1538262841,"4e5","nocode",20],[1538263279,"517","network-fundamentals",10],[1538263458,"4e5","sequence",60],[1538263947,"517","network-fundamentals",1],[1538264147,"af","codebreaking",50],[1538264152,"5","base",11],[1538264243,"5","base",12],[1538264246,"3","codebreaking",5],[1538264303,"5","base",13],[1538264385,"5","base",14],[1538264417,"af","network-fundamentals",1],[1538264444,"5","base",15],[1538264572,"1fb","netarch",100],[1538264575,"5","base",16],[1538264649,"5","base",17],[1538264662,"1fb","netarch",200],[1538264693,"517","netarch",1],[1538264694,"5","base",18],[1538264749,"5","base",19],[1538264832,"5","base",20],[1538264873,"5","base",21],[1538264895,"4e5","sequence",100],[1538264919,"5","base",22],[1538264953,"af","sequence",1],[1538264961,"5","base",23],[1538264982,"af","sequence",2],[1538264985,"1fb","netarch",250],[1538265008,"af","sequence",8],[1538265057,"af","sequence",16],[1538265064,"5","base",24],[1538265075,"8d","network-fundamentals",72],[1538265079,"af","sequence",19],[1538265092,"af","sequence",25],[1538265102,"5","base",25],[1538265104,"af","sequence",35],[1538265133,"5","base",26],[1538265163,"5","base",27],[1538265194,"5","base",28],[1538265236,"5","base",29],[1538265267,"5","base",30],[1538265300,"5","base",31],[1538265302,"c","sequence",1],[1538265338,"c","sequence",2],[1538265446,"c","codebreaking",1],[1538265576,"4e5","sequence",200],[1538265778,"0","sequence",700],[1538265804,"c","nocode",90],[1538265872,"4e5","sequence",300],[1538266214,"af","sequence",100],[1538266233,"4e5","base",9],[1538266267,"4e5","base",10],[1538266325,"af","sequence",200],[1538266408,"c","sequence",35],[1538266472,"c","sequence",19],[1538266489,"c","sequence",25],[1538266607,"c","sequence",50],[1538266687,"af","sequence",300],[1538266972,"af","sequence",400],[1538267151,"4e5","base",11],[1538267206,"4e5","base",12],[1538267228,"4e5","base",13],[1538267251,"4e5","base",14],[1538267271,"4e5","base",15],[1538267300,"4e5","base",16],[1538267317,"4e5","base",17],[1538267332,"4e5","base",18],[1538267349,"4e5","base",19],[1538267371,"4e5","base",20],[1538267390,"4e5","base",21],[1538267407,"4e5","base",22],[1538267430,"4e5","base",23],[1538267453,"4e5","base",24],[1538267633,"4e5","network-fundamentals",1],[1538267683,"c","network-fundamentals",1],[1538268334,"f9","network-fundamentals",64],[1538268453,"517","nocode",1],[1538268488,"517","nocode",2],[1538269087,"1fb","netarch",300],[1538269319,"4e5","netarch",2],[1538269898,"4e5","nocode",90],[1538269993,"10c","netarch",250],[1538270313,"10c","codebreaking",1],[1538270435,"4e5","sequence",400],[1538271128,"517","nocode",4],[1538271165,"9c","codebreaking",1],[1538271210,"9c","codebreaking",2],[1538271274,"517","nocode",10],[1538271661,"103","sequence",1],[1538271682,"103","sequence",2],[1538271904,"103","sequence",16],[1538271966,"103","sequence",19],[1538271981,"103","sequence",25],[1538272000,"103","sequence",35],[1538272454,"11d","network-fundamentals",10],[1538272465,"11d","network-fundamentals",11],[1538272477,"11d","network-fundamentals",12],[1538272791,"3","codebreaking",100],[1538272822,"577","nocode",1],[1538272859,"577","nocode",2],[1538273535,"577","sequence",1],[1538273622,"3","codebreaking",200],[1538273638,"577","sequence",2],[1538275224,"5","codebreaking",50],[1538275475,"b1","reverse",200],[1538275574,"0","codebreaking",500],[1538275592,"103","codebreaking",1],[1538275666,"0","codebreaking",600],[1538275952,"0","codebreaking",200],[1538276189,"1fb","network-fundamentals",10],[1538276374,"1fb","network-fundamentals",11],[1538276452,"1fb","network-fundamentals",12],[1538276465,"103","sequence",200],[1538276491,"1fb","network-fundamentals",13],[1538276600,"1fb","network-fundamentals",14],[1538276629,"1fb","network-fundamentals",15],[1538276645,"1fb","network-fundamentals",16],[1538277168,"5","codebreaking",200],[1538277335,"1fb","network-fundamentals",50],[1538277500,"1fb","network-fundamentals",51],[1538277807,"b1","reverse",500],[1538278030,"5","network-fundamentals",60],[1538278056,"4e5","codebreaking",2],[1538278219,"d0","codebreaking",4],[1538278319,"5","network-fundamentals",50],[1538278624,"5","network-fundamentals",51],[1538278864,"3","base",16],[1538279109,"65","codebreaking",1],[1538279140,"4e5","codebreaking",5],[1538279307,"65","netarch",1],[1538279422,"5","netarch",4],[1538279458,"4e5","codebreaking",4],[1538279890,"5","network-fundamentals",64],[1538280038,"5","network-fundamentals",63],[1538280054,"b1","codebreaking",4],[1538280430,"b1","codebreaking",5],[1538280525,"5","network-fundamentals",59],[1538280603,"5","network-fundamentals",58],[1538280810,"b1","sequence",100],[1538281149,"b1","sequence",300],[1538282318,"5","network-fundamentals",55],[1538282477,"52","netarch",10],[1538282716,"5","network-fundamentals",54],[1538282956,"3","base",30],[1538283110,"3","base",29],[1538283217,"3","base",28],[1538283340,"3","base",27],[1538283420,"3","base",26],[1538283512,"3","base",25],[1538283567,"3","base",24],[1538283585,"44e","nocode",90],[1538283612,"b1","base",16],[1538283618,"3","base",23],[1538283675,"3","base",22],[1538283756,"3","base",21],[1538283819,"3","base",19],[1538284032,"3","base",18],[1538284066,"5","network-fundamentals",62],[1538284088,"3","base",17],[1538284194,"3","base",15],[1538284327,"3","base",14],[1538284389,"3","base",13],[1538284446,"3","base",12],[1538284497,"3","base",11],[1538284555,"3","base",10],[1538284614,"52","network-fundamentals",72],[1538284656,"3","base",9],[1538284786,"52","network-fundamentals",63],[1538284903,"52","network-fundamentals",50],[1538285292,"52","network-fundamentals",55],[1538285373,"3","base",31],[1538285476,"52","network-fundamentals",58],[1538285547,"52","network-fundamentals",59],[1538285643,"3","base",32],[1538285652,"52","network-fundamentals",60],[1538285685,"52","network-fundamentals",61],[1538285947,"52","network-fundamentals",62],[1538286138,"52","network-fundamentals",64],[1538286331,"0","base",58],[1538286745,"0","base",59],[1538286778,"0","base",60],[1538286828,"0","base",61],[1538286859,"0","base",62],[1538287004,"0","base",63],[1538287019,"3","base",33],[1538287065,"0","base",64],[1538287093,"3","base",64],[1538287097,"0","base",65],[1538287127,"0","base",66],[1538287210,"0","base",67],[1538287211,"44e","nocode",20],[1538287311,"0","base",68],[1538287333,"0","base",69],[1538287356,"0","base",70],[1538287377,"0","base",71],[1538287396,"0","base",72],[1538287421,"0","base",73],[1538287469,"0","base",74],[1538287490,"0","base",75],[1538287527,"0","base",76],[1538287548,"0","base",77],[1538287606,"0","base",78],[1538287634,"0","base",79],[1538287657,"0","base",80],[1538287680,"3","base",34],[1538287687,"0","base",81],[1538287723,"0","base",82],[1538287743,"0","base",83],[1538287757,"3","base",35],[1538287782,"0","base",84],[1538287799,"0","base",85],[1538287819,"0","base",86],[1538287834,"3","base",36],[1538287839,"0","base",87],[1538287858,"0","base",88],[1538287879,"0","base",89],[1538287900,"0","base",90],[1538287924,"0","base",91],[1538287943,"0","base",92],[1538287962,"0","base",93],[1538287987,"0","base",94],[1538288133,"1fb","netarch",800],[1538288618,"1fb","nocode",90],[1538288622,"0","base",117],[1538288869,"b1","base",64],[1538289075,"0","base",121],[1538289223,"1fb","nocode",20],[1538290920,"1fb","sequence",500],[1538291036,"3","base",37],[1538291205,"3","base",38],[1538291259,"3","base",39],[1538291347,"3","base",40],[1538291478,"3","base",41],[1538291529,"3","base",42],[1538291575,"3","base",43],[1538291652,"3","base",44],[1538291793,"3","base",45],[1538291840,"3","base",46],[1538291890,"3","base",47],[1538291934,"3","base",48],[1538292306,"1fb","sequence",200],[1538292692,"3","base",49],[1538292983,"3","base",50],[1538293048,"3","base",51],[1538293080,"3","base",52],[1538293160,"3","base",53],[1538293188,"3","base",54],[1538293215,"3","base",55],[1538293240,"3","base",56],[1538293267,"3","base",57],[1538293421,"1fb","codebreaking",50],[1538294553,"3","reverse",200],[1538295876,"3","base",59],[1538295923,"3","base",60],[1538295948,"3","base",61],[1538295978,"3","base",62],[1538295994,"577","sequence",8],[1538296031,"3","base",63],[1538296052,"577","sequence",16],[1538296127,"3","base",65],[1538298562,"3","base",66],[1538298614,"3","base",67],[1538298670,"3","base",68],[1538298723,"3","base",69],[1538298764,"3","base",70],[1538298806,"3","base",71],[1538298968,"3","base",72],[1538299022,"3","base",73],[1538299063,"3","base",74],[1538299104,"3","base",75],[1538299295,"3","base",76],[1538299364,"3","base",77],[1538299425,"3","base",78],[1538299465,"3","base",79],[1538299583,"3","base",80],[1538299649,"3","base",81],[1538299739,"3","base",82],[1538299907,"3","base",83],[1538299966,"3","base",84],[1538300036,"3","base",85],[1538300082,"3","base",86],[1538300133,"3","base",87],[1538300184,"3","base",88],[1538300223,"3","base",89],[1538300269,"3","base",90],[1538300307,"3","base",91],[1538300345,"3","base",92],[1538300412,"3","base",93],[1538300452,"3","base",94],[1538301325,"3","base",117],[1538316409,"81","codebreaking",6],[1538319490,"81","codebreaking",2],[1538319604,"81","base",64],[1538319732,"81","base",32],[1538320290,"81","sequence",25],[1538320325,"81","sequence",35],[1538320912,"3","reverse",500],[1538321166,"315","codebreaking",50],[1538321568,"315","codebreaking",200],[1538322484,"0","js",1],[1538322485,"52","base",64],[1538322549,"0","js",2],[1538323119,"52","base",32],[1538323940,"f9","js",2],[1538324682,"52","sequence",50],[1538325511,"52","netarch",100],[1538328017,"7","js",1],[1538328082,"7","js",2],[1538328296,"3","js",2],[1538328828,"4e5","network-fundamentals",10],[1538328845,"4e5","network-fundamentals",11],[1538328880,"4e5","network-fundamentals",12],[1538328902,"5","js",1],[1538329418,"5","js",2],[1538329436,"3","js",1],[1538329683,"5","js",3],[1538329837,"4e5","network-fundamentals",13],[1538329838,"d0","js",1],[1538329859,"d0","js",2],[1538330249,"5","js",10],[1538330676,"5","js",50],[1538330771,"8d","js",10],[1538331774,"3","js",3],[1538331935,"24","js",1],[1538332032,"24","js",2],[1538332034,"3","js",10],[1538333410,"4a","shadow",100],[1538333914,"4a","shadow",200],[1538334490,"52","network-fundamentals",10],[1538334499,"3","js",50],[1538334775,"5","base",64],[1538334794,"4e5","netarch",100],[1538334864,"81","network-fundamentals",71],[1538334894,"81","network-fundamentals",70],[1538334921,"4e5","netarch",200],[1538335361,"4e5","network-fundamentals",14],[1538335809,"24","network-fundamentals",11],[1538335909,"0","js",3],[1538335948,"0","js",10],[1538335977,"0","js",50],[1538336012,"b1","js",1],[1538336091,"1d0","codebreaking",1],[1538336134,"b1","js",2],[1538336189,"24","network-fundamentals",10],[1538336247,"4a","js",1],[1538336268,"8d","js",300],[1538336292,"4a","js",2],[1538336365,"b1","js",3],[1538336418,"24","network-fundamentals",13],[1538336469,"b1","js",10],[1538336512,"673","base",1],[1538336524,"674","base",1],[1538336544,"674","codebreaking",1],[1538336556,"b1","js",50],[1538336578,"674","nocode",1],[1538336592,"673","base",2],[1538336599,"674","sequence",1],[1538336646,"65","nocode",10],[1538336739,"d0","js",10],[1538336756,"7","js",10],[1538336832,"7","js",50],[1538336869,"65","nocode",4],[1538336900,"5","js",300],[1538337031,"4e5","codebreaking",15],[1538337036,"7","netarch",100],[1538337113,"673","nocode",1],[1538337134,"673","nocode",2],[1538337274,"7","netarch",200],[1538337393,"24","network-fundamentals",15],[1538337457,"24","network-fundamentals",16],[1538337501,"7","base",64],[1538337766,"7","netarch",800],[1538338047,"55","base",3],[1538338204,"24","network-fundamentals",40],[1538338291,"103","sequence",8],[1538338330,"d0","js",3],[1538338359,"65","netarch",250],[1538338369,"d0","js",50],[1538338371,"103","sequence",50],[1538338447,"3f9","base",1],[1538338511,"65","netarch",200],[1538338550,"3f9","codebreaking",1],[1538338641,"65","base",64],[1538338643,"55","base",4],[1538338694,"3f9","nocode",2],[1538338731,"81","network-fundamentals",61],[1538338764,"3f9","nocode",3],[1538338805,"3f9","nocode",4],[1538338884,"103","sequence",100],[1538339277,"8d","js",500],[1538339303,"0","js",300],[1538339305,"673","codebreaking",1],[1538339384,"673","codebreaking",2],[1538339486,"24","js",50],[1538339552,"65","base",32],[1538339639,"103","sequence",300],[1538339662,"55","base",5],[1538339698,"0","js",500],[1538339733,"55","base",6],[1538339741,"673","network-fundamentals",13],[1538339767,"673","network-fundamentals",14],[1538339800,"24","js",10],[1538339808,"55","base",7],[1538340352,"53","base",64],[1538340460,"55","base",10],[1538340550,"55","base",64]]} +{ + "__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] + ] +} diff --git a/theme/puzzle.html b/theme/puzzle.html index 2a8dcba..88de8dc 100644 --- a/theme/puzzle.html +++ b/theme/puzzle.html @@ -5,6 +5,7 @@ + + + + + - -

Scoreboard

+

-
-
+
+
+