Matrix Bot Terry

Last modified by René Schmidt on 2025/10/11 19:30

import asyncio
import logging
import json
import random
import time
from datetime import datetime
from aiohttp import web, ClientSession
from nio import AsyncClient, MatrixRoom, RoomMessageText

# -------------------------------
# Logging-Setup
# -------------------------------
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)

# Reduziere Noise von anderen Loggern
logging.getLogger("nio").setLevel(logging.WARNING)
logging.getLogger("aiohttp").setLevel(logging.WARNING)

# -------------------------------
# Konfiguration - HIER ANPASSEN!
# -------------------------------
HOMESERVER = "https://matrix.rs-servertech.com"
USER_ID = "@terry:matrix.rs-servertech.com"
ACCESS_TOKEN = "HIER_DEIN_NEUER_TOKEN_EINTRAGEN"  # ← Hole Token mit curl!
DEVICE_ID = "MATRIXBOT"

# n8n Konfiguration
N8N_WEBHOOK_URL = "https://n8n.rs-servertech.com/webhook/matrix-bot"
WEBHOOK_PORT = 8080
WEBHOOK_SECRET = "mein-sicherer-key-2024-xyz"  # ← Ändere zu eigenem Secret!
DEFAULT_ROOM_ID = "!dYNoYyZHeJfkqfsSFH:matrix.rs-servertech.com"

# KI Konfiguration - Ollama (Selfhosted)
OLLAMA_URL = "http://localhost:11434"  # Deine Ollama URL
OLLAMA_MODEL = "llama3.2"  # Dein Modell (llama3.2, mistral, phi3, etc.)
AI_ENABLED = True  # Auf True für KI-Chat

# Bot-Verhalten
BOT_PREFIX = "!"
RESPOND_TO_ALL = False  # False = nur auf Erwähnungen/Direktnachrichten
RESPOND_TO_MENTIONS = True  # True = antwortet auf @terry
RESPOND_TO_DM = True  # True = antwortet in Privatnachrichten

# -------------------------------
# Globale Variablen
# -------------------------------
client: AsyncClient = None
app = web.Application()
conversation_history = {}

# -------------------------------
# KI-Integration (Ollama)
# -------------------------------
async def ask_ai(message: str, user_id: str) -> str:
    """Sendet Nachricht an Ollama und erhält Antwort."""
    if not AI_ENABLED:
        return None
   
    if user_id not in conversation_history:
        conversation_history[user_id] = []
   
    conversation_history[user_id].append({"role": "user", "content": message})
   
    # Limitiere History
    if len(conversation_history[user_id]) > 20:
        conversation_history[user_id] = conversation_history[user_id][-20:]
   
    try:
        # Ollama API Format
        messages = [
            {"role": "system", "content": "Du bist Terry, ein hilfreicher Matrix-Bot. Antworte kurz, freundlich und auf Deutsch. Sei persönlich und natürlich im Gespräch."},
            *conversation_history[user_id]
        ]
       
        payload = {
            "model": OLLAMA_MODEL,
            "messages": messages,
            "stream": False,
            "options": {
                "temperature": 0.7,
                "num_predict": 300  # Max Tokens
            }
        }
       
        async with ClientSession() as session:
            async with session.post(
                f"{OLLAMA_URL}/api/chat",
                json=payload,
                timeout=60  # Ollama kann länger brauchen
            ) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    answer = data["message"]["content"]
                   
                    conversation_history[user_id].append({"role": "assistant", "content": answer})
                   
                    logger.info(f"🧠 Ollama Antwort: {answer[:50]}...")
                    return answer
                else:
                    text = await resp.text()
                    logger.error(f"Ollama Fehler {resp.status}: {text}")
                    return None
                   
    except asyncio.TimeoutError:
        logger.error("Ollama Timeout - Modell lädt oder ist zu langsam")
        return "⏱️ Entschuldige, ich brauche gerade etwas länger zum Nachdenken..."
    except Exception as e:
        logger.error(f"KI-Fehler: {e}")
        return None

