613 lines
22 KiB
Python
613 lines
22 KiB
Python
"""Orbitalo Hausmeister — Telegram Bot für Homelab-Management."""
|
|
|
|
import asyncio
|
|
import logging
|
|
import sys
|
|
import os
|
|
import fcntl
|
|
import atexit
|
|
import signal
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
|
|
PIDFILE = "/tmp/hausmeister-bot.pid"
|
|
_lock_fp = None
|
|
|
|
|
|
def _acquire_lock():
|
|
"""Stellt sicher, dass nur eine Bot-Instanz läuft (PID-File + flock)."""
|
|
global _lock_fp
|
|
_lock_fp = open(PIDFILE, "w")
|
|
try:
|
|
fcntl.flock(_lock_fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except OSError:
|
|
print(f"ABBRUCH: Bot läuft bereits (PID-File {PIDFILE} ist gelockt)", file=sys.stderr)
|
|
sys.exit(1)
|
|
_lock_fp.write(str(os.getpid()))
|
|
_lock_fp.flush()
|
|
|
|
|
|
def _release_lock():
|
|
global _lock_fp
|
|
if _lock_fp:
|
|
try:
|
|
fcntl.flock(_lock_fp, fcntl.LOCK_UN)
|
|
_lock_fp.close()
|
|
os.unlink(PIDFILE)
|
|
except OSError:
|
|
pass
|
|
|
|
from telegram import BotCommand, Update, ReplyKeyboardMarkup, KeyboardButton
|
|
from telegram.ext import (
|
|
Application, CommandHandler, MessageHandler, filters, ContextTypes,
|
|
)
|
|
|
|
BOT_COMMANDS = [
|
|
BotCommand("status", "Alle Container"),
|
|
BotCommand("errors", "Aktuelle Fehler"),
|
|
BotCommand("ct", "Container-Detail (/ct 109)"),
|
|
BotCommand("health", "Health-Check (/health wordpress)"),
|
|
BotCommand("logs", "Letzte Logs (/logs rss-manager)"),
|
|
BotCommand("silence", "Stille Hosts"),
|
|
BotCommand("report", "Tagesbericht"),
|
|
BotCommand("check", "Monitoring-Check"),
|
|
BotCommand("feeds", "Feed-Status & Artikel heute"),
|
|
BotCommand("memory", "Gedaechtnis anzeigen"),
|
|
BotCommand("start", "Hilfe anzeigen"),
|
|
]
|
|
|
|
|
|
KEYBOARD = ReplyKeyboardMarkup(
|
|
[
|
|
[KeyboardButton("📊 Status"), KeyboardButton("❌ Fehler"), KeyboardButton("📰 Feeds")],
|
|
[KeyboardButton("📋 Report"), KeyboardButton("🔧 Check"), KeyboardButton("🔇 Stille")],
|
|
],
|
|
resize_keyboard=True,
|
|
is_persistent=True,
|
|
)
|
|
|
|
BUTTON_MAP = {
|
|
"📊 Status": "status",
|
|
"❌ Fehler": "errors",
|
|
"📰 Feeds": "feeds",
|
|
"📋 Report": "report",
|
|
"🔧 Check": "check",
|
|
"🔇 Stille": "silence",
|
|
}
|
|
|
|
import context
|
|
import requests as _req
|
|
import llm
|
|
import memory_client
|
|
import monitor
|
|
import voice
|
|
from core import config
|
|
|
|
logging.basicConfig(
|
|
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
|
level=logging.INFO,
|
|
)
|
|
log = logging.getLogger("hausmeister")
|
|
|
|
ALLOWED_CHAT_IDS: set[int] = set()
|
|
|
|
|
|
def _load_token_and_chat():
|
|
cfg = config.parse_config()
|
|
token = cfg.raw.get("TG_HAUSMEISTER_TOKEN", "")
|
|
chat_id = cfg.raw.get("TG_CHAT_ID", "")
|
|
if chat_id:
|
|
ALLOWED_CHAT_IDS.add(int(chat_id))
|
|
return token
|
|
|
|
|
|
def _authorized(update: Update) -> bool:
|
|
if not ALLOWED_CHAT_IDS:
|
|
return True
|
|
return update.effective_chat.id in ALLOWED_CHAT_IDS
|
|
|
|
|
|
async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
if not _authorized(update):
|
|
return
|
|
await update.message.reply_text(
|
|
"🔧 Orbitalo Hausmeister-Bot\n\n"
|
|
"Befehle:\n"
|
|
"/status — Alle Container\n"
|
|
"/errors — Aktuelle Fehler\n"
|
|
"/ct <nr> — Container-Detail\n"
|
|
"/health <name> — Health-Check\n"
|
|
"/logs <name> — Letzte Logs\n"
|
|
"/silence — Stille Hosts\n"
|
|
"/report — Tagesbericht\n"
|
|
"/check — Monitoring-Check\n"
|
|
"/feeds — Feed-Status & Artikel\n"
|
|
"/memory — Gedaechtnis anzeigen\n\n"
|
|
"📷 Foto senden = Bilderkennung\n\n"
|
|
"Oder einfach eine Frage stellen!",
|
|
reply_markup=KEYBOARD,
|
|
)
|
|
|
|
|
|
async def cmd_status(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
if not _authorized(update):
|
|
return
|
|
await update.message.reply_text("⏳ Lade Container-Status...")
|
|
try:
|
|
text = context.gather_status()
|
|
if len(text) > 4000:
|
|
text = text[:4000] + "\n..."
|
|
await update.message.reply_text(text)
|
|
except Exception as e:
|
|
await update.message.reply_text(f"Fehler: {e}")
|
|
|
|
|
|
async def cmd_errors(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
if not _authorized(update):
|
|
return
|
|
await update.message.reply_text("⏳ Suche Fehler...")
|
|
try:
|
|
text = context.gather_errors(hours=2)
|
|
await update.message.reply_text(text[:4000])
|
|
except Exception as e:
|
|
await update.message.reply_text(f"Fehler: {e}")
|
|
|
|
|
|
async def cmd_ct(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
if not _authorized(update):
|
|
return
|
|
args = ctx.args
|
|
if not args:
|
|
await update.message.reply_text("Bitte CT-Nummer angeben: /ct 109")
|
|
return
|
|
try:
|
|
text = context.gather_container_status(args[0])
|
|
await update.message.reply_text(text)
|
|
except Exception as e:
|
|
await update.message.reply_text(f"Fehler: {e}")
|
|
|
|
|
|
async def cmd_health(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
if not _authorized(update):
|
|
return
|
|
args = ctx.args
|
|
if not args:
|
|
await update.message.reply_text("Bitte Hostname angeben: /health wordpress")
|
|
return
|
|
try:
|
|
text = context.gather_health(args[0])
|
|
await update.message.reply_text(text)
|
|
except Exception as e:
|
|
await update.message.reply_text(f"Fehler: {e}")
|
|
|
|
|
|
async def cmd_logs(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
if not _authorized(update):
|
|
return
|
|
args = ctx.args
|
|
if not args:
|
|
await update.message.reply_text("Bitte Hostname angeben: /logs rss-manager")
|
|
return
|
|
try:
|
|
text = context.gather_logs(args[0])
|
|
await update.message.reply_text(text[:4000])
|
|
except Exception as e:
|
|
await update.message.reply_text(f"Fehler: {e}")
|
|
|
|
|
|
async def cmd_silence(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
if not _authorized(update):
|
|
return
|
|
try:
|
|
text = context.gather_silence()
|
|
await update.message.reply_text(text)
|
|
except Exception as e:
|
|
await update.message.reply_text(f"Fehler: {e}")
|
|
|
|
|
|
async def cmd_report(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
if not _authorized(update):
|
|
return
|
|
await update.message.reply_text("⏳ Erstelle Tagesbericht...")
|
|
try:
|
|
text = monitor.format_report()
|
|
await update.message.reply_text(text[:4000])
|
|
except Exception as e:
|
|
await update.message.reply_text(f"Fehler: {e}")
|
|
|
|
|
|
async def cmd_check(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
if not _authorized(update):
|
|
return
|
|
await update.message.reply_text("⏳ Prüfe Systeme...")
|
|
try:
|
|
alerts = monitor.check_all()
|
|
if alerts:
|
|
text = f"⚠️ {len(alerts)} Alarme:\n\n" + "\n".join(alerts)
|
|
else:
|
|
text = "✅ Keine Alarme — alles läuft."
|
|
await update.message.reply_text(text)
|
|
except Exception as e:
|
|
await update.message.reply_text(f"Fehler: {e}")
|
|
|
|
|
|
|
|
|
|
def _get_feed_stats():
|
|
"""Holt Feed-Statistiken von der RSS Manager API."""
|
|
cfg = config.parse_config()
|
|
ct_109 = config.get_container(cfg, vmid=109)
|
|
url = f"http://{ct_109.tailscale_ip}:8080/api/feed-stats" if ct_109 else None
|
|
if not url:
|
|
return None
|
|
try:
|
|
r = _req.get(url, timeout=10)
|
|
return r.json() if r.ok else None
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def format_feed_report(stats: dict) -> str:
|
|
"""Formatiert Feed-Statistiken für Telegram."""
|
|
today = stats["today"]
|
|
yesterday = stats["yesterday"]
|
|
feeds = stats["feeds"]
|
|
lines = [f"📊 Feed-Report ({stats['date']})", f"Artikel heute: {today} (gestern: {yesterday})", ""]
|
|
active = [f for f in feeds if f["posts_today"] > 0]
|
|
if active:
|
|
lines.append("📰 Aktive Feeds:")
|
|
for f in active:
|
|
lines.append(f" {f['name']}: {f['posts_today']} ({f['schedule']})")
|
|
silent = [f for f in feeds if f["posts_today"] == 0]
|
|
if silent:
|
|
lines.append("")
|
|
lines.append("😴 Keine Artikel heute:")
|
|
for f in silent:
|
|
yd = f"(gestern: {f['posts_yesterday']})" if f["posts_yesterday"] > 0 else "(auch gestern 0)"
|
|
lines.append(f" {f['name']} {yd}")
|
|
errors = [f for f in feeds if f["error_count"] and f["error_count"] > 0]
|
|
if errors:
|
|
lines.append("")
|
|
lines.append("⚠️ Fehler:")
|
|
for f in errors:
|
|
lines.append(f" {f['name']}: {f['error_count']}x — {f['last_error']}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
async def cmd_feeds(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
if not _authorized(update):
|
|
return
|
|
await update.message.reply_text("⏳ Lade Feed-Status...")
|
|
try:
|
|
stats = _get_feed_stats()
|
|
if not stats:
|
|
await update.message.reply_text("RSS Manager nicht erreichbar.")
|
|
return
|
|
text = format_feed_report(stats)
|
|
await update.message.reply_text(text[:4000])
|
|
except Exception as e:
|
|
await update.message.reply_text(f"Fehler: {e}")
|
|
|
|
_STOP_WORDS = {"ich", "bin", "ist", "der", "die", "das", "ein", "eine", "und", "oder",
|
|
"in", "auf", "an", "fuer", "für", "von", "zu", "mit", "nach", "mein",
|
|
"meine", "meinem", "meinen", "hat", "habe", "wird", "will", "soll",
|
|
"nicht", "auch", "noch", "schon", "nur", "aber", "wenn", "weil", "dass"}
|
|
|
|
|
|
def _find_matching_item(user_text: str, items: list[dict]) -> dict | None:
|
|
"""Findet das Item mit der besten Wort-Ueberlappung zum User-Text."""
|
|
words = {w.lower().strip(".,!?") for w in user_text.split() if len(w) > 2}
|
|
words -= _STOP_WORDS
|
|
if not words:
|
|
return None
|
|
|
|
best, best_score = None, 0
|
|
for c in items:
|
|
content_words = {w.lower().strip(".,!?") for w in c["content"].split() if len(w) > 2}
|
|
content_words -= _STOP_WORDS
|
|
if not content_words:
|
|
continue
|
|
overlap = len(words & content_words)
|
|
score = overlap / max(len(words), 1)
|
|
if score > best_score and score >= 0.3:
|
|
best, best_score = c, score
|
|
return best
|
|
|
|
|
|
|
|
|
|
async def cmd_memory(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
if not _authorized(update):
|
|
return
|
|
items = memory_client.get_active_memory()
|
|
if not items:
|
|
await update.message.reply_text("Kein Gedaechtnis vorhanden.")
|
|
return
|
|
TYPE_ICON = {"fact": "📌", "preference": "⭐", "relationship": "👤", "plan": "📅", "temporary": "🕒", "uncertain": "❓"}
|
|
CONF_ICON = {"high": "", "medium": " ⚠", "low": " ❔"}
|
|
groups = {}
|
|
for i in items:
|
|
mt = i.get("memory_type", "fact")
|
|
groups.setdefault(mt, []).append(i)
|
|
lines = [f"🧠 Gedaechtnis: {len(items)} Eintraege\n"]
|
|
for mtype in ("fact", "preference", "relationship", "plan", "temporary", "uncertain"):
|
|
group = groups.get(mtype, [])
|
|
if not group:
|
|
continue
|
|
icon = TYPE_ICON.get(mtype, "•")
|
|
lines.append(f"{icon} {mtype.upper()} ({len(group)}):")
|
|
for i in group:
|
|
conf = CONF_ICON.get(i.get("confidence", "high"), "")
|
|
src = i.get("source_type", "")
|
|
src_tag = f" [{src}]" if src and src != "manual" else ""
|
|
exp = i.get("expires_at")
|
|
exp_str = ""
|
|
if exp:
|
|
from datetime import datetime
|
|
exp_str = f" (bis {datetime.fromtimestamp(exp).strftime('%d.%m.%Y')})"
|
|
lines.append(f" • {i['content'][:90]}{conf}{exp_str}{src_tag}")
|
|
lines.append("")
|
|
text = "\n".join(lines)
|
|
await update.message.reply_text(text[:4000], reply_markup=KEYBOARD)
|
|
|
|
|
|
|
|
|
|
async def handle_voice(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
"""Sprachnachricht: Whisper STT -> LLM -> TTS Antwort als Text + Sprache."""
|
|
if not _authorized(update):
|
|
return
|
|
voice_msg = update.message.voice
|
|
if not voice_msg:
|
|
return
|
|
|
|
await update.message.reply_text("🎙 Höre zu...")
|
|
try:
|
|
tg_file = await ctx.bot.get_file(voice_msg.file_id)
|
|
audio_data = await tg_file.download_as_bytearray()
|
|
|
|
text = voice.transcribe(bytes(audio_data))
|
|
if not text:
|
|
await update.message.reply_text("Konnte die Nachricht nicht verstehen.")
|
|
return
|
|
|
|
log.info("Voice transkribiert: %s", text[:100])
|
|
await update.message.reply_text(f"🗣 \"{text}\"\n\n🤔 Denke nach...")
|
|
|
|
channel_key = str(update.effective_chat.id)
|
|
session_id = memory_client.get_or_create_session(channel_key, source="telegram")
|
|
|
|
context.last_suggest_result = {"type": None}
|
|
context.set_source_type("telegram_voice")
|
|
handlers = context.get_tool_handlers(session_id=session_id)
|
|
answer = llm.ask_with_tools(text, handlers, session_id=session_id)
|
|
if session_id:
|
|
memory_client.log_message(session_id, "user", text)
|
|
memory_client.log_message(session_id, "assistant", answer)
|
|
|
|
await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD)
|
|
|
|
audio_out = voice.synthesize(answer[:4000])
|
|
if audio_out:
|
|
import io as _io
|
|
await update.message.reply_voice(voice=_io.BytesIO(audio_out))
|
|
else:
|
|
log.warning("TTS fehlgeschlagen — nur Text gesendet")
|
|
except Exception as e:
|
|
log.exception("Fehler bei Voice-Nachricht")
|
|
await update.message.reply_text(f"Fehler: {e}")
|
|
|
|
|
|
async def handle_photo(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
"""Foto-Nachricht: Bild analysieren via Vision-LLM."""
|
|
if not _authorized(update):
|
|
return
|
|
photos = update.message.photo
|
|
if not photos:
|
|
return
|
|
|
|
photo = photos[-1]
|
|
caption = update.message.caption or ""
|
|
|
|
await update.message.reply_text("🔍 Analysiere Bild...")
|
|
try:
|
|
import base64
|
|
tg_file = await ctx.bot.get_file(photo.file_id)
|
|
image_data = await tg_file.download_as_bytearray()
|
|
image_base64 = base64.b64encode(bytes(image_data)).decode("utf-8")
|
|
|
|
channel_key = str(update.effective_chat.id)
|
|
session_id = memory_client.get_or_create_session(channel_key, source="telegram")
|
|
|
|
context.last_suggest_result = {"type": None}
|
|
context.set_source_type("telegram_photo")
|
|
handlers = context.get_tool_handlers(session_id=session_id)
|
|
answer = llm.ask_with_image(image_base64, caption, handlers, session_id=session_id)
|
|
|
|
if session_id:
|
|
user_msg = f"[Foto] {caption}" if caption else "[Foto gesendet]"
|
|
memory_client.log_message(session_id, "user", user_msg)
|
|
memory_client.log_message(session_id, "assistant", answer)
|
|
|
|
await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD)
|
|
except Exception as e:
|
|
log.exception("Fehler bei Foto-Analyse")
|
|
await update.message.reply_text(f"Fehler bei Bildanalyse: {e}")
|
|
|
|
|
|
def _extract_pdf_text(pdf_bytes: bytes) -> str:
|
|
"""Extrahiert Text aus PDF via PyPDF2. Gibt leeren String zurueck wenn kein Text."""
|
|
try:
|
|
import io as _io
|
|
from PyPDF2 import PdfReader
|
|
reader = PdfReader(_io.BytesIO(pdf_bytes))
|
|
pages = []
|
|
for i, page in enumerate(reader.pages[:10]):
|
|
text = page.extract_text()
|
|
if text and text.strip():
|
|
pages.append(f"--- Seite {i+1} ---\n{text.strip()}")
|
|
return "\n\n".join(pages)
|
|
except Exception as e:
|
|
log.warning("PDF-Extraktion fehlgeschlagen: %s", e)
|
|
return ""
|
|
|
|
|
|
async def handle_document(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
"""Dokument-Nachricht: Bilder und PDFs analysieren."""
|
|
if not _authorized(update):
|
|
return
|
|
doc = update.message.document
|
|
if not doc:
|
|
return
|
|
|
|
mime = doc.mime_type or ""
|
|
caption = update.message.caption or ""
|
|
channel_key = str(update.effective_chat.id)
|
|
session_id = memory_client.get_or_create_session(channel_key, source="telegram")
|
|
|
|
if mime.startswith("image/"):
|
|
await update.message.reply_text("🔍 Analysiere Bild...")
|
|
try:
|
|
import base64
|
|
tg_file = await ctx.bot.get_file(doc.file_id)
|
|
image_data = await tg_file.download_as_bytearray()
|
|
image_base64 = base64.b64encode(bytes(image_data)).decode("utf-8")
|
|
|
|
context.last_suggest_result = {"type": None}
|
|
context.set_source_type("telegram_photo")
|
|
handlers = context.get_tool_handlers(session_id=session_id)
|
|
answer = llm.ask_with_image(image_base64, caption, handlers, session_id=session_id)
|
|
|
|
if session_id:
|
|
user_msg = f"[Bild-Datei] {caption}" if caption else "[Bild-Datei gesendet]"
|
|
memory_client.log_message(session_id, "user", user_msg)
|
|
memory_client.log_message(session_id, "assistant", answer)
|
|
|
|
await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD)
|
|
except Exception as e:
|
|
log.exception("Fehler bei Bild-Dokument")
|
|
await update.message.reply_text(f"Fehler bei Bildanalyse: {e}")
|
|
|
|
elif mime == "application/pdf":
|
|
await update.message.reply_text("📄 Lese PDF...")
|
|
try:
|
|
tg_file = await ctx.bot.get_file(doc.file_id)
|
|
pdf_data = await tg_file.download_as_bytearray()
|
|
pdf_text = _extract_pdf_text(bytes(pdf_data))
|
|
|
|
if not pdf_text:
|
|
await update.message.reply_text(
|
|
"PDF enthält keinen extrahierbaren Text (evtl. gescannt/Bild-PDF).\n"
|
|
"Tipp: Sende einen Screenshot des PDFs als Foto — dann kann ich es per Bilderkennung lesen."
|
|
)
|
|
return
|
|
|
|
question = caption if caption else "Analysiere dieses Dokument. Was sind die wichtigsten Informationen?"
|
|
full_prompt = f"{question}\n\n--- PDF-INHALT ---\n{pdf_text[:6000]}"
|
|
|
|
context.last_suggest_result = {"type": None}
|
|
context.set_source_type("telegram_pdf")
|
|
handlers = context.get_tool_handlers(session_id=session_id)
|
|
answer = llm.ask_with_tools(full_prompt, handlers, session_id=session_id)
|
|
|
|
if session_id:
|
|
user_msg = f"[PDF: {doc.file_name or 'dokument.pdf'}] {caption}" if caption else f"[PDF: {doc.file_name or 'dokument.pdf'}]"
|
|
memory_client.log_message(session_id, "user", user_msg)
|
|
memory_client.log_message(session_id, "assistant", answer)
|
|
|
|
await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD)
|
|
except Exception as e:
|
|
log.exception("Fehler bei PDF-Analyse")
|
|
await update.message.reply_text(f"Fehler bei PDF: {e}")
|
|
|
|
else:
|
|
await update.message.reply_text(
|
|
f"Dateityp '{mime}' wird nicht unterstuetzt.\n"
|
|
"Unterstuetzt: Bilder (JPG/PNG) und PDFs."
|
|
)
|
|
|
|
|
|
async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
"""Button-Presses und Freitext-Fragen verarbeiten."""
|
|
if not _authorized(update):
|
|
return
|
|
text = update.message.text
|
|
if not text:
|
|
return
|
|
|
|
cmd = BUTTON_MAP.get(text)
|
|
if cmd == "status":
|
|
return await cmd_status(update, ctx)
|
|
elif cmd == "errors":
|
|
return await cmd_errors(update, ctx)
|
|
elif cmd == "feeds":
|
|
return await cmd_feeds(update, ctx)
|
|
elif cmd == "report":
|
|
return await cmd_report(update, ctx)
|
|
elif cmd == "check":
|
|
return await cmd_check(update, ctx)
|
|
elif cmd == "silence":
|
|
return await cmd_silence(update, ctx)
|
|
|
|
channel_key = str(update.effective_chat.id)
|
|
session_id = memory_client.get_or_create_session(channel_key, source="telegram")
|
|
|
|
await update.message.reply_text("🤔 Denke nach...")
|
|
try:
|
|
context.last_suggest_result = {"type": None}
|
|
context.set_source_type("telegram_text")
|
|
handlers = context.get_tool_handlers(session_id=session_id)
|
|
answer = llm.ask_with_tools(text, handlers, session_id=session_id)
|
|
if session_id:
|
|
memory_client.log_message(session_id, "user", text)
|
|
memory_client.log_message(session_id, "assistant", answer)
|
|
|
|
suggest = context.last_suggest_result
|
|
log.info("suggest_result: type=%s", suggest.get("type"))
|
|
|
|
await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD)
|
|
except Exception as e:
|
|
log.exception("Fehler bei Freitext")
|
|
await update.message.reply_text(f"Fehler: {e}")
|
|
|
|
|
|
def main():
|
|
token = _load_token_and_chat()
|
|
if not token:
|
|
log.error("TG_HAUSMEISTER_TOKEN fehlt in homelab.conf!")
|
|
sys.exit(1)
|
|
|
|
_acquire_lock()
|
|
atexit.register(_release_lock)
|
|
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
|
|
|
log.info("Starte Orbitalo Hausmeister-Bot...")
|
|
app = Application.builder().token(token).build()
|
|
|
|
app.add_handler(CommandHandler("start", cmd_start))
|
|
app.add_handler(CommandHandler("status", cmd_status))
|
|
app.add_handler(CommandHandler("errors", cmd_errors))
|
|
app.add_handler(CommandHandler("ct", cmd_ct))
|
|
app.add_handler(CommandHandler("health", cmd_health))
|
|
app.add_handler(CommandHandler("logs", cmd_logs))
|
|
app.add_handler(CommandHandler("silence", cmd_silence))
|
|
app.add_handler(CommandHandler("report", cmd_report))
|
|
app.add_handler(CommandHandler("check", cmd_check))
|
|
app.add_handler(CommandHandler("feeds", cmd_feeds))
|
|
app.add_handler(CommandHandler("memory", cmd_memory))
|
|
app.add_handler(MessageHandler(filters.VOICE, handle_voice))
|
|
app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
|
|
app.add_handler(MessageHandler(filters.Document.ALL, handle_document))
|
|
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
|
|
|
async def post_init(application):
|
|
await application.bot.set_my_commands(BOT_COMMANDS)
|
|
log.info("Kommandomenü registriert")
|
|
|
|
app.post_init = post_init
|
|
log.info("Bot läuft — polling gestartet")
|
|
app.run_polling(allowed_updates=Update.ALL_TYPES)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|