Wiki-Quellcode von code v3
Zuletzt geändert von René Schmidt am 2025/10/12 08:42
Verstecke letzte Bearbeiter
| author | version | line-number | content |
|---|---|---|---|
![]() |
1.1 | 1 | import asyncio |
| 2 | import logging | ||
| 3 | import json | ||
| 4 | import random | ||
| 5 | import time | ||
| 6 | import base64 | ||
| 7 | import io | ||
| 8 | from datetime import datetime | ||
| 9 | from pathlib import Path | ||
| 10 | from aiohttp import web, ClientSession | ||
| 11 | from nio import AsyncClient, MatrixRoom, RoomMessageText, RoomMessageImage, RoomMessageAudio, RoomMessageFile, DownloadError | ||
| 12 | |||
| 13 | # Für Dokumenten-Verarbeitung | ||
| 14 | try: | ||
| 15 | import PyPDF2 | ||
| 16 | import docx | ||
| 17 | import pandas as pd | ||
| 18 | from PIL import Image | ||
| 19 | PDF_SUPPORT = True | ||
| 20 | except ImportError: | ||
| 21 | PDF_SUPPORT = False | ||
| 22 | logging.warning("⚠️ PDF/DOCX Support nicht installiert. Installiere: pip install PyPDF2 python-docx pandas pillow") | ||
| 23 | |||
| 24 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 25 | # Logging-Setup | ||
| 26 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 27 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") | ||
| 28 | logger = logging.getLogger(~_~_name~_~_) | ||
| 29 | |||
| 30 | # Reduziere Noise | ||
| 31 | logging.getLogger("nio").setLevel(logging.ERROR) | ||
| 32 | logging.getLogger("aiohttp").setLevel(logging.WARNING) | ||
| 33 | |||
| 34 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 35 | # Konfiguration - HIER ANPASSEN! | ||
| 36 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 37 | HOMESERVER = "https:~/~/matrix.rs-servertech.com" | ||
| 38 | USER_ID = "@terry:matrix.rs-servertech.com" | ||
| 39 | ACCESS_TOKEN = "HIER_DEIN_TOKEN" # ← Dein Access Token | ||
| 40 | DEVICE_ID = "MATRIXBOT" | ||
| 41 | |||
| 42 | # n8n Konfiguration | ||
| 43 | N8N_WEBHOOK_URL = "https:~/~/n8n.rs-servertech.com/webhook/matrix-bot" | ||
| 44 | WEBHOOK_PORT = 8080 | ||
| 45 | WEBHOOK_SECRET = "mein-sicherer-key-2024-xyz" | ||
| 46 | DEFAULT_ROOM_ID = "!dYNoYyZHeJfkqfsSFH:matrix.rs-servertech.com" | ||
| 47 | |||
| 48 | # KI Konfiguration - Ollama | ||
| 49 | OLLAMA_URL = "http:~/~/192.168.10.210:11434" | ||
| 50 | OLLAMA_MODEL = "llama3.2" # Für Text | ||
| 51 | OLLAMA_VISION_MODEL = "llama3.2-vision" # Für Bilder | ||
| 52 | OLLAMA_WHISPER_MODEL = "whisper" # Für Sprache (falls installiert) | ||
| 53 | AI_ENABLED = True | ||
| 54 | |||
| 55 | # Bot-Verhalten | ||
| 56 | BOT_PREFIX = "!" | ||
| 57 | RESPOND_TO_ALL = False | ||
| 58 | RESPOND_TO_MENTIONS = True | ||
| 59 | RESPOND_TO_DM = False | ||
| 60 | |||
| 61 | # Feature-Flags | ||
| 62 | ENABLE_IMAGE_ANALYSIS = True | ||
| 63 | ENABLE_VOICE_TRANSCRIPTION = True | ||
| 64 | ENABLE_DOCUMENT_ANALYSIS = True | ||
| 65 | |||
| 66 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 67 | # Globale Variablen | ||
| 68 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 69 | client: AsyncClient = None | ||
| 70 | app = web.Application() | ||
| 71 | conversation_history = {} | ||
| 72 | active_conversations = {} | ||
| 73 | |||
| 74 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 75 | # Hilfsfunktionen für Datei-Download | ||
| 76 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 77 | async def download_file(mxc_url: str) -> bytes: | ||
| 78 | """Lädt eine Datei von Matrix herunter.""" | ||
| 79 | try: | ||
| 80 | response = await client.download(mxc_url) | ||
| 81 | if isinstance(response, DownloadError): | ||
| 82 | logger.error(f"Download-Fehler: {response}") | ||
| 83 | return None | ||
| 84 | return response.body | ||
| 85 | except Exception as e: | ||
| 86 | logger.error(f"Fehler beim Download: {e}") | ||
| 87 | return None | ||
| 88 | |||
| 89 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 90 | # Bild-Analyse mit Ollama Vision | ||
| 91 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 92 | async def analyze_image(image_data: bytes, prompt: str = "Beschreibe dieses Bild detailliert auf Deutsch.") -> str: | ||
| 93 | """Analysiert ein Bild mit Ollama Vision.""" | ||
| 94 | if not ENABLE_IMAGE_ANALYSIS: | ||
| 95 | return None | ||
| 96 | |||
| 97 | try: | ||
| 98 | # Konvertiere zu Base64 | ||
| 99 | image_b64 = base64.b64encode(image_data).decode('utf-8') | ||
| 100 | |||
| 101 | payload = { | ||
| 102 | "model": OLLAMA_VISION_MODEL, | ||
| 103 | "prompt": prompt, | ||
| 104 | "images": [image_b64], | ||
| 105 | "stream": False | ||
| 106 | } | ||
| 107 | |||
| 108 | async with ClientSession() as session: | ||
| 109 | async with session.post( | ||
| 110 | f"{OLLAMA_URL}/api/generate", | ||
| 111 | json=payload, | ||
| 112 | timeout=120 | ||
| 113 | ) as resp: | ||
| 114 | if resp.status == 200: | ||
| 115 | data = await resp.json() | ||
| 116 | return data.get("response", "Keine Antwort") | ||
| 117 | else: | ||
| 118 | logger.error(f"Ollama Vision Fehler: {resp.status}") | ||
| 119 | return None | ||
| 120 | except Exception as e: | ||
| 121 | logger.error(f"Bild-Analyse Fehler: {e}") | ||
| 122 | return None | ||
| 123 | |||
| 124 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 125 | # Sprach-Transkription mit Whisper | ||
| 126 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 127 | async def transcribe_audio(audio_data: bytes) -> str: | ||
| 128 | """Transkribiert Audio mit Whisper (lokal oder API).""" | ||
| 129 | if not ENABLE_VOICE_TRANSCRIPTION: | ||
| 130 | return None | ||
| 131 | |||
| 132 | try: | ||
| 133 | # Speichere temporär | ||
| 134 | temp_file = f"/tmp/audio_{int(time.time())}.ogg" | ||
| 135 | with open(temp_file, "wb") as f: | ||
| 136 | f.write(audio_data) | ||
| 137 | |||
| 138 | # Option 1: Lokales Whisper via Ollama (wenn installiert) | ||
| 139 | try: | ||
| 140 | async with ClientSession() as session: | ||
| 141 | with open(temp_file, 'rb') as f: | ||
| 142 | form = aiohttp.FormData() | ||
| 143 | form.add_field('file', f, filename='audio.ogg') | ||
| 144 | form.add_field('model', OLLAMA_WHISPER_MODEL) | ||
| 145 | |||
| 146 | async with session.post( | ||
| 147 | f"{OLLAMA_URL}/api/transcribe", | ||
| 148 | data=form, | ||
| 149 | timeout=120 | ||
| 150 | ) as resp: | ||
| 151 | if resp.status == 200: | ||
| 152 | data = await resp.json() | ||
| 153 | Path(temp_file).unlink(missing_ok=True) | ||
| 154 | return data.get("text", "Keine Transkription") | ||
| 155 | except Exception as e: | ||
| 156 | logger.warning(f"Lokales Whisper nicht verfügbar: {e}") | ||
| 157 | |||
| 158 | # Option 2: Whisper CLI (falls installiert) | ||
| 159 | import subprocess | ||
| 160 | try: | ||
| 161 | result = subprocess.run( | ||
| 162 | ["whisper", temp_file, "~-~-language", "de", "~-~-model", "base", "~-~-output_format", "txt"], | ||
| 163 | capture_output=True, | ||
| 164 | text=True, | ||
| 165 | timeout=120 | ||
| 166 | ) | ||
| 167 | Path(temp_file).unlink(missing_ok=True) | ||
| 168 | return result.stdout.strip() if result.returncode == 0 else None | ||
| 169 | except Exception as e: | ||
| 170 | logger.warning(f"Whisper CLI nicht verfügbar: {e}") | ||
| 171 | |||
| 172 | Path(temp_file).unlink(missing_ok=True) | ||
| 173 | return "⚠️ Whisper ist nicht installiert. Installiere mit: pip install openai-whisper" | ||
| 174 | |||
| 175 | except Exception as e: | ||
| 176 | logger.error(f"Audio-Transkription Fehler: {e}") | ||
| 177 | return None | ||
| 178 | |||
| 179 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 180 | # Dokumenten-Analyse | ||
| 181 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 182 | async def analyze_document(file_data: bytes, filename: str) -> str: | ||
| 183 | """Analysiert verschiedene Dokumenttypen.""" | ||
| 184 | if not ENABLE_DOCUMENT_ANALYSIS: | ||
| 185 | return None | ||
| 186 | |||
| 187 | try: | ||
| 188 | file_ext = Path(filename).suffix.lower() | ||
| 189 | |||
| 190 | |||
| 191 | if file_ext == ".pdf" and PDF_SUPPORT: | ||
| 192 | pdf_reader = PyPDF2.PdfReader(io.BytesIO(file_data)) | ||
| 193 | text = "" | ||
| 194 | for page in pdf_reader.pages[:10]: # Erste 10 Seiten | ||
| 195 | text += page.extract_text() + "\n" | ||
| 196 | |||
| 197 | if len(text) > 3000: | ||
| 198 | text = text[:3000] + "...\n\n(Text gekürzt)" | ||
| 199 | |||
| 200 | return f"📄 ~*~*PDF Inhalt:~*~*\n\n{text}\n\n_(Erste Seiten)_" | ||
| 201 | |||
| 202 | # Word | ||
| 203 | elif file_ext in [".docx", ".doc"] and PDF_SUPPORT: | ||
| 204 | doc = docx.Document(io.BytesIO(file_data)) | ||
| 205 | text = "\n".join([p.text for p in doc.paragraphs[:50]]) | ||
| 206 | |||
| 207 | if len(text) > 3000: | ||
| 208 | text = text[:3000] + "..." | ||
| 209 | |||
| 210 | return f"📝 ~*~*Word Dokument:~*~*\n\n{text}" | ||
| 211 | |||
| 212 | # Text | ||
| 213 | elif file_ext in [".txt", ".md", ".log"]: | ||
| 214 | text = file_data.decode('utf-8', errors='ignore') | ||
| 215 | if len(text) > 3000: | ||
| 216 | text = text[:3000] + "..." | ||
| 217 | return f"📃 ~*~*Text-Datei:~*~*\n\n```\n{text}\n```" | ||
| 218 | |||
| 219 | # CSV/Excel | ||
| 220 | elif file_ext in [".csv", ".xlsx", ".xls"] and PDF_SUPPORT: | ||
| 221 | if file_ext == ".csv": | ||
| 222 | df = pd.read_csv(io.BytesIO(file_data)) | ||
| 223 | else: | ||
| 224 | df = pd.read_excel(io.BytesIO(file_data)) | ||
| 225 | |||
| 226 | info = f"📊 ~*~*Tabelle:~*~* {len(df)} Zeilen, {len(df.columns)} Spalten\n\n" | ||
| 227 | info += f"~*~*Spalten:~*~* {', '.join(df.columns.tolist())}\n\n" | ||
| 228 | info += f"~*~*Erste Zeilen:~*~*\n```\n{df.head(5).to_string()}\n```" | ||
| 229 | |||
| 230 | return info | ||
| 231 | |||
| 232 | # JSON | ||
| 233 | elif file_ext == ".json": | ||
| 234 | data = json.loads(file_data.decode('utf-8')) | ||
| 235 | text = json.dumps(data, indent=2, ensure_ascii=False) | ||
| 236 | if len(text) > 2000: | ||
| 237 | text = text[:2000] + "..." | ||
| 238 | return f"📋 ~*~*JSON:~*~*\n\n```json\n{text}\n```" | ||
| 239 | |||
| 240 | else: | ||
| 241 | return f"ℹ️ Dateiformat `.{file_ext}` wird noch nicht unterstützt.\n\nUnterstützt: PDF, DOCX, TXT, CSV, XLSX, JSON" | ||
| 242 | |||
| 243 | except Exception as e: | ||
| 244 | logger.error(f"Dokument-Analyse Fehler: {e}") | ||
| 245 | return f"❌ Fehler beim Analysieren: {str(e)}" | ||
| 246 | |||
| 247 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 248 | # KI-Integration (Ollama Text) | ||
| 249 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 250 | async def ask_ai(message: str, user_id: str) -> str: | ||
| 251 | """Sendet Nachricht an Ollama.""" | ||
| 252 | if not AI_ENABLED: | ||
| 253 | return None | ||
| 254 | |||
| 255 | if user_id not in conversation_history: | ||
| 256 | conversation_history[user_id] = [] | ||
| 257 | |||
| 258 | conversation_history[user_id].append({"role": "user", "content": message}) | ||
| 259 | |||
| 260 | if len(conversation_history[user_id]) > 20: | ||
| 261 | conversation_history[user_id] = conversation_history[user_id][-20:] | ||
| 262 | |||
| 263 | try: | ||
| 264 | messages = [ | ||
| 265 | {"role": "system", "content": "Du bist Terry, ein hilfreicher Matrix-Bot. Antworte kurz, freundlich und auf Deutsch."}, | ||
| 266 | *conversation_history[user_id] | ||
| 267 | ] | ||
| 268 | |||
| 269 | payload = { | ||
| 270 | "model": OLLAMA_MODEL, | ||
| 271 | "messages": messages, | ||
| 272 | "stream": False, | ||
| 273 | "options": {"temperature": 0.7, "num_predict": 300} | ||
| 274 | } | ||
| 275 | |||
| 276 | async with ClientSession() as session: | ||
| 277 | async with session.post( | ||
| 278 | f"{OLLAMA_URL}/api/chat", | ||
| 279 | json=payload, | ||
| 280 | timeout=60 | ||
| 281 | ) as resp: | ||
| 282 | if resp.status == 200: | ||
| 283 | data = await resp.json() | ||
| 284 | answer = data["message"]["content"] | ||
| 285 | conversation_history[user_id].append({"role": "assistant", "content": answer}) | ||
| 286 | return answer | ||
| 287 | else: | ||
| 288 | logger.error(f"Ollama Fehler: {resp.status}") | ||
| 289 | return None | ||
| 290 | except Exception as e: | ||
| 291 | logger.error(f"KI-Fehler: {e}") | ||
| 292 | return None | ||
| 293 | |||
| 294 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 295 | # Webhook & Health Check | ||
| 296 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 297 | async def webhook_handler(request): | ||
| 298 | """n8n Webhook.""" | ||
| 299 | try: | ||
| 300 | data = await request.json() | ||
| 301 | if data.get("secret") != WEBHOOK_SECRET: | ||
| 302 | return web.json_response({"error": "Unauthorized"}, status=401) | ||
| 303 | |||
| 304 | message = data.get("message") | ||
| 305 | room_id = data.get("room_id", DEFAULT_ROOM_ID) | ||
| 306 | |||
| 307 | if not message: | ||
| 308 | return web.json_response({"error": "No message"}, status=400) | ||
| 309 | |||
| 310 | await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": message}) | ||
| 311 | return web.json_response({"status": "success"}) | ||
| 312 | except Exception as e: | ||
| 313 | return web.json_response({"error": str(e)}, status=500) | ||
| 314 | |||
| 315 | async def health_check(request): | ||
| 316 | """Health Check.""" | ||
| 317 | return web.json_response({ | ||
| 318 | "status": "online", | ||
| 319 | "features": { | ||
| 320 | "images": ENABLE_IMAGE_ANALYSIS, | ||
| 321 | "voice": ENABLE_VOICE_TRANSCRIPTION, | ||
| 322 | "documents": ENABLE_DOCUMENT_ANALYSIS and PDF_SUPPORT | ||
| 323 | } | ||
| 324 | }) | ||
| 325 | |||
| 326 | async def trigger_n8n(trigger_type: str, data: dict): | ||
| 327 | """n8n Trigger.""" | ||
| 328 | try: | ||
| 329 | payload = {"trigger": trigger_type, "data": data, "timestamp": time.time()} | ||
| 330 | async with ClientSession() as session: | ||
| 331 | async with session.post(N8N_WEBHOOK_URL, json=payload, timeout=10) as resp: | ||
| 332 | return await resp.json() if resp.status == 200 else None | ||
| 333 | except: | ||
| 334 | return None | ||
| 335 | |||
| 336 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 337 | # Command Handler | ||
| 338 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 339 | async def handle_command(room: MatrixRoom, event: RoomMessageText, message: str): | ||
| 340 | """Befehle verarbeiten.""" | ||
| 341 | response = None | ||
| 342 | parts = message.split() | ||
| 343 | command = parts[0].lower() if parts else "" | ||
| 344 | args = parts[1:] if len(parts) > 1 else [] | ||
| 345 | |||
| 346 | if message.startswith(BOT_PREFIX) or message.startswith("/"): | ||
| 347 | |||
| 348 | if command in ["!start", "!hilfe", "/start", "/hilfe"]: | ||
| 349 | response = f"""🤖 ~*~*Terry Bot - Multimedia Edition~*~* | ||
| 350 | |||
| 351 | ~*~*Chat:~*~* | ||
| 352 | • `hey terry` - Konversation starten | ||
| 353 | • `stop` - Beenden | ||
| 354 | • `/status` - Status | ||
| 355 | |||
| 356 | ~*~*Medien:~*~* | ||
| 357 | • 🖼️ Bild senden → Automatische Analyse | ||
| 358 | • 🎤 Sprachnachricht → Transkription | ||
| 359 | • 📄 Dokument → Text-Extraktion | ||
| 360 | |||
| 361 | ~*~*Befehle:~*~* | ||
| 362 | • `/würfel` `/münze` `/rechne` `/zeit` `/witz` | ||
| 363 | • `/webhook` - n8n Info | ||
| 364 | • `/features` - Verfügbare Features | ||
| 365 | |||
| 366 | Schick mir einfach Bilder, Sprache oder Dokumente! 📤""" | ||
| 367 | |||
| 368 | elif command in ["!status", "/status"]: | ||
| 369 | active = len(active_conversations) | ||
| 370 | response = f"""📊 ~*~*Bot Status~*~* | ||
| 371 | |||
| 372 | ~*~*KI:~*~* ✅ Ollama ({OLLAMA_MODEL}) | ||
| 373 | ~*~*Features:~*~* | ||
| 374 | • 🖼️ Bildanalyse: {'✅' if ENABLE_IMAGE_ANALYSIS else '❌'} | ||
| 375 | • 🎤 Spracherkennung: {'✅' if ENABLE_VOICE_TRANSCRIPTION else '❌'} | ||
| 376 | • 📄 Dokumente: {'✅' if ENABLE_DOCUMENT_ANALYSIS and PDF_SUPPORT else '❌'} | ||
| 377 | |||
| 378 | ~*~*Konversationen:~*~* {active} aktiv | ||
| 379 | ~*~*Räume:~*~* {len(client.rooms)}""" | ||
| 380 | |||
| 381 | elif command in ["!features", "/features"]: | ||
| 382 | response = f"""✨ ~*~*Verfügbare Features~*~* | ||
| 383 | |||
| 384 | ~*~*🖼️ Bildanalyse:~*~* | ||
| 385 | • Bildbeschreibung mit KI | ||
| 386 | • Objekt-Erkennung | ||
| 387 | • Text-Extraktion (OCR) | ||
| 388 | → Einfach Bild hochladen! | ||
| 389 | |||
| 390 | ~*~*🎤 Spracherkennung:~*~* | ||
| 391 | • Audio → Text | ||
| 392 | • Deutsch & Englisch | ||
| 393 | → Sprachnachricht senden! | ||
| 394 | |||
| 395 | ~*~*📄 Dokumentenanalyse:~*~* | ||
| 396 | • PDF, Word, Excel, CSV | ||
| 397 | • Text-Extraktion | ||
| 398 | • Daten-Übersicht | ||
| 399 | → Datei hochladen! | ||
| 400 | |||
| 401 | ~*~*Beispiel:~*~* | ||
| 402 | Lade ein Foto hoch und frage: | ||
| 403 | "Was ist auf dem Bild?" | ||
| 404 | "Welcher Text steht da?" | ||
| 405 | "Beschreibe die Szene" | ||
| 406 | """ | ||
| 407 | |||
| 408 | elif command in ["!würfel", "/würfel"]: | ||
| 409 | response = f"🎲 ~*~*{random.randint(1, 6)}~*~*" | ||
| 410 | |||
| 411 | elif command in ["!münze", "/münze"]: | ||
| 412 | response = random.choice(["Kopf 🪙", "Zahl 🔢"]) | ||
| 413 | |||
| 414 | elif command in ["!zeit", "/zeit"]: | ||
| 415 | response = f"🕐 ~*~*{datetime.now().strftime('%H:%M:%S')}~*~* Uhr" | ||
| 416 | |||
| 417 | elif command in ["!witz", "/witz"]: | ||
| 418 | witze = [ | ||
| 419 | "Warum können Geister so schlecht lügen? Weil sie so leicht zu durchschauen sind! 👻", | ||
| 420 | "Was ist grün und steht vor der Tür? Ein Klopfsalat! 🥬", | ||
| 421 | "Warum summen Bienen? Weil sie den Text nicht kennen! 🐝" | ||
| 422 | ] | ||
| 423 | response = random.choice(witze) | ||
| 424 | |||
| 425 | elif command in ["!rechne", "/rechne"]: | ||
| 426 | if not args: | ||
| 427 | response = "❌ Beispiel: `/rechne 5 + 3`" | ||
| 428 | else: | ||
| 429 | try: | ||
| 430 | expr = " ".join(args) | ||
| 431 | if all(c in "0123456789+-*/(). " for c in expr): | ||
| 432 | result = eval(expr) | ||
| 433 | response = f"🧮 `{expr}` = ~*~*{result}~*~*" | ||
| 434 | else: | ||
| 435 | response = "❌ Nur Zahlen und +-*/() erlaubt" | ||
| 436 | except: | ||
| 437 | response = "❌ Rechenfehler" | ||
| 438 | |||
| 439 | elif command in ["!webhook", "/webhook"]: | ||
| 440 | response = f"""📡 ~*~*Webhook~*~* | ||
| 441 | POST http:~/~/YOUR_IP:{WEBHOOK_PORT}/webhook | ||
| 442 | Secret: {WEBHOOK_SECRET} | ||
| 443 | Room: {room.room_id}""" | ||
| 444 | |||
| 445 | # Normale Nachricht | ||
| 446 | else: | ||
| 447 | user_key = f"{event.sender}_{room.room_id}" | ||
| 448 | |||
| 449 | activation_words = ["hey terry", "hallo terry", "hi terry", "terry"] | ||
| 450 | is_activation = any(message.lower().startswith(w) for w in activation_words) | ||
| 451 | |||
| 452 | deactivation_words = ["stop", "bye terry", "tschüss terry"] | ||
| 453 | is_deactivation = any(w in message.lower() for w in deactivation_words) | ||
| 454 | |||
| 455 | if is_deactivation and user_key in active_conversations: | ||
| 456 | del active_conversations[user_key] | ||
| 457 | response = "👋 Okay, bis später! Schreib 'hey terry' zum Aktivieren." | ||
| 458 | |||
| 459 | elif is_activation: | ||
| 460 | active_conversations[user_key] = time.time() | ||
| 461 | clean_message = message | ||
| 462 | for w in activation_words: | ||
| 463 | clean_message = clean_message.lower().replace(w, "").strip() | ||
| 464 | |||
| 465 | if clean_message and AI_ENABLED: | ||
| 466 | ai_response = await ask_ai(clean_message, event.sender) | ||
| 467 | response = ai_response if ai_response else "🤖 Ollama antwortet nicht." | ||
| 468 | else: | ||
| 469 | response = "👋 Hi! Ich bin aktiviert. Schreib drauf los oder schick mir Medien!\n\n'stop' zum Beenden." | ||
| 470 | |||
| 471 | elif user_key in active_conversations: | ||
| 472 | if AI_ENABLED: | ||
| 473 | ai_response = await ask_ai(message, event.sender) | ||
| 474 | response = ai_response if ai_response else "🤖 Ollama Problem." | ||
| 475 | |||
| 476 | if response: | ||
| 477 | try: | ||
| 478 | await client.room_send(room.room_id, "m.room.message", {"msgtype": "m.text", "body": response}) | ||
| 479 | logger.info(f"✓ Antwort → {event.sender.split(':')[0]}") | ||
| 480 | except Exception as e: | ||
| 481 | logger.error(f"Senden fehlgeschlagen: {e}") | ||
| 482 | |||
| 483 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 484 | # Message Callbacks | ||
| 485 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 486 | async def message_callback(room: MatrixRoom, event: RoomMessageText): | ||
| 487 | """Text Messages.""" | ||
| 488 | if event.sender == client.user_id: | ||
| 489 | return | ||
| 490 | |||
| 491 | if hasattr(event, 'server_timestamp'): | ||
| 492 | age = time.time() * 1000 - event.server_timestamp | ||
| 493 | if age > 10000: | ||
| 494 | return | ||
| 495 | |||
| 496 | message = event.body.strip() | ||
| 497 | logger.info(f"📨 {event.sender.split(':')[0]}: {message[:50]}...") | ||
| 498 | await handle_command(room, event, message) | ||
| 499 | |||
| 500 | async def image_callback(room: MatrixRoom, event: RoomMessageImage): | ||
| 501 | """Bild-Messages.""" | ||
| 502 | if event.sender == client.user_id or not ENABLE_IMAGE_ANALYSIS: | ||
| 503 | return | ||
| 504 | |||
| 505 | logger.info(f"🖼️ Bild von {event.sender.split(':')[0]}") | ||
| 506 | |||
| 507 | try: | ||
| 508 | image_data = await download_file(event.url) | ||
| 509 | if not image_data: | ||
| 510 | await client.room_send(room.room_id, "m.room.message", | ||
| 511 | {"msgtype": "m.text", "body": "❌ Konnte Bild nicht laden"}) | ||
| 512 | return | ||
| 513 | |||
| 514 | # Prüfe ob User aktiv ist oder Bildanalyse explizit gewünscht | ||
| 515 | user_key = f"{event.sender}_{room.room_id}" | ||
| 516 | if user_key in active_conversations: | ||
| 517 | analysis = await analyze_image(image_data) | ||
| 518 | if analysis: | ||
| 519 | response = f"🖼️ ~*~*Bildanalyse:~*~*\n\n{analysis}" | ||
| 520 | await client.room_send(room.room_id, "m.room.message", | ||
| 521 | {"msgtype": "m.text", "body": response}) | ||
| 522 | else: | ||
| 523 | await client.room_send(room.room_id, "m.room.message", | ||
| 524 | {"msgtype": "m.text", "body": "🖼️ Bild empfangen! Schreib 'hey terry' um es analysieren zu lassen."}) | ||
| 525 | |||
| 526 | except Exception as e: | ||
| 527 | logger.error(f"Bild-Verarbeitung Fehler: {e}") | ||
| 528 | |||
| 529 | async def audio_callback(room: MatrixRoom, event: RoomMessageAudio): | ||
| 530 | """Audio-Messages.""" | ||
| 531 | if event.sender == client.user_id or not ENABLE_VOICE_TRANSCRIPTION: | ||
| 532 | return | ||
| 533 | |||
| 534 | logger.info(f"🎤 Audio von {event.sender.split(':')[0]}") | ||
| 535 | |||
| 536 | try: | ||
| 537 | audio_data = await download_file(event.url) | ||
| 538 | if not audio_data: | ||
| 539 | return | ||
| 540 | |||
| 541 | user_key = f"{event.sender}_{room.room_id}" | ||
| 542 | if user_key in active_conversations: | ||
| 543 | await client.room_send(room.room_id, "m.room.message", | ||
| 544 | {"msgtype": "m.text", "body": "🎤 Transkribiere Audio..."}) | ||
| 545 | |||
| 546 | transcription = await transcribe_audio(audio_data) | ||
| 547 | if transcription: | ||
| 548 | response = f"🎤 ~*~*Transkription:~*~*\n\n{transcription}" | ||
| 549 | |||
| 550 | # Frage KI basierend auf Transkription | ||
| 551 | if AI_ENABLED: | ||
| 552 | ai_response = await ask_ai(transcription, event.sender) | ||
| 553 | if ai_response: | ||
| 554 | response += f"\n\n~*~*Antwort:~*~*\n{ai_response}" | ||
| 555 | |||
| 556 | await client.room_send(room.room_id, "m.room.message", | ||
| 557 | {"msgtype": "m.text", "body": response}) | ||
| 558 | else: | ||
| 559 | await client.room_send(room.room_id, "m.room.message", | ||
| 560 | {"msgtype": "m.text", "body": "❌ Transkription fehlgeschlagen"}) | ||
| 561 | else: | ||
| 562 | await client.room_send(room.room_id, "m.room.message", | ||
| 563 | {"msgtype": "m.text", "body": "🎤 Sprachnachricht! Schreib 'hey terry' für Transkription."}) | ||
| 564 | |||
| 565 | except Exception as e: | ||
| 566 | logger.error(f"Audio-Verarbeitung Fehler: {e}") | ||
| 567 | |||
| 568 | async def file_callback(room: MatrixRoom, event: RoomMessageFile): | ||
| 569 | """Datei-Messages.""" | ||
| 570 | if event.sender == client.user_id or not ENABLE_DOCUMENT_ANALYSIS: | ||
| 571 | return | ||
| 572 | |||
| 573 | filename = event.body | ||
| 574 | logger.info(f"📄 Datei von {event.sender.split(':')[0]}: {filename}") | ||
| 575 | |||
| 576 | try: | ||
| 577 | file_data = await download_file(event.url) | ||
| 578 | if not file_data: | ||
| 579 | return | ||
| 580 | |||
| 581 | user_key = f"{event.sender}_{room.room_id}" | ||
| 582 | if user_key in active_conversations: | ||
| 583 | await client.room_send(room.room_id, "m.room.message", | ||
| 584 | {"msgtype": "m.text", "body": f"📄 Analysiere '{filename}'..."}) | ||
| 585 | |||
| 586 | analysis = await analyze_document(file_data, filename) | ||
| 587 | if analysis: | ||
| 588 | await client.room_send(room.room_id, "m.room.message", | ||
| 589 | {"msgtype": "m.text", "body": analysis}) | ||
| 590 | else: | ||
| 591 | await client.room_send(room.room_id, "m.room.message", | ||
| 592 | {"msgtype": "m.text", "body": "❌ Dokument-Analyse fehlgeschlagen"}) | ||
| 593 | else: | ||
| 594 | await client.room_send(room.room_id, "m.room.message", | ||
| 595 | {"msgtype": "m.text", "body": f"📄 Datei '{filename}' empfangen! Schreib 'hey terry' für Analyse."}) | ||
| 596 | |||
| 597 | except Exception as e: | ||
| 598 | logger.error(f"Datei-Verarbeitung Fehler: {e}") | ||
| 599 | |||
| 600 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 601 | # Server & Client | ||
| 602 | # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-- | ||
| 603 | async def start_webserver(): | ||
| 604 | """Webhook-Server.""" | ||
| 605 | app.router.add_post('/webhook', webhook_handler) | ||
| 606 | app.router.add_get('/health', health_check) | ||
| 607 | |||
| 608 | runner = web.AppRunner(app) | ||
| 609 | await runner.setup() | ||
| 610 | site = web.TCPSite(runner, '0.0.0.0', WEBHOOK_PORT) | ||
| 611 | await site.start() | ||
| 612 | logger.info(f"🌐 Webhook: Port {WEBHOOK_PORT}") | ||
| 613 | |||
| 614 | async def start_matrix_client(): | ||
| 615 | """Matrix Client.""" | ||
| 616 | global client | ||
| 617 | |||
| 618 | client = AsyncClient(HOMESERVER, USER_ID, device_id=DEVICE_ID) | ||
| 619 | client.access_token = ACCESS_TOKEN | ||
| 620 | |||
| 621 | # Registriere alle Callbacks | ||
| 622 | client.add_event_callback(message_callback, RoomMessageText) | ||
| 623 | client.add_event_callback(image_callback, RoomMessageImage) | ||
| 624 | client.add_event_callback(audio_callback, RoomMessageAudio) | ||
| 625 | client.add_event_callback(file_callback, RoomMessageFile) | ||
| 626 | |||
| 627 | await client.sync(timeout=30000, full_state=True) | ||
| 628 | |||
| 629 | logger.info(f"✓ In {len(client.rooms)} Räumen") | ||
| 630 | for room_id, room in client.rooms.items(): | ||
| 631 | logger.info(f" • {room.display_name or room_id}") | ||
| 632 | |||
| 633 | await client.sync_forever(timeout=30000) | ||
| 634 | |||
| 635 | async def main(): | ||
| 636 | """Hauptfunktion.""" | ||
| 637 | logger.info("=" * 70) | ||
| 638 | logger.info("🤖 Terry Bot - Multimedia Edition v4.0") | ||
| 639 | logger.info("=" * 70) | ||
| 640 | logger.info(f"🧠 KI: Ollama ({OLLAMA_MODEL})") | ||
| 641 | logger.info(f"🖼️ Bilder: {'✅' if ENABLE_IMAGE_ANALYSIS else '❌'}") | ||
| 642 | logger.info(f"🎤 Sprache: {'✅' if ENABLE_VOICE_TRANSCRIPTION else '❌'}") | ||
| 643 | logger.info(f"📄 Dokumente: {'✅' if ENABLE_DOCUMENT_ANALYSIS and PDF_SUPPORT else '❌'}") | ||
| 644 | logger.info("=" * 70) | ||
| 645 | |||
| 646 | try: | ||
| 647 | await asyncio.gather( | ||
| 648 | start_webserver(), | ||
| 649 | start_matrix_client() | ||
| 650 | ) | ||
| 651 | except KeyboardInterrupt: | ||
| 652 | logger.info("\n⏹️ Bot beendet") | ||
| 653 | finally: | ||
| 654 | if client: | ||
| 655 | await client.close() | ||
| 656 | |||
| 657 | if ~_~_name~_~_ == "~_~_main~_~_": | ||
| 658 | try: | ||
| 659 | asyncio.run(main()) | ||
| 660 | except KeyboardInterrupt: | ||
| 661 | pass |