# -------------------------------
# Webhook-Handler (n8n → Bot)
# -------------------------------
async def webhook_handler(request):
    """Empfängt Messages von n8n."""
    try:
        data = await request.json()
       
        if data.get("secret") != WEBHOOK_SECRET:
            logger.warning(f"❌ Unauthorized webhook: {request.remote}")
            return web.json_response({"error": "Unauthorized"}, status=401)
       
        message = data.get("message")
        room_id = data.get("room_id", DEFAULT_ROOM_ID)
       
        if not message:
            return web.json_response({"error": "No message"}, status=400)
       
        logger.info(f"📥 n8n → Bot: {message[:50]}...")
       
        await client.room_send(
            room_id,
            "m.room.message",
            {"msgtype": "m.text", "body": message}
        )
       
        logger.info(f"✓ Message an {room_id} gesendet")
        return web.json_response({"status": "success", "room": room_id})
       
    except json.JSONDecodeError:
        return web.json_response({"error": "Invalid JSON"}, status=400)
    except Exception as e:
        logger.error(f"❌ Webhook Fehler: {e}")
        return web.json_response({"error": str(e)}, status=500)

async def health_check(request):
    """Health-Check Endpoint."""
    return web.json_response({
        "status": "online",
        "bot": USER_ID,
        "rooms": len(client.rooms) if client else 0,
        "ai_enabled": AI_ENABLED,
        "webhook_port": WEBHOOK_PORT,
        "n8n_configured": N8N_WEBHOOK_URL != "https://n8n.rs-servertech.com/webhook/matrix-bot"
    })

# -------------------------------
# n8n Trigger (Bot → n8n)
# -------------------------------
async def trigger_n8n(trigger_type: str, data: dict):
    """Sendet Event an n8n."""
    try:
        payload = {
            "trigger": trigger_type,
            "data": data,
            "timestamp": time.time()
        }
       
        async with ClientSession() as session:
            async with session.post(N8N_WEBHOOK_URL, json=payload, timeout=10) as resp:
                if resp.status == 200:
                    result = await resp.json()
                    logger.info(f"✓ n8n Trigger: {trigger_type}")
                    return result
                else:
                    logger.warning(f"n8n returned {resp.status}")
                    return None
    except Exception as e:
        logger.error(f"n8n Fehler: {e}")
        return None

