diff options
| -rw-r--r-- | .gitignore | 176 | ||||
| -rw-r--r-- | birminghack-logo-raster-bw-rs.png | bin | 0 -> 13175 bytes | |||
| -rw-r--r-- | imggen.py | 36 | ||||
| -rw-r--r-- | main.py | 78 | ||||
| -rw-r--r-- | pizza.png | bin | 0 -> 23811 bytes | |||
| -rw-r--r-- | printer.py | 57 |
6 files changed, 347 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65a51db --- /dev/null +++ b/.gitignore @@ -0,0 +1,176 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +config.yaml diff --git a/birminghack-logo-raster-bw-rs.png b/birminghack-logo-raster-bw-rs.png Binary files differnew file mode 100644 index 0000000..e0822bd --- /dev/null +++ b/birminghack-logo-raster-bw-rs.png diff --git a/imggen.py b/imggen.py new file mode 100644 index 0000000..cdc6314 --- /dev/null +++ b/imggen.py @@ -0,0 +1,36 @@ +from PIL import Image, ImageDraw, ImageFont +import math +import printer + +font_bold = ImageFont.truetype("/usr/share/fonts/TTF/IosevkaSS08-Bold.ttc", 70, encoding="unic") +font_norm = ImageFont.truetype("/usr/share/fonts/TTF/IosevkaSS08-Regular.ttc", 40, encoding="unic") + +MAX_WIDTH = 576 + +def name(name: str): + txt_len = font_bold.getlength(name) + if txt_len > MAX_WIDTH: + img = Image.new('RGB', (math.ceil(txt_len), 80), (255, 255, 255)) + else: + img = Image.new('RGB', (MAX_WIDTH, 80), (255, 255, 255)) + + draw = ImageDraw.Draw(img) + draw.text((0, 0), name, font=font_bold, fill="black") + + if font_bold.getlength(name) > MAX_WIDTH: + wpercent = (MAX_WIDTH / float(img.size[0])) + hsize = int((float(img.size[1]) * float(wpercent))) + img = img.resize((MAX_WIDTH, hsize), Image.Resampling.LANCZOS) + + new_img = Image.new('RGB', (MAX_WIDTH, 80), (255, 255, 255)) + new_img.paste(img, (0,math.floor(80-hsize))) + img = new_img + + return img + +def pronouns(pronouns: str): + img = Image.new('RGB', (MAX_WIDTH, 50), (255, 255, 255)) + draw = ImageDraw.Draw(img) + draw.text((0, 0), pronouns, font=font_norm, fill="black") + return img + @@ -0,0 +1,78 @@ +from fastapi import Request, Response, FastAPI +from strictyaml import load +import threading +import logging +import hmac +import hashlib +import printer +import imggen +import base64 +import json +import uvicorn + +with open("config.yaml", "r") as f: + config = load(f.read()).data + +logger = logging.getLogger('uvicorn.error') +lock = threading.Lock() + +app = FastAPI() + +group = 1 +serial = 1 + +def verify_message(message: bytes, signature: bytes): + h = hmac.new(config['tito']['mac_token'].encode('utf-8'), message, hashlib.sha256).digest() + return hmac.compare_digest(h, signature) + +@app.post("/") +async def create_checkin(request: Request): + body = await request.body() + signature = request.headers.get("Tito-Signature") + if signature is None: + logger.warning("Rejecting message with missing signature") + return Response(status_code=403) + + if not verify_message(body, base64.b64decode(signature)): + logger.warning("Rejecting unauthenticated message") + return Response(status_code=403) + + lock.acquire(True) + + try: + global group + global serial + + content = json.loads(body.decode('utf-8')) + attendee_name = content['name'] + ticket_type = content['release_title'] + ticket_ref = content['reference'] + slug = content['slug'] + pronouns = list(filter(lambda a: a['question'] == 'What are your preferred pronouns?', content['answers']))[0]['response'] + logger.info(f"Printing attendee pass for {attendee_name}...") + printer.print_pass(imggen.name(attendee_name), imggen.pronouns(pronouns), ticket_ref, ticket_type, slug) + + pizza_pref = list(filter(lambda a: a['question'] == 'What is your pizza preference?', content['answers'])) + if len(pizza_pref) == 0: + return "ok" + + d_reqs = list(filter(lambda a: a['question'] == 'Do you have any dietary restrictions?', content['answers'])) + logger.info(f"Printing food token for {attendee_name}...") + if len(d_reqs) == 0: + printer.print_food(attendee_name, pizza_pref[0]['response'], str(group), None) + else: + printer.print_food(attendee_name, pizza_pref[0]['response'], str(group), d_reqs[0]['response']) + + serial = serial + 1 + if serial % 10 == 0: + group = group + 1 + logger.info(f"Incrementing group counter to {group}") + + return Response(status_code=204) + finally: + lock.release() + + +if __name__ == '__main__': + uvicorn.run(app, log_level="info", host="0.0.0.0", port=3000) + diff --git a/pizza.png b/pizza.png Binary files differnew file mode 100644 index 0000000..54806f1 --- /dev/null +++ b/pizza.png diff --git a/printer.py b/printer.py new file mode 100644 index 0000000..6bf69e9 --- /dev/null +++ b/printer.py @@ -0,0 +1,57 @@ +from PIL import Image +from escpos.printer import Usb +from typing import Optional, Union +import logging + +logger = logging.getLogger(__name__) +p = Usb(0x1fc9,0x2016,0,profile="TM-P80") + +def print_pass(name_image: Union[str, Image.Image], pronouns_image: Union[str, Image.Image], reference: str, ticket_type: str, slug: str): + logger.info(f"Printing pass for {ticket_type}") + p.linedisplay_clear() + p.image("birminghack-logo-raster-bw-rs.png",center=True) + p.ln() + p.ln() + p.image(name_image,center=True) + p.image(pronouns_image,center=True) + p.set(align="left",bold=False,custom_size=True,width=2,height=2) + p.ln() + p.qr(slug, size=10, center=True) + p.set(align="left",bold=True,normal_textsize=True) + p.software_columns(["Reference", reference], widths=48, align=["left", "right"]) + p.software_columns(["Type", ticket_type], widths=48, align=["left", "right"]) + p.set(align="left",bold=False,normal_textsize=True) + p.ln() + p.textln("By attending this event you agree to the") + p.textln("birmingHack Code of Conduct:") + p.set(align="right",bold=False,normal_textsize=True) + p.textln("birminghack.com/conduct") + p.set(align="left",bold=False,normal_textsize=True) + p.ln() + p.textln("Please wear your attendee pass at all times.") + p.cut() + +def print_food(issued_to: str, pizza_type: str, group: str, d_req: Optional[str] = None): + logger.info(f"Printing pizza token for {issued_to}") + p.linedisplay_clear() + p.ln() + p.set(align="center",bold=True,custom_size=True,width=3,height=3,smooth=True) + p.textln("Pizza Token") + p.ln() + p.set(align="left",bold=False,normal_textsize=True) + p.textln("Exchange me for pizza!") + p.image("pizza.png",center=True) + p.ln() + p.set(align="left",bold=True,normal_textsize=True) + p.software_columns(["Group", group], widths=48, align=["left", "right"]) + p.software_columns(["Pizza Type", pizza_type], widths=48, align=["left", "right"]) + p.software_columns(["Issued To", issued_to], widths=48, align=["left", "right"]) + p.ln() + if d_req: + p.set(align="left",bold=False,custom_size=True,width=2,height=2,invert=True) + p.textln("Dietary Requirement") + p.ln() + p.set(align="left",bold=False,normal_textsize=True,invert=False) + p.textln(d_req) + p.cut() + |
