Version 2.0
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.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
# Reduziere Noise von anderen Loggern
logging.getLogger("nio").setLevel(logging.ERROR) # Verstecke nio warnings
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 # MUSS False sein!
RESPOND_TO_MENTIONS = True # Nur auf @terry
RESPOND_TO_DM = False # KEINE automatischen Antworten in DMs!
# -------------------------------
# Globale Variablen
# -------------------------------
client: AsyncClient = None
app = web.Application()
conversation_history = {}
active_conversations = {} # Speichert aktivierte Konversationen pro User
# -------------------------------
# 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"
# Zähle aktive Konversationen
active_count = len(active_conversations)
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}
**Konversationen:**
• Aktiv: {active_count}
• Schreib 'hey terry' um zu starten
• Schreib 'stop' um zu beenden"""""
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:
user_key = f"{event.sender}_{room.room_id}"
# Prüfe Aktivierungswörter
activation_words = ["hey terry", "hallo terry", "hi terry", "terry"]
is_activation = any(message.lower().startswith(word) for word in activation_words)
# Prüfe Deaktivierungswörter
deactivation_words = ["stop", "bye terry", "tschüss terry", "schluss"]
is_deactivation = any(word in message.lower() for word in deactivation_words)
# Deaktiviere wenn gewünscht
if is_deactivation and user_key in active_conversations:
del active_conversations[user_key]
response = "👋 Okay, ich bin wieder still. Schreib 'hey terry' um mich zu aktivieren!"
# Aktiviere Konversation
elif is_activation:
active_conversations[user_key] = time.time()
logger.info(f"✅ Konversation aktiviert für {event.sender}")
# Entferne "terry" aus der Nachricht für saubere Antwort
clean_message = message
for word in activation_words:
clean_message = clean_message.lower().replace(word, "").strip()
if clean_message:
# Es gibt noch Text nach "hey terry"
if AI_ENABLED:
ai_response = await ask_ai(clean_message, event.sender)
response = ai_response if ai_response else "🤖 Ollama antwortet nicht."
else:
response = "👋 Hi! Ich bin jetzt aktiviert. Schreib einfach drauf los!\n\nTippe 'stop' um mich zu deaktivieren."
else:
response = "👋 Hi! Ich bin jetzt aktiviert. Schreib einfach drauf los!\n\nTippe 'stop' um mich zu deaktivieren."
# Konversation ist aktiv - antworte automatisch
elif user_key in active_conversations:
logger.info(f"💬 Aktive Konversation: {message[:30]}...")
if AI_ENABLED:
ai_response = await ask_ai(message, event.sender)
response = ai_response if ai_response else "🤖 Ollama Problem. Tippe 'stop' um zu beenden."
else:
response = "🤖 KI ist nicht aktiv. Tippe 'stop' um zu beenden."
# Nicht aktiviert und nicht erwähnt - keine Antwort
else:
logger.debug(f"Ignoriere Nachricht - nicht aktiviert")
return
# 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