Matrix-Bot-Jean

Zuletzt geändert von René Schmidt am 2025/10/19 20:54

#!/bin/bash
set -e

cd /root

cat > create_jean_complete_fixed.sh << 'ENDOFSCRIPT'
#!/bin/bash
set -e

PROJECT="jean-matrix-bot-complete"
echo "🚀 Creating Complete Jean Matrix Bot with all fixes..."

rm -rf "$PROJECT"
mkdir -p "$PROJECT"
cd "$PROJECT"

# ============================================
# Verzeichnisstruktur
# ============================================
mkdir -p jean/{core,plugins/{builtin,custom},utils}
mkdir -p config data logs

# ============================================
# 1. TOKEN GENERATOR (VERBESSERT)
# ============================================
cat > get_token.py << 'PYEOF'
#!/usr/bin/env python3
"""
Matrix Access Token Generator - Improved Version
Generiert einen Access Token für den Jean Bot
"""
import asyncio
import sys
from nio import AsyncClient, LoginResponse, LoginError

async def get_token(homeserver: str, username: str, password: str, device_name: str = "Jean Bot"):
    """Generate access token"""
    # Ensure @ prefix
    if not username.startswith("@"):
        username = f"@{username}"
   
    # Remove trailing slash from homeserver
    homeserver = homeserver.rstrip('/')
   
    print(f"\n🔐 Attempting login...")
    print(f"   Homeserver: {homeserver}")
    print(f"   Username: {username}")
    print(f"   Device: {device_name}")
    print()
   
    client = AsyncClient(homeserver, username)
   
    try:
        # Attempt login
        response = await client.login(password, device_name=device_name)
       
        if isinstance(response, LoginResponse):
            print("✅ Login successful!")
            print()
            print("="*70)
            print("📋 YOUR ACCESS TOKEN:")
            print("="*70)
            print()
            print(f"MATRIX_ACCESS_TOKEN={response.access_token}")
            print(f"MATRIX_DEVICE_ID={response.device_id}")
            print(f"MATRIX_USER={username}")
            print(f"MATRIX_HOMESERVER={homeserver}")
            print()
            print("="*70)
            print()
           
            # Try to update .env file
            try:
                import os
                if os.path.exists('.env'):
                    with open('.env', 'r') as f:
                        lines = f.readlines()
                   
                    # Update or add lines
                    updated_lines = []
                    keys_updated = set()
                   
                    for line in lines:
                        if line.startswith('MATRIX_ACCESS_TOKEN='):
                            updated_lines.append(f'MATRIX_ACCESS_TOKEN={response.access_token}\n')
                            keys_updated.add('token')
                        elif line.startswith('MATRIX_DEVICE_ID='):
                            updated_lines.append(f'MATRIX_DEVICE_ID={response.device_id}\n')
                            keys_updated.add('device')
                        elif line.startswith('MATRIX_USER='):
                            updated_lines.append(f'MATRIX_USER={username}\n')
                            keys_updated.add('user')
                        elif line.startswith('MATRIX_HOMESERVER='):
                            updated_lines.append(f'MATRIX_HOMESERVER={homeserver}\n')
                            keys_updated.add('homeserver')
                        else:
                            updated_lines.append(line)
                   
                    # Add missing keys
                    if 'token' not in keys_updated:
                        updated_lines.append(f'MATRIX_ACCESS_TOKEN={response.access_token}\n')
                    if 'device' not in keys_updated:
                        updated_lines.append(f'MATRIX_DEVICE_ID={response.device_id}\n')
                    if 'user' not in keys_updated:
                        updated_lines.append(f'MATRIX_USER={username}\n')
                    if 'homeserver' not in keys_updated:
                        updated_lines.append(f'MATRIX_HOMESERVER={homeserver}\n')
                   
                    # Write back
                    with open('.env', 'w') as f:
                        f.writelines(updated_lines)
                   
                    print("✅ .env file updated automatically!")
                    print()
                else:
                    print("ℹ️  No .env file found.")
                    print("   Create one with: cp .env.example .env")
                    print()
                   
            except Exception as e:
                print(f"⚠️  Could not update .env: {e}")
                print("   Please copy the values above manually.")
                print()
           
            return True
           
        elif isinstance(response, LoginError):
            print("❌ Login failed!")
            print(f"   Error: {response.message}")
            print(f"   Status: {response.status_code}")
            print()
           
            if response.status_code == 403:
                print("💡 Possible reasons:")
                print("   - Wrong password")
                print("   - Account locked")
                print("   - User doesn't exist")
            elif response.status_code == 429:
                print("💡 Too many login attempts. Wait a moment and try again.")
            else:
                print(f"💡 HTTP Status: {response.status_code}")
           
            return False
        else:
            print(f"❌ Unexpected response: {type(response)}")
            print(f"   {response}")
            return False
           
    except Exception as e:
        print(f"❌ Error during login: {e}")
        print()
        import traceback
        traceback.print_exc()
        return False
       
    finally:
        await client.close()

