Wiki source code of Matrix-Bot-Jean
Last modified by René Schmidt on 2025/10/19 20:54
Hide last authors
| author | version | line-number | content |
|---|---|---|---|
![]() |
1.1 | 1 | #!/bin/bash |
| 2 | set -e | ||
| 3 | |||
| 4 | cd /root | ||
| 5 | |||
| 6 | cat > create_jean_complete_fixed.sh << 'ENDOFSCRIPT' | ||
| 7 | #!/bin/bash | ||
| 8 | set -e | ||
| 9 | |||
| 10 | PROJECT="jean-matrix-bot-complete" | ||
| 11 | echo "🚀 Creating Complete Jean Matrix Bot with all fixes..." | ||
| 12 | |||
| 13 | rm -rf "$PROJECT" | ||
| 14 | mkdir -p "$PROJECT" | ||
| 15 | cd "$PROJECT" | ||
| 16 | |||
| 17 | # ============================================ | ||
| 18 | # Verzeichnisstruktur | ||
| 19 | # ============================================ | ||
| 20 | mkdir -p jean/{core,plugins/{builtin,custom},utils} | ||
| 21 | mkdir -p config data logs | ||
| 22 | |||
| 23 | # ============================================ | ||
| 24 | # 1. TOKEN GENERATOR (VERBESSERT) | ||
| 25 | # ============================================ | ||
| 26 | cat > get_token.py << 'PYEOF' | ||
| 27 | #!/usr/bin/env python3 | ||
| 28 | """ | ||
| 29 | Matrix Access Token Generator - Improved Version | ||
| 30 | Generiert einen Access Token für den Jean Bot | ||
| 31 | """ | ||
| 32 | import asyncio | ||
| 33 | import sys | ||
| 34 | from nio import AsyncClient, LoginResponse, LoginError | ||
| 35 | |||
| 36 | async def get_token(homeserver: str, username: str, password: str, device_name: str = "Jean Bot"): | ||
| 37 | """Generate access token""" | ||
| 38 | # Ensure @ prefix | ||
| 39 | if not username.startswith("@"): | ||
| 40 | username = f"@{username}" | ||
| 41 | |||
| 42 | # Remove trailing slash from homeserver | ||
| 43 | homeserver = homeserver.rstrip('/') | ||
| 44 | |||
| 45 | print(f"\n🔐 Attempting login...") | ||
| 46 | print(f" Homeserver: {homeserver}") | ||
| 47 | print(f" Username: {username}") | ||
| 48 | print(f" Device: {device_name}") | ||
| 49 | print() | ||
| 50 | |||
| 51 | client = AsyncClient(homeserver, username) | ||
| 52 | |||
| 53 | try: | ||
| 54 | # Attempt login | ||
| 55 | response = await client.login(password, device_name=device_name) | ||
| 56 | |||
| 57 | if isinstance(response, LoginResponse): | ||
| 58 | print("✅ Login successful!") | ||
| 59 | print() | ||
| 60 | print("="*70) | ||
| 61 | print("📋 YOUR ACCESS TOKEN:") | ||
| 62 | print("="*70) | ||
| 63 | print() | ||
| 64 | print(f"MATRIX_ACCESS_TOKEN={response.access_token}") | ||
| 65 | print(f"MATRIX_DEVICE_ID={response.device_id}") | ||
| 66 | print(f"MATRIX_USER={username}") | ||
| 67 | print(f"MATRIX_HOMESERVER={homeserver}") | ||
| 68 | print() | ||
| 69 | print("="*70) | ||
| 70 | print() | ||
| 71 | |||
| 72 | # Try to update .env file | ||
| 73 | try: | ||
| 74 | import os | ||
| 75 | if os.path.exists('.env'): | ||
| 76 | with open('.env', 'r') as f: | ||
| 77 | lines = f.readlines() | ||
| 78 | |||
| 79 | # Update or add lines | ||
| 80 | updated_lines = [] | ||
| 81 | keys_updated = set() | ||
| 82 | |||
| 83 | for line in lines: | ||
| 84 | if line.startswith('MATRIX_ACCESS_TOKEN='): | ||
| 85 | updated_lines.append(f'MATRIX_ACCESS_TOKEN={response.access_token}\n') | ||
| 86 | keys_updated.add('token') | ||
| 87 | elif line.startswith('MATRIX_DEVICE_ID='): | ||
| 88 | updated_lines.append(f'MATRIX_DEVICE_ID={response.device_id}\n') | ||
| 89 | keys_updated.add('device') | ||
| 90 | elif line.startswith('MATRIX_USER='): | ||
| 91 | updated_lines.append(f'MATRIX_USER={username}\n') | ||
| 92 | keys_updated.add('user') | ||
| 93 | elif line.startswith('MATRIX_HOMESERVER='): | ||
| 94 | updated_lines.append(f'MATRIX_HOMESERVER={homeserver}\n') | ||
| 95 | keys_updated.add('homeserver') | ||
| 96 | else: | ||
| 97 | updated_lines.append(line) | ||
| 98 | |||
| 99 | # Add missing keys | ||
| 100 | if 'token' not in keys_updated: | ||
| 101 | updated_lines.append(f'MATRIX_ACCESS_TOKEN={response.access_token}\n') | ||
| 102 | if 'device' not in keys_updated: | ||
| 103 | updated_lines.append(f'MATRIX_DEVICE_ID={response.device_id}\n') | ||
| 104 | if 'user' not in keys_updated: | ||
| 105 | updated_lines.append(f'MATRIX_USER={username}\n') | ||
| 106 | if 'homeserver' not in keys_updated: | ||
| 107 | updated_lines.append(f'MATRIX_HOMESERVER={homeserver}\n') | ||
| 108 | |||
| 109 | # Write back | ||
| 110 | with open('.env', 'w') as f: | ||
| 111 | f.writelines(updated_lines) | ||
| 112 | |||
| 113 | print("✅ .env file updated automatically!") | ||
| 114 | print() | ||
| 115 | else: | ||
| 116 | print("ℹ️ No .env file found.") | ||
| 117 | print(" Create one with: cp .env.example .env") | ||
| 118 | print() | ||
| 119 | |||
| 120 | except Exception as e: | ||
| 121 | print(f"⚠️ Could not update .env: {e}") | ||
| 122 | print(" Please copy the values above manually.") | ||
| 123 | print() | ||
| 124 | |||
| 125 | return True | ||
| 126 | |||
| 127 | elif isinstance(response, LoginError): | ||
| 128 | print("❌ Login failed!") | ||
| 129 | print(f" Error: {response.message}") | ||
| 130 | print(f" Status: {response.status_code}") | ||
| 131 | print() | ||
| 132 | |||
| 133 | if response.status_code == 403: | ||
| 134 | print("💡 Possible reasons:") | ||
| 135 | print(" - Wrong password") | ||
| 136 | print(" - Account locked") | ||
| 137 | print(" - User doesn't exist") | ||
| 138 | elif response.status_code == 429: | ||
| 139 | print("💡 Too many login attempts. Wait a moment and try again.") | ||
| 140 | else: | ||
| 141 | print(f"💡 HTTP Status: {response.status_code}") | ||
| 142 | |||
| 143 | return False | ||
| 144 | else: | ||
| 145 | print(f"❌ Unexpected response: {type(response)}") | ||
| 146 | print(f" {response}") | ||
| 147 | return False | ||
| 148 | |||
| 149 | except Exception as e: | ||
| 150 | print(f"❌ Error during login: {e}") | ||
| 151 | print() | ||
| 152 | import traceback | ||
| 153 | traceback.print_exc() | ||
| 154 | return False | ||
| 155 | |||
| 156 | finally: | ||
| 157 | await client.close() | ||
| 158 | |||
| 159 | def main(): | ||
| 160 | print() | ||
| 161 | print("="*70) | ||
| 162 | print("🤖 Jean Matrix Bot - Token Generator") | ||
| 163 | print("="*70) | ||
| 164 | print() | ||
| 165 | |||
| 166 | # Check for command line arguments | ||
| 167 | if len(sys.argv) >= 4: | ||
| 168 | homeserver = sys.argv[1] | ||
| 169 | username = sys.argv[2] | ||
| 170 | password = sys.argv[3] | ||
| 171 | device_name = sys.argv[4] if len(sys.argv) > 4 else "Jean Bot" | ||
| 172 | |||
| 173 | print("📋 Using command line arguments") | ||
| 174 | else: | ||
| 175 | # Interactive mode | ||
| 176 | print("📋 Interactive Mode") | ||
| 177 | print() | ||
| 178 | |||
| 179 | homeserver = input("Homeserver URL (e.g. https:~/~/matrix.org): ").strip() | ||
| 180 | if not homeserver: | ||
| 181 | print("❌ Homeserver is required!") | ||
| 182 | sys.exit(1) | ||
| 183 | |||
| 184 | username = input("Username (e.g. @jean:matrix.org or jean): ").strip() | ||
| 185 | if not username: | ||
| 186 | print("❌ Username is required!") | ||
| 187 | sys.exit(1) | ||
| 188 | |||
| 189 | password = input("Password: ").strip() | ||
| 190 | if not password: | ||
| 191 | print("❌ Password is required!") | ||
| 192 | sys.exit(1) | ||
| 193 | |||
| 194 | device_name = input("Device Name [Jean Bot]: ").strip() or "Jean Bot" | ||
| 195 | |||
| 196 | # Run async function | ||
| 197 | success = asyncio.run(get_token(homeserver, username, password, device_name)) | ||
| 198 | |||
| 199 | if success: | ||
| 200 | print("🚀 Next steps:") | ||
| 201 | print(" 1. Run: python -m jean.core.bot") | ||
| 202 | print(" 2. Invite the bot to a room") | ||
| 203 | print(" 3. Send: !help") | ||
| 204 | print() | ||
| 205 | sys.exit(0) | ||
| 206 | else: | ||
| 207 | print("❌ Token generation failed.") | ||
| 208 | print() | ||
| 209 | print("💡 Troubleshooting:") | ||
| 210 | print(" 1. Check homeserver URL (must start with https:~/~/)") | ||
| 211 | print(" 2. Verify username format (@user:server.com)") | ||
| 212 | print(" 3. Confirm password is correct") | ||
| 213 | print(" 4. Check if user exists on the homeserver") | ||
| 214 | print() | ||
| 215 | sys.exit(1) | ||
| 216 | |||
| 217 | if ~_~_name~_~_ == "~_~_main~_~_": | ||
| 218 | main() | ||
| 219 | PYEOF | ||
| 220 | |||
| 221 | chmod +x get_token.py | ||
| 222 | |||
| 223 | # ============================================ | ||
| 224 | # 2. CONFIGURATION | ||
| 225 | # ============================================ | ||
| 226 | cat > jean/core/config.py << 'EOF' | ||
| 227 | """ | ||
| 228 | Configuration Manager für Jean Bot | ||
| 229 | """ | ||
| 230 | import os | ||
| 231 | from pathlib import Path | ||
| 232 | from dotenv import load_dotenv | ||
| 233 | |||
| 234 | load_dotenv() | ||
| 235 | |||
| 236 | class Config: | ||
| 237 | """Bot Configuration""" | ||
| 238 | |||
| 239 | def ~_~_init~_~_(self): | ||
| 240 | # Matrix Config | ||
| 241 | self.homeserver = os.getenv('MATRIX_HOMESERVER') | ||
| 242 | self.user = os.getenv('MATRIX_USER') | ||
| 243 | self.access_token = os.getenv('MATRIX_ACCESS_TOKEN') | ||
| 244 | self.device_id = os.getenv('MATRIX_DEVICE_ID', 'JEAN_BOT') | ||
| 245 | self.password = os.getenv('MATRIX_PASSWORD') | ||
| 246 | |||
| 247 | # Ollama Config | ||
| 248 | self.ollama_url = os.getenv('OLLAMA_URL', 'http:~/~/localhost:11434') | ||
| 249 | self.model = os.getenv('OLLAMA_MODEL', 'llama3.2:latest') | ||
| 250 | self.vision_model = os.getenv('OLLAMA_VISION_MODEL', 'llava:latest') | ||
| 251 | |||
| 252 | # Bot Settings | ||
| 253 | self.command_prefix = os.getenv('COMMAND_PREFIX', '!') | ||
| 254 | self.log_level = os.getenv('LOG_LEVEL', 'INFO') | ||
| 255 | |||
| 256 | # Database | ||
| 257 | self.database_url = os.getenv('DATABASE_URL', 'sqlite:~/~//data/jean.db') | ||
| 258 | |||
| 259 | # Admin Users | ||
| 260 | admin_str = os.getenv('ADMIN_USERS', '') | ||
| 261 | self.admin_users = [u.strip() for u in admin_str.split(',') if u.strip()] | ||
| 262 | |||
| 263 | # Feature Flags | ||
| 264 | self.enable_image_analysis = os.getenv('ENABLE_IMAGE_ANALYSIS', 'true').lower() == 'true' | ||
| 265 | self.enable_voice_transcription = os.getenv('ENABLE_VOICE_TRANSCRIPTION', 'false').lower() == 'true' | ||
| 266 | self.enable_document_analysis = os.getenv('ENABLE_DOCUMENT_ANALYSIS', 'true').lower() == 'true' | ||
| 267 | |||
| 268 | # Validate | ||
| 269 | self._validate() | ||
| 270 | |||
| 271 | def _validate(self): | ||
| 272 | """Validate required config""" | ||
| 273 | required = { | ||
| 274 | 'homeserver': self.homeserver, | ||
| 275 | 'user': self.user, | ||
| 276 | } | ||
| 277 | |||
| 278 | if not self.access_token and not self.password: | ||
| 279 | required['access_token or password'] = None | ||
| 280 | |||
| 281 | missing = [k for k, v in required.items() if not v] | ||
| 282 | |||
| 283 | if missing: | ||
| 284 | raise ValueError(f"Missing required config: {', '.join(missing)}") | ||
| 285 | EOF | ||
| 286 | |||
| 287 | # ============================================ | ||
| 288 | # 3. OLLAMA CLIENT | ||
| 289 | # ============================================ | ||
| 290 | cat > jean/utils/ollama_client.py << 'EOF' | ||
| 291 | """ | ||
| 292 | Ollama Client für Jean Bot | ||
| 293 | """ | ||
| 294 | import aiohttp | ||
| 295 | import logging | ||
| 296 | from typing import List, Dict, Optional | ||
| 297 | |||
| 298 | logger = logging.getLogger(~_~_name~_~_) | ||
| 299 | |||
| 300 | class OllamaClient: | ||
| 301 | """Client für Ollama API""" | ||
| 302 | |||
| 303 | def ~_~_init~_~_(self, base_url: str, model: str, vision_model: str = None): | ||
| 304 | self.base_url = base_url.rstrip('/') | ||
| 305 | self.model = model | ||
| 306 | self.vision_model = vision_model or model | ||
| 307 | |||
| 308 | async def generate( | ||
| 309 | self, | ||
| 310 | prompt: str, | ||
| 311 | model: Optional[str] = None, | ||
| 312 | images: Optional[List[str]] = None, | ||
| 313 | system: Optional[str] = None | ||
| 314 | ) -> Dict: | ||
| 315 | """Generate completion""" | ||
| 316 | try: | ||
| 317 | url = f"{self.base_url}/api/generate" | ||
| 318 | |||
| 319 | payload = { | ||
| 320 | "model": model or self.model, | ||
| 321 | "prompt": prompt, | ||
| 322 | "stream": False | ||
| 323 | } | ||
| 324 | |||
| 325 | if images: | ||
| 326 | payload["images"] = images | ||
| 327 | |||
| 328 | if system: | ||
| 329 | payload["system"] = system | ||
| 330 | |||
| 331 | async with aiohttp.ClientSession() as session: | ||
| 332 | async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp: | ||
| 333 | if resp.status != 200: | ||
| 334 | error_text = await resp.text() | ||
| 335 | raise Exception(f"Ollama API error {resp.status}: {error_text}") | ||
| 336 | |||
| 337 | return await resp.json() | ||
| 338 | |||
| 339 | except Exception as e: | ||
| 340 | logger.error(f"Ollama generate error: {e}") | ||
| 341 | raise | ||
| 342 | |||
| 343 | async def chat( | ||
| 344 | self, | ||
| 345 | messages: List[Dict[str, str]], | ||
| 346 | model: Optional[str] = None, | ||
| 347 | system: Optional[str] = None | ||
| 348 | ) -> Dict: | ||
| 349 | """Chat completion""" | ||
| 350 | try: | ||
| 351 | url = f"{self.base_url}/api/chat" | ||
| 352 | |||
| 353 | payload = { | ||
| 354 | "model": model or self.model, | ||
| 355 | "messages": messages, | ||
| 356 | "stream": False | ||
| 357 | } | ||
| 358 | |||
| 359 | if system: | ||
| 360 | payload["messages"].insert(0, {"role": "system", "content": system}) | ||
| 361 | |||
| 362 | async with aiohttp.ClientSession() as session: | ||
| 363 | async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp: | ||
| 364 | if resp.status != 200: | ||
| 365 | error_text = await resp.text() | ||
| 366 | raise Exception(f"Ollama API error {resp.status}: {error_text}") | ||
| 367 | |||
| 368 | return await resp.json() | ||
| 369 | |||
| 370 | except Exception as e: | ||
| 371 | logger.error(f"Ollama chat error: {e}") | ||
| 372 | raise | ||
| 373 | |||
| 374 | async def list_models(self) -> List[str]: | ||
| 375 | """List available models""" | ||
| 376 | try: | ||
| 377 | url = f"{self.base_url}/api/tags" | ||
| 378 | |||
| 379 | async with aiohttp.ClientSession() as session: | ||
| 380 | async with session.get(url) as resp: | ||
| 381 | if resp.status != 200: | ||
| 382 | return [] | ||
| 383 | |||
| 384 | data = await resp.json() | ||
| 385 | return [m['name'] for m in data.get('models', [])] | ||
| 386 | |||
| 387 | except Exception as e: | ||
| 388 | logger.error(f"List models error: {e}") | ||
| 389 | return [] | ||
| 390 | EOF | ||
| 391 | |||
| 392 | # ============================================ | ||
| 393 | # 4. MEDIA HANDLER (FIXED) | ||
| 394 | # ============================================ | ||
| 395 | cat > jean/core/media_handler.py << 'EOF' | ||
| 396 | """ | ||
| 397 | Fixed Media Handler für Jean Matrix Bot | ||
| 398 | Behebt Download-Probleme für Bilder, Audio, PDFs und DOCX | ||
| 399 | """ | ||
| 400 | import asyncio | ||
| 401 | import aiohttp | ||
| 402 | import aiofiles | ||
| 403 | import logging | ||
| 404 | from pathlib import Path | ||
| 405 | from typing import Optional, Tuple | ||
| 406 | from nio import AsyncClient, DownloadError, DownloadResponse | ||
| 407 | import mimetypes | ||
| 408 | |||
| 409 | logger = logging.getLogger(~_~_name~_~_) | ||
| 410 | |||
| 411 | class MediaHandler: | ||
| 412 | """Handhabt Media-Downloads mit verbessertem Error-Handling""" | ||
| 413 | |||
| 414 | def ~_~_init~_~_(self, client: AsyncClient, temp_dir: str = "/tmp/jean_media"): | ||
| 415 | self.client = client | ||
| 416 | self.temp_dir = Path(temp_dir) | ||
| 417 | self.temp_dir.mkdir(parents=True, exist_ok=True) | ||
| 418 | |||
| 419 | async def download_media( | ||
| 420 | self, | ||
| 421 | mxc_url: str, | ||
| 422 | filename: Optional[str] = None | ||
| 423 | ) -> Optional[Tuple[Path, str]]: | ||
| 424 | """Download media from mxc:~/~/ URL""" | ||
| 425 | if not mxc_url or not mxc_url.startswith("mxc:~/~/"): | ||
| 426 | logger.error(f"Ungültige MXC URL: {mxc_url}") | ||
| 427 | return None | ||
| 428 | |||
| 429 | try: | ||
| 430 | logger.info(f"📥 Downloading: {mxc_url}") | ||
| 431 | |||
| 432 | parts = mxc_url.replace("mxc:~/~/", "").split("/") | ||
| 433 | if len(parts) < 2: | ||
| 434 | logger.error(f"Fehlerhafte MXC URL Struktur: {mxc_url}") | ||
| 435 | return None | ||
| 436 | |||
| 437 | server_name = parts[0] | ||
| 438 | media_id = "/".join(parts[1:]) | ||
| 439 | |||
| 440 | # Method 1: nio download | ||
| 441 | response = await self.client.download( | ||
| 442 | server_name=server_name, | ||
| 443 | media_id=media_id | ||
| 444 | ) | ||
| 445 | |||
| 446 | if isinstance(response, DownloadError): | ||
| 447 | logger.warning(f"nio download fehlgeschlagen: {response.message}") | ||
| 448 | return await self._download_via_http(mxc_url, filename) | ||
| 449 | |||
| 450 | if not isinstance(response, DownloadResponse): | ||
| 451 | logger.error(f"Unerwartete Response: {type(response)}") | ||
| 452 | return await self._download_via_http(mxc_url, filename) | ||
| 453 | |||
| 454 | if not filename: | ||
| 455 | filename = f"media_{media_id.replace('/', '_')}" | ||
| 456 | |||
| 457 | mime_type = response.content_type or "application/octet-stream" | ||
| 458 | |||
| 459 | ext = mimetypes.guess_extension(mime_type) | ||
| 460 | if ext and not filename.endswith(ext): | ||
| 461 | filename = f"{filename}{ext}" | ||
| 462 | |||
| 463 | filepath = self.temp_dir / filename | ||
| 464 | |||
| 465 | async with aiofiles.open(filepath, 'wb') as f: | ||
| 466 | await f.write(response.body) | ||
| 467 | |||
| 468 | file_size = len(response.body) / 1024 | ||
| 469 | logger.info(f"✅ Download erfolgreich: {filename} ({file_size:.2f} KB)") | ||
| 470 | |||
| 471 | return filepath, mime_type | ||
| 472 | |||
| 473 | except Exception as e: | ||
| 474 | logger.error(f"❌ Download fehlgeschlagen für {mxc_url}: {e}") | ||
| 475 | return await self._download_via_http(mxc_url, filename) | ||
| 476 | |||
| 477 | async def _download_via_http( | ||
| 478 | self, | ||
| 479 | mxc_url: str, | ||
| 480 | filename: Optional[str] = None | ||
| 481 | ) -> Optional[Tuple[Path, str]]: | ||
| 482 | """Fallback: HTTP Download""" | ||
| 483 | try: | ||
| 484 | parts = mxc_url.replace("mxc:~/~/", "").split("/", 1) | ||
| 485 | if len(parts) < 2: | ||
| 486 | return None | ||
| 487 | |||
| 488 | server_name, media_id = parts | ||
| 489 | |||
| 490 | homeserver = self.client.homeserver | ||
| 491 | download_url = f"{homeserver}/_matrix/media/v3/download/{server_name}/{media_id}" | ||
| 492 | |||
| 493 | logger.info(f"🔄 Fallback HTTP Download: {download_url}") | ||
| 494 | |||
| 495 | async with aiohttp.ClientSession() as session: | ||
| 496 | headers = { | ||
| 497 | "Authorization": f"Bearer {self.client.access_token}" | ||
| 498 | } | ||
| 499 | |||
| 500 | async with session.get(download_url, headers=headers) as resp: | ||
| 501 | if resp.status != 200: | ||
| 502 | logger.error(f"HTTP {resp.status}: {await resp.text()}") | ||
| 503 | return None | ||
| 504 | |||
| 505 | content = await resp.read() | ||
| 506 | mime_type = resp.headers.get('Content-Type', 'application/octet-stream') | ||
| 507 | |||
| 508 | if not filename: | ||
| 509 | content_disp = resp.headers.get('Content-Disposition', '') | ||
| 510 | if 'filename=' in content_disp: | ||
| 511 | filename = content_disp.split('filename=')[1].strip('"') | ||
| 512 | else: | ||
| 513 | filename = f"media_{media_id.replace('/', '_')}" | ||
| 514 | |||
| 515 | ext = mimetypes.guess_extension(mime_type) | ||
| 516 | if ext and not filename.endswith(ext): | ||
| 517 | filename = f"{filename}{ext}" | ||
| 518 | |||
| 519 | filepath = self.temp_dir / filename | ||
| 520 | |||
| 521 | async with aiofiles.open(filepath, 'wb') as f: | ||
| 522 | await f.write(content) | ||
| 523 | |||
| 524 | file_size = len(content) / 1024 | ||
| 525 | logger.info(f"✅ HTTP Download erfolgreich: {filename} ({file_size:.2f} KB)") | ||
| 526 | |||
| 527 | return filepath, mime_type | ||
| 528 | |||
| 529 | except Exception as e: | ||
| 530 | logger.error(f"❌ HTTP Download fehlgeschlagen: {e}") | ||
| 531 | return None | ||
| 532 | |||
| 533 | async def cleanup_old_files(self, max_age_hours: int = 24): | ||
| 534 | """Lösche alte temporäre Dateien""" | ||
| 535 | import time | ||
| 536 | cutoff = time.time() - (max_age_hours * 3600) | ||
| 537 | |||
| 538 | for filepath in self.temp_dir.iterdir(): | ||
| 539 | if filepath.is_file() and filepath.stat().st_mtime < cutoff: | ||
| 540 | try: | ||
| 541 | filepath.unlink() | ||
| 542 | logger.debug(f"🗑️ Gelöscht: {filepath.name}") | ||
| 543 | except Exception as e: | ||
| 544 | logger.error(f"Fehler beim Löschen {filepath}: {e}") | ||
| 545 | |||
| 546 | |||
| 547 | class MediaEventHandler: | ||
| 548 | """Event Handler für Media-Events""" | ||
| 549 | |||
| 550 | def ~_~_init~_~_(self, bot): | ||
| 551 | self.bot = bot | ||
| 552 | self.media_handler = MediaHandler(bot.client) | ||
| 553 | |||
| 554 | async def handle_image(self, room, event): | ||
| 555 | """Handler für Bild-Events""" | ||
| 556 | try: | ||
| 557 | logger.info(f"🖼️ Bild empfangen in {room.display_name}") | ||
| 558 | |||
| 559 | result = await self.media_handler.download_media(event.url, event.body) | ||
| 560 | |||
| 561 | if not result: | ||
| 562 | await self.bot.send_message(room.room_id, "❌ Fehler beim Herunterladen des Bildes") | ||
| 563 | return | ||
| 564 | |||
| 565 | filepath, mime_type = result | ||
| 566 | logger.info(f"✅ Bild gespeichert: {filepath}") | ||
| 567 | |||
| 568 | analysis = await self._analyze_image(filepath) | ||
| 569 | await self.bot.send_message(room.room_id, f"🔍 Bildanalyse:\n\n{analysis}") | ||
| 570 | |||
| 571 | except Exception as e: | ||
| 572 | logger.error(f"Fehler bei Bildverarbeitung: {e}") | ||
| 573 | await self.bot.send_message(room.room_id, f"❌ Fehler: {str(e)}") | ||
| 574 | |||
| 575 | async def handle_audio(self, room, event): | ||
| 576 | """Handler für Audio-Events""" | ||
| 577 | try: | ||
| 578 | logger.info(f"🎵 Audio empfangen in {room.display_name}") | ||
| 579 | |||
| 580 | result = await self.media_handler.download_media(event.url, event.body) | ||
| 581 | |||
| 582 | if not result: | ||
| 583 | await self.bot.send_message(room.room_id, "❌ Fehler beim Herunterladen der Audiodatei") | ||
| 584 | return | ||
| 585 | |||
| 586 | filepath, mime_type = result | ||
| 587 | logger.info(f"✅ Audio gespeichert: {filepath}") | ||
| 588 | |||
| 589 | transcription = await self._transcribe_audio(filepath) | ||
| 590 | await self.bot.send_message(room.room_id, f"🎤 Transkription:\n\n{transcription}") | ||
| 591 | |||
| 592 | except Exception as e: | ||
| 593 | logger.error(f"Fehler bei Audioverarbeitung: {e}") | ||
| 594 | await self.bot.send_message(room.room_id, f"❌ Fehler: {str(e)}") | ||
| 595 | |||
| 596 | async def handle_file(self, room, event): | ||
| 597 | """Handler für Datei-Events""" | ||
| 598 | try: | ||
| 599 | logger.info(f"📄 Datei empfangen: {event.body}") | ||
| 600 | |||
| 601 | result = await self.media_handler.download_media(event.url, event.body) | ||
| 602 | |||
| 603 | if not result: | ||
| 604 | await self.bot.send_message(room.room_id, "❌ Fehler beim Herunterladen der Datei") | ||
| 605 | return | ||
| 606 | |||
| 607 | filepath, mime_type = result | ||
| 608 | logger.info(f"✅ Datei gespeichert: {filepath}") | ||
| 609 | |||
| 610 | if mime_type == 'application/pdf': | ||
| 611 | analysis = await self._analyze_pdf(filepath) | ||
| 612 | elif mime_type in ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/msword']: | ||
| 613 | analysis = await self._analyze_docx(filepath) | ||
| 614 | elif mime_type in ['application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.ms-powerpoint']: | ||
| 615 | analysis = await self._analyze_pptx(filepath) | ||
| 616 | else: | ||
| 617 | analysis = f"📎 Datei empfangen: {event.body}\nTyp: {mime_type}\nGröße: {filepath.stat().st_size / 1024:.2f} KB" | ||
| 618 | |||
| 619 | await self.bot.send_message(room.room_id, f"📄 Dokument-Analyse:\n\n{analysis}") | ||
| 620 | |||
| 621 | except Exception as e: | ||
| 622 | logger.error(f"Fehler bei Dateiverarbeitung: {e}") | ||
| 623 | await self.bot.send_message(room.room_id, f"❌ Fehler: {str(e)}") | ||
| 624 | |||
| 625 | async def _analyze_image(self, filepath: Path) -> str: | ||
| 626 | """Analysiere Bild mit Vision Model""" | ||
| 627 | try: | ||
| 628 | import base64 | ||
| 629 | |||
| 630 | with open(filepath, 'rb') as f: | ||
| 631 | image_data = base64.b64encode(f.read()).decode() | ||
| 632 | |||
| 633 | response = await self.bot.ollama_client.generate( | ||
| 634 | model=self.bot.config.vision_model, | ||
| 635 | prompt="Beschreibe dieses Bild detailliert auf Deutsch.", | ||
| 636 | images=[image_data] | ||
| 637 | ) | ||
| 638 | |||
| 639 | return response.get('response', 'Keine Antwort erhalten') | ||
| 640 | |||
| 641 | except Exception as e: | ||
| 642 | logger.error(f"Vision-Analyse fehlgeschlagen: {e}") | ||
| 643 | return f"❌ Bildanalyse fehlgeschlagen: {str(e)}" | ||
| 644 | |||
| 645 | async def _transcribe_audio(self, filepath: Path) -> str: | ||
| 646 | """Transkribiere Audio""" | ||
| 647 | return "🚧 Audio-Transkription wird noch implementiert" | ||
| 648 | |||
| 649 | async def _analyze_pdf(self, filepath: Path) -> str: | ||
| 650 | """Analysiere PDF""" | ||
| 651 | try: | ||
| 652 | from PyPDF2 import PdfReader | ||
| 653 | |||
| 654 | reader = PdfReader(str(filepath)) | ||
| 655 | num_pages = len(reader.pages) | ||
| 656 | |||
| 657 | text = "" | ||
| 658 | for i, page in enumerate(reader.pages[:3]): | ||
| 659 | text += f"\n~-~-- Seite {i+1} ~-~--\n{page.extract_text()}\n" | ||
| 660 | |||
| 661 | prompt = f"Fasse dieses PDF-Dokument zusammen:\n\n{text[:2000]}" | ||
| 662 | response = await self.bot.ollama_client.generate( | ||
| 663 | model=self.bot.config.model, | ||
| 664 | prompt=prompt | ||
| 665 | ) | ||
| 666 | |||
| 667 | summary = response.get('response', 'Keine Antwort') | ||
| 668 | |||
| 669 | return f"📊 PDF: {filepath.name}\nSeiten: {num_pages}\n\nZusammenfassung:\n{summary}" | ||
| 670 | |||
| 671 | except Exception as e: | ||
| 672 | return f"❌ PDF-Analyse fehlgeschlagen: {str(e)}" | ||
| 673 | |||
| 674 | async def _analyze_docx(self, filepath: Path) -> str: | ||
| 675 | """Analysiere DOCX""" | ||
| 676 | try: | ||
| 677 | from docx import Document | ||
| 678 | |||
| 679 | doc = Document(str(filepath)) | ||
| 680 | text = "\n".join([para.text for para in doc.paragraphs[:20]]) | ||
| 681 | |||
| 682 | prompt = f"Fasse dieses Word-Dokument zusammen:\n\n{text[:2000]}" | ||
| 683 | response = await self.bot.ollama_client.generate( | ||
| 684 | model=self.bot.config.model, | ||
| 685 | prompt=prompt | ||
| 686 | ) | ||
| 687 | |||
| 688 | summary = response.get('response', 'Keine Antwort') | ||
| 689 | |||
| 690 | return f"📝 DOCX: {filepath.name}\nAbsätze: {len(doc.paragraphs)}\n\nZusammenfassung:\n{summary}" | ||
| 691 | |||
| 692 | except Exception as e: | ||
| 693 | return f"❌ DOCX-Analyse fehlgeschlagen: {str(e)}" | ||
| 694 | |||
| 695 | async def _analyze_pptx(self, filepath: Path) -> str: | ||
| 696 | """Analysiere PPTX""" | ||
| 697 | try: | ||
| 698 | from pptx import Presentation | ||
| 699 | |||
| 700 | prs = Presentation(str(filepath)) | ||
| 701 | slides_text = [] | ||
| 702 | |||
| 703 | for i, slide in enumerate(prs.slides[:5], 1): | ||
| 704 | text = [] | ||
| 705 | for shape in slide.shapes: | ||
| 706 | if hasattr(shape, "text"): | ||
| 707 | text.append(shape.text) | ||
| 708 | slides_text.append(f"Folie {i}: {' '.join(text)}") | ||
| 709 | |||
| 710 | content = "\n".join(slides_text) | ||
| 711 | |||
| 712 | prompt = f"Fasse diese Präsentation zusammen:\n\n{content[:2000]}" | ||
| 713 | response = await self.bot.ollama_client.generate( | ||
| 714 | model=self.bot.config.model, | ||
| 715 | prompt=prompt | ||
| 716 | ) | ||
| 717 | |||
| 718 | summary = response.get('response', 'Keine Antwort') | ||
| 719 | |||
| 720 | return f"📊 PPTX: {filepath.name}\nFolien: {len(prs.slides)}\n\nZusammenfassung:\n{summary}" | ||
| 721 | |||
| 722 | except Exception as e: | ||
| 723 | return f"❌ PPTX-Analyse fehlgeschlagen: {str(e)}" | ||
| 724 | EOF | ||
| 725 | |||
| 726 | # ============================================ | ||
| 727 | # 5. MAIN BOT | ||
| 728 | # ============================================ | ||
| 729 | cat > jean/core/bot.py << 'EOF' | ||
| 730 | """ | ||
| 731 | Jean Matrix Bot - Main Bot File | ||
| 732 | """ | ||
| 733 | import asyncio | ||
| 734 | import logging | ||
| 735 | from nio import ( | ||
| 736 | AsyncClient, | ||
| 737 | MatrixRoom, | ||
| 738 | RoomMessageText, | ||
| 739 | RoomMessageImage, | ||
| 740 | RoomMessageAudio, | ||
| 741 | RoomMessageFile, | ||
| 742 | InviteEvent, | ||
| 743 | SyncResponse, | ||
| 744 | LoginResponse | ||
| 745 | ) | ||
| 746 | from pathlib import Path | ||
| 747 | import sys | ||
| 748 | |||
| 749 | sys.path.insert(0, str(Path(~_~_file~_~_).parent.parent.parent)) | ||
| 750 | |||
| 751 | from jean.core.config import Config | ||
| 752 | from jean.core.media_handler import MediaHandler, MediaEventHandler | ||
| 753 | from jean.utils.ollama_client import OllamaClient | ||
| 754 | |||
| 755 | logging.basicConfig( | ||
| 756 | level=logging.INFO, | ||
| 757 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | ||
| 758 | ) | ||
| 759 | logger = logging.getLogger(~_~_name~_~_) | ||
| 760 | |||
| 761 | |||
| 762 | class JeanBot: | ||
| 763 | """Jean Matrix Bot""" | ||
| 764 | |||
| 765 | def ~_~_init~_~_(self): | ||
| 766 | self.config = Config() | ||
| 767 | self.client = None | ||
| 768 | self.ollama_client = OllamaClient( | ||
| 769 | base_url=self.config.ollama_url, | ||
| 770 | model=self.config.model, | ||
| 771 | vision_model=self.config.vision_model | ||
| 772 | ) | ||
| 773 | self.media_handler = None | ||
| 774 | self.media_event_handler = None | ||
| 775 | self.chat_history = {} | ||
| 776 | |||
| 777 | async def setup(self): | ||
| 778 | """Initialize bot""" | ||
| 779 | logger.info("🚀 Jean Bot wird gestartet...") | ||
| 780 | |||
| 781 | self.client = AsyncClient( | ||
| 782 | self.config.homeserver, | ||
| 783 | self.config.user | ||
| 784 | ) | ||
| 785 | |||
| 786 | # Login if needed | ||
| 787 | if self.config.access_token: | ||
| 788 | self.client.access_token = self.config.access_token | ||
| 789 | self.client.device_id = self.config.device_id | ||
| 790 | elif self.config.password: | ||
| 791 | logger.info("🔐 Logging in with password...") | ||
| 792 | response = await self.client.login(self.config.password) | ||
| 793 | if isinstance(response, LoginResponse): | ||
| 794 | logger.info("✅ Login successful") | ||
| 795 | logger.info(f"💾 Save this token to .env: {response.access_token}") | ||
| 796 | else: | ||
| 797 | raise Exception(f"Login failed: {response}") | ||
| 798 | else: | ||
| 799 | raise Exception("No access_token or password provided") | ||
| 800 | |||
| 801 | # Initialize handlers | ||
| 802 | self.media_handler = MediaHandler(self.client) | ||
| 803 | self.media_event_handler = MediaEventHandler(self) | ||
| 804 | |||
| 805 | # Register callbacks | ||
| 806 | self.client.add_event_callback(self.on_message, RoomMessageText) | ||
| 807 | self.client.add_event_callback(self.on_image, RoomMessageImage) | ||
| 808 | self.client.add_event_callback(self.on_audio, RoomMessageAudio) | ||
| 809 | self.client.add_event_callback(self.on_file, RoomMessageFile) | ||
| 810 | self.client.add_event_callback(self.on_invite, InviteEvent) | ||
| 811 | |||
| 812 | logger.info(f"✅ Bot konfiguriert als {self.config.user}") | ||
| 813 | |||
| 814 | async def start(self): | ||
| 815 | """Start bot""" | ||
| 816 | try: | ||
| 817 | await self.setup() | ||
| 818 | |||
| 819 | sync_response = await self.client.sync(timeout=30000) | ||
| 820 | if not isinstance(sync_response, SyncResponse): | ||
| 821 | logger.error(f"❌ Sync fehlgeschlagen: {sync_response}") | ||
| 822 | return | ||
| 823 | |||
| 824 | logger.info("✅ Verbindung zu Matrix erfolgreich") | ||
| 825 | logger.info(f"📝 In {len(self.client.rooms)} Räumen") | ||
| 826 | |||
| 827 | asyncio.create_task(self._cleanup_loop()) | ||
| 828 | |||
| 829 | await self.sync_loop() | ||
| 830 | |||
| 831 | except Exception as e: | ||
| 832 | logger.error(f"❌ Start error: {e}", exc_info=True) | ||
| 833 | finally: | ||
| 834 | await self.client.close() | ||
| 835 | |||
| 836 | async def sync_loop(self): | ||
| 837 | """Main sync loop""" | ||
| 838 | logger.info("🔄 Sync loop started") | ||
| 839 | |||
| 840 | while True: | ||
| 841 | try: | ||
| 842 | await self.client.sync_forever(timeout=30000, full_state=True) | ||
| 843 | except Exception as e: | ||
| 844 | logger.error(f"❌ Sync error: {e}") | ||
| 845 | await asyncio.sleep(5) | ||
| 846 | |||
| 847 | async def on_message(self, room: MatrixRoom, event: RoomMessageText): | ||
| 848 | """Text message handler""" | ||
| 849 | if event.sender == self.client.user: | ||
| 850 | return | ||
| 851 | |||
| 852 | message = event.body.strip() | ||
| 853 | |||
| 854 | is_dm = len(room.users) == 2 | ||
| 855 | is_mentioned = self.config.user in message | ||
| 856 | |||
| 857 | if not (is_dm or is_mentioned or message.startswith(self.config.command_prefix)): | ||
| 858 | return | ||
| 859 | |||
| 860 | logger.info(f"💬 [{room.display_name}] {event.sender}: {message}") | ||
| 861 | |||
| 862 | if message.startswith(self.config.command_prefix): | ||
| 863 | await self.handle_command(room, event, message) | ||
| 864 | else: | ||
| 865 | await self.handle_chat(room, event, message) | ||
| 866 | |||
| 867 | async def on_image(self, room: MatrixRoom, event: RoomMessageImage): | ||
| 868 | """Image handler""" | ||
| 869 | if event.sender == self.client.user: | ||
| 870 | return | ||
| 871 | logger.info(f"🖼️ Image received: {event.body}") | ||
| 872 | await self.media_event_handler.handle_image(room, event) | ||
| 873 | |||
| 874 | async def on_audio(self, room: MatrixRoom, event: RoomMessageAudio): | ||
| 875 | """Audio handler""" | ||
| 876 | if event.sender == self.client.user: | ||
| 877 | return | ||
| 878 | logger.info(f"🎵 Audio received: {event.body}") | ||
| 879 | await self.media_event_handler.handle_audio(room, event) | ||
| 880 | |||
| 881 | async def on_file(self, room: MatrixRoom, event: RoomMessageFile): | ||
| 882 | """File handler""" | ||
| 883 | if event.sender == self.client.user: | ||
| 884 | return | ||
| 885 | logger.info(f"📄 File received: {event.body}") | ||
| 886 | await self.media_event_handler.handle_file(room, event) | ||
| 887 | |||
| 888 | async def on_invite(self, room: MatrixRoom, event: InviteEvent): | ||
| 889 | """Invite handler""" | ||
| 890 | logger.info(f"📨 Invite from {event.sender} to {room.room_id}") | ||
| 891 | |||
| 892 | try: | ||
| 893 | await self.client.join(room.room_id) | ||
| 894 | logger.info(f"✅ Joined room: {room.room_id}") | ||
| 895 | |||
| 896 | await self.send_message( | ||
| 897 | room.room_id, | ||
| 898 | f"👋 Hallo! Ich bin Jean, dein KI-Assistent.\n\nSende `{self.config.command_prefix}help` für Hilfe." | ||
| 899 | ) | ||
| 900 | except Exception as e: | ||
| 901 | logger.error(f"❌ Join error: {e}") | ||
| 902 | |||
| 903 | async def handle_command(self, room: MatrixRoom, event: RoomMessageText, message: str): | ||
| 904 | """Handle commands""" | ||
| 905 | parts = message[1:].split(maxsplit=1) | ||
| 906 | command = parts[0].lower() | ||
| 907 | args = parts[1] if len(parts) > 1 else "" | ||
| 908 | |||
| 909 | if command == "help": | ||
| 910 | await self.cmd_help(room) | ||
| 911 | elif command == "ping": | ||
| 912 | await self.send_message(room.room_id, "🏓 Pong!") | ||
| 913 | elif command == "status": | ||
| 914 | await self.cmd_status(room) | ||
| 915 | elif command == "chat": | ||
| 916 | await self.handle_chat(room, event, args) | ||
| 917 | elif command == "reset": | ||
| 918 | self.chat_history[room.room_id] = [] | ||
| 919 | await self.send_message(room.room_id, "🔄 Chat-Verlauf gelöscht!") | ||
| 920 | else: | ||
| 921 | await self.send_message( | ||
| 922 | room.room_id, | ||
| 923 | f"❌ Unbekannter Command: {command}\nSende `!help` für Hilfe." | ||
| 924 | ) | ||
| 925 | |||
| 926 | async def cmd_help(self, room: MatrixRoom): | ||
| 927 | """Help command""" | ||
| 928 | help_text = f"""📚 Jean Bot - Hilfe | ||
| 929 | |||
| 930 | Commands: | ||
| 931 | • {self.config.command_prefix}help - Diese Hilfe | ||
| 932 | • {self.config.command_prefix}ping - Test | ||
| 933 | • {self.config.command_prefix}chat <text> - Chat mit KI | ||
| 934 | • {self.config.command_prefix}reset - Chat-Verlauf löschen | ||
| 935 | • {self.config.command_prefix}status - Bot-Status | ||
| 936 | |||
| 937 | Multimedia: | ||
| 938 | • 🖼️ Bilder → Automatische Analyse | ||
| 939 | • 🎵 Audio → Transkription | ||
| 940 | • 📄 Dateien → Dokument-Analyse (PDF, DOCX, PPTX) | ||
| 941 | |||
| 942 | Tipps: | ||
| 943 | • In DMs kannst du direkt ohne Command schreiben | ||
| 944 | • In Gruppen erwähne mich mit @jean | ||
| 945 | • Sende Bilder für Vision-Analyse | ||
| 946 | """ | ||
| 947 | await self.send_message(room.room_id, help_text) | ||
| 948 | |||
| 949 | async def cmd_status(self, room: MatrixRoom): | ||
| 950 | """Status command""" | ||
| 951 | try: | ||
| 952 | import psutil | ||
| 953 | process = psutil.Process() | ||
| 954 | memory_mb = process.memory_info().rss / 1024 / 1024 | ||
| 955 | memory_info = f"💾 Memory: {memory_mb:.1f} MB\n" | ||
| 956 | except: | ||
| 957 | memory_info = "" | ||
| 958 | |||
| 959 | ollama_status = "✅ Online" | ||
| 960 | try: | ||
| 961 | models = await self.ollama_client.list_models() | ||
| 962 | model_info = f"\nModelle: {', '.join(models[:3])}" if models else "" | ||
| 963 | except: | ||
| 964 | ollama_status = "❌ Offline" | ||
| 965 | model_info = "" | ||
| 966 | |||
| 967 | status_text = f"""📊 Bot Status | ||
| 968 | |||
| 969 | 🤖 Jean Bot: Online | ||
| 970 | 🏠 Homeserver: {self.config.homeserver} | ||
| 971 | 👤 User: {self.config.user} | ||
| 972 | 📝 Räume: {len(self.client.rooms)} | ||
| 973 | |||
| 974 | 🧠 Ollama: {ollama_status}{model_info} | ||
| 975 | |||
| 976 | {memory_info} | ||
| 977 | """ | ||
| 978 | await self.send_message(room.room_id, status_text) | ||
| 979 | |||
| 980 | async def handle_chat(self, room: MatrixRoom, event: RoomMessageText, message: str): | ||
| 981 | """Chat with AI""" | ||
| 982 | try: | ||
| 983 | message = message.replace(self.config.user, "").strip() | ||
| 984 | |||
| 985 | if not message: | ||
| 986 | return | ||
| 987 | |||
| 988 | if room.room_id not in self.chat_history: | ||
| 989 | self.chat_history[room.room_id] = [] | ||
| 990 | |||
| 991 | history = self.chat_history[room.room_id] | ||
| 992 | history.append({"role": "user", "content": message}) | ||
| 993 | |||
| 994 | if len(history) > 10: | ||
| 995 | history = history[-10:] | ||
| 996 | |||
| 997 | await self.client.room_typing(room.room_id, typing_state=True) | ||
| 998 | |||
| 999 | response = await self.ollama_client.chat( | ||
| 1000 | messages=history, | ||
| 1001 | model=self.config.model | ||
| 1002 | ) | ||
| 1003 | |||
| 1004 | reply = response.get('message', {}).get('content', 'Keine Antwort erhalten') | ||
| 1005 | |||
| 1006 | history.append({"role": "assistant", "content": reply}) | ||
| 1007 | self.chat_history[room.room_id] = history | ||
| 1008 | |||
| 1009 | await self.send_message(room.room_id, reply) | ||
| 1010 | |||
| 1011 | except Exception as e: | ||
| 1012 | logger.error(f"❌ Chat error: {e}") | ||
| 1013 | await self.send_message(room.room_id, f"❌ Fehler: {str(e)}") | ||
| 1014 | finally: | ||
| 1015 | await self.client.room_typing(room.room_id, typing_state=False) | ||
| 1016 | |||
| 1017 | async def send_message(self, room_id: str, message: str): | ||
| 1018 | """Send message""" | ||
| 1019 | try: | ||
| 1020 | await self.client.room_send( | ||
| 1021 | room_id=room_id, | ||
| 1022 | message_type="m.room.message", | ||
| 1023 | content={ | ||
| 1024 | "msgtype": "m.text", | ||
| 1025 | "body": message | ||
| 1026 | } | ||
| 1027 | ) | ||
| 1028 | except Exception as e: | ||
| 1029 | logger.error(f"❌ Send error: {e}") | ||
| 1030 | |||
| 1031 | async def _cleanup_loop(self): | ||
| 1032 | """Periodic cleanup""" | ||
| 1033 | while True: | ||
| 1034 | try: | ||
| 1035 | await asyncio.sleep(3600) | ||
| 1036 | await self.media_handler.cleanup_old_files(max_age_hours=24) | ||
| 1037 | except Exception as e: | ||
| 1038 | logger.error(f"Cleanup error: {e}") | ||
| 1039 | |||
| 1040 | |||
| 1041 | async def main(): | ||
| 1042 | """Main function""" | ||
| 1043 | bot = JeanBot() | ||
| 1044 | await bot.start() | ||
| 1045 | |||
| 1046 | |||
| 1047 | if ~_~_name~_~_ == "~_~_main~_~_": | ||
| 1048 | try: | ||
| 1049 | asyncio.run(main()) | ||
| 1050 | except KeyboardInterrupt: | ||
| 1051 | logger.info("👋 Bot wird beendet...") | ||
| 1052 | EOF | ||
| 1053 | |||
| 1054 | # ============================================ | ||
| 1055 | # 6. ~_~_init~_~_ FILES | ||
| 1056 | # ============================================ | ||
| 1057 | cat > jean/~_~_init~_~_.py << 'EOF' | ||
| 1058 | """Jean Matrix Bot Package""" | ||
| 1059 | ~_~_version~_~_ = "1.0.0" | ||
| 1060 | EOF | ||
| 1061 | |||
| 1062 | cat > jean/core/~_~_init~_~_.py << 'EOF' | ||
| 1063 | """Jean Core Package""" | ||
| 1064 | EOF | ||
| 1065 | |||
| 1066 | cat > jean/utils/~_~_init~_~_.py << 'EOF' | ||
| 1067 | """Jean Utils Package""" | ||
| 1068 | from .ollama_client import OllamaClient | ||
| 1069 | |||
| 1070 | ~_~_all~_~_ = ['OllamaClient'] | ||
| 1071 | EOF | ||
| 1072 | |||
| 1073 | cat > jean/plugins/~_~_init~_~_.py << 'EOF' | ||
| 1074 | """Jean Plugins Package""" | ||
| 1075 | EOF | ||
| 1076 | |||
| 1077 | cat > jean/plugins/builtin/~_~_init~_~_.py << 'EOF' | ||
| 1078 | """Builtin Plugins""" | ||
| 1079 | EOF | ||
| 1080 | |||
| 1081 | cat > jean/plugins/custom/~_~_init~_~_.py << 'EOF' | ||
| 1082 | """Custom Plugins""" | ||
| 1083 | EOF | ||
| 1084 | |||
| 1085 | # ============================================ | ||
| 1086 | # 7. .env.example | ||
| 1087 | # ============================================ | ||
| 1088 | cat > .env.example << 'EOF' | ||
| 1089 | # ======================================== | ||
| 1090 | # Jean Matrix Bot Configuration | ||
| 1091 | # ======================================== | ||
| 1092 | |||
| 1093 | # SETUP: Run python3 get_token.py to auto-fill these values | ||
| 1094 | |||
| 1095 | # Matrix Server Configuration | ||
| 1096 | MATRIX_HOMESERVER=https:~/~/matrix.rs-servertech.com | ||
| 1097 | MATRIX_USER=@jean:matrix.rs-servertech.com | ||
| 1098 | MATRIX_ACCESS_TOKEN=your_token_here | ||
| 1099 | MATRIX_DEVICE_ID=JEAN_BOT_001 | ||
| 1100 | |||
| 1101 | # Alternative: Use Password (not recommended) | ||
| 1102 | # MATRIX_PASSWORD=your_password_here | ||
| 1103 | |||
| 1104 | # Ollama AI Configuration | ||
| 1105 | OLLAMA_URL=http:~/~/192.168.10.210:11434 | ||
| 1106 | OLLAMA_MODEL=llama3.2:latest | ||
| 1107 | OLLAMA_VISION_MODEL=llava:latest | ||
| 1108 | |||
| 1109 | # Bot Settings | ||
| 1110 | COMMAND_PREFIX=! | ||
| 1111 | LOG_LEVEL=INFO | ||
| 1112 | DATABASE_URL=sqlite:~/~//data/jean.db | ||
| 1113 | |||
| 1114 | # Admin Users (comma-separated) | ||
| 1115 | ADMIN_USERS=@admin:matrix.rs-servertech.com | ||
| 1116 | |||
| 1117 | # Feature Flags | ||
| 1118 | ENABLE_IMAGE_ANALYSIS=true | ||
| 1119 | ENABLE_VOICE_TRANSCRIPTION=false | ||
| 1120 | ENABLE_DOCUMENT_ANALYSIS=true | ||
| 1121 | |||
| 1122 | # Rate Limiting | ||
| 1123 | RATE_LIMIT_MESSAGES=10 | ||
| 1124 | RATE_LIMIT_WINDOW=60 | ||
| 1125 | EOF | ||
| 1126 | |||
| 1127 | # ============================================ | ||
| 1128 | # 8. requirements.txt (KOMPLETT) | ||
| 1129 | # ============================================ | ||
| 1130 | cat > requirements.txt << 'EOF' | ||
| 1131 | # Matrix Client | ||
| 1132 | matrix-nio[e2e]==0.24.0 | ||
| 1133 | |||
| 1134 | # HTTP & Async | ||
| 1135 | aiohttp==3.9.1 | ||
| 1136 | aiofiles==23.2.1 | ||
| 1137 | |||
| 1138 | # Database | ||
| 1139 | sqlalchemy[asyncio]==2.0.23 | ||
| 1140 | aiosqlite==0.19.0 | ||
| 1141 | |||
| 1142 | # Configuration & Logging | ||
| 1143 | python-dotenv==1.0.0 | ||
| 1144 | pyyaml==6.0.1 | ||
| 1145 | python-json-logger==2.0.7 | ||
| 1146 | |||
| 1147 | # System Utils | ||
| 1148 | psutil==5.9.6 | ||
| 1149 | |||
| 1150 | # Document Processing | ||
| 1151 | PyPDF2==3.0.1 | ||
| 1152 | python-docx==1.1.0 | ||
| 1153 | python-pptx==0.6.23 | ||
| 1154 | beautifulsoup4==4.12.2 | ||
| 1155 | lxml==5.1.0 | ||
| 1156 | |||
| 1157 | # Image Processing | ||
| 1158 | Pillow==10.1.0 | ||
| 1159 | |||
| 1160 | # Data Processing | ||
| 1161 | pandas==2.1.4 | ||
| 1162 | numpy==1.26.2 | ||
| 1163 | |||
| 1164 | # Additional | ||
| 1165 | requests==2.31.0 | ||
| 1166 | EOF | ||
| 1167 | |||
| 1168 | # ============================================ | ||
| 1169 | # 9. setup.sh | ||
| 1170 | # ============================================ | ||
| 1171 | cat > setup.sh << 'EOFSETUP' | ||
| 1172 | #!/bin/bash | ||
| 1173 | set -e | ||
| 1174 | |||
| 1175 | echo "🚀 Jean Matrix Bot Setup" | ||
| 1176 | echo "" | ||
| 1177 | |||
| 1178 | # Check Python | ||
| 1179 | if ! command -v python3 &> /dev/null; then | ||
| 1180 | echo "❌ Python 3 is required but not installed." | ||
| 1181 | exit 1 | ||
| 1182 | fi | ||
| 1183 | |||
| 1184 | # Create venv | ||
| 1185 | if [ ! -d "venv" ]; then | ||
| 1186 | echo "📦 Creating virtual environment..." | ||
| 1187 | python3 -m venv venv | ||
| 1188 | fi | ||
| 1189 | |||
| 1190 | # Activate venv | ||
| 1191 | source venv/bin/activate | ||
| 1192 | |||
| 1193 | # Install dependencies | ||
| 1194 | echo "📥 Installing dependencies..." | ||
| 1195 | pip install -q ~-~-upgrade pip | ||
| 1196 | pip install -q -r requirements.txt | ||
| 1197 | |||
| 1198 | # Create .env if not exists | ||
| 1199 | if [ ! -f ".env" ]; then | ||
| 1200 | echo "📝 Creating .env file..." | ||
| 1201 | cp .env.example .env | ||
| 1202 | fi | ||
| 1203 | |||
| 1204 | echo "" | ||
| 1205 | echo "✅ Setup complete!" | ||
| 1206 | echo "" | ||
| 1207 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | ||
| 1208 | echo "📋 NEXT STEPS:" | ||
| 1209 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | ||
| 1210 | echo "" | ||
| 1211 | echo "🔑 OPTION 1: Auto-Generate Token (Recommended)" | ||
| 1212 | echo " python3 get_token.py" | ||
| 1213 | echo "" | ||
| 1214 | echo "🔧 OPTION 2: Manual Token" | ||
| 1215 | echo " 1. Get token from Element/Matrix client" | ||
| 1216 | echo " 2. Edit .env: nano .env" | ||
| 1217 | echo "" | ||
| 1218 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | ||
| 1219 | echo "" | ||
| 1220 | echo "🚀 After configuration, start with:" | ||
| 1221 | echo " source venv/bin/activate" | ||
| 1222 | echo " python -m jean.core.bot" | ||
| 1223 | echo "" | ||
| 1224 | EOFSETUP | ||
| 1225 | |||
| 1226 | chmod +x setup.sh | ||
| 1227 | |||
| 1228 | # ============================================ | ||
| 1229 | # 10. README.md | ||
| 1230 | # ============================================ | ||
| 1231 | cat > README.md << 'EOFREADME' | ||
| 1232 | # Jean Matrix Bot 🤖 | ||
| 1233 | |||
| 1234 | Vollständiger Matrix-Bot mit KI-Integration, Multimedia-Analyse und Plugin-System. | ||
| 1235 | |||
| 1236 | ~#~# ✨ Features | ||
| 1237 | |||
| 1238 | - 🤖 ~*~*KI-Chat~*~* via Ollama (LLaMA, Mistral, etc.) | ||
| 1239 | - 🖼️ ~*~*Bildanalyse~*~* mit Ollama Vision (LLaVA) | ||
| 1240 | - 🎵 ~*~*Audio-Transkription~*~* mit Whisper (optional) | ||
| 1241 | - 📄 ~*~*Dokument-Analyse~*~* (PDF, DOCX, PPTX) | ||
| 1242 | - 🔌 ~*~*Plugin-System~*~* - einfach erweiterbar | ||
| 1243 | - 🚪 ~*~*Auto-Join~*~* bei Einladungen | ||
| 1244 | - 💬 ~*~*Smart Chat~*~* - Erkennt DMs und Mentions | ||
| 1245 | |||
| 1246 | ~#~# 🚀 Quick Start | ||
| 1247 | |||
| 1248 | ~#~## Automatische Installation (Empfohlen) | ||
| 1249 | |||
| 1250 | ```bash | ||
| 1251 | # 1. Setup ausführen | ||
| 1252 | ./setup.sh | ||
| 1253 | |||
| 1254 | # 2. Token generieren (automatisch) | ||
| 1255 | source venv/bin/activate | ||
| 1256 | python3 get_token.py | ||
| 1257 | |||
| 1258 | # 3. Bot starten | ||
| 1259 | python -m jean.core.bot | ||
| 1260 | ``` | ||
| 1261 | |||
| 1262 | ~#~## Manuelle Installation | ||
| 1263 | |||
| 1264 | ```bash | ||
| 1265 | # 1. Virtual Environment | ||
| 1266 | python3 -m venv venv | ||
| 1267 | source venv/bin/activate | ||
| 1268 | |||
| 1269 | # 2. Dependencies | ||
| 1270 | pip install -r requirements.txt | ||
| 1271 | |||
| 1272 | # 3. Configuration | ||
| 1273 | cp .env.example .env | ||
| 1274 | nano .env # Token einfügen | ||
| 1275 | |||
| 1276 | # 4. Start | ||
| 1277 | python -m jean.core.bot | ||
| 1278 | ``` | ||
| 1279 | |||
| 1280 | ~#~# 🔑 Authentifizierung | ||
| 1281 | |||
| 1282 | ~#~## Auto-Generate Token (Empfohlen) | ||
| 1283 | |||
| 1284 | ```bash | ||
| 1285 | python3 get_token.py | ||
| 1286 | ``` | ||
| 1287 | |||
| 1288 | Das Script fragt interaktiv nach: | ||
| 1289 | - Homeserver URL | ||
| 1290 | - Username | ||
| 1291 | - Password | ||
| 1292 | - Device Name | ||
| 1293 | |||
| 1294 | Und schreibt den Token automatisch in `.env`! | ||
| 1295 | |||
| 1296 | ~#~## Manueller Token | ||
| 1297 | |||
| 1298 | ~1. In Element: Settings → Help & About → Advanced → Access Token | ||
| 1299 | 2. Token in `.env` einfügen | ||
| 1300 | |||
| 1301 | ~#~# 🎮 Commands | ||
| 1302 | |||
| 1303 | - `!help` - Zeigt alle verfügbaren Commands | ||
| 1304 | - `!ping` - Testet Bot-Antwort | ||
| 1305 | - `!chat <text>` - Chat mit KI | ||
| 1306 | - `!reset` - Chat-Verlauf löschen | ||
| 1307 | - `!status` - Bot-Status anzeigen | ||
| 1308 | |||
| 1309 | ~#~## Multimedia | ||
| 1310 | |||
| 1311 | - ~*~*Bilder senden~*~* → Automatische Analyse | ||
| 1312 | - ~*~*Audio senden~*~* → Transkription | ||
| 1313 | - ~*~*Dateien senden~*~* → Dokument-Analyse | ||
| 1314 | |||
| 1315 | ~#~# 📁 Projektstruktur | ||
| 1316 | |||
| 1317 | ``` | ||
| 1318 | jean-matrix-bot/ | ||
| 1319 | ├── jean/ | ||
| 1320 | │ ├── core/ # Bot-Kern | ||
| 1321 | │ ├── plugins/ # Erweiterungen | ||
| 1322 | │ └── utils/ # Hilfsfunktionen | ||
| 1323 | ├── config/ | ||
| 1324 | ├── data/ # Datenbank | ||
| 1325 | ├── logs/ # Log-Dateien | ||
| 1326 | ├── get_token.py # Token-Generator | ||
| 1327 | ├── setup.sh # Setup-Script | ||
| 1328 | ├── requirements.txt | ||
| 1329 | └── README.md | ||
| 1330 | ``` | ||
| 1331 | |||
| 1332 | ~#~# ⚙️ Konfiguration | ||
| 1333 | |||
| 1334 | Alle Einstellungen in `.env`: | ||
| 1335 | |||
| 1336 | ```env | ||
| 1337 | MATRIX_HOMESERVER=https:~/~/your-server.com | ||
| 1338 | MATRIX_USER=@jean:your-server.com | ||
| 1339 | MATRIX_ACCESS_TOKEN=syt_xxxxx | ||
| 1340 | OLLAMA_URL=http:~/~/localhost:11434 | ||
| 1341 | ``` | ||
| 1342 | |||
| 1343 | ~#~# 🔧 Troubleshooting | ||
| 1344 | |||
| 1345 | ~*~*Bot antwortet nicht:~*~* | ||
| 1346 | ```bash | ||
| 1347 | # Prüfe Token | ||
| 1348 | python3 get_token.py | ||
| 1349 | |||
| 1350 | # Prüfe Ollama | ||
| 1351 | curl http:~/~/localhost:11434/api/tags | ||
| 1352 | ``` | ||
| 1353 | |||
| 1354 | ~*~*Bildanalyse funktioniert nicht:~*~* | ||
| 1355 | ```bash | ||
| 1356 | # Installiere llava model | ||
| 1357 | ollama pull llava | ||
| 1358 | ``` | ||
| 1359 | |||
| 1360 | ~#~# 📋 System-Anforderungen | ||
| 1361 | |||
| 1362 | - Python 3.11+ | ||
| 1363 | - 2GB RAM (minimal) | ||
| 1364 | - Ollama Server (für KI) | ||
| 1365 | - Matrix Homeserver Zugang | ||
| 1366 | |||
| 1367 | ~#~# 📝 Lizenz | ||
| 1368 | |||
| 1369 | MIT License | ||
| 1370 | |||
| 1371 | ~-~-- | ||
| 1372 | |||
| 1373 | ~*~*Made with ❤️ for Matrix~*~* | ||
| 1374 | EOFREADME | ||
| 1375 | |||
| 1376 | echo "" | ||
| 1377 | echo "✅ =====================================" | ||
| 1378 | echo "✅ PROJECT COMPLETE!" | ||
| 1379 | echo "✅ =====================================" | ||
| 1380 | echo "" | ||
| 1381 | echo "📦 Creating archive..." | ||
| 1382 | |||
| 1383 | cd .. | ||
| 1384 | tar -czf "${PROJECT}.tar.gz" "$PROJECT" | ||
| 1385 | |||
| 1386 | echo "" | ||
| 1387 | echo "✅ FINISHED!" | ||
| 1388 | echo "" | ||
| 1389 | echo "📁 Project: $PROJECT" | ||
| 1390 | echo "📦 Archive: ${PROJECT}.tar.gz" | ||
| 1391 | echo "" | ||
| 1392 | echo "📋 Created Files:" | ||
| 1393 | echo " ✅ get_token.py - Token Generator" | ||
| 1394 | echo " ✅ jean/core/config.py - Configuration" | ||
| 1395 | echo " ✅ jean/core/bot.py - Main Bot" | ||
| 1396 | echo " ✅ jean/core/media_handler.py - Media Handler (FIXED)" | ||
| 1397 | echo " ✅ jean/utils/ollama_client.py - Ollama Client" | ||
| 1398 | echo " ✅ setup.sh - Setup Script" | ||
| 1399 | echo " ✅ requirements.txt - Dependencies" | ||
| 1400 | echo " ✅ README.md - Documentation" | ||
| 1401 | echo "" | ||
| 1402 | echo "🚀 Quick Start:" | ||
| 1403 | echo " cd $PROJECT" | ||
| 1404 | echo " ./setup.sh" | ||
| 1405 | echo " python3 get_token.py" | ||
| 1406 | echo " python -m jean.core.bot" | ||
| 1407 | echo "" | ||
| 1408 | |||
| 1409 | ENDOFSCRIPT | ||
| 1410 | |||
| 1411 | chmod +x create_jean_complete_fixed.sh | ||
| 1412 | ./create_jean_complete_fixed.sh | ||
| 1413 | |||
| 1414 | |||
| 1415 | |||
| 1416 | |||
| 1417 | bash | ||
| 1418 | |||
| 1419 | {{{# 1. Test ob Matrix-Server erreichbar ist | ||
| 1420 | curl -I https://matrix.rs-servertech.com | ||
| 1421 | |||
| 1422 | # 2. Test Login mit curl | ||
| 1423 | curl -X POST https://matrix.rs-servertech.com/_matrix/client/v3/login \ | ||
| 1424 | -H "Content-Type: application/json" \ | ||
| 1425 | -d '{ | ||
| 1426 | "type": "m.login.password", | ||
| 1427 | "user": "jean", | ||
| 1428 | "password": "DEIN_PASSWORD" | ||
| 1429 | }'}}} |