# -------------------------------
# Command Handler
# -------------------------------
async def handle_command(room: MatrixRoom, event: RoomMessageText, message: str):
    """Verarbeitet Befehle und Messages."""
   
    response = None
    parts = message.split()
    command = parts[0].lower() if parts else ""
    args = parts[1:] if len(parts) > 1 else []
   
    # Befehle mit ! oder /
    if message.startswith(BOT_PREFIX) or message.startswith("/"):
       
        if command in ["!start", "!hilfe", "/start", "/hilfe"]:
            response = f"""🤖 **Matrix Bot - Telegram Style**

**Befehle:**
• `/start` `/hilfe` - Diese Hilfe
• `/status` - Bot Status
• `/reset` - Chat löschen
• `/würfel` - Würfel 🎲
• `/münze` - Münze werfen
• `/rechne [...]` - Rechner
• `/zeit` - Uhrzeit
• `/witz` - Witz
• `/ping` - Test

**n8n:**
• `/webhook` - Webhook Info
• `/n8n [cmd]` - n8n Befehl

**Raum:** `{room.room_id}`

KI-Chat: {'✅ Aktiv' if AI_ENABLED else '❌ Inaktiv'}
Schreib mir einfach! 💬"""
       
        elif command in ["!status", "/status"]:
            ai = "✅ Aktiv" if AI_ENABLED else "❌ Inaktiv"
            response = f"""📊 **Status**

**Matrix:** ✅ Online
• User: {USER_ID}
• Räume: {len(client.rooms)}

**Features:**
• KI: {ai} (Ollama: {OLLAMA_MODEL})
• n8n: ✅ Konfiguriert
• Webhook: Port {WEBHOOK_PORT}

**Modus:**
{'Antwortet auf alle' if RESPOND_TO_ALL else 'Nur Erwähnungen'}"""
       
        elif command in ["!reset", "/reset"]:
            if event.sender in conversation_history:
                del conversation_history[event.sender]
            response = "🔄 Chat-Verlauf gelöscht!"
       
        elif command in ["!würfel", "/würfel"]:
            result = random.randint(1, 6)
            response = f"🎲 **{result}**"
       
        elif command in ["!münze", "/münze"]:
            result = random.choice(["Kopf 🪙", "Zahl 🔢"])
            response = f"{result}"
       
        elif command in ["!zeit", "/zeit"]:
            response = f"🕐 **{datetime.now().strftime('%H:%M:%S')}** Uhr"
       
        elif command in ["!ping", "/ping"]:
            response = "🏓 Pong!"
       
        elif command in ["!witz", "/witz"]:
            witze = [
                "Warum können Geister so schlecht lügen? Weil sie so leicht zu durchschauen sind! 👻",
                "Was ist grün und steht vor der Tür? Ein Klopfsalat! 🥬",
                "Warum summen Bienen? Weil sie den Text nicht kennen! 🐝",
                "Was macht ein Clown im Büro? Faxen! 🤡",
                "Welche Handwerker essen am meisten? Die Maurer! 🧱"
            ]
            response = random.choice(witze)
       
        elif command in ["!rechne", "/rechne"]:
            if not args:
                response = "❌ Beispiel: `/rechne 5 + 3`"
            else:
                try:
                    expr = " ".join(args)
                    if all(c in "0123456789+-*/(). " for c in expr):
                        result = eval(expr)
                        response = f"🧮 `{expr}` = **{result}**"
                    else:
                        response = "❌ Nur Zahlen und +-*/() erlaubt"
                except:
                    response = "❌ Rechenfehler"
       
        elif command in ["!webhook", "/webhook"]:
            response = f"""📡 **Webhook Info**

**n8n → Bot:**
```
POST http://YOUR_IP:{WEBHOOK_PORT}/webhook
{{
  "secret": "{WEBHOOK_SECRET}",
  "message": "Text",
  "room_id": "{room.room_id}"
}}
```

**Health:** http://YOUR_IP:{WEBHOOK_PORT}/health
**Raum-ID:** `{room.room_id}`"""
       
        elif command in ["!n8n", "/n8n"]:
            if not args:
                response = "❌ Beispiel: `/n8n status`"
            else:
                cmd = " ".join(args)
                result = await trigger_n8n("command", {
                    "command": cmd,
                    "room_id": room.room_id,
                    "sender": event.sender
                })
               
                if result and "response" in result:
                    response = f"✅ n8n:\n{result['response']}"
                elif result:
                    response = f"✅ Befehl gesendet: `{cmd}`"
                else:
                    response = "❌ n8n nicht erreichbar"
   
    # Normale Nachricht (kein Befehl)
    else:
        # Prüfe ob Bot erwähnt wurde
        bot_mentioned = (
            f"@{USER_ID.split(':')[0][1:]}" in message.lower() or 
            USER_ID in message or 
            "@terry" in message.lower()
        )
       
        # Ist es ein Direktchat (1-zu-1)?
        is_direct_chat = False
        if room.room_id in client.rooms:
            room_obj = client.rooms[room.room_id]
            is_direct_chat = len(room_obj.users) <= 2
       
        # Bot antwortet nur wenn:
        # - Erwähnt (@terry)
        # - In Direktnachrichten
        # - Oder RESPOND_TO_ALL aktiv
        should_respond = (
            RESPOND_TO_ALL or 
            (RESPOND_TO_MENTIONS and bot_mentioned) or
            (RESPOND_TO_DM and is_direct_chat)
        )
       
        if should_respond:
            logger.info(f"💬 Verarbeite Nachricht (Mentioned: {bot_mentioned}, DM: {is_direct_chat})")
           
            # Versuche KI
            if AI_ENABLED:
                logger.info("🧠 Sende an Ollama...")
                ai_response = await ask_ai(message, event.sender)
                if ai_response:
                    response = ai_response
                else:
                    response = "🤖 Ollama antwortet nicht. Prüfe `/status`"
            else:
                # Einfache Antworten (OHNE Grüße die sich selbst triggern)
                msg_lower = message.lower()
               
                # NUR wenn Message MIT Gruß BEGINNT (nicht enthält)
                if any(message.lower().startswith(w) for w in ["hallo", "hi", "hey", "moin", "guten tag", "servus"]):
                    username = event.sender.split(':')[0][1:]
                    response = f"Hey {username}! 👋 Tippe `/hilfe` für alle Befehle."
               
                elif any(w in msg_lower for w in ["danke", "thx", "thanks"]):
                    response = "Gerne! 😊"
               
                elif any(w in msg_lower for w in ["wie geht", "wie gehts"]):
                    response = "Mir geht's super! 🤖 Wie kann ich helfen?"
               
                elif "?" in message:
                    response = "🤔 Gute Frage! Tippe `/hilfe` für verfügbare Befehle."
   
    # Antwort senden
    if response:
        try:
            await client.room_send(
                room.room_id,
                "m.room.message",
                {"msgtype": "m.text", "body": response}
            )
            logger.info(f"✓ Antwort → {event.sender.split(':')[0]}")
        except Exception as e:
            logger.error(f"❌ Senden fehlgeschlagen: {e}")

