#!/usr/bin/env python3 """Fehler-Melde-Server fuer brettspiel-news.de Mini-HTTP-Server auf Port 8888. Nur das Formular + API. Isoliert von allen anderen Diensten. Kein Flask, nur stdlib.""" import http.server import json import sqlite3 import os import urllib.parse import urllib.request from datetime import datetime PORT = 8888 HOST = "127.0.0.1" DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fehler.db") TOKEN = os.environ.get("FEHLER_TOKEN", "br3ttsp1el-f3hl3r-2026") TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "") # ═══════════════════════════════════════════════════ # SQLite # ═══════════════════════════════════════════════════ def init_db(): conn = sqlite3.connect(DB_PATH) conn.execute(""" CREATE TABLE IF NOT EXISTS fehler ( id INTEGER PRIMARY KEY AUTOINCREMENT, article_id TEXT NOT NULL, melder_name TEXT NOT NULL, melder_email TEXT DEFAULT '', fehler_text TEXT NOT NULL, status TEXT DEFAULT 'offen', loesung TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) ) """) conn.commit() conn.close() def save_report(article_id, name, email, text): conn = sqlite3.connect(DB_PATH) conn.execute( "INSERT INTO fehler (article_id, melder_name, melder_email, fehler_text) VALUES (?,?,?,?)", (str(article_id), name, email, text) ) rid = conn.execute("SELECT last_insert_rowid()").fetchone()[0] conn.commit() conn.close() return rid def get_stats(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row total = conn.execute("SELECT COUNT(*) as c FROM fehler").fetchone()["c"] offen = conn.execute("SELECT COUNT(*) as c FROM fehler WHERE status='offen'").fetchone()["c"] behoben = conn.execute("SELECT COUNT(*) as c FROM fehler WHERE status='behoben'").fetchone()["c"] verworfen = conn.execute("SELECT COUNT(*) as c FROM fehler WHERE status='verworfen'").fetchone()["c"] conn.close() return {"total": total, "offen": offen, "behoben": behoben, "verworfen": verworfen} def get_meldungen(status_filter=None, limit=100): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row if status_filter: rows = conn.execute( "SELECT * FROM fehler WHERE status=? ORDER BY created_at DESC LIMIT ?", (status_filter, limit) ).fetchall() else: rows = conn.execute( "SELECT * FROM fehler ORDER BY created_at DESC LIMIT ?", (limit,) ).fetchall() conn.close() return [dict(r) for r in rows] def update_status(rid, status, loesung): conn = sqlite3.connect(DB_PATH) conn.execute( "UPDATE fehler SET status=?, loesung=?, updated_at=datetime('now','localtime') WHERE id=?", (status, loesung, rid) ) conn.commit() conn.close() # ═══════════════════════════════════════════════════ # Basti-Benachrichtigung (Datei-basiert) # ═══════════════════════════════════════════════════ FEHLER_EINGANG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "eingang") def notify_basti(report_id, article_id, name, text): """Schreibt Fehlermeldung als .md-Datei für Basti.""" os.makedirs(FEHLER_EINGANG, exist_ok=True) ts = datetime.now().strftime("%Y%m%d-%H%M%S") fname = f"{ts}_{report_id}.md" fpath = os.path.join(FEHLER_EINGANG, fname) url = f"https://www.brettspiel-news.de/index.php/de/component/content/article/{article_id}" content = f"""# 🐛 Fehlermeldung #{report_id} **Artikel:** [#{article_id}]({url}) **Gemeldet von:** {name} **Datum:** {datetime.now().strftime('%d.%m.%Y %H:%M')} ## Fehlerbeschreibung {text} --- *Status: offen | Bearbeiter: Basti* """ with open(fpath, "w") as f: f.write(content) print(f"[OK] Basti benachrichtigt → {fname}") return fpath # ═══════════════════════════════════════════════════ # HTML-Formular (inline, self-contained) # ═══════════════════════════════════════════════════ FORM_HTML = r""" Fehler melden
""" # ═══════════════════════════════════════════════════ # JSON-Helfer # ═══════════════════════════════════════════════════ def json_response(handler, status, data): body = json.dumps(data, ensure_ascii=False).encode() handler.send_response(status) handler.send_header("Content-Type", "application/json; charset=utf-8") handler.send_header("Access-Control-Allow-Origin", "*") handler.send_header("Content-Length", str(len(body))) handler.end_headers() handler.wfile.write(body) # ═══════════════════════════════════════════════════ # HTTP-Handler # ═══════════════════════════════════════════════════ class FehlerHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): p = urllib.parse.urlparse(self.path) # Formular: / oder /form if p.path in ("/", "/form"): html = FORM_HTML.encode() self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(html))) self.end_headers() self.wfile.write(html) return # Stats (nur localhost) if p.path == "/api/stats": if self.client_address[0] not in ("127.0.0.1", "::1"): json_response(self, 403, {"error": "forbidden"}) return json_response(self, 200, get_stats()) return # Meldungen (nur localhost) if p.path == "/api/meldungen": if self.client_address[0] not in ("127.0.0.1", "::1"): json_response(self, 403, {"error": "forbidden"}) return qs = urllib.parse.parse_qs(p.query) status = qs.get("status", [None])[0] json_response(self, 200, get_meldungen(status)) return self.send_error(404) def do_POST(self): # CORS-Preflight if self.path == "/report": length = int(self.headers.get("Content-Length", 0)) if length == 0: json_response(self, 400, {"error": "Leerer Request"}) return body = self.rfile.read(length) try: data = json.loads(body) except json.JSONDecodeError: json_response(self, 400, {"error": "Ungültiges JSON"}) return article_id = data.get("article_id", "") melder_name = data.get("melder_name", "").strip() melder_email = data.get("melder_email", "").strip() fehler_text = data.get("fehler_text", "").strip() if not melder_name: json_response(self, 400, {"error": "Name ist erforderlich"}) return if not fehler_text: json_response(self, 400, {"error": "Fehlerbeschreibung ist erforderlich"}) return rid = save_report(article_id, melder_name, melder_email, fehler_text) notify_basti(rid, article_id, melder_name, fehler_text) json_response(self, 201, {"id": rid, "message": "Fehler gemeldet \u2014 danke!"}) return # Status-Update (mit Token) if self.path == "/api/update": length = int(self.headers.get("Content-Length", 0)) data = json.loads(self.rfile.read(length)) if data.get("token") != TOKEN: json_response(self, 403, {"error": "forbidden"}) return update_status(data["id"], data["status"], data.get("loesung", "")) json_response(self, 200, {"ok": True}) return self.send_error(404) def do_OPTIONS(self): self.send_response(204) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() def log_message(self, format, *args): print(f"[{datetime.now().strftime('%H:%M:%S')}] {args[0]}") # ═══════════════════════════════════════════════════ # Main # ═══════════════════════════════════════════════════ if __name__ == "__main__": init_db() print(f"🐛 Fehler-Server auf http://{HOST}:{PORT}") print(f" Formular: http://{HOST}:{PORT}/?article_id=123") print(f" Stats: http://{HOST}:{PORT}/api/stats") httpd = http.server.HTTPServer((HOST, PORT), FehlerHandler) try: httpd.serve_forever() except KeyboardInterrupt: print("\n👋 Bye!") httpd.server_close()