mirror of https://github.com/dirtbags/moth.git
Merge branch 'master' of https://github.com/dirtbags/moth into neale
This commit is contained in:
commit
bab22b8e79
|
@ -5,9 +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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [3.4] - 2019-11-13
|
||||||
### Added
|
### Added
|
||||||
- A changelog
|
- A changelog
|
||||||
- Support for embedding Python libraries at the category or puzzle level
|
- Support for embedding Python libraries at the category or puzzle level
|
||||||
|
- Minimal PWA support to permit caching of currently-unlocked content
|
||||||
- Embedded graph in scoreboard
|
- Embedded graph in scoreboard
|
||||||
- Optional tracking of participant IDs
|
- Optional tracking of participant IDs
|
||||||
- New `notices.html` file for sending broadcast messages to players
|
- New `notices.html` file for sending broadcast messages to players
|
||||||
|
|
12
README.md
12
README.md
|
@ -133,6 +133,18 @@ We sometimes to set `teamids.txt` to a bunch of random 8-digit hex values:
|
||||||
Remember that team IDs are essentially passwords.
|
Remember that team IDs are essentially passwords.
|
||||||
|
|
||||||
|
|
||||||
|
Enabling offline/PWA mode
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
If the file `state/export_manifest` is found, the server will expose the
|
||||||
|
endpoint `/current_manifest.json?id=<teamId>`. This endpoint will return
|
||||||
|
a list of all files, including static theme content and JSON and content
|
||||||
|
for currently-unlocked puzzles. This is used by the native PWA
|
||||||
|
implementation and `Cache` button on the index page to cache all of the
|
||||||
|
content necessary to display currently-open puzzles while offline.
|
||||||
|
Grading will be unavailable while offline. Some puzzles may not function
|
||||||
|
as expected while offline. A valid team ID must be provided.
|
||||||
|
|
||||||
Mothball Directory
|
Mothball Directory
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,9 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -103,7 +106,7 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
pointstr := req.FormValue("points")
|
pointstr := req.FormValue("points")
|
||||||
answer := req.FormValue("answer")
|
answer := req.FormValue("answer")
|
||||||
|
|
||||||
if ! ctx.ValidTeamId(teamId) {
|
if !ctx.ValidTeamId(teamId) {
|
||||||
respond(
|
respond(
|
||||||
w, req, JSendFail,
|
w, req, JSendFail,
|
||||||
"Invalid team ID",
|
"Invalid team ID",
|
||||||
|
@ -247,6 +250,65 @@ func (ctx *Instance) staticHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
http.ServeContent(w, req, path, d.ModTime(), f)
|
http.ServeContent(w, req, path, d.ModTime(), f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *Instance) manifestHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if !ctx.Runtime.export_manifest {
|
||||||
|
http.Error(w, "Endpoint disabled", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
teamId := req.FormValue("id")
|
||||||
|
if _, err := ctx.TeamName(teamId); err != nil {
|
||||||
|
http.Error(w, "Must provide a valid team ID", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Method == http.MethodHead {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest := make([]string, 0)
|
||||||
|
manifest = append(manifest, "puzzles.json")
|
||||||
|
manifest = append(manifest, "points.json")
|
||||||
|
|
||||||
|
// Pack up the theme files
|
||||||
|
theme_root_re := regexp.MustCompile(fmt.Sprintf("^%s/", ctx.ThemeDir))
|
||||||
|
filepath.Walk(ctx.ThemeDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() { // Only package up files
|
||||||
|
localized_path := theme_root_re.ReplaceAllLiteralString(path, "")
|
||||||
|
manifest = append(manifest, localized_path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Package up files for currently-unlocked puzzles in categories
|
||||||
|
for category_name, category := range ctx.categories {
|
||||||
|
if _, ok := ctx.MaxPointsUnlocked[category_name]; ok { // Check that the category is actually unlocked. This should never fail, probably
|
||||||
|
for _, file := range category.zf.File {
|
||||||
|
parts := strings.Split(file.Name, "/")
|
||||||
|
|
||||||
|
if parts[0] == "content" { // Only pick up content files, not thing like map.txt
|
||||||
|
for _, puzzlemap := range category.puzzlemap { // Figure out which puzzles are currently unlocked
|
||||||
|
if puzzlemap.Path == parts[1] && puzzlemap.Points <= ctx.MaxPointsUnlocked[category_name] {
|
||||||
|
|
||||||
|
manifest = append(manifest, path.Join("content", category_name, path.Join(parts[1:]...)))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
manifest_json, _ := json.Marshal(manifest)
|
||||||
|
w.Write(manifest_json)
|
||||||
|
}
|
||||||
|
|
||||||
type FurtiveResponseWriter struct {
|
type FurtiveResponseWriter struct {
|
||||||
w http.ResponseWriter
|
w http.ResponseWriter
|
||||||
statusCode *int
|
statusCode *int
|
||||||
|
@ -289,4 +351,5 @@ func (ctx *Instance) BindHandlers() {
|
||||||
ctx.mux.HandleFunc(ctx.Base+"/content/", ctx.contentHandler)
|
ctx.mux.HandleFunc(ctx.Base+"/content/", ctx.contentHandler)
|
||||||
ctx.mux.HandleFunc(ctx.Base+"/puzzles.json", ctx.puzzlesHandler)
|
ctx.mux.HandleFunc(ctx.Base+"/puzzles.json", ctx.puzzlesHandler)
|
||||||
ctx.mux.HandleFunc(ctx.Base+"/points.json", ctx.pointsHandler)
|
ctx.mux.HandleFunc(ctx.Base+"/points.json", ctx.pointsHandler)
|
||||||
|
ctx.mux.HandleFunc(ctx.Base+"/current_manifest.json", ctx.manifestHandler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RuntimeConfig struct {
|
||||||
|
export_manifest bool
|
||||||
|
}
|
||||||
|
|
||||||
type Instance struct {
|
type Instance struct {
|
||||||
Base string
|
Base string
|
||||||
MothballDir string
|
MothballDir string
|
||||||
|
@ -22,13 +26,16 @@ type Instance struct {
|
||||||
ThemeDir string
|
ThemeDir string
|
||||||
AttemptInterval time.Duration
|
AttemptInterval time.Duration
|
||||||
|
|
||||||
categories map[string]*Mothball
|
Runtime RuntimeConfig
|
||||||
update chan bool
|
|
||||||
jPuzzleList []byte
|
categories map[string]*Mothball
|
||||||
jPointsLog []byte
|
MaxPointsUnlocked map[string]int
|
||||||
nextAttempt map[string]time.Time
|
update chan bool
|
||||||
nextAttemptMutex *sync.RWMutex
|
jPuzzleList []byte
|
||||||
mux *http.ServeMux
|
jPointsLog []byte
|
||||||
|
nextAttempt map[string]time.Time
|
||||||
|
nextAttemptMutex *sync.RWMutex
|
||||||
|
mux *http.ServeMux
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) Initialize() error {
|
func (ctx *Instance) Initialize() error {
|
||||||
|
@ -132,15 +139,15 @@ func (ctx *Instance) ThemePath(parts ...string) string {
|
||||||
|
|
||||||
func (ctx *Instance) TooFast(teamId string) bool {
|
func (ctx *Instance) TooFast(teamId string) bool {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
ctx.nextAttemptMutex.RLock()
|
ctx.nextAttemptMutex.RLock()
|
||||||
next, _ := ctx.nextAttempt[teamId]
|
next, _ := ctx.nextAttempt[teamId]
|
||||||
ctx.nextAttemptMutex.RUnlock()
|
ctx.nextAttemptMutex.RUnlock()
|
||||||
|
|
||||||
ctx.nextAttemptMutex.Lock()
|
ctx.nextAttemptMutex.Lock()
|
||||||
ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval)
|
ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval)
|
||||||
ctx.nextAttemptMutex.Unlock()
|
ctx.nextAttemptMutex.Unlock()
|
||||||
|
|
||||||
return now.Before(next)
|
return now.Before(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PuzzleMap struct {
|
|
||||||
Points int
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *PuzzleMap) MarshalJSON() ([]byte, error) {
|
func (pm *PuzzleMap) MarshalJSON() ([]byte, error) {
|
||||||
if pm == nil {
|
if pm == nil {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
|
@ -41,45 +36,29 @@ func (ctx *Instance) generatePuzzleList() {
|
||||||
|
|
||||||
ret := map[string][]PuzzleMap{}
|
ret := map[string][]PuzzleMap{}
|
||||||
for catName, mb := range ctx.categories {
|
for catName, mb := range ctx.categories {
|
||||||
mf, err := mb.Open("map.txt")
|
filtered_puzzlemap := make([]PuzzleMap, 0, 30)
|
||||||
if err != nil {
|
|
||||||
// File isn't in there
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer mf.Close()
|
|
||||||
|
|
||||||
pm := make([]PuzzleMap, 0, 30)
|
|
||||||
completed := true
|
completed := true
|
||||||
scanner := bufio.NewScanner(mf)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
|
|
||||||
var pointval int
|
for _, pm := range mb.puzzlemap {
|
||||||
var dir string
|
filtered_puzzlemap = append(filtered_puzzlemap, pm)
|
||||||
|
|
||||||
n, err := fmt.Sscanf(line, "%d %s", &pointval, &dir)
|
if pm.Points > maxByCategory[catName] {
|
||||||
if err != nil {
|
|
||||||
log.Printf("Parsing map for %s: %v", catName, err)
|
|
||||||
continue
|
|
||||||
} else if n != 2 {
|
|
||||||
log.Printf("Parsing map for %s: short read", catName)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pm = append(pm, PuzzleMap{pointval, dir})
|
|
||||||
|
|
||||||
if pointval > maxByCategory[catName] {
|
|
||||||
completed = false
|
completed = false
|
||||||
|
maxByCategory[catName] = pm.Points
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if completed {
|
if completed {
|
||||||
pm = append(pm, PuzzleMap{0, ""})
|
filtered_puzzlemap = append(filtered_puzzlemap, PuzzleMap{0, ""})
|
||||||
}
|
}
|
||||||
|
|
||||||
ret[catName] = pm
|
ret[catName] = filtered_puzzlemap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache the unlocked points for use in other functions
|
||||||
|
ctx.MaxPointsUnlocked = maxByCategory
|
||||||
|
|
||||||
jpl, err := json.Marshal(ret)
|
jpl, err := json.Marshal(ret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Marshalling puzzles.js: %v", err)
|
log.Printf("Marshalling puzzles.js: %v", err)
|
||||||
|
@ -124,6 +103,9 @@ func (ctx *Instance) tidy() {
|
||||||
// Do they want to reset everything?
|
// Do they want to reset everything?
|
||||||
ctx.MaybeInitialize()
|
ctx.MaybeInitialize()
|
||||||
|
|
||||||
|
// Check set config
|
||||||
|
ctx.UpdateConfig()
|
||||||
|
|
||||||
// Refresh all current categories
|
// Refresh all current categories
|
||||||
for categoryName, mb := range ctx.categories {
|
for categoryName, mb := range ctx.categories {
|
||||||
if err := mb.Refresh(); err != nil {
|
if err := mb.Refresh(); err != nil {
|
||||||
|
@ -286,6 +268,20 @@ func (ctx *Instance) isEnabled() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *Instance) UpdateConfig() {
|
||||||
|
// Handle export manifest
|
||||||
|
if _, err := os.Stat(ctx.StatePath("export_manifest")); err == nil {
|
||||||
|
if !ctx.Runtime.export_manifest {
|
||||||
|
log.Print("Enabling manifest export")
|
||||||
|
ctx.Runtime.export_manifest = true
|
||||||
|
}
|
||||||
|
} else if ctx.Runtime.export_manifest {
|
||||||
|
log.Print("Disabling manifest export")
|
||||||
|
ctx.Runtime.export_manifest = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// maintenance is the goroutine that runs a periodic maintenance task
|
// maintenance is the goroutine that runs a periodic maintenance task
|
||||||
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
||||||
for {
|
for {
|
||||||
|
|
|
@ -2,18 +2,26 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PuzzleMap struct {
|
||||||
|
Points int
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
type Mothball struct {
|
type Mothball struct {
|
||||||
zf *zip.ReadCloser
|
zf *zip.ReadCloser
|
||||||
filename string
|
filename string
|
||||||
mtime time.Time
|
puzzlemap []PuzzleMap
|
||||||
|
mtime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type MothballFile struct {
|
type MothballFile struct {
|
||||||
|
@ -150,6 +158,35 @@ func (m *Mothball) Refresh() error {
|
||||||
m.zf = zf
|
m.zf = zf
|
||||||
m.mtime = mtime
|
m.mtime = mtime
|
||||||
|
|
||||||
|
mf, err := m.Open("map.txt")
|
||||||
|
if err != nil {
|
||||||
|
// File isn't in there
|
||||||
|
} else {
|
||||||
|
defer mf.Close()
|
||||||
|
|
||||||
|
pm := make([]PuzzleMap, 0, 30)
|
||||||
|
scanner := bufio.NewScanner(mf)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
var pointval int
|
||||||
|
var dir string
|
||||||
|
|
||||||
|
n, err := fmt.Sscanf(line, "%d %s", &pointval, &dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Parsing map for %v", err)
|
||||||
|
} else if n != 2 {
|
||||||
|
log.Printf("Parsing map: short read")
|
||||||
|
}
|
||||||
|
|
||||||
|
pm = append(pm, PuzzleMap{pointval, dir})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
m.puzzlemap = pm
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,3 +136,7 @@ li[draggable] {
|
||||||
[draggable].over {
|
[draggable].over {
|
||||||
border: 1px white dashed;
|
border: 1px white dashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cacheButton.disabled {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,9 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
<link rel="stylesheet" href="basic.css">
|
<link rel="stylesheet" href="basic.css">
|
||||||
|
<script src="moth-pwa.js" type="text/javascript"></script>
|
||||||
<script src="moth.js"></script>
|
<script src="moth.js"></script>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1 id="title">MOTH</h1>
|
<h1 id="title">MOTH</h1>
|
||||||
|
@ -27,11 +29,12 @@
|
||||||
|
|
||||||
<div id="puzzles"></div>
|
<div id="puzzles"></div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="scoreboard.html">Scoreboard</a></li>
|
<li><a href="scoreboard.html">Scoreboard</a></li>
|
||||||
<li><a href="logout.html">Sign Out</a></li>
|
<li><a href="logout.html">Sign Out</a></li>
|
||||||
|
<li id="cacheButton" class="disabled"><a href="#" onclick='fetchAll()' title="Cache am offline copy of current content">Cache</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
function pwa_init() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register("./sw.js").then(function(reg) {
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
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()
|
||||||
|
}
|
|
@ -88,6 +88,7 @@ function renderPuzzles(obj) {
|
||||||
container.appendChild(puzzlesElement)
|
container.appendChild(puzzlesElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function heartbeat(teamId, participantId) {
|
function heartbeat(teamId, participantId) {
|
||||||
let noticesUrl = new URL("notices.html", window.location)
|
let noticesUrl = new URL("notices.html", window.location)
|
||||||
fetch(noticesUrl)
|
fetch(noticesUrl)
|
||||||
|
@ -137,6 +138,90 @@ function showPuzzles(teamId, participantId) {
|
||||||
document.getElementById("puzzles").appendChild(spinner)
|
document.getElementById("puzzles").appendChild(spinner)
|
||||||
heartbeat(teamId, participantId)
|
heartbeat(teamId, participantId)
|
||||||
setInterval(e => { heartbeat(teamId) }, 40000)
|
setInterval(e => { heartbeat(teamId) }, 40000)
|
||||||
|
drawCacheButton(teamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCacheButton(teamId) {
|
||||||
|
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.getItem("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) {
|
function login(e) {
|
||||||
|
@ -186,12 +271,13 @@ function init() {
|
||||||
if (teamId) {
|
if (teamId) {
|
||||||
showPuzzles(teamId, participantId)
|
showPuzzles(teamId, participantId)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("login").addEventListener("submit", login)
|
document.getElementById("login").addEventListener("submit", login)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", init);
|
document.addEventListener("DOMContentLoaded", init)
|
||||||
} else {
|
} else {
|
||||||
init();
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<link rel="stylesheet" href="basic.css">
|
<link rel="stylesheet" href="basic.css">
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<script src="moth-pwa.js"></script>
|
||||||
<script src="puzzle.js"></script>
|
<script src="puzzle.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<title>Scoreboard</title>
|
<title>Scoreboard</title>
|
||||||
<link rel="stylesheet" href="basic.css">
|
<link rel="stylesheet" href="basic.css">
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<script src="moth-pwa.js"></script>
|
||||||
<script src="moment.min.js" async></script>
|
<script src="moment.min.js" async></script>
|
||||||
<script src="Chart.min.js" async></script>
|
<script src="Chart.min.js" async></script>
|
||||||
<script src="scoreboard.js" async></script>
|
<script src="scoreboard.js" async></script>
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
var cacheName = "moth:v1"
|
||||||
|
var content = [
|
||||||
|
"index.html",
|
||||||
|
"basic.css",
|
||||||
|
"puzzle.js",
|
||||||
|
"puzzle.html",
|
||||||
|
"scoreboard.html",
|
||||||
|
"moth.js",
|
||||||
|
"sw.js",
|
||||||
|
"points.json",
|
||||||
|
]
|
||||||
|
|
||||||
|
self.addEventListener("install", function(e) {
|
||||||
|
e.waitUntil(
|
||||||
|
caches.open(cacheName).then(function(cache) {
|
||||||
|
return cache.addAll(content).then(
|
||||||
|
function() {
|
||||||
|
self.skipWaiting()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
/* Attempt to fetch live resources, first, then fall back to cache */
|
||||||
|
self.addEventListener('fetch', function(event) {
|
||||||
|
let cache_used = false
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.catch(function(evt) {
|
||||||
|
//console.log("Falling back to cache for " + event.request.url)
|
||||||
|
cache_used = true
|
||||||
|
return caches.match(event.request, {ignoreSearch: true})
|
||||||
|
}).then(function(res) {
|
||||||
|
if (res && res.ok) {
|
||||||
|
let res_clone = res.clone()
|
||||||
|
if (! cache_used && event.request.method == "GET" ) {
|
||||||
|
caches.open(cacheName).then(function(cache) {
|
||||||
|
cache.put(event.request, res_clone)
|
||||||
|
//console.log("Storing " + event.request.url + " in cache")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
} else {
|
||||||
|
console.log("Failed to retrieve resource")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
Loading…
Reference in New Issue