def main():
    print()
    print("="*70)
    print("🤖 Jean Matrix Bot - Token Generator")
    print("="*70)
    print()
   
    # Check for command line arguments
    if len(sys.argv) >= 4:
        homeserver = sys.argv[1]
        username = sys.argv[2]
        password = sys.argv[3]
        device_name = sys.argv[4] if len(sys.argv) > 4 else "Jean Bot"
       
        print("📋 Using command line arguments")
    else:
        # Interactive mode
        print("📋 Interactive Mode")
        print()
       
        homeserver = input("Homeserver URL (e.g. https://matrix.org): ").strip()
        if not homeserver:
            print("❌ Homeserver is required!")
            sys.exit(1)
       
        username = input("Username (e.g. @jean:matrix.org or jean): ").strip()
        if not username:
            print("❌ Username is required!")
            sys.exit(1)
       
        password = input("Password: ").strip()
        if not password:
            print("❌ Password is required!")
            sys.exit(1)
       
        device_name = input("Device Name [Jean Bot]: ").strip() or "Jean Bot"
   
    # Run async function
    success = asyncio.run(get_token(homeserver, username, password, device_name))
   
    if success:
        print("🚀 Next steps:")
        print("   1. Run: python -m jean.core.bot")
        print("   2. Invite the bot to a room")
        print("   3. Send: !help")
        print()
        sys.exit(0)
    else:
        print("❌ Token generation failed.")
        print()
        print("💡 Troubleshooting:")
        print("   1. Check homeserver URL (must start with https://)")
        print("   2. Verify username format (@user:server.com)")
        print("   3. Confirm password is correct")
        print("   4. Check if user exists on the homeserver")
        print()
        sys.exit(1)

if __name__ == "__main__":
    main()
PYEOF

chmod +x get_token.py

# ============================================
# 2. CONFIGURATION
# ============================================
cat > jean/core/config.py << 'EOF'
"""
Configuration Manager für Jean Bot
"""
import os
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()

class Config:
    """Bot Configuration"""
   
    def __init__(self):
        # Matrix Config
        self.homeserver = os.getenv('MATRIX_HOMESERVER')
        self.user = os.getenv('MATRIX_USER')
        self.access_token = os.getenv('MATRIX_ACCESS_TOKEN')
        self.device_id = os.getenv('MATRIX_DEVICE_ID', 'JEAN_BOT')
        self.password = os.getenv('MATRIX_PASSWORD')
       
        # Ollama Config
        self.ollama_url = os.getenv('OLLAMA_URL', 'http://localhost:11434')
        self.model = os.getenv('OLLAMA_MODEL', 'llama3.2:latest')
        self.vision_model = os.getenv('OLLAMA_VISION_MODEL', 'llava:latest')
       
        # Bot Settings
        self.command_prefix = os.getenv('COMMAND_PREFIX', '!')
        self.log_level = os.getenv('LOG_LEVEL', 'INFO')
       
        # Database
        self.database_url = os.getenv('DATABASE_URL', 'sqlite:///data/jean.db')
       
        # Admin Users
        admin_str = os.getenv('ADMIN_USERS', '')
        self.admin_users = [u.strip() for u in admin_str.split(',') if u.strip()]
       
        # Feature Flags
        self.enable_image_analysis = os.getenv('ENABLE_IMAGE_ANALYSIS', 'true').lower() == 'true'
        self.enable_voice_transcription = os.getenv('ENABLE_VOICE_TRANSCRIPTION', 'false').lower() == 'true'
        self.enable_document_analysis = os.getenv('ENABLE_DOCUMENT_ANALYSIS', 'true').lower() == 'true'
       
        # Validate
        self._validate()
   
    def _validate(self):
        """Validate required config"""
        required = {
            'homeserver': self.homeserver,
            'user': self.user,
        }
       
        if not self.access_token and not self.password:
            required['access_token or password'] = None
       
        missing = [k for k, v in required.items() if not v]
       
        if missing:
            raise ValueError(f"Missing required config: {', '.join(missing)}")
EOF

# ============================================
# 3. OLLAMA CLIENT
# ============================================
cat > jean/utils/ollama_client.py << 'EOF'
"""
Ollama Client für Jean Bot
"""
import aiohttp
import logging
from typing import List, Dict, Optional

logger = logging.getLogger(__name__)

class OllamaClient:
    """Client für Ollama API"""
   
    def __init__(self, base_url: str, model: str, vision_model: str = None):
        self.base_url = base_url.rstrip('/')
        self.model = model
        self.vision_model = vision_model or model
       
    async def generate(
        self, 
        prompt: str, 
        model: Optional[str] = None,
        images: Optional[List[str]] = None,
        system: Optional[str] = None
    ) -> Dict:
        """Generate completion"""
        try:
            url = f"{self.base_url}/api/generate"
           
            payload = {
                "model": model or self.model,
                "prompt": prompt,
                "stream": False
            }
           
            if images:
                payload["images"] = images
               
            if system:
                payload["system"] = system
           
            async with aiohttp.ClientSession() as session:
                async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp:
                    if resp.status != 200:
                        error_text = await resp.text()
                        raise Exception(f"Ollama API error {resp.status}: {error_text}")
                   
                    return await resp.json()
                   
        except Exception as e:
            logger.error(f"Ollama generate error: {e}")
            raise
   
    async def chat(
        self,
        messages: List[Dict[str, str]],
        model: Optional[str] = None,
        system: Optional[str] = None
    ) -> Dict:
        """Chat completion"""
        try:
            url = f"{self.base_url}/api/chat"
           
            payload = {
                "model": model or self.model,
                "messages": messages,
                "stream": False
            }
           
            if system:
                payload["messages"].insert(0, {"role": "system", "content": system})
           
            async with aiohttp.ClientSession() as session:
                async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp:
                    if resp.status != 200:
                        error_text = await resp.text()
                        raise Exception(f"Ollama API error {resp.status}: {error_text}")
                   
                    return await resp.json()
                   
        except Exception as e:
            logger.error(f"Ollama chat error: {e}")
            raise
   
    async def list_models(self) -> List[str]:
        """List available models"""
        try:
            url = f"{self.base_url}/api/tags"
           
            async with aiohttp.ClientSession() as session:
                async with session.get(url) as resp:
                    if resp.status != 200:
                        return []
                   
                    data = await resp.json()
                    return [m['name'] for m in data.get('models', [])]
                   
        except Exception as e:
            logger.error(f"List models error: {e}")
            return []
EOF

# ============================================
# 4. MEDIA HANDLER (FIXED)
# ============================================
cat > jean/core/media_handler.py << 'EOF'
"""
Fixed Media Handler für Jean Matrix Bot
Behebt Download-Probleme für Bilder, Audio, PDFs und DOCX
"""
import asyncio
import aiohttp
import aiofiles
import logging
from pathlib import Path
from typing import Optional, Tuple
from nio import AsyncClient, DownloadError, DownloadResponse
import mimetypes

logger = logging.getLogger(__name__)

class MediaHandler:
    """Handhabt Media-Downloads mit verbessertem Error-Handling"""
   
    def __init__(self, client: AsyncClient, temp_dir: str = "/tmp/jean_media"):
        self.client = client
        self.temp_dir = Path(temp_dir)
        self.temp_dir.mkdir(parents=True, exist_ok=True)
       
    async def download_media(
        self, 
        mxc_url: str, 
        filename: Optional[str] = None
    ) -> Optional[Tuple[Path, str]]:
        """Download media from mxc:// URL"""
        if not mxc_url or not mxc_url.startswith("mxc://"):
            logger.error(f"Ungültige MXC URL: {mxc_url}")
            return None
           
        try:
            logger.info(f"📥 Downloading: {mxc_url}")
           
            parts = mxc_url.replace("mxc://", "").split("/")
            if len(parts) < 2:
                logger.error(f"Fehlerhafte MXC URL Struktur: {mxc_url}")
                return None
               
            server_name = parts[0]
            media_id = "/".join(parts[1:])
           
            # Method 1: nio download
            response = await self.client.download(
                server_name=server_name,
                media_id=media_id
            )
           
            if isinstance(response, DownloadError):
                logger.warning(f"nio download fehlgeschlagen: {response.message}")
                return await self._download_via_http(mxc_url, filename)
               
            if not isinstance(response, DownloadResponse):
                logger.error(f"Unerwartete Response: {type(response)}")
                return await self._download_via_http(mxc_url, filename)
           
            if not filename:
                filename = f"media_{media_id.replace('/', '_')}"
           
            mime_type = response.content_type or "application/octet-stream"
           
            ext = mimetypes.guess_extension(mime_type)
            if ext and not filename.endswith(ext):
                filename = f"{filename}{ext}"
           
            filepath = self.temp_dir / filename
           
            async with aiofiles.open(filepath, 'wb') as f:
                await f.write(response.body)
           
            file_size = len(response.body) / 1024
            logger.info(f"✅ Download erfolgreich: {filename} ({file_size:.2f} KB)")
           
            return filepath, mime_type
           
        except Exception as e:
            logger.error(f"❌ Download fehlgeschlagen für {mxc_url}: {e}")
            return await self._download_via_http(mxc_url, filename)
   
    async def _download_via_http(
        self, 
        mxc_url: str, 
        filename: Optional[str] = None
    ) -> Optional[Tuple[Path, str]]:
        """Fallback: HTTP Download"""
        try:
            parts = mxc_url.replace("mxc://", "").split("/", 1)
            if len(parts) < 2:
                return None
               
            server_name, media_id = parts
           
            homeserver = self.client.homeserver
            download_url = f"{homeserver}/_matrix/media/v3/download/{server_name}/{media_id}"
           
            logger.info(f"🔄 Fallback HTTP Download: {download_url}")
           
            async with aiohttp.ClientSession() as session:
                headers = {
                    "Authorization": f"Bearer {self.client.access_token}"
                }
               
                async with session.get(download_url, headers=headers) as resp:
                    if resp.status != 200:
                        logger.error(f"HTTP {resp.status}: {await resp.text()}")
                        return None
                   
                    content = await resp.read()
                    mime_type = resp.headers.get('Content-Type', 'application/octet-stream')
                   
                    if not filename:
                        content_disp = resp.headers.get('Content-Disposition', '')
                        if 'filename=' in content_disp:
                            filename = content_disp.split('filename=')[1].strip('"')
                        else:
                            filename = f"media_{media_id.replace('/', '_')}"
                   
                    ext = mimetypes.guess_extension(mime_type)
                    if ext and not filename.endswith(ext):
                        filename = f"{filename}{ext}"
                   
                    filepath = self.temp_dir / filename
                   
                    async with aiofiles.open(filepath, 'wb') as f:
                        await f.write(content)
                   
                    file_size = len(content) / 1024
                    logger.info(f"✅ HTTP Download erfolgreich: {filename} ({file_size:.2f} KB)")
                   
                    return filepath, mime_type
                   
        except Exception as e:
            logger.error(f"❌ HTTP Download fehlgeschlagen: {e}")
            return None
   
    async def cleanup_old_files(self, max_age_hours: int = 24):
        """Lösche alte temporäre Dateien"""
        import time
        cutoff = time.time() - (max_age_hours * 3600)
       
        for filepath in self.temp_dir.iterdir():
            if filepath.is_file() and filepath.stat().st_mtime < cutoff:
                try:
                    filepath.unlink()
                    logger.debug(f"🗑️ Gelöscht: {filepath.name}")
                except Exception as e:
                    logger.error(f"Fehler beim Löschen {filepath}: {e}")

class MediaEventHandler:
    """Event Handler für Media-Events"""
   
    def __init__(self, bot):
        self.bot = bot
        self.media_handler = MediaHandler(bot.client)
       
    async def handle_image(self, room, event):
        """Handler für Bild-Events"""
        try:
            logger.info(f"🖼️ Bild empfangen in {room.display_name}")
           
            result = await self.media_handler.download_media(event.url, event.body)
           
            if not result:
                await self.bot.send_message(room.room_id, "❌ Fehler beim Herunterladen des Bildes")
                return
           
            filepath, mime_type = result
            logger.info(f"✅ Bild gespeichert: {filepath}")
           
            analysis = await self._analyze_image(filepath)
            await self.bot.send_message(room.room_id, f"🔍 Bildanalyse:\n\n{analysis}")
           
        except Exception as e:
            logger.error(f"Fehler bei Bildverarbeitung: {e}")
            await self.bot.send_message(room.room_id, f"❌ Fehler: {str(e)}")
   
    async def handle_audio(self, room, event):
        """Handler für Audio-Events"""
        try:
            logger.info(f"🎵 Audio empfangen in {room.display_name}")
           
            result = await self.media_handler.download_media(event.url, event.body)
           
            if not result:
                await self.bot.send_message(room.room_id, "❌ Fehler beim Herunterladen der Audiodatei")
                return
           
            filepath, mime_type = result
            logger.info(f"✅ Audio gespeichert: {filepath}")
           
            transcription = await self._transcribe_audio(filepath)
            await self.bot.send_message(room.room_id, f"🎤 Transkription:\n\n{transcription}")
           
        except Exception as e:
            logger.error(f"Fehler bei Audioverarbeitung: {e}")
            await self.bot.send_message(room.room_id, f"❌ Fehler: {str(e)}")
   
    async def handle_file(self, room, event):
        """Handler für Datei-Events"""
        try:
            logger.info(f"📄 Datei empfangen: {event.body}")
           
            result = await self.media_handler.download_media(event.url, event.body)
           
            if not result:
                await self.bot.send_message(room.room_id, "❌ Fehler beim Herunterladen der Datei")
                return
           
            filepath, mime_type = result
            logger.info(f"✅ Datei gespeichert: {filepath}")
           
            if mime_type == 'application/pdf':
                analysis = await self._analyze_pdf(filepath)
            elif mime_type in ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/msword']:
                analysis = await self._analyze_docx(filepath)
            elif mime_type in ['application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.ms-powerpoint']:
                analysis = await self._analyze_pptx(filepath)
            else:
                analysis = f"📎 Datei empfangen: {event.body}\nTyp: {mime_type}\nGröße: {filepath.stat().st_size / 1024:.2f} KB"
           
            await self.bot.send_message(room.room_id, f"📄 Dokument-Analyse:\n\n{analysis}")
           
        except Exception as e:
            logger.error(f"Fehler bei Dateiverarbeitung: {e}")
            await self.bot.send_message(room.room_id, f"❌ Fehler: {str(e)}")
   
    async def _analyze_image(self, filepath: Path) -> str:
        """Analysiere Bild mit Vision Model"""
        try:
            import base64
           
            with open(filepath, 'rb') as f:
                image_data = base64.b64encode(f.read()).decode()
           
            response = await self.bot.ollama_client.generate(
                model=self.bot.config.vision_model,
                prompt="Beschreibe dieses Bild detailliert auf Deutsch.",
                images=[image_data]
            )
           
            return response.get('response', 'Keine Antwort erhalten')
           
        except Exception as e:
            logger.error(f"Vision-Analyse fehlgeschlagen: {e}")
            return f"❌ Bildanalyse fehlgeschlagen: {str(e)}"
   
    async def _transcribe_audio(self, filepath: Path) -> str:
        """Transkribiere Audio"""
        return "🚧 Audio-Transkription wird noch implementiert"
   
    async def _analyze_pdf(self, filepath: Path) -> str:
        """Analysiere PDF"""
        try:
            from PyPDF2 import PdfReader
           
            reader = PdfReader(str(filepath))
            num_pages = len(reader.pages)
           
            text = ""
            for i, page in enumerate(reader.pages[:3]):
                text += f"\n--- Seite {i+1} ---\n{page.extract_text()}\n"
           
            prompt = f"Fasse dieses PDF-Dokument zusammen:\n\n{text[:2000]}"
            response = await self.bot.ollama_client.generate(
                model=self.bot.config.model,
                prompt=prompt
            )
           
            summary = response.get('response', 'Keine Antwort')
           
            return f"📊 PDF: {filepath.name}\nSeiten: {num_pages}\n\nZusammenfassung:\n{summary}"
           
        except Exception as e:
            return f"❌ PDF-Analyse fehlgeschlagen: {str(e)}"
   
    async def _analyze_docx(self, filepath: Path) -> str:
        """Analysiere DOCX"""
        try:
            from docx import Document
           
            doc = Document(str(filepath))
            text = "\n".join([para.text for para in doc.paragraphs[:20]])
           
            prompt = f"Fasse dieses Word-Dokument zusammen:\n\n{text[:2000]}"
            response = await self.bot.ollama_client.generate(
                model=self.bot.config.model,
                prompt=prompt
            )
           
            summary = response.get('response', 'Keine Antwort')
           
            return f"📝 DOCX: {filepath.name}\nAbsätze: {len(doc.paragraphs)}\n\nZusammenfassung:\n{summary}"
           
        except Exception as e:
            return f"❌ DOCX-Analyse fehlgeschlagen: {str(e)}"
   
    async def _analyze_pptx(self, filepath: Path) -> str:
        """Analysiere PPTX"""
        try:
            from pptx import Presentation
           
            prs = Presentation(str(filepath))
            slides_text = []
           
            for i, slide in enumerate(prs.slides[:5], 1):
                text = []
                for shape in slide.shapes:
                    if hasattr(shape, "text"):
                        text.append(shape.text)
                slides_text.append(f"Folie {i}: {' '.join(text)}")
           
            content = "\n".join(slides_text)
           
            prompt = f"Fasse diese Präsentation zusammen:\n\n{content[:2000]}"
            response = await self.bot.ollama_client.generate(
                model=self.bot.config.model,
                prompt=prompt
            )
           
            summary = response.get('response', 'Keine Antwort')
           
            return f"📊 PPTX: {filepath.name}\nFolien: {len(prs.slides)}\n\nZusammenfassung:\n{summary}"
           
        except Exception as e:
            return f"❌ PPTX-Analyse fehlgeschlagen: {str(e)}"
