Files
BSN-Chatsystem/telegram_handler.py
T
Hermes Agent 5181a09c20 feat: Telegram-Bot-Integration — Webhook-Endpoint + Download + Thread-Merge + Auto-Reply
- Neuer Endpoint POST /telegram/webhook (GET=Health-Check)
- Parser für alle Telegram-Nachrichtentypen (Text, Foto, Video, Voice, Audio, Dokument, Sticker, Standort)
- Media-Download via Telegram File API (getFile + Download)
- .mov → .mp4 Konvertierung (ffmpeg)
- 5-Min-Thread-Merge-Logik (gleiches Pattern wie WhatsApp)
- Auto-Reply: Willkommensnachricht für neue Nutzer, Danke für bestehende
- GDPR-Trigger 'Meine Daten löschen' integriert
- #out-Erkennung + ausverkauft-Markierung
- Hintergrund-Transkription + LLM-Auto-Analyse
- TELEGRAM_TOKEN via .env konfigurierbar
2026-06-18 18:38:09 +02:00

265 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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