#!/usr/bin/python3 import asyncio import cgitb import html from aiohttp import web import io import json import mimetypes import moth import logging import os import pathlib import random import shutil import socketserver import sys import traceback import mothballer sys.dont_write_bytecode = True # Don't write .pyc files def get_seed(request): seedstr = request.match_info.get("seed") if seedstr == "random": return random.getrandbits(32) else: return int(seedstr) def get_puzzle(request): seed = get_seed(request) category = request.match_info.get("category") points = int(request.match_info.get("points")) filename = request.match_info.get("filename") cat = moth.Category(request.app["puzzles_dir"].joinpath(category), seed) puzzle = cat.puzzle(points) return puzzle async def handle_puzzlelist(request): seed = get_seed(request) puzzles = { "__devel__": [[0, ""]], } for p in request.app["puzzles_dir"].glob("*"): if not p.is_dir() or p.match(".*"): continue catName = p.parts[-1] cat = moth.Category(p, seed) puzzles[catName] = [[i, str(i)] for i in cat.pointvals()] puzzles[catName].append([0, ""]) if len(puzzles) <= 1: logging.warning("No directories found matching {}/*".format(request.app["puzzles_dir"])) return web.Response( content_type="application/json", body=json.dumps(puzzles), ) async def handle_puzzle(request): seed = get_seed(request) category = request.match_info.get("category") points = int(request.match_info.get("points")) cat = moth.Category(request.app["puzzles_dir"].joinpath(category), seed) puzzle = cat.puzzle(points) obj = puzzle.package() obj["answers"] = puzzle.answers obj["hint"] = puzzle.hint obj["summary"] = puzzle.summary obj["logs"] = puzzle.logs return web.Response( content_type="application/json", body=json.dumps(obj), ) async def handle_puzzlefile(request): seed = get_seed(request) category = request.match_info.get("category") points = int(request.match_info.get("points")) filename = request.match_info.get("filename") cat = moth.Category(request.app["puzzles_dir"].joinpath(category), seed) puzzle = cat.puzzle(points) try: file = puzzle.files[filename] except KeyError: return web.Response(status=404) content_type, _ = mimetypes.guess_type(file.name) return web.Response( body=file.stream.read(), # Is there no way to pipe this, must we slurp the whole thing into memory? content_type=content_type, ) async def handle_answer(request): seed = get_seed(request) category = request.match_info.get("category") points = int(request.match_info.get("points")) filename = request.match_info.get("filename") cat = moth.Category(request.app["puzzles_dir"].joinpath(category), seed) puzzle = cat.puzzle(points) async def handle_mothballer(request): seed = get_seed(request) category = request.match_info.get("category") try: catdir = request.app["puzzles_dir"].joinpath(category) mb = mothballer.package(category, catdir, seed) except: body = cgitb.html(sys.exc_info()) resp = web.Response(text=body, content_type="text/html") return resp mb_buf = mb.read() resp = web.Response( body=mb_buf, headers={"Content-Disposition": "attachment; filename={}.mb".format(category)}, content_type="application/octet_stream", ) return resp async def handle_index(request): seed = random.getrandbits(32) body = """ Dev Server

Dev Server

Pick a seed:

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) return web.Response( content_type="text/html", body=body, ) async def handle_static(request): themes = request.app["theme_dir"] fn = request.match_info.get("filename") if not fn: fn = "index.html" path = themes.joinpath(fn) return web.FileResponse(path) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser(description="MOTH puzzle development server") parser.add_argument( '--puzzles', default='puzzles', help="Directory containing your puzzles" ) parser.add_argument( '--theme', default='theme', help="Directory containing theme files") parser.add_argument( '--bind', default="127.0.0.1:8080", help="Bind to ip:port" ) parser.add_argument( '--base', default="", help="Base URL to this server, for reverse proxy setup" ) args = parser.parse_args() parts = args.bind.split(":") addr = parts[0] or "0.0.0.0" port = int(parts[1]) logging.basicConfig(level=logging.INFO) mydir = os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0]))) app = web.Application() app["base_url"] = args.base app["puzzles_dir"] = pathlib.Path(args.puzzles) app["theme_dir"] = pathlib.Path(args.theme) app.router.add_route("GET", "/", handle_index) app.router.add_route("*", "/{seed}/puzzles.json", handle_puzzlelist) app.router.add_route("GET", "/{seed}/content/{category}/{points}/puzzle.json", handle_puzzle) app.router.add_route("GET", "/{seed}/content/{category}/{points}/{filename}", handle_puzzlefile) app.router.add_route("GET", "/{seed}/mothballer/{category}", handle_mothballer) app.router.add_route("GET", "/{seed}/{filename:.*}", handle_static) web.run_app(app, host=addr, port=port)