Double-Opt-In Verification für Kommentare (E-Mail/WhatsApp)
- 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/<token>: 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
This commit is contained in:
@@ -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/<token>")
|
||||
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 "<h1>Ungültiger Link</h1><p>Dieser Bestätigungslink ist nicht gültig oder wurde bereits verwendet.</p><p><a href='/'>← Zur Startseite</a></p>", 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"<html lang=de><body style='font-family:sans-serif;text-align:center;padding:3rem;'><h1 style='color:#c62828;'>🚫 Kommentar abgelehnt</h1><p>{mod_reason or 'Verstoß gegen Community-Richtlinien'}</p><p><a href='/'>← Zur Startseite</a></p></body></html>", 403
|
||||
|
||||
# Redirect to the submission detail page
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head><meta charset="UTF-8"><meta http-equiv="refresh" content="3;url=/beitrag/{row['submission_id']}">
|
||||
<title>Kommentar bestätigt</title>
|
||||
<style>
|
||||
body {{font-family:'Inter',sans-serif;text-align:center;padding:3rem;background:#f5f3ee;color:#1a1a2e;}}
|
||||
h1 {{color:#20228a;}}
|
||||
a {{color:#20228a;}}
|
||||
</style></head>
|
||||
<body>
|
||||
<h1>✅ Kommentar bestätigt!</h1>
|
||||
<p>Dein Kommentar ist jetzt auf boardgame.network sichtbar.</p>
|
||||
<p>Du wirst in 3 Sekunden weitergeleitet…<br><a href="/beitrag/{row['submission_id']}">→ Direkt zum Beitrag</a></p>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import threading
|
||||
def keepalive():
|
||||
|
||||
Reference in New Issue
Block a user