#!/usr/bin/env python3 """ BSN Chatbot — Multi-Channel Intake System ========================================== WhatsApp webhook receiver + admin dashboard + knowledge query API. Architecture: WhatsApp → Meta Cloud API → POST /whatsapp/webhook → SQLite Admin: GET /admin → filterable table of all submissions Query: POST /query → RAG-style answer from knowledge table Usage: source .env && python3 app.py """ import json import os import re import sqlite3 import sys from datetime import datetime from pathlib import Path import requests # type: ignore from flask import Flask, request, jsonify, g # type: ignore # ── Config ────────────────────────────────────────────────────────── BASE_DIR = Path(__file__).parent # Load .env file if it exists (for tokens that can't be passed via env) _ENV_FILE = BASE_DIR / ".env" if _ENV_FILE.exists(): with open(_ENV_FILE) as _f: for _line in _f: _line = _line.strip() if _line and not _line.startswith("#") and "=" in _line: _key, _, _val = _line.partition("=") _key = _key.strip() _val = _val.strip().strip('"').strip("'") if _key not in os.environ or not os.environ[_key]: os.environ[_key] = _val DB_PATH = os.environ.get("DATABASE_PATH", str(BASE_DIR / "bsn_intake.db")) VERIFY_TOKEN = os.environ.get("VERIFY_TOKEN", "bsn_verify_2026") META_TOKEN = os.environ.get("META_TOKEN", "") PHONE_NUMBER_ID = os.environ.get("META_PHONE_NUMBER_ID", "1152708791258718") PORT = int(os.environ.get("PORT", "5002")) MEDIA_DIR = BASE_DIR / "media" MEDIA_DIR.mkdir(exist_ok=True) # Brettspiel News logo (base64-encoded JPEG, 993x217) LOGO_B64 = """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""" app = Flask(__name__) # ── Hall extraction ────────────────────────────────────────────────── def extract_halle(text: str) -> str | None: """Extract SPIEL Essen hall+stand from text. Returns e.g. 'H3 B123' or None.""" import re if not text: return None # Pattern: H[1-7] optionally followed by stand letter+number # Matches: "H3", "Halle 3", "H 3", "H3 B123", "Halle 6 Stand A42" patterns = [ # Halle with stand: H3 B123, Halle 6 A42, H 4 C7 r'(?:H|Halle)\s*([1-7])\s*[,\-]?\s*(?:Stand\s*)?([A-Z]\d{1,4})', # Just hall: H3, Halle 6, H 7 r'(?:H|Halle)\s*([1-7])\b', # Stand alone: B123 (only if preceded by "Stand" or after hall context) r'Stand\s*([A-Z]\d{1,4})', ] for pat in patterns: m = re.search(pat, text, re.IGNORECASE) if m: groups = m.groups() if len(groups) == 2 and groups[1]: return f"H{groups[0]} {groups[1]}" elif groups[0]: return f"H{groups[0]}" return None # ── Game title extraction from cover images ────────────────────────── def extract_game_title_from_image(image_path: str) -> str | None: """Use Gemini Flash vision to read the board game title from a cover image.""" import base64 import openai if not os.path.exists(image_path): return None try: with open(image_path, "rb") as f: img_b64 = base64.b64encode(f.read()).decode() except Exception: return None # Read API config config_path = os.path.expanduser("~/.hermes/config.yaml") try: with open(config_path) as f: import yaml cfg = yaml.safe_load(f) api_key = cfg["providers"]["datenhimmel"]["api_key"] base_url = cfg["providers"]["datenhimmel"]["base_url"] except Exception: return None client = openai.OpenAI(base_url=base_url, api_key=api_key) try: r = client.chat.completions.create( model="gemini-3-flash-preview:cloud", messages=[{ "role": "user", "content": [ {"type": "text", "text": ( "This is a board game cover. What is the EXACT game title?\n" "Reply with ONLY the title, nothing else. " "If it's a known game, use the official German title if it exists.\n" "Examples: 'Cascadia', 'Flügelschlag', 'Die Siedler von Catan', 'Azul'" )}, {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}}, ] }], max_tokens=50, temperature=0.1, ) title = r.choices[0].message.content.strip() # Clean up: remove quotes, extra text, limit length title = title.strip('"').strip("'").strip() if len(title) > 120: title = title[:120] return title if title else None except Exception as e: print(f"[vision error] {e}", file=sys.stderr) return None # ── Database helpers ──────────────────────────────────────────────── def get_db() -> sqlite3.Connection: if "db" not in g: g.db = sqlite3.connect(DB_PATH) g.db.row_factory = sqlite3.Row g.db.execute("PRAGMA journal_mode=WAL") return g.db @app.teardown_appcontext def close_db(_exception=None): db = g.pop("db", None) if db: db.close() # ── WhatsApp API helpers ──────────────────────────────────────────── def _gdpr_delete_sender(db: sqlite3.Connection, sender_id: str): """GDPR: Delete all submissions from a sender, leave a placeholder.""" # Delete media files (supports ||-separated paths) rows = db.execute( "SELECT media_path FROM submissions WHERE sender_id = ? AND channel = 'whatsapp'", (sender_id,), ).fetchall() for r in rows: if r["media_path"]: for p in r["media_path"].split("||"): if os.path.exists(p): os.remove(p) # Delete all submissions from this sender db.execute( "DELETE FROM submissions WHERE sender_id = ? AND channel = 'whatsapp'", (sender_id,), ) # Insert GDPR placeholder db.execute( """INSERT INTO submissions (channel, sender_id, type, content, status, category) VALUES ('whatsapp', ?, 'text', ?, 'gdpr_deleted', 'dsgvo')""", (sender_id, "Nutzer hat die Löschung verlangt"), ) def send_whatsapp_message(to: str, text: str) -> dict: """Send a text message back via WhatsApp Cloud API.""" url = f"https://graph.facebook.com/v25.0/{PHONE_NUMBER_ID}/messages" headers = { "Authorization": f"Bearer {META_TOKEN}", "Content-Type": "application/json", } payload = { "messaging_product": "whatsapp", "to": to, "type": "text", "text": {"body": text}, } r = requests.post(url, headers=headers, json=payload, timeout=15) return r.json() def generate_thorsten_tts(text: str) -> str | None: """Generate German TTS using Piper Thorsten voice. Returns path to OGG file or None.""" import subprocess import tempfile PIPER = "/home/hermes/.local/bin/piper" MODEL = "/home/hermes/.hermes/cache/piper-voices/de_DE-thorsten-medium.onnx" FFMPEG = "/home/hermes/.local/bin/ffmpeg" LD_PATH = "/home/hermes/.local/lib" ESPEAK_PATH = "/home/hermes/.local/share/espeak-ng-data" ogg_path = str(MEDIA_DIR / f"onboarding_{datetime.now().strftime('%Y%m%d%H%M%S')}.ogg") try: p1 = subprocess.Popen( [PIPER, "--model", MODEL, "--output-raw", "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={"LD_LIBRARY_PATH": LD_PATH, "ESPEAK_DATA_PATH": ESPEAK_PATH, "PATH": os.environ.get("PATH", "")}, ) p2 = subprocess.Popen( [FFMPEG, "-f", "s16le", "-ar", "22050", "-ac", "1", "-i", "-", "-c:a", "libopus", "-b:a", "16k", "-f", "ogg", ogg_path, "-y"], stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) p1.stdin.write(text.encode("utf-8")) p1.stdin.close() p1.stdout.close() p2.communicate(timeout=30) p1.wait(timeout=10) if os.path.exists(ogg_path) and os.path.getsize(ogg_path) > 100: return ogg_path except Exception as e: print(f"[tts error] {e}", file=sys.stderr) return None def upload_whatsapp_media(file_path: str, mime_type: str = "audio/ogg") -> str | None: """Upload media to WhatsApp Cloud API. Returns media_id or None.""" url = f"https://graph.facebook.com/v25.0/{PHONE_NUMBER_ID}/media" headers = {"Authorization": f"Bearer {META_TOKEN}"} try: with open(file_path, "rb") as f: files = { "file": (os.path.basename(file_path), f, mime_type), } data = {"messaging_product": "whatsapp"} r = requests.post(url, headers=headers, files=files, data=data, timeout=20) result = r.json() return result.get("id") except Exception as e: print(f"[upload error] {e}", file=sys.stderr) return None def send_whatsapp_audio(to: str, media_id: str) -> dict: """Send an audio voice message via WhatsApp Cloud API.""" url = f"https://graph.facebook.com/v25.0/{PHONE_NUMBER_ID}/messages" headers = { "Authorization": f"Bearer {META_TOKEN}", "Content-Type": "application/json", } payload = { "messaging_product": "whatsapp", "to": to, "type": "audio", "audio": {"id": media_id}, } r = requests.post(url, headers=headers, json=payload, timeout=15) return r.json() def _send_email(to: str, subject: str, body: str) -> None: """Send an email notification using sendmail.""" import subprocess from email.message import EmailMessage msg = EmailMessage() msg["From"] = "noreply@brettspiel-news.de" msg["To"] = to msg["Subject"] = subject msg.set_content(body) try: subprocess.run( ["/usr/sbin/sendmail", "-t", "-oi"], input=msg.as_bytes(), timeout=10, check=True, ) except Exception: pass # silently ignore email failures def download_media(media_id: str) -> tuple[str | None, str | None]: """Download media from WhatsApp. Returns (local_path, mime_type) or (None, None).""" url = f"https://graph.facebook.com/v25.0/{media_id}" headers = {"Authorization": f"Bearer {META_TOKEN}"} r = requests.get(url, headers=headers, timeout=10) if r.status_code != 200: print(f"[download_media] Meta API returned {r.status_code}: {r.text[:200]}", file=sys.stderr) return None, None data = r.json() media_url = data.get("url") mime_type = data.get("mime_type", "unknown") if not media_url: print(f"[download_media] No URL in Meta response for media {media_id}", file=sys.stderr) return None, None r2 = requests.get(media_url, headers=headers, timeout=30) if r2.status_code != 200: print(f"[download_media] Media download returned {r2.status_code} for {media_id}", file=sys.stderr) return None, None ext = mime_type.split("/")[-1] if "/" in mime_type else "bin" filename = f"{media_id}.{ext}" local_path = MEDIA_DIR / filename local_path.write_bytes(r2.content) return str(local_path), mime_type def auto_process_bg(db_path: str, submission_id: int): """Background: after transcription, run LLM to summarize + extract halle/stand.""" import yaml as _yaml try: conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row row = conn.execute("SELECT content, halle FROM submissions WHERE id=?", (submission_id,)).fetchone() if not row or not row["content"]: conn.close() return text = (row["content"] or "").strip() # Skip if text is too short or placeholder if len(text) < 20: conn.close() return # Read API key config_path = os.path.expanduser("~/.hermes/config.yaml") api_key = "" try: with open(config_path) as f: cfg = _yaml.safe_load(f) api_key = cfg.get("providers", {}).get("datenhimmel", {}).get("api_key", "") except Exception: pass if not api_key: conn.close() return prompt = ( "Du bist Redaktionsassistent für brettspiel-news.de. Analysiere folgende Community-Nachricht " "und antworte NUR mit diesem JSON-Format, kein weiterer Text:\n" '{"summary": "Kurze Zusammenfassung in 1-2 Sätzen, max 250 Zeichen, journalistisch, Präsens", ' '"halle": "Halle und Stand im Format z.B. H3 B123, oder null wenn nichts erkannt", ' '"spiel_titel": "Name des Brettspiels falls genannt, sonst null"}\n\n' f"Nachricht:\n{text[:2000]}" ) import requests as _req r = _req.post( "https://ui.datenhimmel.work/api/chat/completions", headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, json={"model": "deepseek-v4-flash:cloud", "messages": [{"role": "user", "content": prompt}], "max_tokens": 300, "temperature": 0.2}, timeout=20, ) if r.status_code != 200: conn.close() return import json as _json data = r.json() result_text = data["choices"][0]["message"]["content"].strip() # Parse JSON from response (may have markdown wrapping) if "```" in result_text: result_text = result_text.split("```")[1] if result_text.startswith("json"): result_text = result_text[4:] result_text = result_text.strip() parsed = _json.loads(result_text) summary = parsed.get("summary", "").strip() llm_halle = parsed.get("halle", "").strip() if parsed.get("halle") and parsed["halle"] != "null" else None spiel_titel = parsed.get("spiel_titel", "").strip() if parsed.get("spiel_titel") and parsed["spiel_titel"] != "null" else None # Build update fields existing = conn.execute("SELECT halle, spiel_titel, summary FROM submissions WHERE id=?", (submission_id,)).fetchone() updates = [] params = [] if summary: updates.append("summary=?") params.append(summary[:500]) if llm_halle and not existing["halle"]: updates.append("halle=?") params.append(llm_halle) if spiel_titel and not existing["spiel_titel"]: updates.append("spiel_titel=?") params.append(spiel_titel) if updates: params.append(submission_id) conn.execute(f"UPDATE submissions SET {', '.join(updates)} WHERE id=?", params) conn.commit() conn.close() except Exception as e: print(f"[auto_process error] {e}", file=sys.stderr) def transcribe_audio(audio_path: str) -> str | None: """Transcribe an audio file using faster-whisper (local). Returns transcription text or None.""" import subprocess import json WHISPER_PYTHON = "/home/hermes/.hermes/venvs/speech/bin/python" script = ( "from faster_whisper import WhisperModel\n" "import sys, json\n" "model = WhisperModel('base', device='cpu', compute_type='int8')\n" "segments, info = model.transcribe(sys.argv[1])\n" "text = ' '.join(seg.text.strip() for seg in segments)\n" "print(json.dumps({'text': text, 'lang': info.language}))\n" ) try: result = subprocess.run( [WHISPER_PYTHON, "-c", script, audio_path], capture_output=True, text=True, timeout=120, ) if result.returncode == 0 and result.stdout.strip(): data = json.loads(result.stdout.strip()) return data.get("text", "") else: print(f"[transcribe error] {result.stderr[:200]}", file=sys.stderr) return None except Exception as e: print(f"[transcribe exception] {e}", file=sys.stderr) return None def _transcribe_bg(db_path: str, submission_id: int, audio_path: str): """Background thread: transcribe audio and update the DB record.""" text = transcribe_audio(audio_path) if text: try: conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row row = conn.execute( "SELECT content FROM submissions WHERE id=?", (submission_id,) ).fetchone() if row: old = row["content"] or "" new_content = old.replace( "[🎤 Sprachnachricht — Transkription läuft...]", f"🎤 {text}", ) if new_content == old: # fallback if placeholder not found (e.g. old records) new_content = old + f"\n🎤 Transkription: {text}" conn.execute( "UPDATE submissions SET content=? WHERE id=?", (new_content, submission_id), ) conn.commit() conn.close() except Exception as e: print(f"[transcribe bg error] {e}", file=sys.stderr) # Auto-summarize + hall extraction after transcription auto_process_bg(db_path, submission_id) def transcribe_video(video_path: str) -> str | None: """Extract audio from video, then transcribe. Returns text or None.""" import subprocess import tempfile import json WHISPER_PYTHON = "/home/hermes/.hermes/venvs/speech/bin/python" FFMPEG = "/home/hermes/.local/bin/ffmpeg" LD_PATH = "/home/hermes/.local/lib" wav_path = None try: # Extract audio to temp WAV file fd, wav_path = tempfile.mkstemp(suffix=".wav") os.close(fd) result = subprocess.run( [FFMPEG, "-i", video_path, "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", wav_path, "-y"], capture_output=True, text=True, timeout=60, env={"LD_LIBRARY_PATH": LD_PATH, "PATH": os.environ.get("PATH", "")}, ) if not os.path.exists(wav_path) or os.path.getsize(wav_path) < 100: return None # Now transcribe the extracted audio return transcribe_audio(wav_path) except Exception as e: print(f"[video transcribe error] {e}", file=sys.stderr) return None finally: if wav_path and os.path.exists(wav_path): os.unlink(wav_path) def generate_video_thumbnail(video_path: str) -> str | None: """Extract a thumbnail frame from a video at 1 second. Returns path to .jpg or None.""" import subprocess FFMPEG = "/home/hermes/.local/bin/ffmpeg" thumb_path = video_path + ".thumb.jpg" # Don't regenerate if exists and is not empty if os.path.exists(thumb_path) and os.path.getsize(thumb_path) > 100: return thumb_path try: subprocess.run( [FFMPEG, "-i", video_path, "-ss", "00:00:01", "-vframes", "1", "-q:v", "5", thumb_path, "-y"], capture_output=True, timeout=30, check=True ) if os.path.exists(thumb_path) and os.path.getsize(thumb_path) > 100: return thumb_path except Exception: pass return None def _transcribe_video_bg(db_path: str, submission_id: int, video_path: str): """Background thread: extract audio from video, transcribe, update DB.""" text = transcribe_video(video_path) if text: try: conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row row = conn.execute( "SELECT content FROM submissions WHERE id=?", (submission_id,) ).fetchone() if row: old = row["content"] or "" new_content = old.replace( "[🎬 Video — Transkription läuft...]", f"🎬 {text}", ) if new_content == old: new_content = old + f"\n🎬 Transkription: {text}" conn.execute( "UPDATE submissions SET content=? WHERE id=?", (new_content, submission_id), ) conn.commit() conn.close() except Exception as e: print(f"[video transcribe bg error] {e}", file=sys.stderr) # Auto-summarize + hall extraction after transcription auto_process_bg(db_path, submission_id) # ── Routes ────────────────────────────────────────────────────────── @app.route("/whatsapp/webhook", methods=["GET", "POST"]) def whatsapp_webhook(): """Meta WhatsApp Webhook. GET=verification, POST=incoming messages.""" if request.method == "GET": mode = request.args.get("hub.mode") token = request.args.get("hub.verify_token") challenge = request.args.get("hub.challenge") if mode == "subscribe" and token == VERIFY_TOKEN: return challenge, 200 return "Forbidden", 403 # POST: Incoming message body = request.get_json(force=True, silent=True) if not body: return "OK", 200 try: entries = body.get("entry", []) db = get_db() for entry in entries: changes = entry.get("changes", []) for change in changes: value = change.get("value", {}) contacts = value.get("contacts", [{}]) messages = value.get("messages", []) for msg in messages: sender_id = msg.get("from", "unknown") sender_name = contacts[0].get("profile", {}).get("name", "unknown") msg_type = msg.get("type", "text") content = "" media_path = None spiel_titel = None category_override = None # set to "ausverkauft" for #out if msg_type == "text": content = msg.get("text", {}).get("body", "") elif msg_type == "image": content = msg.get("image", {}).get("caption", "[Bild ohne Text]") media_id = msg.get("image", {}).get("id") if media_id: media_path, _ = download_media(media_id) elif msg_type == "audio": content = "[🎤 Sprachnachricht — Transkription läuft...]" media_id = msg.get("audio", {}).get("id") if media_id: media_path, _ = download_media(media_id) elif msg_type == "video": content = msg.get("video", {}).get( "caption", "[🎬 Video — Transkription läuft...]" ) media_id = msg.get("video", {}).get("id") if media_id: media_path, _ = download_media(media_id) elif msg_type == "document": content = msg.get("document", {}).get( "caption", "[Dokument]" ) media_id = msg.get("document", {}).get("id") if media_id: media_path, _ = download_media(media_id) else: content = f"[{msg_type}]" # #out detection: set category to "ausverkauft" and extract game title if "#out" in content.lower(): category_override = "ausverkauft" idx = content.lower().find("#out") after = content[idx + 4:].strip() if after: spiel_titel = re.split(r'[.!?\n]| - | – | — | ist | war | in ', after, maxsplit=1)[0].strip()[:200] # GDPR: "Meine Daten löschen" trigger if msg_type == "text" and content.strip().lower() == "meine daten löschen": _gdpr_delete_sender(db, sender_id) if META_TOKEN: try: send_whatsapp_message( sender_id, "✅ Alle deine Daten wurden vollständig und endgültig vom Server gelöscht. " "Es befinden sich keinerlei personenbezogene Daten mehr von dir bei uns. " "Lediglich ein anonymer Vermerk über die erfolgte Löschung bleibt für " "unsere Dokumentationspflicht.", ) except Exception: pass continue # Check for existing thread from this sender (5-min window) recent = db.execute( """SELECT id, content, media_path, halle FROM submissions WHERE channel='whatsapp' AND sender_id=? AND status != 'gdpr_deleted' ORDER BY created_at DESC LIMIT 1""", (sender_id,), ).fetchone() is_new_user = db.execute( "SELECT COUNT(*) FROM submissions WHERE channel='whatsapp' AND sender_id=? AND status != 'gdpr_deleted'", (sender_id,), ).fetchone()[0] == 0 THREAD_WINDOW_MINUTES = 5 thread_found = False if recent: age_check = db.execute( "SELECT (strftime('%s','now') - strftime('%s',created_at)) < ? FROM submissions WHERE id=?", (THREAD_WINDOW_MINUTES * 60, recent["id"]), ).fetchone() if age_check and age_check[0]: thread_found = True if thread_found: # Append to existing thread prefix = { "text": "", "image": "\n[🖼️ Bild] ", "audio": "\n[🎤 Sprachnachricht]", "video": "\n[🎬 Video] ", "document": "\n[📎 Dokument] ", }.get(msg_type, f"\n[{msg_type}] ") new_content = (recent["content"] or "") + prefix + content # Re-extract halle from combined content new_halle = extract_halle(new_content) or recent["halle"] db.execute( "UPDATE submissions SET content=?, halle=?, created_at=CURRENT_TIMESTAMP WHERE id=?", (new_content, new_halle, recent["id"]), ) # Check for #out in appended content — update category + spiel_titel if "#out" in content.lower(): out_title = None idx = content.lower().find("#out") after = content[idx + 4:].strip() if after: out_title = re.split(r'[.!?\n]| - | – | — | ist | war | in ', after, maxsplit=1)[0].strip()[:200] db.execute( "UPDATE submissions SET category=?, spiel_titel=? WHERE id=?", ("ausverkauft", out_title, recent["id"]), ) # Merge media paths (keep all, separated by ||) if media_path: merged_media = media_path if recent["media_path"]: merged_media = recent["media_path"] + "||" + media_path db.execute( "UPDATE submissions SET media_path=? WHERE id=?", (merged_media, recent["id"]), ) else: # New thread halle = extract_halle(content) cat = category_override or None db.execute( """INSERT INTO submissions (channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel) VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?)""", ("whatsapp", sender_id, sender_name, msg_type, content, media_path, halle, cat, spiel_titel), ) # Auto-reply: only on NEW threads, not appends if not thread_found and META_TOKEN: try: if is_new_user: reply = ( f"🎲 Hey {sender_name}! Cool, dass du da bist — willkommen in der " f"brettspiel-news.de Community!\n\n" f"Schick uns einfach, was dich bewegt: 💬 Text, 🎤 Sprachnachrichten, " f"🖼️ Fotos oder 🎬 Videos — alles rund um Brettspiele. Wir schauen uns alles an!\n\n" f"✍️ Nenn uns deinen Vornamen, wenn du magst — das ist deine Einwilligung, " f"dass wir dich als Quelle nennen, falls wir deinen Beitrag verwenden. " f"Ohne Namen schreiben wir „anonyme Quelle\".\n\n" f"🔒 „Meine Daten löschen\" genügt — und alles ist sofort und vollständig weg." ) send_whatsapp_message(sender_id, reply) # Thorsten voice: same text but without emojis import re tts_text = re.sub(r'[^\x00-\x7F\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u1E00-\u1EFF\u2018-\u201F\u2013\u2014\u2026\u00A0\u00AB\u00BB\u20AC]', '', reply) tts_text = re.sub(r' +', ' ', tts_text).strip() audio_path = generate_thorsten_tts(tts_text) if audio_path: media_id = upload_whatsapp_media(audio_path) if media_id: send_whatsapp_audio(sender_id, media_id) else: reply = ( f"📨 Danke {sender_name}! Ist angekommen — " f"wir kümmern uns drum." ) send_whatsapp_message(sender_id, reply) except Exception: pass # Transcribe audio in background (non-blocking) if msg_type == "audio" and media_path: import threading sub_id = recent["id"] if thread_found else db.execute( "SELECT last_insert_rowid()" ).fetchone()[0] threading.Thread( target=_transcribe_bg, args=(DB_PATH, sub_id, media_path), daemon=True, ).start() # Transcribe video in background if msg_type == "video" and media_path: import threading sub_id = recent["id"] if thread_found else db.execute( "SELECT last_insert_rowid()" ).fetchone()[0] threading.Thread( target=_transcribe_video_bg, args=(DB_PATH, sub_id, media_path), daemon=True, ).start() # Auto-summarize text/image submissions (LLM hall + summary) if msg_type in ("text", "image") and not thread_found and content and len(content.strip()) > 20: import threading sub_id = db.execute("SELECT last_insert_rowid()").fetchone()[0] # Only if regex didn't already find a halle if not halle: threading.Thread( target=auto_process_bg, args=(DB_PATH, sub_id), daemon=True, ).start() db.commit() except Exception as e: print(f"[webhook error] {e}", file=sys.stderr) return "OK", 200 return "OK", 200 @app.route("/admin") def admin_dashboard(): """Internal admin dashboard — filterable table of all submissions.""" db = get_db() channel_filter = request.args.get("channel", "") status_filter = request.args.get("status", "") category_filter = request.args.get("category", "") search = request.args.get("q", "") halle_filter = request.args.get("halle", "") query = "SELECT * FROM submissions WHERE deleted_at IS NULL" params: list = [] if channel_filter: query += " AND channel = ?" params.append(channel_filter) if status_filter: query += " AND status = ?" params.append(status_filter) if category_filter: query += " AND category = ?" params.append(category_filter) if halle_filter: query += " AND halle = ?" params.append(halle_filter) if search: query += " AND (content LIKE ? OR sender_id LIKE ?)" params.extend([f"%{search}%", f"%{search}%"]) query += " ORDER BY created_at DESC LIMIT 200" rows = db.execute(query, params).fetchall() channels = [ r[0] for r in db.execute("SELECT DISTINCT channel FROM submissions").fetchall() ] statuses = [ r[0] for r in db.execute("SELECT DISTINCT status FROM submissions").fetchall() ] categories = [ r[0] for r in db.execute( "SELECT DISTINCT category FROM submissions WHERE category IS NOT NULL" ).fetchall() ] # Channel totals totals_html = "".join( f'
' f'{db.execute("SELECT COUNT(*) FROM submissions WHERE channel=?", (c,)).fetchone()[0]}' f" {c}
" for c in channels ) # Get distinct halle values hallen = [r[0] for r in db.execute("SELECT DISTINCT halle FROM submissions WHERE halle IS NOT NULL ORDER BY halle").fetchall()] # Filter dropdowns filter_html = f"""
Reset
""" # Status display names (German) status_names = { "new": "Neu", "processing": "In Bearbeitung", "done": "Erledigt", "flagged": "Markiert", "gdpr_deleted": "DSGVO-Gelöscht", "published": "Veröffentlicht", } table_rows = "" for r in rows: content_snippet = (r["content"] or "")[:120] media_icon = {"image": "🖼️", "audio": "🎤", "video": "🎬", "document": "📎"}.get(r["type"], "") st_label = status_names.get(r["status"], r["status"]) table_rows += f""" {r['id']} {r['channel']} {r['sender_id']} {media_icon} {r['type']} {r['halle'] or '-'} {r['spiel_titel'] or '-'} {content_snippet} {st_label} {r['category'] or '-'} {r['created_at']} """ # Mobile card rows mobile_cards = "" for r in rows: content_snippet = (r["content"] or "")[:80] media_icon = {"image": "🖼️", "audio": "🎤", "video": "🎬", "document": "📎"}.get(r["type"], "") st_label = status_names.get(r["status"], r["status"]) mobile_cards += f"""
#{r['id']}{st_label}
{media_icon} {r['type']} · {r['channel']}
{'📍 ' + r['halle'] if r['halle'] else ''}{' 🎯 ' + r['spiel_titel'] if r['spiel_titel'] else ''}
{content_snippet}
{r['created_at']}
""" return f""" BSN Intake — Admin
Brettspiel News Boardgame Social
{totals_html}
{filter_html.replace('
','').replace('style="padding:0.3rem 0.8rem;"','').replace('style="padding:0.3rem;"','').replace('onchange="this.form.submit()"','onchange="this.form.submit()"')}
Reset
{mobile_cards}
{table_rows}
IDSenderTyp📍 Halle🎯 SpielInhaltStatusZeit
""".replace("{LOGO_B64}", LOGO_B64) @app.route("/test") def test_page(): """Page with a curl snippet for simulated testing.""" return """ Simulierter Test Brettspiel News

🧪 Simulierter WhatsApp Test

Dieser curl-Befehl sendet eine Fake-WhatsApp-Nachricht an den lokalen Webhook:

curl -X POST http://127.0.0.1:5002/whatsapp/webhook \\
  -H "Content-Type: application/json" \\
  -d '{
    "object": "whatsapp_business_account",
    "entry": [{
      "id": "TEST",
      "changes": [{
        "value": {
          "messaging_product": "whatsapp",
          "metadata": {
            "display_phone_number": "TEST",
            "phone_number_id": "TEST"
          },
          "contacts": [{
            "profile": {"name": "Max Mustermann"}
          }],
          "messages": [{
            "from": "+491234567890",
            "id": "wamid.test123",
            "timestamp": "1718550000",
            "type": "text",
            "text": {"body": "Hallo! Gibt es Catan noch in Halle 5?"}
          }]
        }
      }]
    }]
  }'

← Zurück zum Dashboard

""".replace("{LOGO_B64}", LOGO_B64) @app.route("/health") def health(): return jsonify( { "status": "ok", "db": str(DB_PATH), "whatsapp_configured": bool(META_TOKEN), "phone_number_id": PHONE_NUMBER_ID, } ) @app.route("/submission/") def submission_detail(sub_id: int): """Detail view for a single submission.""" db = get_db() row = db.execute("SELECT * FROM submissions WHERE id = ?", (sub_id,)).fetchone() if not row: return "

Nicht gefunden

← Zurück

", 404 import base64 # Media preview with per-item publish checkboxes media_html = "" if row["media_path"]: paths = row["media_path"].split("||") # Parse existing publish flags flags_str = row["media_publish_flags"] or "" flags = flags_str.split(",") if flags_str else ["1"] * len(paths) # Pad flags if needed while len(flags) < len(paths): flags.append("1") mime_map = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", ".gif": "image/gif", ".mp4": "video/mp4", ".webm": "video/webm", ".mov": "video/mp4", ".ogg": "audio/ogg", ".mp3": "audio/mpeg", ".wav": "audio/wav", } for i, path in enumerate(paths): checked = "checked" if flags[i] == "1" else "" if not os.path.exists(path): media_html += f'

📁 Datei nicht gefunden: {os.path.basename(path)}

' continue _, ext = os.path.splitext(path) mime = mime_map.get(ext.lower(), "application/octet-stream") try: basename = os.path.basename(path) src = f"/media/{basename}" media_html += f'
' # Media element if mime.startswith("image/"): media_html += f'' elif mime.startswith("video/"): thumb = generate_video_thumbnail(path) poster_attr = f' poster="/media/{os.path.basename(thumb)}"' if thumb else "" media_html += f'' elif mime.startswith("audio/"): media_html += f'' else: media_html += f'

📎 {os.path.basename(path)}

' # Toggle bar media_html += f'
' media_html += f'' media_html += f'Veröffentlichen' media_html += f'
' media_html += f'
' except Exception as e: media_html += f'

Fehler: {e}

' # SVG type icons (16x16, BSN blue) _svg_icons = { "text": '', "image": '', "audio": '', "video": '', "document": '', } type_emoji = _svg_icons.get(row["type"], _svg_icons["document"]) return f""" #{row['id']} — BSN
← Zurück

{type_emoji} #{row['id']}

{row['channel']} · {row['created_at']}
{f'
📍 {row["halle"]}
' if row['halle'] else ''}
{"".join([f'' if row['status'] == 'published' else ''])}
Absender
{row['sender_name'] or row['sender_id']}({row['sender_id']})
Typ
{type_emoji} {row['type']}
Inhalt
{row['content'] or '(leer)'}
Status
{row['status']}
Kategorie
{row['category'] or '-'}
📍 Halle / Stand
{row['halle'] or '– nicht erkannt –'}
🎯 Spieltitel
✍️ Für Frontend-Veröffentlichung
📍 Halle / Stand
🎯 Spieltitel
Zusammenfassung (LLM/Mensch)
Name zur Veröffentlichung
🖼️ Medien verwalten

Nur Medien mit Haken erscheinen im Frontend.

{media_html}
🗑️ Gefahrenzone

Löscht den Eintrag und alle Mediendateien unwiderruflich.

""" @app.route("/submission//update-summary", methods=["POST"]) def submission_update_summary(sub_id: int): """Update summary, publish_name, and change status.""" db = get_db() action = request.form.get("action", "save") summary = request.form.get("summary", "").strip() publish_name = request.form.get("publish_name", "").strip() halle = request.form.get("halle", "").strip() spiel_titel = request.form.get("spiel_titel", "").strip() if action == "publish": db.execute( "UPDATE submissions SET status='published', published_at=CURRENT_TIMESTAMP WHERE id=?", (sub_id,), ) elif action == "done": db.execute("UPDATE submissions SET status='done' WHERE id=?", (sub_id,)) elif action == "save": # Partial update — only update fields that are provided fields = [] params = [] if summary: fields.append("summary=?") params.append(summary) if publish_name: fields.append("publish_name=?") params.append(publish_name) if halle: fields.append("halle=?") params.append(halle) if spiel_titel: fields.append("spiel_titel=?") params.append(spiel_titel) # Clear fields explicitly set to empty if request.form.get("summary") is not None and not summary: fields.append("summary=NULL") if request.form.get("publish_name") is not None and not publish_name: fields.append("publish_name=NULL") if request.form.get("halle") is not None and not halle: fields.append("halle=NULL") if request.form.get("spiel_titel") is not None and not spiel_titel: fields.append("spiel_titel=NULL") if fields: params.append(sub_id) db.execute(f"UPDATE submissions SET {', '.join(fields)} WHERE id=?", params) elif action == "save_media": # Collect media publish flags from form import re flags_dict = {} for key in request.form: m = re.match(r'media_publish_(\d+)', key) if m: flags_dict[int(m.group(1))] = "1" # Get current media paths count row = db.execute("SELECT media_path FROM submissions WHERE id=?", (sub_id,)).fetchone() if row and row["media_path"]: n = len(row["media_path"].split("||")) flags = [flags_dict.get(i, "0") for i in range(n)] db.execute( "UPDATE submissions SET media_publish_flags=? WHERE id=?", (",".join(flags), sub_id), ) db.commit() return """""" @app.route("/submission//unpublish", methods=["POST"]) def submission_unpublish(sub_id: int): """Unpublish a submission - set status back to 'new' and clear published_at.""" db = get_db() row = db.execute("SELECT id, status FROM submissions WHERE id = ?", (sub_id,)).fetchone() if not row: return jsonify({"error": "Nicht gefunden"}), 404 if row["status"] != "published": return jsonify({"error": "Beitrag ist nicht veroffentlicht"}), 400 db.execute("UPDATE submissions SET status='new', published_at=NULL WHERE id=?", (sub_id,)) db.commit() return jsonify({"ok": True, "new_status": "new"}) @app.route("/submission//done", methods=["POST"]) def submission_done(sub_id: int): """Mark a submission as done/bearbeitet.""" db = get_db() db.execute("UPDATE submissions SET status='done' WHERE id=?", (sub_id,)) db.commit() return """
✅ Beitrag als erledigt markiert
""" @app.route("/submission//generate-summary", methods=["POST"]) def generate_llm_summary(sub_id: int): """Call DeepSeek V4 Flash to summarize the submission content.""" db = get_db() row = db.execute("SELECT * FROM submissions WHERE id = ?", (sub_id,)).fetchone() if not row: return jsonify({"error": "Nicht gefunden"}), 404 content = (row["content"] or "").strip() if not content: return jsonify({"summary": ""}) # Truncate very long content if len(content) > 3000: content = content[:3000] + "..." import requests as req import yaml # Read API key from Hermes config config_path = os.path.expanduser("~/.hermes/config.yaml") api_key = "" try: with open(config_path) as f: cfg = yaml.safe_load(f) api_key = cfg.get("providers", {}).get("datenhimmel", {}).get("api_key", "") except Exception: pass if not api_key: return jsonify({"error": "API-Key nicht konfiguriert"}), 500 prompt = ( "Du bist Redaktionsassistent für brettspiel-news.de. " "Fasse folgende Community-Nachricht in 2-3 Sätzen zusammen. " "Schreibe im Präsens, journalistisch, ohne Floskeln. " "Kein 'Der Nutzer schreibt...' oder 'In dieser Nachricht...'. " "Maximal 250 Zeichen.\n\n" f"Nachricht:\n{content}" ) try: r = req.post( "https://ui.datenhimmel.work/api/chat/completions", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }, json={ "model": "deepseek-v4-flash:cloud", "messages": [{"role": "user", "content": prompt}], "max_tokens": 300, "temperature": 0.3, }, timeout=30, ) if r.status_code == 200: data = r.json() summary = data["choices"][0]["message"]["content"].strip() return jsonify({"summary": summary}) else: return jsonify({"error": f"API-Fehler: {r.status_code}"}), 500 except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/submission//delete", methods=["POST"]) def submission_delete(sub_id: int): """Delete a submission and its media file.""" db = get_db() row = db.execute("SELECT media_path FROM submissions WHERE id = ?", (sub_id,)).fetchone() if row and row["media_path"]: for p in row["media_path"].split("||"): if os.path.exists(p): os.remove(p) db.execute("DELETE FROM submissions WHERE id = ?", (sub_id,)) db.commit() return f"""
🗑️ Beitrag #{sub_id} gelöscht
""" @app.route("/") def public_frontend(): """Public frontend showing published submissions — SPIEL Essen inspired.""" import base64 db = get_db() # Filters halle_filter = request.args.get("halle", "").strip() cat_filter = request.args.get("cat", "").strip() sort = request.args.get("sort", "neu").strip() query = "SELECT * FROM submissions WHERE status='published' AND deleted_at IS NULL" params = [] if halle_filter: query += " AND halle LIKE ?" params.append(f"{halle_filter}%") if cat_filter: query += " AND category = ?" params.append(cat_filter) if sort == "beliebt": query += " ORDER BY likes DESC, views DESC, published_at DESC LIMIT 50" elif sort == "gesehen": query += " ORDER BY views DESC, published_at DESC LIMIT 50" else: query += " ORDER BY published_at DESC LIMIT 50" rows = db.execute(query, params).fetchall() # Get available filter values — group by hall number (H1-H7) hallen = [] hall_rows = db.execute( "SELECT DISTINCT halle FROM submissions WHERE halle IS NOT NULL AND status='published' ORDER BY halle" ).fetchall() seen = set() for r in hall_rows: hnum = r[0][:2] if r[0] else "" if hnum and hnum[0] == 'H' and hnum[1:].isdigit() and hnum not in seen: seen.add(hnum) hallen.append(hnum) categories = [r[0] for r in db.execute( "SELECT DISTINCT category FROM submissions WHERE category IS NOT NULL AND status='published' ORDER BY category" ).fetchall()] empty_state = """ BSN Community — Stimmen aus der Szene

Bald geht's los!

Noch keine ver\u00f6ffentlichten Beitr\u00e4ge. Komm bald wieder — die Community f\u00fcllt diesen Ort mit spannenden Geschichten.

""" if not rows: return empty_state # Build filter bar HTML — hallen as dropdown filter_html = '
Halle: ' filter_html += '
' if cat_filter: filter_html += f'' filter_html += '
' if categories: filter_html += '📂 Kategorie: ' filter_html += '
' if halle_filter: filter_html += f'' filter_html += '
' # Sort dropdown filter_html += ' Sortierung: ' filter_html += '
' if halle_filter: filter_html += f'' if cat_filter: filter_html += f'' filter_html += '
' if halle_filter or cat_filter or sort != "neu": filter_html += '✕ Filter zurücksetzen' filter_html += "
" cards = "" for r in rows: media_html = "" media_first = "" if r["media_path"]: paths = r["media_path"].split("||") # Parse publish flags flags_str = r["media_publish_flags"] or "" flags = flags_str.split(",") if flags_str else ["1"] * len(paths) while len(flags) < len(paths): flags.append("1") mime_map = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", ".gif": "image/gif", ".mp4": "video/mp4", ".webm": "video/webm", ".mov": "video/mp4", ".ogg": "audio/ogg", ".mp3": "audio/mpeg", ".wav": "audio/wav", } for i, path in enumerate(paths): # Skip media not marked for publishing if flags[i] != "1": continue if not os.path.exists(path): continue basename = os.path.basename(path) _, ext = os.path.splitext(path) mime = mime_map.get(ext.lower(), "application/octet-stream") try: if mime.startswith("image/"): # Use thumbnail for frontend cards thumb = generate_image_thumbnail(path) if thumb: img_src = f"/media/{os.path.basename(thumb)}" else: img_src = f"/media/{basename}" img_tag = ( '' '
' 'Beitragsbild' '
' ) if i == 0 and not media_first: media_first = img_tag else: media_html += img_tag elif mime.startswith("video/"): thumb = generate_video_thumbnail(path) thumb_src = f"/media/{os.path.basename(thumb)}" if thumb else "" vid_tag = ( '' '
' + (f'Video-Vorschau' if thumb_src else '
🎬
') + '
' + '' + '
' ) if i == 0 and not media_first: media_first = vid_tag else: media_html += vid_tag elif mime.startswith("audio/"): aud_tag = '
🎤 Sprachnachricht anhören →
' media_html += aud_tag except Exception: pass _fe_svg_icons = { "text": '', "image": '', "audio": '', "video": '', "document": '', } type_emoji = _fe_svg_icons.get(r["type"], _fe_svg_icons["document"]) name = (r["publish_name"] or r["sender_name"] or "Anonym").strip() if not name or name.lower() in ("unknown", "anonym"): name = "Anonym" summary_text = (r["summary"] or r["content"] or "") # Truncate for card preview preview = summary_text[:200] + ("..." if len(summary_text) > 200 else "") date_str = (r["published_at"] or r["created_at"] or "") # Format with relative + absolute time time_html = "" try: from datetime import datetime dt = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S") now = datetime.now() diff = now - dt if diff.days == 0: if diff.seconds < 3600: time_html = f'vor {diff.seconds // 60} Min.' else: time_html = f'heute {dt.strftime("%H:%M")} Uhr' elif diff.days == 1: time_html = f'gestern {dt.strftime("%H:%M")} Uhr' elif diff.days < 7: time_html = f'vor {diff.days} Tagen {dt.strftime("%H:%M")}' else: time_html = dt.strftime("%d.%m.%y %H:%M") + " Uhr" date_str = dt.strftime("%d. %B %Y") except: pass text_only_class = " text-only" if not media_first else "" is_ausverkauft = r["category"] == "ausverkauft" and r["spiel_titel"] time_block = "" if time_html and not is_ausverkauft: time_overlay = f'
{time_html}
' time_block = time_overlay # Always overlay now elif not media_first and not is_ausverkauft: time_block = '' if not media_first: # Text-only card: link-wrapped, time overlay at top (same as media cards) cards += f"""
""" + time_block + """ """ + (f'
{r["halle"]}
' if r["halle"] else '') + """ """ + (f'
🚫 AUSVERKAUFT{(" · " + r["spiel_titel"] + "") if r["spiel_titel"] else ""}
' if r["category"] == "ausverkauft" else '') + """

""" + preview + """

""" + str(r['views'] or 0) + """
""" else: # Media card: author at bottom (same as text-only) cards += f"""
""" + media_first + time_block + """ """ + (f'
{r["halle"]}
' if r["halle"] else '') + """ """ + (f'
🚫 AUSVERKAUFT{(" · " + r["spiel_titel"] + "") if r["spiel_titel"] else ""}
' if r["category"] == "ausverkauft" else '') + """

""" + preview + """

""" + str(r['views'] or 0) + """
""" return (""" BSN Community — Stimmen aus der Szene
""" + filter_html + """
""" + cards + """
💬 BSN Assistant
Hey! 👋 Ich bin der BSN Community Assistant. Frag mich nach Spielen, Ständen oder was auf der SPIEL Essen los ist!
📨 Etwas einreichen
🎲 Schick uns, was dich bewegt! Texte, Fotos, Videos oder Sprachnachrichten — alles rund um Brettspiele und die SPIEL Essen. Wir schauen uns alles an und veröffentlichen die spannendsten Beiträge.
Nur Text oder nur eine Datei reicht völlig.
Foto
Video
Audio
""").replace("{LOGO_B64}", LOGO_B64) def generate_image_thumbnail(image_path: str, width: int = 300) -> str | None: """Generate a thumbnail for an image. Returns path to .thumb.jpg or None.""" thumb_path = image_path + ".thumb.jpg" if os.path.exists(thumb_path) and os.path.getsize(thumb_path) > 100: return thumb_path try: from PIL import Image img = Image.open(image_path) img.thumbnail((width, width * 3), Image.LANCZOS) if img.mode in ('RGBA', 'P'): img = img.convert('RGB') img.save(thumb_path, 'JPEG', quality=75) return thumb_path except Exception: return None # ── Legal pages ───────────────────────────────────────────────────── def _legal_page(title, content_html): """Render a legal page with BSN header/footer styling.""" return f""" {title} — BSN Community
{content_html} ← Zurück zur Startseite
""" @app.route("/impressum") def legal_impressum(): return _legal_page("Impressum", """

Impressum

Angaben gemäß § 5 DDG (Digitale-Dienste-Gesetz)

Verantwortlich für den Inhalt

Daniel Krause
Brettspiel-News.de
Teichweg 13a
50374 Erftstadt
Deutschland

Kontakt

Telefon: +49 152 27909535
E-Mail: kontakt@brettspiel-news.de

Umsatzsteuer-ID

USt-IdNr. gemäß § 27 a UStG: DE238244230

Redaktionell verantwortlich

Daniel Krause
Teichweg 13a
50374 Erftstadt


Hinweis zur Streitschlichtung

Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: https://ec.europa.eu/consumers/odr/

Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.

Haftung für Inhalte

Als Diensteanbieter sind wir gemäß § 7 Abs. 1 DDG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.

Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.

Haftung für Links

Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.

Urheberrecht

Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.

Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.


BSN-Chatsystem (chat.datenhimmel.work) ist ein Angebot von Brettspiel-News.de, betrieben durch Daniel Krause.

""") @app.route("/datenschutz") def legal_datenschutz(): return _legal_page("Datenschutzerklärung", """

Datenschutzerklärung

Stand: 17. Juni 2026 — Geltungsbereich: BSN-Chatsystem (chat.datenhimmel.work)

1. Verantwortlicher

Daniel Krause
Teichweg 13a
50374 Erftstadt
Deutschland
E-Mail: kontakt@brettspiel-news.de
Telefon: +49 152 27909535

2. Allgemeines zur Datenverarbeitung

2.1 Umfang

Wir verarbeiten personenbezogene Daten nur, soweit dies zur Bereitstellung des BSN-Chatsystems erforderlich ist.

2.2 Rechtsgrundlagen (Art. 6 DSGVO)

  • lit. a: Einwilligung
  • lit. b: Vertragserfüllung
  • lit. c: Rechtliche Verpflichtung
  • lit. f: Berechtigtes Interesse (Betrieb, redaktionelle Berichterstattung)

2.3 Speicherdauer

Daten werden gelöscht, sobald der Zweck entfällt, es sei denn gesetzliche Aufbewahrungspflichten bestehen.

3. Datenerhebung über WhatsApp

Verarbeitet werden: WhatsApp-Telefonnummer, Profilname, Nachrichteninhalte, Medien (Bilder, Videos, Audio), Transkriptionen, KI-Zusammenfassungen.

Zweck: Entgegennahme von Community-Beiträgen, redaktionelle Sichtung, Veröffentlichung.

Empfänger: Meta Platforms, Inc. (USA) — zertifiziert unter EU-US Data Privacy Framework. DeepSeek / datenhimmel.work — zur KI-Zusammenfassung.

4. Datenerhebung über das Web-Formular

Verarbeitet werden: Name (optional), Nachricht, hochgeladene Medien, Metadaten, KI-Zusammenfassung, Referenz-ID.

Zweck und Rechtsgrundlagen wie unter Ziffer 3.

5. Veröffentlichung von Beiträgen

Bei Veröffentlichung werden sichtbar: Ihr Name (oder "Anonyme Quelle"), Beitragsinhalt, Medien, KI-Zusammenfassung, Halle/Kategorie. Ohne Namensangabe erfolgt die Veröffentlichung als "Anonyme Quelle".

6. Chat-Assistant (KI-Chat)

Kein Zugriff auf nicht-veröffentlichte Daten. Keine Speicherung von Chat-Verläufen. Keine Verarbeitung personenbezogener Daten der Fragesteller.

7. Cookies und Tracking

Keine Cookies, kein Tracking. Nur localStorage für Theme-Präferenz (Dark/Light).

8. Ihre Rechte

  • Auskunft (Art. 15 DSGVO)
  • Berichtigung (Art. 16 DSGVO)
  • Löschung (Art. 17 DSGVO): WhatsApp: "Meine Daten löschen" senden. Web: Referenz-ID an kontakt@brettspiel-news.de
  • Einschränkung (Art. 18 DSGVO)
  • Datenübertragbarkeit (Art. 20 DSGVO)
  • Widerspruch (Art. 21 DSGVO)
  • Widerruf (Art. 7 Abs. 3 DSGVO)
  • Beschwerde (Art. 77 DSGVO): LDI NRW, Kavalleriestraße 2–4, 40213 Düsseldorf

9. Sicherheit (Art. 32 DSGVO)

HTTPS-Verschlüsselung, Zugriffskontrolle, Datenminimierung, Löschkonzept.

10. Drittlandtransfer

Meta (USA): EU-US DPF-zertifiziert. DeepSeek/datenhimmel.work: Serverstandort zu prüfen; ggf. Standardvertragsklauseln nach Art. 46 DSGVO.

11. Automatisierte Entscheidungsfindung

Kein Profiling nach Art. 22 DSGVO.


Dieses Dokument wurde mit Sorgfalt erstellt, ersetzt jedoch keine individuelle Rechtsberatung durch einen Fachanwalt für Datenschutzrecht.
""") @app.route("/agb") def legal_agb(): return _legal_page("AGB", """

Allgemeine Nutzungsbedingungen (AGB)

Geltungsbereich: BSN-Chatsystem (chat.datenhimmel.work) — Stand: 17. Juni 2026

1. Geltungsbereich

Diese AGB regeln die Nutzung des BSN-Chatsystems, betrieben von:
Daniel Krause, Teichweg 13a, 50374 Erftstadt
E-Mail: kontakt@brettspiel-news.de

Mit der Nutzung erkennen Sie diese Bedingungen an.

2. Nutzungsvoraussetzungen

Die Nutzung ist kostenlos. Kein Anspruch auf Veröffentlichung. Mindestalter: 16 Jahre.

3. Einreichung von Inhalten

Sie können Texte (max. 5.000 Zeichen), Fotos, Videos und Sprachnachrichten einreichen. Sie versichern: Urheberschaft, keine Rechtsverletzungen, Einwilligung abgebildeter Personen, keine rechtswidrigen Inhalte.

4. Rechteeinräumung

Sie räumen ein einfaches, zeitlich und räumlich unbeschränktes, unentgeltliches Nutzungsrecht ein (Speicherung, Bearbeitung, öffentliche Wiedergabe, Vervielfältigung). Sie bleiben Urheber. Das Recht erlischt bei Löschung.

5. Veröffentlichung

Keine automatische Veröffentlichung. Redaktionelle Prüfung vorbehalten. Kein Anspruch auf Veröffentlichung. Bei Veröffentlichung: Vorname/Spitzname oder "Anonyme Quelle". Beiträge sind öffentlich einsehbar.

6. Pflichten des Nutzers

Keine rechtswidrigen, Rechte-verletzenden Inhalte, kein Spam, keine Malware, keine Desinformation. Sie stellen den Betreiber von Ansprüchen Dritter frei.

7. Moderation und Löschung

Der Betreiber kann Beiträge jederzeit ändern, zurücknehmen oder löschen.

So löschen Sie Ihre Daten:
WhatsApp: "Meine Daten löschen" senden.
Web: Referenz-ID an kontakt@brettspiel-news.de

8. Haftung

Unbeschränkt bei Vorsatz/grober Fahrlässigkeit. Bei leichter Fahrlässigkeit nur bei Kardinalpflichtverletzung, begrenzt auf vorhersehbaren Schaden. Keine Haftung für Nutzerinhalte. §§ 8–10 DDG: Keine Überwachungspflicht, aber Entfernung bei Kenntnis.

9. KI-Chat-Assistant

Beantwortet Fragen auf Basis veröffentlichter Beiträge (DeepSeek V4 Flash). Antworten unverbindlich, können Fehler enthalten. Keine Speicherung von Chat-Verläufen.

10. Änderungen der AGB

Änderungen mit Wirkung für die Zukunft vorbehalten. Aktuelle Version unter chat.datenhimmel.work/agb.

11. Schlussbestimmungen

Es gilt deutsches Recht unter Ausschluss des CISG. Gerichtsstand: Erftstadt (für Kaufleute). Salvatorische Klausel.


Diese AGB regeln ausschließlich das BSN-Chatsystem. Für brettspiel-news.de gelten die dort hinterlegten Bedingungen.
Die Datenschutzerklärung ist unter chat.datenhimmel.work/datenschutz abrufbar.
""") @app.route('/media/') def serve_media(filename): """Serve media files with aggressive caching headers.""" safe = os.path.normpath(filename) if '..' in safe or safe.startswith('/'): return '', 404 filepath = MEDIA_DIR / safe if not filepath.is_file(): # Try with .thumb.jpg suffix filepath_thumb = MEDIA_DIR / (safe + '.thumb.jpg') if filepath_thumb.is_file(): filepath = filepath_thumb else: return '', 404 from flask import send_file response = send_file(str(filepath)) response.headers['Cache-Control'] = 'public, max-age=86400, immutable' response.headers['ETag'] = f'"{filepath.stat().st_mtime}-{filepath.stat().st_size}"' return response @app.route('/api/query') def api_query(): '''Instant lookup: find submissions by hall, keyword, or sender.''' db = get_db() q = request.args.get('q', '').strip() halle = request.args.get('halle', '').strip() limit = min(int(request.args.get('limit', 5)), 20) query = 'SELECT id, halle, content, summary, type, sender_name, status FROM submissions WHERE status="published"' params = [] if q: query += ' AND (content LIKE ? OR summary LIKE ?)' params.extend([f'%{q}%', f'%{q}%']) if halle: query += ' AND halle = ?' params.append(halle) query += ' ORDER BY published_at DESC LIMIT ?' params.append(limit) rows = db.execute(query, params).fetchall() results = [] for r in rows: results.append({ 'id': r['id'], 'halle': r['halle'], 'content': (r['summary'] or r['content'] or '')[:200], 'type': r['type'], 'sender_name': r['sender_name'] or 'Anonym', 'status': r['status'], }) return jsonify({'results': results, 'count': len(results)}) @app.route('/api/chat', methods=['POST']) def api_chat(): """Chat agent with read-only access to published submissions.""" db = get_db() data = request.get_json(force=True, silent=True) or {} user_message = (data.get('message') or '').strip() if not user_message: return jsonify({'reply': 'Leere Nachricht. Was möchtest du wissen?'}) # Build context from published submissions rows = db.execute( "SELECT id, halle, content, summary, type, sender_name, category, " "spiel_titel, published_at " "FROM submissions WHERE status='published' AND deleted_at IS NULL " "ORDER BY published_at DESC LIMIT 60" ).fetchall() if not rows: return jsonify({'reply': 'Noch keine Beiträge veröffentlicht. Schau später wieder vorbei!'}) # Build compact context string context_parts = [] for i, r in enumerate(rows, 1): text = (r['summary'] or r['content'] or '').strip()[:400] halle = r['halle'] or '' spiel = r['spiel_titel'] or '' cat = r['category'] or '' name = (r['sender_name'] or 'Anonym').strip() context_parts.append( f"[{i}] {text}" + (f" | 📍{halle}" if halle else '') + (f" | 🎲{spiel}" if spiel else '') + (f" | #{cat}" if cat else '') + (f" | von {name}" if name else '') ) context = '\n'.join(context_parts) # System prompt system = ( "Du bist der BSN Community Assistant von brettspiel-news.de. " "Du hilfst Besuchern, Informationen über die Brettspiel-Community auf der SPIEL Essen zu finden.\n\n" "WICHTIGE REGELN:\n" "- Beantworte Fragen NUR mit Informationen aus dem Kontext unten.\n" "- Wenn die Antwort nicht im Kontext steht, sage ehrlich: 'Dazu habe ich leider keine Informationen.'\n" "- Erfinde NIEMALS Informationen, Namen, Stände oder Spieletitel.\n" "- Antworte immer auf Deutsch, freundlich und hilfsbereit.\n" "- Halte Antworten kurz und präzise (2-4 Sätze).\n" "- Bei Standortfragen nenne immer die Hallen-Nummer.\n" "- Der Kontext enthält nummerierte Beiträge [1], [2] usw. — du kannst darauf verweisen.\n\n" f"KONTEXT (veröffentlichte Community-Beiträge):\n{context[:8000]}" ) # Read API key config_path = os.path.expanduser("~/.hermes/config.yaml") api_key = "" try: with open(config_path) as f: import yaml as _yaml cfg = _yaml.safe_load(f) api_key = cfg.get("providers", {}).get("datenhimmel", {}).get("api_key", "") except Exception: pass if not api_key: return jsonify({'reply': 'Chat-Assistent ist aktuell nicht verfügbar (API-Key fehlt).'}) # Build messages with conversation history messages = [{"role": "system", "content": system}] history = data.get('history') or [] if isinstance(history, list): for h in history[-10:]: # last 10 messages max if isinstance(h, dict) and 'role' in h and 'content' in h: messages.append({"role": h['role'], "content": h['content']}) messages.append({"role": "user", "content": user_message}) try: r = requests.post( "https://ui.datenhimmel.work/api/chat/completions", headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, json={"model": "deepseek-v4-flash:cloud", "messages": messages, "max_tokens": 400, "temperature": 0.4}, timeout=15, ) if r.status_code != 200: return jsonify({'reply': 'Der Chat-Assistent antwortet gerade nicht. Bitte versuch es später nochmal.'}) reply = r.json()["choices"][0]["message"]["content"].strip() except Exception: return jsonify({'reply': 'Der Chat-Assistent antwortet gerade nicht. Bitte versuch es später nochmal.'}) return jsonify({'reply': reply}) @app.route('/api/speak', methods=['POST']) def api_speak(): """Generate TTS audio via Piper (Thorsten voice) and return as WAV.""" import subprocess import tempfile data = request.get_json(force=True, silent=True) or {} text = (data.get('text') or '').strip() if not text or len(text) > 2000: return jsonify({'error': 'Text required, max 2000 chars'}), 400 model = '/home/hermes/.local/share/piper-voices/de_DE-thorsten-medium.onnx' if not os.path.exists(model): model = '/home/hermes/.hermes/cache/piper-voices/de_DE-thorsten-medium.onnx' if not os.path.exists(model): return jsonify({'error': 'Thorsten voice not found'}), 500 with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp: tmp_path = tmp.name try: env = os.environ.copy() env['LD_LIBRARY_PATH'] = '/home/hermes/.local/lib:' + env.get('LD_LIBRARY_PATH', '') env['ESPEAK_DATA_PATH'] = '/home/hermes/.local/share/espeak-ng-data' result = subprocess.run( ['/home/hermes/.local/bin/piper', '--model', model, '--output_file', tmp_path], input=text.encode('utf-8'), capture_output=True, timeout=30, env=env, ) if result.returncode != 0: return jsonify({'error': 'TTS generation failed'}), 500 from flask import send_file response = send_file(tmp_path, mimetype='audio/wav', as_attachment=False) response.headers['X-Content-Type-Options'] = 'nosniff' return response except Exception as e: return jsonify({'error': f'TTS failed: {str(e)}'}), 500 finally: # Clean up temp file after response is sent import threading threading.Timer(5.0, lambda: os.unlink(tmp_path) if os.path.exists(tmp_path) else None).start() @app.route('/api/submit', methods=['POST']) def api_submit(): """Public submission endpoint — text + optional media upload.""" db = get_db() name = (request.form.get('name') or '').strip()[:100] text = (request.form.get('text') or '').strip()[:5000] halle = (request.form.get('halle') or '').strip()[:20] spiel_titel = (request.form.get('spiel_titel') or '').strip()[:200] email = (request.form.get('email') or '').strip()[:200] # Require at least text or a file file = request.files.get('media') if not text and (not file or not file.filename): return jsonify({'error': 'Bitte Text oder eine Datei (Foto/Video/Audio) angeben.'}), 400 media_path = None msg_type = 'text' if file and file.filename: # Determine type from extension ext = os.path.splitext(file.filename)[1].lower() mime_map = { '.jpg': 'image', '.jpeg': 'image', '.png': 'image', '.webp': 'image', '.gif': 'image', '.mp4': 'video', '.webm': 'video', '.mov': 'video', '.ogg': 'audio', '.mp3': 'audio', '.wav': 'audio', } msg_type = mime_map.get(ext, 'document') # Add type prefix to content prefix = { 'image': '\n[🖼️ Bild]', 'video': '\n[🎬 Video]', 'audio': '\n[🎤 Sprachnachricht]', 'document': '\n[📎 Dokument]', }.get(msg_type, f'\n[{msg_type}]') if text: text = text + prefix else: text = prefix.strip() # Save file ts = datetime.now().strftime('%Y%m%d_%H%M%S') safe_name = f"web_{ts}{ext}" save_path = MEDIA_DIR / safe_name try: file.save(str(save_path)) media_path = str(save_path) # Convert .mov to .mp4 for browser compatibility if ext == '.mov': mp4_path = save_path.with_suffix('.mp4') import subprocess result = subprocess.run( ['ffmpeg', '-y', '-i', str(save_path), '-c:v', 'libx264', '-preset', 'fast', '-crf', '23', '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', str(mp4_path)], capture_output=True, text=True, timeout=120 ) if result.returncode == 0 and mp4_path.exists(): os.remove(str(save_path)) # Remove original .mov media_path = str(mp4_path) msg_type = 'video' # If conversion fails, keep .mov as-is except Exception: return jsonify({'error': 'Datei konnte nicht gespeichert werden.'}), 500 # Validate halle halle_valid = None if halle: if re.match(r'^H[1-7]\s*[A-Z]\d{1,4}$', halle, re.IGNORECASE): halle_valid = halle.upper() sender_id = 'web_' + (name.replace(' ', '_') if name else 'anonym') sender_name = name or None # #out detection for web form category = None if text and "#out" in text.lower(): category = "ausverkauft" if not spiel_titel: idx = text.lower().find("#out") after = text[idx + 4:].strip() if after: spiel_titel = re.split(r'[.!?\n]| - | – | — | ist | war | in ', after, maxsplit=1)[0].strip()[:200] db.execute( """INSERT INTO submissions (channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel, email) VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?, ?)""", ('web', sender_id, sender_name, msg_type, text or None, media_path, halle_valid, category, spiel_titel or None, email or None), ) db.commit() sub_id = db.execute("SELECT last_insert_rowid()").fetchone()[0] # Auto-summarize via LLM (only if no halle was provided by user) if not halle_valid and text and len(text.strip()) > 20: import threading threading.Thread( target=auto_process_bg, args=(DB_PATH, sub_id), daemon=True, ).start() return jsonify({ 'ok': True, 'message': f'Beitrag #{sub_id} erfolgreich übermittelt! 🎲', 'submission_id': sub_id, 'delete_hint': 'Notiere dir diese ID — mit ihr kannst du den Beitrag später löschen lassen.', }) @app.route("/beitrag/") def public_detail(sub_id: int): """Public detail view — full media, text, and metadata.""" import base64 db = get_db() row = db.execute( "SELECT * FROM submissions WHERE id = ? AND status='published' AND deleted_at IS NULL", (sub_id,), ).fetchone() if not row: return "

Nicht gefunden

← Zurück

", 404 # Track view db.execute("UPDATE submissions SET views = views + 1 WHERE id = ?", (sub_id,)) db.commit() # Build media HTML — videos are playable, images full-size media_html = "" if row["media_path"]: paths = row["media_path"].split("||") flags_str = row["media_publish_flags"] or "" flags = flags_str.split(",") if flags_str else ["1"] * len(paths) while len(flags) < len(paths): flags.append("1") mime_map = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", ".gif": "image/gif", ".mp4": "video/mp4", ".webm": "video/webm", ".mov": "video/mp4", ".ogg": "audio/ogg", ".mp3": "audio/mpeg", ".wav": "audio/wav", } for i, path in enumerate(paths): if flags[i] != "1": continue if not os.path.exists(path): continue _, ext = os.path.splitext(path) mime = mime_map.get(ext.lower(), "application/octet-stream") try: basename = os.path.basename(path) src = f"/media/{basename}" if mime.startswith("image/"): media_html += f'Bild' elif mime.startswith("video/"): thumb = generate_video_thumbnail(path) poster = f' poster="/media/{os.path.basename(thumb)}"' if thumb else "" media_html += ( f'' ) elif mime.startswith("audio/"): media_html += f'' except Exception: pass _detail_svg_icons = { "text": '', "image": '', "audio": '', "video": '', "document": '', } type_emoji = _detail_svg_icons.get(row["type"], _detail_svg_icons["document"]) name = (row["publish_name"] or row["sender_name"] or "Anonym").strip() if not name or name.lower() in ("unknown", "anonym"): name = "Anonym" date_str = row["published_at"] or row["created_at"] or "" try: from datetime import datetime as dt d = dt.strptime(date_str, "%Y-%m-%d %H:%M:%S") date_str = d.strftime("%d. %B %Y") except Exception: pass content = (row["summary"] or row["content"] or "").strip() # Comments comment_rows = db.execute( "SELECT author_name, content, created_at FROM comments WHERE submission_id = ? ORDER BY created_at ASC", (sub_id,), ).fetchall() comments_html = "" for c in comment_rows: c_author = (c["author_name"] or "Anonym").strip() c_date = c["created_at"] or "" try: cd = datetime.strptime(c_date, "%Y-%m-%d %H:%M:%S") c_date = cd.strftime("%d.%m.%y %H:%M") except Exception: pass comments_html += f"""
{c_author} {c_date}

{c["content"]}

""" return f""" Beitrag — BSN Community
← Zurück zur Übersicht
{"".join([f'
{row["halle"]}
' if row["halle"] else '', f'
🚫 AUSVERKAUFT{(" · " + row["spiel_titel"] + "") if row["spiel_titel"] else ""}
' if row["category"] == "ausverkauft" else ''])}
{type_emoji} {name} {date_str}
{media_html}
{"".join([f'
{content}
' if content else ''])}
{row['views'] or 0} Aufrufe

💬 Kommentare ({len(comment_rows)})

{comments_html}
Beitrags-ID: #{sub_id} \u2014 Bei Fragen oder Löschwunsch bitte diese ID angeben.
""" @app.route("/api/like/", methods=["POST"]) def api_like(sub_id: int): db = get_db() row = db.execute( "SELECT id, likes FROM submissions WHERE id = ? AND status='published' AND deleted_at IS NULL", (sub_id,), ).fetchone() if not row: return jsonify({"error": "Nicht gefunden"}), 404 liked = request.cookies.get(f"liked_{sub_id}", "") if liked == "1": return jsonify({"likes": row["likes"] or 0, "already": True}) db.execute("UPDATE submissions SET likes = likes + 1 WHERE id = ?", (sub_id,)) db.commit() new_likes = db.execute("SELECT likes FROM submissions WHERE id = ?", (sub_id,)).fetchone()[0] resp = jsonify({"likes": new_likes or 0, "already": False}) resp.set_cookie(f"liked_{sub_id}", "1", max_age=365*24*60*60, httponly=True, samesite="Lax") return resp @app.route("/api/comment/", methods=["POST"]) def api_comment(sub_id: int): db = get_db() row = db.execute( "SELECT id, channel, sender_id, sender_name, email FROM submissions WHERE id = ? AND status='published' AND deleted_at IS NULL", (sub_id,), ).fetchone() if not row: return jsonify({"error": "Beitrag nicht gefunden"}), 404 data = request.get_json(force=True, silent=True) or {} author = (data.get("author") or "").strip()[:80] content = (data.get("content") or "").strip()[:2000] if not content: return jsonify({"error": "Kommentar darf nicht leer sein"}), 400 db.execute( "INSERT INTO comments (submission_id, author_name, content) VALUES (?, ?, ?)", (sub_id, author or None, content), ) db.commit() # Build notification link base = request.host_url.rstrip("/") link = f"{base}/beitrag/{sub_id}" # WhatsApp notification (only for WhatsApp submissions) if row["channel"] == "whatsapp" and row["sender_id"]: try: msg = ( f"💬 *Neuer Kommentar zu deinem Beitrag!*\n\n" f"{'*' + (author or 'Jemand') + '* schreibt' if author else 'Jemand hat kommentiert'}:\n" f"_{content[:200]}{'...' if len(content) > 200 else ''}_\n\n" f"👉 {link}" ) send_whatsapp_message(row["sender_id"], msg) except Exception: pass # silently ignore notification failures # Email notification (only for web submissions with email) if row["channel"] == "web" and row["email"]: try: subject = f"💬 Neuer Kommentar zu deinem BSN-Beitrag #{sub_id}" body = ( f"Hallo,\n\n" f"{author or 'Jemand'} hat deinen Beitrag kommentiert:\n\n" f"\"{content[:300]}{'...' if len(content) > 300 else ''}\"\n\n" f"👉 {link}\n\n" f"Viele Grüße,\n" f"Boardgame Social / brettspiel-news.de\n" ) _send_email(row["email"], subject, body) except Exception: pass # silently ignore notification failures return jsonify({"ok": True}), 201 if __name__ == "__main__": import threading def keepalive(): """Prevent process from being killed by inactivity timeout.""" import time while True: time.sleep(60) import sys sys.stdout.write("[keepalive]\n") sys.stdout.flush() threading.Thread(target=keepalive, daemon=True).start() print(f"🚀 BSN Intake starting on http://127.0.0.1:{PORT}") print(f" DB: {DB_PATH}") print(f" Media: {MEDIA_DIR}") print(f" Meta API: {'✅ configured' if META_TOKEN else '❌ no token'}") print(f" Admin: http://127.0.0.1:{PORT}/admin") print(f" Webhook: http://127.0.0.1:{PORT}/whatsapp/webhook") app.run(host="127.0.0.1", port=PORT, debug=False)