initial commit
This commit is contained in:
		
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
.venv
 | 
			
		||||
__pycache__
 | 
			
		||||
*.pyc
 | 
			
		||||
config.yaml
 | 
			
		||||
printed.csv
 | 
			
		||||
							
								
								
									
										81
									
								
								config.yaml.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								config.yaml.example
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
mails:
 | 
			
		||||
    sender: sender@email.address
 | 
			
		||||
    server: server.example.org
 | 
			
		||||
    user: sender-user
 | 
			
		||||
    password: sender-pass
 | 
			
		||||
    port: 587
 | 
			
		||||
    starttls: true
 | 
			
		||||
 | 
			
		||||
meals:
 | 
			
		||||
    tag-1mittag:
 | 
			
		||||
        subevent: 1
 | 
			
		||||
        date: 2025-12-25
 | 
			
		||||
        name: "Tag -1 Mittagessen"
 | 
			
		||||
        name_en: "Day -1 Lunch"
 | 
			
		||||
    tag-1abend: 
 | 
			
		||||
        subevent: 2
 | 
			
		||||
        date: 2025-12-25
 | 
			
		||||
        name: "Tag -1 Abendessen"
 | 
			
		||||
        name_en: "Day -1 Dinner"
 | 
			
		||||
    tag0mittag: 
 | 
			
		||||
        subevent: 3
 | 
			
		||||
        date: 2025-12-26
 | 
			
		||||
        name: "Tag 0 Mittagessen"
 | 
			
		||||
        name_en: "Day 0 Lunch"
 | 
			
		||||
    tag0abend: 
 | 
			
		||||
        subevent: 4
 | 
			
		||||
        date: 2025-12-26
 | 
			
		||||
        name: "Tag 0 Abendessen"
 | 
			
		||||
        name_en: "Day 0 Dinner"
 | 
			
		||||
    tag1mittag: 
 | 
			
		||||
        subevent: 5
 | 
			
		||||
        date: 2025-12-27
 | 
			
		||||
        name: "Tag 1 Mittagessen"
 | 
			
		||||
        name_en: "Day 1 Lunch"
 | 
			
		||||
    tag1abend: 
 | 
			
		||||
        subevent: 6
 | 
			
		||||
        date: 2025-12-27
 | 
			
		||||
        name: "Tag 1 Abendessen"
 | 
			
		||||
        name_en: "Day 1 Dinner"
 | 
			
		||||
    tag2mittag: 
 | 
			
		||||
        subevent: 7
 | 
			
		||||
        date: 2025-12-28
 | 
			
		||||
        name: "Tag 2 Mittagessen"
 | 
			
		||||
        name_en: "Day 2 Lunch"
 | 
			
		||||
    tag2abend: 
 | 
			
		||||
        subevent: 8
 | 
			
		||||
        date: 2025-12-28
 | 
			
		||||
        name: "Tag 2 Abendessen"
 | 
			
		||||
        name_en: "Day 2 Dinner"
 | 
			
		||||
    tag3mittag: 
 | 
			
		||||
        subevent: 9
 | 
			
		||||
        date: 2025-12-29
 | 
			
		||||
        name: "Tag 3 Mittagessen"
 | 
			
		||||
        name_en: "Day 3 Lunch"
 | 
			
		||||
    tag3abend: 
 | 
			
		||||
        subevent: 10
 | 
			
		||||
        date: 2025-12-29
 | 
			
		||||
        name: "Tag 3 Abendessen"
 | 
			
		||||
        name_en: "Day 3 Dinner"
 | 
			
		||||
    tag4mittag: 
 | 
			
		||||
        subevent: 11
 | 
			
		||||
        date: 2025-12-30
 | 
			
		||||
        name: "Tag 4 Mittagessen"
 | 
			
		||||
        name_en: "Day 4 Lunch"
 | 
			
		||||
    tag4abend: 
 | 
			
		||||
        subevent: 12
 | 
			
		||||
        date: 2025-12-30
 | 
			
		||||
        name: "Tag 4 Abendessen"
 | 
			
		||||
        name_en: "Day 4 Dinner"
 | 
			
		||||
 | 
			
		||||
types:
 | 
			
		||||
    crew: id_for_crew
 | 
			
		||||
    regular: id_for_regular
 | 
			
		||||
 | 
			
		||||
pretix:
 | 
			
		||||
    host: https://tickets.events.ccc.de
 | 
			
		||||
    organizer: organizer
 | 
			
		||||
    event: eventslug
 | 
			
		||||
    token: randomtokenforpretixapiaccess1234567890 
 | 
			
		||||
auth:
 | 
			
		||||
    token: randomtokenforauthaccess1234567890
 | 
			
		||||
							
								
								
									
										174
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,174 @@
 | 
			
		||||
import time
 | 
			
		||||
import smtplib
 | 
			
		||||
import yaml
 | 
			
		||||
import requests
 | 
			
		||||