# -------------------------------
# Message Callback
# -------------------------------
async def message_callback(room: MatrixRoom, event: RoomMessageText) -> None:
    """Reagiert auf Messages."""
   
    # WICHTIG: Ignoriere eigene Messages
    if event.sender == client.user_id:
        logger.debug(f"Ignoriere eigene Nachricht von {event.sender}")
        return
   
    # Ignoriere alte Messages beim Start
    if hasattr(event, 'server_timestamp'):
        current_time = time.time() * 1000
        age = current_time - event.server_timestamp
        if age > 5000:  # Älter als 5 Sekunden
            return
   
    message = event.body.strip()
    logger.info(f"📨 {event.sender.split(':')[0]}: {message[:50]}...")
   
    await handle_command(room, event, message)

# -------------------------------
# Webserver
# -------------------------------
async def start_webserver():
    """Startet Webhook-Server."""
    app.router.add_post('/webhook', webhook_handler)
    app.router.add_get('/health', health_check)
   
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '0.0.0.0', WEBHOOK_PORT)
    await site.start()
   
    logger.info(f"🌐 Webhook: Port {WEBHOOK_PORT}")

# -------------------------------
# Matrix Client
# -------------------------------
async def start_matrix_client():
    """Startet Matrix Client."""
    global client
   
    client = AsyncClient(HOMESERVER, USER_ID, device_id=DEVICE_ID)
    client.access_token = ACCESS_TOKEN
    client.add_event_callback(message_callback, RoomMessageText)
   
    await client.sync(timeout=30000, full_state=True)
   
    logger.info(f"✓ In {len(client.rooms)} Räumen:")
    for room_id, room in client.rooms.items():
        logger.info(f"   • {room.display_name or room.room_id}")
   
    await client.sync_forever(timeout=30000)

# -------------------------------
# Main
# -------------------------------
async def main():
    """Hauptfunktion."""
    logger.info("=" * 70)
    logger.info("🤖 Matrix Bot - Telegram Style v3.0")
    logger.info("=" * 70)
    logger.info(f"🔗 {HOMESERVER}")
    logger.info(f"👤 {USER_ID}")
    logger.info(f"🧠 KI: {'✅' if AI_ENABLED else '❌'}")
    logger.info(f"🌐 Webhook: Port {WEBHOOK_PORT}")
    logger.info(f"🎯 n8n: https://n8n.rs-servertech.com")
    logger.info(f"📍 Raum: {DEFAULT_ROOM_ID}")
    logger.info("=" * 70)
   
    try:
        await asyncio.gather(
            start_webserver(),
            start_matrix_client()
        )
    except KeyboardInterrupt:
        logger.info("\n⏹️  Bot beendet")
    finally:
        if client:
            await client.close()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass