diff --git a/app.py b/app.py index f3cf7a9..c158ca3 100644 --- a/app.py +++ b/app.py @@ -47,6 +47,9 @@ 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")) +TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN", "") +TELEGRAM_API = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}" if TELEGRAM_TOKEN else "" + 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) @@ -4131,6 +4134,272 @@ def verify_comment(token: str): """ + +# -*- coding: utf-8 -*- +"""Telegram Bot webhook handler — integrated into app.py""" + +def _telegram_download_file(file_id): + """Download a file from Telegram servers. Returns local path or None.""" + import requests as _r + if not TELEGRAM_TOKEN: + return None + try: + gr = _r.get("{}/getFile?file_id={}".format(TELEGRAM_API, file_id), timeout=10) + gr.raise_for_status() + gdata = gr.json() + if not gdata.get("ok"): + print("[telegram] getFile failed: {}".format(gdata), file=sys.stderr) + return None + file_path = gdata["result"]["file_path"] + ext = os.path.splitext(file_path)[1].lower() or ".bin" + safe_name = "tg_{}{}".format(file_id, ext) + dest = str(MEDIA_DIR / safe_name) + dl_url = "https://api.telegram.org/file/bot{}/{}".format(TELEGRAM_TOKEN, file_path) + fr = _r.get(dl_url, timeout=60) + fr.raise_for_status() + with open(dest, "wb") as f: + f.write(fr.content) + if ext == ".mov": + mp4_path = dest + ".mp4" + import subprocess + result = subprocess.run( + ["/home/hermes/.local/bin/ffmpeg", "-y", "-i", dest, + "-c:v", "libx264", "-preset", "fast", "-crf", "23", + "-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart", mp4_path], + capture_output=True, text=True, timeout=120 + ) + if result.returncode == 0 and os.path.exists(mp4_path): + os.remove(dest) + return mp4_path + return dest + except Exception as e: + print("[telegram download error] {}".format(e), file=sys.stderr) + return None + + +def _telegram_autoreply(chat_id, sender_name, is_new_user): + """Send welcome or thank-you message via Telegram Send API.""" + if not TELEGRAM_TOKEN: + return + import requests as _r + try: + if is_new_user: + text = ( + "🎲 Hey {}! Cool, dass du da bist — willkommen in der ".format(sender_name) + + "brettspiel-news.de Community!\n\n" + "Schick uns einfach, was dich bewegt: 💬 Text, 🎤 Sprachnachrichten, " + "🖼️ Fotos oder 🎬 Videos — alles rund um Brettspiele. Wir schauen uns alles an!\n\n" + "✍️ Nenn uns deinen Vornamen, wenn du magst — das ist deine Einwilligung, " + "dass wir dich als Quelle nennen, falls wir deinen Beitrag verwenden. " + "Ohne Namen schreiben wir „anonyme Quelle\".\n\n" + "🔒 „Meine Daten löschen\" genügt — und alles ist sofort und vollständig weg." + ) + else: + text = "📨 Danke {}! Ist angekommen — wir kümmern uns drum.".format(sender_name) + + _r.post( + "{}/sendMessage".format(TELEGRAM_API), + json={"chat_id": chat_id, "text": text}, + timeout=10, + ) + except Exception as e: + print("[telegram reply error] {}".format(e), file=sys.stderr) + + +@app.route("/telegram/webhook", methods=["GET", "POST"]) +def telegram_webhook(): + """Telegram Bot Webhook. GET=health check, POST=incoming messages.""" + if request.method == "GET": + if TELEGRAM_TOKEN: + import requests as _r + try: + r = _r.get("{}/getMe".format(TELEGRAM_API), timeout=5) + data = r.json() + if data.get("ok"): + return jsonify({"status": "ok", "bot": data["result"]["username"]}) + except Exception: + pass + return jsonify({"status": "ok", "token_configured": True}) + return jsonify({"status": "off", "reason": "TELEGRAM_TOKEN not set"}), 500 + + # POST: Incoming message + body = request.get_json(force=True, silent=True) + if not body or not TELEGRAM_TOKEN: + return "OK", 200 + + try: + msg = body.get("message", {}) + if not msg: + return "OK", 200 + + chat = msg.get("chat", {}) + sender_id = str(chat.get("id", "unknown")) + sender_name = msg.get("from", {}).get("first_name", "") or "" + last_name = msg.get("from", {}).get("last_name", "") + if last_name: + sender_name = "{} {}".format(sender_name, last_name).strip() + if not sender_name: + sender_name = sender_id + + msg_type = "text" + content = "" + media_path = None + file_id = None + + if "text" in msg: + msg_type = "text" + content = msg["text"] + elif "photo" in msg: + msg_type = "image" + content = msg.get("caption", "[Bild ohne Text]") + file_id = msg["photo"][-1]["file_id"] + elif "video" in msg: + msg_type = "video" + content = msg.get("caption", "[🎬 Video — Transkription läuft...]") + file_id = msg["video"]["file_id"] + elif "voice" in msg: + msg_type = "audio" + content = "[🎤 Sprachnachricht — Transkription läuft...]" + file_id = msg["voice"]["file_id"] + elif "audio" in msg: + msg_type = "audio" + content = msg.get("caption", "[🎤 Audio]") + file_id = msg["audio"]["file_id"] + elif "document" in msg: + msg_type = "document" + content = msg.get("caption", "[📎 Dokument]") + file_id = msg["document"]["file_id"] + elif "sticker" in msg: + content = "[Sticker: {}]".format(msg['sticker'].get('emoji', '')) + msg_type = "document" + elif "location" in msg: + lat = msg["location"]["latitude"] + lon = msg["location"]["longitude"] + content = "[📍 Standort: {}, {}]".format(lat, lon) + msg_type = "text" + else: + content = "[Unbekannter Nachrichtentyp]" + msg_type = "text" + + if file_id: + media_path = _telegram_download_file(file_id) + + # GDPR delete trigger + if msg_type == "text" and content.strip().lower() == "meine daten löschen": + _gdpr_delete_sender(get_db(), sender_id) + if TELEGRAM_TOKEN: + import requests as _r + try: + _r.post( + "{}/sendMessage".format(TELEGRAM_API), + json={"chat_id": int(sender_id), + "text": "✅ Alle deine Daten wurden vollständig und endgültig vom Server gelöscht. " + "Es befinden sich keinerlei personenbezogene Daten mehr von dir bei uns."}, + timeout=10, + ) + except Exception: + pass + return "OK", 200 + + db = get_db() + spiel_titel = None + category_override = None + + 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] + + halle = extract_halle(content) + + THREAD_WINDOW_MINUTES = 5 + recent = db.execute( + """SELECT id, content, media_path, halle FROM submissions + WHERE channel='telegram' 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='telegram' AND sender_id=? AND status != 'gdpr_deleted'", + (sender_id,), + ).fetchone()[0] == 0 + + 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: + prefix = { + "text": "", "image": "\n[🖼️ Bild] ", + "audio": "\n[🎤 Sprachnachricht]", "video": "\n[🎬 Video] ", + "document": "\n[📎 Dokument] ", + }.get(msg_type, "\n[{}] ".format(msg_type)) + new_content = (recent["content"] or "") + prefix + 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"]), + ) + if "#out" in content.lower(): + out_title = None + idx2 = content.lower().find("#out") + after2 = content[idx2 + 4:].strip() + if after2: + out_title = re.split(r'[.!?\n]| - | – | — | ist | war | in ', after2, maxsplit=1)[0].strip()[:200] + db.execute("UPDATE submissions SET category=?, spiel_titel=? WHERE id=?", + ("ausverkauft", out_title, recent["id"])) + if out_title: + mark_ausverkauft_in_neuheiten(db, out_title) + if media_path: + merged = media_path + if recent["media_path"]: + merged = recent["media_path"] + "||" + media_path + db.execute("UPDATE submissions SET media_path=? WHERE id=?", (merged, recent["id"])) + else: + 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', ?, ?, ?)""", + ("telegram", sender_id, sender_name, msg_type, content, media_path, halle, cat, spiel_titel), + ) + if cat == "ausverkauft" and spiel_titel: + mark_ausverkauft_in_neuheiten(db, spiel_titel) + + if not thread_found: + _telegram_autoreply(int(sender_id), sender_name, is_new_user) + + 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() + + 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() + + 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] + if not halle: + threading.Thread(target=auto_process_bg, args=(DB_PATH, sub_id), daemon=True).start() + + db.commit() + except Exception as e: + print("[telegram webhook error] {}".format(e), file=sys.stderr) + return "OK", 200 + + return "OK", 200 + if __name__ == "__main__": import threading def keepalive(): @@ -4150,4 +4419,5 @@ if __name__ == "__main__": 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") + print(f" Telegram: {"configured" if TELEGRAM_TOKEN else "no token"}") app.run(host="127.0.0.1", port=PORT, debug=False) diff --git a/telegram_handler.py b/telegram_handler.py new file mode 100644 index 0000000..e5b9c32 --- /dev/null +++ b/telegram_handler.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- +"""Telegram Bot webhook handler — integrated into app.py""" + +def _telegram_download_file(file_id): + """Download a file from Telegram servers. Returns local path or None.""" + import requests as _r + if not TELEGRAM_TOKEN: + return None + try: + gr = _r.get("{}/getFile?file_id={}".format(TELEGRAM_API, file_id), timeout=10) + gr.raise_for_status() + gdata = gr.json() + if not gdata.get("ok"): + print("[telegram] getFile failed: {}".format(gdata), file=sys.stderr) + return None + file_path = gdata["result"]["file_path"] + ext = os.path.splitext(file_path)[1].lower() or ".bin" + safe_name = "tg_{}{}".format(file_id, ext) + dest = str(MEDIA_DIR / safe_name) + dl_url = "https://api.telegram.org/file/bot{}/{}".format(TELEGRAM_TOKEN, file_path) + fr = _r.get(dl_url, timeout=60) + fr.raise_for_status() + with open(dest, "wb") as f: + f.write(fr.content) + if ext == ".mov": + mp4_path = dest + ".mp4" + import subprocess + result = subprocess.run( + ["/home/hermes/.local/bin/ffmpeg", "-y", "-i", dest, + "-c:v", "libx264", "-preset", "fast", "-crf", "23", + "-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart", mp4_path], + capture_output=True, text=True, timeout=120 + ) + if result.returncode == 0 and os.path.exists(mp4_path): + os.remove(dest) + return mp4_path + return dest + except Exception as e: + print("[telegram download error] {}".format(e), file=sys.stderr) + return None + + +def _telegram_autoreply(chat_id, sender_name, is_new_user): + """Send welcome or thank-you message via Telegram Send API.""" + if not TELEGRAM_TOKEN: + return + import requests as _r + try: + if is_new_user: + text = ( + "🎲 Hey {}! Cool, dass du da bist — willkommen in der ".format(sender_name) + + "brettspiel-news.de Community!\n\n" + "Schick uns einfach, was dich bewegt: 💬 Text, 🎤 Sprachnachrichten, " + "🖼️ Fotos oder 🎬 Videos — alles rund um Brettspiele. Wir schauen uns alles an!\n\n" + "✍️ Nenn uns deinen Vornamen, wenn du magst — das ist deine Einwilligung, " + "dass wir dich als Quelle nennen, falls wir deinen Beitrag verwenden. " + "Ohne Namen schreiben wir „anonyme Quelle\".\n\n" + "🔒 „Meine Daten löschen\" genügt — und alles ist sofort und vollständig weg." + ) + else: + text = "📨 Danke {}! Ist angekommen — wir kümmern uns drum.".format(sender_name) + + _r.post( + "{}/sendMessage".format(TELEGRAM_API), + json={"chat_id": chat_id, "text": text}, + timeout=10, + ) + except Exception as e: + print("[telegram reply error] {}".format(e), file=sys.stderr) + + +@app.route("/telegram/webhook", methods=["GET", "POST"]) +def telegram_webhook(): + """Telegram Bot Webhook. GET=health check, POST=incoming messages.""" + if request.method == "GET": + if TELEGRAM_TOKEN: + import requests as _r + try: + r = _r.get("{}/getMe".format(TELEGRAM_API), timeout=5) + data = r.json() + if data.get("ok"): + return jsonify({"status": "ok", "bot": data["result"]["username"]}) + except Exception: + pass + return jsonify({"status": "ok", "token_configured": True}) + return jsonify({"status": "off", "reason": "TELEGRAM_TOKEN not set"}), 500 + + # POST: Incoming message + body = request.get_json(force=True, silent=True) + if not body or not TELEGRAM_TOKEN: + return "OK", 200 + + try: + msg = body.get("message", {}) + if not msg: + return "OK", 200 + + chat = msg.get("chat", {}) + sender_id = str(chat.get("id", "unknown")) + sender_name = msg.get("from", {}).get("first_name", "") or "" + last_name = msg.get("from", {}).get("last_name", "") + if last_name: + sender_name = "{} {}".format(sender_name, last_name).strip() + if not sender_name: + sender_name = sender_id + + msg_type = "text" + content = "" + media_path = None + file_id = None + + if "text" in msg: + msg_type = "text" + content = msg["text"] + elif "photo" in msg: + msg_type = "image" + content = msg.get("caption", "[Bild ohne Text]") + file_id = msg["photo"][-1]["file_id"] + elif "video" in msg: + msg_type = "video" + content = msg.get("caption", "[🎬 Video — Transkription läuft...]") + file_id = msg["video"]["file_id"] + elif "voice" in msg: + msg_type = "audio" + content = "[🎤 Sprachnachricht — Transkription läuft...]" + file_id = msg["voice"]["file_id"] + elif "audio" in msg: + msg_type = "audio" + content = msg.get("caption", "[🎤 Audio]") + file_id = msg["audio"]["file_id"] + elif "document" in msg: + msg_type = "document" + content = msg.get("caption", "[📎 Dokument]") + file_id = msg["document"]["file_id"] + elif "sticker" in msg: + content = "[Sticker: {}]".format(msg['sticker'].get('emoji', '')) + msg_type = "document" + elif "location" in msg: + lat = msg["location"]["latitude"] + lon = msg["location"]["longitude"] + content = "[📍 Standort: {}, {}]".format(lat, lon) + msg_type = "text" + else: + content = "[Unbekannter Nachrichtentyp]" + msg_type = "text" + + if file_id: + media_path = _telegram_download_file(file_id) + + # GDPR delete trigger + if msg_type == "text" and content.strip().lower() == "meine daten löschen": + _gdpr_delete_sender(get_db(), sender_id) + if TELEGRAM_TOKEN: + import requests as _r + try: + _r.post( + "{}/sendMessage".format(TELEGRAM_API), + json={"chat_id": int(sender_id), + "text": "✅ Alle deine Daten wurden vollständig und endgültig vom Server gelöscht. " + "Es befinden sich keinerlei personenbezogene Daten mehr von dir bei uns."}, + timeout=10, + ) + except Exception: + pass + return "OK", 200 + + db = get_db() + spiel_titel = None + category_override = None + + 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] + + halle = extract_halle(content) + + THREAD_WINDOW_MINUTES = 5 + recent = db.execute( + """SELECT id, content, media_path, halle FROM submissions + WHERE channel='telegram' 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='telegram' AND sender_id=? AND status != 'gdpr_deleted'", + (sender_id,), + ).fetchone()[0] == 0 + + 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: + prefix = { + "text": "", "image": "\n[🖼️ Bild] ", + "audio": "\n[🎤 Sprachnachricht]", "video": "\n[🎬 Video] ", + "document": "\n[📎 Dokument] ", + }.get(msg_type, "\n[{}] ".format(msg_type)) + new_content = (recent["content"] or "") + prefix + 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"]), + ) + if "#out" in content.lower(): + out_title = None + idx2 = content.lower().find("#out") + after2 = content[idx2 + 4:].strip() + if after2: + out_title = re.split(r'[.!?\n]| - | – | — | ist | war | in ', after2, maxsplit=1)[0].strip()[:200] + db.execute("UPDATE submissions SET category=?, spiel_titel=? WHERE id=?", + ("ausverkauft", out_title, recent["id"])) + if out_title: + mark_ausverkauft_in_neuheiten(db, out_title) + if media_path: + merged = media_path + if recent["media_path"]: + merged = recent["media_path"] + "||" + media_path + db.execute("UPDATE submissions SET media_path=? WHERE id=?", (merged, recent["id"])) + else: + 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', ?, ?, ?)""", + ("telegram", sender_id, sender_name, msg_type, content, media_path, halle, cat, spiel_titel), + ) + if cat == "ausverkauft" and spiel_titel: + mark_ausverkauft_in_neuheiten(db, spiel_titel) + + if not thread_found: + _telegram_autoreply(int(sender_id), sender_name, is_new_user) + + 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() + + 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() + + 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] + if not halle: + threading.Thread(target=auto_process_bg, args=(DB_PATH, sub_id), daemon=True).start() + + db.commit() + except Exception as e: + print("[telegram webhook error] {}".format(e), file=sys.stderr) + return "OK", 200 + + return "OK", 200