import csv
 | 
			
		||||
import qrcode
 | 
			
		||||
import io
 | 
			
		||||
from email.message import EmailMessage
 | 
			
		||||
from flask import Flask, request, jsonify, abort
 | 
			
		||||
from jinja2 import Template
 | 
			
		||||
from functools import wraps
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
app = Flask(__name__)
 | 
			
		||||
 | 
			
		||||
with open("config.yaml", "r") as f:
 | 
			
		||||
    config = yaml.safe_load(f)
 | 
			
		||||
 | 
			
		||||
MAIL_CONFIG = config["mails"]
 | 
			
		||||
MEALS = config["meals"]
 | 
			
		||||
TYPES = config["types"]
 | 
			
		||||
PRETIX = config["pretix"]
 | 
			
		||||
AUTH_TOKEN = config.get("auth", {}).get("token")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
with open("template.txt", "r") as f:
 | 
			
		||||
    MAIL_TEMPLATE = Template(f.read())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def require_auth(f):
 | 
			
		||||
    @wraps(f)
 | 
			
		||||
    def decorated(*args, **kwargs):
 | 
			
		||||
        token = request.headers.get("Authorization")
 | 
			
		||||
        if not token or token != f"Bearer {AUTH_TOKEN}":
 | 
			
		||||
            abort(401, description="Unauthorized: Invalid or missing token")
 | 
			
		||||
        return f(*args, **kwargs)
 | 
			
		||||
    return decorated
 | 
			
		||||
 | 
			
		||||
def send_email(to_email, subject, body, attachment_bytes, filename):
 | 
			
		||||
    print(f"[DEBUG] Sending email to {to_email} with attachment {filename}")
 | 
			
		||||
    msg = EmailMessage()
 | 
			
		||||
    msg["From"] = MAIL_CONFIG["sender"]
 | 
			
		||||
    msg["To"] = to_email
 | 
			
		||||
    msg["Subject"] = subject
 | 
			
		||||
    msg.set_content(body)
 | 
			
		||||
    msg.add_attachment(attachment_bytes, maintype="application", subtype="pdf", filename=filename)
 | 
			
		||||
 | 
			
		||||
    server = smtplib.SMTP(MAIL_CONFIG["server"], MAIL_CONFIG["port"])
 | 
			
		||||
    if MAIL_CONFIG.get("starttls"):
 | 
			
		||||
        server.starttls()
 | 
			
		||||
    server.login(MAIL_CONFIG["user"], MAIL_CONFIG["password"])
 | 
			
		||||
    server.send_message(msg)
 | 
			
		||||
    server.quit()
 | 
			
		||||
    print("[DEBUG] Email sent successfully")
 | 
			
		||||
 | 
			
		||||
def get_meal_times(meal_key):
 | 
			
		||||
    if meal_key.endswith("mittag"):
 | 
			
		||||
        return "11:30 - 13:30"
 | 
			
		||||
    elif meal_key.endswith("abend"):
 | 
			
		||||
        return "17:30 - 19:30"
 | 
			
		||||
    return ""
 | 
			
		||||
 | 
			
		||||
def log_printed(email, meal_key):
 | 
			
		||||
    location = email.split("@")[0]
 | 
			
		||||
    now = datetime.now()
 | 
			
		||||
    date_str = now.strftime("%Y-%m-%d")
 | 
			
		||||
    time_str = now.strftime("%H:%M:%S")
 | 
			
		||||
    with open("printed.csv", "a", newline="") as csvfile:
 | 
			
		||||
        writer = csv.writer(csvfile, delimiter=";")
 | 
			
		||||
        writer.writerow([date_str, time_str, meal_key, location])
 | 
			
		||||
    print(f"[DEBUG] Logged printed PDF: {location} - {meal_key}")
 | 
			
		||||
 | 
			
		||||
def generate_qr(secret):
 | 
			
		||||
    qr = qrcode.QRCode(box_size=10, border=4)
 | 
			
		||||
    qr.add_data(secret)
 | 
			
		||||
    qr.make(fit=True)
 | 
			
		||||
    img = qr.make_image(fill_color="black", back_color="white")
 | 
			
		||||
    buf = io.BytesIO()
 | 
			
		||||
    img.save(buf, format="PNG")
 | 
			
		||||
    buf.seek(0)
 | 
			
		||||
    return buf.getvalue()
 | 
			
		||||
 | 
			
		||||
@app.route("/order", methods=["POST"])
 | 
			
		||||
@require_auth
 | 
			
		||||
def order():
 | 
			
		||||
    data = request.get_json()
 | 
			
		||||
    print(f"[DEBUG] Incoming request JSON: {data}")
 | 
			
		||||
 | 
			
		||||
    email = data.get("email")
 | 
			
		||||
    typ = data.get("type")
 | 
			
		||||
    meal_key = data.get("meal")
 | 
			
		||||
 | 
			
		||||
    if not email or not typ or not meal_key:
 | 
			
		||||
        return jsonify({"error": "Missing field"}), 400
 | 
			
		||||
 | 
			
		||||
    meal_info = MEALS.get(meal_key)
 | 
			
		||||
    if not meal_info:
 | 
			
		||||
        return jsonify({"error": "Meal not found in config"}), 400
 | 
			
		||||
 | 
			
		||||
    position = {
 | 
			
		||||
        "positionid": 1,
 | 
			
		||||
        "item": TYPES.get(typ),
 | 
			
		||||
        "variation": None,
 | 
			
		||||
        "price": "0",
 | 
			
		||||
        "attendee_email": None,
 | 
			
		||||
        "addon_to": None,
 | 
			
		||||
        "subevent": meal_info["subevent"]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pretix_body = {
 | 
			
		||||
        "email": "pretixfood@td00.de",
 | 
			
		||||
        "locale": "en",
 | 
			
		||||
        "sales_channel": "web",
 | 
			
		||||
        "payment_provider": "manual",
 | 
			
		||||
        "positions": [position]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    url = f"{PRETIX['host']}/api/v1/organizers/{PRETIX['organizer']}/events/{PRETIX['event']}/orders/"
 | 
			
		||||
    headers = {"Authorization": f"Token {PRETIX['token']}"}
 | 
			
		||||
 | 
			
		||||
    print(f"[DEBUG] Sending POST to Pretix: {url} with body {pretix_body}")
 | 
			
		||||
    resp = requests.post(url, json=pretix_body, headers=headers)
 | 
			
		||||
    print(f"[DEBUG] Pretix response status: {resp.status_code}")
 | 
			
		||||
    print(f"[DEBUG] Pretix response text: {resp.text}")
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        resp_json = resp.json()
 | 
			
		||||
        print(f"[DEBUG] Pretix response JSON: {resp_json}")
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        print("[DEBUG] Pretix returned no JSON")
 | 
			
		||||
        return jsonify({"error": "Pretix returned no JSON", "status_code": resp.status_code, "text": resp.text}), 502
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    if "positions" in resp_json and "item" in resp_json["positions"][0] and isinstance(resp_json["positions"][0]["item"], list):
 | 
			
		||||
        if isinstance(resp_json["positions"][0]["item"][0], str):
 | 
			
		||||
            print("[DEBUG] Not enough quota available")
 | 
			
		||||
            return "Essen ist bereits alle", 418
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        secret = resp_json["positions"][0]["secret"]
 | 
			
		||||
        print(f"[DEBUG] Secret: {secret}")
 | 
			
		||||
    except (KeyError, IndexError):
 | 
			
		||||
        return jsonify({"error": "Secret not found in Pretix response", "resp_json": resp_json}), 500
 | 
			
		||||
 | 
			
		||||
    qr_bytes = generate_qr(secret)
 | 
			
		||||
    meal_times = get_meal_times(meal_key)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    mail_body = MAIL_TEMPLATE.render(
 | 
			
		||||
        meal_name=meal_info["name"],
 | 
			
		||||
        meal_name_en=meal_info["name_en"],
 | 
			
		||||
        meal_date=meal_info["date"],
 | 
			
		||||
        meal_times=meal_times
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    if email.endswith("@printme.local"):
 | 
			
		||||
        log_printed(email, meal_key)
 | 
			
		||||
        response = app.response_class(
 | 
			
		||||
            response=qr_bytes,
 | 
			
		||||
            status=200,
 | 
			
		||||
            mimetype='image/png'
 | 
			
		||||
        )
 | 
			
		||||
        response.headers["Content-Disposition"] = f"inline; filename=ticket.png"
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    send_email(email, "Dein Engelessen / Your Angel Meal", mail_body, qr_bytes, "ticket.png")
 | 
			
		||||
 | 
			
		||||
    return "Token gesendet", 201
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    app.run(host="0.0.0.0", port=8000, debug=True)
 | 
			
		||||
							
								
								
									
										21
									
								
								template.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								template.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
Hallo,
 | 
			
		||||
 | 
			
		||||
Hier sind deine Informationen zum Essen:
 | 
			
		||||
 | 
			
		||||
Mahlzeit: {{meal_name}}
 | 
			
		||||
Datum: {{meal_date}}
 | 
			
		||||
Essenszeiten: {{meal_times}}
 | 
			
		||||
 | 
			
		||||
Bitte zeige den QR-Code vor, um dein Essen abzuholen.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
Hello,
 | 
			
		||||
 | 
			
		||||
Here is your meal information:
 | 
			
		||||
 | 
			
		||||
Meal: {{meal_name_en}}
 | 
			
		||||
Date: {{meal_date}}
 | 
			
		||||
Meal times: {{meal_times}}
 | 
			
		||||
 | 
			
		||||
Please show the QR code to pick up your meal.
 | 
			
		||||
		Reference in New Issue
	
	Block a user