EOF

# ============================================
# 5. MAIN BOT
# ============================================
cat > jean/core/bot.py << 'EOF'
"""
Jean Matrix Bot - Main Bot File
"""
import asyncio
import logging
from nio import (
    AsyncClient, 
    MatrixRoom, 
    RoomMessageText,
    RoomMessageImage,
    RoomMessageAudio,
    RoomMessageFile,
    InviteEvent,
    SyncResponse,
    LoginResponse
)
from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from jean.core.config import Config
from jean.core.media_handler import MediaHandler, MediaEventHandler
from jean.utils.ollama_client import OllamaClient

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

class JeanBot:
    """Jean Matrix Bot"""
   
    def __init__(self):
        self.config = Config()
        self.client = None
        self.ollama_client = OllamaClient(
            base_url=self.config.ollama_url,
            model=self.config.model,
            vision_model=self.config.vision_model
        )
        self.media_handler = None
        self.media_event_handler = None
        self.chat_history = {}
       
    async def setup(self):
        """Initialize bot"""
        logger.info("🚀 Jean Bot wird gestartet...")
       
        self.client = AsyncClient(
            self.config.homeserver,
            self.config.user
        )
       
        # Login if needed
        if self.config.access_token:
            self.client.access_token = self.config.access_token
            self.client.device_id = self.config.device_id
        elif self.config.password:
            logger.info("🔐 Logging in with password...")
            response = await self.client.login(self.config.password)
            if isinstance(response, LoginResponse):
                logger.info("✅ Login successful")
                logger.info(f"💾 Save this token to .env: {response.access_token}")
            else:
                raise Exception(f"Login failed: {response}")
        else:
            raise Exception("No access_token or password provided")
       
        # Initialize handlers
        self.media_handler = MediaHandler(self.client)
        self.media_event_handler = MediaEventHandler(self)
       
        # Register callbacks
        self.client.add_event_callback(self.on_message, RoomMessageText)
        self.client.add_event_callback(self.on_image, RoomMessageImage)
        self.client.add_event_callback(self.on_audio, RoomMessageAudio)
        self.client.add_event_callback(self.on_file, RoomMessageFile)
        self.client.add_event_callback(self.on_invite, InviteEvent)
       
        logger.info(f"✅ Bot konfiguriert als {self.config.user}")
       
    async def start(self):
        """Start bot"""
        try:
            await self.setup()
           
            sync_response = await self.client.sync(timeout=30000)
            if not isinstance(sync_response, SyncResponse):
                logger.error(f"❌ Sync fehlgeschlagen: {sync_response}")
                return
           
            logger.info("✅ Verbindung zu Matrix erfolgreich")
            logger.info(f"📝 In {len(self.client.rooms)} Räumen")
           
            asyncio.create_task(self._cleanup_loop())
           
            await self.sync_loop()
           
        except Exception as e:
            logger.error(f"❌ Start error: {e}", exc_info=True)
        finally:
            await self.client.close()
           
    async def sync_loop(self):
        """Main sync loop"""
        logger.info("🔄 Sync loop started")
       
        while True:
            try:
                await self.client.sync_forever(timeout=30000, full_state=True)
            except Exception as e:
                logger.error(f"❌ Sync error: {e}")
                await asyncio.sleep(5)
               
    async def on_message(self, room: MatrixRoom, event: RoomMessageText):
        """Text message handler"""
        if event.sender == self.client.user:
            return
           
        message = event.body.strip()
       
        is_dm = len(room.users) == 2
        is_mentioned = self.config.user in message
       
        if not (is_dm or is_mentioned or message.startswith(self.config.command_prefix)):
            return
           
        logger.info(f"💬 [{room.display_name}] {event.sender}: {message}")
       
        if message.startswith(self.config.command_prefix):
            await self.handle_command(room, event, message)
        else:
            await self.handle_chat(room, event, message)
   
    async def on_image(self, room: MatrixRoom, event: RoomMessageImage):
        """Image handler"""
        if event.sender == self.client.user:
            return
        logger.info(f"🖼️ Image received: {event.body}")
        await self.media_event_handler.handle_image(room, event)
   
    async def on_audio(self, room: MatrixRoom, event: RoomMessageAudio):
        """Audio handler"""
        if event.sender == self.client.user:
            return
        logger.info(f"🎵 Audio received: {event.body}")
        await self.media_event_handler.handle_audio(room, event)
   
    async def on_file(self, room: MatrixRoom, event: RoomMessageFile):
        """File handler"""
        if event.sender == self.client.user:
            return
        logger.info(f"📄 File received: {event.body}")
        await self.media_event_handler.handle_file(room, event)
   
    async def on_invite(self, room: MatrixRoom, event: InviteEvent):
        """Invite handler"""
        logger.info(f"📨 Invite from {event.sender} to {room.room_id}")
       
        try:
            await self.client.join(room.room_id)
            logger.info(f"✅ Joined room: {room.room_id}")
           
            await self.send_message(
                room.room_id,
                f"👋 Hallo! Ich bin Jean, dein KI-Assistent.\n\nSende `{self.config.command_prefix}help` für Hilfe."
            )
        except Exception as e:
            logger.error(f"❌ Join error: {e}")
   
    async def handle_command(self, room: MatrixRoom, event: RoomMessageText, message: str):
        """Handle commands"""
        parts = message[1:].split(maxsplit=1)
        command = parts[0].lower()
        args = parts[1] if len(parts) > 1 else ""
       
        if command == "help":
            await self.cmd_help(room)
        elif command == "ping":
            await self.send_message(room.room_id, "🏓 Pong!")
        elif command == "status":
            await self.cmd_status(room)
        elif command == "chat":
            await self.handle_chat(room, event, args)
        elif command == "reset":
            self.chat_history[room.room_id] = []
            await self.send_message(room.room_id, "🔄 Chat-Verlauf gelöscht!")
        else:
            await self.send_message(
                room.room_id,
                f"❌ Unbekannter Command: {command}\nSende `!help` für Hilfe."
            )
   
    async def cmd_help(self, room: MatrixRoom):
        """Help command"""
        help_text = f"""📚 Jean Bot - Hilfe

Commands:
• {self.config.command_prefix}help - Diese Hilfe
• {self.config.command_prefix}ping - Test
• {self.config.command_prefix}chat <text> - Chat mit KI
• {self.config.command_prefix}reset - Chat-Verlauf löschen
• {self.config.command_prefix}status - Bot-Status

Multimedia:
• 🖼️ Bilder → Automatische Analyse
• 🎵 Audio → Transkription
• 📄 Dateien → Dokument-Analyse (PDF, DOCX, PPTX)

Tipps:
• In DMs kannst du direkt ohne Command schreiben
• In Gruppen erwähne mich mit @jean
• Sende Bilder für Vision-Analyse
        """
        await self.send_message(room.room_id, help_text)
   
    async def cmd_status(self, room: MatrixRoom):
        """Status command"""
        try:
            import psutil
            process = psutil.Process()
            memory_mb = process.memory_info().rss / 1024 / 1024
            memory_info = f"💾 Memory: {memory_mb:.1f} MB\n"
        except:
            memory_info = ""
       
        ollama_status = "✅ Online"
        try:
            models = await self.ollama_client.list_models()
            model_info = f"\nModelle: {', '.join(models[:3])}" if models else ""
        except:
            ollama_status = "❌ Offline"
            model_info = ""
       
        status_text = f"""📊 Bot Status

🤖 Jean Bot: Online
🏠 Homeserver: {self.config.homeserver}
👤 User: {self.config.user}
📝 Räume: {len(self.client.rooms)}

🧠 Ollama: {ollama_status}{model_info}

{memory_info}
        """
        await self.send_message(room.room_id, status_text)
   
    async def handle_chat(self, room: MatrixRoom, event: RoomMessageText, message: str):
        """Chat with AI"""
        try:
            message = message.replace(self.config.user, "").strip()
           
            if not message:
                return
           
            if room.room_id not in self.chat_history:
                self.chat_history[room.room_id] = []
           
            history = self.chat_history[room.room_id]
            history.append({"role": "user", "content": message})
           
            if len(history) > 10:
                history = history[-10:]
           
            await self.client.room_typing(room.room_id, typing_state=True)
           
            response = await self.ollama_client.chat(
                messages=history,
                model=self.config.model
            )
           
            reply = response.get('message', {}).get('content', 'Keine Antwort erhalten')
           
            history.append({"role": "assistant", "content": reply})
            self.chat_history[room.room_id] = history
           
            await self.send_message(room.room_id, reply)
           
        except Exception as e:
            logger.error(f"❌ Chat error: {e}")
            await self.send_message(room.room_id, f"❌ Fehler: {str(e)}")
        finally:
            await self.client.room_typing(room.room_id, typing_state=False)
   
    async def send_message(self, room_id: str, message: str):
        """Send message"""
        try:
            await self.client.room_send(
                room_id=room_id,
                message_type="m.room.message",
                content={
                    "msgtype": "m.text",
                    "body": message
                }
            )
        except Exception as e:
            logger.error(f"❌ Send error: {e}")
   
    async def _cleanup_loop(self):
        """Periodic cleanup"""
        while True:
            try:
                await asyncio.sleep(3600)
                await self.media_handler.cleanup_old_files(max_age_hours=24)
            except Exception as e:
                logger.error(f"Cleanup error: {e}")

async def main():
    """Main function"""
    bot = JeanBot()
    await bot.start()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        logger.info("👋 Bot wird beendet...")
EOF

# ============================================
# 6. __init__ FILES
# ============================================
cat > jean/__init__.py << 'EOF'
"""Jean Matrix Bot Package"""
__version__ = "1.0.0"
EOF

cat > jean/core/__init__.py << 'EOF'
"""Jean Core Package"""
EOF

cat > jean/utils/__init__.py << 'EOF'
"""Jean Utils Package"""
from .ollama_client import OllamaClient

__all__ = ['OllamaClient']
EOF

cat > jean/plugins/__init__.py << 'EOF'
"""Jean Plugins Package"""
EOF

cat > jean/plugins/builtin/__init__.py << 'EOF'
"""Builtin Plugins"""
EOF

cat > jean/plugins/custom/__init__.py << 'EOF'
"""Custom Plugins"""
EOF

# ============================================
# 7. .env.example
# ============================================
cat > .env.example << 'EOF'
# ========================================
# Jean Matrix Bot Configuration
# ========================================

# SETUP: Run python3 get_token.py to auto-fill these values

# Matrix Server Configuration
MATRIX_HOMESERVER=https://matrix.rs-servertech.com
MATRIX_USER=@jean:matrix.rs-servertech.com
MATRIX_ACCESS_TOKEN=your_token_here
MATRIX_DEVICE_ID=JEAN_BOT_001

# Alternative: Use Password (not recommended)
# MATRIX_PASSWORD=your_password_here

# Ollama AI Configuration
OLLAMA_URL=http://192.168.10.210:11434
OLLAMA_MODEL=llama3.2:latest
OLLAMA_VISION_MODEL=llava:latest

# Bot Settings
COMMAND_PREFIX=!
LOG_LEVEL=INFO
DATABASE_URL=sqlite:///data/jean.db

# Admin Users (comma-separated)
ADMIN_USERS=@admin:matrix.rs-servertech.com

# Feature Flags
ENABLE_IMAGE_ANALYSIS=true
ENABLE_VOICE_TRANSCRIPTION=false
ENABLE_DOCUMENT_ANALYSIS=true

# Rate Limiting
RATE_LIMIT_MESSAGES=10
RATE_LIMIT_WINDOW=60
EOF

# ============================================
# 8. requirements.txt (KOMPLETT)
# ============================================
cat > requirements.txt << 'EOF'
# Matrix Client
matrix-nio[e2e]==0.24.0

# HTTP & Async
aiohttp==3.9.1
aiofiles==23.2.1

# Database
sqlalchemy[asyncio]==2.0.23
aiosqlite==0.19.0

# Configuration & Logging
python-dotenv==1.0.0
pyyaml==6.0.1
python-json-logger==2.0.7

# System Utils
psutil==5.9.6

# Document Processing
PyPDF2==3.0.1
python-docx==1.1.0
python-pptx==0.6.23
beautifulsoup4==4.12.2
lxml==5.1.0

# Image Processing
Pillow==10.1.0

# Data Processing
pandas==2.1.4
numpy==1.26.2

# Additional
requests==2.31.0
EOF

# ============================================
# 9. setup.sh
# ============================================
cat > setup.sh << 'EOFSETUP'
#!/bin/bash
set -e

echo "🚀 Jean Matrix Bot Setup"
echo ""

# Check Python
if ! command -v python3 &> /dev/null; then
    echo "❌ Python 3 is required but not installed."
    exit 1
fi

# Create venv
if [ ! -d "venv" ]; then
    echo "📦 Creating virtual environment..."
    python3 -m venv venv
fi

# Activate venv
source venv/bin/activate

# Install dependencies
echo "📥 Installing dependencies..."
pip install -q --upgrade pip
pip install -q -r requirements.txt

# Create .env if not exists
if [ ! -f ".env" ]; then
    echo "📝 Creating .env file..."
    cp .env.example .env
fi

echo ""
echo "✅ Setup complete!"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 NEXT STEPS:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🔑 OPTION 1: Auto-Generate Token (Recommended)"
echo "   python3 get_token.py"
echo ""
echo "🔧 OPTION 2: Manual Token"
echo "   1. Get token from Element/Matrix client"
echo "   2. Edit .env: nano .env"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🚀 After configuration, start with:"
echo "   source venv/bin/activate"
echo "   python -m jean.core.bot"
echo ""
EOFSETUP

