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 theme /theme/
|
||||
|
||||
WORKDIR /moth/
|
||||
ENTRYPOINT [ "python3", "/app/devel-server.py" ]
|
||||
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
|
||||
============================
|
||||
|
||||
To use example puzzles
|
||||
|
||||
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).
|
||||
|
||||
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 random
|
||||
import shutil
|
||||
import socketserver
|
||||
import sys
|
||||
import traceback
|
||||
import mothballer
|
||||
import parse
|
||||
import urllib.parse
|
||||
import posixpath
|
||||
from http import HTTPStatus
|
||||
|
||||
|
||||
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):
|
||||
super().__init__(server_address, RequestHandlerClass)
|
||||
self.args = {}
|
||||
|
@ -35,13 +42,28 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
endpoints = []
|
||||
|
||||
def __init__(self, request, client_address, server):
|
||||
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):
|
||||
category = self.req.get("cat")
|
||||
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)
|
||||
return puzzle
|
||||
|
||||
|
@ -75,7 +97,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
if not p.is_dir() or p.match(".*"):
|
||||
continue
|
||||
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].append([0, ""])
|
||||
if len(puzzles) <= 1:
|
||||
|
@ -255,8 +277,6 @@ if __name__ == '__main__':
|
|||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
mydir = os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0])))
|
||||
|
||||
server = MothServer((addr, port), MothRequestHandler)
|
||||
server.args["base_url"] = args.base
|
||||
server.args["puzzles_dir"] = pathlib.Path(args.puzzles)
|
||||
|
|
|
@ -12,6 +12,7 @@ import os
|
|||
import random
|
||||
import string
|
||||
import tempfile
|
||||
import shlex
|
||||
|
||||
messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
|
||||
|
@ -112,7 +113,7 @@ class Puzzle:
|
|||
elif key == 'name':
|
||||
pass
|
||||
elif key == 'file':
|
||||
parts = val.split()
|
||||
parts = shlex.split(val)
|
||||
name = parts[0]
|
||||
hidden = False
|
||||
stream = open(name, 'rb')
|
||||
|
|
|
@ -5,6 +5,7 @@ Being in this list is voluntary. Add your name when you contribute code.
|
|||
* Paul Ferrell
|
||||
* Shannon Steinfadt
|
||||
* John Donaldson
|
||||
* 3ch01c
|
||||
|
||||
Word List
|
||||
---------
|
||||
|
|
|
@ -17,7 +17,14 @@ function helperUpdateAnswer(event) {
|
|||
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
|
||||
|
@ -33,8 +40,42 @@ function helperUpdateAnswer(event) {
|
|||
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) {
|
||||
e.addEventListener("input", helperUpdateAnswer)
|
||||
for (let exp of e.querySelectorAll(".expand")) {
|
||||
exp.addEventListener("click", helperExpandInputs)
|
||||
}
|
||||
}
|
||||
|
||||
function helperInit(event) {
|
||||
|
|
|
@ -17,8 +17,13 @@ This is just a demonstration page.
|
|||
You will probably only want one of these in a page,
|
||||
to avoid confusing people.
|
||||
|
||||
Timestamp
|
||||
<input type="datetime-local" class="answer">
|
||||
RFC3339 Timestamp
|
||||
<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
|
||||
<input class="answer lower">
|
||||
|
@ -31,6 +36,12 @@ Multiple concatenated values
|
|||
<input>
|
||||
</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
|
||||
<ul class="answer">
|
||||
<li><input type="checkbox" value="horn">Horns</li>
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -26,6 +27,7 @@ type Instance struct {
|
|||
jPuzzleList []byte
|
||||
jPointsLog []byte
|
||||
nextAttempt map[string]time.Time
|
||||
nextAttemptMutex *sync.RWMutex
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
|
@ -42,6 +44,7 @@ func (ctx *Instance) Initialize() error {
|
|||
ctx.categories = map[string]*Mothball{}
|
||||
ctx.update = make(chan bool, 10)
|
||||
ctx.nextAttempt = map[string]time.Time{}
|
||||
ctx.nextAttemptMutex = new(sync.RWMutex)
|
||||
ctx.mux = http.NewServeMux()
|
||||
|
||||
ctx.BindHandlers()
|
||||
|
@ -129,8 +132,15 @@ func (ctx *Instance) ThemePath(parts ...string) string {
|
|||
|
||||
func (ctx *Instance) TooFast(teamId string) bool {
|
||||
now := time.Now()
|
||||
|
||||
ctx.nextAttemptMutex.RLock()
|
||||
next, _ := ctx.nextAttempt[teamId]
|
||||
ctx.nextAttemptMutex.RUnlock()
|
||||
|
||||
ctx.nextAttemptMutex.Lock()
|
||||
ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval)
|
||||
ctx.nextAttemptMutex.Unlock()
|
||||
|
||||
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 {
|
||||
ctx.nextAttemptMutex.RLock()
|
||||
_, ok := ctx.nextAttempt[teamId]
|
||||
ctx.nextAttemptMutex.RUnlock()
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
|
|
|
@ -186,19 +186,28 @@ func (ctx *Instance) readTeams() {
|
|||
now := time.Now()
|
||||
added := 0
|
||||
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.nextAttemptMutex.Unlock()
|
||||
|
||||
added += 1
|
||||
}
|
||||
}
|
||||
|
||||
// For any removed team IDs, remove them
|
||||
removed := 0
|
||||
ctx.nextAttemptMutex.Lock() // XXX: This could be less of a cludgel
|
||||
for k, _ := range ctx.nextAttempt {
|
||||
if _, ok := newList[k]; !ok {
|
||||
delete(ctx.nextAttempt, k)
|
||||
}
|
||||
}
|
||||
ctx.nextAttemptMutex.Unlock()
|
||||
|
||||
if (added > 0) || (removed > 0) {
|
||||
log.Printf("Team IDs updated: %d added, %d removed", added, removed)
|
||||
|
|
Loading…
Reference in New Issue