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