Wiki source code of code v3

Last modified by René Schmidt on 2025/10/12 08:42

Hide last authors
René Schmidt 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 # PDF
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