chmod +x setup.sh

# ============================================
# 10. README.md
# ============================================
cat > README.md << 'EOFREADME'
# Jean Matrix Bot 🤖

Vollständiger Matrix-Bot mit KI-Integration, Multimedia-Analyse und Plugin-System.

## ✨ Features

- 🤖 **KI-Chat** via Ollama (LLaMA, Mistral, etc.)
- 🖼️ **Bildanalyse** mit Ollama Vision (LLaVA)
- 🎵 **Audio-Transkription** mit Whisper (optional)
- 📄 **Dokument-Analyse** (PDF, DOCX, PPTX)
- 🔌 **Plugin-System** - einfach erweiterbar
- 🚪 **Auto-Join** bei Einladungen
- 💬 **Smart Chat** - Erkennt DMs und Mentions

## 🚀 Quick Start

### Automatische Installation (Empfohlen)

```bash
# 1. Setup ausführen
./setup.sh

# 2. Token generieren (automatisch)
source venv/bin/activate
python3 get_token.py

# 3. Bot starten
python -m jean.core.bot
```

### Manuelle Installation

```bash
# 1. Virtual Environment
python3 -m venv venv
source venv/bin/activate

# 2. Dependencies
pip install -r requirements.txt

# 3. Configuration
cp .env.example .env
nano .env  # Token einfügen

# 4. Start
python -m jean.core.bot
```

## 🔑 Authentifizierung

### Auto-Generate Token (Empfohlen)

```bash
python3 get_token.py
```

Das Script fragt interaktiv nach:
- Homeserver URL
- Username
- Password
- Device Name

Und schreibt den Token automatisch in `.env`!

### Manueller Token

1. In Element: Settings → Help & About → Advanced → Access Token
2. Token in `.env` einfügen

## 🎮 Commands

- `!help` - Zeigt alle verfügbaren Commands
- `!ping` - Testet Bot-Antwort
- `!chat <text>` - Chat mit KI
- `!reset` - Chat-Verlauf löschen
- `!status` - Bot-Status anzeigen

### Multimedia

- **Bilder senden** → Automatische Analyse
- **Audio senden** → Transkription
- **Dateien senden** → Dokument-Analyse

## 📁 Projektstruktur

```
jean-matrix-bot/
├── jean/
│   ├── core/           # Bot-Kern
│   ├── plugins/        # Erweiterungen
│   └── utils/          # Hilfsfunktionen
├── config/
├── data/               # Datenbank
├── logs/               # Log-Dateien
├── get_token.py        # Token-Generator
├── setup.sh            # Setup-Script
├── requirements.txt
└── README.md
```

## ⚙️ Konfiguration

Alle Einstellungen in `.env`:

```env
MATRIX_HOMESERVER=https://your-server.com
MATRIX_USER=@jean:your-server.com
MATRIX_ACCESS_TOKEN=syt_xxxxx
OLLAMA_URL=http://localhost:11434
```

## 🔧 Troubleshooting

**Bot antwortet nicht:**
```bash
# Prüfe Token
python3 get_token.py

# Prüfe Ollama
curl http://localhost:11434/api/tags
```

**Bildanalyse funktioniert nicht:**
```bash
# Installiere llava model
ollama pull llava
```

## 📋 System-Anforderungen

- Python 3.11+
- 2GB RAM (minimal)
- Ollama Server (für KI)
- Matrix Homeserver Zugang

## 📝 Lizenz

MIT License

---

**Made with ❤️ for Matrix**
EOFREADME

echo ""
echo "✅ ====================================="
echo "✅ PROJECT COMPLETE!"
echo "✅ ====================================="
echo ""
echo "📦 Creating archive..."

cd ..
tar -czf "${PROJECT}.tar.gz" "$PROJECT"

echo ""
echo "✅ FINISHED!"
echo ""
echo "📁 Project: $PROJECT"
echo "📦 Archive: ${PROJECT}.tar.gz"
echo ""
echo "📋 Created Files:"
echo "  ✅ get_token.py - Token Generator"
echo "  ✅ jean/core/config.py - Configuration"
echo "  ✅ jean/core/bot.py - Main Bot"
echo "  ✅ jean/core/media_handler.py - Media Handler (FIXED)"
echo "  ✅ jean/utils/ollama_client.py - Ollama Client"
echo "  ✅ setup.sh - Setup Script"
echo "  ✅ requirements.txt - Dependencies"
echo "  ✅ README.md - Documentation"
echo ""
echo "🚀 Quick Start:"
echo "  cd $PROJECT"
echo "  ./setup.sh"
echo "  python3 get_token.py"
echo "  python -m jean.core.bot"
echo ""

ENDOFSCRIPT

chmod +x create_jean_complete_fixed.sh
./create_jean_complete_fixed.sh

bash

# 1. Test ob Matrix-Server erreichbar ist
curl -I https://matrix.rs-servertech.com

# 2. Test Login mit curl
curl -X POST https://matrix.rs-servertech.com/_matrix/client/v3/login \
  -H "Content-Type: application/json" \
  -d '{
    "type": "m.login.password",
    "user": "jean",
    "password": "DEIN_PASSWORD"
  }'
    

Anwendungen

Benötigen Sie Hilfe?

Wenn Sie Hilfe mit XWiki benötigen, wenden Sie sich an: