mirror of https://github.com/dirtbags/moth.git
Merge branch 'master' of https://github.com/dirtbags/moth
This commit is contained in:
commit
dab09db585
|
@ -16,6 +16,5 @@ COPY devel /app/
|
||||||
COPY example-puzzles /puzzles/
|
COPY example-puzzles /puzzles/
|
||||||
COPY theme /theme/
|
COPY theme /theme/
|
||||||
|
|
||||||
WORKDIR /moth/
|
|
||||||
ENTRYPOINT [ "python3", "/app/devel-server.py" ]
|
ENTRYPOINT [ "python3", "/app/devel-server.py" ]
|
||||||
CMD [ "--bind", "0.0.0.0:8080", "--puzzles", "/puzzles", "--theme", "/theme" ]
|
CMD [ "--bind", "0.0.0.0:8080", "--puzzles", "/puzzles", "--theme", "/theme" ]
|
||||||
|
|
|
@ -23,8 +23,14 @@ and comes with a JavaScript-based scoreboard to display team rankings.
|
||||||
Running a Development Server
|
Running a Development Server
|
||||||
============================
|
============================
|
||||||
|
|
||||||
|
To use example puzzles
|
||||||
|
|
||||||
docker run --rm -it -p 8080:8080 dirtbags/moth-devel
|
docker run --rm -it -p 8080:8080 dirtbags/moth-devel
|
||||||
|
|
||||||
|
or, to use your own puzzles
|
||||||
|
|
||||||
|
docker run --rm -it -p 8080:8080 -v /path/to/puzzles:/puzzles:ro dirtbags/moth-devel
|
||||||
|
|
||||||
And point a browser to http://localhost:8080/ (or whatever host is running the server).
|
And point a browser to http://localhost:8080/ (or whatever host is running the server).
|
||||||
|
|
||||||
The development server includes a number of Python libraries that we have found useful in writing puzzles.
|
The development server includes a number of Python libraries that we have found useful in writing puzzles.
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
## Run two of these to trigger the race condition from
|
||||||
|
|
||||||
|
BASEURL=http://localhost:8080
|
||||||
|
URL=$BASEURL/answer
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
curl \
|
||||||
|
-X POST \
|
||||||
|
-F "cat=byobf" \
|
||||||
|
-F "points=10" \
|
||||||
|
-F "id=test" \
|
||||||
|
-F "answer=6" \
|
||||||
|
$URL
|
||||||
|
done
|
|
@ -14,18 +14,25 @@ import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
import socketserver
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import mothballer
|
import mothballer
|
||||||
import parse
|
import parse
|
||||||
|
import urllib.parse
|
||||||
|
import posixpath
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
|
||||||
sys.dont_write_bytecode = True # Don't write .pyc files
|
sys.dont_write_bytecode = True # Don't write .pyc files
|
||||||
|
|
||||||
|
try:
|
||||||
|
ThreadingHTTPServer = http.server.ThreadingHTTPServer
|
||||||
|
except AttributeError:
|
||||||
|
import socketserver
|
||||||
|
class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
||||||
|
daemon_threads = True
|
||||||
|
|
||||||
class MothServer(http.server.ThreadingHTTPServer):
|
class MothServer(ThreadingHTTPServer):
|
||||||
def __init__(self, server_address, RequestHandlerClass):
|
def __init__(self, server_address, RequestHandlerClass):
|
||||||
super().__init__(server_address, RequestHandlerClass)
|
super().__init__(server_address, RequestHandlerClass)
|
||||||
self.args = {}
|
self.args = {}
|
||||||
|
@ -35,13 +42,28 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
endpoints = []
|
endpoints = []
|
||||||
|
|
||||||
def __init__(self, request, client_address, server):
|
def __init__(self, request, client_address, server):
|
||||||
super().__init__(request, client_address, server, directory=server.args["theme_dir"])
|
self.directory = str(server.args["theme_dir"])
|
||||||
|
try:
|
||||||
|
super().__init__(request, client_address, server, directory=server.args["theme_dir"])
|
||||||
|
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
|
||||||
|
getcwd = os.getcwd
|
||||||
|
os.getcwd = lambda: self.directory
|
||||||
|
ret = super().translate_path(path)
|
||||||
|
os.getcwd = getcwd
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def get_puzzle(self):
|
def get_puzzle(self):
|
||||||
category = self.req.get("cat")
|
category = self.req.get("cat")
|
||||||
points = int(self.req.get("points"))
|
points = int(self.req.get("points"))
|
||||||
cat = moth.Category(self.server.args["puzzles_dir"].joinpath(category), self.seed)
|
catpath = str(self.server.args["puzzles_dir"].joinpath(category))
|
||||||
|
cat = moth.Category(catpath, self.seed)
|
||||||
puzzle = cat.puzzle(points)
|
puzzle = cat.puzzle(points)
|
||||||
return puzzle
|
return puzzle
|
||||||
|
|
||||||
|
@ -75,7 +97,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
if not p.is_dir() or p.match(".*"):
|
if not p.is_dir() or p.match(".*"):
|
||||||
continue
|
continue
|
||||||
catName = p.parts[-1]
|
catName = p.parts[-1]
|
||||||
cat = moth.Category(p, self.seed)
|
cat = moth.Category(str(p), self.seed)
|
||||||
puzzles[catName] = [[i, str(i)] for i in cat.pointvals()]
|
puzzles[catName] = [[i, str(i)] for i in cat.pointvals()]
|
||||||
puzzles[catName].append([0, ""])
|
puzzles[catName].append([0, ""])
|
||||||
if len(puzzles) <= 1:
|
if len(puzzles) <= 1:
|
||||||
|
@ -255,8 +277,6 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
mydir = os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0])))
|
|
||||||
|
|
||||||
server = MothServer((addr, port), MothRequestHandler)
|
server = MothServer((addr, port), MothRequestHandler)
|
||||||
server.args["base_url"] = args.base
|
server.args["base_url"] = args.base
|
||||||
server.args["puzzles_dir"] = pathlib.Path(args.puzzles)
|
server.args["puzzles_dir"] = pathlib.Path(args.puzzles)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import os
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import shlex
|
||||||
|
|
||||||
messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||||
|
|
||||||
|
@ -112,7 +113,7 @@ class Puzzle:
|
||||||
elif key == 'name':
|
elif key == 'name':
|
||||||
pass
|
pass
|
||||||
elif key == 'file':
|
elif key == 'file':
|
||||||
parts = val.split()
|
parts = shlex.split(val)
|
||||||
name = parts[0]
|
name = parts[0]
|
||||||
hidden = False
|
hidden = False
|
||||||
stream = open(name, 'rb')
|
stream = open(name, 'rb')
|
||||||
|
@ -264,10 +265,10 @@ class Puzzle:
|
||||||
def html_body(self):
|
def html_body(self):
|
||||||
"""Format and return the markdown for the puzzle body."""
|
"""Format and return the markdown for the puzzle body."""
|
||||||
return mistune.markdown(self.get_body(), escape=False)
|
return mistune.markdown(self.get_body(), escape=False)
|
||||||
|
|
||||||
def package(self, answers=False):
|
def package(self, answers=False):
|
||||||
"""Return a dict packaging of the puzzle."""
|
"""Return a dict packaging of the puzzle."""
|
||||||
|
|
||||||
files = [fn for fn,f in self.files.items() if f.visible]
|
files = [fn for fn,f in self.files.items() if f.visible]
|
||||||
return {
|
return {
|
||||||
'authors': self.authors,
|
'authors': self.authors,
|
||||||
|
|
|
@ -5,6 +5,7 @@ Being in this list is voluntary. Add your name when you contribute code.
|
||||||
* Paul Ferrell
|
* Paul Ferrell
|
||||||
* Shannon Steinfadt
|
* Shannon Steinfadt
|
||||||
* John Donaldson
|
* John Donaldson
|
||||||
|
* 3ch01c
|
||||||
|
|
||||||
Word List
|
Word List
|
||||||
---------
|
---------
|
||||||
|
|
|
@ -17,7 +17,14 @@ function helperUpdateAnswer(event) {
|
||||||
values.push(c.value)
|
values.push(c.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
value = values.join(",")
|
if (e.classList.contains("sort")) {
|
||||||
|
values.sort()
|
||||||
|
}
|
||||||
|
let join = e.dataset.join
|
||||||
|
if (join === undefined) {
|
||||||
|
join = ","
|
||||||
|
}
|
||||||
|
value = values.join(join)
|
||||||
}
|
}
|
||||||
|
|
||||||
// First make any adjustments to the value
|
// First make any adjustments to the value
|
||||||
|
@ -33,8 +40,42 @@ function helperUpdateAnswer(event) {
|
||||||
answer.dispatchEvent(new InputEvent("input"))
|
answer.dispatchEvent(new InputEvent("input"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function helperRemoveInput(e) {
|
||||||
|
let item = e.target.parentElement
|
||||||
|
let container = item.parentElement
|
||||||
|
item.remove()
|
||||||
|
|
||||||
|
var event = new Event("input")
|
||||||
|
container.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
function helperExpandInputs(e) {
|
||||||
|
let item = e.target.parentElement
|
||||||
|
let container = item.parentElement
|
||||||
|
let template = container.firstElementChild
|
||||||
|
let newElement = template.cloneNode(true)
|
||||||
|
|
||||||
|
// Add remove button
|
||||||
|
let remove = document.createElement("button")
|
||||||
|
remove.innerText = "➖"
|
||||||
|
remove.title = "Remove this input"
|
||||||
|
remove.addEventListener("click", helperRemoveInput)
|
||||||
|
newElement.appendChild(remove)
|
||||||
|
|
||||||
|
// Zero it out, otherwise whatever's in first element is copied too
|
||||||
|
newElement.querySelector("input").value = ""
|
||||||
|
|
||||||
|
container.insertBefore(newElement, item)
|
||||||
|
|
||||||
|
var event = new Event("input")
|
||||||
|
container.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
function helperActivate(e) {
|
function helperActivate(e) {
|
||||||
e.addEventListener("input", helperUpdateAnswer)
|
e.addEventListener("input", helperUpdateAnswer)
|
||||||
|
for (let exp of e.querySelectorAll(".expand")) {
|
||||||
|
exp.addEventListener("click", helperExpandInputs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function helperInit(event) {
|
function helperInit(event) {
|
||||||
|
|
|
@ -17,8 +17,13 @@ This is just a demonstration page.
|
||||||
You will probably only want one of these in a page,
|
You will probably only want one of these in a page,
|
||||||
to avoid confusing people.
|
to avoid confusing people.
|
||||||
|
|
||||||
Timestamp
|
RFC3339 Timestamp
|
||||||
<input type="datetime-local" class="answer">
|
<div class="answer" data-join="">
|
||||||
|
<input type="date">
|
||||||
|
<input type="hidden" value="T">
|
||||||
|
<input type="time" step="1">
|
||||||
|
<input type="hidden" value="Z">
|
||||||
|
</div>
|
||||||
|
|
||||||
All lower-case letters
|
All lower-case letters
|
||||||
<input class="answer lower">
|
<input class="answer lower">
|
||||||
|
@ -31,6 +36,12 @@ Multiple concatenated values
|
||||||
<input>
|
<input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Free input, sorted, concatenated values
|
||||||
|
<ul class="answer lower sort">
|
||||||
|
<li><input></li>
|
||||||
|
<li><button class="expand" title="Add another input">➕</button><l/i>
|
||||||
|
</ul>
|
||||||
|
|
||||||
Select from an ordered list of options
|
Select from an ordered list of options
|
||||||
<ul class="answer">
|
<ul class="answer">
|
||||||
<li><input type="checkbox" value="horn">Horns</li>
|
<li><input type="checkbox" value="horn">Horns</li>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,12 +22,13 @@ type Instance struct {
|
||||||
ThemeDir string
|
ThemeDir string
|
||||||
AttemptInterval time.Duration
|
AttemptInterval time.Duration
|
||||||
|
|
||||||
categories map[string]*Mothball
|
categories map[string]*Mothball
|
||||||
update chan bool
|
update chan bool
|
||||||
jPuzzleList []byte
|
jPuzzleList []byte
|
||||||
jPointsLog []byte
|
jPointsLog []byte
|
||||||
nextAttempt map[string]time.Time
|
nextAttempt map[string]time.Time
|
||||||
mux *http.ServeMux
|
nextAttemptMutex *sync.RWMutex
|
||||||
|
mux *http.ServeMux
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) Initialize() error {
|
func (ctx *Instance) Initialize() error {
|
||||||
|
@ -42,6 +44,7 @@ func (ctx *Instance) Initialize() error {
|
||||||
ctx.categories = map[string]*Mothball{}
|
ctx.categories = map[string]*Mothball{}
|
||||||
ctx.update = make(chan bool, 10)
|
ctx.update = make(chan bool, 10)
|
||||||
ctx.nextAttempt = map[string]time.Time{}
|
ctx.nextAttempt = map[string]time.Time{}
|
||||||
|
ctx.nextAttemptMutex = new(sync.RWMutex)
|
||||||
ctx.mux = http.NewServeMux()
|
ctx.mux = http.NewServeMux()
|
||||||
|
|
||||||
ctx.BindHandlers()
|
ctx.BindHandlers()
|
||||||
|
@ -129,8 +132,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()
|
||||||
next, _ := ctx.nextAttempt[teamId]
|
next, _ := ctx.nextAttempt[teamId]
|
||||||
|
ctx.nextAttemptMutex.RUnlock()
|
||||||
|
|
||||||
|
ctx.nextAttemptMutex.Lock()
|
||||||
ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval)
|
ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval)
|
||||||
|
ctx.nextAttemptMutex.Unlock()
|
||||||
|
|
||||||
return now.Before(next)
|
return now.Before(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,7 +222,10 @@ func (ctx *Instance) OpenCategoryFile(category string, parts ...string) (io.Read
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) ValidTeamId(teamId string) bool {
|
func (ctx *Instance) ValidTeamId(teamId string) bool {
|
||||||
|
ctx.nextAttemptMutex.RLock()
|
||||||
_, ok := ctx.nextAttempt[teamId]
|
_, ok := ctx.nextAttempt[teamId]
|
||||||
|
ctx.nextAttemptMutex.RUnlock()
|
||||||
|
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -186,19 +186,28 @@ func (ctx *Instance) readTeams() {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
added := 0
|
added := 0
|
||||||
for k, _ := range newList {
|
for k, _ := range newList {
|
||||||
if _, ok := ctx.nextAttempt[k]; !ok {
|
ctx.nextAttemptMutex.RLock()
|
||||||
|
_, ok := ctx.nextAttempt[k]
|
||||||
|
ctx.nextAttemptMutex.RUnlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
ctx.nextAttemptMutex.Lock()
|
||||||
ctx.nextAttempt[k] = now
|
ctx.nextAttempt[k] = now
|
||||||
|
ctx.nextAttemptMutex.Unlock()
|
||||||
|
|
||||||
added += 1
|
added += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For any removed team IDs, remove them
|
// For any removed team IDs, remove them
|
||||||
removed := 0
|
removed := 0
|
||||||
|
ctx.nextAttemptMutex.Lock() // XXX: This could be less of a cludgel
|
||||||
for k, _ := range ctx.nextAttempt {
|
for k, _ := range ctx.nextAttempt {
|
||||||
if _, ok := newList[k]; !ok {
|
if _, ok := newList[k]; !ok {
|
||||||
delete(ctx.nextAttempt, k)
|
delete(ctx.nextAttempt, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ctx.nextAttemptMutex.Unlock()
|
||||||
|
|
||||||
if (added > 0) || (removed > 0) {
|
if (added > 0) || (removed > 0) {
|
||||||
log.Printf("Team IDs updated: %d added, %d removed", added, removed)
|
log.Printf("Team IDs updated: %d added, %d removed", added, removed)
|
||||||
|
|
Loading…
Reference in New Issue