Wiki source code of Matrix-Bot-Jean

Last modified by René Schmidt on 2025/10/19 20:54

Hide last authors
René Schmidt 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 }'}}}