From 7c96743237ead436802a9cbef18c6ed4588be0ce Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 18 Jun 2026 01:28:34 +0200 Subject: [PATCH] =?UTF-8?q?Double-Opt-In=20Verification=20f=C3=BCr=20Komme?= =?UTF-8?q?ntare=20(E-Mail/WhatsApp)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neues comments.verification_token Feld (UUID) - /api/comment: Bei VERIFICATION_REQUIRED=1 → pending + Token - Verification-Link per E-Mail und WhatsApp versendet - GET /verify-comment/: Prüfung → LLM-Moderation → published/blocked - Testsystem-Bypass: VERIFICATION_REQUIRED=0 → direkt published (default) - Bestehende 40 Kommentare bleiben unangetastet (status='published') - Token-Einmalgebrauch: 404 bei Wiederverwendung --- app.py | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 95 insertions(+), 13 deletions(-) diff --git a/app.py b/app.py index 19eec7c..1527d3c 100644 --- a/app.py +++ b/app.py @@ -16,8 +16,10 @@ Usage: import json import os import re +import secrets import sqlite3 import sys +import uuid from datetime import datetime from pathlib import Path @@ -45,6 +47,8 @@ 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")) +VERIFICATION_REQUIRED = os.environ.get("VERIFICATION_REQUIRED", "").lower() in ("1", "true", "yes") +# Test system: verification is OFF by default (set VERIFICATION_REQUIRED=1 in .env to enable) MEDIA_DIR = BASE_DIR / "media" MEDIA_DIR.mkdir(exist_ok=True) @@ -3741,14 +3745,58 @@ def api_comment(sub_id: int): if not email and not phone: return jsonify({"error": "Bitte E-Mail-Adresse oder Handynummer angeben, damit wir dich bei Antworten benachrichtigen können."}), 400 + verification_token = str(uuid.uuid4()) if VERIFICATION_REQUIRED else None + db.execute( - "INSERT INTO comments (submission_id, author_name, content, parent_id, email, phone) VALUES (?, ?, ?, ?, ?, ?)", - (sub_id, author or None, content, parent_id, email or None, phone or None), + "INSERT INTO comments (submission_id, author_name, content, parent_id, email, phone, status, verification_token) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + (sub_id, author or None, content, parent_id, email or None, phone or None, "pending" if VERIFICATION_REQUIRED else "published", verification_token), ) db.commit() comment_id = db.execute("SELECT last_insert_rowid()").fetchone()[0] - # LLM Moderation — run after insert, update status + # === VERIFICATION REQUIRED (production mode) === + if VERIFICATION_REQUIRED and verification_token: + base = request.host_url.rstrip("/") + verify_url = f"{base}/verify-comment/{verification_token}" + + # Send verification via WhatsApp + if phone: + try: + msg = ( + f"📨 *Bitte bestätige deinen Kommentar!*\n\n" + f"Jemand (du?) hat auf boardgame.network kommentiert:\n" + f"_{content[:150]}{'...' if len(content) > 150 else ''}_\n\n" + f"Klicke hier zum Bestätigen:\n" + f"👉 {verify_url}\n\n" + f"Falls du das nicht warst, ignoriere diese Nachricht einfach." + ) + send_whatsapp_message(phone, msg) + except Exception: + pass + + # Send verification via email + if email: + try: + subject = "📨 Bitte bestätige deinen Kommentar auf boardgame.network" + body = ( + f"Hallo,\n\n" + f"jemand (du?) hat auf boardgame.network kommentiert:\n\n" + f"\"{content[:200]}{'...' if len(content) > 200 else ''}\"\n\n" + f"Klicke zum Bestätigen:\n" + f"👉 {verify_url}\n\n" + f"Falls du das nicht warst, ignoriere diese E-Mail einfach.\n\n" + f"Viele Grüße,\n" + f"boardgame.network\n" + ) + _send_email(email, subject, body) + except Exception: + pass + + return jsonify({"ok": True, "comment_id": comment_id, "pending": True, + "message": "Bitte bestätige deinen Kommentar über den Link in deiner E-Mail oder WhatsApp-Nachricht."}), 201 + + # === NO VERIFICATION (test system) — immediate moderation + notification === + # LLM Moderation mod_status, mod_reason = moderate_comment(content) db.execute( "UPDATE comments SET status=?, moderation_reason=? WHERE id=?", @@ -3756,7 +3804,6 @@ def api_comment(sub_id: int): ) db.commit() - # If blocked, skip notifications and return success silently if mod_status == "blocked": print(f"[moderate] Comment #{comment_id} BLOCKED: {mod_reason}", file=sys.stderr) return jsonify({"ok": True, "comment_id": comment_id}), 201 @@ -3768,11 +3815,10 @@ def api_comment(sub_id: int): # 1. Notify parent comment author (if replying to a comment) if parent_id: parent = db.execute( - "SELECT author_name, email, phone FROM comments WHERE id = ? AND submission_id = ?", + "SELECT author_name, email, phone FROM comments WHERE id = ? AND submission_id = ? AND status='published'", (parent_id, sub_id), ).fetchone() if parent: - p_author = parent["author_name"] or "Jemand" # WhatsApp to parent comment author if parent["phone"]: try: @@ -3788,7 +3834,7 @@ def api_comment(sub_id: int): # Email to parent comment author if parent["email"]: try: - subject = f"💬 Antwort auf deinen BSN-Kommentar" + subject = "💬 Antwort auf deinen BSN-Kommentar" body = ( f"Hallo,\n\n" f"{author or 'Jemand'} hat auf deinen Kommentar geantwortet:\n\n" @@ -3805,14 +3851,13 @@ def api_comment(sub_id: int): notify_submission_author = True if parent_id: parent = db.execute( - "SELECT author_name, email, phone FROM comments WHERE id = ? AND submission_id = ?", + "SELECT author_name, email, phone FROM comments WHERE id = ? AND submission_id = ? AND status='published'", (parent_id, sub_id), ).fetchone() if parent and (parent["email"] == row["email"] or parent["phone"] == row["phone"]): - notify_submission_author = False # Conversation with themselves + notify_submission_author = False if notify_submission_author: - # WhatsApp notification if row["channel"] == "whatsapp" and row["sender_id"]: try: msg = ( @@ -3824,7 +3869,6 @@ def api_comment(sub_id: int): send_whatsapp_message(row["sender_id"], msg) except Exception: pass - # Email to submission author if row["email"]: try: subject = f"💬 Neuer Kommentar zu deinem BSN-Beitrag #{sub_id}" @@ -3839,8 +3883,6 @@ def api_comment(sub_id: int): _send_email(row["email"], subject, body) except Exception: pass - - # WhatsApp to submission author via phone if row["phone"]: try: msg = ( @@ -3856,6 +3898,46 @@ def api_comment(sub_id: int): return jsonify({"ok": True, "comment_id": comment_id}), 201 +@app.route("/verify-comment/") +def verify_comment(token: str): + """Verify a pending comment via token link.""" + db = get_db() + row = db.execute( + "SELECT c.*, s.spiel_titel FROM comments c LEFT JOIN submissions s ON c.submission_id = s.id WHERE c.verification_token = ? AND c.status = 'pending'", + (token,), + ).fetchone() + if not row: + return "

Ungültiger Link

Dieser Bestätigungslink ist nicht gültig oder wurde bereits verwendet.

← Zur Startseite

", 404 + + # Run LLM moderation + mod_status, mod_reason = moderate_comment(row["content"]) + db.execute( + "UPDATE comments SET status=?, moderation_reason=?, verification_token=NULL WHERE id=?", + (mod_status if mod_status == "blocked" else "published", mod_reason, row["id"]), + ) + db.commit() + + if mod_status == "blocked": + return f"

🚫 Kommentar abgelehnt

{mod_reason or 'Verstoß gegen Community-Richtlinien'}

← Zur Startseite

", 403 + + # Redirect to the submission detail page + return f""" + + +Kommentar bestätigt + + +

✅ Kommentar bestätigt!

+

Dein Kommentar ist jetzt auf boardgame.network sichtbar.

+

Du wirst in 3 Sekunden weitergeleitet…
→ Direkt zum Beitrag

+ +""" + + if __name__ == "__main__": import threading def keepalive():