Matrix-Bot-Jean

Last modified by Renรฉ Schmidt on 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"
  }'
    

Applications

Need help?

If you need help with XWiki you can contact: