diff --git a/.gitignore b/.gitignore index 38bdde6..5758c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,16 +30,16 @@ checker.py bot_stats.json # Build-Ordner: alles ignorieren... -_build/* +build/* .pickle .doctree .buildinfo .nojekyll .inv -docs/_build/html/_sources/ +docs/build/ furo.js.LICENSE.txt fontawesome.js.LICENSE.txt .map -docs/_build/* +docs/build/* ManagerX.egg-info/ dist/ \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index e144f6b..0000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,26 +0,0 @@ - -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required -version: 2 - -# Set the OS, Python version, and other tools you might need -build: - os: ubuntu-24.04 - tools: - python: "3.12" - -# Build documentation in the "docs/" directory with Sphinx -sphinx: - configuration: docs/conf.py - -# Optionally, but recommended, -# declare the Python requirements required to build your documentation -# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -# python: -# install: -# - requirements: docs/requirements.txt -python: - install: - - requirements: requirements/docs_req.txt diff --git a/LICENSE b/LICENSE index b5874a1..5b5f70b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,6 @@ -Copyright © 2024 OPPRO.NET Development -Copyright © 2025-present OPPRO.NET Network + Copyright © 2024 OPPRO.NET Development + Copyright © 2025-2026 OPPRO.NET Network + Copyright © 2026 ManagerX Development GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 diff --git a/README.md b/README.md index e952827..f90eb50 100644 --- a/README.md +++ b/README.md @@ -915,6 +915,18 @@ Wir verwenden standardisierte Commit-Prefixes für bessere Nachvollziehbarkeit:
+### 📌 Versionierungs-Schema + +Um maximale Transparenz und Aktualität zu gewährleisten, nutzen wir eine duale Strategie: + +* **GitHub (Source Code):** Nutzt das **Semantic Versioning** (Beispiel: `2.0.0`). Dies markiert große Meilensteine und strukturelle Änderungen im Code. +* **PyPI (Distribution):** Nutzt **CalVer (Calendar Versioning)** (Beispiel: `2.2026.1.9.1`). Dies ermöglicht es Entwicklern, sofort zu erkennen, wie aktuell das installierte Paket ist. + +| Plattform | Schema | Beispiel | +| :--- | :--- | :--- | +| **GitHub** | MAJOR.MINOR.PATCH | `2.0.0` | +| **PyPI** | MAJOR.JJJJ.MM.TT | `2.2026.01.09.1` | + ### 🔧 Development Workflow ```bash diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 36dce02..0000000 --- a/TODO.md +++ /dev/null @@ -1,60 +0,0 @@ -# 📝 ManagerX Developer Todo-Guide - -> Übersicht über offene Aufgaben, Bugs, Refactors und Dokumentation. -> Dieses Dokument dient der internen Organisation und Contributor-Info. - ---- - -## 📂 Kategorien - -### 1️⃣ Features -Neue Funktionen, die implementiert werden sollen. - -| Status | Aufgabe | Beschreibung | Priorität | Verantwortlich | -|--------|--------|--------------|-----------|----------------| -| ⬜ | | | | | - ---- - -### 2️⃣ Bugs / Fixes -Probleme, die behoben werden müssen. - -| Status | Aufgabe | Beschreibung | Priorität | Verantwortlich | -|--------|--------|--------------|-----------|----------------| -| ⬜ | Wikipedia Link Button | Link Button fixen | Hoch | - | - ---- - -### 3️⃣ Refactors / Code-Optimierung -Code-Struktur verbessern, Lesbarkeit erhöhen. - -| Status | Aufgabe | Beschreibung | Priorität | Verantwortlich | -|--------|--------|--------------|-----------|----------------| -| ⬜ | | | | | - ---- - -### 4️⃣ Documentation -Alles, was mit Docs zu tun hat. - -| Status | Aufgabe | Beschreibung | Priorität | Verantwortlich | -|--------|--------|--------------|-----------|----------------| -| ⬜ | | | | | - ---- - -### ✅ Status-Markierungen -- ⬜ Offene Aufgabe -- 🔄 In Arbeit -- ✅ Erledigt - -> Tipp: Fertige Aufgaben regelmäßig aus der Tabelle entfernen oder archivieren. - ---- - -### 💡 Best Practices -1. Aufgaben nach Kategorie sortieren → leichter Überblick -2. Priorität klar angeben: Hoch / Mittel / Niedrig -3. Verantwortlich eintragen, wenn mehrere Entwickler beteiligt sind -4. Status aktuell halten → kein veralteter TODO -5. Externe Tools optional, aber nicht zwingend notwendig diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 13afe8f..0000000 --- a/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .src import handler -from .src import DevTools \ No newline at end of file diff --git a/api.py b/api.py deleted file mode 100644 index cef30dd..0000000 --- a/api.py +++ /dev/null @@ -1,478 +0,0 @@ -# ________ ________ ___ -#|\ __ \|\ __ \|\ \ -#\ \ \|\ \ \ \|\ \ \ \ -# \ \ __ \ \ ____\ \ \ -# \ \ \ \ \ \ \___|\ \ \ -# \ \__\ \__\ \__\ \ \__\ -# \|__|\|__|\|__| \|__| - -# --- STANDARD LIBRARIES --- -import json -import logging -import os -from typing import Optional, List, Dict, Any - -# --- THIRD PARTY LIBRARIES --- -import httpx -import uvicorn -import yaml -from dotenv import load_dotenv -from fastapi import Depends, FastAPI, HTTPException, Query, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import RedirectResponse -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from fastapi.staticfiles import StaticFiles -from pydantic import BaseModel - -# --- LOCAL IMPORTS --- -try: - from src.DevTools import TempVCDatabase - from src.DevTools.backend.database.welcome_db import WelcomeDatabase - from src.DevTools.backend.database.levelsystem_db import LevelDatabase -except ImportError: - TempVCDatabase = None - WelcomeDatabase = None - LevelDatabase = None - logging.warning("Database modules not found - some features may be unavailable") - -# --- LOGGING SETUP --- -logging.basicConfig(level=logging.WARNING) -logger = logging.getLogger("ManagerX-API") -logger.setLevel(logging.INFO) -logging.getLogger("httpx").setLevel(logging.WARNING) -logging.getLogger("uvicorn.access").setLevel(logging.WARNING) - -# --- KONFIGURATION --- -load_dotenv(os.path.join("config", ".env")) - -# Bot-Config laden -config_path = os.path.join("config", "config.yaml") -bot_config = {} -try: - with open(config_path, 'r', encoding='utf-8') as f: - bot_config = yaml.safe_load(f) - logger.info("Bot-Config für API-Prüfungen geladen") -except FileNotFoundError: - logger.warning("Config-Datei nicht gefunden") -except Exception as e: - logger.error(f"Fehler beim Laden der Config: {e}") - -# --- FASTAPI APP --- -app = FastAPI(title="ManagerX Ultimate API") - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# --- STATISCHE DATEIEN --- -if os.path.exists("site"): - app.mount("/site", StaticFiles(directory="site", html=True), name="site") - -# --- DATENBANKEN INITIALISIEREN --- -DB_PATH = os.path.join("data", "tempvc.db") -db = TempVCDatabase(DB_PATH) if TempVCDatabase else None -welcome_db = WelcomeDatabase() if WelcomeDatabase else None -level_db = LevelDatabase() if LevelDatabase else None - -# --- SECURITY --- -security = HTTPBearer() - -def get_token(credentials: HTTPAuthorizationCredentials = Depends(security)): - return credentials.credentials - -# --- DATEN-MODELLE --- -class TempVCUpdate(BaseModel): - token: str - creator_channel_id: str - category_id: str - auto_delete_time: int - ui_enabled: bool - ui_prefix: str - -class WelcomeUpdate(BaseModel): - token: str - channel_id: str - welcome_message: str = "" - enabled: bool = True - embed_enabled: bool = False - embed_color: str = "#00ff00" - embed_title: str = "" - embed_description: str = "" - embed_thumbnail: bool = False - embed_footer: str = "" - ping_user: bool = False - delete_after: int = 0 - -class LevelUpdate(BaseModel): - token: str - levelsystem_enabled: bool = True - min_xp: int = 10 - max_xp: int = 20 - xp_cooldown: int = 30 - level_up_channel: str = "" - prestige_enabled: bool = True - prestige_min_level: int = 50 - -class RefreshTokenRequest(BaseModel): - refresh_token: str - -# --- HILFSFUNKTIONEN --- -def is_feature_enabled(feature_path: str) -> bool: - """Prüft, ob ein Feature in der Config aktiviert ist.""" - keys = feature_path.split('.') - current = bot_config - try: - for key in keys: - current = current.get(key, {}) - return current if isinstance(current, bool) else True - except: - return True - -async def check_admin_permissions(guild_id: int, token: str): - """Prüft bei Discord, ob der User Admin-Rechte hat.""" - async with httpx.AsyncClient() as client: - try: - res = await client.get( - "https://discord.com/api/users/@me/guilds", - headers={"Authorization": f"Bearer {token}"}, - timeout=5.0 - ) - if res.status_code == 401: - raise HTTPException(status_code=401, detail="Token abgelaufen") - if res.status_code != 200: - raise HTTPException(status_code=401, detail="Sitzung abgelaufen oder Token ungültig") - - guilds = res.json() - guild = next((g for g in guilds if int(g['id']) == guild_id), None) - - if not guild: - raise HTTPException(status_code=404, detail="Server nicht gefunden") - - # Bitwise check für ADMINISTRATOR (0x8) - if not (int(guild.get('permissions', 0)) & 0x8) == 0x8: - raise HTTPException(status_code=403, detail="Du hast keine Admin-Rechte") - - return True - except HTTPException: - raise - except Exception as e: - logger.error(f"Fehler bei Discord-Validierung: {e}") - raise HTTPException(status_code=500, detail="Discord API Kommunikationsfehler") - -# --- ENDPOINTS --- - -@app.get("/") -async def root(): - """Redirect zur Landing Page""" - return RedirectResponse(url="/site/index.html") - -@app.get("/api/managerx/stats") -@app.get("/api/v2/stats") -async def get_bot_stats(): - """Bot-Statistiken laden""" - stats_file = "bot_stats.json" - if os.path.exists(stats_file): - try: - with open(stats_file, "r", encoding="utf-8") as f: - return json.load(f) - except Exception as e: - logger.error(f"Fehler beim Laden der Stats: {e}") - - return { - "stats": {"server_count": 50, "user_count": 15000}, - "bot_info": {"latency": 35, "status": "Online"} - } - -@app.get("/api/auth/callback") -async def auth_callback(code: str): - """OAuth2 Callback für Discord-Login""" - async with httpx.AsyncClient() as client: - payload = { - 'client_id': os.getenv("DISCORD_CLIENT_ID"), - 'client_secret': os.getenv("DISCORD_CLIENT_SECRET"), - 'grant_type': 'authorization_code', - 'code': code, - 'redirect_uri': os.getenv("DISCORD_REDIRECT_URI") - } - r = await client.post('https://discord.com/api/oauth2/token', data=payload) - - if r.status_code != 200: - logger.error(f"Login Fehler: {r.text}") - raise HTTPException(status_code=400, detail="Discord Token Austausch fehlgeschlagen") - - tokens = r.json() - u = await client.get( - 'https://discord.com/api/users/@me', - headers={'Authorization': f"Bearer {tokens['access_token']}"} - ) - - return { - "access_token": tokens['access_token'], - "refresh_token": tokens.get('refresh_token'), - "user": u.json() - } - -@app.post("/api/auth/refresh") -async def refresh_access_token(data: RefreshTokenRequest): - """Token erneuern""" - async with httpx.AsyncClient() as client: - payload = { - 'client_id': os.getenv("DISCORD_CLIENT_ID"), - 'client_secret': os.getenv("DISCORD_CLIENT_SECRET"), - 'grant_type': 'refresh_token', - 'refresh_token': data.refresh_token - } - r = await client.post('https://discord.com/api/oauth2/token', data=payload) - - if r.status_code != 200: - raise HTTPException(status_code=400, detail="Token-Refresh fehlgeschlagen") - - tokens = r.json() - return { - "access_token": tokens['access_token'], - "refresh_token": tokens.get('refresh_token') - } - -@app.get("/api/user/guilds") -async def get_user_guilds(token: str = Depends(get_token)): - """Alle Server mit Admin-Rechten""" - async with httpx.AsyncClient() as client: - res = await client.get( - "https://discord.com/api/users/@me/guilds", - headers={"Authorization": f"Bearer {token}"} - ) - if res.status_code != 200: - return [] - - # Filtert nur Server mit Admin-Rechten - return [g for g in res.json() if (int(g.get('permissions', 0)) & 0x8) == 0x8] - -@app.get("/api/guild/{guild_id}/channels") -async def get_guild_channels(guild_id: int, token: str = Depends(get_token)): - """Kanäle eines Servers laden""" - await check_admin_permissions(guild_id, token) - - async with httpx.AsyncClient() as client: - headers = {"Authorization": f"Bearer {token}"} - res = await client.get( - f"https://discord.com/api/guilds/{guild_id}/channels", - headers=headers - ) - - if res.status_code == 401: - raise HTTPException(status_code=401, detail="Token abgelaufen") - if res.status_code != 200: - logger.error(f"Discord API Fehler: {res.status_code} - {res.text}") - raise HTTPException(status_code=500, detail=f"Discord API Fehler: {res.status_code}") - - channels = res.json() - # Filtere Text-, Voice-Kanäle und Kategorien - filtered = [ - {"id": str(ch["id"]), "name": ch["name"], "type": ch["type"]} - for ch in channels if ch["type"] in [0, 2, 4] # 0=Text, 2=Voice, 4=Category - ] - return {"channels": filtered} - -@app.get("/api/guild/{guild_id}/tempvc") -async def get_tempvc(guild_id: int, token: str = Depends(get_token)): - """TempVC Einstellungen laden""" - await check_admin_permissions(guild_id, token) - - if not is_feature_enabled('features.cogs.server_management.tempvc'): - raise HTTPException(status_code=403, detail="TempVC Feature ist deaktiviert") - - if not db: - raise HTTPException(status_code=500, detail="TempVC Database nicht verfügbar") - - settings = db.get_tempvc_settings(guild_id) - ui = db.get_ui_settings(guild_id) - - return { - "creator_channel_id": str(settings[0]) if settings else "", - "category_id": str(settings[1]) if settings else "", - "auto_delete_time": settings[2] if settings and len(settings) > 2 else 0, - "ui_enabled": bool(ui[0]) if ui else False, - "ui_prefix": ui[1] if ui else "🔧" - } - -@app.post("/api/guild/{guild_id}/tempvc") -async def save_tempvc(guild_id: int, data: TempVCUpdate): - """TempVC Einstellungen speichern""" - await check_admin_permissions(guild_id, data.token) - - if not is_feature_enabled('features.cogs.server_management.tempvc'): - raise HTTPException(status_code=403, detail="TempVC Feature ist deaktiviert") - - if not db: - raise HTTPException(status_code=500, detail="TempVC Database nicht verfügbar") - - try: - c_id = int(data.creator_channel_id) - cat_id = int(data.category_id) - logger.info(f"💾 SPEICHERN: Guild {guild_id} | IDs: {c_id}, {cat_id}") - - db.set_tempvc_settings(guild_id, c_id, cat_id, data.auto_delete_time) - db.set_ui_settings(guild_id, data.ui_enabled, data.ui_prefix) - - return {"status": "success", "message": "Daten wurden permanent gespeichert"} - except ValueError: - raise HTTPException(status_code=400, detail="Kanal- und Kategorie-IDs müssen Zahlen sein") - except Exception as e: - logger.error(f"Datenbank-Fehler: {e}") - raise HTTPException(status_code=500, detail="Interner Datenbank-Fehler") - -@app.get("/api/guild/{guild_id}/welcome") -async def get_welcome(guild_id: int, token: str = Depends(get_token)): - """Welcome Einstellungen laden""" - await check_admin_permissions(guild_id, token) - - if not is_feature_enabled('features.cogs.server_management.welcome'): - raise HTTPException(status_code=403, detail="Welcome Feature ist deaktiviert") - - if not welcome_db: - raise HTTPException(status_code=500, detail="Welcome Database nicht verfügbar") - - settings = welcome_db.get_welcome_settings(guild_id) - - if not settings: - return { - "channel_id": "", - "welcome_message": "Willkommen {user} auf {server}!", - "enabled": True, - "embed_enabled": False, - "embed_color": "#00ff00", - "embed_title": "Willkommen!", - "embed_description": "", - "embed_thumbnail": False, - "embed_footer": "", - "ping_user": False, - "delete_after": 0 - } - - return { - "channel_id": str(settings.get('channel_id', '')), - "welcome_message": settings.get('welcome_message', ''), - "enabled": bool(settings.get('enabled', True)), - "embed_enabled": bool(settings.get('embed_enabled', False)), - "embed_color": settings.get('embed_color', '#00ff00'), - "embed_title": settings.get('embed_title', ''), - "embed_description": settings.get('embed_description', ''), - "embed_thumbnail": bool(settings.get('embed_thumbnail', False)), - "embed_footer": settings.get('embed_footer', ''), - "ping_user": bool(settings.get('ping_user', False)), - "delete_after": settings.get('delete_after', 0) - } - -@app.post("/api/guild/{guild_id}/welcome") -async def save_welcome(guild_id: int, data: WelcomeUpdate): - """Welcome Einstellungen speichern""" - await check_admin_permissions(guild_id, data.token) - - if not is_feature_enabled('features.cogs.server_management.welcome'): - raise HTTPException(status_code=403, detail="Welcome Feature ist deaktiviert") - - if not welcome_db: - raise HTTPException(status_code=500, detail="Welcome Database nicht verfügbar") - - try: - ch_id = int(data.channel_id) if data.channel_id else None - logger.info(f"💾 SPEICHERN WELCOME: Guild {guild_id} | Channel: {ch_id}") - - success = welcome_db.update_welcome_settings( - guild_id, - channel_id=ch_id, - welcome_message=data.welcome_message, - enabled=data.enabled, - embed_enabled=data.embed_enabled, - embed_color=data.embed_color, - embed_title=data.embed_title, - embed_description=data.embed_description, - embed_thumbnail=data.embed_thumbnail, - embed_footer=data.embed_footer, - ping_user=data.ping_user, - delete_after=data.delete_after - ) - - if success: - return {"status": "success", "message": "Welcome-Einstellungen gespeichert"} - else: - raise HTTPException(status_code=500, detail="Fehler beim Speichern") - except ValueError: - raise HTTPException(status_code=400, detail="Ungültige Channel-ID") - except Exception as e: - logger.error(f"Fehler beim Speichern: {e}") - raise HTTPException(status_code=500, detail="Interner Fehler") - -@app.get("/api/guild/{guild_id}/levelsystem") -async def get_levelsystem(guild_id: int, token: str = Query(...)): - """Levelsystem Einstellungen laden""" - await check_admin_permissions(guild_id, token) - - if not is_feature_enabled('features.cogs.server_management.levelsystem'): - raise HTTPException(status_code=403, detail="Levelsystem Feature ist deaktiviert") - - if not level_db: - raise HTTPException(status_code=500, detail="Levelsystem Database nicht verfügbar") - - settings = level_db.get_guild_config(guild_id) - - if not settings: - return { - "levelsystem_enabled": True, - "min_xp": 10, - "max_xp": 20, - "xp_cooldown": 30, - "level_up_channel": "", - "prestige_enabled": True, - "prestige_min_level": 50 - } - - return { - "levelsystem_enabled": settings.get('levelsystem_enabled', True), - "min_xp": settings.get('min_xp', 10), - "max_xp": settings.get('max_xp', 20), - "xp_cooldown": settings.get('xp_cooldown', 30), - "level_up_channel": str(settings.get('level_up_channel', '')), - "prestige_enabled": settings.get('prestige_enabled', True), - "prestige_min_level": settings.get('prestige_min_level', 50) - } - -@app.post("/api/guild/{guild_id}/levelsystem") -async def save_levelsystem(guild_id: int, data: LevelUpdate): - """Levelsystem Einstellungen speichern""" - await check_admin_permissions(guild_id, data.token) - - if not is_feature_enabled('features.cogs.server_management.levelsystem'): - raise HTTPException(status_code=403, detail="Levelsystem Feature ist deaktiviert") - - if not level_db: - raise HTTPException(status_code=500, detail="Levelsystem Database nicht verfügbar") - - try: - ch_id = int(data.level_up_channel) if data.level_up_channel else None - logger.info(f"💾 SPEICHERN LEVELSYSTEM: Guild {guild_id} | Channel: {ch_id}") - - config = { - 'levelsystem_enabled': data.levelsystem_enabled, - 'min_xp': data.min_xp, - 'max_xp': data.max_xp, - 'xp_cooldown': data.xp_cooldown, - 'level_up_channel': ch_id, - 'prestige_enabled': data.prestige_enabled, - 'prestige_min_level': data.prestige_min_level - } - level_db.update_guild_config(guild_id, config) - - return {"status": "success", "message": "Levelsystem-Einstellungen gespeichert"} - except ValueError: - raise HTTPException(status_code=400, detail="Ungültige Channel-ID") - except Exception as e: - logger.error(f"Fehler beim Speichern: {e}") - raise HTTPException(status_code=500, detail="Interner Fehler") - -if __name__ == "__main__": - uvicorn.run(app, host="127.0.0.1", port=3002, log_level="warning") \ No newline at end of file diff --git a/assets/img/ManagerX_banner.png b/assets/img/ManagerX_banner.png deleted file mode 100644 index ed15b3e..0000000 Binary files a/assets/img/ManagerX_banner.png and /dev/null differ diff --git a/config/example.env b/config/example.env deleted file mode 100644 index 15e6eeb..0000000 --- a/config/example.env +++ /dev/null @@ -1,6 +0,0 @@ -TOKEN= dein_discord_bot_token_hier -WEATHER_API= dein_api_key_hier - -DISCORD_CLIENT_ID= deine_client_id_hier -DISCORD_CLIENT_SECRET= dein_client -DISCORD_REDIRECT_URI= deine_redirect_uri_hier \ No newline at end of file diff --git a/config/version.txt b/config/version.txt deleted file mode 100644 index fb0ed55..0000000 --- a/config/version.txt +++ /dev/null @@ -1 +0,0 @@ -2.0.0-dev \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile index d4bb2cb..d0c3cbf 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -5,8 +5,8 @@ # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build +SOURCEDIR = source +BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: diff --git a/docs/_static/custom.css b/docs/_static/custom.css deleted file mode 100644 index 573cc6d..0000000 --- a/docs/_static/custom.css +++ /dev/null @@ -1,399 +0,0 @@ -/* ========================================================================== - MANAGERX ULTIMATE RED THEME (Sphinx Optimized) - ========================================================================== */ - -@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=JetBrains+Mono:wght@400;500&display=swap'); - -:root { - /* ManagerX Core Palette */ - --mx-red-primary: #e11d48; /* Modernes, kräftiges Rot (Rose-Red) */ - --mx-red-dark: #9f1239; /* Für Hover & Header */ - --mx-red-light: #fff1f2; /* Für Hintergründe */ - --mx-red-glow: rgba(225, 29, 72, 0.15); - - /* Layout Overrides */ - --pst-font-family-base: 'Outfit', sans-serif; - --pst-font-family-heading: 'Outfit', sans-serif; - --pst-font-family-monospace: 'JetBrains Mono', monospace; - - --pst-color-primary: var(--mx-red-primary); - --pst-color-link: var(--mx-red-primary); - --pst-color-target: #fbbf24; /* Gold-Gelb für Fokus-Anker */ -} - -/* --- 1. GLOBAL DESIGN & DEPTH --- */ -body { - -webkit-font-smoothing: antialiased; - letter-spacing: -0.01em; -} - -/* Scrollbar für "Control Center" Look */ -::-webkit-scrollbar { width: 10px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { - background: #e2e8f0; - border-radius: 10px; - border: 3px solid white; -} -[data-theme="dark"] ::-webkit-scrollbar-thumb { border-color: #0f172a; background: #334155; } -::-webkit-scrollbar-thumb:hover { background: var(--mx-red-primary); } - -/* --- 2. HEADER (Glassmorphism + Red Accent) --- */ -.bd-header { - background-color: rgba(255, 255, 255, 0.8) !important; - backdrop-filter: blur(12px); - border-bottom: 3px solid var(--mx-red-primary) !important; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); -} - -[data-theme="dark"] .bd-header { - background-color: rgba(15, 23, 42, 0.9) !important; - border-bottom-color: var(--mx-red-dark) !important; -} - -/* --- 3. RST COMPONENTS (Admonitions & Notes) --- */ -/* Sphinx nutzt Klassen wie .admonition, .note, .warning */ -.admonition { - border: none !important; - border-left: 6px solid var(--mx-red-primary) !important; - border-radius: 12px !important; - background: #ffffff !important; - box-shadow: 0 4px 12px rgba(0,0,0,0.05) !important; - padding: 1.25rem !important; -} - -[data-theme="dark"] .admonition { - background: #1e293b !important; -} - -.admonition-title { - background: transparent !important; - color: var(--mx-red-primary) !important; - font-weight: 800 !important; - text-transform: uppercase; - font-size: 0.8rem !important; - letter-spacing: 0.05em; -} - -/* Spezifisch für Warnungen */ -.admonition.warning { border-left-color: #f59e0b !important; } -.admonition.danger { border-left-color: #ef4444 !important; } - -/* --- 4. SIDEBAR NAVIGATION --- */ -/* Aktive RST Toctree Links */ -.bd-sidebar-primary .nav-item.current > a { - color: var(--mx-red-primary) !important; - font-weight: 600; - background: linear-gradient(90deg, var(--mx-red-glow) 0%, transparent 100%); - border-radius: 0 20px 20px 0; -} - -.bd-sidebar-primary .caption-text { - color: var(--mx-red-dark); - font-weight: 800; - font-size: 0.7rem; - text-transform: uppercase; -} - -/* --- 5. CODE BLOCKS & KBD --- */ -div.highlight { - border: 1px solid rgba(225, 29, 72, 0.1) !important; - border-radius: 14px !important; - box-shadow: inset 0 2px 4px rgba(0,0,0,0.02); - padding: 5px; -} - -/* Wenn du in RST :kbd:`STRG` nutzt */ -kbd { - background: #f8fafc; - border: 1px solid #cbd5e1; - border-radius: 5px; - box-shadow: 0 2px 0 #cbd5e1; - color: var(--mx-red-dark); - font-family: var(--pst-font-family-monospace); - padding: 2px 6px; -} - -/* --- 6. RST TABLES --- */ -table.docutils { - width: 100%; - border-collapse: separate !important; - border-spacing: 0; - border-radius: 12px; - overflow: hidden; - border: 1px solid rgba(0,0,0,0.05) !important; - margin: 2rem 0; -} - -table.docutils thead th { - background: var(--mx-red-primary) !important; - color: white !important; - padding: 12px !important; - border: none !important; -} - -table.docutils tbody td { - padding: 12px !important; - border-bottom: 1px solid rgba(0,0,0,0.05) !important; -} - -/* --- 7. CUSTOM RST CLASSES (Für deine Container) --- */ -/* Nutzung in RST via .. container:: mx-hero */ -.mx-hero { - text-align: center; - padding: 4rem 2rem; - background: radial-gradient(circle at center, var(--mx-red-glow) 0%, transparent 70%); - border-radius: 30px; - margin-bottom: 3rem; -} - -/* --- 8. SMOOTH ANCHORING & SECTIONS --- */ -/* Verhindert, dass Überschriften beim Springen hinter dem Header verschwinden */ -:target { - scroll-margin-top: 100px; - animation: highlight-pulse 2s ease-out; -} - -@keyframes highlight-pulse { - 0% { background-color: var(--mx-red-glow); } - 100% { background-color: transparent; } -} - -/* --- 9. PYGMENTS SYNTAX HIGHLIGHTING TUNING --- */ -/* Wir färben die Syntax-Elemente dezent passend zum Bot-Thema */ -.highlight .k { color: var(--mx-red-primary); font-weight: bold; } /* Keywords */ -.highlight .nc { color: var(--mx-red-dark); font-weight: bold; } /* Class names */ -.highlight .s2 { color: #2d5a27; } /* Strings (grünlich für Kontrast) */ -.highlight .c1 { color: #94a3b8; font-style: italic; } /* Comments */ - -/* --- 10. MODERNE NAVIGATION BUTTONS --- */ -/* Die "Previous" und "Next" Buttons am Ende jeder Seite */ -.prev-next-area a { - border-radius: 12px !important; - border: 1px solid rgba(0,0,0,0.05) !important; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; -} - -.prev-next-area a:hover { - border-color: var(--mx-red-primary) !important; - box-shadow: 0 10px 20px var(--mx-red-glow) !important; - transform: translateY(-2px); -} - -.prev-next-area .prev-next-info .prev-next-title { - color: var(--mx-red-primary) !important; -} - -/* --- 11. API REFERENCE (Autodoc) STYLING --- */ -/* Wenn du Python-Klassen oder Funktionen dokumentierst */ -dl.py.function, dl.py.class, dl.py.method { - background: var(--mx-red-light); - border-radius: 10px; - padding: 1rem; - margin-bottom: 2rem; - border: 1px solid rgba(225, 29, 72, 0.05); -} - -[data-theme="dark"] dl.py.function, -[data-theme="dark"] dl.py.class { - background: rgba(225, 29, 72, 0.05); -} - -dt.sig { - font-family: var(--pst-font-family-monospace); - font-size: 1.1rem; - color: var(--mx-red-primary); -} - -/* --- 12. SIDEBAR-TOGGLE FÜR MOBILE --- */ -/* Den mobilen Button anpassen */ -.bd-header .navbar-toggler { - border-color: var(--mx-red-primary); - color: var(--mx-red-primary); -} - -/* --- 13. LAYOUT HELPERS --- */ -.mx-grid-2 { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - margin: 20px 0; -} - -@media (max-width: 768px) { - .mx-grid-2 { grid-template-columns: 1fr; } -} - -.mx-box { - padding: 1.5rem; - border-radius: 12px; - background: #f8fafc; - border-bottom: 4px solid var(--mx-red-primary); -} - -[data-theme="dark"] .mx-box { - background: #1e293b; -} - -/* --- 14. SEARCH EXPERIENCE (Modal & Input) --- */ -/* Das Suchfeld oben rechts im ManagerX Look */ -.bd-search .form-control { - border-radius: 50px !important; - border: 1px solid rgba(225, 29, 72, 0.2) !important; - transition: all 0.3s ease; -} - -.bd-search .form-control:focus { - border-color: var(--mx-red-primary) !important; - box-shadow: 0 0 0 4px var(--mx-red-glow) !important; - outline: none; -} - -/* Such-Shortcut KBD im Input */ -.search-button__kbd-shortcut { - background: var(--mx-red-light) !important; - color: var(--mx-red-primary) !important; - border: none !important; -} - -/* --- 15. INTERACTIVE LINKS & INLINE ELEMENTS --- */ -/* Hover-Effekt für normale Textlinks */ -article a.reference.internal, -article a.reference.external { - position: relative; - text-decoration: none !important; - transition: color 0.3s ease; -} - -article a.reference:after { - content: ''; - position: absolute; - width: 100%; - transform: scaleX(0); - height: 1px; - bottom: -1px; - left: 0; - background-color: var(--mx-red-primary); - transform-origin: bottom right; - transition: transform 0.25s ease-out; -} - -article a.reference:hover:after { - transform: scaleX(1); - transform-origin: bottom left; -} - -/* --- 16. IMAGE & FIGURE STYLING --- */ -/* Bilder erhalten einen dezenten Shadow und abgerundete Ecken */ -figure.align-default { - margin: 2rem 0; - padding: 10px; - background: white; - border-radius: 16px; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); - text-align: center; -} - -[data-theme="dark"] figure.align-default { - background: #1e293b; -} - -figure.align-default img { - border-radius: 10px; -} - -figcaption { - margin-top: 10px; - font-size: 0.85rem; - color: #64748b; - font-style: italic; -} - -/* --- 17. IMPROVED TYPOGRAPHY (Vertical Rhythm) --- */ -h1, h2, h3, h4 { - color: #0f172a; - margin-top: 2.5rem !important; - margin-bottom: 1.25rem !important; -} - -[data-theme="dark"] h1, -[data-theme="dark"] h2, -[data-theme="dark"] h3 { - color: #f1f5f9; -} - -/* Marker vor Überschriften (Dezenter ManagerX-Dot) */ -h2::before { - content: "◢"; - font-size: 0.6em; - margin-right: 10px; - color: var(--mx-red-primary); - vertical-align: middle; -} - -/* --- 18. VERSION SWITCHER & DROPDOWNS --- */ -.bd-version-switcher__button { - border-radius: 8px !important; - border: 1px solid var(--mx-red-glow) !important; -} - -.bd-version-switcher__button:hover { - background-color: var(--mx-red-light) !important; - color: var(--mx-red-primary) !important; -} - -/* --- 19. FOOTER CLEANUP --- */ -.bd-footer { - padding: 3rem 0; - font-size: 0.9rem; - border-top: 1px solid rgba(0,0,0,0.05); -} - -.footer-items__end { - color: #94a3b8; -} - -.footer-items__end strong { - color: var(--mx-red-primary); -} - -/* --- 20. CUSTOM NOTIFICATIONS (Banner) --- */ -/* Falls du oben eine Ankündigung einblendest (.. announcement::) */ -.bd-header-announcement { - background: linear-gradient(90deg, var(--mx-red-dark), var(--mx-red-primary)) !important; - color: white !important; - font-weight: 600; -} - -/* --- 21. BUTTON OVERRIDES (From Blue to ManagerX Red) --- */ - -/* Haupt-Buttons (Primary) */ -.btn-primary { - background-color: var(--mx-red-primary) !important; - border-color: var(--mx-red-primary) !important; - color: #ffffff !important; - font-weight: 600; - transition: all 0.2s ease-in-out; -} - -.btn-primary:hover, -.btn-primary:focus, -.btn-primary:active { - background-color: var(--mx-red-dark) !important; - border-color: var(--mx-red-dark) !important; - box-shadow: 0 0 0 0.25rem var(--mx-red-glow) !important; -} - -/* Outline-Buttons (z.B. in der Suche oder API-Links) */ -.btn-outline-primary { - color: var(--mx-red-primary) !important; - border-color: var(--mx-red-primary) !important; -} - -.btn-outline-primary:hover { - background-color: var(--mx-red-primary) !important; - border-color: var(--mx-red-primary) !important; - color: #ffffff !important; -} - -/* --- END OF MANAGERX ULTIMATE RED THEME --- */ \ No newline at end of file diff --git a/docs/_static/managerx.png b/docs/_static/managerx.png deleted file mode 100644 index 950a896..0000000 Binary files a/docs/_static/managerx.png and /dev/null differ diff --git a/docs/dev_guide/api_reference/endpoints/authentication.rst b/docs/dev_guide/api_reference/endpoints/authentication.rst deleted file mode 100644 index ef10082..0000000 --- a/docs/dev_guide/api_reference/endpoints/authentication.rst +++ /dev/null @@ -1,87 +0,0 @@ -Authentication API -================== - -This section documents the authentication endpoints available in ManagerX. -These endpoints handle Discord OAuth2 login and token refresh for users. - -Available Endpoints -------------------- - -1. **OAuth2 Callback** - - - **Endpoint**: ``/api/auth/callback`` - - **Method**: GET - - **Description**: Exchanges the authorization code from Discord for access and refresh tokens, and returns the authenticated user's info. - - **Query Parameters**: - - - ``code`` (str, required): The authorization code provided by Discord after user login. - - **Response Example**:: - - { - "access_token": "ACCESS_TOKEN_HERE", - "refresh_token": "REFRESH_TOKEN_HERE", - "user": { - "id": "123456789012345678", - "username": "ExampleUser", - "discriminator": "1234", - "avatar": "avatar_hash" - } - } - - **Error Responses**: - - - 400 Bad Request: Discord token exchange failed. - - **Example Usage with Python requests**:: - - import requests - - code = "AUTHORIZATION_CODE_FROM_DISCORD" - response = requests.get(f"https://api.yourdomain.com/api/auth/callback?code={code}") - data = response.json() - print(data) - ---- - -2. **Refresh Access Token** - - - **Endpoint**: ``/api/auth/refresh`` - - **Method**: POST - - **Description**: Refreshes the access token using a valid refresh token. - - **Request Body (JSON)**:: - - { - "refresh_token": "REFRESH_TOKEN_HERE" - } - - **Response Example**:: - - { - "access_token": "NEW_ACCESS_TOKEN", - "refresh_token": "NEW_REFRESH_TOKEN" - } - - **Error Responses**: - - - 400 Bad Request: Missing refresh token. - - 400 Bad Request: Token refresh failed. - - **Example Usage with Python requests**:: - - import requests - - data = {"refresh_token": "REFRESH_TOKEN_HERE"} - response = requests.post("https://api.yourdomain.com/api/auth/refresh", json=data) - tokens = response.json() - print(tokens) - -Notes ------ - -- All responses are returned in **JSON format**. -- Tokens should be stored securely by the client. -- The `/auth/callback` endpoint requires a valid **Discord OAuth2 authorization code**. -- The `/auth/refresh` endpoint requires a **refresh token** previously obtained from `/auth/callback`. diff --git a/docs/dev_guide/api_reference/endpoints/guilds.rst b/docs/dev_guide/api_reference/endpoints/guilds.rst deleted file mode 100644 index 3a5561f..0000000 --- a/docs/dev_guide/api_reference/endpoints/guilds.rst +++ /dev/null @@ -1,155 +0,0 @@ -Guild & Server Management API -============================= - -This section documents the endpoints for managing guild-related settings in ManagerX. -These endpoints require admin permissions on the Discord server and allow retrieving and updating server configurations such as TempVC, Welcome messages, and Levelsystem settings. - -Available Endpoints -------------------- - -1. **Get User Guilds** - - - **Endpoint**: ``/api/user/guilds`` - - **Method**: GET - - **Description**: Returns the list of guilds where the user has admin permissions. - - **Response Example**:: - - [ - { - "id": "123456789012345678", - "name": "Example Server", - "permissions": 8 - } - ] - ---- - -2. **Get Guild Channels** - - - **Endpoint**: ``/api/guild/{guild_id}/channels`` - - **Method**: GET - - **Description**: Returns all text, voice channels and categories for the specified guild. - - **Response Example**:: - - { - "channels": [ - {"id": "111", "name": "general", "type": 0}, - {"id": "222", "name": "voice", "type": 2} - ] - } - ---- - -3. **Get TempVC Settings** - - - **Endpoint**: ``/api/guild/{guild_id}/tempvc`` - - **Method**: GET - - **Description**: Retrieves temporary voice channel settings for the guild. - - **Response Example**:: - - { - "creator_channel_id": "123", - "category_id": "456", - "auto_delete_time": 10, - "ui_enabled": true, - "ui_prefix": "🔧" - } - ---- - -4. **Save TempVC Settings** - - - **Endpoint**: ``/api/guild/{guild_id}/tempvc`` - - **Method**: POST - - **Request Body**: - - - ``creator_channel_id`` (str) - - ``category_id`` (str) - - ``auto_delete_time`` (int) - - ``ui_enabled`` (bool) - - ``ui_prefix`` (str) - - - **Response Example**:: - - { - "status": "success", - "message": "Daten wurden permanent gespeichert" - } - ---- - -5. **Get Welcome Settings** - - - **Endpoint**: ``/api/guild/{guild_id}/welcome`` - - **Method**: GET - - **Description**: Retrieves the guild's welcome message settings. - - **Response Example**:: - - { - "channel_id": "123456", - "welcome_message": "Willkommen {user} auf {server}!", - "enabled": true, - "embed_enabled": false, - "embed_color": "#00ff00", - "embed_title": "Willkommen!", - "embed_description": "", - "embed_thumbnail": false, - "embed_footer": "", - "ping_user": false, - "delete_after": 0 - } - ---- - -6. **Save Welcome Settings** - - - **Endpoint**: ``/api/guild/{guild_id}/welcome`` - - **Method**: POST - - **Request Body**: All fields as in the GET response. - - **Response Example**:: - - { - "status": "success", - "message": "Welcome-Einstellungen gespeichert" - } - ---- - -7. **Get Levelsystem Settings** - - - **Endpoint**: ``/api/guild/{guild_id}/levelsystem`` - - **Method**: GET - - **Description**: Retrieves leveling system settings for the guild. - - **Response Example**:: - - { - "levelsystem_enabled": true, - "min_xp": 10, - "max_xp": 20, - "xp_cooldown": 30, - "level_up_channel": "123", - "prestige_enabled": true, - "prestige_min_level": 50 - } - ---- - -8. **Save Levelsystem Settings** - - - **Endpoint**: ``/api/guild/{guild_id}/levelsystem`` - - **Method**: POST - - **Request Body**: All fields as in the GET response. - - **Response Example**:: - - { - "status": "success", - "message": "Levelsystem-Einstellungen gespeichert" - } - -Notes ------ - -- All endpoints require a valid Discord admin token. -- Responses are returned in JSON format. -- Features must be enabled in the bot configuration; otherwise, a 403 error is returned. -- Invalid IDs or missing database entries may return 400 or 500 errors. diff --git a/docs/dev_guide/api_reference/endpoints/stats.rst b/docs/dev_guide/api_reference/endpoints/stats.rst deleted file mode 100644 index 1498565..0000000 --- a/docs/dev_guide/api_reference/endpoints/stats.rst +++ /dev/null @@ -1,74 +0,0 @@ -Stats API Endpoint -================== - -This section documents the statistics API endpoints available in ManagerX. -These endpoints provide information about the bot's server count, user count, latency, and status. - -Available Endpoints -------------------- - -- **Version 1 API**:: - - /api/managerx/stats - -- **Version 2 API**:: - - /api/v2/stats - -HTTP Method ------------ - -- **GET**: Retrieve current statistics. - -Response Format ---------------- - -The endpoints return a JSON object with the following structure:: - - { - "stats": { - "server_count": 50, - "user_count": 15000 - }, - "bot_info": { - "latency": 35, - "status": "Online" - } - } - -Fields ------- - -**stats** - -- ``server_count``: Total number of servers the bot is in. -- ``user_count``: Total number of users across all servers. - -**bot_info** - -- ``latency``: Current bot latency in milliseconds. -- ``status``: Current status of the bot (e.g., "Online", "Offline"). - -Example Usage -------------- - -Using **curl**:: - - curl -X GET https://api.yourdomain.com/api/v2/stats - -Using **Python requests**:: - - import requests - - url = "https://api.yourdomain.com/api/v2/stats" - response = requests.get(url) - data = response.json() - print(data) - -Notes ------ - -- If the local `bot_stats.json` file exists, the endpoint will return the stored stats. -- If the file does not exist or is unreadable, default statistics will be returned. -- All responses are in JSON format. -- The endpoint is **read-only** and does not require authentication. diff --git a/docs/dev_guide/api_reference/examples/api_js.rst b/docs/dev_guide/api_reference/examples/api_js.rst deleted file mode 100644 index 3b1d64b..0000000 --- a/docs/dev_guide/api_reference/examples/api_js.rst +++ /dev/null @@ -1,94 +0,0 @@ -Using the API with JavaScript -============================= - -This section demonstrates how to use the ManagerX API from a frontend JavaScript application. -It shows authentication handling, token refresh, and usage of various endpoints like TempVC, Welcome, Levelsystem, and Stats. - -API Base --------- - -All API requests are made relative to the base URL: - -:: - - const API_BASE = "http://127.0.0.1:3002/api"; - -or your Domain. - -Authentication --------------- - -Store and retrieve your Discord OAuth tokens from localStorage: - -- Access token: `discord_token` -- Refresh token: `discord_refresh_token` - -Use `checkTokenStatus()` to inspect token availability: - -:: - - const tokens = checkTokenStatus(); - console.log(tokens.hasToken, tokens.hasRefreshToken); - -Refreshing Tokens ------------------ - -Call `refreshToken()` to refresh an expired access token: - -:: - - await refreshToken(); - -API Fetch Helper ----------------- - -Use `apiFetch(url, options)` to make authorized requests. It automatically attaches the access token -and handles 401 errors by redirecting to the login page. - -:: - - const response = await apiFetch(`${API_BASE}/guild/${guildId}/tempvc`); - -Example Usage -------------- - -- Load guilds (servers where user has admin rights): - -:: - - await loadGuilds(); - -- Load and save TempVC settings: - -:: - - await loadTempVCModule(guildId); - await saveTempVC(guildId); - -- Load and save Welcome settings: - -:: - - await loadWelcomeModule(guildId); - await saveWelcome(guildId); - -- Load and save Levelsystem settings: - -:: - - await loadLevelsystemModule(guildId); - await saveLevelsystem(guildId); - -- Load bot statistics: - -:: - - await loadBotStats(); - -Notes ------ - -- Always use the recommended endpoint `api/managerx/stats` for bot statistics. -- Ensure all features (TempVC, Welcome, Levelsystem) are enabled in the bot config before using them. -- API errors are logged to the console and shown via alert dialogs in this example. - diff --git a/docs/dev_guide/api_reference/index.rst b/docs/dev_guide/api_reference/index.rst deleted file mode 100644 index 44eb9b4..0000000 --- a/docs/dev_guide/api_reference/index.rst +++ /dev/null @@ -1,48 +0,0 @@ -API Reference -============= - -Overview --------- - -The API of ManagerX is built using `FastAPI `_, a modern, fast web framework for building APIs with Python. FastAPI provides automatic interactive API documentation, type validation, and asynchronous support out of the box. - -The API serves as the interface between the website, dashboard, bot, and Discord API. - -API Versioning --------------- - -ManagerX API currently has a single version: - -- **v2**: The current and stable version. All endpoints are technically under `/api/v2/`, but it is **recommended to use `/api/managerx/stats`** for statistics-related requests. This ensures compatibility with future updates and simplifies integration. - -Authentication --------------- -- OAuth2 via Discord -- `/api/auth/callback` - exchange code for access token -- `/api/auth/refresh` - refresh access token -- Admin vs user permissions explained - -Error Handling --------------- -- 400 Bad Request → invalid IDs or missing parameters -- 401 Unauthorized → invalid or expired token -- 403 Forbidden → feature disabled or missing admin rights -- 500 Internal Server Error → database or Discord API issues - -Notes & Best Practices ----------------------- -- All responses are JSON -- Respect rate limits / cooldowns -- Only admins should call admin-only endpoints -- Store tokens securely - -Contents --------- -.. toctree:: - :maxdepth: 2 - :caption: API Reference: - - endpoints/stats - endpoints/guilds - endpoints/authentication - examples/api_js \ No newline at end of file diff --git a/docs/dev_guide/architecture/cog_system.rst b/docs/dev_guide/architecture/cog_system.rst deleted file mode 100644 index 99b80ab..0000000 --- a/docs/dev_guide/architecture/cog_system.rst +++ /dev/null @@ -1,183 +0,0 @@ -Cog System -==================== -The cog system of ManagerX is designed to modularize bot functionality into separate, manageable components called cogs. Each cog encapsulates a specific set of commands and event listeners, allowing for easier maintenance, scalability, and customization of the bot's features. -Cogs are implemented as Python classes that inherit from the base Cog class provided by the Pycord library. This structure enables developers to add or remove features without affecting the core bot functionality. -Key Features of the Cog System - -- **Modularity:** Each cog represents a distinct feature set, making it easy to enable or disable specific functionalities as needed. -- **Ease of Maintenance:** Isolating features into cogs simplifies debugging and updating individual components without impacting the entire bot. -- **Scalability:** New features can be added as separate cogs, allowing the bot to grow in functionality over time. -- **Event Handling:** Cogs can listen to specific events, enabling them to respond to user actions or other triggers within the Discord server. -- **Command Grouping:** Related commands can be grouped within a single cog, providing a logical organization of functionalities. -To create a new cog, developers typically define a class that extends the Cog base class and implement the desired commands and event listeners. Once defined, the cog can be loaded into the bot using the bot's load_extension method. -Overall, the cog system is a powerful architectural feature of ManagerX that enhances the bot's flexibility and maintainability, making it easier for developers to manage and expand its capabilities. - -Py-cord Emample (without Ezcord) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - from discord import slash_command - from discord.ext import commands - - class MyCog(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @commands.command() - async def my_command(self, ctx): - await ctx.send("This is a command from MyCog!") - - def setup(bot): - bot.add_cog(MyCog(bot)) - -This example demonstrates how to define a simple cog with a command. The `setup` function is used to add the cog to the bot when it is loaded. - -Ezcord Example (With Py-cord) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -With Ezcord, you can simplify cog creation even further: - -.. code-block:: python - - import ezcord - import discord - from discord import slash_command - - class MyCog(ezcord.Cog): - def __init__(self, bot): - self.bot = bot - - @slash_command() - async def my_command(self, ctx: discord.ApplicationContext): - await ctx.respond("This is a command from MyCog!") - - def setup(bot: ezcord.Bot): - bot.add_cog(MyCog(bot)) - -This example demonstrates how to create a cog using the Ezcord extension for Py-Cord, which extends Py-Cord's functionality by simplifying the creation of Discord bots with slash commands. Ezcord builds on top of Py-Cord, allowing developers to define slash commands more easily while maintaining compatibility with Py-Cord's core features. - -Cog Loading System ------------------- - -Cogs are automatically loaded from the `src/cogs/` directory when the bot starts, allowing for seamless integration of new features. ManagerX uses a dynamic cog loading system that recursively scans the cogs directory and loads all Python modules. - -Dynamic Cog Loading Implementation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ManagerX bot implements dynamic cog loading through the following process: - -.. code-block:: python - - def _load_all_cogs(self): - """ - Dynamically loads all cog modules from the src/cogs/ directory. - Returns the total number of successfully loaded cogs. - 1. Scans the cogs directory for Python files. - 2. Normalizes file paths to Python module names. - """ - cogs_dir = "src/cogs" - - # Sucht rekursiv nach allen Python-Dateien in Unterordnern von cogs - cog_files = glob.glob(f"{cogs_dir}/**/[!__]*.py", recursive=True) - total_cogs = 0 - - for file_path in cog_files: - # 1. Normalize the path to a Python module name - # This ensures that the entire path is converted to Python module naming convention. - normalized_path = file_path.replace(os.path.sep, ".").replace("/", ".") - - # 2. Remove the file extension '.py' - module_name = normalized_path[:-3] - - # 3. CHECK: Ensure that the module name begins with 'src.cogs' - if not module_name.startswith("src.cogs"): - logger.warn("COGS SKIP", f"Skipping non-standard cog path: {file_path}") - continue - - try: - self.load_extension(module_name) - logger.info(Category.COGS, f"Loaded: {module_name}") - total_cogs += 1 - except Exception as e: - logger.error("COGS FAIL", f"Laden von {module_name} fehlgeschlagen: {e.__class__.__name__}: {e}") - logger.info("COGS FAIL", "--- Start Traceback ---") - traceback.print_exc() - logger.info("COGS FAIL", "--- Ende Traceback ---") - - logger.success(Category.COGS, f"Insgesamt {total_cogs} Cogs dynamisch geladen.") - return total_cogs - -How It Works -~~~~~~~~~~~~ - -1. **Directory Scanning**: The system uses `glob.glob()` to recursively find all Python files in the `src/cogs/` directory, excluding `__init__.py` files. - -2. **Path Normalization**: File paths are converted to Python module names by: - - - Replacing OS-specific path separators (`\` on Windows, `/` on Unix) with dots - - Removing the `.py` extension - - This ensures cross-platform compatibility - -3. **Module Validation**: Each found module is checked to ensure it starts with `src.cogs`, preventing loading of unintended files. - -4. **Dynamic Loading**: The bot uses `self.load_extension(module_name)` to load each valid cog module, which triggers the `setup()` function defined in each cog file. - -5. **Error Handling**: If a cog fails to load, the error is logged with full traceback information, but the bot continues loading other cogs. - -6. **Success Reporting**: After all cogs are loaded, a success message displays the total number of cogs loaded. - -Calling the Loader -~~~~~~~~~~~~~~~~~~ - -The cog loader is called during the bot's `on_ready()` event: - -.. code-block:: python - - async def on_ready(self): - logger.success("READY", f"Logged in as {self.user}") - - # --- COG LOADING (Short form) --- - logger.loading(Category.COGS, "Starting dynamic cog loading...") - self._load_all_cogs() - # ------- - -This ensures all cogs are loaded after the bot successfully connects to Discord. - -Directory Structure -~~~~~~~~~~~~~~~~~~~ - -The cogs directory structure should follow this pattern: - -.. code-block:: - - src/cogs/ - ├── fun/ - │ ├── __init__.py - │ ├── jokes.py - │ ├── weather.py - │ └── wikipedia.py - ├── moderation/ - │ ├── __init__.py - │ ├── antispam.py - │ ├── moderation.py - │ └── warningsystem.py - ├── informationen/ - │ ├── __init__.py - │ ├── botstatus.py - │ └── serverinfo.py - └── Servermanament/ - ├── __init__.py - ├── welcome.py - └── logging.py - -Each subdirectory should contain an `__init__.py` file (can be empty) to mark it as a Python package. - -Best Practices -~~~~~~~~~~~~~~ - -- **Naming Convention**: Use lowercase names for cog directories and files -- **Initialization**: Always include a `setup()` function that adds the cog to the bot -- **Error Handling**: Include try-except blocks in your cogs to handle errors gracefully -- **Logging**: Use the bot's logger to report important events and errors -- **Organization**: Group related commands into the same cog based on functionality \ No newline at end of file diff --git a/docs/dev_guide/architecture/command_handler.rst b/docs/dev_guide/architecture/command_handler.rst deleted file mode 100644 index 76c9d5d..0000000 --- a/docs/dev_guide/architecture/command_handler.rst +++ /dev/null @@ -1,69 +0,0 @@ -Slash Command Handler for ManagerX -================================== - -The **Slash Command Handler** is a core component of ManagerX, responsible for processing and executing user commands as **Slash Commands** (``/command``). It replaces traditional prefix-based commands with Py-cord’s ``@slash_command`` system, enabling modern, native Discord interactions. - -The handler automatically registers all Slash Commands, validates parameters, and routes them to the appropriate cogs or functions for execution. - -Key Features ------------- - -- **Slash Command Registration:** All commands are registered using ``@slash_command`` in Py-cord. -- **Parameter Parsing:** Extracts and checks parameters directly from the Slash Command input. -- **Validation:** Ensures all input parameters meet expected types and formats. -- **Routing:** Directs commands to the correct cog or function for execution. -- **Error Handling:** Provides clear feedback when a command fails due to invalid input or insufficient permissions. -- **Extensibility:** Seamlessly integrates with the cog system, allowing modular command definitions. - -Command Processing Workflow ---------------------------- - -1. **Listening for Slash Commands:** Continuously monitors for Slash Command invocations. -2. **Parsing Input:** Identifies the command name and extracts parameters. -3. **Validation:** Confirms that input parameters match expected types and formats. -4. **Permission Check:** Ensures the user has the necessary permissions to execute the command. -5. **Routing to Cog:** Forwards valid commands with proper permissions to the appropriate cog or function. -6. **Execution:** Executes the command via the designated cog or function. -7. **User Feedback:** Sends a response to the user indicating success or detailing any errors encountered. - -Py-cord Slash Command Structure for ManagerX ---------------------------------------------- -ManagerX uses a modular Cog system with Slash Commands (`@slash_command`) for clean, maintainable command handling. Every command is a slash command with automatic parameter parsing, validation, and permission checks. - -1. Example Cog with Slash Commands ------------------------------------- - -.. code-block:: python - - from dicord import slash_command - from discord.ext import commands - - class FunCommands(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @slash_command(name="connect4", description="Starts a game of Connect 4 with another user.") - async def connect4(self, ctx, user: discord.Member): - # Command logic here - await ctx.respond(f"Starting Connect 4 with {user.mention}!") - - @slash_command(name="tictactoe", description="Starts a game of Tic Tac Toe with another user.") - async def tictactoe(self, ctx, user: discord.Member): - # Command logic here - await ctx.respond(f"Starting Tic Tac Toe with {user.mention}!") - - def setup(bot): - bot.add_cog(FunCommands(bot)) - -This example demonstrates how to define a cog with multiple Slash Commands. Each command is decorated with `@slash_command`, specifying its name and description. The commands accept parameters, which are automatically parsed and validated by Py-cord. - -2. Features Demonstrated ------------------------------ -- **Slash Command Registration:** `@discord.slash_command` or `@slash_command` automatically registers commands with Discord. -- **Parameter Parsing:** Parameters like `user: discord.Member` are automatically parsed and validated. -- **Validation:** Py-cord ensures parameters are of the correct type (e.g., `discord.Member`). -- **Routing:** Commands are routed to the appropriate cog methods. -- **Error Handling:** Py-cord provides built-in error handling for invalid inputs or permission issues. -- **Extensibility:** New commands can be easily added to the cog without modifying existing code. - -This structure allows ManagerX to fully utilize Slash Commands with clean cogs, parameter validation, and user feedback. \ No newline at end of file diff --git a/docs/dev_guide/architecture/database_handler.rst b/docs/dev_guide/architecture/database_handler.rst deleted file mode 100644 index ab38830..0000000 --- a/docs/dev_guide/architecture/database_handler.rst +++ /dev/null @@ -1,43 +0,0 @@ -Database Handler -========================= - -The **Database Handler** is a crucial component of ManagerX, responsible for managing all interactions with the underlying database system. It provides a structured and efficient way to store, retrieve, and manipulate data required by various features of the bot. - -Architecture -------------------------- - -The Database Handler is designed to abstract the complexities of database operations, allowing developers to interact with the database through a simplified interface. It supports various database systems, ensuring flexibility and scalability for different deployment scenarios. - -Key Features -------------------------- - -- **Connection Management:** Handles the establishment and termination of database connections, ensuring optimal resource usage. -- **Query Execution:** Provides methods to execute SQL queries and commands, including support for prepared statements to enhance security and performance. -- **Data Retrieval:** Facilitates the retrieval of data in various formats, making it easy to work with the results of database queries. -- **Error Handling:** Implements robust error handling mechanisms to manage database-related exceptions and ensure data integrity. -- **Transaction Management:** Supports database transactions, allowing for atomic operations and rollback capabilities in case of failures. -- **ORM Integration:** Optionally integrates with Object-Relational Mapping (ORM) libraries to simplify data modeling and manipulation. - -Usage -------------------------- - -Developers can utilize the Database Handler to perform CRUD (Create, Read, Update, Delete) operations on the database. The handler exposes a set of methods that can be called to interact with the database without needing to write raw SQL queries. - -Example -------------------------- - -.. code-block:: python - - # Example of using the Database Handler to fetch user data - db_handler = DatabaseHandler() - - # Fetch user by ID - user_data = db_handler.fetch_one("SELECT * FROM users WHERE id = %s", (user_id,)) - - # Insert a new user - db_handler.execute("INSERT INTO users (username, email) VALUES (%s, %s)", (username, email)) - -Conclusion -------------------------- - -The Database Handler is an essential part of ManagerX's architecture, providing a reliable and efficient way to manage data storage and retrieval. Its design focuses on ease of use, performance, and scalability, making it a vital tool for developers working with the bot's data layer. \ No newline at end of file diff --git a/docs/dev_guide/architecture/error_handler.rst b/docs/dev_guide/architecture/error_handler.rst deleted file mode 100644 index 413dd07..0000000 --- a/docs/dev_guide/architecture/error_handler.rst +++ /dev/null @@ -1,48 +0,0 @@ -Error Handler -================= - -The **Error Handler** in ManagerX is a dedicated component responsible for managing and responding to errors that occur during the bot's operation. It ensures that errors are logged appropriately and that users receive meaningful feedback when something goes wrong. - -Architecture -------------------------- - -The Error Handler is designed to capture exceptions raised during command execution, event handling, and other bot operations. It integrates with the bot's logging system to record error details, including stack traces and contextual information. - -Key Features -------------------------- - -- **Centralized Error Management:** All errors are routed through a single handler, simplifying maintenance and updates. -- **Detailed Logging:** Errors are logged with comprehensive details to facilitate debugging and issue resolution. -- **User Feedback:** Provides informative messages to users when errors occur, enhancing user experience. -- **Custom Exception Handling:** Supports custom exceptions for specific error scenarios, allowing tailored responses. -- **Extensibility:** Easily extendable to handle new types of errors as the bot's functionality grows. - -Usage -------------------------- - -Developers can utilize the Error Handler by raising exceptions within their commands or event listeners. The handler will automatically catch these exceptions and process them according to its configuration. - -Example -------------------------- - -.. code-block:: python - from discord.ext import commands - - class MyCog(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @commands.command() - async def risky_command(self, ctx): - try: - # Some operation that may fail - result = 1 / 0 # This will raise a ZeroDivisionError - except Exception as e: - raise commands.CommandError("An error occurred while executing the command.") from e - def setup(bot): - bot.add_cog(MyCog(bot)) - -Conclusion -------------------------- - -The Error Handler is a vital component of ManagerX's architecture, providing robust error management capabilities. Its design focuses on centralized handling, detailed logging, and user feedback, ensuring that both developers and users can effectively deal with errors that arise during the bot's operation. diff --git a/docs/dev_guide/architecture/event_loop.rst b/docs/dev_guide/architecture/event_loop.rst deleted file mode 100644 index 96b1300..0000000 --- a/docs/dev_guide/architecture/event_loop.rst +++ /dev/null @@ -1,42 +0,0 @@ -Event Loop -================== - -The event loop is a core component of the ManagerX architecture, responsible for handling asynchronous events and tasks. It allows the bot to efficiently manage multiple operations concurrently, ensuring responsiveness and scalability. - -Architecture ------------------- - -The event loop is built on top of Python's asyncio library, which provides the necessary infrastructure for asynchronous programming. ManagerX leverages this library to create an event-driven architecture that can handle various types of events, such as user commands, message events, and background tasks. - -Key Features ------------------- - -- **Asynchronous Execution:** The event loop enables non-blocking execution of tasks, allowing the bot to handle multiple events simultaneously without waiting for each task to complete. -- **Event Handling:** The event loop listens for events from the Discord API and dispatches them to the appropriate handlers, ensuring that user interactions are processed in real-time. -- **Task Scheduling:** The event loop can schedule tasks to run at specific intervals or after certain delays, enabling features like periodic updates and time-based actions. -- **Concurrency Management:** The event loop efficiently manages concurrent tasks, ensuring that resources are utilized optimally and that tasks do not interfere with each other. -Usage ------------------- - -Developers can interact with the event loop by defining asynchronous functions (coroutines) that are executed in response to specific events. These functions can be registered as event handlers or scheduled as background tasks. -Example ------------------- -.. code-block:: python - import asyncio - - async def my_event_handler(): - print("Event handled") - - async def main(): - # Schedule the event handler to run - asyncio.create_task(my_event_handler()) - - # Run the event loop for a short time - await asyncio.sleep(1) - - asyncio.run(main()) - -Conclusion ------------------- - -The event loop is a fundamental part of ManagerX's architecture, enabling efficient and responsive handling of asynchronous events. Its design focuses on concurrency, scalability, and real-time processing, making it a vital component for the bot's operation. diff --git a/docs/dev_guide/architecture/index.rst b/docs/dev_guide/architecture/index.rst deleted file mode 100644 index 2b676f1..0000000 --- a/docs/dev_guide/architecture/index.rst +++ /dev/null @@ -1,17 +0,0 @@ -Architecture -==================== -Architecture of ManagerX. -ManagerX is built with a modular architecture that emphasizes scalability, maintainability, and ease of development. The core components of the architecture include the event loop, command handler, database handler, cog system, logging system, and error handling mechanisms. -Each component is designed to handle specific responsibilities, allowing developers to work on individual parts of the bot without affecting the overall system. This modular approach facilitates collaboration among developers and enables the addition of new features with minimal disruption. - - -.. toctree:: - :maxdepth: 2 - :caption: Architecture Components: - - Event Loop - Command Handler - Database Handler - Cog System - Logging System - Error Handling diff --git a/docs/dev_guide/architecture/logging_system.rst b/docs/dev_guide/architecture/logging_system.rst deleted file mode 100644 index 5ff0170..0000000 --- a/docs/dev_guide/architecture/logging_system.rst +++ /dev/null @@ -1,44 +0,0 @@ -Logging System -================= - -The **Logging System** in ManagerX is a crucial component that handles the recording and management of log messages generated by the bot during its operation. It provides developers and administrators with insights into the bot's behavior, performance, and any issues that may arise. - -Architecture -------------------------- - -The Logging System is designed to capture log messages from various parts of the bot, including command execution, event handling, and system operations. It categorizes logs based on severity levels (e.g., DEBUG, INFO, WARNING, ERROR, CRITICAL) and formats them for easy readability. - -Key Features -------------------------- - -- **Centralized Logging:** All log messages are routed through a central logging handler, ensuring consistency in log formatting and storage. -- **Multiple Log Levels:** Supports various log levels to filter messages based on their importance, allowing developers to focus on critical issues or debug information as needed. -- **File and Console Output:** Logs can be directed to both console output for real-time monitoring and log files for persistent storage and later analysis. -- **Timestamping:** Each log entry is timestamped, providing context for when events occurred. -- **Custom Log Handlers:** Supports custom log handlers for specialized logging needs, such as sending logs to external monitoring services or databases. -- **Rotating Log Files:** Implements log rotation to manage log file sizes and prevent disk space exhaustion. - -Usage -------------------------- - -Developers can utilize the Logging System by importing the logging module and using predefined loggers to record messages at various levels. The system is configurable, allowing for adjustments to log levels and output formats as needed. - -Example -------------------------- - -.. code-block:: python - from logs import logger, LogLevel, Category, LogFormat - - - # Log messages at different levels - logger.debug(Category.DEBUG, "This is a debug message.") - logger.info(Category.INFO, "Bot started successfully.") - logger.warning(Category.API, "This is a warning message.") - logger.error(Category.COMMAND, "An error occurred during command execution.") - logger.critical(Category.API, "Critical issue! Immediate attention required.") - -Conclusion -------------------------- - -Requires the SimpleColoredLogs package for colored console output. -The Logging System is an essential part of ManagerX's architecture, providing robust logging capabilities that enhance the bot's maintainability and debuggability. Its design focuses on flexibility, clarity, and ease of use, making it a valuable tool for developers and administrators alike. \ No newline at end of file diff --git a/docs/dev_guide/contributing/coding_standards.rst b/docs/dev_guide/contributing/coding_standards.rst deleted file mode 100644 index 342c716..0000000 --- a/docs/dev_guide/contributing/coding_standards.rst +++ /dev/null @@ -1,16 +0,0 @@ -Coding Standards -================ - -To maintain high code quality, please follow these guidelines: - -Python Style ------------- -* **PEP8**: Follow the PEP8 style guide strictly. -* **Naming**: Use meaningful variable and function names. -* **Docstrings**: Add docstrings for all public functions and classes. - -Design Principles ------------------ -* Write **modular** and reusable code. -* Ensure **backward compatibility** wherever possible. -* Keep functions small and focused on a single task. \ No newline at end of file diff --git a/docs/dev_guide/contributing/contributing.rst b/docs/dev_guide/contributing/contributing.rst deleted file mode 100644 index 8bd0457..0000000 --- a/docs/dev_guide/contributing/contributing.rst +++ /dev/null @@ -1,17 +0,0 @@ -Contributing to ManagerX -======================== - -Welcome to the ManagerX development community! 🚀 -Your contributions help make ManagerX better for everyone. Whether you're fixing bugs, adding features, or improving documentation, we value your help. - -.. note:: - This project is permanently under development. We encourage developers to explore these resources and contribute to the growth and improvement of ManagerX! - - -Additional Information ----------------------- - -* :doc:`code_of_conduct` -* `Issue Tracker (GitHub) `_ - -Thank you for contributing! \ No newline at end of file diff --git a/docs/dev_guide/contributing/documentation.rst b/docs/dev_guide/contributing/documentation.rst deleted file mode 100644 index 50797e5..0000000 --- a/docs/dev_guide/contributing/documentation.rst +++ /dev/null @@ -1,13 +0,0 @@ -Documentation Guide -=================== - -Good documentation is just as important as good code. - -Guidelines ----------- -* **Update Docs**: If your changes affect functionality, update the relevant files. -* **Examples**: Add examples for new commands or features. -* **Format**: Use the same **reStructuredText** format as used here. - -.. hint:: - Always proofread your documentation for clarity and grammar before submitting. \ No newline at end of file diff --git a/docs/dev_guide/contributing/testing.rst b/docs/dev_guide/contributing/testing.rst deleted file mode 100644 index 5e45588..0000000 --- a/docs/dev_guide/contributing/testing.rst +++ /dev/null @@ -1,24 +0,0 @@ -Quality Assurance & Testing -=========================== - -In ManagerX, we focus on manual verification and functional integrity rather than using external testing libraries. - -How to Verify Your Changes --------------------------- - -Before submitting a Pull Request, please ensure the following: - -1. **Manual Functional Test**: - Run the application and manually trigger the feature you changed. Verify that it behaves exactly as described in your PR. - -2. **No Regression**: - Check that your changes do not break existing core functionalities of ManagerX. - -3. **Log Check**: - Monitor the console output or log files while running your changes to ensure no new warnings or errors are being triggered. - -4. **Environment Check**: - Ensure your code runs in the standard development environment without requiring additional, unlisted dependencies. - -.. warning:: - Code that causes the application to crash or introduces obvious logic errors will be sent back for revision. \ No newline at end of file diff --git a/docs/dev_guide/contributing/workflow.rst b/docs/dev_guide/contributing/workflow.rst deleted file mode 100644 index 4ce24db..0000000 --- a/docs/dev_guide/contributing/workflow.rst +++ /dev/null @@ -1,41 +0,0 @@ -Development Workflow -==================== - -Follow these 10 steps to contribute your changes to ManagerX. - -How to Contribute ------------------ - -1. **Fork the Repository**: Create your copy on GitHub. -2. **Clone Your Fork**: - .. code-block:: bash - - git clone https://github.com/Oppro-net-Development/ManagerX.git - -3. **Create a New Branch**: - .. code-block:: bash - - git checkout -b feature/your-feature-name - -4. **Make Your Changes**: Follow our coding standards. -5. **Test Your Changes**: Ensure everything works. -6. **Commit Your Changes**: - .. code-block:: bash - - git add . - git commit -m "Add feature: your description" - -7. **Push Your Changes**: - .. code-block:: bash - - git push origin feature/your-feature-name - -8. **Create a Pull Request**: Navigate to the original ManagerX repo. -9. **Address Feedback**: Collaborate with maintainers. -10. **Celebrate!** 🎉 - -Issue Reporting ---------------- - -If you encounter any bugs, please report them on the `GitHub Issues `_ tab. -Include steps to reproduce, expected behavior, and logs. \ No newline at end of file diff --git a/docs/dev_guide/database/index.rst b/docs/dev_guide/database/index.rst deleted file mode 100644 index 82d20eb..0000000 --- a/docs/dev_guide/database/index.rst +++ /dev/null @@ -1,514 +0,0 @@ -Database & Database Handler -================================= - -ManagerX uses SQLite databases to persist data for various features. Each database handler is responsible for managing a specific feature's data storage. - -.. toctree:: - :maxdepth: 2 - :caption: Database Handlers: - - AutoDelete Database - Spam Detection Database - Warning System Database - Welcome System Database - Level System Database - Logging Database - Notes Database - Global Chat Database - Voice Channel Database - Stats Database - - -Database Overview ------------------ - -The following databases are used in ManagerX: - -.. list-table:: - :header-rows: 1 - :widths: 20 50 30 - - * - Database File - - Purpose - - Handler Class - * - `autodelete.db` - - Auto-delete messages in channels - - `AutoDeleteDB` - * - `spam.db` - - Anti-spam detection and configuration - - `SpamDB` - * - `warns.db` - - User warning system - - `WarnDatabase` - * - `welcome.db` - - Welcome messages and settings - - `WelcomeDatabase` - * - `levelsystem.db` - - User levels and XP tracking - - `LevelDatabase` - * - `log_channels.db` - - Server logging configuration - - `LoggingDatabase` - * - `notes.db` - - User notes and moderator notes - - `NotesDatabase` - * - `globalchat.db` - - Global chat network settings - - `GlobalChatDB` - * - `vc.db` - - Voice channel management - - `VoiceChannelDB` - * - `stats.db` - - Server statistics - - `StatsDB` - -Detailed Database Documentation --------------------------------- - -AutoDelete Database -~~~~~~~~~~~~~~~~~~~ - -**File:** `data/autodelete.db` - -**Purpose:** Manages auto-deletion of messages in specific channels. - -**Tables:** - -- **autodelete**: Main configuration table - - - `channel_id`: Channel ID (UNIQUE) - - `duration`: Seconds before message deletion - - `exclude_pinned`: Exclude pinned messages (default: 1) - - `exclude_bots`: Exclude bot messages (default: 0) - - `created_at`: Timestamp of creation - - `updated_at`: Last update timestamp - -- **autodelete_whitelist**: User/Role whitelist - - - `channel_id`: Reference to autodelete channel - - `target_id`: User or Role ID - - `target_type`: 'user' or 'role' - - `added_at`: When added to whitelist - -- **autodelete_schedules**: Scheduled deletion timeframes - - - `channel_id`: Reference to autodelete channel - - `start_time`: Start time (HH:MM format) - - `end_time`: End time (HH:MM format) - - `days`: Days of week (JSON array or comma-separated) - -- **autodelete_stats**: Statistics tracking - - - `channel_id`: Reference to autodelete channel - - `deleted_count`: Total messages deleted - - `error_count`: Failed deletion attempts - - `last_deletion`: Timestamp of last deletion - -**Key Methods:** - -.. code-block:: python - - # Add or update auto-delete configuration - add_autodelete(channel_id, duration, exclude_pinned=True, exclude_bots=False) - - # Add user/role to whitelist - add_whitelist(channel_id, target_id, target_type) - - # Get configuration for channel - get_autodelete(channel_id) - ---- - -Spam Detection Database -~~~~~~~~~~~~~~~~~~~~~~~ - -**File:** `data/spam.db` - -**Purpose:** Tracks spam patterns and manages anti-spam settings. - -**Tables:** - -- **spam_settings**: Server spam configuration - - - `guild_id`: Server ID (PRIMARY KEY) - - `max_messages`: Max messages in time window - - `time_window`: Time window in seconds - - `action`: Action to take (kick, mute, delete) - - `created_at`: Configuration creation date - - `updated_at`: Last configuration update - -- **spam_logs**: Spam detection logs - - - `id`: Log entry ID - - `guild_id`: Server ID - - `user_id`: User ID - - `message_count`: Number of messages - - `timestamp`: Detection timestamp - - `action_taken`: Action that was performed - -- **spam_whitelist**: Exempt users/roles - - - `guild_id`: Server ID - - `target_id`: User or Role ID - - `target_type`: 'user' or 'role' - - `added_by`: User ID who added to whitelist - - `reason`: Reason for whitelist - - `added_at`: When added - -**Features:** - -- Context manager for database operations -- Automatic database migration support -- Enhanced error handling and logging -- Support for user and role whitelisting - -**Key Methods:** - -.. code-block:: python - - # Get spam settings for guild - get_spam_settings(guild_id) - - # Update spam detection settings - update_spam_settings(guild_id, max_messages, time_window) - - # Log spam detection - add_spam_log(guild_id, user_id, message_count) - - # Add user to whitelist - add_to_whitelist(guild_id, target_id, target_type, reason) - ---- - -Warning System Database -~~~~~~~~~~~~~~~~~~~~~~~ - -**File:** `data/Datenbanken/warns.db` - -**Purpose:** Stores user warnings for moderation. - -**Tables:** - -- **warns**: Warning records - - - `id`: Warning ID (PRIMARY KEY) - - `guild_id`: Server ID - - `user_id`: User ID - - `moderator_id`: Moderator who issued warning - - `reason`: Warning reason - - `timestamp`: When warning was issued - -**Key Methods:** - -.. code-block:: python - - # Add warning for user - add_warning(guild_id, user_id, moderator_id, reason, timestamp) - - # Get all warnings for user - get_warnings(guild_id, user_id) - - # Get warning by ID - get_warning_by_id(warn_id) - - # Delete warning - delete_warning(warn_id) - - # Get warning count - get_warning_count(guild_id, user_id) - ---- - -Welcome System Database -~~~~~~~~~~~~~~~~~~~~~~~ - -**File:** `data/welcome.db` - -**Purpose:** Manages welcome message configuration and settings. - -**Tables:** - -- **welcome_settings**: Server welcome configuration - - - `guild_id`: Server ID (PRIMARY KEY) - - `channel_id`: Welcome channel ID - - `welcome_message`: Welcome message text - - `enabled`: Whether welcome is enabled - - `embed_enabled`: Use embed format - - `embed_color`: Embed color (HEX format) - - `embed_title`: Embed title - - `embed_description`: Embed description - - `embed_thumbnail`: Show member avatar - - `embed_footer`: Embed footer text - - `ping_user`: Ping the new user - - `delete_after`: Auto-delete after N seconds - - `created_at`: Creation timestamp - - `updated_at`: Last update timestamp - -**Features:** - -- Supports both text and embed messages -- Automatic database migration -- Backward compatibility with older versions -- Asynchronous and synchronous methods - -**Key Methods:** - -.. code-block:: python - - # Update welcome settings - await update_welcome_settings(guild_id, channel_id=None, message=None, ...) - - # Get welcome settings - await get_welcome_settings(guild_id) - - # Enable/disable welcome - await toggle_welcome(guild_id, enabled) - - # Delete welcome settings - await delete_welcome_settings(guild_id) - ---- - -Level System Database -~~~~~~~~~~~~~~~~~~~~~ - -**File:** `data/levelsystem.db` - -**Purpose:** Tracks user XP, levels, and progression. - -**Tables:** - -- **user_levels**: User XP and level data - - - `guild_id`: Server ID - - `user_id`: User ID - - `level`: Current level - - `xp`: Current XP - - `total_xp`: Total XP earned - - `last_message_time`: Last message timestamp - - `prestige_count`: Prestige level - -- **level_roles**: Reward roles for levels - - - `guild_id`: Server ID - - `level`: Level requirement - - `role_id`: Reward role ID - -- **level_settings**: Server configuration - - - `guild_id`: Server ID - - `enabled`: Level system enabled - - `xp_per_message`: XP per message - - `cooldown_seconds`: Message cooldown - -**Features:** - -- Anti-spam detection to prevent XP farming -- Level role rewards -- Prestige system -- Caching for performance -- Comprehensive logging - -**Key Methods:** - -.. code-block:: python - - # Add XP to user (with anti-spam check) - add_xp(guild_id, user_id, xp_amount) - - # Get user level data - get_user_level(guild_id, user_id) - - # Set level role reward - set_level_role(guild_id, level, role_id) - - # Get leaderboard - get_leaderboard(guild_id, limit=10) - ---- - -Logging Database -~~~~~~~~~~~~~~~~ - -**File:** `data/log_channels.db` - -**Purpose:** Stores logging channel configuration for different log types. - -**Tables:** - -- **log_channels**: Log channel configuration - - - `guild_id`: Server ID - - `log_type`: Type of log (member_join, member_leave, message_delete, etc.) - - `channel_id`: Discord channel ID for logs - - `enabled`: Whether this log type is enabled - - `created_at`: Creation timestamp - - `updated_at`: Last update timestamp - -**Log Types Supported:** - -- `member_join`: New member joins -- `member_leave`: Member leaves -- `member_ban`: Member banned -- `member_kick`: Member kicked -- `member_unban`: Member unbanned -- `message_delete`: Message deletion -- `message_edit`: Message editing -- `role_create`: Role created -- `role_delete`: Role deleted -- `channel_create`: Channel created -- `channel_delete`: Channel deleted - -**Key Methods:** - -.. code-block:: python - - # Set log channel for type - set_log_channel(guild_id, log_type, channel_id) - - # Get log channel for type - get_log_channel(guild_id, log_type) - - # Enable/disable log type - set_log_enabled(guild_id, log_type, enabled) - - # Get all logs for guild - get_all_logs(guild_id) - ---- - -Notes Database -~~~~~~~~~~~~~~ - -**File:** `data/notes.db` - -**Purpose:** Stores moderator notes about users. - -**Tables:** - -- **notes**: User notes - - - `id`: Note ID (PRIMARY KEY) - - `guild_id`: Server ID - - `user_id`: User the note is about - - `author_id`: User who created the note - - `author_name`: Name of note author - - `note`: Note content - - `timestamp`: Creation timestamp - -**Key Methods:** - -.. code-block:: python - - # Add note to user - add_note(guild_id, user_id, author_id, author_name, note, timestamp) - - # Get all notes for user - get_notes(guild_id, user_id) - - # Get specific note - get_note_by_id(note_id) - - # Delete note - delete_note(note_id) - ---- - -Database Patterns and Best Practices ------------------------------------- - -Connection Management -~~~~~~~~~~~~~~~~~~~~~ - -All database handlers use context managers for safe connection handling: - -.. code-block:: python - - @contextmanager - def get_cursor(self): - """Context manager for database operations""" - cursor = self.conn.cursor() - try: - yield cursor - except sqlite3.Error as e: - self.conn.rollback() - logger.error(f"Database error: {e}") - raise - finally: - cursor.close() - -Error Handling -~~~~~~~~~~~~~~ - -Databases include comprehensive error handling: - -- Custom exception classes (e.g., `SpamDBError`) -- Automatic rollback on errors -- Detailed logging of all operations -- Graceful degradation when operations fail - -Caching and Performance -~~~~~~~~~~~~~~~~~~~~~~~ - -Some databases implement caching for frequently accessed data: - -.. code-block:: python - - # Example from LevelDatabase - self.level_roles_cache = {} - self.enabled_guilds_cache = set() - self.guild_configs_cache = {} - -Migration Strategy -~~~~~~~~~~~~~~~~~~ - -Databases support automatic schema migration: - -.. code-block:: python - - def _migrate_database(self): - """Handle database migrations for schema changes""" - # Adds new columns to existing tables - # Migrates data from old structure to new - # Maintains backward compatibility - -Directory Structure -~~~~~~~~~~~~~~~~~~~ - -Database files are stored in the `data/` directory: - -.. code-block:: - - data/ - ├── autodelete.db - ├── spam.db - ├── welcome.db - ├── levelsystem.db - ├── log_channels.db - ├── notes.db - ├── globalchat.db - ├── vc.db - ├── stats.db - └── Datenbanken/ - └── warns.db - -Access Location -~~~~~~~~~~~~~~~ - -All database handlers are located in: - -.. code-block:: - - src/DevTools/backend/database/ - ├── __init__.py - ├── autodelete_db.py - ├── spam_db.py - ├── warn_db.py - ├── welcome_db.py - ├── levelsystem_db.py - ├── logging_db.py - ├── notes_db.py - ├── globalchat_db.py - ├── vc_db.py - └── stats_db.py \ No newline at end of file diff --git a/docs/dev_guide/index.rst b/docs/dev_guide/index.rst deleted file mode 100644 index 5989969..0000000 --- a/docs/dev_guide/index.rst +++ /dev/null @@ -1,50 +0,0 @@ -Developer Guide -========================= - -Welcome to the ManagerX Developer Guide! This section provides in-depth information for developers looking to contribute to or extend ManagerX. - -.. note:: - This project is permanently under development. We recommend checking this guide frequently for updates. - -.. toctree:: - :maxdepth: 2 - :caption: Getting Started: - - Installation - Development Setup - Quickstart Guide - -.. toctree:: - :maxdepth: 2 - :caption: Architecture: - - Architecture Overview - API Reference - -.. toctree:: - :maxdepth: 2 - :titlesonly: - :caption: Contribution: - - Contribution - Workflow - Coding Standards - Testing Procedures - Documentation Guidelines - -.. toctree:: - :maxdepth: 2 - :caption: Advanced Topics: - - Performance Optimization - Security Best Practices - -.. toctree:: - :maxdepth: 1 - :caption: Community & Support: - - FAQ - Glossary - Support Channels - -We encourage all developers to actively participate in the ManagerX community. Your contributions help make ManagerX better for everyone! \ No newline at end of file diff --git a/docs/dev_guide/installation/index.rst b/docs/dev_guide/installation/index.rst deleted file mode 100644 index 8f0b1de..0000000 --- a/docs/dev_guide/installation/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -installation of ManagerX -=============================== - -This guide explains how to install ManagerX on your system. - -.. toctree:: - :maxdepth: 2 - :caption: Installation Methods: - - Install from Source Code - Dependencies & Requirements \ No newline at end of file diff --git a/docs/dev_guide/installation/install_from_source.rst b/docs/dev_guide/installation/install_from_source.rst deleted file mode 100644 index c50f23e..0000000 --- a/docs/dev_guide/installation/install_from_source.rst +++ /dev/null @@ -1,105 +0,0 @@ -============================= -Install from Source Code -============================= - -This guide explains how to install **ManagerX** directly from its source code. This method is ideal for developers looking to contribute or those who require the latest features from the repository. - -.. note:: - If you encounter any issues during installation, please check our Troubleshooting section or open an issue on GitHub. - -Prerequisites -------------- - -Before proceeding, ensure you have the following installed on your system: - -* **Python 3.10** or higher -* **Git** -* A valid **Discord Bot Token** from the `Discord Developer Portal `_ - -Cloning the Repository ----------------------- - -First, clone the ManagerX repository from GitHub and navigate into the project directory: - -.. code-block:: bash - - git clone https://github.com/Oppro-net-Development/ManagerX.git - cd ManagerX - -Setting Up a Virtual Environment --------------------------------- - -It is highly recommended to use a virtual environment to isolate dependencies and avoid conflicts with your system's Python packages. - -**On Linux/macOS:** - -.. code-block:: bash - - python3 -m venv venv - source venv/bin/activate - -**On Windows:** - -.. code-block:: bash - - python -m venv venv - .\venv\Scripts\activate - -Installing Dependencies ------------------------ - -ManagerX uses modular requirement files depending on your use case. Choose **one** of the following options: - -**1. Standard Installation** -Basic requirements for running the application: - -.. code-block:: bash - - pip install -r requirements/req.txt - -**2. Bot Only** -Minimal requirements to run only the Discord bot component: - -.. code-block:: bash - - pip install -r requirements/bot_req.txt - -**3. Development Environment** -Includes testing frameworks and code formatters (e.g., black, pytest): - -.. code-block:: bash - - pip install -r requirements/dev_req.txt - -**4. Documentation Tools** -Includes tools like Sphinx for building the documentation: - -.. code-block:: bash - - pip install -r requirements/docs_req.txt - -Configuration -------------- - -Before running the bot, you need to set up your environment variables. - -1. Create a ``.env`` file in the root directory. -2. Add your bot token as follows: - -.. code-block:: text - - TOKEN=your_discord_bot_token_here - -Running ManagerX ----------------- - -Once the installation and configuration are complete, start the application: - -.. code-block:: bash - - python main.py - -You should see an output indicating that **ManagerX** is successfully connected to Discord. - -.. tip:: - Ensure that **Privileged Gateway Intents** (Member, Presence, Message Content) are enabled in the Discord Developer Portal under the "Bot" tab, or the bot may not function correctly. \ No newline at end of file diff --git a/docs/dev_guide/self_hosting/index.rst b/docs/dev_guide/self_hosting/index.rst deleted file mode 100644 index 7692402..0000000 --- a/docs/dev_guide/self_hosting/index.rst +++ /dev/null @@ -1,29 +0,0 @@ -How to self-host ManagerX? -============================== -This guide will walk you through the steps required to self-host ManagerX on your own server. - -Prerequisites -------------- - -- A server or VPS with at least 2GB of RAM - -- Python 3.8 or higher installed - -- Git installed - -- A Discord bot token (create a bot on the Discord Developer Portal) - -Step 1: Clone the Repository ----------------------------- -First, clone the ManagerX repository from GitHub: - -:: - git clone https://github.com/Oppro-net-Development/ManagerX.git - - -Step 2: Install Dependencies - -:: - pip install -r requirements/req.txt - - diff --git a/docs/make.bat b/docs/make.bat index 954237b..dc1312a 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -7,8 +7,8 @@ REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) -set SOURCEDIR=. -set BUILDDIR=_build +set SOURCEDIR=source +set BUILDDIR=build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst deleted file mode 100644 index b27cbc7..0000000 --- a/docs/plugins/index.rst +++ /dev/null @@ -1,47 +0,0 @@ -Available Plugins for ManagerX -============================== - -No Plugins Available 😥 ------------------------ - -At the moment, there are **no plugins available** for ManagerX. -We are actively working on developing a variety of plugins to expand the functionality of ManagerX. -Stay tuned for future releases! - -Code Your Own Plugins ---------------------- - -If you're interested in creating your own plugins for ManagerX, the **Developer Guide** provides detailed instructions, examples, and best practices. -Read the `Own Plugins Guide `_ to get started on building custom plugins tailored to your needs. - -Submitting Your Plugin ----------------------- - -Once your plugin is ready and tested, you can submit it to the ManagerX team for review: - -1. **Ensure your plugin meets all requirements** - - Follows the **ManagerX Plugin Guidelines** (safety, structure, versioning, documentation). - - Verified to be **free of malicious code**. - -2. **Prepare a repository** - - Include your plugin code in a clean GitHub repository. - - Provide a `README.md` and any required metadata (e.g., `pyproject.toml` for PyPI plugins). - -3. **Submit for review via Discord** - - Join the **OPPRO.NET Development** Discord server. - - Go to the `#plugins` channel. - - Submit your plugin repository **with the tag `ManagerX`**. - - The ManagerX team will review your submission for **quality, security, and compliance**. - -4. **Approval & Listing** - - Approved plugins will be listed in the **Official Plugins** section of the docs. - - Plugins that are safe but not officially endorsed may appear in the **Other Plugins** section. - -.. toctree:: - :maxdepth: 2 - :caption: Plugin Documentation: - :hidden: - - Own Plugins Guide - Official Plugins - Other Plugins diff --git a/docs/plugins/official_plugins/managerx_handler.rst b/docs/plugins/official_plugins/managerx_handler.rst deleted file mode 100644 index a6cb9c0..0000000 --- a/docs/plugins/official_plugins/managerx_handler.rst +++ /dev/null @@ -1,50 +0,0 @@ -========================== -ManagerX-Handler (Official) -========================== - -**The Definitive Integration Layer for the ManagerX Ecosystem** - -Official Core Distribution --------------------------- - -The **ManagerX-Handler** is the officially maintained plugin developed and issued by the **OPPRO.NET Development Team (ManagerX)**. It serves as the authoritative bridge between the ManagerX management system and your local platform. - -As an official distribution, this handler is engineered to the exact specifications of the ManagerX core, ensuring 100% compatibility, high-level security, and high-performance resource monitoring. - -Key Features ------------- - -* **Native Integration**: Direct, low-latency communication with the ManagerX API. -* **Official Maintenance**: Guaranteed compatibility with all future ManagerX updates. -* **Resource Monitoring**: Optimized for real-time tracking and seamless data synchronization. -* **Enterprise Standards**: Standardized deployment suitable for both production and development environments. - -Installation ------------- - -The ManagerX-Handler is distributed via PyPi for secure and version-controlled deployment. - -.. code-block:: bash - - pip install managerx-handler - -*Note: To ensure system stability, always verify you are installing the official package from the OPPRO.NET PyPi repository.* - -Usage & Implementation ----------------------- - -Once installed, the ManagerX-Handler integrates as a core component of your environment. It automatically establishes the necessary communication hooks for platform interoperability. - -For detailed API references and advanced implementation guides, please refer to the official documentation provided by the **OPPRO.NET Development Team**. - -Support & Contact ------------------ - -As this is an official product, we provide dedicated support for our ecosystem. - -* **Lead Developers**: OPPRO.NET Development Team -* **Official Support**: development@oppro-network.de -* **Official Website**: https://oppro.net - -Copyright (c) 2026 OPPRO.NET Network | All Rights Reserved. -*This software is an official release. Unauthorized redistribution or modification is strictly prohibited.* \ No newline at end of file diff --git a/docs/plugins/own_plugins/create_local_plugin.rst b/docs/plugins/own_plugins/create_local_plugin.rst deleted file mode 100644 index 2da8bae..0000000 --- a/docs/plugins/own_plugins/create_local_plugin.rst +++ /dev/null @@ -1,103 +0,0 @@ -Local Plugins (Main GitHub Repository) -================================================ - -In addition to PyPI-based plugins, ManagerX also supports **local plugins** that live directly inside the **main GitHub repository**. - -These plugins are intended for: -- experimental features -- optional extensions -- community contributions -- features that may become core plugins later - -Local plugins are **not enabled or shipped by default**. - -Design Principle --------------------------------- - -Local plugins follow the same core philosophy as external plugins: - -- ❌ Not part of the default ManagerX installation -- 🔒 Do not affect the core unless explicitly enabled -- 🧪 Can evolve independently - -This keeps the ManagerX core clean while still allowing flexibility and experimentation. - -Repository Structure ------------------------------------- - -Local plugins are stored in a dedicated folder inside the main GitHub repository. - -Example structure: - -:: - - plugins/ - ├── README.md - ├── example_plugin/ - │ ├── plugin.py - │ └── README.md - ├── logging_plugin/ - │ ├── plugin.py - │ └── README.md - -- Each plugin lives in its own folder -- ``plugin.py`` contains the implementation -- ``README.md`` explains usage, configuration, and behavior - -Local plugins **must not** modify core files directly. - -Enabling Local Plugins --------------------------------------- - -Local plugins are **disabled by default**. - -To enable a local plugin: -1. Place it inside the ``plugins/`` directory -2. Enable it via the ManagerX configuration -3. Restart ManagerX - -Only explicitly enabled plugins are loaded. - -Differences to PyPI Plugins -------------------------------------------- - -+-------------------+------------------------+-------------------------+ -| Feature | Local Plugins | PyPI Plugins | -+===================+========================+=========================+ -| Location | Main GitHub repository | External (PyPI) | -+-------------------+------------------------+-------------------------+ -| Installed via pip | No | Yes | -+-------------------+------------------------+-------------------------+ -| Enabled by default| No | No | -+-------------------+------------------------+-------------------------+ -| Naming scheme | Flexible | ``managerx-*`` required | -+-------------------+------------------------+-------------------------+ -| Intended use | Experimental / optional| Public distribution | -+-------------------+------------------------+-------------------------+ - -Promotion to PyPI ---------------------------------- - -A local plugin can later be **promoted to a PyPI plugin** if it: -- proves stable -- has good documentation -- is useful to a wider audience - -In this case, it must follow the PyPI naming scheme: - -:: - - managerx-[plugin-name] - -Conclusion --------------------------- - -Local plugins provide a **safe space for innovation** without increasing the default footprint of ManagerX. - -They allow contributors to: -- test new ideas -- share optional features -- collaborate inside the main repository - -All without forcing functionality onto every ManagerX user. -Happy coding! 🚀 \ No newline at end of file diff --git a/docs/plugins/own_plugins/create_pypi_plugin.rst b/docs/plugins/own_plugins/create_pypi_plugin.rst deleted file mode 100644 index a968f8d..0000000 --- a/docs/plugins/own_plugins/create_pypi_plugin.rst +++ /dev/null @@ -1,102 +0,0 @@ -Create your own PyPI Plugin for ManagerX -====================================================== - -ManagerX provides a flexible **plugin system** that allows developers to extend its functionality without modifying the core project. Plugins are developed as **separate Python packages** and distributed via **PyPI**. - -Plugin Philosophy --------------------------- - -Plugins are **not included by default** in ManagerX. - -This is an intentional design decision to keep ManagerX: -- lightweight -- secure -- easy to maintain - -Every plugin is installed **explicitly by the user** and lives outside the core project. - -Plugin Naming Convention ---------------------------- - -All ManagerX plugins **must** follow this naming scheme: - -:: - - managerx-[your-plugin-name] - -Examples: -- ``managerx-logger`` -- ``managerx-moderation`` -- ``managerx-backup`` - -This naming convention ensures: -- clear identification on PyPI -- no name collisions -- automatic recognition by ManagerX - -Plugin Examples on GitHub ----------------------------------- - -The official ManagerX GitHub repository contains an **examples folder** to help developers get started. - -Structure: - -:: - - examples/ - └── plugins/ - ├── basic_plugin/ - │ ├── plugin.py - │ └── README.md - ├── advanced_plugin/ - │ ├── plugin.py - │ └── README.md - -- ``.py`` files show how to implement plugins -- ``.md`` files explain the plugin logic and structure -- examples are **reference implementations**, not production code - -How Plugins Work -------------------- - -A ManagerX plugin is a **standalone Python package** that: - -- is installed via ``pip`` -- exposes a defined entry point -- is detected and loaded automatically by ManagerX - -Once installed, ManagerX scans for compatible plugins and enables them at runtime. - -Installing a Plugin ----------------------------- - -Plugins are installed like any other PyPI package: - -:: - - pip install managerx-your-plugin-name - -After installation, restart ManagerX to activate the plugin. - -Creating Your Own Plugin ------------------------------- - -1. Create a new Python project -2. Name it using the required prefix: ``managerx-`` -3. Implement the plugin interface -4. Add documentation (README.md) -5. Publish the package to PyPI - -By following standard Python packaging rules, plugins remain: -- easy to install -- easy to update -- easy to remove - -Conclusion ----------------- - -The ManagerX plugin system is designed for **modularity and freedom**. -You decide which features you need, and plugins provide them — without bloating the core. - -Build small, focused plugins and share them with the community 🚀 -Happy coding! diff --git a/docs/plugins/own_plugins/index.rst b/docs/plugins/own_plugins/index.rst deleted file mode 100644 index 1a64e56..0000000 --- a/docs/plugins/own_plugins/index.rst +++ /dev/null @@ -1,77 +0,0 @@ -Developing Your Own Plugins for ManagerX -======================================== - -Extend ManagerX with your own custom functionality! This guide helps you create plugins, -whether as local Cogs in the bot or as standalone PyPi projects, from setup to deployment. - -Getting Started ---------------- - -Before you begin, make sure you have: - -- Python 3.8+ installed -- A basic understanding of Python -- Familiarity with the ManagerX bot architecture - -1. Set Up Your Development Environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Install all dependencies required by ManagerX. -- Clone the ManagerX repository from GitHub to get the latest codebase. -- For PyPi plugins, ensure you have `setuptools` and `wheel` installed to package your plugin. - -2. Understand the Plugin Structure -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- **Local Cogs**: look inside the `cogs` or `plugins` directory. Each plugin is typically a single Python file or module. -- **PyPi Plugins**: structure your project like a standard Python package with a `setup.py` or `pyproject.toml` file. -- Review existing plugins to understand naming conventions, commands, and event listeners. - -3. Create Your Plugin -~~~~~~~~~~~~~~~~~~~~~~~~ - -**Local Cog Example:** - -.. code-block:: python - - from discord.ext import commands - - class MyPlugin(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @commands.slash_command(name="hello", description="Say hello!") - async def hello(self, ctx): - await ctx.respond(f"Hello, {ctx.author.mention}!") - - def setup(bot): - bot.add_cog(MyPlugin(bot)) - -**PyPi Plugin Tips:** - -- Wrap your plugin in a package structure (`myplugin/`) -- Include a `setup.py` or `pyproject.toml` for distribution -- Make sure your Cog is loaded automatically when installed - -4. Register Your Plugin -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- **Local Cogs**: add the Cog in your bot’s `setup` function or main script. -- **PyPi Plugins**: once installed via `pip`, ensure ManagerX can discover and load your plugin dynamically. - -Best Practices ---------------- - -- Keep your plugin modular, documented, and maintainable. -- Use ManagerX’s `SimpleColoredLogs` for logging plugin events and errors. -- Follow existing naming and coding conventions for compatibility. -- For PyPi plugins, include versioning, dependencies, and metadata in your package. - - -.. toctree:: - :maxdepth: 2 - :caption: Next Steps - - Create a PyPi Plugin - Local Cog Development - Plugin Guidelines \ No newline at end of file diff --git a/docs/plugins/own_plugins/plugin_guidelines.rst b/docs/plugins/own_plugins/plugin_guidelines.rst deleted file mode 100644 index 49e36ae..0000000 --- a/docs/plugins/own_plugins/plugin_guidelines.rst +++ /dev/null @@ -1,182 +0,0 @@ -Plugin Policy -============================================ - -This document defines the official rules and requirements for all ManagerX -plugins, including **Local Plugins** and **PyPI Plugins**. - -The goal of this policy is to keep ManagerX: -- stable -- secure -- modular -- lightweight - -Plugins are never part of the core by default. - -------------------------------------------------------------------------------------------------------------------- - -General Principles ----------------------------------- - -- All plugins are **optional**. -- All plugins are **disabled by default**. -- The ManagerX core must never depend on plugins. -- Plugins must not modify or patch core files. - ---------------------------------------------------------------------------------------------------- - -Plugin Types ----------------------------- - -ManagerX supports two types of plugins: - -1. **PyPI Plugins** -2. **Local Plugins** - -**Local Plugins**: -- Live in the main ManagerX repository under the folder: - -:: - - plugins/ - -- Are **not Cogs**, but fully independent plugins -- Can be experimental or optional features -- Are never enabled by default - -**PyPI Plugins**: -- Distributed via PyPI -- Must follow the naming convention: - -:: - - managerx-[plugin-name] - -- Fully independent and always stored in their own GitHub repository - ------------------------------------------------------------------------------------ - -License ------------------------ - -- ManagerX itself is licensed under **GPL-3.0**. -- Local Plugins included in the repository **must also be GPL-3.0 compatible**. -- PyPI Plugins can use any license **compatible with GPL-3.0**. -- Plugins without a clear license will **not be accepted**. - ------------------------------------------------------------------------------------ - -Source Code Separation Requirement --------------------------------------------------- - -All plugins, whether Local or PyPI, must have a **dedicated GitHub repository**. - -- The repository in the main ManagerX repo (`plugins/`) is only a mirror or example. -- The **canonical source** is always the plugin's own repo. -- One plugin = one repository. - -------------------------------------------------------------------- - -Ownership & Responsibility ------------------------------------------- - -- Each plugin must have a clearly defined owner or maintainer. -- The owner is responsible for: - - bugs - - security issues - - legal matters -- ManagerX does **not** provide support for third-party plugins. - ------------------------------------------------------------------------------------ - -Security Requirements -------------------------------------- - -Plugins must not: -- contain malicious code -- collect tokens or credentials -- perform hidden network requests -- auto-update without user consent - -------------------------------------------------------------------- - -Versioning Rules --------------------------------- - -- Every plugin must define a version. -- Breaking changes require a major version bump. -- Plugin versions are independent from ManagerX versions. - ---- - -Documentation Requirement ------------------------------------------ - -Each plugin must include a `README.md` that explains: -- what the plugin does -- how to enable or install it -- configuration options -- dependencies - -Plugins without documentation are not accepted. - -------------------------------------------------------------------- - -Compatibility Rules ------------------------------------ - -- Plugins must declare supported ManagerX versions. -- Plugins must use only public plugin APIs. -- Plugins must not depend on private core interfaces. - ---- - -Enable / Disable & Removal ------------------------------------------- - -- Plugins must be safely disableable. -- Plugins must not block ManagerX startup. -- Uninstalling a plugin must not leave persistent data behind. - ---- - -Official Plugin Status --------------------------------------- - -If a plugin fulfills **all requirements** in this policy, it **may** be: - -- listed in the official ManagerX documentation -- marked as an **Official ManagerX Plugin** - -Official status means: -- recommended by ManagerX -- documented in the main docs -- still optional and not part of the core - -Promotion is evaluated on: -- stability -- code quality -- documentation -- maintenance activity - ---- - -Final Checklist -------------------------------- - -Before a plugin can be accepted or promoted: - -- [ ] Separate GitHub repository -- [ ] Correct naming (PyPI only) -- [ ] License included (GPL-3.0 compatible for Local Plugins) -- [ ] Version defined -- [ ] README.md present -- [ ] No core modifications -- [ ] Disabled by default - ---- - -Conclusion --------------------------- - -Local and PyPI plugins provide **modularity and freedom**. -The core remains clean and minimal, while plugins can evolve independently. \ No newline at end of file diff --git a/docs/releases/alpha/1.7.2a.rst b/docs/releases/alpha/1.7.2a.rst deleted file mode 100644 index 185c443..0000000 --- a/docs/releases/alpha/1.7.2a.rst +++ /dev/null @@ -1,20 +0,0 @@ -Release of Version 1.7.2 Alpha -============================== - -🛠️ **Update:** Version 1.7.2 Alpha - -**Released by:** @Medicopter117 - -**Release Date:** Nov 20 - ---- - -📝 **Description** - -This is the latest Alpha release of ManagerX. - -Full changelog: v1.7.2-beta → v1.7.2-alpha - - -**Contributors** -- @Medicopter117 diff --git a/docs/releases/beta/1.7.2b.rst b/docs/releases/beta/1.7.2b.rst deleted file mode 100644 index 41282a5..0000000 --- a/docs/releases/beta/1.7.2b.rst +++ /dev/null @@ -1,18 +0,0 @@ -Release of Version 1.7.2 Beta -============================= - -🛠️ **Update:** Version 1.7.2 Beta - -**Released by:** @Medicopter117 - -**Release Date:** Nov 11 - ---- - -📝 **Description** -This is a pre-release Beta version. - -Update includes container added. - -**Contributors** -- @Medicopter117 diff --git a/docs/releases/index.rst b/docs/releases/index.rst deleted file mode 100644 index 53c236a..0000000 --- a/docs/releases/index.rst +++ /dev/null @@ -1,48 +0,0 @@ -Changelog & Releases of ManagerX -=============================================== - -Stable Versions ----------------- - -Here you can find all stable releases of ManagerX. These versions have been thoroughly tested and are recommended for production use. - -.. toctree:: - :maxdepth: 2 - :caption: Stable: - - ✅ 1.7.1 - ✅ 1.7.0 - ✅ 1.6.6 - ✅ 1.6.5 - ✅ 1.6.4 - ✅ 1.6.3 - ✅ 1.6.2 - ✅ 1.6.1 - ✅ 1.6.0 - ✅ 1.5.0 - ✅ 1.4LOG - ✅ 1.3LOG - ✅ 1.1GLO - -Alpha Versions ----------------- - -Alpha releases contain new features and changes that are still under testing. They are primarily for developers or testers who want to try out the latest updates. - -.. toctree:: - :maxdepth: 2 - :caption: Alpha: - - 🅰️ 1.7.2a - 🅰️ 1.0alpha1 - -Beta Versions ----------------- - -Beta releases are more stable than alpha versions but are not fully tested yet. They are intended for early users who want to test new functionality before it is finalized. - -.. toctree:: - :maxdepth: 2 - :caption: Beta: - - 🅱️ 1.7.2b diff --git a/docs/releases/version/1.1glo.rst b/docs/releases/version/1.1glo.rst deleted file mode 100644 index a2c5157..0000000 --- a/docs/releases/version/1.1glo.rst +++ /dev/null @@ -1,34 +0,0 @@ -Release of Version 1.1GLO -========================== - -🛠️ **Update:** Version 1.1GLO - -**Released by:** @Medicopter117 - -**Release Date:** Jul 19 - -**Commits since last release:** 208 commits to main - ---- - -📦 **New Files** - -- `FastCoding/backend/database/levelroles_db.py` – New database module for storing and managing level roles - -- `cogs/levelsystem/levelsystem.py` – New cog for managing the leveling system (XP, progress, role assignment) - -✏️ **Modified Files** - -- `FastCoding/backend/database/__init__.py` – Database initialization extended for new modules - -- `cogs/Servermanament/globalchat.py` – Improved global chat logic and minor bug fixes - -- `FastCoding/backend/database/globalchat_db.py` – Optimized database queries & structures - -📝 **Description** - -This update introduces a new leveling system including level roles, as well as important improvements and fixes for the global chat system. - -The backend has been modularized and prepared for future features. - -✨ **Developed by:** OPPRO.NET Development diff --git a/docs/releases/version/1.3log.rst b/docs/releases/version/1.3log.rst deleted file mode 100644 index 8c62a61..0000000 --- a/docs/releases/version/1.3log.rst +++ /dev/null @@ -1,30 +0,0 @@ -Release of Version 1.3LOG -========================== - -🛠️ **Update:** Version 1.3LOG - -**Released by:** @Medicopter117 - ---- - -📦 **New Files** - -- `FastCoding/backend/database/logging_db.py` – New database module for logging functions - -- `cogs/Servermanament/logging.py` – New cog for logging management - -✏️ **Modified Files** - -- `FastCoding/backend/database/__init__.py` - -- `FastCoding/backend/database/globalchat_db.py` - -- `FastCoding/backend/logging.py` - -- `cogs/informationen/botstatus.py` - -📝 **Description** - -This update extends the backend with a new logging system including database integration and a bot cog. Existing modules were also improved and optimized. - -✨ **Developed by:** OPPRO.NET Development diff --git a/docs/releases/version/1.4log.rst b/docs/releases/version/1.4log.rst deleted file mode 100644 index 2f082f3..0000000 --- a/docs/releases/version/1.4log.rst +++ /dev/null @@ -1,30 +0,0 @@ -Release of Version 1.4LOG -========================== - -🛠️ **Update:** Version 1.4LOG - -**Released by:** @Medicopter117 - ---- - -📦 **New Files** - -- `cogs/informationen/serverinfo.py` – New cog for server information - -✏️ **Modified Files** - -- `cogs/Servermanament/globalchat.py` - -- `cogs/Servermanament/logging.py` - -- `main.py` - -🐞 **Bugfixes** - -- Fixed issue with duplicate messages when editing in `cogs/Servermanament/logging.py` - -📝 **Description** - -This update adds a new information cog, improves global chat and logging modules, and fixes a critical logging cog bug that caused message duplication. - -✨ **Developed by:** OPPRO.NET Development diff --git a/docs/releases/version/1.5.0.rst b/docs/releases/version/1.5.0.rst deleted file mode 100644 index 6c0b1a6..0000000 --- a/docs/releases/version/1.5.0.rst +++ /dev/null @@ -1,42 +0,0 @@ -Release of Version 1.5 -======================= - -🛠️ **Update:** Version 1.5 - -**Released by:** @Medicopter117 - ---- - -📦 **New Files** - -- `cogs/Servermanament/autodelete.py` – Automatic message deletion cog - -- `FastCoding/backend/database/autodelete_db.py` – Database module for autodelete - -✏️ **Modified Files** - -- `FastCoding/backend/database/__init__.py` - -- `FastCoding/ui/templates/embeds.py` - -- `cogs/Servermanament/stats.py` - -- `cogs/fun/wikipedia.py` - -- `cogs/levelsystem/levelsystem.py` - -- `cogs/moderation/moderation.py` - -🐞 **Bugfixes** - -- No specific bugfixes reported - -📝 **Description** - -This update extends server management with an automatic message system, improves existing modules, and further structures database connections. - -✨ **Developed by:** OPPRO.NET Development, @Medicopter117 - -**Contributors** - -- @Medicopter117 diff --git a/docs/releases/version/1.6.0.rst b/docs/releases/version/1.6.0.rst deleted file mode 100644 index deddd06..0000000 --- a/docs/releases/version/1.6.0.rst +++ /dev/null @@ -1,31 +0,0 @@ -Release of Version 1.6 -======================= - -🛠️ **Update:** Version 1.6 - -**Released by:** @Oppro-net-Development, @Medicopter117 - ---- - -✏️ **Modified Files** - -- `FastCoding/backend/database/autodelete_db.py` - -- `FastCoding/backend/database/logging_db.py` - -- `cogs/Servermanament/autodelete.py` - -- `cogs/moderation/moderation.py` - -- `main.py` - -📝 **Description** - -This update extends server management with an automatic message system, improves existing modules, and further structures database connections. - -✨ **Developed by:** @Oppro-net-Development, @Medicopter117 - -**Contributors** - -- @Medicopter117 -- @Oppro-net-Development diff --git a/docs/releases/version/1.6.1.rst b/docs/releases/version/1.6.1.rst deleted file mode 100644 index cfc54ca..0000000 --- a/docs/releases/version/1.6.1.rst +++ /dev/null @@ -1,32 +0,0 @@ -Release of Version 1.6.1 -========================= - -🛠️ **Update:** Version 1.6.1 - -**Released by:** @Medicopter117 - -**Release Date:** Jul 30 - -✏️ **Modified Files** - -- `.gitignore` - -- `cogs/Servermanament/globalchat.py` - -- `cogs/Servermanament/logging.py` - -- `cogs/Servermanament/stats.py` - -- `cogs/Temp/tempvc.py` - -- `cogs/levelsystem/levelsystem.py` - -- `cogs/moderation/moderation.py` - -📝 **Description** - -Mainly adds slash command groups. - -**Contributors** - -- @Medicopter117 diff --git a/docs/releases/version/1.6.2.rst b/docs/releases/version/1.6.2.rst deleted file mode 100644 index eb1130d..0000000 --- a/docs/releases/version/1.6.2.rst +++ /dev/null @@ -1,26 +0,0 @@ -Release of Version 1.6.2 -========================= - -🛠️ **Update:** Version 1.6.2 - -**Released by:** @Medicopter117 - -**Release Date:** Aug 4 - -✏️ **Modified Files** -- `.CHANGELOG.md` - - -- `CONTRIBUTING.md` - -- `cogs/levelsystem/levelsystem.py` - -- `README.md` - -📝 **Description** - -Mainly adds .md files. - -**Contributors** - -- @Medicopter117 diff --git a/docs/releases/version/1.6.3.rst b/docs/releases/version/1.6.3.rst deleted file mode 100644 index 31fd3f6..0000000 --- a/docs/releases/version/1.6.3.rst +++ /dev/null @@ -1,30 +0,0 @@ -Release of Version 1.6.3 -========================= - -🛠️ **Update:** Version 1.6.3 - -**Released by:** @Medicopter117 - -**Release Date:** Aug 6 - ---- - -📦 **New Files** - -- `cogs/informationen/usermanagemt.py` - -- `template.env` - -- `cogs/moderation/anticapslock.py` *(may be faulty, report if errors occur)* - -✏️ **Modified Files** - -- `CHANGELOG.md` - - -⭕ **Deleted Files** -- `user.py` - -**Contributors** - -- @Medicopter117 diff --git a/docs/releases/version/1.6.4.rst b/docs/releases/version/1.6.4.rst deleted file mode 100644 index 60259f5..0000000 --- a/docs/releases/version/1.6.4.rst +++ /dev/null @@ -1,23 +0,0 @@ -Release of Version 1.6.4 -========================= - -🛠️ **Update:** Version 1.6.4 - -**Released by:** @Medicopter117 - -**Release Date:** Aug 15 - ---- - -📦 **New Files** - -- `cogs/Servermanament/welcome.py` -- `FastCoding/backend/database/welcome_db.py` - -✏️ **Modified Files** - -- `FastCoding/backend/database/__init__.py` - -**Contributors** - -- @Medicopter117 diff --git a/docs/releases/version/1.6.5.rst b/docs/releases/version/1.6.5.rst deleted file mode 100644 index 47aae83..0000000 --- a/docs/releases/version/1.6.5.rst +++ /dev/null @@ -1,32 +0,0 @@ -Release of Version 1.6.5 -========================= - -🛠️ **Update:** Version 1.6.5 - -**Released by:** @Medicopter117 - -**Release Date:** Sep 10 - ---- - -📦 **New Files** - -- `cogs/Servermanament/welcome.py` -- `FastCoding/backend/database/welcome_db.py` - -✏️ **Modified Files** - -- `FastCoding/backend/database/__init__.py` -- `cogs/Servermanament/stats.py` -- `cogs/fun/wikipedia.py` -- `cogs/levelsystem/levelsystem.py` -- `cogs/moderation/moderation.py` - -📝 **Description** - -Db memory patch and updates to welcome_db.py and welcome.py. Full changelog: V1.6.4 → V1.6.5 - -**New Contributors** - -- @verleihernix -- @Medicopter117 diff --git a/docs/releases/version/1.6.6.rst b/docs/releases/version/1.6.6.rst deleted file mode 100644 index 7f20733..0000000 --- a/docs/releases/version/1.6.6.rst +++ /dev/null @@ -1,16 +0,0 @@ -Release of Version 1.6.6 -========================= - -🛠️ **Update:** Version 1.6.6 - -**Released by:** @Medicopter117 - -**Release Date:** Sep 11 - -📝 **Description** - -Minor fixes and updates. Full changelog: V1.6.5 → V1.6.6 - -**Contributors** - -- @Medicopter117 diff --git a/docs/releases/version/1.7.0.rst b/docs/releases/version/1.7.0.rst deleted file mode 100644 index 432f979..0000000 --- a/docs/releases/version/1.7.0.rst +++ /dev/null @@ -1,50 +0,0 @@ -Release of Version 1.7 -======================= - -🛠️ **Update:** Version 1.7 - -**Released by:** @Medicopter117 - -**Release Date:** Sep 17 - ---- - -✏️ **Modified Files** - -**Backend / Database** - -- `FastCoding/backend/database/__init__.py` - -- `FastCoding/backend/database/levelsystem_db.py` - -- `FastCoding/backend/database/logging_db.py` - -- `FastCoding/backend/database/vc_db.py` - -**Cogs** - -- `cogs/Servermanament/logging.py` - -- `cogs/Servermanament/welcome.py` - -- `cogs/Temp/tempvc.py` - -- `cogs/informationen/botstatus.py` - -- `cogs/moderation/moderation.py` - -- `cogs/moderation/warningsystem.py` - -**Others** - -- `main.py` - -- `req.txt` - -**New** - -- `resources/ManagerX.png` - -**Contributors** - -- @Medicopter117 diff --git a/docs/releases/version/1.7.1.rst b/docs/releases/version/1.7.1.rst deleted file mode 100644 index 9e1ad88..0000000 --- a/docs/releases/version/1.7.1.rst +++ /dev/null @@ -1,65 +0,0 @@ -Release of Version 1.7.1 -========================= - -🛠️ **Update:** Version 1.7.1 - -**Released by:** @Medicopter117 - -**Release Date:** Oct 10 - ---- - -✏️ **Modified Files** - -- `.gitignore` - -- `DevTools/backend/database/welcome_db.py` - -- `DevTools/ui/emojis.py` - -- `DevTools/ui/templates/embeds.py` - -- `README.md` - -- `cogs/Servermanament/autodelete.py` - -- `cogs/Servermanament/levelsystem.py` - -- `cogs/Servermanament/logging.py` - -- `cogs/Servermanament/stats.py` - -- `cogs/Servermanament/tempvc.py` - -- `cogs/Servermanament/welcome.py` - -- `cogs/fun/gewinnt.py` - -- `cogs/fun/tictactoe.py` - -- `cogs/fun/weather.py` - -- `cogs/fun/wikipedia.py` - -- `cogs/informationen/serverinfo.py` - -- `cogs/informationen/usermanagemt.py` - -- `cogs/moderation/antispam.py` - -- `cogs/moderation/moderation.py` - -- `cogs/moderation/notes.py` - -- `cogs/moderation/warningsystem.py` - -- `version.txt` - - -📝 **Description** - -Update & docs changes, Pycache removed. Full changelog: V1.7 → V1.7.1 - -**Contributors** - -- @Medicopter117 diff --git a/docs/req.txt b/docs/req.txt deleted file mode 100644 index 11062cd..0000000 --- a/docs/req.txt +++ /dev/null @@ -1,5 +0,0 @@ -sphinx==7.3.2 -pydata-sphinx-theme==0.16.1 -sphinx-autodoc-typehints==1.25.0 -myst-parser==2.0.0 -sphinx-copybutton==0.6.0 diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css new file mode 100644 index 0000000..4b4c030 --- /dev/null +++ b/docs/source/_static/custom.css @@ -0,0 +1,1058 @@ +/* ========================================================================== + MANAGERX PREMIUM DOCS THEME + PyData Sphinx Theme - Optimized & Refined + ========================================================================== */ + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap'); + +:root { + /* ManagerX Premium Color System */ + --mx-red-primary: #dc2626; + --mx-red-dark: #991b1b; + --mx-red-darker: #7f1d1d; + --mx-red-light: #fef2f2; + --mx-red-accent: #f87171; + --mx-red-glow: rgba(220, 38, 38, 0.1); + + /* Neutral Palette */ + --mx-gray-50: #f8fafc; + --mx-gray-100: #f1f5f9; + --mx-gray-200: #e2e8f0; + --mx-gray-300: #cbd5e1; + --mx-gray-400: #94a3b8; + --mx-gray-500: #64748b; + --mx-gray-600: #475569; + --mx-gray-700: #334155; + --mx-gray-800: #1e293b; + --mx-gray-900: #0f172a; + + /* Semantic Colors */ + --mx-success: #059669; + --mx-warning: #d97706; + --mx-danger: #dc2626; + --mx-info: #0284c7; + + /* Typography System */ + --pst-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --pst-font-family-heading: 'Space Grotesk', 'Inter', sans-serif; + --pst-font-family-monospace: 'JetBrains Mono', 'Consolas', 'Monaco', monospace; + + /* PyData Theme Overrides */ + --pst-color-primary: var(--mx-red-primary); + --pst-color-secondary: var(--mx-gray-600); + --pst-color-link: var(--mx-red-primary); + --pst-color-link-hover: var(--mx-red-dark); + --pst-color-target: #fbbf24; + + /* Shadows & Effects */ + --mx-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05); + --mx-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.06); + --mx-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --mx-shadow-lg: 0 10px 24px rgba(0, 0, 0, 0.1); + --mx-shadow-xl: 0 20px 40px rgba(0, 0, 0, 0.12); + + /* Transitions */ + --mx-transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1); + --mx-transition-base: 0.25s cubic-bezier(0.4, 0, 0.2, 1); + --mx-transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1); + + /* Border Radius */ + --mx-radius-sm: 6px; + --mx-radius-md: 10px; + --mx-radius-lg: 14px; + --mx-radius-xl: 20px; + --mx-radius-full: 9999px; +} + +/* ========================================================================== + 1. GLOBAL FOUNDATION & TYPOGRAPHY + ========================================================================== */ + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + letter-spacing: -0.011em; + line-height: 1.65; +} + +/* Premium Scrollbar Design */ +::-webkit-scrollbar { + width: 14px; + height: 14px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--mx-gray-300); + border-radius: var(--mx-radius-full); + border: 3px solid var(--mx-gray-50); + transition: background var(--mx-transition-base); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--mx-red-primary); +} + +[data-theme="dark"] ::-webkit-scrollbar-thumb { + background: var(--mx-gray-600); + border-color: var(--mx-gray-900); +} + +[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { + background: var(--mx-red-accent); +} + +/* Improved Typography Hierarchy */ +h1, h2, h3, h4, h5, h6 { + font-family: var(--pst-font-family-heading); + font-weight: 700; + letter-spacing: -0.025em; + color: var(--mx-gray-900); + line-height: 1.25; +} + +[data-theme="dark"] h1, +[data-theme="dark"] h2, +[data-theme="dark"] h3, +[data-theme="dark"] h4 { + color: var(--mx-gray-50); +} + +h1 { + font-size: 2.5rem; + margin-top: 0 !important; + margin-bottom: 2rem !important; +} + +h2 { + font-size: 2rem; + margin-top: 3rem !important; + margin-bottom: 1.5rem !important; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--mx-gray-100); +} + +[data-theme="dark"] h2 { + border-bottom-color: var(--mx-gray-700); +} + +h3 { + font-size: 1.5rem; + margin-top: 2rem !important; + margin-bottom: 1rem !important; +} + +h4 { + font-size: 1.25rem; + margin-top: 1.5rem !important; + margin-bottom: 0.75rem !important; +} + +/* Subtle Section Indicators */ +h2::before { + content: ""; + display: inline-block; + width: 4px; + height: 1.5rem; + background: linear-gradient(180deg, var(--mx-red-primary), var(--mx-red-accent)); + margin-right: 1rem; + border-radius: var(--mx-radius-sm); + vertical-align: middle; +} + +/* ========================================================================== + 2. HEADER & NAVIGATION + ========================================================================== */ + +/* Premium Glassmorphic Header */ +.bd-header { + background: rgba(255, 255, 255, 0.85) !important; + backdrop-filter: blur(20px) saturate(180%); + -webkit-backdrop-filter: blur(20px) saturate(180%); + border-bottom: 1px solid rgba(220, 38, 38, 0.1) !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(220, 38, 38, 0.03); + transition: all var(--mx-transition-base); +} + +[data-theme="dark"] .bd-header { + background: rgba(15, 23, 42, 0.9) !important; + border-bottom-color: rgba(220, 38, 38, 0.2) !important; +} + +/* Logo & Brand Styling */ +.navbar-brand { + font-family: var(--pst-font-family-heading); + font-weight: 700; + font-size: 1.25rem; + letter-spacing: -0.02em; + transition: transform var(--mx-transition-fast); +} + +.navbar-brand:hover { + transform: translateX(2px); +} + +/* Mobile Toggle */ +.bd-header .navbar-toggler { + border: 2px solid var(--mx-red-primary); + border-radius: var(--mx-radius-md); + color: var(--mx-red-primary); + transition: all var(--mx-transition-base); +} + +.bd-header .navbar-toggler:hover { + background: var(--mx-red-light); + transform: scale(1.05); +} + +/* ========================================================================== + 3. SIDEBAR NAVIGATION + ========================================================================== */ + +/* Primary Sidebar Styling */ +.bd-sidebar-primary { + border-right: 1px solid var(--mx-gray-100); +} + +[data-theme="dark"] .bd-sidebar-primary { + border-right-color: var(--mx-gray-700); +} + +/* Section Headers */ +.bd-sidebar-primary .caption-text { + color: var(--mx-red-dark); + font-family: var(--pst-font-family-heading); + font-weight: 700; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.1em; + margin-top: 1.5rem; + margin-bottom: 0.75rem; + padding-left: 1rem; +} + +/* Navigation Links */ +.bd-sidebar-primary .nav-link { + border-radius: var(--mx-radius-md); + margin: 2px 0; + padding: 0.5rem 1rem; + transition: all var(--mx-transition-base); + font-size: 0.9rem; +} + +.bd-sidebar-primary .nav-link:hover { + background: var(--mx-gray-50); + color: var(--mx-red-primary); + transform: translateX(4px); +} + +[data-theme="dark"] .bd-sidebar-primary .nav-link:hover { + background: var(--mx-gray-800); +} + +/* Active Navigation Item */ +.bd-sidebar-primary .nav-item.current > a, +.bd-sidebar-primary .nav-item.active > a { + background: linear-gradient(90deg, var(--mx-red-light) 0%, transparent 100%); + color: var(--mx-red-primary) !important; + font-weight: 600; + border-left: 3px solid var(--mx-red-primary); + padding-left: calc(1rem - 3px); +} + +[data-theme="dark"] .bd-sidebar-primary .nav-item.current > a { + background: linear-gradient(90deg, rgba(220, 38, 38, 0.15) 0%, transparent 100%); +} + +/* Nested Navigation */ +.bd-sidebar-primary .nav-item .nav-item .nav-link { + font-size: 0.85rem; + padding-left: 2rem; +} + +/* ========================================================================== + 4. MAIN CONTENT AREA + ========================================================================== */ + +/* Content Container */ +.bd-main { + padding-top: 2rem; + padding-bottom: 4rem; +} + +article.bd-article { + max-width: 850px; +} + +/* Paragraph Spacing */ +article p { + margin-bottom: 1.25rem; + line-height: 1.75; +} + +/* Target Highlighting */ +:target { + scroll-margin-top: 120px; + animation: highlight-pulse 2s ease-out; +} + +@keyframes highlight-pulse { + 0%, 50% { + background-color: var(--mx-red-glow); + box-shadow: 0 0 0 8px var(--mx-red-glow); + } + 100% { + background-color: transparent; + box-shadow: 0 0 0 0 transparent; + } +} + +/* ========================================================================== + 5. ADMONITIONS & CALLOUTS + ========================================================================== */ + +/* Base Admonition Styling */ +.admonition { + border: none !important; + border-left: 4px solid var(--mx-red-primary) !important; + border-radius: var(--mx-radius-lg) !important; + background: var(--mx-gray-50) !important; + box-shadow: var(--mx-shadow-sm) !important; + padding: 1.5rem !important; + margin: 2rem 0 !important; + transition: all var(--mx-transition-base); +} + +.admonition:hover { + box-shadow: var(--mx-shadow-md) !important; + transform: translateY(-2px); +} + +[data-theme="dark"] .admonition { + background: var(--mx-gray-800) !important; +} + +/* Admonition Title */ +.admonition-title { + background: transparent !important; + color: var(--mx-red-primary) !important; + font-family: var(--pst-font-family-heading) !important; + font-weight: 700 !important; + font-size: 0.875rem !important; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.75rem !important; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.admonition-title::before { + content: "●"; + font-size: 0.6em; +} + +/* Admonition Variants */ +.admonition.note { + border-left-color: var(--mx-info) !important; +} + +.admonition.note .admonition-title { + color: var(--mx-info) !important; +} + +.admonition.warning { + border-left-color: var(--mx-warning) !important; +} + +.admonition.warning .admonition-title { + color: var(--mx-warning) !important; +} + +.admonition.danger, +.admonition.error { + border-left-color: var(--mx-danger) !important; +} + +.admonition.danger .admonition-title, +.admonition.error .admonition-title { + color: var(--mx-danger) !important; +} + +.admonition.tip, +.admonition.hint { + border-left-color: var(--mx-success) !important; +} + +.admonition.tip .admonition-title, +.admonition.hint .admonition-title { + color: var(--mx-success) !important; +} + +/* ========================================================================== + 6. CODE BLOCKS & SYNTAX HIGHLIGHTING + ========================================================================== */ + +/* Code Block Container */ +div.highlight { + border: 1px solid var(--mx-gray-200) !important; + border-radius: var(--mx-radius-lg) !important; + background: var(--mx-gray-50) !important; + box-shadow: var(--mx-shadow-sm); + padding: 0 !important; + margin: 1.5rem 0; + overflow: hidden; + transition: all var(--mx-transition-base); +} + +div.highlight:hover { + box-shadow: var(--mx-shadow-md); +} + +[data-theme="dark"] div.highlight { + background: var(--mx-gray-900) !important; + border-color: var(--mx-gray-700) !important; +} + +/* Code Block Pre */ +div.highlight pre { + padding: 1.25rem !important; + margin: 0 !important; + overflow-x: auto; + font-size: 0.875rem; + line-height: 1.6; + background: transparent !important; +} + +/* Inline Code */ +code.literal { + background: var(--mx-red-light); + color: var(--mx-red-dark); + padding: 0.15em 0.4em; + border-radius: var(--mx-radius-sm); + font-size: 0.875em; + font-weight: 500; + border: 1px solid rgba(220, 38, 38, 0.1); +} + +[data-theme="dark"] code.literal { + background: rgba(220, 38, 38, 0.15); + color: var(--mx-red-accent); + border-color: rgba(220, 38, 38, 0.2); +} + +/* Keyboard Shortcuts */ +kbd { + background: linear-gradient(180deg, var(--mx-gray-50), var(--mx-gray-100)); + border: 1px solid var(--mx-gray-300); + border-radius: var(--mx-radius-sm); + box-shadow: 0 2px 0 var(--mx-gray-300), inset 0 1px 0 rgba(255, 255, 255, 0.8); + color: var(--mx-gray-700); + font-family: var(--pst-font-family-monospace); + font-size: 0.85em; + padding: 0.2em 0.5em; + font-weight: 500; +} + +[data-theme="dark"] kbd { + background: linear-gradient(180deg, var(--mx-gray-700), var(--mx-gray-800)); + border-color: var(--mx-gray-600); + box-shadow: 0 2px 0 var(--mx-gray-600); + color: var(--mx-gray-200); +} + +/* Syntax Highlighting Customization */ +.highlight .k, +.highlight .kd { + color: var(--mx-red-primary); + font-weight: 600; +} + +.highlight .nc, +.highlight .nn { + color: var(--mx-red-dark); + font-weight: 600; +} + +.highlight .s, +.highlight .s2, +.highlight .s1 { + color: #059669; +} + +.highlight .c1, +.highlight .cm { + color: var(--mx-gray-400); + font-style: italic; +} + +.highlight .nf { + color: #0284c7; +} + +.highlight .mi, +.highlight .mf { + color: #7c3aed; +} + +/* ========================================================================== + 7. TABLES + ========================================================================== */ + +table.docutils { + width: 100%; + border-collapse: separate !important; + border-spacing: 0; + border-radius: var(--mx-radius-lg); + overflow: hidden; + border: 1px solid var(--mx-gray-200) !important; + margin: 2rem 0; + box-shadow: var(--mx-shadow-sm); +} + +[data-theme="dark"] table.docutils { + border-color: var(--mx-gray-700) !important; +} + +/* Table Header */ +table.docutils thead { + background: linear-gradient(180deg, var(--mx-red-primary), var(--mx-red-dark)); +} + +table.docutils thead th { + color: white !important; + font-family: var(--pst-font-family-heading); + font-weight: 600 !important; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 1rem !important; + border: none !important; + text-align: left; +} + +/* Table Body */ +table.docutils tbody tr { + transition: background-color var(--mx-transition-fast); +} + +table.docutils tbody tr:hover { + background-color: var(--mx-gray-50); +} + +[data-theme="dark"] table.docutils tbody tr:hover { + background-color: var(--mx-gray-800); +} + +table.docutils tbody td { + padding: 0.875rem 1rem !important; + border-bottom: 1px solid var(--mx-gray-100) !important; + font-size: 0.9rem; +} + +[data-theme="dark"] table.docutils tbody td { + border-bottom-color: var(--mx-gray-700) !important; +} + +table.docutils tbody tr:last-child td { + border-bottom: none !important; +} + +/* ========================================================================== + 8. LINKS & REFERENCES + ========================================================================== */ + +/* Internal & External Links */ +article a.reference.internal, +article a.reference.external { + color: var(--mx-red-primary); + text-decoration: none; + font-weight: 500; + position: relative; + transition: color var(--mx-transition-fast); +} + +article a.reference:hover { + color: var(--mx-red-dark); +} + +/* Animated Underline */ +article a.reference::after { + content: ''; + position: absolute; + width: 100%; + height: 2px; + bottom: -2px; + left: 0; + background: linear-gradient(90deg, var(--mx-red-primary), var(--mx-red-accent)); + transform: scaleX(0); + transform-origin: bottom right; + transition: transform var(--mx-transition-base); + border-radius: var(--mx-radius-full); +} + +article a.reference:hover::after { + transform: scaleX(1); + transform-origin: bottom left; +} + +/* External Link Icon */ +article a.reference.external::before { + content: "↗"; + font-size: 0.7em; + margin-left: 0.2em; + opacity: 0.6; +} + +/* ========================================================================== + 9. IMAGES & FIGURES + ========================================================================== */ + +/* Figure Container */ +figure.align-default, +figure.align-center { + margin: 3rem auto; + padding: 1.5rem; + background: white; + border-radius: var(--mx-radius-xl); + box-shadow: var(--mx-shadow-md); + text-align: center; + transition: all var(--mx-transition-base); +} + +figure.align-default:hover, +figure.align-center:hover { + box-shadow: var(--mx-shadow-lg); + transform: translateY(-4px); +} + +[data-theme="dark"] figure.align-default, +[data-theme="dark"] figure.align-center { + background: var(--mx-gray-800); +} + +/* Figure Image */ +figure.align-default img, +figure.align-center img { + border-radius: var(--mx-radius-md); + max-width: 100%; + height: auto; +} + +/* Figure Caption */ +figcaption, +.caption-text { + margin-top: 1rem; + font-size: 0.875rem; + color: var(--mx-gray-500); + font-style: italic; + line-height: 1.5; +} + +/* ========================================================================== + 10. API DOCUMENTATION (Autodoc) + ========================================================================== */ + +/* Function/Class/Method Containers */ +dl.py.function, +dl.py.class, +dl.py.method, +dl.py.attribute { + background: var(--mx-gray-50); + border: 1px solid var(--mx-gray-200); + border-left: 4px solid var(--mx-red-primary); + border-radius: var(--mx-radius-lg); + padding: 1.5rem; + margin: 2rem 0; + box-shadow: var(--mx-shadow-sm); + transition: all var(--mx-transition-base); +} + +dl.py.function:hover, +dl.py.class:hover, +dl.py.method:hover { + box-shadow: var(--mx-shadow-md); +} + +[data-theme="dark"] dl.py.function, +[data-theme="dark"] dl.py.class, +[data-theme="dark"] dl.py.method { + background: var(--mx-gray-800); + border-color: var(--mx-gray-700); +} + +/* Signature */ +dt.sig { + font-family: var(--pst-font-family-monospace); + font-size: 1rem; + color: var(--mx-red-primary); + background: rgba(220, 38, 38, 0.05); + padding: 0.75rem; + border-radius: var(--mx-radius-md); + margin-bottom: 1rem; + font-weight: 500; +} + +/* Parameters */ +.sig-param { + color: var(--mx-gray-700); +} + +[data-theme="dark"] .sig-param { + color: var(--mx-gray-300); +} + +/* Return Type */ +.sig-return { + color: var(--mx-success); +} + +/* ========================================================================== + 11. SEARCH + ========================================================================== */ + +/* Search Input */ +.bd-search .form-control { + border-radius: var(--mx-radius-full) !important; + border: 2px solid var(--mx-gray-200) !important; + padding: 0.625rem 1.25rem !important; + transition: all var(--mx-transition-base); + font-size: 0.9rem; +} + +.bd-search .form-control:focus { + border-color: var(--mx-red-primary) !important; + box-shadow: 0 0 0 4px var(--mx-red-glow) !important; + outline: none; +} + +[data-theme="dark"] .bd-search .form-control { + border-color: var(--mx-gray-700) !important; + background: var(--mx-gray-800) !important; +} + +/* Search Keyboard Shortcut */ +.search-button__kbd-shortcut { + background: var(--mx-red-light) !important; + color: var(--mx-red-primary) !important; + border: 1px solid rgba(220, 38, 38, 0.2) !important; + border-radius: var(--mx-radius-sm) !important; + font-weight: 600; +} + +/* ========================================================================== + 12. PAGINATION & NAVIGATION + ========================================================================== */ + +/* Previous/Next Navigation */ +.prev-next-area { + margin-top: 4rem; + padding-top: 2rem; + border-top: 2px solid var(--mx-gray-100); +} + +[data-theme="dark"] .prev-next-area { + border-top-color: var(--mx-gray-700); +} + +.prev-next-area a { + border: 1px solid var(--mx-gray-200) !important; + border-radius: var(--mx-radius-lg) !important; + padding: 1.5rem !important; + transition: all var(--mx-transition-base) !important; + background: white; +} + +[data-theme="dark"] .prev-next-area a { + background: var(--mx-gray-800); + border-color: var(--mx-gray-700) !important; +} + +.prev-next-area a:hover { + border-color: var(--mx-red-primary) !important; + box-shadow: 0 8px 24px var(--mx-red-glow) !important; + transform: translateY(-4px); +} + +.prev-next-area .prev-next-title { + color: var(--mx-red-primary) !important; + font-family: var(--pst-font-family-heading); + font-weight: 600; + font-size: 1.125rem; +} + +/* ========================================================================== + 13. BUTTONS + ========================================================================== */ + +/* Primary Buttons */ +.btn-primary { + background: linear-gradient(135deg, var(--mx-red-primary), var(--mx-red-dark)) !important; + border: none !important; + color: white !important; + font-family: var(--pst-font-family-heading); + font-weight: 600; + padding: 0.625rem 1.5rem; + border-radius: var(--mx-radius-md); + transition: all var(--mx-transition-base); + box-shadow: var(--mx-shadow-sm); +} + +.btn-primary:hover, +.btn-primary:focus { + transform: translateY(-2px); + box-shadow: 0 6px 20px var(--mx-red-glow) !important; +} + +.btn-primary:active { + transform: translateY(0); +} + +/* Outline Buttons */ +.btn-outline-primary { + color: var(--mx-red-primary) !important; + border: 2px solid var(--mx-red-primary) !important; + background: transparent !important; + font-weight: 600; + border-radius: var(--mx-radius-md); + transition: all var(--mx-transition-base); +} + +.btn-outline-primary:hover { + background: var(--mx-red-primary) !important; + color: white !important; + transform: translateY(-2px); + box-shadow: 0 6px 20px var(--mx-red-glow); +} + +/* ========================================================================== + 14. FOOTER + ========================================================================== */ + +.bd-footer { + margin-top: 6rem; + padding: 3rem 0 2rem; + border-top: 2px solid var(--mx-gray-100); + font-size: 0.875rem; +} + +[data-theme="dark"] .bd-footer { + border-top-color: var(--mx-gray-700); +} + +.footer-items__end { + color: var(--mx-gray-500); +} + +.footer-items__end strong { + color: var(--mx-red-primary); + font-weight: 600; +} + +/* ========================================================================== + 15. CUSTOM COMPONENTS + ========================================================================== */ + +/* Hero Section */ +.mx-hero { + text-align: center; + padding: 5rem 2rem; + background: radial-gradient( + circle at center, + var(--mx-red-glow) 0%, + transparent 70% + ); + border-radius: var(--mx-radius-xl); + margin: 3rem 0; +} + +/* Grid Layout Helper */ +.mx-grid-2 { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + margin: 2rem 0; +} + +@media (max-width: 768px) { + .mx-grid-2 { + grid-template-columns: 1fr; + } +} + +/* Card Component */ +.mx-box { + padding: 2rem; + border-radius: var(--mx-radius-lg); + background: white; + border: 1px solid var(--mx-gray-200); + box-shadow: var(--mx-shadow-sm); + transition: all var(--mx-transition-base); +} + +.mx-box:hover { + box-shadow: var(--mx-shadow-md); + transform: translateY(-4px); +} + +[data-theme="dark"] .mx-box { + background: var(--mx-gray-800); + border-color: var(--mx-gray-700); +} + +/* Accent Border */ +.mx-box-accent { + border-bottom: 4px solid var(--mx-red-primary); +} + +/* ========================================================================== + 16. VERSION SWITCHER & DROPDOWNS + ========================================================================== */ + +.bd-version-switcher__button { + border-radius: var(--mx-radius-md) !important; + border: 1px solid var(--mx-gray-200) !important; + transition: all var(--mx-transition-base); +} + +[data-theme="dark"] .bd-version-switcher__button { + border-color: var(--mx-gray-700) !important; +} + +.bd-version-switcher__button:hover { + background: var(--mx-red-light) !important; + color: var(--mx-red-primary) !important; + border-color: var(--mx-red-primary) !important; +} + +/* ========================================================================== + 17. ANNOUNCEMENT BANNER + ========================================================================== */ + +.bd-header-announcement { + background: linear-gradient(135deg, var(--mx-red-dark), var(--mx-red-primary)) !important; + color: white !important; + font-weight: 600; + padding: 0.75rem; + text-align: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/* ========================================================================== + 18. RESPONSIVE DESIGN + ========================================================================== */ + +@media (max-width: 992px) { + h1 { + font-size: 2rem; + } + + h2 { + font-size: 1.65rem; + } + + h3 { + font-size: 1.35rem; + } + + .bd-main { + padding-top: 1rem; + } + + .mx-hero { + padding: 3rem 1.5rem; + } +} + +@media (max-width: 768px) { + h1 { + font-size: 1.75rem; + } + + h2 { + font-size: 1.5rem; + } + + .admonition { + padding: 1.25rem !important; + } + + table.docutils { + font-size: 0.85rem; + } + + .prev-next-area a { + padding: 1.25rem !important; + } +} + +/* ========================================================================== + 19. PRINT STYLES + ========================================================================== */ + +@media print { + .bd-header, + .bd-sidebar-primary, + .bd-sidebar-secondary, + .bd-footer, + .prev-next-area { + display: none !important; + } + + article { + max-width: 100% !important; + } + + .admonition { + page-break-inside: avoid; + } +} + +/* ========================================================================== + 20. ACCESSIBILITY IMPROVEMENTS + ========================================================================== */ + +/* Focus Visible States */ +a:focus-visible, +button:focus-visible, +input:focus-visible { + outline: 3px solid var(--mx-red-primary); + outline-offset: 2px; + border-radius: var(--mx-radius-sm); +} + +/* Skip to Content Link */ +.skip-link { + position: absolute; + top: -40px; + left: 0; + background: var(--mx-red-primary); + color: white; + padding: 0.5rem 1rem; + text-decoration: none; + border-radius: 0 0 var(--mx-radius-md) 0; + z-index: 100; +} + +.skip-link:focus { + top: 0; +} + +/* Reduced Motion */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* ========================================================================== + END OF MANAGERX PREMIUM DOCS THEME + ========================================================================== */ \ No newline at end of file diff --git a/docs/user_guide/commands/moderation.rst b/docs/source/changelog/index.rst similarity index 100% rename from docs/user_guide/commands/moderation.rst rename to docs/source/changelog/index.rst diff --git a/docs/conf.py b/docs/source/conf.py similarity index 89% rename from docs/conf.py rename to docs/source/conf.py index 33c561f..c1b17ee 100644 --- a/docs/conf.py +++ b/docs/source/conf.py @@ -11,8 +11,8 @@ # -- Project information ----------------------------------------------------- project = 'ManagerX' -copyright = '2025, OPPRO.NET Network' -author = 'OPPRO.NET Development' +copyright = '2026 ManagerX Development' +author = 'ManagerX Development' release = '2.0.0' version = '2.0' # Kurzversion language = 'en' @@ -54,7 +54,6 @@ # -- Options for HTML output ------------------------------------------------- html_theme = 'pydata_sphinx_theme' -html_favicon = "_static/managerx.png" html_static_path = ['_static'] html_css_files = [ 'custom.css', @@ -63,9 +62,10 @@ "icon_links": [ { "name": "GitHub", - "url": "https://github.com/Oppro-net-Development/ManagerX", # required + "url": "https://github.com/ManagerX-Development/ManagerX", "icon": "fa-brands fa-square-github", "type": "fontawesome", } ], + } \ No newline at end of file diff --git a/docs/source/dev_guide/getting_start/installation.rst b/docs/source/dev_guide/getting_start/installation.rst new file mode 100644 index 0000000..955e5f1 --- /dev/null +++ b/docs/source/dev_guide/getting_start/installation.rst @@ -0,0 +1,65 @@ +Installation +============ + +A powerful Discord bot for server management and fun. + +---- + +Installation Methods +-------------------- + +GitHub Installation (Recommended) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Get the latest version directly from the source:** + +.. code-block:: bash + + git clone https://github.com/ManagerX-Development/ManagerX + cd ManagerX + pip install . + +.. note:: + ✨ **Latest Version:** This method always provides the most current version with all the latest features and updates. + +---- + +PyPI Installation +~~~~~~~~~~~~~~~~~ + +Install via pip with different configurations: + +**Standard Installation:** + +.. code-block:: bash + + pip install managerx + +**With Developer Tools:** + +.. code-block:: bash + + pip install "managerx[dev]" + +**With Documentation Tools:** + +.. code-block:: bash + + pip install "managerx[docs]" + +**Full Installation (All Features):** + +.. code-block:: bash + + pip install "managerx[all]" + +.. warning:: + PyPI releases may not always contain the absolute latest version. For the most up-to-date code, use the GitHub installation method. + +---- + +Requirements +------------ + +* Python 3.8 or higher +* pip (Python package installer) \ No newline at end of file diff --git a/docs/source/dev_guide/index.rst b/docs/source/dev_guide/index.rst new file mode 100644 index 0000000..15e5eab --- /dev/null +++ b/docs/source/dev_guide/index.rst @@ -0,0 +1,20 @@ +Developer Guide +======================== + +Welcome to the ManagerX Developer Guide! + +.. note:: + This project is permanently under development. We recommend checking this guide frequently for updates. + +.. toctree:: + :maxdepth: 2 + :caption: Getting Started: + +.. toctree:: + :maxdepth: 2 + :caption: Architecture: + +.. toctree:: + :maxdepth: 2 + :caption: Contribution: + \ No newline at end of file diff --git a/docs/index.rst b/docs/source/index.rst similarity index 95% rename from docs/index.rst rename to docs/source/index.rst index 4770f20..f28de32 100644 --- a/docs/index.rst +++ b/docs/source/index.rst @@ -4,9 +4,8 @@ ManagerX Documentation .. container:: mx-hero - .. image:: https://img.shields.io/badge/Version-1.7.2-e11d48?style=for-the-badge - :target: https://github.com/Oppro-net-Development/ManagerX - :alt: Version 1.7.2 + .. image:: https://img.shields.io/badge/Version-2.0.0-e11d48?style=for-the-badge + :alt: Version 2.0.0 .. image:: https://img.shields.io/badge/Python-3.8+-green.svg?style=for-the-badge :alt: Python 3.8+ @@ -118,5 +117,5 @@ We welcome contributions from the community! Whether it's bug reports, feature r --- -**© 2025 OPPRO.NET Network** +**© 2026 ManagerX Development** *Version 2.0.0-dev | Last Updated: December 7, 2025* \ No newline at end of file diff --git a/docs/user_guide/commands/index.rst b/docs/user_guide/commands/index.rst deleted file mode 100644 index 340bff4..0000000 --- a/docs/user_guide/commands/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -User Guide - Commands -============================= -This section of the User Guide provides detailed information about the various commands available in ManagerX. Each command is explained with its purpose, usage syntax, and examples to help you understand how to effectively utilize them in your projects. - -.. toctree:: - :maxdepth: 2 - :caption: Commands: - - Moderation - Fun Commands Overview \ No newline at end of file diff --git a/docs/user_guide/index.rst b/docs/user_guide/index.rst deleted file mode 100644 index 66648c0..0000000 --- a/docs/user_guide/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -User Guide -======================= -Welcome to the ManagerX User Guide! This section provides detailed information on how to use ManagerX, including tutorials, feature explanations, and best practices. - -.. toctree:: - :maxdepth: 2 - :caption: Guide: - - Commands - Setup diff --git a/examples/plugins/README.md b/examples/plugins/README.md deleted file mode 100644 index 15166d4..0000000 --- a/examples/plugins/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# ManagerX Plugin-System - -Das Plugin-System von **ManagerX** erlaubt es Entwicklern und der Community, den Bot flexibel zu erweitern. -Jedes Plugin ist eine **Extension**, die neue Funktionen bereitstellt und in den Bot integriert werden kann. - -## Wie Plugins funktionieren - -- **Freiwillige Entwicklung:** Jeder kann Plugins für ManagerX erstellen, sei es für neue Befehle, Automatisierungen oder Utility-Funktionen. -- **Modularer Aufbau:** Plugins sind von Haus aus **separat vom Hauptcode**. Das sorgt für Stabilität und einfache Updates. -- **Manuelles Laden:** Im Gegensatz zu manchen Bot-Systemen werden Plugins in ManagerX manuell in Cogs oder direkt im Bot geladen – so behält der Betreiber volle Kontrolle. -- **PyPI & Local Plugins:** - - **PyPI Plugins**: Externe Plugins, die über PyPI installiert werden können. - - **Local Plugins**: Plugins, die lokal im Projekt gespeichert und direkt vom Bot geladen werden. - -## Vorteile des Systems - -1. **Community-getrieben** – Jeder kann Ideen umsetzen. -2. **Sauber & modular** – Plugins stören den Kern-Bot nicht. -3. **Erweiterbar** – Neue Funktionen können einfach hinzugefügt oder entfernt werden. -4. **Sicher** – Trennung von Kern-Bot und Plugins reduziert das Risiko von Fehlern. - -## Offizielle Plugins - -Damit ein Plugin als offizielle Extension gilt, muss es alle Anforderungen des **ManagerX Plugin Guidelines** erfüllen. -Nur so kann es in die **offizielle Dokumentation** aufgenommen und vom Bot als „offizielles Plugin“ behandelt werden. - ---- - -### Mehr dazu - -Weitere Informationen, Beispiele und detaillierte Anleitungen zu Plugins findest du **bald in unseren offiziellen Docs**. -Bleib dran, um zu erfahren, wie du eigene Plugins entwickeln und veröffentlichen kannst! diff --git a/examples/plugins/example__init__.py b/examples/plugins/example__init__.py deleted file mode 100644 index 2b2e17a..0000000 --- a/examples/plugins/example__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -ManagerX Example Plugin - -This package exposes functions for ManagerX bots. -No automatic bot registration is done here. -""" - -from .functions import hello, add_numbers, multiply_numbers, format_user_message -from .utils import current_time, is_even - -__all__ = [ - "hello", - "add_numbers", - "multiply_numbers", - "format_user_message", - "current_time", - "is_even" -] \ No newline at end of file diff --git a/examples/plugins/example_plugin.py b/examples/plugins/example_plugin.py deleted file mode 100644 index 29e2725..0000000 --- a/examples/plugins/example_plugin.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -This Cog loads the functions from the PyPI plugin -and exposes them as Discord commands. -""" - -from discord.ext import commands -from managerx_example_plugin import ( - hello, - add_numbers, - multiply_numbers, - format_user_message, - current_time, - is_even -) - -class ExamplePlugin(commands.Cog): - """Example Cog that uses the ManagerX Example Plugin functions.""" - - def __init__(self, bot): - self.bot = bot - - @commands.command(name="sayhello") - async def say_hello(self, ctx): - user = ctx.author.name - result = hello(user) - await ctx.send(result) - - @commands.command(name="add") - async def add(self, ctx, a: int, b: int): - result = add_numbers(a, b) - await ctx.send(f"{a} + {b} = {result}") - - @commands.command(name="multiply") - async def multiply(self, ctx, a: int, b: int): - result = multiply_numbers(a, b) - await ctx.send(f"{a} × {b} = {result}") - - @commands.command(name="formatmsg") - async def formatmsg(self, ctx, *, message: str): - user = ctx.author.name - formatted = format_user_message(user, message) - await ctx.send(formatted) - - @commands.command(name="time") - async def time(self, ctx): - await ctx.send(f"Current time: {current_time()}") - - @commands.command(name="iseven") - async def is_even_cmd(self, ctx, number: int): - result = is_even(number) - await ctx.send(f"{number} is even? {result}") - -# Setup function for bot -async def setup(bot): - await bot.add_cog(ExamplePlugin(bot)) \ No newline at end of file diff --git a/examples/plugins/example_pyproject.toml b/examples/plugins/example_pyproject.toml deleted file mode 100644 index fa1f167..0000000 --- a/examples/plugins/example_pyproject.toml +++ /dev/null @@ -1,29 +0,0 @@ -[project] -name = "managerx-example-plugin" # Name muss managerx-* sein -version = "0.1.0" # Startversion -description = "An example plugin for ManagerX demonstrating PyPI plugin structure" -readme = "README.md" # Für PyPI / Dokumentation -authors = [ - { name = "Your Name", email = "you@example.com" } -] -license = { text = "GPL-3.0-or-later" } -keywords = ["managerx", "plugin", "example"] -classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", - "Operating System :: OS Independent" -] -requires-python = ">=3.10" - -[project.dependencies] -# Beispielabhängigkeiten, falls dein Plugin externe Bibliotheken benötigt -# requests = ">=2.30.0" -# aiohttp = ">=3.8.0" - -[build-system] -requires = ["setuptools>=42", "wheel"] -build-backend = "setuptools.build_meta" - -# Optional: entry points für Plugins, falls später automatische Discovery genutzt wird -[project.scripts] -# hello-plugin = "managerx_example_plugin.functions:hello" diff --git a/examples/plugins/functions.py b/examples/plugins/functions.py deleted file mode 100644 index c880583..0000000 --- a/examples/plugins/functions.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -This file contains the main plugin functions. -These functions are independent and can be used in any bot Cog. -""" - -def hello(user: str) -> str: - """ - Example greeting function. - """ - return f"Hello {user}! Welcome to ManagerX Example Plugin." - -def add_numbers(a: int, b: int) -> int: - """ - Example calculation function. - """ - return a + b - -def multiply_numbers(a: int, b: int) -> int: - """ - Another example function for multiplication. - """ - return a * b - -def format_user_message(user: str, message: str) -> str: - """ - Formats a message with the user's name. - """ - return f"{user} says: {message}" \ No newline at end of file diff --git a/examples/plugins/utils.py b/examples/plugins/utils.py deleted file mode 100644 index dd172ff..0000000 --- a/examples/plugins/utils.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Utility functions for ManagerX Example Plugin. -""" - -from datetime import datetime - -def current_time() -> str: - """ - Returns the current time as a formatted string. - """ - return datetime.now().strftime("%Y-%m-%d %H:%M:%S") - -def is_even(number: int) -> bool: - """ - Returns True if the number is even, False otherwise. - """ - return number % 2 == 0 \ No newline at end of file diff --git a/main.py b/main.py index cc5e344..6dc07c8 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,8 @@ -# Copyright (c) 2025 OPPRO.NET Network """ ManagerX Discord Bot - Main Entry Point ======================================== +Copyright (c) 2025 OPPRO.NET Network Version: 2.0.0 """ @@ -10,276 +10,124 @@ # IMPORTS # ============================================================================= import discord -import os -import asyncio -import logging -import re import sys -import glob -import json -from datetime import datetime -from dotenv import load_dotenv +from pathlib import Path from colorama import Fore, Style, init as colorama_init -import aiohttp -import traceback -from pathlib import Path +from dotenv import load_dotenv import ezcord from ezcord import CogLog -import yaml -from discord.ext import tasks -from logger import logger, LogLevel, LogFormat, Category +# Logger (muss existieren!) +from logger import logger +# Lokale Module aus src/bot/core +from src.bot.core.config import ConfigLoader, BotConfig +from src.bot.core.bot_setup import BotSetup +from src.bot.core.cog_manager import CogManager +from src.bot.core.database import DatabaseManager +from src.bot.core.dashboard import DashboardTask +from src.bot.core.utils import print_logo + +# ============================================================================= +# SETUP +# ============================================================================= BASEDIR = Path(__file__).resolve().parent load_dotenv(dotenv_path=BASEDIR / 'config' / '.env') +colorama_init(autoreset=True) - -# ❗ LOKALE BIBLIOTHEKEN -try: - from DevTools import SettingsDB - - class BotConfig: - TOKEN = os.getenv("TOKEN") - -except ImportError as e: - print(f"[{Fore.RED}CRITICAL{Style.RESET_ALL}] [STARTUP] Fataler Fehler beim Import der lokalen Bibliotheken: {e.__class__.__name__}: {e}") - sys.exit(1) - - -if os.path.dirname(os.path.abspath(__file__)) not in sys.path: - sys.path.append(os.path.dirname(os.path.abspath(__file__))) - +# Sys-Path +if str(BASEDIR) not in sys.path: + sys.path.append(str(BASEDIR)) # ============================================================================= -# INITIALISIERUNG & CONFIG LOADING +# MAIN EXECUTION # ============================================================================= - -colorama_init(autoreset=True) - -config_path = BASEDIR / 'config' / 'config.yaml' -try: - with open(config_path, 'r', encoding='utf-8') as f: - config = yaml.safe_load(f) - - if not config.get('enabled', True): - print(f"[{Fore.YELLOW}INFO{Style.RESET_ALL}] Bot ist in config.yaml deaktiviert. Beende...") - sys.exit(0) - - config_version = config.get('version', '1.0.0') - BotConfig.VERSION = config_version - - features = config.get('features', {}) - update_checker_enabled = features.get('update_checker', True) - bot_status_enabled = features.get('bot_status', True) - cogs_config = features.get('cogs', {}) +if __name__ == '__main__': + # Logo ausgeben + print_logo() - bot_behavior = config.get('bot_behavior', {}) - command_prefix = bot_behavior.get('command_prefix', '!') - global_cooldown = bot_behavior.get('global_cooldown_seconds', 5) - max_messages_per_minute = bot_behavior.get('max_messages_per_minute', 10) - maintenance_mode = bot_behavior.get('maintenance_mode', False) + # Konfiguration laden + logger.info("BOT", "Lade Konfiguration...") + config_loader = ConfigLoader(BASEDIR) + config = config_loader.load() + logger.success("BOT", "Konfiguration geladen") - ui_config = config.get('ui', {}) - embed_color = ui_config.get('embed_color', '#00ff00') - footer_text = ui_config.get('footer_text', 'ManagerX Bot') - theme = ui_config.get('theme', 'dark') - show_timestamps = ui_config.get('show_timestamps', True) + # Bot erstellen + logger.info("BOT", "Initialisiere Bot...") + bot_setup = BotSetup(config) + bot = bot_setup.create_bot() - security_config = config.get('security', {}) - required_permissions = security_config.get('required_permissions', []) - blacklist_servers = security_config.get('blacklist_servers', []) - whitelist_users = security_config.get('whitelist_users', []) - enable_command_logging = security_config.get('enable_command_logging', True) + # Datenbank initialisieren (optional - Bot läuft auch ohne) + db_manager = DatabaseManager() + if not db_manager.initialize(bot): + logger.warning("DATABASE", "Bot läuft ohne Datenbank weiter...") + else: + logger.success("DATABASE", "Datenbank erfolgreich initialisiert") - performance_config = config.get('performance', {}) - max_concurrent_tasks = performance_config.get('max_concurrent_tasks', 10) - task_timeout = performance_config.get('task_timeout_seconds', 30) - memory_limit = performance_config.get('memory_limit_mb', 512) - enable_gc_optimization = performance_config.get('enable_gc_optimization', True) + # Dashboard-Task registrieren + dashboard = DashboardTask(bot, BASEDIR) + dashboard.register() -except Exception as e: - print(f"[{Fore.RED}ERROR{Style.RESET_ALL}] Fehler beim Laden der config.yaml: {e}") - sys.exit(1) - -# ============================================================================= -# COG LOGIK -# ============================================================================= - -def get_ignored_list(cogs_config): - """ - Erstellt eine Liste von Dateinamen (ohne .py), die EzCord ignorieren soll. - """ - # 1. Manuelle Liste von Hilfsdateien, die KEINE Cogs sind - ignored = [ - "autocomplete", - "cache", - "components", - "config", - "containers", - "utils", - "backend", # Falls DevTools Ordner gescannt werden würden - "emojis" - ] - - # Mapping für Deaktivierung via config.yaml - # Hier prüfen wir nur, welche Cogs laut Config auf 'false' stehen - cog_mapping = { - 'fun': { - 'gewinnt': 'gewinnt', - 'tictactoe': 'tictactoe', - 'weather': 'weather', - 'wikipedia': 'cog' # Die Wikipedia Hauptdatei heißt 'cog.py' - }, - 'information': { - 'botstatus': 'botstatus', - 'serverinfo': 'serverinfo', - 'usermanagemt': 'usermanagemt' - }, - 'moderation': { - 'antispam': 'antispam', - 'moderation': 'moderation', - 'notes': 'notes', - 'warningsystem': 'warningsystem' - }, - 'server_management': { - 'autodelete': 'autodelete', - 'globalchat': 'globalchat', - 'levelsystem': 'levelsystem', - 'logging': 'logging', - 'stats': 'stats', - 'tempvc': 'tempvc', - 'welcome': 'welcome' - }, - 'other': { - 'setlang': 'setlang' - } - } + # Event Handler + @bot.event + async def on_ready(): + logger.success("BOT", f"Logged in as {bot.user.name}") + + # Dashboard starten + dashboard.start() + + # Bot-Status + if config['features'].get('bot_status', True): + await bot.change_presence( + activity=discord.Activity( + type=discord.ActivityType.watching, + name=f"ManagerX v{BotConfig.VERSION}" + ) + ) + + # Commands sync + await bot.sync_commands() + logger.success("COMMANDS", "Application Commands synchronisiert") - for category, cogs in cog_mapping.items(): - category_config = cogs_config.get(category, {}) - for cog_key, file_name in cogs.items(): - if not category_config.get(cog_key, True): - ignored.append(file_name) - - return ignored - -# ============================================================================= -# BOT INITIALISIERUNG -# ============================================================================= - -intents = discord.Intents.default() -intents.members = True -intents.message_content = True - -bot = ezcord.Bot( - intents=intents, - language="de" -) - -bot.config = { - 'embed_color': embed_color, - 'footer_text': footer_text, - 'theme': theme, - 'show_timestamps': show_timestamps, - 'maintenance_mode': maintenance_mode, - 'global_cooldown': global_cooldown, - 'max_messages_per_minute': max_messages_per_minute, - 'required_permissions': required_permissions, - 'blacklist_servers': blacklist_servers, - 'whitelist_users': whitelist_users, - 'enable_command_logging': enable_command_logging, - 'max_concurrent_tasks': max_concurrent_tasks, - 'task_timeout': task_timeout, - 'memory_limit': memory_limit, - 'enable_gc_optimization': enable_gc_optimization -} - -# ============================================================================= -# DASHBOARD EXPORT TASK -# ============================================================================= - -@tasks.loop(minutes=1) -async def update_dashboard_data(): - try: - stats = { - "bot_info": { - "name": str(bot.user.name), - "status": "online", - "latency": round(bot.latency * 1000, 1) - }, - "stats": { - "server_count": len(bot.guilds), - "user_count": sum(g.member_count for g in bot.guilds if g.member_count), - "shards": bot.shard_count or 1 - }, - "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - with open(BASEDIR / 'bot_stats.json', 'w', encoding='utf-8') as f: - json.dump(stats, f, indent=4, ensure_ascii=False) - except: - pass - -# ============================================================================= -# EVENTS -# ============================================================================= - -@bot.event -async def on_ready(): - logger.success(Category.BOT, f"Logged in as {bot.user.name}") - if not update_dashboard_data.is_running(): - update_dashboard_data.start() + # Minimaler KeepAlive Cog - damit Bot immer online bleibt + class KeepAlive(discord.ext.commands.Cog): + """Minimal Cog to keep bot online""" + def __init__(self, bot): + self.bot = bot + + @discord.ext.commands.Cog.listener() + async def on_ready(self): + logger.info("KEEPALIVE", "KeepAlive Cog aktiv - Bot bleibt online") - if bot_status_enabled: - await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=f"ManagerX v{BotConfig.VERSION}")) - - await bot.sync_commands() - logger.success(Category.COMMANDS, "Application Commands synchronisiert.") - -# ============================================================================= -# MAIN EXECUTION -# ============================================================================= - -if __name__ == '__main__': - # Definieren des Logos als Liste von Strings, um Formatierungsprobleme zu umgehe - logo_lines = [ - r" _____ ______ ________ ________ ________ ________ _______ ________ ___ ___ ", - r"|\ _ \ _ \|\ __ \|\ ___ \|\ __ \|\ ____\|\ ___ \ |\ __ \ |\ \ / /|", - r"\ \ \\\__\ \ \ \ \|\ \ \ \\ \ \ \ \|\ \ \ \___|\ \ __/|\ \ \|\ \ \ \ \/ / /", - r" \ \ \\|__| \ \ \ __ \ \ \\ \ \ \ __ \ \ \ __\ \ _|/_\ \ _ _\ \ \ / / ", - r" \ \ \ \ \ \ \ \ \ \ \ \\ \ \ \ \ \ \ \ \|\ \ \ \_|\ \ \ \\ \| / \/ ", - r" \ \__\ \ \__\ \__\ \__\ \__\\ \__\ \__\ \__\ \_______\ \_______\ \__\\ _\ / /\ \ ", - r" \|__| \|__|\|__|\|__|\|__| \|__|\|__|\|__|\|_______|\|_______|\|__|\|__|/__/ /\ __\ ", - r" |__|/ \|__| " - ] - - # Ausgabe - print(Fore.CYAN) - for line in logo_lines: - print(line) - print(f"{'=' * 91}") - print(f" ManagerX Discord Bot v{BotConfig.VERSION}") - print(f"{'=' * 91}{Style.RESET_ALL}\n") + # KeepAlive Cog immer laden + bot.add_cog(KeepAlive(bot)) + logger.success("BOT", "KeepAlive Cog geladen") - try: - db = SettingsDB() - bot.settings_db = db - logger.info(Category.DATABASE, "Settings Database initialized ✓") - except Exception as e: - logger.critical(Category.DATABASE, f"Datenbankfehler: {e}") - - # --- GEFIXTER LOAD-PROZESS --- - ignored = get_ignored_list(cogs_config) + # Cogs laden + logger.info("BOT", "Lade Cogs...") + cog_manager = CogManager(config['cogs']) + ignored = cog_manager.get_ignored_cogs() bot.load_cogs( - "src/cogs", - subdirectories=True, + "src/bot/cogs", + subdirectories=True, ignored_cogs=ignored, log=CogLog.sum ) - + logger.success("BOT", "Cogs geladen") + + # Token prüfen if not BotConfig.TOKEN: - logger.critical(Category.DEBUG, "Kein TOKEN gefunden!") - import sys + logger.critical("DEBUG", "Kein TOKEN in .env gefunden!") sys.exit(1) - bot.run(BotConfig.TOKEN) \ No newline at end of file + # Bot starten + logger.info("BOT", "Starte Bot...") + try: + bot.run(BotConfig.TOKEN) + except discord.LoginFailure: + logger.critical("BOT", "Ungültiger Token!") + sys.exit(1) + except Exception as e: + logger.critical("BOT", f"Bot-Start fehlgeschlagen: {e}") + sys.exit(1) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d3e6b88..6e5ae71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ManagerX" -version = "2026.0.1" +version = "2.2026.01.11" description = "A powerful Discord bot for server management and fun." readme = "README.md" requires-python = ">=3.8" @@ -45,8 +45,8 @@ dependencies = [ "MarkupSafe==3.0.3", "starlette==0.50.0", "timedelta==2020.12.3", - "ManagerX-Handler==1.0.4", - "ManagerX-DevTools==1.0.0" + "ManagerX-Handler==1.2026.01.10", + "ManagerX-DevTools==1.2026.01.11.1" ] [project.urls] diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 2eebada..0000000 --- a/renovate.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "extends": [ - "config:recommended" - ], - "labels": [ - "dependencies" - ], - "prHourlyLimit": 1, - "prConcurrentLimit": 3, - "dependencyDashboard": true, - "dependencyDashboardTitle": "ManagerX – Dependency Updates", - "automerge": false, - "rangeStrategy": "pin", - "timezone": "Europe/Berlin", - - "schedule": ["immediately"], - "pip_requirements": { - "fileMatch": ["requirements/.*\\.txt"] - }, - "packageRules": [ - { - "matchDepTypes": ["dependencies"], - "matchUpdateTypes": ["patch", "minor"], - "groupName": "All Patch & Minor Python Updates" - }, - { - "matchDepTypes": ["dependencies"], - "matchUpdateTypes": ["major"], - "groupName": "All Major Python Updates" - }, - { - "matchDepTypes": ["github-actions"], - "groupName": "All GitHub Actions Updates" - } - ] - -} diff --git a/requirements/bot_req.txt b/requirements/bot_req.txt deleted file mode 100644 index c57a8c7..0000000 --- a/requirements/bot_req.txt +++ /dev/null @@ -1,16 +0,0 @@ -ezcord==0.7.4 -py-cord==2.7.0 -aiosqlite==0.22.1 -aiohttp==3.13.2 -aiocache==0.12.3 -propcache==0.4.1 -requests==2.32.5 -wikipedia==1.4.0 -beautifulsoup4==4.14.3 -soupsieve==2.8.1 -yarl==1.22.0 -frozenlist==1.8.0 -h11==0.16.0 -multidict==6.7.0 -ManagerX-Handler==1.0.1 -ManagerX-DevTools==1.0.0 \ No newline at end of file diff --git a/requirements/dev_req.txt b/requirements/dev_req.txt deleted file mode 100644 index 069cae8..0000000 --- a/requirements/dev_req.txt +++ /dev/null @@ -1,21 +0,0 @@ -python-dotenv==1.2.1 -click==8.3.1 -colorama==0.4.6 -typing_extensions==4.15.0 -typing-inspection==0.4.2 -attrs==25.4.0 -annotated-types==0.7.0 -anyio==4.12.0 -certifi==2025.11.12 -charset-normalizer==3.4.4 -idna==3.11 -urllib3==2.6.2 -Jinja2==3.1.6 -MarkupSafe==3.0.3 -starlette==0.50.0 -FastAPI -uvicorn -SimpleColoredLogs -timedelta==2020.12.3 -ManagerX-Handler==1.0.1 -ManagerX-DevTools==1.0.0 \ No newline at end of file diff --git a/requirements/docs_req.txt b/requirements/docs_req.txt deleted file mode 100644 index 57b3817..0000000 --- a/requirements/docs_req.txt +++ /dev/null @@ -1,7 +0,0 @@ -# You need to install Sphinx with `pip install sphinx` and these extensions to build the docs -pydata-sphinx-theme # for a modern documentation theme -sphinx-autodoc-typehints # for better type hinting support -myst-parser # for Markdown support -sphinx-copybutton # adds copy buttons to code blocks -sphinx-autobuild # optional: live preview during development - # (remove this before pushing; ReadTheDocs doesn't need it) diff --git a/requirements/req.txt b/requirements/req.txt deleted file mode 100644 index ef43f5d..0000000 --- a/requirements/req.txt +++ /dev/null @@ -1,47 +0,0 @@ -ezcord==0.7.4 -py-cord==2.7.0 -aiosqlite==0.22.1 -aiohttp==3.13.2 -aiocache==0.12.3 -propcache==0.4.1 -requests==2.32.5 -wikipedia==1.4.0 -beautifulsoup4==4.14.3 -soupsieve==2.8.1 -yarl==1.22.0 -frozenlist==1.8.0 -h11==0.16.0 -multidict==6.7.0 -FastAPI==0.128.0 -uvicorn==0.22.0 -ManagerX-Handler==1.0.1 -ManagerX-DevTools==1.0.0 - -# Dev -python-dotenv==1.2.1 -click==8.3.1 -colorama==0.4.6 -typing_extensions==4.15.0 -typing-inspection==0.4.2 -attrs==25.4.0 -annotated-types==0.7.0 -anyio==4.12.0 -certifi==2025.11.12 -charset-normalizer==3.4.4 -idna==3.11 -urllib3==2.6.2 -Jinja2==3.1.6 -MarkupSafe==3.0.3 -starlette==0.50.0 -FastAPI -uvicorn -SimpleColoredLogs -timedelta==2020.12.3 - -# Docs -sphinx -pydata-sphinx-theme -sphinx-autodoc-typehints -myst-parser -sphinx-copybutton -sphinx-autobuild \ No newline at end of file diff --git a/site/callback.html b/site/callback.html deleted file mode 100644 index 836ab52..0000000 --- a/site/callback.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - Logging in... - - -

Authentifizierung erfolgreich. Leite weiter...

- - - - \ No newline at end of file diff --git a/site/css/styles.css b/site/css/styles.css deleted file mode 100644 index 6be28ad..0000000 --- a/site/css/styles.css +++ /dev/null @@ -1,481 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap'); - -:root { - --primary: #5865F2; - --primary-glow: rgba(88, 101, 242, 0.4); - --bg: #0b0e14; - --card-bg: rgba(255, 255, 255, 0.03); - --border: rgba(255, 255, 255, 0.08); - --text: #ffffff; - --text-muted: #a0a0a0; -} - -* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', sans-serif; } -body { background: var(--bg); color: var(--text); line-height: 1.6; overflow-x: hidden; } - -#particleCanvas { position: fixed; top: 0; left: 0; z-index: -1; opacity: 0.5; pointer-events: none; } - -/* NAVIGATION - Modern & Clean */ -nav { - display: flex; justify-content: space-between; align-items: center; - padding: 20px 10%; background: rgba(11, 14, 20, 0.8); - backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); - border-bottom: 1px solid var(--border); - position: sticky; top: 0; z-index: 100; -} - -.nav-content { display: flex; justify-content: space-between; align-items: center; width: 100%; } - -.logo { - font-size: 1.6rem; font-weight: 800; - background: linear-gradient(90deg, #fff, var(--primary)); - -webkit-background-clip: text; -webkit-text-fill-color: transparent; -} - -.links { display: flex; align-items: center; gap: 20px; } -.links a { color: var(--text-muted); text-decoration: none; font-weight: 600; transition: 0.3s; } -.links a:hover { color: var(--primary); text-shadow: 0 0 10px var(--primary-glow); } - -.user-profile { display: flex; align-items: center; color: var(--text); font-weight: 600; } - -/* HERO SECTION - Starker Fokus */ -.hero { - height: 65vh; display: flex; flex-direction: column; - justify-content: center; align-items: center; text-align: center; - padding: 0 10%; -} - -.hero h1 { - font-size: 4rem; font-weight: 800; margin-bottom: 15px; - letter-spacing: -2px; line-height: 1.1; - background: linear-gradient(to bottom, #fff 0%, #a0a0a0 100%); - -webkit-background-clip: text; -webkit-text-fill-color: transparent; -} - -.hero p { color: var(--text-muted); font-size: 1.2rem; max-width: 600px; margin-bottom: 35px; } - -/* STATS - Schickes Grid */ -.stats-container { - display: flex; justify-content: center; gap: 50px; - padding: 80px 10%; background: linear-gradient(180deg, rgba(88, 101, 242, 0.05) 0%, transparent 100%); -} - -.stat-card { text-align: center; padding: 20px; } -.stat-value { font-size: 3.5rem; font-weight: 800; color: var(--primary); display: block; text-shadow: 0 0 20px var(--primary-glow); } -.stat-label { color: var(--text-muted); text-transform: uppercase; letter-spacing: 2px; font-size: 0.8rem; } - -/* GLASS CARDS - Das Herzstück */ -.glass-card { - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--border); - border-radius: 24px; - padding: 40px; - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - box-shadow: 0 20px 40px rgba(0,0,0,0.3); - margin-bottom: 30px; - transition: 0.3s ease; -} - -.glass-card:hover { - transform: translateY(-2px); - box-shadow: 0 25px 50px rgba(0,0,0,0.4); -} - -/* GUILD CARDS */ -.guild-grid { - display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 20px; margin-top: 30px; -} - -.guild-card { - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--border); - border-radius: 16px; - padding: 20px; - text-align: center; - transition: 0.3s ease; - cursor: pointer; -} - -.guild-card:hover { - transform: translateY(-5px); - box-shadow: 0 15px 30px rgba(88, 101, 242, 0.2); - border-color: var(--primary); -} - -.guild-card img { width: 64px; height: 64px; border-radius: 50%; margin-bottom: 15px; } -.guild-card h3 { margin-bottom: 10px; color: var(--text); } -.guild-card a { text-decoration: none; } - -/* MODULE CARDS */ -.module-grid { - display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 25px; margin-top: 30px; -} - -.module-card { - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--border); - border-radius: 20px; - padding: 30px; - text-align: center; - transition: 0.3s ease; - position: relative; - overflow: hidden; -} - -.module-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(90deg, var(--primary), transparent); - opacity: 0; - transition: 0.3s ease; -} - -.module-card:hover { - transform: translateY(-8px); - box-shadow: 0 20px 40px rgba(88, 101, 242, 0.15); - border-color: var(--primary); -} - -.module-card:hover::before { - opacity: 1; -} - -.module-card.disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.module-card.disabled:hover { - transform: none; - box-shadow: none; -} - -.module-icon { - font-size: 2.5rem; margin-bottom: 15px; - display: block; -} - -.module-card h3 { - margin-bottom: 15px; color: var(--text); font-size: 1.3rem; -} - -.module-card p { - color: var(--text-muted); font-size: 0.95rem; line-height: 1.5; margin-bottom: 20px; -} - -/* SERVER HEADER */ -.server-header { - display: flex; - align-items: center; - gap: 20px; - margin-bottom: 40px; -} - -.guild-avatar { - width: 80px; - height: 80px; - border-radius: 50%; - border: 3px solid var(--primary); - box-shadow: 0 0 20px var(--primary-glow); -} - -.guild-info h1 { - font-size: 2.2rem; - margin-bottom: 5px; -} - -.guild-info p { - color: var(--text-muted); -} - -/* MODULE PAGES */ -.module-header { - text-align: center; - margin-bottom: 40px; -} - -.module-icon-large { - font-size: 4rem; - margin-bottom: 20px; - display: block; -} - -.module-header h1 { - font-size: 2.5rem; - margin-bottom: 10px; -} - -.module-header p { - color: var(--text-muted); - font-size: 1.1rem; -} - -/* FORM ELEMENTS */ -.form-section { - margin-bottom: 40px; -} - -.form-section h3 { - color: var(--primary); - margin-bottom: 20px; - font-size: 1.3rem; -} - -.form-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 20px; -} - -.input-group { - margin-bottom: 20px; -} - -.input-group label { - display: block; - margin-bottom: 8px; - color: var(--text); - font-weight: 600; -} - -.input-group input, -.input-group select { - width: 100%; - padding: 12px 16px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--border); - border-radius: 12px; - color: var(--text); - font-size: 1rem; - transition: 0.3s ease; -} - -.input-group input:focus, -.input-group select:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 3px var(--primary-glow); -} - -.input-group small { - display: block; - margin-top: 5px; - color: var(--text-muted); - font-size: 0.85rem; -} - -/* CHECKBOX STYLING */ -.checkbox-label { - display: flex; - align-items: center; - cursor: pointer; - font-weight: 600; -} - -.checkbox-label input[type="checkbox"] { - display: none; -} - -.checkmark { - width: 20px; - height: 20px; - border: 2px solid var(--border); - border-radius: 4px; - margin-right: 10px; - position: relative; - transition: 0.3s ease; -} - -.checkbox-label input[type="checkbox"]:checked + .checkmark { - background: var(--primary); - border-color: var(--primary); -} - -.checkbox-label input[type="checkbox"]:checked + .checkmark::after { - content: '✓'; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: white; - font-size: 12px; - font-weight: bold; -} - -/* FORM ACTIONS */ -.form-actions { - display: flex; - align-items: center; - gap: 20px; - margin-top: 30px; -} - -.save-status { - font-weight: 600; - transition: 0.3s ease; -} - -/* BUTTONS - Der Glow ist zurück */ -.btn-primary { - background: var(--primary); color: white !important; - padding: 16px 36px; border-radius: 14px; text-decoration: none; - font-weight: 700; font-size: 1.1rem; display: inline-block; - transition: 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); - box-shadow: 0 8px 25px var(--primary-glow); - will-change: transform; -} - -.btn-primary:hover { - transform: translateY(-4px) scale(1.02); - box-shadow: 0 12px 35px var(--primary-glow); -} - -/* CONTAINER */ -.container { - max-width: 1200px; margin: 0 auto; padding: 0 20px; -} - -/* RESPONSIVE */ -@media (max-width: 768px) { - .hero h1 { font-size: 2.5rem; } - .module-grid { grid-template-columns: 1fr; } - .guild-grid { grid-template-columns: 1fr; } - .form-grid { grid-template-columns: 1fr; } - .server-header { flex-direction: column; text-align: center; } - nav { padding: 15px 5%; } - .glass-card { padding: 20px; } -} - -/* OPTIMIERTE LEGAL PAGES */ -.legal-container { - max-width: 900px; - margin: 60px auto; - background: rgba(255, 255, 255, 0.02); - border: 1px solid var(--border); - border-radius: 32px; - padding: 60px; - backdrop-filter: blur(20px); - box-shadow: 0 40px 100px rgba(0,0,0,0.5); - position: relative; -} - -.legal-header { - text-align: center; - margin-bottom: 50px; - border-bottom: 1px solid var(--border); - padding-bottom: 30px; -} - -.legal-header h1 { - font-size: 3.2rem; - font-weight: 800; - background: linear-gradient(to bottom, #fff, #888); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - margin-bottom: 10px; -} - -.legal-header p { - color: var(--primary); - text-transform: uppercase; - letter-spacing: 3px; - font-weight: 700; - font-size: 0.9rem; -} - -.legal-content section { - margin-bottom: 40px; - padding: 20px; - border-radius: 16px; - transition: background 0.3s ease; -} - -.legal-content section:hover { - background: rgba(255, 255, 255, 0.015); -} - -.legal-content h2 { - color: var(--primary); - font-size: 1.5rem; - margin-bottom: 15px; - display: flex; - align-items: center; - gap: 12px; -} - -.legal-content h2::before { - content: ''; - width: 4px; - height: 24px; - background: var(--primary); - border-radius: 2px; - display: inline-block; -} - -.legal-content p, .legal-content li { - color: var(--text-muted); - font-size: 1.05rem; - line-height: 1.8; -} - -.legal-content ul { - list-style: none; - margin-top: 15px; -} - -.legal-content li { - margin-bottom: 12px; - padding-left: 25px; - position: relative; -} - -.legal-content li::after { - content: '→'; - position: absolute; - left: 0; - color: var(--primary); -} - -.contact-link { - display: flex; - align-items: center; - justify-content: center; - gap: 15px; - background: rgba(88, 101, 242, 0.1); - border: 1px solid var(--primary); - padding: 20px; - border-radius: 16px; - color: #fff; - text-decoration: none; - font-weight: 600; - transition: 0.3s; - margin-top: 20px; -} - -.contact-link:hover { - background: var(--primary); - transform: scale(1.02); - box-shadow: 0 10px 30px var(--primary-glow); -} - -@media (max-width: 768px) { - .legal-container { padding: 30px; margin: 20px; } - .legal-header h1 { font-size: 2.2rem; } -} - -/* FOOTER */ -footer { padding: 60px 10% 40px; border-top: 1px solid var(--border); text-align: center; color: var(--text-muted); } -footer a { color: var(--text-muted); text-decoration: none; margin: 0 15px; font-weight: 600; transition: 0.3s; } -footer a:hover { color: var(--primary); } - -@media (max-width: 768px) { - .hero h1 { font-size: 2.8rem; } - .stats-container { flex-direction: column; gap: 30px; } -} \ No newline at end of file diff --git a/site/dashboard.html b/site/dashboard.html deleted file mode 100644 index d08b66d..0000000 --- a/site/dashboard.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - ManagerX - Dashboard - - - - - - - - - -
-
-

Server Dashboard

-

Wähle einen Server aus, um die Einstellungen zu verwalten

-
- -
-

🎯 Deine Server

-

Nur Server mit Administrator-Rechten werden angezeigt.

-
-
-
- - - - \ No newline at end of file diff --git a/site/guild.html b/site/guild.html deleted file mode 100644 index df3184b..0000000 --- a/site/guild.html +++ /dev/null @@ -1,102 +0,0 @@ - - - - - ManagerX - Server Hub - - - - - - - - - -
-
- -
-

Lade Server...

-

Verwalte die Bot-Einstellungen für diesen Server

-
-
- -
-

🚀 Verfügbare Module

-
-
-
🔊
-

TempVC

-

Kanäle, Kategorien und Interface-Einstellungen für temporäre Voice-Channels.

- Konfigurieren -
- -
-
👋
-

Welcome

-

Willkommensnachrichten und Embed-Einstellungen für neue Mitglieder.

- Konfigurieren -
- -
-
-

Levelsystem

-

XP-System, Level-Ups und Prestige-Einstellungen.

- Konfigurieren -
- -
-
🛡️
-

Security

-

Anti-Spam, Moderation und Sicherheitsfeatures.

- Bald verfügbar -
- -
-
📊
-

Analytics

-

Server-Statistiken und Nutzungsanalysen.

- Bald verfügbar -
- -
-
🎮
-

Fun

-

Spaß-Commands und Unterhaltungsfeatures.

- Bald verfügbar -
-
-
-
- - - - \ No newline at end of file diff --git a/site/index.html b/site/index.html deleted file mode 100644 index 8c9222a..0000000 --- a/site/index.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - ManagerX | Dashboard - - - - - - - - -
-

ManagerX

-

Willkommen zu ManagerX!

-

Dein Bot für Discord-Server

- -
- -
-
-
0
-
Server
-
-
-
0
-
Nutzer
-
-
-
--ms
-
Latenz (Ping)
-
-
-
Offline
-
System-Status
-
-
- - - - - - - - \ No newline at end of file diff --git a/site/js/api.js b/site/js/api.js deleted file mode 100644 index 853b42f..0000000 --- a/site/js/api.js +++ /dev/null @@ -1,494 +0,0 @@ -// Konfiguriere hier deine Server-URL für die API -// Für lokale Entwicklung: "http://127.0.0.1:3002/api" -// Für Produktion: "https://deine-domain.com/api" -const API_BASE = "https://managerx-api.oppro.net/api"; - -// Hilfsfunktion: Token holen -const getToken = () => localStorage.getItem('discord_token'); -const getRefreshToken = () => localStorage.getItem('discord_refresh_token'); - -// Token-Status prüfen -function checkTokenStatus() { - const token = getToken(); - const refreshToken = getRefreshToken(); - console.log("🔍 Token-Status:"); - console.log(" - Access Token:", token ? "Vorhanden (" + token.substring(0, 10) + "...)" : "Nicht vorhanden"); - console.log(" - Refresh Token:", refreshToken ? "Vorhanden (" + refreshToken.substring(0, 10) + "...)" : "Nicht vorhanden"); - return { hasToken: !!token, hasRefreshToken: !!refreshToken }; -} - -// Debug-Funktion global verfügbar machen -window.checkTokenStatus = checkTokenStatus; - -async function refreshToken() { - const refreshToken = getRefreshToken(); - console.log("🔑 Refresh-Token vorhanden:", refreshToken ? "Ja" : "Nein"); - if (!refreshToken) { - throw new Error("Kein Refresh-Token verfügbar"); - } - - const response = await fetch(`${API_BASE}/auth/refresh`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refresh_token: refreshToken }) - }); - console.log("🔄 Refresh-API Response Status:", response.status); - - if (!response.ok) { - const errorText = await response.text(); - console.error("❌ Refresh-API Fehler:", errorText); - throw new Error("Token-Refresh fehlgeschlagen"); - } - - const data = await response.json(); - console.log("✅ Neuer Token erhalten:", data.access_token ? "Ja" : "Nein"); - localStorage.setItem('discord_token', data.access_token); - if (data.refresh_token) { - localStorage.setItem('discord_refresh_token', data.refresh_token); - } - return data.access_token; -} - -// --- API FETCH HELPER (vereinfacht - bei 401 zur Login-Seite) --- -async function apiFetch(url, options = {}) { - const token = getToken(); - if (!token) { - console.log("❌ Kein Token gefunden - Weiterleitung zur Login-Seite"); - window.location.href = 'index.html'; - throw new Error("Kein Token gefunden"); - } - - // Authorization header für alle Requests - const headers = { ...options.headers, "Authorization": `Bearer ${token}` }; - - let res = await fetch(url, { ...options, headers }); - - // Wenn 401, direkt zur Login-Seite (kein Refresh mehr) - if (res.status === 401) { - console.log("🔄 Token abgelaufen - Weiterleitung zur Login-Seite"); - // Tokens löschen - localStorage.removeItem('discord_token'); - localStorage.removeItem('discord_refresh_token'); - localStorage.removeItem('user_info'); - // Zur Login-Seite mit Hinweis - window.location.href = 'index.html?logged_out=true'; - throw new Error("Token abgelaufen"); - } - - return res; -} - -document.addEventListener('DOMContentLoaded', async () => { - const params = new URLSearchParams(window.location.search); - const guildId = params.get('id'); - const path = window.location.pathname; - - console.log("ManagerX JS geladen auf:", path); - - // --- Seite: dashboard.html --- - if (path.includes('dashboard.html')) { - console.log("Lade Server-Liste"); - await loadGuilds(); - } - - // --- Seite: tempvc.html --- - if (path.includes('tempvc.html')) { - if (!guildId) return window.location.href = '../dashboard.html'; - - console.log("Initialisiere TempVC Modul für Guild:", guildId); - loadTempVCModule(guildId); - - const form = document.getElementById('tempvc-form'); - if (form) { - form.onsubmit = async (e) => { - e.preventDefault(); - await saveTempVC(guildId); - }; - } - } - - // --- Seite: welcome.html --- - if (path.includes('welcome.html')) { - if (!guildId) return window.location.href = '../dashboard.html'; - - console.log("Initialisiere Welcome Modul für Guild:", guildId); - loadWelcomeModule(guildId); - - const form = document.getElementById('welcome-form'); - if (form) { - form.onsubmit = async (e) => { - e.preventDefault(); - await saveWelcome(guildId); - }; - } - } - - // --- Seite: levelsystem.html --- - if (path.includes('levelsystem.html')) { - if (!guildId) return window.location.href = '../dashboard.html'; - - console.log("Initialisiere Levelsystem Modul für Guild:", guildId); - loadLevelsystemModule(guildId); - - const form = document.getElementById('levelsystem-form'); - if (form) { - form.onsubmit = async (e) => { - e.preventDefault(); - await saveLevelsystem(guildId); - }; - } - } -}); - -// --- FUNKTION: Speichern (Ungekürzt) --- -async function saveTempVC(guildId) { - console.log("Speichervorgang für Guild ausgelöst:", guildId); - - const payload = { - creator_channel_id: document.getElementById('creator_channel_id').value, - category_id: document.getElementById('category_id').value, - auto_delete_time: parseInt(document.getElementById('auto_delete_time').value) || 0, - ui_enabled: document.getElementById('ui_enabled').checked, - ui_prefix: document.getElementById('ui_prefix').value || "🔧" - }; - - try { - const response = await apiFetch(`${API_BASE}/guild/${guildId}/tempvc`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - const data = await response.json(); - - if (response.ok) { - alert("✅ Erfolg: " + (data.message || "Gespeichert!")); - } else { - if (response.status === 403 && data.detail && data.detail.includes("deaktiviert")) { - alert("❌ Dieses Feature ist in der Bot-Config deaktiviert."); - return; - } - alert("❌ Fehler: " + (data.detail || "Unbekannter Fehler")); - } - } catch (error) { - console.error("Netzwerkfehler beim Speichern:", error); - alert("❌ Netzwerkfehler: Backend unter http://127.0.0.1:3002 erreichbar?"); - } -} - -// --- FUNKTION: Laden --- -async function loadTempVCModule(guildId) { - try { - const res = await apiFetch(`${API_BASE}/guild/${guildId}/tempvc`); - if (!res.ok) { - if (res.status === 403) { - const errorData = await res.json(); - if (errorData.detail && errorData.detail.includes("deaktiviert")) { - alert("❌ Dieses Feature ist in der Bot-Config deaktiviert."); - window.location.href = `../guild.html?id=${guildId}`; - return; - } - } - throw new Error("Laden fehlgeschlagen: " + (await res.text())); - } - - const data = await res.json(); - - // Lade Kanäle für Dropdowns - await loadChannels(guildId); - - // Felder befüllen - document.getElementById('creator_channel_id').value = data.creator_channel_id || ""; - document.getElementById('category_id').value = data.category_id || ""; - document.getElementById('auto_delete_time').value = data.auto_delete_time || 0; - document.getElementById('ui_enabled').checked = data.ui_enabled || false; - document.getElementById('ui_prefix').value = data.ui_prefix || "🔧"; - } catch (err) { - console.error("Fehler beim Laden der Daten:", err); - alert("❌ Fehler beim Laden: " + err.message); - } -} - -// --- FUNKTION: Kanäle laden --- -async function loadChannels(guildId) { - try { - const res = await apiFetch(`${API_BASE}/guild/${guildId}/channels`); - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`Kanäle laden fehlgeschlagen (${res.status}): ${errorText}`); - } - - const data = await res.json(); - const channels = data.channels; - - // Creator Channel Dropdown (Voice-Kanäle, type 2) - const creatorSelect = document.getElementById('creator_channel_id'); - if (creatorSelect) { - creatorSelect.innerHTML = ''; - channels.filter(ch => ch.type === 2).forEach(ch => { - const option = document.createElement('option'); - option.value = ch.id; - option.textContent = ch.name; - creatorSelect.appendChild(option); - }); - } - - // Kategorie Dropdown (Kategorien, type 4) - const categorySelect = document.getElementById('category_id'); - if (categorySelect) { - categorySelect.innerHTML = ''; - channels.filter(ch => ch.type === 4).forEach(ch => { - const option = document.createElement('option'); - option.value = ch.id; - option.textContent = ch.name; - categorySelect.appendChild(option); - }); - } - - // Level Up Channel Dropdown (Text-Kanäle, type 0) - const levelSelect = document.getElementById('level_up_channel'); - if (levelSelect) { - levelSelect.innerHTML = ''; - channels.filter(ch => ch.type === 0).forEach(ch => { - const option = document.createElement('option'); - option.value = ch.id; - option.textContent = ch.name; - levelSelect.appendChild(option); - }); - } - } catch (err) { - console.error("Fehler beim Laden der Kanäle:", err); - alert("❌ Kanäle konnten nicht geladen werden: " + err.message); - } -} - -// --- FUNKTION: Welcome laden --- -async function loadWelcomeModule(guildId) { - try { - const res = await apiFetch(`${API_BASE}/guild/${guildId}/welcome`); - if (!res.ok) { - if (res.status === 403) { - const errorData = await res.json(); - if (errorData.detail && errorData.detail.includes("deaktiviert")) { - alert("❌ Dieses Feature ist in der Bot-Config deaktiviert."); - window.location.href = `../guild.html?id=${guildId}`; - return; - } - } - throw new Error("Laden fehlgeschlagen: " + (await res.text())); - } - - const data = await res.json(); - - // Lade Kanäle für Dropdowns - await loadChannels(guildId); - - // Felder befüllen - document.getElementById('channel_id').value = data.channel_id || ""; - document.getElementById('welcome_message').value = data.welcome_message || ""; - document.getElementById('enabled').checked = data.enabled || false; - document.getElementById('embed_enabled').checked = data.embed_enabled || false; - document.getElementById('embed_color').value = data.embed_color || "#00ff00"; - document.getElementById('embed_title').value = data.embed_title || ""; - document.getElementById('embed_description').value = data.embed_description || ""; - document.getElementById('embed_thumbnail').checked = data.embed_thumbnail || false; - document.getElementById('embed_footer').value = data.embed_footer || ""; - document.getElementById('ping_user').checked = data.ping_user || false; - document.getElementById('delete_after').value = data.delete_after || 0; - } catch (err) { - console.error("Fehler beim Laden der Welcome-Daten:", err); - alert("❌ Fehler beim Laden: " + err.message); - } -} - -// --- FUNKTION: Levelsystem laden --- -async function loadLevelsystemModule(guildId) { - try { - const res = await apiFetch(`${API_BASE}/guild/${guildId}/levelsystem`); - if (!res.ok) { - if (res.status === 403) { - const errorData = await res.json(); - if (errorData.detail && errorData.detail.includes("deaktiviert")) { - alert("❌ Dieses Feature ist in der Bot-Config deaktiviert."); - window.location.href = `../guild.html?id=${guildId}`; - return; - } - } - throw new Error("Laden fehlgeschlagen: " + (await res.text())); - } - - const data = await res.json(); - - // Lade Kanäle für Dropdowns - await loadChannels(guildId); - - // Felder befüllen - document.getElementById('levelsystem_enabled').checked = data.levelsystem_enabled || false; - document.getElementById('min_xp').value = data.min_xp || 10; - document.getElementById('max_xp').value = data.max_xp || 20; - document.getElementById('xp_cooldown').value = data.xp_cooldown || 30; - document.getElementById('level_up_channel').value = data.level_up_channel || ""; - document.getElementById('prestige_enabled').checked = data.prestige_enabled || false; - document.getElementById('prestige_min_level').value = data.prestige_min_level || 50; - } catch (err) { - console.error("Fehler beim Laden der Levelsystem-Daten:", err); - alert("❌ Fehler beim Laden: " + err.message); - } -} - -// --- FUNKTION: Levelsystem speichern --- -async function saveLevelsystem(guildId) { - console.log("Speichervorgang für Levelsystem ausgelöst:", guildId); - - const payload = { - levelsystem_enabled: document.getElementById('levelsystem_enabled').checked, - min_xp: parseInt(document.getElementById('min_xp').value) || 10, - max_xp: parseInt(document.getElementById('max_xp').value) || 20, - xp_cooldown: parseInt(document.getElementById('xp_cooldown').value) || 30, - level_up_channel: document.getElementById('level_up_channel').value, - prestige_enabled: document.getElementById('prestige_enabled').checked, - prestige_min_level: parseInt(document.getElementById('prestige_min_level').value) || 50 - }; - - try { - const response = await apiFetch(`${API_BASE}/guild/${guildId}/levelsystem`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - const data = await response.json(); - - if (response.ok) { - alert("✅ Erfolg: " + (data.message || "Gespeichert!")); - } else { - if (response.status === 403 && data.detail && data.detail.includes("deaktiviert")) { - alert("❌ Dieses Feature ist in der Bot-Config deaktiviert."); - return; - } - alert("❌ Fehler: " + (data.detail || "Unbekannter Fehler")); - } - } catch (error) { - console.error("Netzwerkfehler beim Speichern:", error); - alert("❌ Netzwerkfehler: Backend unter http://127.0.0.1:3002 erreichbar?"); - } -} - -// --- FUNKTION: Server-Liste laden --- -async function loadGuilds() { - try { - const res = await apiFetch(`${API_BASE}/user/guilds`); - if (!res.ok) throw new Error("Server laden fehlgeschlagen"); - - const guilds = await res.json(); - const guildList = document.getElementById('guild-list'); - - if (guilds.length === 0) { - guildList.innerHTML = '

Keine Server mit Admin-Rechten gefunden.

'; - return; - } - - guildList.innerHTML = ''; - guilds.forEach(guild => { - const guildCard = document.createElement('div'); - guildCard.className = 'guild-card'; - guildCard.innerHTML = ` - ${guild.name} -

${guild.name}

- Verwalten - `; - guildList.appendChild(guildCard); - }); - } catch (err) { - console.error("Fehler beim Laden der Server:", err); - document.getElementById('guild-list').innerHTML = '

❌ Fehler beim Laden der Server.

'; - } -} - -// --- FUNKTION: Guild-Details laden (für guild.html) --- -async function fetchGuildDetails(guildId) { - const token = getToken(); - try { - // Hole Guild-Info von Discord API über unseren Endpoint - const res = await fetch(`${API_BASE}/user/guilds?token=${token}`); - if (!res.ok) throw new Error("Guild-Details laden fehlgeschlagen"); - - const guilds = await res.json(); - const guild = guilds.find(g => g.id == guildId); - - if (guild) { - document.getElementById('guild-icon').src = `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png`; - document.getElementById('guild-icon').onerror = () => this.src = 'https://via.placeholder.com/64x64?text=?'; - document.getElementById('guild-name').textContent = guild.name; - } else { - document.getElementById('guild-name').textContent = 'Server nicht gefunden'; - } - } catch (err) { - console.error("Fehler beim Laden der Guild-Details:", err); - document.getElementById('guild-name').textContent = 'Fehler beim Laden'; - } -} - -// --- FUNKTION: Welcome speichern --- -async function saveWelcome(guildId) { - console.log("Speichervorgang für Welcome ausgelöst:", guildId); - - const payload = { - channel_id: document.getElementById('channel_id').value, - welcome_message: document.getElementById('welcome_message').value, - enabled: document.getElementById('enabled').checked, - embed_enabled: document.getElementById('embed_enabled').checked, - embed_color: document.getElementById('embed_color').value, - embed_title: document.getElementById('embed_title').value, - embed_description: document.getElementById('embed_description').value, - embed_thumbnail: document.getElementById('embed_thumbnail').checked, - embed_footer: document.getElementById('embed_footer').value, - ping_user: document.getElementById('ping_user').checked, - delete_after: parseInt(document.getElementById('delete_after').value) || 0 - }; - - try { - const response = await apiFetch(`${API_BASE}/guild/${guildId}/welcome`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - const data = await response.json(); - - if (response.ok) { - alert("✅ Erfolg: " + (data.message || "Gespeichert!")); - } else { - if (response.status === 403 && data.detail && data.detail.includes("deaktiviert")) { - alert("❌ Dieses Feature ist in der Bot-Config deaktiviert."); - return; - } - alert("❌ Fehler: " + (data.detail || "Unbekannter Fehler")); - } - } catch (error) { - console.error("Netzwerkfehler beim Speichern:", error); - alert("❌ Netzwerkfehler: Backend unter http://127.0.0.1:3002 erreichbar?"); - } -} - -// --- FUNKTION: Bot-Stats laden (für index.html) --- -async function loadBotStats() { - try { - const response = await fetch(`${API_BASE}/managerx/stats`); - const data = await response.json(); - - document.getElementById('server-count').textContent = data.stats?.server_count || '0'; - document.getElementById('user-count').textContent = data.stats?.user_count || '0'; - document.getElementById('bot-ping').textContent = data.bot_info?.latency ? data.bot_info.latency + 'ms' : '--ms'; - document.getElementById('bot-status').textContent = data.bot_info?.status || 'Offline'; - - console.log("✅ Bot-Stats erfolgreich geladen"); - } catch (error) { - console.error('❌ Fehler beim Laden der Bot-Stats:', error); - // Bei Fehler Standardwerte setzen - document.getElementById('server-count').textContent = '--'; - document.getElementById('user-count').textContent = '--'; - document.getElementById('bot-ping').textContent = '--ms'; - document.getElementById('bot-status').textContent = 'Offline'; - } -} \ No newline at end of file diff --git a/site/js/particles.js b/site/js/particles.js deleted file mode 100644 index ed47069..0000000 --- a/site/js/particles.js +++ /dev/null @@ -1,66 +0,0 @@ -const canvas = document.getElementById('particleCanvas'); -const ctx = canvas.getContext('2d'); - -let particlesArray = []; -// Weniger Partikel = mehr FPS -const numberOfParticles = 50; - -canvas.width = window.innerWidth; -canvas.height = window.innerHeight; - -class Particle { - constructor() { - this.reset(); - } - reset() { - this.x = Math.random() * canvas.width; - this.y = Math.random() * canvas.height; - this.size = Math.random() * 1.5 + 0.5; - this.speedX = (Math.random() - 0.5) * 0.5; - this.speedY = (Math.random() - 0.5) * 0.5; - } - update() { - this.x += this.speedX; - this.y += this.speedY; - - if (this.x > canvas.width || this.x < 0) this.speedX *= -1; - if (this.y > canvas.height || this.y < 0) this.speedY *= -1; - } - draw() { - ctx.fillStyle = 'rgba(88, 101, 242, 0.3)'; - ctx.beginPath(); - ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); - ctx.fill(); - } -} - -function init() { - particlesArray = []; - for (let i = 0; i < numberOfParticles; i++) { - particlesArray.push(new Particle()); - } -} - -function animate() { - ctx.clearRect(0, 0, canvas.width, canvas.height); - for (let i = 0; i < particlesArray.length; i++) { - particlesArray[i].update(); - particlesArray[i].draw(); - } - // Verhindert unnötiges Rechnen, wenn man den Tab wechselt - requestAnimationFrame(animate); -} - -// Performance-Check beim Resize -let resizeTimeout; -window.addEventListener('resize', () => { - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(() => { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - init(); - }, 200); -}); - -init(); -animate(); \ No newline at end of file diff --git a/site/modules/index.html b/site/modules/index.html deleted file mode 100644 index 2d256c5..0000000 --- a/site/modules/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - ManagerX - Module - - -

Weiterleitung zu Server-Modulen...

- - \ No newline at end of file diff --git a/site/modules/levelsystem.html b/site/modules/levelsystem.html deleted file mode 100644 index d7aec5b..0000000 --- a/site/modules/levelsystem.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - ManagerX - Levelsystem Einstellungen - - - - - - -
-

⭐ Levelsystem Einstellungen

- -
-
-

Grundeinstellungen

-
- - -
-
- -
-
- -
-

XP-Einstellungen

-
- - -
-
- - -
-
- - -
-
- -
-

Prestige-System

-
- -
-
- - -
-
- - -
-
- - - - \ No newline at end of file diff --git a/site/modules/tempvc.html b/site/modules/tempvc.html deleted file mode 100644 index f1a8867..0000000 --- a/site/modules/tempvc.html +++ /dev/null @@ -1,125 +0,0 @@ - - - - - ManagerX - TempVC Einstellungen - - - - - - - - -
-
-
🔊
-

TempVC Konfiguration

-

Verwalte temporäre Voice-Channels für deinen Server

-
- -
-
-

🎯 Kern-Einstellungen

-
-
- - - Wähle den Channel, bei dem Mitglieder Voice-Channels erstellen können -
- -
- - - Kategorie, in der die temporären Channels erstellt werden -
- -
- - - Zeit bis zum automatischen Löschen leerer Channels (0 = deaktiviert) -
-
-
- -
-

🎨 UI / Interface

-
-
- - Zeigt Interface-Buttons in Voice-Channels an -
- -
- - - Emoji oder Text für Interface-Buttons -
-
-
- -
- -
-
-
-
- - - - \ No newline at end of file diff --git a/site/modules/welcome.html b/site/modules/welcome.html deleted file mode 100644 index d2aa14a..0000000 --- a/site/modules/welcome.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - ManagerX - Welcome Einstellungen - - - - - - -
-

👋 Welcome Einstellungen

- -
-
-

Grundeinstellungen

-
- - -
-
- - -
-
- -
-
- -
-

Embed-Einstellungen

-
- -
-
- - -
-
- - -
-
- - -
-
- -
-
- - -
-
- -
-

Zusätzliche Optionen

-
- -
-
- - -
-
- - -
-
- - - - \ No newline at end of file diff --git a/site/privacy.html b/site/privacy.html deleted file mode 100644 index a91f1de..0000000 --- a/site/privacy.html +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - ManagerX | Datenschutz - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/site/tos.html b/site/tos.html deleted file mode 100644 index 9a04f79..0000000 --- a/site/tos.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - ManagerX | Nutzungsbedingungen - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/bot/__init__.py b/src/bot/__init__.py new file mode 100644 index 0000000..a8ce586 --- /dev/null +++ b/src/bot/__init__.py @@ -0,0 +1 @@ +from .core import * \ No newline at end of file diff --git a/src/bot/cogs/fun/4gewinnt.py b/src/bot/cogs/fun/4gewinnt.py new file mode 100644 index 0000000..9af9f1c --- /dev/null +++ b/src/bot/cogs/fun/4gewinnt.py @@ -0,0 +1,802 @@ +# Copyright (c) 2026 ManagerX Development +# ─────────────────────────────────────────────── +# >> Import +# ─────────────────────────────────────────────── +from discord.ui import Button, View, Select +import discord +from discord.ext import commands +import ezcord +import yaml +from pathlib import Path +from datetime import datetime, timedelta +from typing import Optional, Dict, List, Tuple +import asyncio +import random + +# ─────────────────────────────────────────────── +# >> Constants +# ─────────────────────────────────────────────── +ROWS = 6 +COLUMNS = 7 +DEFAULT_TIMEOUT = 300 # 5 Minuten + +# Improved difficulty levels with better depth and strategy +DIFFICULTY_CONFIG = { + "easy": { + "depth": 2, + "randomness": 0.3, # 30% zufällige Züge + "name": "Anfänger" + }, + "medium": { + "depth": 4, + "randomness": 0.1, # 10% zufällige Züge + "name": "Fortgeschritten" + }, + "hard": { + "depth": 6, + "randomness": 0.0, # Keine zufälligen Züge + "name": "Experte" + } +} + +# ─────────────────────────────────────────────── +# >> Statistics Manager +# ─────────────────────────────────────────────── +class GameStats: + """Verwaltet Spielstatistiken für Connect4""" + + def __init__(self): + self.stats: Dict[int, Dict] = {} + + def get_user_stats(self, user_id: int) -> Dict: + """Gibt Statistiken für einen Benutzer zurück""" + if user_id not in self.stats: + self.stats[user_id] = { + "wins": 0, + "losses": 0, + "draws": 0, + "total_games": 0, + "win_streak": 0, + "best_streak": 0, + "ai_wins": 0, + "ai_losses": 0 + } + return self.stats[user_id] + + def record_win(self, user_id: int, vs_ai: bool = False): + """Zeichnet einen Sieg auf""" + stats = self.get_user_stats(user_id) + stats["wins"] += 1 + stats["total_games"] += 1 + stats["win_streak"] += 1 + stats["best_streak"] = max(stats["best_streak"], stats["win_streak"]) + if vs_ai: + stats["ai_wins"] += 1 + + def record_loss(self, user_id: int, vs_ai: bool = False): + """Zeichnet eine Niederlage auf""" + stats = self.get_user_stats(user_id) + stats["losses"] += 1 + stats["total_games"] += 1 + stats["win_streak"] = 0 + if vs_ai: + stats["ai_losses"] += 1 + + def record_draw(self, user_id: int): + """Zeichnet ein Unentschieden auf""" + stats = self.get_user_stats(user_id) + stats["draws"] += 1 + stats["total_games"] += 1 + stats["win_streak"] = 0 + + def get_winrate(self, user_id: int) -> float: + """Berechnet die Gewinnrate""" + stats = self.get_user_stats(user_id) + if stats["total_games"] == 0: + return 0.0 + return (stats["wins"] / stats["total_games"]) * 100 + +# Global stats instance +game_stats = GameStats() + +# ─────────────────────────────────────────────── +# >> Load messages from YAML +# ─────────────────────────────────────────────── +def load_messages(lang_code: str): + """ + Lädt Nachrichten für den angegebenen Sprachcode. + Fällt auf 'en' und dann auf 'de' zurück, falls die Datei fehlt. + """ + base_path = Path("translation") / "messages" + + lang_file = base_path / f"{lang_code}.yaml" + if not lang_file.exists(): + lang_file = base_path / "en.yaml" + if not lang_file.exists(): + lang_file = base_path / "de.yaml" + + if not lang_file.exists(): + raise FileNotFoundError(f"Missing language files: {lang_code}.yaml, en.yaml, and de.yaml") + + with open(lang_file, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + +# ─────────────────────────────────────────────── +# >> Enhanced AI Engine (Improved Minimax) +# ─────────────────────────────────────────────── +class Connect4AI: + """Verbesserte KI mit optimiertem Minimax-Algorithmus""" + + def __init__(self, difficulty: str = "medium"): + config = DIFFICULTY_CONFIG.get(difficulty, DIFFICULTY_CONFIG["medium"]) + self.max_depth = config["depth"] + self.randomness = config["randomness"] + self.difficulty_name = config["name"] + + def evaluate_window(self, window: List[str], ai_symbol: str, player_symbol: str) -> int: + """Verbesserte Fensterbewertung mit genaueren Heuristiken""" + score = 0 + ai_count = window.count(ai_symbol) + player_count = window.count(player_symbol) + empty_count = window.count("⚪") + + # AI-Bewertung + if ai_count == 4: + score += 10000 # Gewinn + elif ai_count == 3 and empty_count == 1: + score += 100 # Fast gewonnen + elif ai_count == 2 and empty_count == 2: + score += 10 # Gute Position + elif ai_count == 1 and empty_count == 3: + score += 1 # Basis-Position + + # Gegner-Bewertung (Verteidigung) + if player_count == 4: + score -= 10000 # Verloren (sollte nicht passieren) + elif player_count == 3 and empty_count == 1: + score -= 500 # Muss blocken! + elif player_count == 2 and empty_count == 2: + score -= 50 # Gefährliche Position + + return score + + def score_position(self, board: List[List[str]], ai_symbol: str, player_symbol: str) -> int: + """Verbesserte Positionsbewertung mit strategischen Präferenzen""" + score = 0 + + # Zentrum bevorzugen (stärkere Gewichtung) + center_array = [board[i][COLUMNS // 2] for i in range(ROWS)] + center_count = center_array.count(ai_symbol) + score += center_count * 6 + + # Mittlere Spalten bevorzugen + for row in range(ROWS): + for col in [2, 3, 4]: # Mittlere Spalten + if board[row][col] == ai_symbol: + score += 2 + + # Horizontal scannen + for row in range(ROWS): + for col in range(COLUMNS - 3): + window = board[row][col:col + 4] + score += self.evaluate_window(window, ai_symbol, player_symbol) + + # Vertikal scannen + for col in range(COLUMNS): + for row in range(ROWS - 3): + window = [board[row + i][col] for i in range(4)] + score += self.evaluate_window(window, ai_symbol, player_symbol) + + # Diagonal (rechts-unten) + for row in range(ROWS - 3): + for col in range(COLUMNS - 3): + window = [board[row + i][col + i] for i in range(4)] + score += self.evaluate_window(window, ai_symbol, player_symbol) + + # Diagonal (rechts-oben) + for row in range(3, ROWS): + for col in range(COLUMNS - 3): + window = [board[row - i][col + i] for i in range(4)] + score += self.evaluate_window(window, ai_symbol, player_symbol) + + return score + + def get_valid_columns(self, board: List[List[str]]) -> List[int]: + """Gibt alle gültigen Spalten zurück""" + return [col for col in range(COLUMNS) if board[0][col] == "⚪"] + + def is_terminal_node(self, board: List[List[str]], ai_symbol: str, player_symbol: str) -> Tuple[bool, Optional[str]]: + """Prüft ob das Spiel beendet ist und wer gewonnen hat""" + # Check für Gewinn + for symbol in [ai_symbol, player_symbol]: + # Horizontal + for row in range(ROWS): + for col in range(COLUMNS - 3): + if all(board[row][col + i] == symbol for i in range(4)): + return True, symbol + + # Vertikal + for col in range(COLUMNS): + for row in range(ROWS - 3): + if all(board[row + i][col] == symbol for i in range(4)): + return True, symbol + + # Diagonal (rechts-unten) + for row in range(ROWS - 3): + for col in range(COLUMNS - 3): + if all(board[row + i][col + i] == symbol for i in range(4)): + return True, symbol + + # Diagonal (rechts-oben) + for row in range(3, ROWS): + for col in range(COLUMNS - 3): + if all(board[row - i][col + i] == symbol for i in range(4)): + return True, symbol + + # Check für Unentschieden + if len(self.get_valid_columns(board)) == 0: + return True, None + + return False, None + + def minimax(self, board: List[List[str]], depth: int, alpha: float, beta: float, + maximizing: bool, ai_symbol: str, player_symbol: str) -> Tuple[Optional[int], float]: + """Optimierter Minimax mit Alpha-Beta-Pruning und Move-Ordering""" + valid_cols = self.get_valid_columns(board) + is_terminal, winner = self.is_terminal_node(board, ai_symbol, player_symbol) + + # Terminal-Zustände + if depth == 0 or is_terminal: + if is_terminal: + if winner == ai_symbol: + return None, 100000000 + elif winner == player_symbol: + return None, -100000000 + else: + return None, 0 + else: + return None, self.score_position(board, ai_symbol, player_symbol) + + # Move ordering: Zentrum zuerst prüfen + valid_cols.sort(key=lambda x: abs(x - COLUMNS // 2)) + + if maximizing: + value = float('-inf') + column = random.choice(valid_cols) if valid_cols else None + + for col in valid_cols: + temp_board = [row[:] for row in board] + self._drop_piece(temp_board, col, ai_symbol) + new_score = self.minimax(temp_board, depth - 1, alpha, beta, False, ai_symbol, player_symbol)[1] + + if new_score > value: + value = new_score + column = col + + alpha = max(alpha, value) + if alpha >= beta: + break # Beta cutoff + + return column, value + else: + value = float('inf') + column = random.choice(valid_cols) if valid_cols else None + + for col in valid_cols: + temp_board = [row[:] for row in board] + self._drop_piece(temp_board, col, player_symbol) + new_score = self.minimax(temp_board, depth - 1, alpha, beta, True, ai_symbol, player_symbol)[1] + + if new_score < value: + value = new_score + column = col + + beta = min(beta, value) + if alpha >= beta: + break # Alpha cutoff + + return column, value + + def _drop_piece(self, board: List[List[str]], column: int, symbol: str) -> Optional[int]: + """Lässt einen Spielstein in die Spalte fallen und gibt die Zeile zurück""" + for row in reversed(range(ROWS)): + if board[row][column] == "⚪": + board[row][column] = symbol + return row + return None + + def get_best_move(self, board: List[List[str]], ai_symbol: str, player_symbol: str) -> int: + """Gibt den besten Zug zurück mit optionaler Zufälligkeit""" + valid_cols = self.get_valid_columns(board) + + if not valid_cols: + return 0 + + # Zufälligkeit für niedrigere Schwierigkeitsgrade + if random.random() < self.randomness: + return random.choice(valid_cols) + + # Prüfe auf sofortigen Gewinnzug + for col in valid_cols: + temp_board = [row[:] for row in board] + self._drop_piece(temp_board, col, ai_symbol) + is_terminal, winner = self.is_terminal_node(temp_board, ai_symbol, player_symbol) + if is_terminal and winner == ai_symbol: + return col + + # Prüfe ob Gegner blockiert werden muss + for col in valid_cols: + temp_board = [row[:] for row in board] + self._drop_piece(temp_board, col, player_symbol) + is_terminal, winner = self.is_terminal_node(temp_board, ai_symbol, player_symbol) + if is_terminal and winner == player_symbol: + return col + + # Verwende Minimax für den besten Zug + column, _ = self.minimax(board, self.max_depth, float('-inf'), float('inf'), + True, ai_symbol, player_symbol) + + return column if column is not None else random.choice(valid_cols) + +# ─────────────────────────────────────────────── +# >> Game Timer +# ─────────────────────────────────────────────── +class GameTimer: + """Verwaltet Zugzeiten und Gesamtspielzeit""" + + def __init__(self): + self.start_time = datetime.now() + self.move_times: List[timedelta] = [] + self.current_move_start: Optional[datetime] = None + + def start_move(self): + """Startet den Timer für einen Zug""" + self.current_move_start = datetime.now() + + def end_move(self): + """Beendet den Timer für einen Zug""" + if self.current_move_start: + duration = datetime.now() - self.current_move_start + self.move_times.append(duration) + self.current_move_start = None + + def get_game_duration(self) -> timedelta: + """Gibt die Gesamtspielzeit zurück""" + return datetime.now() - self.start_time + + def get_average_move_time(self) -> Optional[timedelta]: + """Gibt die durchschnittliche Zugzeit zurück""" + if not self.move_times: + return None + return sum(self.move_times, timedelta()) / len(self.move_times) + +# ─────────────────────────────────────────────── +# >> Enhanced Button & View +# ─────────────────────────────────────────────── +class Connect4Button(Button): + def __init__(self, column, view): + # Dynamische Farben basierend auf Spalte + styles = [ + discord.ButtonStyle.primary, + discord.ButtonStyle.secondary, + discord.ButtonStyle.success, + ] + style = styles[column % 3] + + # Verteile Buttons auf 2 Reihen (4 + 3) + row = 0 if column < 4 else 1 + + super().__init__(style=style, label=str(column + 1), row=row) + self.column = column + self.view_ref = view + + async def callback(self, interaction: discord.Interaction): + view = self.view_ref + msgs = view.messages + + # Prüfe ob Spiel bereits beendet + if view.game_ended: + await interaction.response.send_message( + "Das Spiel ist bereits beendet!", + ephemeral=True + ) + return + + # PvP mode checks + if not view.is_ai_mode and interaction.user != view.current_player: + await interaction.response.send_message( + msgs["cog_4gewinnt"]["error_types"]["not_your_turn"], + ephemeral=True + ) + return + + # AI mode checks + if view.is_ai_mode and interaction.user != view.player1: + await interaction.response.send_message( + msgs["cog_4gewinnt"]["error_types"]["not_your_turn"], + ephemeral=True + ) + return + + # End move timer + view.timer.end_move() + + if not view.make_move(self.column): + await interaction.response.send_message( + msgs["cog_4gewinnt"]["error_types"]["this_column_full"], + ephemeral=True + ) + view.timer.start_move() + return + + winner = view.check_winner() + board_str = view.board_to_str() + + if winner or view.is_draw(): + await view.end_game(interaction, winner, board_str) + return + + view.switch_player() + + # AI turn + if view.is_ai_mode and view.current_player == view.player2: + await interaction.response.edit_message( + content=f"🤖 **{view.ai.difficulty_name} KI denkt nach...**\n{board_str}", + view=view + ) + + # Simuliere Denkzeit (abhängig von Schwierigkeit) + think_time = { + "easy": 0.5, + "medium": 1.0, + "hard": 1.5 + } + await asyncio.sleep(think_time.get(view.difficulty, 1.0)) + + view.timer.start_move() + ai_col = view.ai.get_best_move(view.board, view.current_symbol, "🔴") + view.timer.end_move() + + view.make_move(ai_col) + + winner = view.check_winner() + board_str = view.board_to_str() + + if winner or view.is_draw(): + await view.end_game(interaction, winner, board_str, is_followup=True) + return + + view.switch_player() + view.timer.start_move() + + # Automatisches Update nach KI-Zug + await interaction.edit_original_response( + content=f"✅ KI hat Spalte **{ai_col + 1}** gewählt!\n\n" + f"{view.current_player.mention}, du bist dran! 🔴\n\n" + f"{board_str}\n" + f"Zug: {view.move_count}", + view=view + ) + else: + view.timer.start_move() + await interaction.response.edit_message( + content=msgs["cog_4gewinnt"]["message"]["player_turn"].format( + current_player=view.current_player.mention, + board_str=board_str, + move_count=view.move_count + ), + view=view + ) + +class Connect4View(View): + def __init__(self, player1, player2, messages, is_ai_mode=False, difficulty="medium"): + super().__init__(timeout=DEFAULT_TIMEOUT) + self.player1 = player1 + self.player2 = player2 + self.current_player = player1 + self.current_symbol = "🔴" + self.board = [["⚪" for _ in range(COLUMNS)] for _ in range(ROWS)] + self.messages = messages + self.is_ai_mode = is_ai_mode + self.difficulty = difficulty + self.ai = Connect4AI(difficulty) if is_ai_mode else None + self.timer = GameTimer() + self.move_count = 0 + self.move_history: List[tuple] = [] + self.game_ended = False + + for col in range(COLUMNS): + self.add_item(Connect4Button(col, self)) + + # Start timer for first move + self.timer.start_move() + + def make_move(self, column: int) -> bool: + """Führt einen Zug aus""" + if column < 0 or column >= COLUMNS: + return False + + for row in reversed(range(ROWS)): + if self.board[row][column] == "⚪": + self.board[row][column] = self.current_symbol + self.move_history.append((row, column, self.current_symbol)) + self.move_count += 1 + return True + return False + + def switch_player(self): + """Wechselt den aktuellen Spieler""" + if self.current_player == self.player1: + self.current_player = self.player2 + self.current_symbol = "🟡" + else: + self.current_player = self.player1 + self.current_symbol = "🔴" + + def check_winner(self) -> bool: + """Überprüft, ob es einen Gewinner gibt""" + b = self.board + + # horizontal + for row in range(ROWS): + for col in range(COLUMNS - 3): + if (b[row][col] == b[row][col+1] == b[row][col+2] == b[row][col+3] + and b[row][col] != "⚪"): + return True + + # vertikal + for col in range(COLUMNS): + for row in range(ROWS - 3): + if (b[row][col] == b[row+1][col] == b[row+2][col] == b[row+3][col] + and b[row][col] != "⚪"): + return True + + # diagonal rechts unten + for row in range(ROWS - 3): + for col in range(COLUMNS - 3): + if (b[row][col] == b[row+1][col+1] == b[row+2][col+2] == b[row+3][col+3] + and b[row][col] != "⚪"): + return True + + # diagonal rechts oben + for row in range(3, ROWS): + for col in range(COLUMNS - 3): + if (b[row][col] == b[row-1][col+1] == b[row-2][col+2] == b[row-3][col+3] + and b[row][col] != "⚪"): + return True + + return False + + def is_draw(self) -> bool: + """Überprüft, ob das Spiel unentschieden ist""" + return all(cell != "⚪" for row in self.board for cell in row) + + def board_to_str(self) -> str: + """Konvertiert das Board zu einem String""" + numbers = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"] + header = "".join(numbers) + board_rows = "\n".join("".join(row) for row in self.board) + return f"{header}\n{board_rows}" + + async def end_game(self, interaction: discord.Interaction, winner: bool, board_str: str, is_followup: bool = False): + """Beendet das Spiel und zeigt Statistiken""" + self.game_ended = True + + for child in self.children: + child.disabled = True + + msgs = self.messages + game_duration = self.timer.get_game_duration() + avg_move_time = self.timer.get_average_move_time() + + # Update statistics + if winner: + if self.is_ai_mode: + if self.current_player == self.player1: + game_stats.record_win(self.player1.id, vs_ai=True) + else: + game_stats.record_loss(self.player1.id, vs_ai=True) + else: + game_stats.record_win(self.current_player.id) + other_player = self.player2 if self.current_player == self.player1 else self.player1 + game_stats.record_loss(other_player.id) + else: + game_stats.record_draw(self.player1.id) + if not self.is_ai_mode: + game_stats.record_draw(self.player2.id) + + # Build result message + embed = discord.Embed( + title="🎮 4 Gewinnt - Spiel beendet!", + color=discord.Color.gold() if winner else discord.Color.greyple() + ) + + # Ergebnis + if winner: + if self.is_ai_mode and self.current_player == self.player2: + result_text = f"🤖 **Die {self.ai.difficulty_name} KI hat gewonnen!**" + embed.color = discord.Color.red() + else: + result_text = msgs["cog_4gewinnt"]["win_types"]["win"].format( + winner=self.current_player.mention + ) + embed.color = discord.Color.green() + else: + result_text = msgs["cog_4gewinnt"]["win_types"]["draw"] + + embed.add_field( + name="🎯 Ergebnis", + value=result_text, + inline=False + ) + + # Spielstatistiken + avg_time_str = f"{avg_move_time.seconds}s" if avg_move_time else "0s" + embed.add_field( + name="📊 Spielstatistiken", + value=f"⏱️ Spielzeit: {game_duration.seconds // 60}m {game_duration.seconds % 60}s\n" + f"🔢 Züge: {self.move_count}\n" + f"⚡ Ø Zugzeit: {avg_time_str}", + inline=True + ) + + # Sieger-Stats + if winner: + winner_stats = game_stats.get_user_stats(self.current_player.id if not self.is_ai_mode or self.current_player == self.player1 else self.player1.id) + + if self.is_ai_mode: + if self.current_player == self.player1: + stats_text = f"🏆 Siege vs KI: {winner_stats['ai_wins']}\n💔 Niederlagen vs KI: {winner_stats['ai_losses']}\n🔥 Aktuelle Serie: {winner_stats['win_streak']}" + else: + stats_text = f"Die KI bleibt ungeschlagen! 🤖" + else: + stats_text = f"🏆 Siege: {winner_stats['wins']}\n💔 Niederlagen: {winner_stats['losses']}\n🔥 Serie: {winner_stats['win_streak']}" + + embed.add_field( + name="📈 Spieler-Stats", + value=stats_text, + inline=True + ) + + # Spielfeld + embed.add_field( + name="🎲 Endposition", + value=f"```\n{board_str}\n```", + inline=False + ) + + embed.set_footer(text=f"Spiel-ID: {interaction.id} • Schwierigkeit: {self.ai.difficulty_name if self.is_ai_mode else 'PvP'}") + embed.timestamp = datetime.now() + + if is_followup: + await interaction.edit_original_response(embed=embed, view=self) + else: + await interaction.response.edit_message(embed=embed, view=self) + + self.stop() + + async def on_timeout(self): + """Wird aufgerufen wenn das Timeout erreicht wird""" + self.game_ended = True + for child in self.children: + child.disabled = True + +# ─────────────────────────────────────────────── +# >> Cog +# ─────────────────────────────────────────────── +class Connect4Cog(ezcord.Cog, group="fun"): + + @commands.slash_command(name="connect4", description="Starte ein 4 Gewinnt Spiel!") + async def connect4( + self, + ctx: discord.ApplicationContext, + opponent: Optional[discord.Member] = None, + difficulty: discord.Option( + str, + description="KI-Schwierigkeit (nur wenn kein Gegner gewählt)", + choices=["easy", "medium", "hard"], + default="medium", + required=False + ) = "medium" + ): + try: + lang_code = self.bot.get_user_language(ctx.author.id) + except AttributeError: + lang_code = "de" + + try: + messages = load_messages(lang_code) + except FileNotFoundError as e: + print(f"CRITICAL: {e}") + messages = {"cog_4gewinnt": {"error_types": {}, "message": {}, "win_types": {}}} + + # AI mode + if opponent is None: + ai_user = ctx.guild.me + view = Connect4View(ctx.author, ai_user, messages, is_ai_mode=True, difficulty=difficulty) + + difficulty_info = DIFFICULTY_CONFIG.get(difficulty, DIFFICULTY_CONFIG["medium"]) + difficulty_emoji = {"easy": "😊", "medium": "🤔", "hard": "😈"} + + await ctx.respond( + f"🤖 **4 Gewinnt vs KI** {difficulty_emoji.get(difficulty, '🤖')}\n" + f"**Schwierigkeit:** {difficulty_info['name']}\n" + f"{ctx.author.mention} 🔴 spielt gegen die KI! 🟡\n\n" + f"{view.board_to_str()}", + view=view + ) + return + + # PvP mode validations + if opponent.bot: + await ctx.respond( + messages["cog_4gewinnt"]["error_types"]["is_opponent_bot"], + ephemeral=True + ) + return + + if opponent == ctx.author: + await ctx.respond( + messages["cog_4gewinnt"]["error_types"]["is_opponent_self"], + ephemeral=True + ) + return + + view = Connect4View(ctx.author, opponent, messages) + + await ctx.respond( + f"🎮 **4 Gewinnt - PvP Match**\n" + f"{ctx.author.mention} 🔴 vs 🟡 {opponent.mention}\n\n" + f"{view.board_to_str()}", + view=view + ) + + @commands.slash_command(name="connect4stats", description="Zeige deine 4 Gewinnt Statistiken!") + async def stats(self, ctx: discord.ApplicationContext, user: Optional[discord.Member] = None): + target_user = user or ctx.author + stats = game_stats.get_user_stats(target_user.id) + winrate = game_stats.get_winrate(target_user.id) + + embed = discord.Embed( + title=f"📊 4 Gewinnt Statistiken - {target_user.display_name}", + color=discord.Color.blue() + ) + + embed.set_thumbnail(url=target_user.display_avatar.url) + + embed.add_field( + name="🎯 Übersicht", + value=f"**Gesamt:** {stats['total_games']}\n" + f"🏆 Siege: {stats['wins']}\n" + f"💔 Niederlagen: {stats['losses']}\n" + f"🤝 Unentschieden: {stats['draws']}", + inline=True + ) + + embed.add_field( + name="📈 Performance", + value=f"**Siegrate:** {winrate:.1f}%\n" + f"🔥 Aktuelle Serie: {stats['win_streak']}\n" + f"⭐ Beste Serie: {stats['best_streak']}", + inline=True + ) + + # KI-Stats + if stats['ai_wins'] > 0 or stats['ai_losses'] > 0: + ai_total = stats['ai_wins'] + stats['ai_losses'] + ai_winrate = (stats['ai_wins'] / ai_total * 100) if ai_total > 0 else 0 + embed.add_field( + name="🤖 KI-Statistiken", + value=f"🏆 Siege: {stats['ai_wins']}\n" + f"💔 Niederlagen: {stats['ai_losses']}\n" + f"📊 Siegrate: {ai_winrate:.1f}%", + inline=True + ) + + embed.set_footer(text=f"Abgefragt von {ctx.author.display_name}") + embed.timestamp = datetime.now() + + await ctx.respond(embed=embed) + +def setup(bot): + bot.add_cog(Connect4Cog(bot)) \ No newline at end of file diff --git a/src/bot/cogs/fun/tictactoe.py b/src/bot/cogs/fun/tictactoe.py new file mode 100644 index 0000000..8d77f7f --- /dev/null +++ b/src/bot/cogs/fun/tictactoe.py @@ -0,0 +1,602 @@ +# Copyright (c) 2025 OPPRO.NET Network +# ─────────────────────────────────────────────── +# >> Import +# ─────────────────────────────────────────────── +from discord.ui import Button, View +import discord +from discord.ext import commands +import ezcord +import yaml +from pathlib import Path +from typing import Optional, List, Tuple +import asyncio +import random + +# ─────────────────────────────────────────────── +# >> Constants +# ─────────────────────────────────────────────── +DEFAULT_TIMEOUT = 120 + +DIFFICULTY_CONFIG = { + "easy": { + "name": "Anfänger", + "randomness": 0.5 # 50% zufällige Züge + }, + "medium": { + "name": "Fortgeschritten", + "randomness": 0.2 # 20% zufällige Züge + }, + "hard": { + "name": "Experte", + "randomness": 0.0 # Perfektes Spiel + } +} + +# ─────────────────────────────────────────────── +# >> Statistics Manager +# ─────────────────────────────────────────────── +class GameStats: + """Verwaltet Spielstatistiken für TicTacToe""" + + def __init__(self): + self.stats = {} + + def get_user_stats(self, user_id: int) -> dict: + if user_id not in self.stats: + self.stats[user_id] = { + "wins": 0, + "losses": 0, + "draws": 0, + "total_games": 0, + "win_streak": 0, + "best_streak": 0, + "ai_wins": 0, + "ai_losses": 0 + } + return self.stats[user_id] + + def record_win(self, user_id: int, vs_ai: bool = False): + stats = self.get_user_stats(user_id) + stats["wins"] += 1 + stats["total_games"] += 1 + stats["win_streak"] += 1 + stats["best_streak"] = max(stats["best_streak"], stats["win_streak"]) + if vs_ai: + stats["ai_wins"] += 1 + + def record_loss(self, user_id: int, vs_ai: bool = False): + stats = self.get_user_stats(user_id) + stats["losses"] += 1 + stats["total_games"] += 1 + stats["win_streak"] = 0 + if vs_ai: + stats["ai_losses"] += 1 + + def record_draw(self, user_id: int): + stats = self.get_user_stats(user_id) + stats["draws"] += 1 + stats["total_games"] += 1 + stats["win_streak"] = 0 + + def get_winrate(self, user_id: int) -> float: + stats = self.get_user_stats(user_id) + if stats["total_games"] == 0: + return 0.0 + return (stats["wins"] / stats["total_games"]) * 100 + +# Global stats instance +game_stats = GameStats() + +# ─────────────────────────────────────────────── +# >> Load messages from YAML +# ─────────────────────────────────────────────── +def load_messages(lang_code: str): + """ + Lädt Nachrichten für den angegebenen Sprachcode. + Fällt auf 'en' und dann auf 'de' zurück, falls die Datei fehlt. + """ + base_path = Path("translation") / "messages" + + lang_file = base_path / f"{lang_code}.yaml" + if not lang_file.exists(): + lang_file = base_path / "en.yaml" + if not lang_file.exists(): + lang_file = base_path / "de.yaml" + + if not lang_file.exists(): + print(f"WARNUNG: Keine Sprachdatei für '{lang_code}' gefunden. Verwende leere Texte.") + return {} + + with open(lang_file, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + +# ─────────────────────────────────────────────── +# >> AI Engine (Minimax Algorithm) +# ─────────────────────────────────────────────── +class TicTacToeAI: + """KI-Gegner mit Minimax-Algorithmus für TicTacToe""" + + def __init__(self, difficulty: str = "medium"): + config = DIFFICULTY_CONFIG.get(difficulty, DIFFICULTY_CONFIG["medium"]) + self.randomness = config["randomness"] + self.difficulty_name = config["name"] + + def get_available_moves(self, board: List[List[str]]) -> List[Tuple[int, int]]: + """Gibt alle verfügbaren Züge zurück""" + moves = [] + for i in range(3): + for j in range(3): + if board[i][j] == "": + moves.append((i, j)) + return moves + + def check_winner(self, board: List[List[str]]) -> Optional[str]: + """Prüft ob es einen Gewinner gibt""" + # Horizontal + for i in range(3): + if board[i][0] == board[i][1] == board[i][2] != "": + return board[i][0] + + # Vertikal + for i in range(3): + if board[0][i] == board[1][i] == board[2][i] != "": + return board[0][i] + + # Diagonal + if board[0][0] == board[1][1] == board[2][2] != "": + return board[0][0] + if board[0][2] == board[1][1] == board[2][0] != "": + return board[0][2] + + return None + + def is_board_full(self, board: List[List[str]]) -> bool: + """Prüft ob das Board voll ist""" + return all(cell != "" for row in board for cell in row) + + def minimax(self, board: List[List[str]], depth: int, is_maximizing: bool, + ai_symbol: str, player_symbol: str) -> int: + """Minimax-Algorithmus für optimale Züge""" + winner = self.check_winner(board) + + # Terminal-Zustände + if winner == ai_symbol: + return 10 - depth # Schnellerer Gewinn ist besser + elif winner == player_symbol: + return depth - 10 # Schnellerer Verlust ist schlechter + elif self.is_board_full(board): + return 0 # Unentschieden + + if is_maximizing: + best_score = float('-inf') + for i, j in self.get_available_moves(board): + board[i][j] = ai_symbol + score = self.minimax(board, depth + 1, False, ai_symbol, player_symbol) + board[i][j] = "" + best_score = max(score, best_score) + return best_score + else: + best_score = float('inf') + for i, j in self.get_available_moves(board): + board[i][j] = player_symbol + score = self.minimax(board, depth + 1, True, ai_symbol, player_symbol) + board[i][j] = "" + best_score = min(score, best_score) + return best_score + + def get_best_move(self, board: List[List[str]], ai_symbol: str, player_symbol: str) -> Tuple[int, int]: + """Gibt den besten Zug zurück""" + available_moves = self.get_available_moves(board) + + if not available_moves: + return (0, 0) + + # Zufälligkeit für niedrigere Schwierigkeitsgrade + if random.random() < self.randomness: + return random.choice(available_moves) + + # Prüfe auf Gewinnzug + for i, j in available_moves: + board[i][j] = ai_symbol + if self.check_winner(board) == ai_symbol: + board[i][j] = "" + return (i, j) + board[i][j] = "" + + # Prüfe ob Gegner blockiert werden muss + for i, j in available_moves: + board[i][j] = player_symbol + if self.check_winner(board) == player_symbol: + board[i][j] = "" + return (i, j) + board[i][j] = "" + + # Verwende Minimax für optimalen Zug + best_score = float('-inf') + best_move = available_moves[0] + + for i, j in available_moves: + board[i][j] = ai_symbol + score = self.minimax(board, 0, False, ai_symbol, player_symbol) + board[i][j] = "" + + if score > best_score: + best_score = score + best_move = (i, j) + + return best_move + +# ─────────────────────────────────────────────── +# >> Enhanced Button & View +# ─────────────────────────────────────────────── +class TicTacToeButton(Button): + def __init__(self, x, y): + super().__init__(style=discord.ButtonStyle.secondary, label="\u200b", row=x) + self.x = x + self.y = y + self.clicked = False + + async def callback(self, interaction: discord.Interaction): + view: TicTacToeView = self.view + messages = view.messages + + # Prüfe ob Spiel bereits beendet + if view.game_ended: + await interaction.response.send_message( + "Das Spiel ist bereits beendet!", + ephemeral=True + ) + return + + # PvP mode checks + if not view.is_ai_mode and interaction.user != view.current_player: + await interaction.response.send_message( + messages.get("cog_tictactoe", {}).get("error_types", {}).get("not_your_turn", "Not your turn!"), + ephemeral=True + ) + return + + # AI mode checks + if view.is_ai_mode and interaction.user != view.player1: + await interaction.response.send_message( + messages.get("cog_tictactoe", {}).get("error_types", {}).get("not_your_turn", "Not your turn!"), + ephemeral=True + ) + return + + if self.clicked: + await interaction.response.send_message( + messages.get("cog_tictactoe", {}).get("error_types", {}).get("this_cell_taken", "This cell is already taken!"), + ephemeral=True + ) + return + + # Spieler-Zug + self.clicked = True + if view.current_turn == 0: + self.style = discord.ButtonStyle.danger # rot = X + self.label = "X" + view.board[self.x][self.y] = "X" + view.current_turn = 1 + view.current_player = view.player2 + else: + self.style = discord.ButtonStyle.success # grün = O + self.label = "O" + view.board[self.x][self.y] = "O" + view.current_turn = 0 + view.current_player = view.player1 + + winner = view.check_winner() + + if winner: + await view.end_game(interaction, winner) + return + + elif view.is_draw(): + await view.end_game(interaction, None) + return + + # AI-Zug + if view.is_ai_mode and view.current_player == view.player2: + next_turn_msg = messages.get("cog_tictactoe", {}).get("message", {}).get("ai_thinking", "🤖 KI denkt nach...").format( + player=view.current_player.mention + ) + await interaction.response.edit_message(content=next_turn_msg, view=view) + + # Simuliere Denkzeit + await asyncio.sleep(0.8) + + # KI macht Zug + ai_move = view.ai.get_best_move(view.board, "O", "X") + if ai_move: + ai_x, ai_y = ai_move + for child in view.children: + if isinstance(child, TicTacToeButton) and child.x == ai_x and child.y == ai_y: + child.clicked = True + child.style = discord.ButtonStyle.success + child.label = "O" + view.board[ai_x][ai_y] = "O" + view.current_turn = 0 + view.current_player = view.player1 + break + + winner = view.check_winner() + + if winner: + await view.end_game(interaction, winner, is_followup=True) + return + + elif view.is_draw(): + await view.end_game(interaction, None, is_followup=True) + return + + # Zeige KI-Zug an + next_turn_msg = messages.get("cog_tictactoe", {}).get("message", {}).get("ai_moved", "✅ KI hat Feld ({x}, {y}) gewählt!\n\n{player}, du bist dran!").format( + x=ai_x + 1, + y=ai_y + 1, + player=view.current_player.mention + ) + await interaction.edit_original_response(content=next_turn_msg, view=view) + else: + next_turn_msg = messages.get("cog_tictactoe", {}).get("message", {}).get("next_turn", "It is now {player}'s turn!").format( + player=view.current_player.mention + ) + await interaction.response.edit_message(content=next_turn_msg, view=view) + +class TicTacToeView(View): + def __init__(self, player1, player2, messages, is_ai_mode=False, difficulty="medium"): + super().__init__(timeout=DEFAULT_TIMEOUT) + self.player1 = player1 + self.player2 = player2 + self.current_player = player1 + self.current_turn = 0 # 0 = X (player1), 1 = O (player2) + self.board = [["" for _ in range(3)] for _ in range(3)] + self.messages = messages + self.is_ai_mode = is_ai_mode + self.difficulty = difficulty + self.ai = TicTacToeAI(difficulty) if is_ai_mode else None + self.game_ended = False + + for x in range(3): + for y in range(3): + self.add_item(TicTacToeButton(x, y)) + + def check_winner(self): + """Prüft auf Gewinner""" + b = self.board + players_map = {"X": self.player1, "O": self.player2} + + # Horizontal + for i in range(3): + if b[i][0] == b[i][1] == b[i][2] != "": + winner_symbol = b[i][0] + return f"{winner_symbol} ({players_map[winner_symbol].display_name})" + + # Vertikal + for i in range(3): + if b[0][i] == b[1][i] == b[2][i] != "": + winner_symbol = b[0][i] + return f"{winner_symbol} ({players_map[winner_symbol].display_name})" + + # Diagonal + if b[0][0] == b[1][1] == b[2][2] != "": + winner_symbol = b[0][0] + return f"{winner_symbol} ({players_map[winner_symbol].display_name})" + if b[0][2] == b[1][1] == b[2][0] != "": + winner_symbol = b[0][2] + return f"{winner_symbol} ({players_map[winner_symbol].display_name})" + + return None + + def is_draw(self): + """Prüft auf Unentschieden""" + return all(cell != "" for row in self.board for cell in row) + + async def end_game(self, interaction: discord.Interaction, winner: Optional[str], is_followup: bool = False): + """Beendet das Spiel und zeigt Statistiken""" + self.game_ended = True + + for child in self.children: + child.disabled = True + + messages = self.messages + + # Update Statistiken + if winner: + winner_symbol = winner[0] # "X" oder "O" + winner_player = self.player1 if winner_symbol == "X" else self.player2 + loser_player = self.player2 if winner_symbol == "X" else self.player1 + + if self.is_ai_mode: + if winner_player == self.player1: + game_stats.record_win(self.player1.id, vs_ai=True) + else: + game_stats.record_loss(self.player1.id, vs_ai=True) + else: + game_stats.record_win(winner_player.id) + game_stats.record_loss(loser_player.id) + else: + game_stats.record_draw(self.player1.id) + if not self.is_ai_mode: + game_stats.record_draw(self.player2.id) + + # Erstelle Embed + embed = discord.Embed( + title="🎮 Tic Tac Toe - Spiel beendet!", + color=discord.Color.green() if winner else discord.Color.gold() + ) + + # Ergebnis + if winner: + if self.is_ai_mode and winner[0] == "O": + result_text = f"🤖 **Die {self.ai.difficulty_name} KI hat gewonnen!**" + embed.color = discord.Color.red() + else: + result_text = messages.get("cog_tictactoe", {}).get("win_types", {}).get("win", "WINNER: {winner}").format(winner=winner) + else: + result_text = messages.get("cog_tictactoe", {}).get("win_types", {}).get("draw", "It's a draw!") + + embed.add_field( + name="🎯 Ergebnis", + value=result_text, + inline=False + ) + + # Statistiken + if winner: + winner_player = self.player1 if winner[0] == "X" else self.player2 + if not self.is_ai_mode or winner_player == self.player1: + stats = game_stats.get_user_stats(winner_player.id) + + if self.is_ai_mode: + stats_text = f"🏆 Siege vs KI: {stats['ai_wins']}\n💔 Niederlagen vs KI: {stats['ai_losses']}\n🔥 Serie: {stats['win_streak']}" + else: + stats_text = f"🏆 Siege: {stats['wins']}\n💔 Niederlagen: {stats['losses']}\n🔥 Serie: {stats['win_streak']}" + + embed.add_field( + name="📈 Spieler-Stats", + value=stats_text, + inline=True + ) + + # Spielfeld anzeigen + board_display = "" + for row in self.board: + board_display += " | ".join([cell if cell else "·" for cell in row]) + "\n" + + embed.add_field( + name="🎲 Endposition", + value=f"```\n{board_display}```", + inline=False + ) + + embed.set_footer(text=f"Schwierigkeit: {self.ai.difficulty_name if self.is_ai_mode else 'PvP'}") + + if is_followup: + await interaction.edit_original_response(embed=embed, view=self) + else: + await interaction.response.edit_message(embed=embed, view=self) + + self.stop() + + async def on_timeout(self): + """Wird aufgerufen wenn das Timeout erreicht wird""" + self.game_ended = True + for child in self.children: + child.disabled = True + +# ─────────────────────────────────────────────── +# >> Cog +# ─────────────────────────────────────────────── +class fun(ezcord.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.slash_command(name="tictactoe", description="Starte ein Tic Tac Toe Spiel!") + async def tictactoe( + self, + ctx: discord.ApplicationContext, + opponent: Optional[discord.Member] = None, + difficulty: discord.Option( + str, + description="KI-Schwierigkeit (nur wenn kein Gegner gewählt)", + choices=["easy", "medium", "hard"], + default="medium", + required=False + ) = "medium" + ): + try: + lang_code = self.bot.settings_db.get_user_language(ctx.author.id) + except: + lang_code = "de" + + messages = load_messages(lang_code) + + # AI mode + if opponent is None: + ai_user = ctx.guild.me + view = TicTacToeView(ctx.author, ai_user, messages, is_ai_mode=True, difficulty=difficulty) + + difficulty_info = DIFFICULTY_CONFIG.get(difficulty, DIFFICULTY_CONFIG["medium"]) + difficulty_emoji = {"easy": "😊", "medium": "🤔", "hard": "😈"} + + await ctx.respond( + f"🤖 **Tic Tac Toe vs KI** {difficulty_emoji.get(difficulty, '🤖')}\n" + f"**Schwierigkeit:** {difficulty_info['name']}\n" + f"{ctx.author.mention} (X) spielt gegen die KI! (O)\n\n" + f"Du bist dran!", + view=view + ) + return + + # PvP mode validations + if opponent.bot: + await ctx.respond( + messages.get("cog_tictactoe", {}).get("error_types", {}).get("is_opponent_bot", "You cannot challenge a bot."), + ephemeral=True + ) + return + + if opponent == ctx.author: + await ctx.respond( + messages.get("cog_tictactoe", {}).get("error_types", {}).get("is_opponent_self", "You cannot challenge yourself."), + ephemeral=True + ) + return + + view = TicTacToeView(ctx.author, opponent, messages) + + start_msg = messages.get("cog_tictactoe", {}).get("message", {}).get("start_game", "Tic Tac Toe: {author_mention} vs {opponent_mention}").format( + author_mention=ctx.author.mention, + opponent_mention=opponent.mention + ) + await ctx.respond(start_msg, view=view) + + @commands.slash_command(name="tictactoestats", description="Zeige deine Tic Tac Toe Statistiken!") + async def stats(self, ctx: discord.ApplicationContext, user: Optional[discord.Member] = None): + target_user = user or ctx.author + stats = game_stats.get_user_stats(target_user.id) + winrate = game_stats.get_winrate(target_user.id) + + embed = discord.Embed( + title=f"📊 Tic Tac Toe Statistiken - {target_user.display_name}", + color=discord.Color.blue() + ) + + embed.set_thumbnail(url=target_user.display_avatar.url) + + embed.add_field( + name="🎯 Übersicht", + value=f"**Gesamt:** {stats['total_games']}\n" + f"🏆 Siege: {stats['wins']}\n" + f"💔 Niederlagen: {stats['losses']}\n" + f"🤝 Unentschieden: {stats['draws']}", + inline=True + ) + + embed.add_field( + name="📈 Performance", + value=f"**Siegrate:** {winrate:.1f}%\n" + f"🔥 Aktuelle Serie: {stats['win_streak']}\n" + f"⭐ Beste Serie: {stats['best_streak']}", + inline=True + ) + + # KI-Stats + if stats['ai_wins'] > 0 or stats['ai_losses'] > 0: + ai_total = stats['ai_wins'] + stats['ai_losses'] + ai_winrate = (stats['ai_wins'] / ai_total * 100) if ai_total > 0 else 0 + embed.add_field( + name="🤖 KI-Statistiken", + value=f"🏆 Siege: {stats['ai_wins']}\n" + f"💔 Niederlagen: {stats['ai_losses']}\n" + f"📊 Siegrate: {ai_winrate:.1f}%", + inline=True + ) + + embed.set_footer(text=f"Abgefragt von {ctx.author.display_name}") + + await ctx.respond(embed=embed) + +def setup(bot): + bot.add_cog(fun(bot)) \ No newline at end of file diff --git a/src/bot/core/__init__.py b/src/bot/core/__init__.py new file mode 100644 index 0000000..a269625 --- /dev/null +++ b/src/bot/core/__init__.py @@ -0,0 +1,26 @@ +""" +ManagerX Core Module +==================== + +Zentrale Module für Bot-Initialisierung und -Verwaltung +""" + +from .config import ConfigLoader, BotConfig +from .bot_setup import BotSetup +from .cog_manager import CogManager +from .database import DatabaseManager +from .dashboard import DashboardTask +from .utils import print_logo, format_uptime, truncate_text +from .groups import * + +__all__ = [ + 'ConfigLoader', + 'BotConfig', + 'BotSetup', + 'CogManager', + 'DatabaseManager', + 'DashboardTask', + 'print_logo', + 'format_uptime', + 'truncate_text' +] \ No newline at end of file diff --git a/src/bot/core/bot_setup.py b/src/bot/core/bot_setup.py new file mode 100644 index 0000000..9c81795 --- /dev/null +++ b/src/bot/core/bot_setup.py @@ -0,0 +1,76 @@ +""" +ManagerX - Bot Setup +==================== + +Initialisiert und konfiguriert die Discord Bot-Instanz +Pfad: src/bot/core/bot_setup.py +""" + +import discord +import ezcord + +class BotSetup: + """Verwaltet die Bot-Initialisierung""" + + def __init__(self, config: dict): + self.config = config + + def create_bot(self) -> ezcord.Bot: + """ + Erstellt und konfiguriert die Bot-Instanz. + + Returns: + ezcord.Bot: Konfigurierte Bot-Instanz + """ + # Intents konfigurieren + intents = discord.Intents.default() + intents.members = True + intents.message_content = True + + # Bot erstellen + bot = ezcord.Bot( + intents=intents, + language="de" + ) + + # Bot-Konfiguration anhängen + bot.config = self._build_bot_config() + + return bot + + def _build_bot_config(self) -> dict: + """ + Erstellt die Bot-Config aus der geladenen Konfiguration. + + Returns: + dict: Bot-Konfiguration für Runtime + """ + ui = self.config.get('ui', {}) + behavior = self.config.get('bot_behavior', {}) + security = self.config.get('security', {}) + performance = self.config.get('performance', {}) + + return { + # UI Settings + 'embed_color': ui.get('embed_color', '#00ff00'), + 'footer_text': ui.get('footer_text', 'ManagerX Bot'), + 'theme': ui.get('theme', 'dark'), + 'show_timestamps': ui.get('show_timestamps', True), + + # Behavior + 'maintenance_mode': behavior.get('maintenance_mode', False), + 'global_cooldown': behavior.get('global_cooldown_seconds', 5), + 'max_messages_per_minute': behavior.get('max_messages_per_minute', 10), + + # Security + 'required_permissions': security.get('required_permissions', []), + 'blacklist_servers': security.get('blacklist_servers', []), + 'whitelist_users': security.get('whitelist_users', []), + 'enable_command_logging': security.get('enable_command_logging', True), + + # Performance + 'max_concurrent_tasks': performance.get('max_concurrent_tasks', 10), + 'task_timeout': performance.get('task_timeout_seconds', 30), + 'memory_limit': performance.get('memory_limit_mb', 512), + 'enable_gc_optimization': performance.get('enable_gc_optimization', True) + } \ No newline at end of file diff --git a/src/bot/core/cog_manager.py b/src/bot/core/cog_manager.py new file mode 100644 index 0000000..6d5932c --- /dev/null +++ b/src/bot/core/cog_manager.py @@ -0,0 +1,116 @@ +""" +ManagerX - Cog Manager +====================== + +Verwaltet das Laden und Deaktivieren von Cogs +Pfad: src/bot/core/cog_manager.py +""" + +from logger import logger, Category + +class CogManager: + """Verwaltet Cog-Loading und Ignore-Liste""" + + # Hilfs-/Utility-Dateien, die keine Cogs sind + UTILITY_FILES = [ + "autocomplete", + "cache", + "components", + "config", + "containers", + "utils", + "backend", + "emojis" + ] + + # Mapping: Config-Key -> Dateiname + COG_MAPPING = { + 'fun': { + 'gewinnt': 'gewinnt', + 'tictactoe': 'tictactoe', + 'weather': 'weather', + 'wikipedia': 'cog' + }, + 'information': { + 'botstatus': 'botstatus', + 'serverinfo': 'serverinfo', + 'usermanagemt': 'usermanagemt' + }, + 'moderation': { + 'antispam': 'antispam', + 'moderation': 'moderation', + 'notes': 'notes', + 'warningsystem': 'warningsystem' + }, + 'server_management': { + 'autodelete': 'autodelete', + 'globalchat': 'globalchat', + 'levelsystem': 'levelsystem', + 'logging': 'logging', + 'stats': 'stats', + 'tempvc': 'tempvc', + 'welcome': 'welcome' + }, + 'other': { + 'setlang': 'setlang' + } + } + + def __init__(self, cogs_config: dict): + self.cogs_config = cogs_config + + def get_ignored_cogs(self) -> list: + """ + Erstellt Liste von zu ignorierenden Cogs basierend auf config.yaml. + + Returns: + list: Dateinamen (ohne .py) der zu ignorierenden Cogs + """ + ignored = self.UTILITY_FILES.copy() + + # Deaktivierte Cogs hinzufügen + for category, cogs in self.COG_MAPPING.items(): + category_config = self.cogs_config.get(category, {}) + + for cog_key, file_name in cogs.items(): + if not category_config.get(cog_key, True): + ignored.append(file_name) + logger.info(Category.BOT, f"Cog '{file_name}' deaktiviert (config.yaml)") + + return ignored + + def is_cog_enabled(self, category: str, cog_name: str) -> bool: + """ + Prüft ob ein bestimmter Cog aktiviert ist. + + Args: + category: Kategorie des Cogs (z.B. 'fun', 'moderation') + cog_name: Name des Cogs + + Returns: + bool: True wenn aktiviert, sonst False + """ + category_config = self.cogs_config.get(category, {}) + return category_config.get(cog_name, True) + + def get_enabled_cogs(self) -> dict: + """ + Gibt alle aktivierten Cogs nach Kategorie zurück. + + Returns: + dict: Dictionary mit Kategorien und aktivierten Cogs + """ + enabled = {} + + for category, cogs in self.COG_MAPPING.items(): + category_config = self.cogs_config.get(category, {}) + enabled_in_category = [] + + for cog_key, file_name in cogs.items(): + if category_config.get(cog_key, True): + enabled_in_category.append(file_name) + + if enabled_in_category: + enabled[category] = enabled_in_category + + return enabled \ No newline at end of file diff --git a/src/bot/core/config.py b/src/bot/core/config.py new file mode 100644 index 0000000..1240754 --- /dev/null +++ b/src/bot/core/config.py @@ -0,0 +1,74 @@ +""" +ManagerX - Configuration Loader +================================ + +Lädt und verwaltet die Bot-Konfiguration aus config.yaml +""" + +import os +import sys +import yaml +from pathlib import Path +from colorama import Fore, Style +from dotenv import load_dotenv +base_path = Path(__file__).resolve().parent.parent.parent.parent +env_path = base_path / "config" / ".env" + +# Lade die .env Datei +load_dotenv(dotenv_path=env_path) + +class BotConfig: + """Zentrale Konfigurationsklasse""" + TOKEN = os.getenv("TOKEN") + VERSION = "2.0.0" + +class ConfigLoader: + """Lädt die Bot-Konfiguration aus config.yaml""" + + def __init__(self, basedir: Path): + self.basedir = basedir + self.config_path = basedir / 'config' / 'config.yaml' + + def load(self) -> dict: + """ + Lädt die Konfigurationsdatei und gibt alle Einstellungen zurück. + + Returns: + dict: Vollständige Konfiguration + + Raises: + SystemExit: Bei kritischen Fehlern + """ + try: + with open(self.config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + # Bot deaktiviert? + if not config.get('enabled', True): + print(f"[{Fore.YELLOW}INFO{Style.RESET_ALL}] Bot ist in config.yaml deaktiviert. Beende...") + sys.exit(0) + + # Version übernehmen + BotConfig.VERSION = config.get('version', '2.0.0') + + # Strukturierte Rückgabe + return { + 'enabled': config.get('enabled', True), + 'version': BotConfig.VERSION, + 'features': config.get('features', {}), + 'bot_behavior': config.get('bot_behavior', {}), + 'ui': config.get('ui', {}), + 'security': config.get('security', {}), + 'performance': config.get('performance', {}), + 'cogs': config.get('features', {}).get('cogs', {}) + } + + except FileNotFoundError: + print(f"[{Fore.RED}ERROR{Style.RESET_ALL}] config.yaml nicht gefunden: {self.config_path}") + sys.exit(1) + except yaml.YAMLError as e: + print(f"[{Fore.RED}ERROR{Style.RESET_ALL}] YAML-Parsing-Fehler: {e}") + sys.exit(1) + except Exception as e: + print(f"[{Fore.RED}ERROR{Style.RESET_ALL}] Fehler beim Laden der config.yaml: {e}") + sys.exit(1) \ No newline at end of file diff --git a/src/bot/core/dashboard.py b/src/bot/core/dashboard.py new file mode 100644 index 0000000..e39f610 --- /dev/null +++ b/src/bot/core/dashboard.py @@ -0,0 +1,101 @@ +""" +ManagerX - Dashboard Task +========================== + +Verwaltet das Dashboard-Update-System +Pfad: src/bot/core/dashboard.py +""" + +import json +from datetime import datetime +from pathlib import Path +from discord.ext import tasks +from logger import logger, Category + +class DashboardTask: + """Verwaltet periodische Dashboard-Updates""" + + def __init__(self, bot, basedir: Path): + self.bot = bot + self.basedir = basedir + self.stats_file = basedir / 'bot_stats.json' + self._task = None + + # Task definieren + @tasks.loop(minutes=1) + async def update_dashboard(): + await self._update_stats() + + self._task = update_dashboard + + async def _update_stats(self): + """Aktualisiert die Dashboard-Statistiken""" + try: + # Basis-Statistiken sammeln + stats = { + "bot_info": { + "name": str(self.bot.user.name) if self.bot.user else "Unknown", + "id": str(self.bot.user.id) if self.bot.user else "0", + "status": "online", + "latency": round(self.bot.latency * 1000, 1) + }, + "stats": { + "server_count": len(self.bot.guilds), + "user_count": sum(g.member_count for g in self.bot.guilds if g.member_count), + "shards": self.bot.shard_count or 1, + "commands": len(self.bot.tree.get_commands()) if hasattr(self.bot, 'tree') else 0 + }, + "system": { + "uptime": self._get_uptime(), + "python_version": self._get_python_version() + }, + "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + + # In Datei schreiben + with open(self.stats_file, 'w', encoding='utf-8') as f: + json.dump(stats, f, indent=4, ensure_ascii=False) + + except Exception as e: + logger.error(Category.BOT, f"Dashboard-Update fehlgeschlagen: {e}") + + def _get_uptime(self) -> str: + """Berechnet die Bot-Uptime""" + if hasattr(self.bot, 'start_time'): + delta = datetime.now() - self.bot.start_time + hours, remainder = divmod(int(delta.total_seconds()), 3600) + minutes, seconds = divmod(remainder, 60) + return f"{hours}h {minutes}m {seconds}s" + return "Unknown" + + def _get_python_version(self) -> str: + """Gibt die Python-Version zurück""" + import sys + return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + + def register(self): + """Registriert den Task (startet ihn noch nicht)""" + # Startzeit speichern + self.bot.start_time = datetime.now() + logger.info(Category.DISCORD_BOT, "Dashboard-Task registriert") + + def start(self): + """Startet den Dashboard-Update-Task""" + if self._task and not self._task.is_running(): + self._task.start() + logger.success(Category.DISCORD_BOT, "Dashboard-Task gestartet") + + def stop(self): + """Stoppt den Dashboard-Update-Task""" + if self._task and self._task.is_running(): + self._task.cancel() + logger.info(Category.DISCORD_BOT, "Dashboard-Task gestoppt") + + def is_running(self) -> bool: + """ + Prüft ob der Task läuft. + + Returns: + bool: True wenn Task läuft + """ + return self._task.is_running() if self._task else False \ No newline at end of file diff --git a/src/bot/core/database.py b/src/bot/core/database.py new file mode 100644 index 0000000..397739a --- /dev/null +++ b/src/bot/core/database.py @@ -0,0 +1,74 @@ +""" +ManagerX - Database Manager +============================ + +Verwaltet Datenbankverbindungen und Initialisierung +Pfad: src/bot/core/database.py +""" + +from logger import logger, Category + +try: + from DevTools import SettingsDB +except ImportError as e: + logger.critical(Category.DATABASE, f"SettingsDB Import fehlgeschlagen: {e}") + SettingsDB = None + +class DatabaseManager: + """Verwaltet die Datenbank-Initialisierung""" + + def __init__(self): + self.db = None + + def initialize(self, bot) -> bool: + """ + Initialisiert die Datenbank und hängt sie an den Bot an. + + Args: + bot: Bot-Instanz + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if SettingsDB is None: + logger.critical(Category.DATABASE, "SettingsDB nicht verfügbar!") + return False + + try: + self.db = SettingsDB() + bot.settings_db = self.db + logger.success(Category.DATABASE, "Settings Database initialized ✓") + return True + + except Exception as e: + logger.critical(Category.DATABASE, f"Datenbankfehler: {e}") + return False + + def get_database(self): + """ + Gibt die Datenbankinstanz zurück. + + Returns: + SettingsDB: Datenbankinstanz oder None + """ + return self.db + + def close(self): + """Schließt die Datenbankverbindung""" + if self.db: + try: + # Falls SettingsDB eine close()-Methode hat + if hasattr(self.db, 'close'): + self.db.close() + logger.info(Category.DATABASE, "Datenbankverbindung geschlossen") + except Exception as e: + logger.error(Category.DATABASE, f"Fehler beim Schließen der DB: {e}") + + def is_connected(self) -> bool: + """ + Prüft ob die Datenbank verbunden ist. + + Returns: + bool: True wenn verbunden, sonst False + """ + return self.db is not None \ No newline at end of file diff --git a/src/bot/core/utils.py b/src/bot/core/utils.py new file mode 100644 index 0000000..c7a7d72 --- /dev/null +++ b/src/bot/core/utils.py @@ -0,0 +1,73 @@ +""" +ManagerX - Utility Functions +============================= + +Hilfsfunktionen für den Bot +""" + +from colorama import Fore, Style +from .config import BotConfig + +def print_logo(): + """Gibt das ManagerX ASCII-Logo in der Konsole aus""" + logo_lines = [ + r" _____ ______ ________ ________ ________ ________ _______ ________ ___ ___ ", + r"|\ _ \ _ \|\ __ \|\ ___ \|\ __ \|\ ____\|\ ___ \ |\ __ \ |\ \ / /|", + r"\ \ \\\__\ \ \ \ \|\ \ \ \\ \ \ \ \|\ \ \ \___|\ \ __/|\ \ \|\ \ \ \ \/ / /", + r" \ \ \\|__| \ \ \ __ \ \ \\ \ \ \ __ \ \ \ __\ \ _|/_\ \ _ _\ \ \ / / ", + r" \ \ \ \ \ \ \ \ \ \ \ \\ \ \ \ \ \ \ \ \|\ \ \ \_|\ \ \ \\ \| / \/ ", + r" \ \__\ \ \__\ \__\ \__\ \__\\ \__\ \__\ \__\ \_______\ \_______\ \__\\ _\ / /\ \ ", + r" \|__| \|__|\|__|\|__|\|__| \|__|\|__|\|__|\|_______|\|_______|\|__|\|__|/__/ /\ __\ ", + r" |__|/ \|__| " + ] + + print(Fore.CYAN) + for line in logo_lines: + print(line) + print(f"{'=' * 91}") + print(f" ManagerX Discord Bot v{BotConfig.VERSION}") + print(f"{'=' * 91}{Style.RESET_ALL}\n") + + +def format_uptime(seconds: int) -> str: + """ + Formatiert Sekunden in lesbare Uptime. + + Args: + seconds: Anzahl Sekunden + + Returns: + str: Formatierte Uptime (z.B. "2d 5h 30m") + """ + days, remainder = divmod(seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + + parts = [] + if days > 0: + parts.append(f"{int(days)}d") + if hours > 0: + parts.append(f"{int(hours)}h") + if minutes > 0: + parts.append(f"{int(minutes)}m") + if seconds > 0 or not parts: + parts.append(f"{int(seconds)}s") + + return " ".join(parts) + + +def truncate_text(text: str, max_length: int = 100, suffix: str = "...") -> str: + """ + Kürzt Text auf maximale Länge. + + Args: + text: Zu kürzender Text + max_length: Maximale Länge + suffix: Suffix bei gekürztem Text + + Returns: + str: Gekürzter Text + """ + if len(text) <= max_length: + return text + return text[:max_length - len(suffix)] + suffix \ No newline at end of file diff --git a/src/cogs/Servermanament/autodelete.py b/src/cogs/Servermanament/autodelete.py deleted file mode 100644 index 9ba365c..0000000 --- a/src/cogs/Servermanament/autodelete.py +++ /dev/null @@ -1,309 +0,0 @@ -from DevTools import AutoDeleteDB -import discord -from discord.ext import tasks -from discord.commands import SlashCommandGroup, Option -import ezcord -import asyncio -from datetime import datetime, timedelta -import logging - -logger = logging.getLogger(__name__) - - -class AutoDelete(ezcord.Cog): - def __init__(self, bot): - self.bot = bot - self.delete_task.start() - self.processing_channels = set() # Verhindert doppelte Verarbeitung - - autodelete = SlashCommandGroup("autodelete", "Automatische Nachrichtenlöschung") - - @autodelete.command(name="setup", description="Richtet AutoDelete für einen Kanal ein.") - async def setup(self, ctx, - channel: Option(discord.TextChannel, "Kanal", required=True), - duration: Option(int, "Zeit in Sekunden (min: 60, max: 604800)", required=True), - exclude_pinned: Option(bool, "Angepinnte Nachrichten ausschließen", default=True), - exclude_bots: Option(bool, "Bot-Nachrichten ausschließen", default=False)): - - # Validierung - if duration < 60: - await ctx.respond("❌ Mindestdauer ist 60 Sekunden (1 Minute).", ephemeral=True) - return - if duration > 604800: - await ctx.respond("❌ Maximaldauer ist 604800 Sekunden (7 Tage).", ephemeral=True) - return - - # Permissions prüfen - if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.respond("❌ Ich habe keine Berechtigung, Nachrichten in diesem Kanal zu löschen.", ephemeral=True) - return - - db = AutoDeleteDB() - db.add_autodelete(channel.id, duration, exclude_pinned, exclude_bots) - - duration_str = self._format_duration(duration) - await ctx.respond( - f"✅ AutoDelete für {channel.mention} wurde aktiviert!\n" - f"📅 Dauer: {duration_str}\n" - f"📌 Angepinnte Nachrichten: {'Ausgeschlossen' if exclude_pinned else 'Eingeschlossen'}\n" - f"🤖 Bot-Nachrichten: {'Ausgeschlossen' if exclude_bots else 'Eingeschlossen'}", - ephemeral=True - ) - - @autodelete.command(name="list", description="Zeigt alle aktiven AutoDelete-Kanäle.") - async def list(self, ctx): - db = AutoDeleteDB() - channels = db.get_all() - if not channels: - await ctx.respond("❌ Keine AutoDelete-Kanäle gefunden.", ephemeral=True) - return - - embed = discord.Embed( - title="🗑️ Aktive AutoDelete-Kanäle", - color=discord.Color.blue(), - timestamp=datetime.utcnow() - ) - - for chan_id, duration, exclude_pinned, exclude_bots in channels: - channel = self.bot.get_channel(chan_id) - if channel: - duration_str = self._format_duration(duration) - settings = [] - if exclude_pinned: - settings.append("📌 Angepinnte ausgeschlossen") - if exclude_bots: - settings.append("🤖 Bots ausgeschlossen") - - settings_str = "\n".join(settings) if settings else "Keine besonderen Einstellungen" - - embed.add_field( - name=f"#{channel.name}", - value=f"⏱️ {duration_str}\n{settings_str}", - inline=True - ) - else: - embed.add_field( - name="❌ Unbekannter Kanal", - value=f"ID: {chan_id}\n⏱️ {self._format_duration(duration)}", - inline=True - ) - - await ctx.respond(embed=embed, ephemeral=True) - - @autodelete.command(name="remove", description="Entfernt AutoDelete von einem Kanal.") - async def remove(self, ctx, - channel: Option(discord.TextChannel, "Kanal", required=True)): - db = AutoDeleteDB() - if db.get_autodelete(channel.id): - db.remove_autodelete(channel.id) - await ctx.respond(f"🗑️ AutoDelete für {channel.mention} wurde entfernt.", ephemeral=True) - else: - await ctx.respond(f"❌ AutoDelete war für {channel.mention} nicht aktiviert.", ephemeral=True) - - @autodelete.command(name="stats", description="Zeigt Statistiken für einen AutoDelete-Kanal.") - async def stats(self, ctx, - channel: Option(discord.TextChannel, "Kanal", required=True)): - db = AutoDeleteDB() - config = db.get_autodelete_full(channel.id) - if not config: - await ctx.respond(f"❌ AutoDelete ist für {channel.mention} nicht aktiviert.", ephemeral=True) - return - - duration, exclude_pinned, exclude_bots = config - stats = db.get_stats(channel.id) - - embed = discord.Embed( - title=f"📊 AutoDelete Statistiken - #{channel.name}", - color=discord.Color.green(), - timestamp=datetime.utcnow() - ) - - embed.add_field(name="⏱️ Löschzeit", value=self._format_duration(duration), inline=True) - embed.add_field(name="📌 Angepinnte", value="Ausgeschlossen" if exclude_pinned else "Eingeschlossen", - inline=True) - embed.add_field(name="🤖 Bots", value="Ausgeschlossen" if exclude_bots else "Eingeschlossen", inline=True) - - if stats: - embed.add_field(name="🗑️ Gelöschte Nachrichten", value=str(stats['deleted_count']), inline=True) - embed.add_field(name="❌ Fehler", value=str(stats['error_count']), inline=True) - if stats['last_deletion']: - embed.add_field(name="🕒 Letzte Löschung", value=f"", inline=True) - - await ctx.respond(embed=embed, ephemeral=True) - - @autodelete.command(name="test", description="Testet die AutoDelete-Funktion für einen Kanal.") - async def test(self, ctx, - channel: Option(discord.TextChannel, "Kanal", required=True)): - db = AutoDeleteDB() - config = db.get_autodelete_full(channel.id) - if not config: - await ctx.respond(f"❌ AutoDelete ist für {channel.mention} nicht aktiviert.", ephemeral=True) - return - - await ctx.defer(ephemeral=True) - - try: - deleted_count = await self._process_channel_deletion(channel.id, test_mode=True) - await ctx.followup.send( - f"✅ Test erfolgreich!\n" - f"📝 {deleted_count} Nachrichten würden gelöscht werden.", - ephemeral=True - ) - except Exception as e: - await ctx.followup.send(f"❌ Test fehlgeschlagen: {str(e)}", ephemeral=True) - - @tasks.loop(seconds=30) # Erhöht auf 30 Sekunden für bessere Performance - async def delete_task(self): - try: - db = AutoDeleteDB() - channels = db.get_all() - - # Verarbeite Kanäle parallel, aber begrenzt - semaphore = asyncio.Semaphore(3) # Max 3 Kanäle gleichzeitig - tasks = [] - - for chan_id, duration, exclude_pinned, exclude_bots in channels: - if chan_id not in self.processing_channels: - task = self._process_channel_with_semaphore(semaphore, chan_id) - tasks.append(task) - - if tasks: - await asyncio.gather(*tasks, return_exceptions=True) - - except Exception as e: - logger.error(f"Fehler im delete_task: {e}") - - async def _process_channel_with_semaphore(self, semaphore, channel_id): - async with semaphore: - await self._process_channel_deletion(channel_id) - - async def _process_channel_deletion(self, channel_id, test_mode=False): - if channel_id in self.processing_channels and not test_mode: - return 0 - - if not test_mode: - self.processing_channels.add(channel_id) - - try: - db = AutoDeleteDB() - config = db.get_autodelete_full(channel_id) - if not config: - return 0 - - duration, exclude_pinned, exclude_bots = config - - # Zeitplan-Prüfung - if not self._is_in_schedule(channel_id): - return 0 - - channel = self.bot.get_channel(channel_id) - if not channel: - return 0 - - deleted_count = 0 - error_count = 0 - cutoff_time = datetime.utcnow() - timedelta(seconds=duration) - - try: - messages_to_delete = [] - async for msg in channel.history(limit=200, oldest_first=True): - if msg.created_at >= cutoff_time: - break - - # Filterlogik - if exclude_pinned and msg.pinned: - continue - if exclude_bots and msg.author.bot: - continue - - # Whitelist-Prüfung - if self._check_whitelist(msg, channel_id): - continue - - messages_to_delete.append(msg) - - # Batch-Löschung für bessere Performance - if len(messages_to_delete) >= 10: - if not test_mode: - deleted, errors = await self._bulk_delete_messages(channel, messages_to_delete) - deleted_count += deleted - error_count += errors - else: - deleted_count += len(messages_to_delete) - messages_to_delete.clear() - - # Restliche Nachrichten löschen - if messages_to_delete: - if not test_mode: - deleted, errors = await self._bulk_delete_messages(channel, messages_to_delete) - deleted_count += deleted - error_count += errors - else: - deleted_count += len(messages_to_delete) - - # Statistiken aktualisieren - if not test_mode and (deleted_count > 0 or error_count > 0): - db.update_stats(channel_id, deleted_count, error_count) - - except discord.errors.Forbidden: - logger.warning(f"Keine Berechtigung für Kanal {channel_id}") - except Exception as e: - logger.error(f"Fehler beim Verarbeiten von Kanal {channel_id}: {e}") - if not test_mode: - db.update_stats(channel_id, 0, 1) - - return deleted_count - - finally: - if not test_mode: - self.processing_channels.discard(channel_id) - - async def _bulk_delete_messages(self, channel, messages): - deleted_count = 0 - error_count = 0 - - # Trenne alte und neue Nachrichten (Discord API Limitation) - old_messages = [] - new_messages = [] - two_weeks_ago = datetime.utcnow() - timedelta(days=14) - - for msg in messages: - if msg.created_at < two_weeks_ago: - old_messages.append(msg) - else: - new_messages.append(msg) - - # Bulk delete für neue Nachrichten - if new_messages: - try: - await channel.delete_messages(new_messages) - deleted_count += len(new_messages) - except Exception as e: - logger.error(f"Bulk delete Fehler: {e}") - - return deleted_count, error_count - - # Platzhalter für fehlende Methoden, um den Code lauffähig zu machen - def _format_duration(self, duration: int) -> str: - """Formatiert die Dauer in eine lesbare Zeichenkette (z.B. '1 Stunde').""" - if duration >= 86400 and duration % 86400 == 0: - return f"{duration // 86400} Tage" - if duration >= 3600 and duration % 3600 == 0: - return f"{duration // 3600} Stunden" - if duration >= 60 and duration % 60 == 0: - return f"{duration // 60} Minuten" - return f"{duration} Sekunden" - - def _is_in_schedule(self, channel_id: int) -> bool: - """Platzhalter: Prüft, ob der Kanal gerade gelöscht werden soll (immer True im Platzhalter).""" - # Da diese Methode in Ihrem Originalcode nicht definiert ist, aber aufgerufen wird, - # muss sie entweder in der DB/Config abrufbar sein oder als Platzhalter existieren. - # Wir lassen sie hier True zurückgeben, um die Löschlogik nicht zu blockieren. - return True - - def _check_whitelist(self, message: discord.Message, channel_id: int) -> bool: - """Platzhalter: Prüft, ob die Nachricht von der Löschung ausgenommen ist (immer False im Platzhalter).""" - return False - -def setup(bot): - bot.add_cog(AutoDelete(bot)) \ No newline at end of file diff --git a/src/cogs/Servermanament/autorole.py b/src/cogs/Servermanament/autorole.py deleted file mode 100644 index 6d5944c..0000000 --- a/src/cogs/Servermanament/autorole.py +++ /dev/null @@ -1,273 +0,0 @@ -import discord -from discord.ext import commands -from discord import option -from DevTools import AutoRoleDatabase -from handler import TranslationHandler as TH - -class AutoRole(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.db = AutoRoleDatabase() - - async def cog_load(self): - """Wird aufgerufen, wenn der Cog geladen wird""" - await self.db.init_db() - - autorole = discord.SlashCommandGroup( - name="autorole", - description="Verwalte das Autorole-System", - default_member_permissions=discord.Permissions(administrator=True) - ) - - @autorole.command(name="add", description="Füge eine neue Autorole hinzu") - @option( - name="rolle", - description="Die Rolle, die vergeben werden soll", - type=discord.Role, - required=True - ) - async def autorole_add(self, ctx: discord.ApplicationContext, rolle: discord.Role): - """Fügt eine neue Autorole hinzu""" - - # Prüfe, ob der Bot die Rolle vergeben kann - if rolle.position >= ctx.guild.me.top_role.position: - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.role_to_high.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.role_to_high.desc"), - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - if rolle.managed: - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.role_managed.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.role_managed.desc"), - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - # Füge die Autorole hinzu - autorole_id = await self.db.add_autorole(ctx.guild.id, rolle.id) - - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.messages.add_success.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.messages.add_success.desc", role=rolle.mention, autorole_id=autorole_id), - color=discord.Color.green() - ) - await ctx.respond(embed=embed) - - @autorole.command(name="remove", description="Entferne eine Autorole") - @option( - name="autorole_id", - description="Die ID der Autorole (z.B. 26-25-153)", - type=str, - required=True - ) - async def autorole_remove(self, ctx: discord.ApplicationContext, autorole_id: str): - """Entfernt eine Autorole anhand der ID""" - - config = await self.db.get_autorole(autorole_id) - - if not config: - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.not_found.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.not_found.desc", autorole_id=autorole_id), - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - if config["guild_id"] != ctx.guild.id: - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.wrong_guild.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.wrong_guild.desc"), - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - await self.db.remove_autorole(autorole_id) - - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.messages.remove_success.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.messages.remove_success.desc", autorole_id=autorole_id), - color=discord.Color.green() - ) - await ctx.respond(embed=embed) - - @autorole.command(name="toggle", description="Aktiviere oder deaktiviere eine Autorole") - @option( - name="autorole_id", - description="Die ID der Autorole (z.B. 26-25-153)", - type=str, - required=True - ) - @option( - name="status", - description="Status der Autorole", - type=str, - choices=["aktivieren", "deaktivieren"], - required=True - ) - async def autorole_toggle(self, ctx: discord.ApplicationContext, autorole_id: str, status: str): - """Aktiviert oder deaktiviert eine Autorole""" - - config = await self.db.get_autorole(autorole_id) - - if not config: - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.not_found.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.not_found.desc", autorole_id=autorole_id), - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - if config["guild_id"] != ctx.guild.id: - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.wrong_guild.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.wrong_guild.desc"), - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - enabled = status == "aktivieren" - await self.db.toggle_autorole(autorole_id, enabled) - - status_text = "enabled" if enabled else "disabled" - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, f"cog_autorole.messages.toggle_success.{status_text}_title"), - description=await TH.get_for_user(self.bot, ctx.author.id, f"cog_autorole.messages.toggle_success.{status_text}_desc", autorole_id=autorole_id), - color=discord.Color.green() - ) - await ctx.respond(embed=embed) - - @autorole.command(name="list", description="Zeige alle Autoroles auf diesem Server") - async def autorole_list(self, ctx: discord.ApplicationContext): - """Zeigt alle Autoroles für den Server""" - - autoroles = await self.db.get_all_autoroles(ctx.guild.id) - - if not autoroles: - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.no_roles.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.no_roles.desc"), - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.messages.list.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.messages.list.desc", guild_name=ctx.guild.name), - color=discord.Color.blue() - ) - - for ar in autoroles: - role = ctx.guild.get_role(ar["role_id"]) - if role: - status = "🟢 Aktiv" if ar["enabled"] else "🔴 Inaktiv" - embed.add_field( - name=f"ID: `{ar['autorole_id']}`", - value=f"**Rolle:** {role.mention}\n**Status:** {status}\n**Mitglieder:** {len(role.members)}", - inline=False - ) - else: - embed.add_field( - name=f"ID: `{ar['autorole_id']}`", - value=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.messages.list.role_deleted"), - inline=False - ) - - await ctx.respond(embed=embed) - - @autorole.command(name="info", description="Zeige Details zu einer spezifischen Autorole") - @option( - name="autorole_id", - description="Die ID der Autorole (z.B. 26-25-153)", - type=str, - required=True - ) - async def autorole_info(self, ctx: discord.ApplicationContext, autorole_id: str): - """Zeigt Details zu einer spezifischen Autorole""" - - config = await self.db.get_autorole(autorole_id) - - if not config: - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.not_found.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.not_found.desc", autorole_id=autorole_id), - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - if config["guild_id"] != ctx.guild.id: - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.wrong_guild.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.wrong_guild.desc"), - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - role = ctx.guild.get_role(config["role_id"]) - - if not role: - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.role_deleted.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.error_types.role_deleted.desc", autorole_id=autorole_id), - color=discord.Color.orange() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - embed = discord.Embed( - title=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.messages.info.title"), - description=await TH.get_for_user(self.bot, ctx.author.id, "cog_autorole.messages.info.desc", autorole_id=autorole_id), - color=discord.Color.blue() - ) - embed.add_field(name="Rolle", value=role.mention, inline=True) - embed.add_field(name="Status", value="🟢 Aktiviert" if config["enabled"] else "🔴 Deaktiviert", inline=True) - embed.add_field(name="Mitglieder mit dieser Rolle", value=str(len(role.members)), inline=True) - embed.add_field(name="Rollen-ID", value=f"`{role.id}`", inline=True) - embed.add_field(name="Autorole-ID", value=f"`{autorole_id}`", inline=True) - - await ctx.respond(embed=embed) - - @commands.Cog.listener() - async def on_member_join(self, member: discord.Member): - """Event: Wird ausgelöst, wenn ein neues Mitglied dem Server beitritt""" - - role_ids = await self.db.get_enabled_autoroles(member.guild.id) - - if not role_ids: - return - - roles_to_add = [] - - for role_id in role_ids: - role = member.guild.get_role(role_id) - if role and role.position < member.guild.me.top_role.position: - roles_to_add.append(role) - - if not roles_to_add: - return - - try: - audit_reason = TH.get("de", "cog_autorole.system.audit_reason") - await member.add_roles(*roles_to_add, reason=audit_reason) - - role_names = ", ".join([r.name for r in roles_to_add]) - log_msg = TH.get("de", "cog_autorole.system.console_log", role_names=role_names, member_name=member.name) - print(log_msg) - except discord.Forbidden: - print(TH.get("de", "cog_autorole.system.error_forbidden")) - except discord.HTTPException as e: - print(TH.get("de", "cog_autorole.system.error_http", error=str(e))) - -def setup(bot): - bot.add_cog(AutoRole(bot)) \ No newline at end of file diff --git a/src/cogs/Servermanament/globalchat.py b/src/cogs/Servermanament/globalchat.py deleted file mode 100644 index 295ad0b..0000000 --- a/src/cogs/Servermanament/globalchat.py +++ /dev/null @@ -1,1517 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -import discord -from discord.ext import commands, tasks -from discord import slash_command, Option, SlashCommandGroup -from DevTools.backend.database.globalchat_db import GlobalChatDatabase, db -import asyncio -import logging -import re -import time -from typing import List, Optional, Dict, Tuple -import aiohttp -import io -import json -from datetime import datetime, timedelta -import ezcord -from collections import defaultdict -from discord.ui import Container - -# Logger konfigurieren -logger = logging.getLogger(__name__) - - -class GlobalChatConfig: - """Zentrale Konfiguration für GlobalChat""" - RATE_LIMIT_MESSAGES = 15 - RATE_LIMIT_SECONDS = 60 - CACHE_DURATION = 180 # 3 Minuten - CLEANUP_DAYS = 30 - MIN_MESSAGE_LENGTH = 0 # Erlaube Nachrichten ohne Text (nur Medien) - DEFAULT_MAX_MESSAGE_LENGTH = 1900 - DEFAULT_EMBED_COLOR = '#5865F2' - - # Medien-Limits - MAX_FILE_SIZE_MB = 25 # Discord-Standard - MAX_ATTACHMENTS = 10 - ALLOWED_IMAGE_FORMATS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'] - ALLOWED_VIDEO_FORMATS = ['mp4', 'mov', 'webm', 'avi', 'mkv'] - ALLOWED_AUDIO_FORMATS = ['mp3', 'wav', 'ogg', 'm4a', 'flac'] - ALLOWED_DOCUMENT_FORMATS = ['pdf', 'txt', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar', '7z'] - - # Bot Owner IDs - BOT_OWNERS = [1093555256689959005, 1427994077332373554] - - # Content Filter Patterns - DISCORD_INVITE_PATTERN = r'(?i)\b(discord\.gg|discord\.com/invite|discordapp\.com/invite)/[a-zA-Z0-9]+\b' - URL_PATTERN = r'(?i)\bhttps?://(?:[a-zA-Z0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F]{2}))+\b' - - # NSFW Keywords - NSFW_KEYWORDS = [ - 'nsfw', 'porn', 'sex', 'xxx', 'nude', 'hentai', - 'dick', 'pussy', 'cock', 'tits', 'ass', 'fuck' - ] - - -class MediaHandler: - """Verarbeitet alle Arten von Medien und Anhängen""" - - def __init__(self, config: GlobalChatConfig): - self.config = config - - def validate_attachments(self, attachments: List[discord.Attachment]) -> Tuple[bool, str, List[discord.Attachment]]: - """Validiert Attachments und gibt valide zurück""" - if not attachments: - return True, "", [] - - if len(attachments) > self.config.MAX_ATTACHMENTS: - return False, f"Zu viele Anhänge (max. {self.config.MAX_ATTACHMENTS})", [] - - valid_attachments = [] - max_size_bytes = self.config.MAX_FILE_SIZE_MB * 1024 * 1024 - - for attachment in attachments: - # Größe prüfen - if attachment.size > max_size_bytes: - return False, f"Datei '{attachment.filename}' ist zu groß (max. {self.config.MAX_FILE_SIZE_MB}MB)", [] - - # Dateiformat prüfen - file_ext = attachment.filename.split('.')[-1].lower() if '.' in attachment.filename else '' - - all_allowed = ( - self.config.ALLOWED_IMAGE_FORMATS + - self.config.ALLOWED_VIDEO_FORMATS + - self.config.ALLOWED_AUDIO_FORMATS + - self.config.ALLOWED_DOCUMENT_FORMATS - ) - - if file_ext and file_ext not in all_allowed: - return False, f"Dateiformat '.{file_ext}' nicht erlaubt", [] - - valid_attachments.append(attachment) - - return True, "", valid_attachments - - def categorize_attachment(self, attachment: discord.Attachment) -> str: - """Kategorisiert einen Anhang nach Typ""" - if not attachment.filename or '.' not in attachment.filename: - return 'other' - - file_ext = attachment.filename.split('.')[-1].lower() - - if file_ext in self.config.ALLOWED_IMAGE_FORMATS: - return 'image' - elif file_ext in self.config.ALLOWED_VIDEO_FORMATS: - return 'video' - elif file_ext in self.config.ALLOWED_AUDIO_FORMATS: - return 'audio' - elif file_ext in self.config.ALLOWED_DOCUMENT_FORMATS: - return 'document' - else: - return 'other' - - def get_attachment_icon(self, attachment: discord.Attachment) -> str: - """Gibt passendes Icon für Attachment-Typ zurück""" - category = self.categorize_attachment(attachment) - - icons = { - 'image': '🖼️', - 'video': '🎥', - 'audio': '🎵', - 'document': '📄', - 'other': '📎' - } - - return icons.get(category, '📎') - - def format_file_size(self, size_bytes: int) -> str: - """Formatiert Dateigröße leserlich""" - for unit in ['B', 'KB', 'MB']: - if size_bytes < 1024.0: - return f"{size_bytes:.1f} {unit}" - size_bytes /= 1024.0 - return f"{size_bytes:.1f} GB" - - -class MessageValidator: - """Validiert und filtert Nachrichten""" - - def __init__(self, config: GlobalChatConfig): - self.config = config - self.media_handler = MediaHandler(config) - self._compile_patterns() - - def _compile_patterns(self): - """Kompiliert Regex-Patterns für bessere Performance""" - self.invite_pattern = re.compile(self.config.DISCORD_INVITE_PATTERN) - self.url_pattern = re.compile(self.config.URL_PATTERN) - - def validate_message(self, message: discord.Message, settings: Dict) -> Tuple[bool, str]: - """Hauptvalidierung für Nachrichten""" - # Bot-Nachrichten ignorieren - if message.author.bot: - return False, "Bot-Nachricht" - - # Blacklist prüfen - if db.is_blacklisted('user', message.author.id): - return False, "User auf Blacklist" - - if db.is_blacklisted('guild', message.guild.id): - return False, "Guild auf Blacklist" - - # Leere Nachrichten (ohne Text UND ohne Anhänge/Sticker) - if not message.content and not message.attachments and not message.stickers: - return False, "Leere Nachricht" - - # Nachrichtenlänge (nur wenn Text vorhanden) - if message.content: - content_length = len(message.content.strip()) - - # Mindestlänge nur bei reinen Text-Nachrichten - if content_length < self.config.MIN_MESSAGE_LENGTH and not message.attachments and not message.stickers: - return False, "Zu kurze Nachricht" - - max_length = settings.get('max_message_length', self.config.DEFAULT_MAX_MESSAGE_LENGTH) - if content_length > max_length: - return False, f"Nachricht zu lang (max. {max_length} Zeichen)" - - # Attachments validieren - if message.attachments: - valid, reason, _ = self.media_handler.validate_attachments(message.attachments) - if not valid: - return False, f"Ungültige Anhänge: {reason}" - - # Content Filter - if settings.get('filter_enabled', True): - is_filtered, filter_reason = self.check_filtered_content(message.content) - if is_filtered: - return False, f"Gefilterte Inhalte: {filter_reason}" - - # NSFW Filter - if settings.get('nsfw_filter', True): - if self.check_nsfw_content(message.content): - return False, "NSFW Inhalt erkannt" - - return True, "OK" - - def check_filtered_content(self, content: str) -> Tuple[bool, str]: - """Prüft auf gefilterte Inhalte mit detailliertem Grund""" - if not content: - return False, "" - - # Discord Invites - if self.invite_pattern.search(content): - return True, "Discord Invite" - - return False, "" - - def check_nsfw_content(self, content: str) -> bool: - """Erweiterte NSFW-Erkennung""" - if not content: - return False - - content_lower = content.lower() - - # Keyword-Check mit Wortgrenzen - for keyword in self.config.NSFW_KEYWORDS: - pattern = r'\b' + re.escape(keyword) + r'\b' - if re.search(pattern, content_lower): - return True - - return False - - def clean_content(self, content: str) -> str: - """Bereinigt Nachrichteninhalt""" - if not content: - return "" - - # @everyone und @here neutralisieren - content = content.replace('@everyone', '@everyone') - content = content.replace('@here', '@here') - - # Rolle-Mentions neutralisieren - content = re.sub(r'<@&(\d+)>', r'@role', content) - - return content - - -class EmbedBuilder: - """Erstellt formatierte Embeds für GlobalChat mit vollständigem Medien-Support""" - - def __init__(self, config: GlobalChatConfig, bot=None): - self.config = config - self.media_handler = MediaHandler(config) - self.bot = bot # Bot für Message-Fetching - - async def create_message_embed(self, message: discord.Message, settings: Dict, attachment_data: List[Tuple[str, bytes, str]] = None) -> Tuple[discord.Embed, List[Tuple[str, bytes]]]: - """Erstellt ein verbessertes Embed mit vollständigem Medien-Support - - attachment_data: Liste von (filename, bytes, content_type) - schon heruntergeladene Dateien - Gibt (embed, [(filename, bytes), ...]) zurück - Bytes statt discord.File! - """ - if attachment_data is None: - attachment_data = [] - - content = self._clean_content(message.content) - - # Embed-Farbe - embed_color = self._parse_color(settings.get('embed_color', self.config.DEFAULT_EMBED_COLOR)) - - # Beschreibung - if content: - description = content - elif message.attachments or message.stickers or attachment_data: - description = "*Medien-Nachricht*" - else: - description = "*Keine Beschreibung*" - - # Embed erstellen - embed = discord.Embed( - description=description, - color=embed_color, - timestamp=message.created_at - ) - - # Author mit Badges - author_text, badges = self._build_author_info(message.author) - embed.set_author( - name=author_text, - icon_url=message.author.display_avatar.url - ) - - # Footer mit Server-Info UND Original-Message-ID (für Reply-Tracking) - footer_text = f"📍 {message.guild.name} • #{message.channel.name} • ID:{message.id}" - embed.set_footer( - text=footer_text, - icon_url=message.guild.icon.url if message.guild.icon else None - ) - - # Reply-Kontext hinzufügen (robust, ohne invasive Änderungen) - if message.reference: - try: - # Versuche zuerst die gecachte referenzierte Nachricht - replied_msg = message.reference.resolved - - # Falls nicht im Cache, versuche die referenzierte Nachricht aus dem referenzierten Kanal zu holen - if not replied_msg and getattr(message.reference, 'message_id', None): - ref_channel = None - ref_chan_id = getattr(message.reference, 'channel_id', None) - if ref_chan_id: - # Versuche zuerst den Kanal vom Bot-Cache - ref_channel = self.bot.get_channel(ref_chan_id) - # Fallback auf Guild-Kanal - if not ref_channel and message.guild: - try: - ref_channel = message.guild.get_channel(ref_chan_id) - except Exception: - ref_channel = None - if not ref_channel: - ref_channel = message.channel - - if ref_channel: - try: - replied_msg = await ref_channel.fetch_message(message.reference.message_id) - except Exception: - replied_msg = None - - # Wenn wir eine referenzierte Nachricht haben, bereite Vorschau vor - if isinstance(replied_msg, discord.Message): - # Text-Vorschau (bevorzuge echten content) - preview = replied_msg.content or "" - - # Wenn die referenzierte Nachricht das Relay-Bot-Embed ist, versuche Text aus dem Embed - if not preview and replied_msg.embeds: - try: - preview = replied_msg.embeds[0].description or "" - except Exception: - preview = "" - - # Fallback auf Anhänge/Sticker - if not preview: - if replied_msg.attachments: - preview = f"📎 {len(replied_msg.attachments)} Datei(en)" - elif replied_msg.stickers: - preview = "🎨 Sticker" - else: - preview = "*(Leere Nachricht)*" - - preview = self._clean_content(preview) - preview_short = (preview[:200] + "...") if len(preview) > 200 else preview - - # Author bestimmen: falls die referenzierte Nachricht vom Bot ist, versuche embed.author - author_display = None - try: - if replied_msg.author and replied_msg.author.id == getattr(self.bot, 'user', None).id and replied_msg.embeds: - emb = replied_msg.embeds[0] - if emb.author and emb.author.name: - author_display = emb.author.name - except Exception: - author_display = None - - if not author_display: - try: - author_display = replied_msg.author.display_name - except Exception: - author_display = "Unbekannter User" - - # Herkunft (Server • #channel) - origin = None - try: - if getattr(replied_msg, 'guild', None) and getattr(replied_msg, 'channel', None): - origin = f"{replied_msg.guild.name} • #{replied_msg.channel.name}" - except Exception: - origin = None - - reply_field = f"**{author_display}:** {preview_short}" - if origin: - reply_field += f"\n_{origin}_" - - embed.add_field(name="↩️ Antwort (Vorschau)", value=reply_field, inline=False) - except Exception: - # Never fail building the embed just because reply resolution failed - pass - - # Medien verarbeiten mit heruntergeladenen Dateien - files_to_upload = await self._process_media(embed, message, attachment_data) - - # Rückgabe: Embed + Liste von discord.File Objekten - return embed, files_to_upload - - async def _process_media(self, embed: discord.Embed, message: discord.Message, attachment_data: List[Tuple[str, bytes, str]] = None) -> List[Tuple[str, bytes]]: - """Verarbeitet alle Medien-Typen mit heruntergeladenen Anhängen - - attachment_data: Liste von (filename, bytes, content_type) - bereits heruntergeladen - Gibt Liste von (filename, bytes) zurück - NOT discord.File! - """ - if attachment_data is None: - attachment_data = [] - - attachment_bytes: List[Tuple[str, bytes]] = [] - - # === HERUNTERGELADENE ATTACHMENTS === - if attachment_data: - attachment_bytes.extend(self._process_downloaded_attachments(embed, attachment_data)) - - # === STICKERS === - if message.stickers: - self._process_stickers(embed, message.stickers) - - # === ORIGINAL EMBEDS (z.B. von Links) === - if message.embeds: - self._process_embeds(embed, message.embeds) - - return attachment_bytes - - def _process_downloaded_attachments(self, embed: discord.Embed, attachment_data: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes]]: - """Verarbeitet heruntergeladene Anhänge und gibt (filename, bytes) zurück - - attachment_data: [(filename, bytes_data, content_type), ...] - Gibt [(filename, bytes), ...] zurück - NICHT discord.File! - """ - attachment_bytes: List[Tuple[str, bytes]] = [] - - # Kategorisiere nach Typ - images = [] - videos = [] - audios = [] - documents = [] - others = [] - - for filename, data, content_type in attachment_data: - # Bestimme Dateityp anhand von content_type und Dateiendung - category = self._get_attachment_category(filename, content_type) - - if category == 'image': - images.append((filename, data)) - elif category == 'video': # HIER wurde der Code vervollständigt - videos.append((filename, data)) - elif category == 'audio': - audios.append((filename, data)) - elif category == 'document': - documents.append((filename, data)) - else: - others.append((filename, data)) # Vervollständigt - - # === IMAGE (NUR das erste Bild als embed.image) === - if images: - # Das erste Bild als Embed-Bild setzen - embed.set_image(url=f"attachment://{images[0][0]}") - # Alle Bilder für den Upload vorbereiten - for filename, data in images: - attachment_bytes.append((filename, data)) - - if len(images) > 1: - # Füge einen Hinweis hinzu, dass weitere Bilder angehängt sind - embed.add_field( - name="🖼️ Weitere Bilder", - value=f"_{len(images)-1} zusätzliche Bilder angehängt._", - inline=False - ) - - # === VIDEOS === - if videos: - video_links = [] - for video_name, video_data in videos: - size = len(video_data) - size_str = self.media_handler.format_file_size(size) - video_links.append(f"🎥 {video_name} ({size_str})") - attachment_bytes.append((video_name, video_data)) - - if video_links: - embed.add_field( - name="🎬 Videos", - value="\n".join(video_links[:3]), # Max 3 - inline=False - ) - - # === AUDIO === - if audios: - audio_links = [] - for audio_name, audio_data in audios: - size = len(audio_data) - size_str = self.media_handler.format_file_size(size) - audio_links.append(f"🎵 {audio_name} ({size_str})") - attachment_bytes.append((audio_name, audio_data)) - - if audio_links: - embed.add_field( - name="🎧 Audio-Dateien", - value="\n".join(audio_links[:3]), # Max 3 - inline=False - ) - - # === DOKUMENTE === - if documents: - doc_links = [] - for doc_name, doc_data in documents: - size = len(doc_data) - size_str = self.media_handler.format_file_size(size) - doc_links.append(f"📄 {doc_name} ({size_str})") - attachment_bytes.append((doc_name, doc_data)) - - if doc_links: - embed.add_field( - name="📄 Dokumente", - value="\n".join(doc_links[:3]), # Max 3 - inline=False - ) - - # === SONSTIGE === - if others: - other_links = [] - for other_name, other_data in others: - size = len(other_data) - size_str = self.media_handler.format_file_size(size) - other_links.append(f"📎 {other_name} ({size_str})") - attachment_bytes.append((other_name, other_data)) - - if other_links: - embed.add_field( - name="📎 Sonstige", - value="\n".join(other_links[:3]), # Max 3 - inline=False - ) - - return attachment_bytes # Wichtig: bytes zurückgeben - - def _process_stickers(self, embed: discord.Embed, stickers: List[discord.StickerItem]): - """Verarbeitet Discord Sticker""" - if not stickers: - return - - sticker_info = [] - for sticker in stickers: - sticker_type = "Standard" if sticker.url.endswith('.png') else "Animiert" - sticker_info.append(f"🎨 **{sticker.name}** ({sticker_type})") - - embed.add_field( - name="🎨 Sticker", - value="\n".join(sticker_info[:3]), - inline=False - ) - - # Versuche, das erste Bild (falls vorhanden) als Thumbnail zu setzen - if stickers[0].format.name in ['PNG', 'LOTTIE']: - embed.set_thumbnail(url=stickers[0].url) - - def _process_embeds(self, main_embed: discord.Embed, embeds: List[discord.Embed]): - """Verarbeitet Original-Embeds (z.B. Link-Vorschauen)""" - if not embeds: - return - - link_embeds = [] - for embed in embeds: - # Nur Embeds mit Titeln oder Beschreibungen, die keine eigenen Attachments sind, verarbeiten - if embed.type not in ['image', 'video', 'gifv'] and (embed.title or embed.description or embed.url): - - title = embed.title or "Unbekannter Link" - description = (embed.description[:100] + "...") if embed.description else "" - url = embed.url or "" - - link_embeds.append(f"**[{title}]({url})**\n_{description}_") - - if link_embeds: - main_embed.add_field( - name="🔗 Verlinkte Inhalte", - value="\n\n".join(link_embeds), - inline=False - ) - - def _get_attachment_category(self, filename: str, content_type: str) -> str: - """Hilfsfunktion zur Kategorisierung basierend auf Name und Content-Type""" - if content_type.startswith('image/'): - return 'image' - elif content_type.startswith('video/'): - return 'video' - elif content_type.startswith('audio/'): - return 'audio' - - # Fallback auf Dateiendung - if not filename or '.' not in filename: - return 'other' - - file_ext = filename.split('.')[-1].lower() - if file_ext in self.config.ALLOWED_IMAGE_FORMATS: - return 'image' - elif file_ext in self.config.ALLOWED_VIDEO_FORMATS: - return 'video' - elif file_ext in self.config.ALLOWED_AUDIO_FORMATS: - return 'audio' - elif file_ext in self.config.ALLOWED_DOCUMENT_FORMATS: - return 'document' - else: - return 'other' - - def _clean_content(self, content: str) -> str: - """Bereinigt Nachrichteninhalt""" - if not content: - return "" - content = content.replace('@everyone', '@everyone') - content = content.replace('@here', '@here') - content = re.sub(r'<@&(\d+)>', r'@role', content) - return content.strip() - - def _parse_color(self, color_hex: str) -> discord.Color: - """Parst Hex-Farbe zu discord.Color""" - try: - color_hex = color_hex.lstrip('#') - return discord.Color(int(color_hex, 16)) - except (ValueError, TypeError): - return discord.Color.blurple() - - def _build_author_info(self, author: discord.Member) -> Tuple[str, List[str]]: - """Baut Author-Text mit Badges""" - badges = [] - roles = [] - # Bot Owner - if author.id in self.config.BOT_OWNERS: - badges.append("👑") - roles.append("Bot Owner") - # Server Admin/Mod - if author.guild_permissions.administrator: - badges.append("⚡") - roles.append("Admin") - elif author.guild_permissions.manage_guild: - badges.append("🔧") - roles.append("Mod") - - badge_text = " ".join(badges) - author_text = f"{badge_text} {author.display_name}".strip() - - # Hinzufügen von Discord System Badges (z.B. Bot, Verified Bot) - if author.bot: - author_text += " [BOT]" - - return author_text, roles - - -class GlobalChatSender: - """Verantwortlich für das Senden der Nachricht an alle verbundenen Kanäle""" - def __init__(self, bot, config: GlobalChatConfig, embed_builder: EmbedBuilder, cache_ref: List[int]): - self.bot = bot - self.config = config - self.embed_builder = embed_builder - self._cached_channels = cache_ref # Referenz zum Cache in der Cog - - async def _get_all_active_channels(self) -> List[int]: - """Ruft alle aktiven Channel-IDs ab, nutzt den Cache""" - if self._cached_channels is None: - # Cache initial füllen - self._cached_channels = await self._fetch_all_channels() - return self._cached_channels - - async def _fetch_all_channels(self) -> List[int]: - """Holt Channel IDs direkt aus der Datenbank""" - try: - channel_ids = db.get_all_channels() - return channel_ids - except Exception as e: - logger.error(f"❌ Fehler beim Abrufen aller Channel-IDs: {e}", exc_info=True) - return [] - - async def _send_to_channel(self, channel_id: int, embed: discord.Embed, attachment_bytes: List[Tuple[str, bytes]]) -> bool: - """Sendet die Embed-Nachricht an einen spezifischen Channel mit Error-Handling - attachment_bytes: Liste von (filename, bytes) - wird zu discord.File konvertiert - Wichtig: Raw bytes, nicht discord.File, da File-Streams verbraucht sind! - """ - try: - channel = self.bot.get_channel(channel_id) - if not channel: - logger.warning(f"⚠️ Channel {channel_id} nicht gefunden") - return False - - # Permissions prüfen - perms = channel.permissions_for(channel.guild.me) - if not perms.send_messages or not perms.embed_links: - logger.warning(f"⚠️ Keine Permissions in {channel_id}") - return False - - # Erstelle NEUE discord.File Objekte für diesen Channel (wichtig!) - # Jeder Channel bekommt seine eigenen frischen Files! - files = [] - if attachment_bytes: - for filename, data in attachment_bytes: - try: - files.append(discord.File(io.BytesIO(data), filename=filename)) - except Exception as e: - logger.warning(f"⚠️ Error creating file {filename}: {e}") - - # Sende mit Retry-Logik - max_retries = 3 - for attempt in range(max_retries): - try: - if files: - await channel.send(embed=embed, files=files) - else: - await channel.send(embed=embed) - return True - except (ConnectionResetError, aiohttp.ClientConnectorError, asyncio.TimeoutError) as e: - logger.warning(f"❌ Sendefehler (Retry {attempt+1}/{max_retries}) in {channel_id}: {e}") - await asyncio.sleep(1 + attempt * 2) - except discord.Forbidden: - logger.warning(f"❌ Bot hat Senderechte in {channel_id} verloren. Enferne aus Cache.") - if channel_id in self._cached_channels: - self._cached_channels.remove(channel_id) - return False - except Exception as e: - logger.error(f"❌ Unerwarteter Sendefehler in {channel_id}: {e}") - return False - - # Wenn alle Retries fehlschlagen - logger.error(f"❌ Senden nach {max_retries} Retries in {channel_id} fehlgeschlagen.") - return False - - except Exception as e: - logger.error(f"❌ Generischer Fehler im _send_to_channel: {e}", exc_info=True) - return False - - async def send_global_message(self, message: discord.Message, attachment_data: List[Tuple[str, bytes, str]] = None) -> Tuple[int, int]: - """Sendet eine Nachricht global an alle verbundenen Channels""" - settings = db.get_guild_settings(message.guild.id) - - embed, files_to_upload = await self.embed_builder.create_message_embed(message, settings, attachment_data) - - active_channels = await self._get_all_active_channels() - successful_sends = 0 - failed_sends = 0 - - # Berechne, wie viele Tasks gleichzeitig laufen sollen (z.B. 10) - tasks = [] - for channel_id in active_channels: - # Sende nicht an den Ursprungskanal zurück - if channel_id == message.channel.id: - continue - - tasks.append(self._send_to_channel(channel_id, embed, files_to_upload)) - - results = await asyncio.gather(*tasks, return_exceptions=True) - - for result in results: - if result is True: - successful_sends += 1 - else: - failed_sends += 1 - if isinstance(result, Exception): - logger.error(f"❌ Task-Fehler beim Senden: {result}") - - return successful_sends, failed_sends - - -class GlobalChatCog(ezcord.Cog): - """Haupt-Cog für das GlobalChat-System""" - - globalchat = SlashCommandGroup("globalchat", "GlobalChat Verwaltung") - - def __init__(self, bot): - self.bot = bot - self.config = GlobalChatConfig() - self.validator = MessageValidator(self.config) - self.embed_builder = EmbedBuilder(self.config, bot) - self.message_cooldown = commands.CooldownMapping.from_cooldown( - self.config.RATE_LIMIT_MESSAGES, - self.config.RATE_LIMIT_SECONDS, - commands.BucketType.user - ) - self._cached_channels: Optional[List[int]] = None - self.sender = GlobalChatSender(self.bot, self.config, self.embed_builder, self._cached_channels) - self.cleanup_task.start() - - @tasks.loop(hours=12) - async def cleanup_task(self): - """Task zur Bereinigung abgelaufener Blacklist-Einträge und Cache-Aktualisierung""" - # db.delete_expired_blacklist_entries() <--- DIESE ZEILE AUSKOMMENTIEREN - # logger.info("🗑️ GlobalChat: Abgelaufene Blacklist-Einträge bereinigt.") - - # Cache neu laden, um Änderungen in der DB zu sehen - self._cached_channels = await self.sender._fetch_all_channels() - logger.info("🧠 GlobalChat: Channel-Cache neu geladen.") - - @ezcord.Cog.listener() - async def on_message(self, message: discord.Message): - """Haupt-Listener für eingehende GlobalChat-Nachrichten""" - if not message.guild or message.author.bot: - return - - # Prüfen ob Channel ein GlobalChat-Channel ist - global_chat_channel_id = db.get_globalchat_channel(message.guild.id) - if message.channel.id != global_chat_channel_id: - return - - # Guild-Settings laden - settings = db.get_guild_settings(message.guild.id) - - # Message validieren - is_valid, reason = self.validator.validate_message(message, settings) - if not is_valid: - logger.debug(f"❌ Nachricht abgelehnt: {reason} (User: {message.author.id})") - - # User benachrichtigen bei bestimmten Gründen - if any(keyword in reason for keyword in ["Blacklist", "NSFW", "Gefilterte", "Ungültige Anhänge", "zu groß"]): - try: - await message.add_reaction("❌") - # Info-Nachricht für spezifische Fehler - if "Ungültige Anhänge" in reason or "zu groß" in reason: - info_msg = await message.reply( - f"❌ **Fehler:** {reason}\n" - f"**Max. Größe:** {self.config.MAX_FILE_SIZE_MB}MB pro Datei\n" - f"**Max. Anhänge:** {self.config.MAX_ATTACHMENTS}", - delete_after=7 - ) - await asyncio.sleep(2) - await message.delete() - except (discord.Forbidden, discord.NotFound): - pass # Kann Nachricht nicht löschen/reagieren - return - - # Rate Limiting prüfen - bucket = self.message_cooldown.get_bucket(message) - retry_after = bucket.update_rate_limit() - if retry_after: - try: - await message.add_reaction("⏰") - await asyncio.sleep(2) - await message.delete() - logger.debug(f"⏰ Nachricht von {message.author.id} wegen Rate Limit entfernt.") - except (discord.Forbidden, discord.NotFound): - pass - return - - # === Medien herunterladen (wenn vorhanden) === - attachment_data: List[Tuple[str, bytes, str]] = [] - if message.attachments: - try: - await message.channel.trigger_typing() - for attachment in message.attachments: - # Maximal 25MB (Discord-Limit) - if attachment.size <= self.config.MAX_FILE_SIZE_MB * 1024 * 1024: - data = await attachment.read() - attachment_data.append((attachment.filename, data, attachment.content_type)) - except Exception as e: - logger.error(f"❌ Fehler beim Herunterladen von Attachments: {e}") - # Wenn Download fehlschlägt, Nachricht trotzdem ohne Medien senden - attachment_data = [] - - # Nachricht senden - successful, failed = await self.sender.send_global_message(message, attachment_data) - - # Ursprüngliche Nachricht löschen, wenn Relaying erfolgreich war - if settings.get('delete_original', False): - try: - await message.delete() - except discord.Forbidden: - logger.warning(f"⚠️ Keine Permissions zum Löschen der Original-Nachricht in {message.channel.id}") - except discord.NotFound: - pass - - logger.info(f"🌍 GlobalChat: Nachricht von {message.guild.name} | User: {message.author.name} | ✅ {successful} | ❌ {failed}") - - - # ==================== Slash Commands ==================== - - @globalchat.command( - name="setup", - description="Richtet einen GlobalChat-Channel ein" - ) - async def setup_globalchat( - self, - ctx: discord.ApplicationContext, - channel: discord.TextChannel = Option(discord.TextChannel, "Der GlobalChat-Channel", required=True) - ): - """Setup-Command für GlobalChat""" - if not ctx.author.guild_permissions.manage_guild: - await ctx.respond("❌ Du benötigst die **Server verwalten** Berechtigung!", ephemeral=True) - return - - # Bot Permissions prüfen - bot_perms = channel.permissions_for(ctx.guild.me) - missing_perms = [] - if not bot_perms.send_messages: missing_perms.append("Nachrichten senden") - if not bot_perms.manage_messages: missing_perms.append("Nachrichten verwalten") - if not bot_perms.embed_links: missing_perms.append("Links einbetten") - if not bot_perms.read_message_history: missing_perms.append("Nachrichten-Historie lesen") - if not bot_perms.attach_files: missing_perms.append("Dateien anhängen") # Wichtig für Medien - - if missing_perms: - perms_list = "\n".join([f"• {p}" for p in missing_perms]) - await ctx.respond( - f"❌ Mir fehlen wichtige Berechtigungen in {channel.mention}:\n{perms_list}", - ephemeral=True - ) - return - - try: - db.set_globalchat_channel(ctx.guild.id, channel.id) - - # Cache aktualisieren - self._cached_channels = await self.sender._fetch_all_channels() - - # UI Container für eine schönere Antwort (falls vorhanden) - container = Container() - - status_text = f"✅ **GlobalChat eingerichtet!**\n\n" - status_text += f"Der GlobalChat ist nun in {channel.mention} aktiv.\n" - status_text += f"Aktuell verbunden: **{len(self._cached_channels)}** Server." - - container.add_text(status_text) - container.add_separator() - - # Feature-Liste - feature_text = ( - "**Unterstützte Features:**\n" - "• 🖼️ Bilder, 🎥 Videos, 🎵 Audio\n" - "• 📄 Dokumente (Office, PDF, Archive)\n" - "• 🎨 Discord Sticker\n" - "• 🔗 Automatische Link-Previews\n" - "• ↩️ Reply auf andere Nachrichten\n\n" - "**Nächste Schritte:**\n" - "• `/globalchat settings` - Einstellungen anpassen\n" - "• `/globalchat stats` - Statistiken anzeigen\n" - "• `/globalchat media-info` - Medien-Limits anzeigen" - ) - container.add_text(feature_text) - - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - - except Exception as e: - logger.error(f"❌ Setup-Fehler: {e}", exc_info=True) - await ctx.respond("❌ Ein Fehler ist aufgetreten!", ephemeral=True) - - @globalchat.command( - name="remove", - description="Entfernt den GlobalChat-Channel" - ) - async def remove_globalchat(self, ctx: discord.ApplicationContext): - """Entfernt GlobalChat vom Server""" - if not ctx.author.guild_permissions.manage_guild: - await ctx.respond("❌ Du benötigst die **Server verwalten** Berechtigung!", ephemeral=True) - return - - # Prüfen ob Channel existiert - channel_id = db.get_globalchat_channel(ctx.guild.id) - if not channel_id: - await ctx.respond("❌ GlobalChat ist auf diesem Server nicht eingerichtet.", ephemeral=True) - return - - try: - db.set_globalchat_channel(ctx.guild.id, None) - - # Cache aktualisieren - self._cached_channels = await self.sender._fetch_all_channels() - - await ctx.respond( - f"✅ **GlobalChat entfernt!**\n\n" - f"Der GlobalChat wurde von diesem Server entfernt.\n" - f"Es sind nun noch **{len(self._cached_channels)}** Server verbunden.", - ephemeral=True - ) - except Exception as e: - logger.error(f"❌ Remove-Fehler: {e}", exc_info=True) - await ctx.respond("❌ Ein Fehler ist aufgetreten!", ephemeral=True) - - @globalchat.command( - name="settings", - description="Verwaltet Server-spezifische GlobalChat-Einstellungen" - ) - async def settings_globalchat( - self, - ctx: discord.ApplicationContext, - filter_enabled: Optional[bool] = Option(bool, "Content-Filter aktivieren/deaktivieren (Invites, etc.)", required=False), - nsfw_filter: Optional[bool] = Option(bool, "NSFW-Filter aktivieren/deaktivieren", required=False), - embed_color: Optional[str] = Option(str, "Hex-Farbcode für Embeds (z.B. #FF00FF)", required=False), - max_message_length: Optional[int] = Option( - int, - "Maximale Nachrichtenlänge", - required=False, - min_value=50, - max_value=2000 - ) - ): - """Verwaltet Server-spezifische Einstellungen""" - if not ctx.author.guild_permissions.manage_guild: - await ctx.respond("❌ Du benötigst die **Server verwalten** Berechtigung!", ephemeral=True) - return - - # Prüfen ob GlobalChat aktiv - if not db.get_globalchat_channel(ctx.guild.id): - await ctx.respond( - "❌ Dieser Server nutzt GlobalChat nicht!\n" - "Nutze `/globalchat setup` zuerst.", - ephemeral=True - ) - return - - updated = [] - # Filter aktivieren/deaktivieren - if filter_enabled is not None: - if db.update_guild_setting(ctx.guild.id, 'filter_enabled', filter_enabled): - updated.append(f"Content-Filter: {'✅ An' if filter_enabled else '❌ Aus'}") - - if nsfw_filter is not None: - if db.update_guild_setting(ctx.guild.id, 'nsfw_filter', nsfw_filter): - updated.append(f"NSFW-Filter: {'✅ An' if nsfw_filter else '❌ Aus'}") - - if embed_color: - # Hex-Validierung - if not re.match(r'^#[0-9a-fA-F]{6}$', embed_color): - await ctx.respond("❌ Ungültiger Hex-Farbcode. Erwarte z.B. `#5865F2`.", ephemeral=True) - return - if db.update_guild_setting(ctx.guild.id, 'embed_color', embed_color): - updated.append(f"Embed-Farbe: `{embed_color}`") - - if max_message_length is not None: - if db.update_guild_setting(ctx.guild.id, 'max_message_length', max_message_length): - updated.append(f"Max. Länge: **{max_message_length}** Zeichen") - - if not updated: - await ctx.respond("ℹ️ Keine Änderungen vorgenommen.", ephemeral=True) - return - - # Erfolgs-Embed - embed = discord.Embed( - title="✅ GlobalChat Einstellungen aktualisiert", - description="\n".join(updated), - color=discord.Color.green() - ) - await ctx.respond(embed=embed, ephemeral=True) - - - @globalchat.command( - name="ban", - description="🔨 Bannt einen User oder Server vom GlobalChat" - ) - async def globalchat_ban( - self, - ctx: discord.ApplicationContext, - entity_id: str = Option(str, "ID des Users oder Servers (Guild-ID)", required=True), - entity_type: str = Option(str, "Typ der Entität", choices=["user", "guild"], required=True), - reason: str = Option(str, "Grund für den Ban", required=True), - duration: Optional[int] = Option(int, "Dauer in Stunden (optional, permanent wenn leer)", required=False) - ): - """Bannt eine Entität aus dem GlobalChat""" - if ctx.author.id not in self.config.BOT_OWNERS: - await ctx.respond("❌ Nur Bot-Owner können diesen Befehl nutzen.", ephemeral=True) - return - - try: - entity_id_int = int(entity_id) - except ValueError: - await ctx.respond("❌ Ungültige ID. Erwarte eine Zahl.", ephemeral=True) - return - - # Ban ausführen - try: - success = db.add_to_blacklist( - entity_type, - entity_id_int, - reason, - ctx.author.id, - duration - ) - if not success: - await ctx.respond("❌ Fehler beim Bannen!", ephemeral=True) - return - - # Success-Response - duration_text = f"{duration} Stunden" if duration else "Permanent" - embed = discord.Embed( - title="🔨 GlobalChat-Ban verhängt", - color=discord.Color.red(), - timestamp=datetime.utcnow() - ) - embed.add_field(name="Typ", value=entity_type.title(), inline=True) - embed.add_field(name="ID", value=f"`{entity_id_int}`", inline=True) - embed.add_field(name="Dauer", value=duration_text, inline=True) - embed.add_field(name="Grund", value=reason, inline=False) - embed.add_field(name="Von", value=ctx.author.mention, inline=True) - - if duration: - expires = datetime.utcnow() + timedelta(hours=duration) - embed.add_field( - name="Läuft ab", - value=f"", - inline=True - ) - - await ctx.respond(embed=embed) - logger.info( - f"🔨 Ban: {entity_type} {entity_id_int} | Grund: {reason} | Dauer: {duration_text} | Von: {ctx.author.id}" - ) - - except Exception as e: - logger.error(f"❌ Ban-Fehler: {e}", exc_info=True) - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Bannen!", ephemeral=True) - - - @globalchat.command( - name="unban", - description="🔓 Entfernt einen User oder Server von der GlobalChat-Blacklist" - ) - async def globalchat_unban( - self, - ctx: discord.ApplicationContext, - entity_id: str = Option(str, "ID des Users oder Servers", required=True), - entity_type: str = Option(str, "Typ der Entität", choices=["user", "guild"], required=True) - ): - """Entfernt eine Entität von der GlobalChat Blacklist""" - if ctx.author.id not in self.config.BOT_OWNERS: - await ctx.respond("❌ Nur Bot-Owner können diesen Befehl nutzen.", ephemeral=True) - return - - try: - entity_id_int = int(entity_id) - except ValueError: - await ctx.respond("❌ Ungültige ID. Erwarte eine Zahl.", ephemeral=True) - return - - try: - if not db.is_blacklisted(entity_type, entity_id_int): - await ctx.respond(f"ℹ️ {entity_type.title()} `{entity_id_int}` ist nicht auf der Blacklist.", ephemeral=True) - return - - if db.remove_from_blacklist(entity_type, entity_id_int): - embed = discord.Embed( - title="🔓 GlobalChat-Unban erfolgreich", - description=f"{entity_type.title()} mit ID `{entity_id_int}` wurde von der Blacklist entfernt.", - color=discord.Color.green(), - timestamp=datetime.utcnow() - ) - await ctx.respond(embed=embed) - logger.info(f"🔓 Unban: {entity_type} {entity_id_int} | Von: {ctx.author.id}") - else: - await ctx.respond("❌ Fehler beim Entfernen von der Blacklist!", ephemeral=True) - - except Exception as e: - logger.error(f"❌ Unban-Fehler: {e}", exc_info=True) - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Unbannen!", ephemeral=True) - - - @globalchat.command( - name="info", - description="Zeigt Informationen über den GlobalChat" - ) - async def globalchat_info(self, ctx: discord.ApplicationContext): - """Zeigt allgemeine Informationen""" - active_servers = await self.sender._get_all_active_channels() - - embed = discord.Embed( - title="🌍 GlobalChat - Vollständiger Medien-Support", - description=( - "Ein serverübergreifendes Chat-System mit vollständigem Medien-Support.\n\n" - f"**📊 Aktuell verbunden:** **{len(active_servers)}** Server\n\n" - "**🎯 Hauptfeatures:**\n" - "• Nachrichten werden an alle verbundenen Server gesendet\n" - "• Vollständiger Medien-Support (Bilder, Videos, Audio, Dokumente)\n" - "• Discord Sticker und Link-Previews\n" - "• Reply-Unterstützung mit Kontext\n" - "• Automatische Moderation und Filter\n" - "• Rate-Limiting gegen Spam\n" - "• Individuelle Server-Einstellungen" - ), - color=discord.Color.blue(), - timestamp=datetime.utcnow() - ) - - embed.add_field( - name="📁 Unterstützte Medien (Details: `/globalchat media-info`)", - value=( - "• 🖼️ Bilder\n" - "• 🎥 Videos\n" - "• 🎵 Audio\n" - "• 📄 Dokumente (PDF, Office, Archive)" - ), - inline=True - ) - - embed.add_field( - name="🛡️ Moderation", - value=( - f"• **Content-Filter:** {db.get_guild_settings(ctx.guild.id).get('filter_enabled', True) and '✅ An' or '❌ Aus'}\n" - f"• **NSFW-Filter:** {db.get_guild_settings(ctx.guild.id).get('nsfw_filter', True) and '✅ An' or '❌ Aus'}\n" - f"• **Nachrichtenlänge:** {db.get_guild_settings(ctx.guild.id).get('max_message_length', self.config.DEFAULT_MAX_MESSAGE_LENGTH)} Zeichen\n" - ), - inline=True - ) - - await ctx.respond(embed=embed, ephemeral=True) - - @globalchat.command( - name="stats", - description="Zeigt GlobalChat-Statistiken" - ) - async def globalchat_stats(self, ctx: discord.ApplicationContext): - """Zeigt Statistiken (z.B. Blacklist-Einträge)""" - if ctx.author.id not in self.config.BOT_OWNERS: - await ctx.respond("❌ Nur Bot-Owner können diesen Befehl nutzen.", ephemeral=True) - return - - user_bans, guild_bans = db.get_blacklist_stats() - active_servers = await self.sender._get_all_active_channels() - - embed = discord.Embed( - title="📊 GlobalChat System-Statistiken", - color=discord.Color.gold(), - timestamp=datetime.utcnow() - ) - - embed.add_field(name="🌍 Verbundene Server", value=f"**{len(active_servers)}**", inline=True) - embed.add_field(name="👥 Gebannte User", value=f"**{user_bans}**", inline=True) - embed.add_field(name="🛡️ Gebannte Server", value=f"**{guild_bans}**", inline=True) - embed.add_field(name="⏳ Cache-Dauer", value=f"{self.config.CACHE_DURATION} Sekunden", inline=True) - embed.add_field(name="📜 Protokoll Bereinigung", value=f"Alle {self.config.CLEANUP_DAYS} Tage", inline=True) - embed.add_field( - name="⏰ Rate-Limit", - value=f"{self.config.RATE_LIMIT_MESSAGES} Nachrichten / {self.config.RATE_LIMIT_SECONDS} Sekunden", - inline=True - ) - - await ctx.respond(embed=embed, ephemeral=True) - - - @globalchat.command( - name="media-info", - description="Zeigt Details zu Medien-Limits und erlaubten Formaten" - ) - async def globalchat_media_info(self, ctx: discord.ApplicationContext): - """Zeigt Medien-Limits und unterstützte Formate""" - embed = discord.Embed( - title="📁 GlobalChat Medien-Limits & Formate", - description="Details zu den maximal erlaubten Dateigrößen und unterstützten Formaten.", - color=discord.Color.purple(), - timestamp=datetime.utcnow() - ) - - # Limits - embed.add_field( - name="⚠️ Wichtige Limits", - value=( - f"• **Max. {self.config.MAX_ATTACHMENTS} Anhänge** pro Nachricht\n" - f"• **Max. {self.config.MAX_FILE_SIZE_MB} MB** pro Datei (Discord-Limit)\n" - f"• **Max. {self.config.DEFAULT_MAX_MESSAGE_LENGTH} Zeichen** Textlänge\n" - f"• **Rate-Limit:** {self.config.RATE_LIMIT_MESSAGES} Nachrichten pro {self.config.RATE_LIMIT_SECONDS} Sekunden" - ), - inline=False - ) - - # Unterstützte Formate - embed.add_field( - name="🖼️ Bilder", - value=", ".join(self.config.ALLOWED_IMAGE_FORMATS).upper(), - inline=True - ) - embed.add_field( - name="🎥 Videos", - value=", ".join(self.config.ALLOWED_VIDEO_FORMATS).upper(), - inline=True - ) - embed.add_field( - name="🎵 Audio", - value=", ".join(self.config.ALLOWED_AUDIO_FORMATS).upper(), - inline=True - ) - embed.add_field( - name="📄 Dokumente/Archive", - value=", ".join(self.config.ALLOWED_DOCUMENT_FORMATS).upper(), - inline=False - ) - - await ctx.respond(embed=embed, ephemeral=True) - - - @globalchat.command( - name="help", - description="Zeigt die Hilfe-Seite für GlobalChat" - ) - async def globalchat_help(self, ctx: discord.ApplicationContext): - """Zeigt eine Übersicht aller verfügbaren Commands und Features.""" - embed = discord.Embed( - title="❓ GlobalChat Hilfe & Übersicht", - description="Übersicht aller verfügbaren Commands und Features.", - color=discord.Color.blue(), - timestamp=datetime.utcnow() - ) - - # Setup & Verwaltung - embed.add_field( - name="⚙️ Setup & Verwaltung", - value=( - "`/globalchat setup` - Channel einrichten\n" - "`/globalchat remove` - Channel entfernen\n" - "`/globalchat settings` - Einstellungen anpassen" - ), - inline=False - ) - - # Informationen - embed.add_field( - name="📊 Informationen", - value=( - "`/globalchat info` - Allgemeine Infos\n" - "`/globalchat stats` - Statistiken anzeigen\n" - "`/globalchat media-info` - Medien-Details\n" - "`/globalchat help` - Diese Hilfe" - ), - inline=False - ) - - # Moderation (Admin) - Nur für Bot Owner - if ctx.author.id in self.config.BOT_OWNERS: - embed.add_field( - name="🛡️ Moderation (Bot Owner)", - value=( - "`/globalchat ban` - User/Server bannen\n" - "`/globalchat unban` - User/Server entbannen" - ), - inline=False - ) - - # Test & Debug (Admin) - if ctx.author.id in self.config.BOT_OWNERS: - embed.add_field( - name="🧪 Test & Debug (Bot Owner)", - value=( - "`/globalchat test-media` - Medien-Test\n" - "`/globalchat broadcast` - Nachricht an alle senden\n" - "`/globalchat reload-cache` - Cache neu laden\n" - "`/globalchat debug` - Debug-Info" - ), - inline=False - ) - - await ctx.respond(embed=embed, ephemeral=True) - - - @globalchat.command( - name="test-media", - description="🧪 Test-Command für Medien-Upload und -Anzeige" - ) - async def globalchat_test_media(self, ctx: discord.ApplicationContext): - """Zeigt Anweisungen für den Medien-Test""" - channel_id = db.get_globalchat_channel(ctx.guild.id) - if not channel_id: - await ctx.respond("❌ GlobalChat ist auf diesem Server nicht eingerichtet.", ephemeral=True) - return - - embed = discord.Embed( - title="🧪 GlobalChat Medien-Test", - description=( - "Dieser Test zeigt dir, welche Medien-Typen erfolgreich übermittelt werden können.\n\n" - "**Unterstützte Medien:**\n" - "• Bilder, Videos, Audio, Dokumente\n" - "• Discord Sticker\n" - "• Antworten auf andere Nachrichten\n\n" - "**So testest du:**\n" - f"1. Gehe zu <#{channel_id}> und sende eine Nachricht mit Anhängen.\n" - "2. Die Nachricht erscheint auf allen verbundenen Servern.\n\n" - "Probiere verschiedene Kombinationen aus! (Mehrere Dateien, Sticker + Text, Reply + Datei)" - ), - color=discord.Color.green(), - timestamp=datetime.utcnow() - ) - - embed.add_field( - name="📊 Aktuelle Limits", - value=( - f"• Max. {self.config.MAX_ATTACHMENTS} Anhänge\n" - f"• Max. {self.config.MAX_FILE_SIZE_MB} MB pro Datei\n" - f"• {self.config.RATE_LIMIT_MESSAGES} Nachrichten / {self.config.RATE_LIMIT_SECONDS} Sekunden" - ), - inline=True - ) - - embed.add_field( - name="✅ Unterstützte Formate", - value=( - "Bilder, Videos, Audio,\n" - "Dokumente, Archive,\n" - "Office-Dateien, PDFs" - ), - inline=True - ) - - embed.set_footer(text=f"Test von {ctx.author}", icon_url=ctx.author.display_avatar.url) - - await ctx.respond(embed=embed, ephemeral=True) - - - @globalchat.command( - name="broadcast", - description="📢 Sendet eine Nachricht an alle verbundenen GlobalChat-Server" - ) - async def globalchat_broadcast( - self, - ctx: discord.ApplicationContext, - title: str = Option(str, "Der Titel der Broadcast-Nachricht", required=True), - message: str = Option(str, "Die Nachricht selbst", required=True) - ): - """Sendet einen Broadcast (nur Bot Owner)""" - if ctx.author.id not in self.config.BOT_OWNERS: - await ctx.respond("❌ Nur Bot-Owner können diesen Befehl nutzen.", ephemeral=True) - return - - await ctx.defer(ephemeral=True) - - try: - # Broadcast Embed erstellen - embed = discord.Embed( - title=f"📢 GlobalChat Broadcast: {title}", - description=message, - color=discord.Color.red(), - timestamp=datetime.utcnow() - ) - embed.set_footer( - text=f"GlobalChat Broadcast von {ctx.author}", - icon_url=ctx.author.display_avatar.url - ) - - # An alle Channels senden - successful, failed = await self.sender.send_global_broadcast_message(embed) # Annahme: Eine separate Broadcast-Methode in Sender - - # Response - result_embed = discord.Embed( - title="✅ Broadcast gesendet", - color=discord.Color.green(), - timestamp=datetime.utcnow() - ) - result_embed.add_field( - name="📊 Ergebnis", - value=( - f"**Erfolgreich:** {successful}\n" - f"**Fehlgeschlagen:** {failed}\n" - f"**Gesamt:** {successful + failed}" - ), - inline=False - ) - result_embed.add_field( - name="📝 Nachricht", - value=f"**{title}**\n{message[:100]}{'...' if len(message) > 100 else ''}", - inline=False - ) - await ctx.respond(embed=result_embed, ephemeral=True) - logger.info( - f"📢 Broadcast: '{title}' | Von: {ctx.author} | " - f"✅ {successful} | ❌ {failed}" - ) - except Exception as e: - logger.error(f"❌ Broadcast-Fehler: {e}", exc_info=True) - await ctx.respond("❌ Fehler beim Senden des Broadcasts!", ephemeral=True) - - @globalchat.command( - name="reload-cache", - description="🧠 Lädt alle Cache-Daten neu (Admin)" - ) - async def globalchat_reload_cache(self, ctx: discord.ApplicationContext): - """Lädt den Channel-Cache neu (Bot Owner)""" - if ctx.author.id not in self.config.BOT_OWNERS: - await ctx.respond("❌ Nur Bot-Owner können diesen Befehl nutzen.", ephemeral=True) - return - - await ctx.defer(ephemeral=True) - try: - old_count = len(self._cached_channels or []) - self._cached_channels = await self.sender._fetch_all_channels() - new_count = len(self._cached_channels) - - await ctx.respond( - f"✅ **Cache neu geladen!**\n\n" - f"Alte Channel-Anzahl: **{old_count}**\n" - f"Neue Channel-Anzahl: **{new_count}**", - ephemeral=True - ) - logger.info(f"🧠 GlobalChat Cache manuell neu geladen. {old_count} -> {new_count}") - - except Exception as e: - logger.error(f"❌ Cache Reload Fehler: {e}", exc_info=True) - await ctx.respond("❌ Ein Fehler ist aufgetreten!", ephemeral=True) - - - @globalchat.command( - name="debug", - description="🐛 Zeigt Debug-Informationen an (Admin)" - ) - async def globalchat_debug(self, ctx: discord.ApplicationContext): - """Zeigt Debug-Informationen (Bot Owner)""" - if ctx.author.id not in self.config.BOT_OWNERS: - await ctx.respond("❌ Nur Bot-Owner können diesen Befehl nutzen.", ephemeral=True) - return - - await ctx.defer(ephemeral=True) - try: - cached_channels = len(self._cached_channels or []) - all_settings = db.get_all_guild_settings() - - debug_info = ( - f"**Bot-Status:**\n" - f"• Latency: `{round(self.bot.latency * 1000)}ms`\n" - f"• Guilds: `{len(self.bot.guilds)}`\n" - f"• Uptime: ``\n\n" - f"**GlobalChat-Status:**\n" - f"• Aktive Channels (Cache): `{cached_channels}`\n" - f"• DB Settings Einträge: `{len(all_settings)}`\n" - f"• Cleanup Task: `{'Aktiv' if self.cleanup_task.is_running() else 'Inaktiv'}`\n" - ) - - # Beispiel für Blacklist-Info - user_bans, guild_bans = db.get_blacklist_stats() - debug_info += ( - f"• Gebannte User/Server: `{user_bans} / {guild_bans}`" - ) - - embed = discord.Embed( - title="🐛 GlobalChat Debug-Informationen", - description=debug_info, - color=discord.Color.orange(), - timestamp=datetime.utcnow() - ) - await ctx.respond(embed=embed, ephemeral=True) - except Exception as e: - logger.error(f"❌ Debug Fehler: {e}", exc_info=True) - await ctx.respond("❌ Ein Fehler ist aufgetreten!", ephemeral=True) - - -# ==================== Setup Funktion ==================== -def setup(bot): - """Setup-Funktion für the cog when loaded by classic...""" - # Stelle sicher, dass die Datenbank initialisiert wird, falls nicht schon geschehen - GlobalChatDatabase().create_tables() - # Füge die Cog hinzu - bot.add_cog(GlobalChatCog(bot)) \ No newline at end of file diff --git a/src/cogs/Servermanament/levelsystem.py b/src/cogs/Servermanament/levelsystem.py deleted file mode 100644 index e669b69..0000000 --- a/src/cogs/Servermanament/levelsystem.py +++ /dev/null @@ -1,974 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -import discord -from discord import SlashCommandGroup, Option -from discord.ext import commands, tasks -import time -import random -from DevTools import LevelDatabase -import asyncio -import io -import csv -from typing import Optional -from discord.ui import Container - - -class PrestigeConfirmView(discord.ui.View): - def __init__(self, db, user, guild): - super().__init__(timeout=300) - self.db = db - self.user = user - self.guild = guild - - @discord.ui.button(label="Bestätigen", style=discord.ButtonStyle.danger, emoji="⚠️") - async def confirm_prestige(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user != self.user: - await interaction.response.send_message("Nur der User kann sein eigenes Prestige bestätigen!", ephemeral=True) - return - - success = self.db.prestige_user(self.user.id, self.guild.id) - if success: - embed = discord.Embed( - title="✨ Prestige erfolgreich!", - description=f"{self.user.mention} hat ein Prestige durchgeführt!\nDu startest wieder bei Level 0, aber behältst deinen Prestige-Rang!", - color=0xff69b4 - ) - embed.set_footer(text="Gratulation zu deinem Prestige!") - else: - embed = discord.Embed( - title="❌ Prestige fehlgeschlagen", - description="Prestige konnte nicht durchgeführt werden. Möglicherweise erfüllst du nicht die Anforderungen.", - color=0xff0000 - ) - - await interaction.response.edit_message(embed=embed, view=None) - - @discord.ui.button(label="Abbrechen", style=discord.ButtonStyle.secondary) - async def cancel_prestige(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user != self.user: - await interaction.response.send_message("Nur der User kann seine eigene Aktion abbrechen!", ephemeral=True) - return - - embed = discord.Embed( - title="❌ Prestige abgebrochen", - description="Das Prestige wurde abgebrochen.", - color=0x999999 - ) - await interaction.response.edit_message(embed=embed, view=None) - - -class LevelSystem(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.db = LevelDatabase() - self.xp_cooldowns = {} # User-ID -> Timestamp - - # Starte Background Tasks - self.cleanup_expired_boosts.start() - self.cleanup_temporary_roles.start() - - def cog_unload(self): - """Cleanup beim Entladen der Cog""" - self.cleanup_expired_boosts.cancel() - self.cleanup_temporary_roles.cancel() - - levelsystem = SlashCommandGroup("levelsystem", "Verwalte das Levelsystem") - levelrole = SlashCommandGroup("levelrole", "Verwalte Level-Rollen") - xpboost = SlashCommandGroup("xpboost", "Verwalte XP-Boosts") - levelconfig = SlashCommandGroup("levelconfig", "Konfiguriere das Levelsystem") - - @tasks.loop(hours=1) - async def cleanup_expired_boosts(self): - """Entfernt abgelaufene XP-Boosts""" - # Hier würde die DB-Cleanup Logik implementiert werden - pass - - @tasks.loop(hours=1) - async def cleanup_temporary_roles(self): - """Entfernt abgelaufene temporäre Rollen""" - # Hier würde die temporäre Rollen Cleanup Logik implementiert werden - pass - - def create_level_up_embed(self, user: discord.Member, level: int, is_role_reward: bool = False, role: Optional[discord.Role] = None): - """Erstellt ein verbessertes Level-Up Embed""" - embed = discord.Embed(color=0x00ff00) - embed.set_author(name="🎉 Level Up!", icon_url=user.avatar.url if user.avatar else user.default_avatar.url) - embed.description = f"**{user.mention}** erreichte **Level {level}**!" - - if is_role_reward and role: - embed.add_field(name="🏆 Neue Rolle erhalten", value=f"**{role.name}**", inline=False) - embed.color = 0xffff00 - - embed.set_thumbnail(url=user.avatar.url if user.avatar else user.default_avatar.url) - return embed - - @commands.Cog.listener() - async def on_message(self, message): - # Ignoriere Bot-Nachrichten - if message.author.bot: - return - - # Nur in Servern, nicht in DMs - if message.guild is None: - return - - # Prüfe ob Levelsystem aktiviert ist - if not self.db.is_levelsystem_enabled(message.guild.id): - return - - # Prüfe ob Kanal auf Blacklist steht - if self.db.is_channel_blacklisted(message.guild.id, message.channel.id): - return - - user_id = message.author.id - guild_id = message.guild.id - current_time = time.time() - - # Guild-Konfiguration holen - config = self.db.get_guild_config(guild_id) - cooldown = config.get('cooldown', 30) - - # XP-Cooldown prüfen - if user_id in self.xp_cooldowns: - if current_time - self.xp_cooldowns[user_id] < cooldown: - return - - # Kanal-spezifischen Multiplikator anwenden - channel_multiplier = self.db.get_channel_multiplier(guild_id, message.channel.id) - - # XP berechnen - min_xp = config.get('min_xp', 10) - max_xp = config.get('max_xp', 20) - base_xp = random.randint(min_xp, max_xp) - final_xp = int(base_xp * channel_multiplier) - - # XP hinzufügen mit Anti-Spam Protection - level_up, new_level = self.db.add_xp(user_id, guild_id, final_xp, message.content) - - if not level_up and new_level == 0: - return # Anti-Spam blockierte die XP - - # Cooldown setzen - self.xp_cooldowns[user_id] = current_time - - # Level Up Behandlung - if level_up: - # Bestimme Zielkanal für Level-Up Nachrichten - target_channel = message.channel - level_up_channel_id = config.get('level_up_channel') - - if level_up_channel_id: - level_up_channel = message.guild.get_channel(level_up_channel_id) - if level_up_channel: - target_channel = level_up_channel - - embed = self.create_level_up_embed(message.author, new_level) - await target_channel.send(embed=embed) - - # Level-Rolle vergeben - role_id = self.db.get_role_for_level(guild_id, new_level) - if role_id: - role = message.guild.get_role(role_id) - if role: - try: - await message.author.add_roles(role, reason=f"Level {new_level} erreicht") - role_embed = discord.Embed( - title="🏆 Neue Rolle erhalten!", - description=f"{message.author.mention} hat die Rolle **{role.name}** erhalten!", - color=0xffff00 - ) - role_embed.set_thumbnail(url=message.author.avatar.url if message.author.avatar else message.author.default_avatar.url) - await target_channel.send(embed=role_embed) - except discord.Forbidden: - # Log oder Nachricht an Admins falls Bot keine Berechtigung hat - pass - - @levelsystem.command(description="Zeigt das Server-Leaderboard mit Paginierung") - async def leaderboard(self, ctx, - anzahl: discord.Option(int, "Anzahl der User", default=10, min_value=1, max_value=50)): - if not self.db.is_levelsystem_enabled(ctx.guild.id): - embed = discord.Embed( - title="❌ Levelsystem deaktiviert", - description="Das Levelsystem ist auf diesem Server deaktiviert.", - color=0xff0000 - ) - await ctx.respond(embed=embed) - return - - leaderboard_data = self.db.get_leaderboard(ctx.guild.id, anzahl) - - if not leaderboard_data: - embed = discord.Embed( - title="📊 Leaderboard", - description="Noch keine User im Leaderboard!", - color=0x0099ff - ) - await ctx.respond(embed=embed) - return - - embed = discord.Embed( - title=f"📊 Leaderboard - Top {len(leaderboard_data)}", - color=0x0099ff, - timestamp=discord.utils.utcnow() - ) - - description = "" - for i, (user_id, xp, level, messages, prestige) in enumerate(leaderboard_data, 1): - user = self.bot.get_user(user_id) - username = user.display_name if user else f"User {user_id}" - - if i == 1: - medal = "🥇" - elif i == 2: - medal = "🥈" - elif i == 3: - medal = "🥉" - else: - medal = f"**{i}.**" - - prestige_text = f"⭐{prestige} " if prestige > 0 else "" - description += f"{medal} {prestige_text}**{username}** - Level {level} ({xp:,} XP)\n" - - embed.description = description - embed.set_footer(text=f"Server: {ctx.guild.name}") - - await ctx.respond(embed=embed) - - @levelsystem.command(description="Zeigt erweiterte Benutzerstatistiken") - async def profil(self, ctx, - user: discord.Option(discord.Member, "User dessen Profil angezeigt werden soll", default=None)): - if not self.db.is_levelsystem_enabled(ctx.guild.id): - embed = discord.Embed( - title="❌ Levelsystem deaktiviert", - description="Das Levelsystem ist auf diesem Server deaktiviert.", - color=0xff0000 - ) - await ctx.respond(embed=embed) - return - - target_user = user or ctx.author - user_stats = self.db.get_user_stats(target_user.id, ctx.guild.id) - - if not user_stats: - embed = discord.Embed( - title="❌ Kein Profil gefunden", - description=f"{target_user.display_name} hat noch keine XP gesammelt!", - color=0xff0000 - ) - await ctx.respond(embed=embed) - return - - xp, level, messages, xp_needed, prestige, total_earned = user_stats - rank = self.db.get_user_rank(target_user.id, ctx.guild.id) - - embed = discord.Embed( - title=f"📊 Profil von {target_user.display_name}", - color=target_user.color or 0x0099ff, - timestamp=discord.utils.utcnow() - ) - - # Erste Zeile - embed.add_field(name="🏆 Level", value=str(level), inline=True) - embed.add_field(name="⭐ XP", value=f"{xp:,}", inline=True) - embed.add_field(name="📈 Rang", value=f"#{rank}", inline=True) - - # Zweite Zeile - embed.add_field(name="💬 Nachrichten", value=f"{messages:,}", inline=True) - embed.add_field(name="🎯 XP bis nächstes Level", value=f"{xp_needed:,}", inline=True) - - if prestige > 0: - embed.add_field(name="✨ Prestige", value=f"⭐{prestige}", inline=True) - - # Dritte Zeile - embed.add_field(name="💰 Gesamt verdiente XP", value=f"{total_earned:,}", inline=True) - - # XP pro Nachricht berechnen - xp_per_msg = total_earned / messages if messages > 0 else 0 - embed.add_field(name="📊 Ø XP/Nachricht", value=f"{xp_per_msg:.1f}", inline=True) - - # Aktiver XP-Multiplikator - multiplier = self.db.get_active_xp_multiplier(ctx.guild.id, target_user.id) - if multiplier > 1.0: - embed.add_field(name="🚀 Aktiver Boost", value=f"{multiplier}x", inline=True) - - # Fortschrittsbalken - current_level_xp = xp - self.db.xp_for_level(level) - next_level_xp = self.db.xp_for_level(level + 1) - self.db.xp_for_level(level) - progress = current_level_xp / next_level_xp if next_level_xp > 0 else 1 - - progress_bar = "█" * int(progress * 15) + "░" * (15 - int(progress * 15)) - embed.add_field( - name="📊 Level-Fortschritt", - value=f"`{progress_bar}` {progress * 100:.1f}%\n`{current_level_xp:,} / {next_level_xp:,} XP`", - inline=False - ) - - embed.set_thumbnail(url=target_user.avatar.url if target_user.avatar else target_user.default_avatar.url) - embed.set_footer(text=f"Server: {ctx.guild.name}") - - await ctx.respond(embed=embed) - - @levelsystem.command(description="Führt ein Prestige durch (Level 50+)") - async def prestige(self, ctx): - if not self.db.is_levelsystem_enabled(ctx.guild.id): - embed = discord.Embed( - title="❌ Levelsystem deaktiviert", - description="Das Levelsystem ist auf diesem Server deaktiviert.", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - config = self.db.get_guild_config(ctx.guild.id) - if not config.get('prestige_enabled', True): - embed = discord.Embed( - title="❌ Prestige deaktiviert", - description="Das Prestige-System ist auf diesem Server deaktiviert.", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - user_stats = self.db.get_user_stats(ctx.author.id, ctx.guild.id) - min_level = config.get('prestige_min_level', 50) - - if not user_stats or user_stats[1] < min_level: - embed = discord.Embed( - title="❌ Prestige nicht verfügbar", - description=f"Du musst mindestens Level {min_level} erreichen!", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - # Bestätigung erforderlich - view = PrestigeConfirmView(self.db, ctx.author, ctx.guild) - embed = discord.Embed( - title="⚠️ Prestige Bestätigung", - description=f"Möchtest du wirklich dein Level zurücksetzen?\n\n**Was passiert:**\n• Dein Level wird auf 0 zurückgesetzt\n• Deine XP werden auf 0 zurückgesetzt\n• Du erhältst einen Prestige-Rang (⭐)\n• Du behältst deine Nachrichten-Anzahl\n\n**Aktuelles Level:** {user_stats[1]}", - color=0xffff00 - ) - embed.set_footer(text="Diese Aktion kann nicht rückgängig gemacht werden!") - await ctx.respond(embed=embed, view=view, ephemeral=True) - - @levelsystem.command(description="Zeigt erweiterte Server-Analytics") - async def analytics(self, ctx): - if not self.db.is_levelsystem_enabled(ctx.guild.id): - embed = discord.Embed( - title="❌ Levelsystem deaktiviert", - description="Das Levelsystem ist auf diesem Server deaktiviert.", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - analytics = self.db.get_detailed_analytics(ctx.guild.id) - - embed = discord.Embed( - title="📊 Server Analytics", - color=0x0099ff, - timestamp=discord.utils.utcnow() - ) - - # Grundlegende Statistiken - embed.add_field(name="👥 Aktive User", value=f"{analytics['total_users']:,}", inline=True) - embed.add_field(name="📈 Durchschnittslevel", value=f"{analytics['avg_level']:.1f}", inline=True) - embed.add_field(name="🏆 Höchstes Level", value=f"{analytics['max_level']}", inline=True) - - embed.add_field(name="⚡ Gesamt XP", value=f"{analytics['total_xp']:,}", inline=True) - embed.add_field(name="💬 Gesamt Nachrichten", value=f"{analytics['total_messages']:,}", inline=True) - embed.add_field(name="🕒 Heute aktiv", value=f"{analytics['active_today']}", inline=True) - - # Level-Verteilung - distribution = analytics['level_distribution'] - embed.add_field( - name="📊 Level-Verteilung", - value=f"🌱 Anfänger (1-10): {distribution['novice']}\n" - f"📚 Fortgeschrittene (11-25): {distribution['intermediate']}\n" - f"🎯 Experten (26-50): {distribution['advanced']}\n" - f"👑 Meister (50+): {distribution['expert']}", - inline=False - ) - - embed.set_footer(text=f"Server: {ctx.guild.name}") - await ctx.respond(embed=embed) - - @levelsystem.command(description="Exportiert Leveldaten als CSV") - @commands.has_permissions(administrator=True) - async def export_data(self, ctx): - if not self.db.is_levelsystem_enabled(ctx.guild.id): - embed = discord.Embed( - title="❌ Levelsystem deaktiviert", - description="Das Levelsystem ist auf diesem Server deaktiviert.", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - await ctx.defer(ephemeral=True) - - data = self.db.export_guild_data(ctx.guild.id) - - output = io.StringIO() - writer = csv.writer(output) - writer.writerow(['User ID', 'Username', 'Level', 'XP', 'Messages', 'Prestige', 'Total XP Earned']) - - for row in data: - user_id, xp, level, messages, prestige, total_earned = row - user = self.bot.get_user(user_id) - username = user.display_name if user else "Unbekannt" - writer.writerow([user_id, username, level, xp, messages, prestige, total_earned]) - - file_content = output.getvalue().encode('utf-8') - file = discord.File(io.BytesIO(file_content), filename=f"leveldata_{ctx.guild.id}_{int(time.time())}.csv") - - embed = discord.Embed( - title="✅ Datenexport erfolgreich", - description=f"Daten von {len(data)} Usern exportiert.", - color=0x00ff00 - ) - - await ctx.followup.send(embed=embed, file=file) - - # Level-Rollen Commands - @levelrole.command(description="Fügt eine Level-Rolle hinzu") - @commands.has_permissions(manage_roles=True) - async def add(self, ctx, - level: discord.Option(int, "Level für die Rolle", min_value=1), - rolle: discord.Option(discord.Role, "Die Rolle die vergeben werden soll"), - temporaer: discord.Option(bool, "Temporäre Rolle", default=False), - dauer_stunden: discord.Option(int, "Dauer in Stunden (nur bei temporären Rollen)", default=0)): - - if rolle.position >= ctx.author.top_role.position and ctx.author != ctx.guild.owner: - embed = discord.Embed( - title="❌ Keine Berechtigung", - description="Du kannst keine Rolle hinzufügen, die höher oder gleich deiner höchsten Rolle ist!", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - if rolle.position >= ctx.guild.me.top_role.position: - embed = discord.Embed( - title="❌ Bot-Berechtigung fehlt", - description="Ich kann diese Rolle nicht vergeben, da sie höher oder gleich meiner höchsten Rolle ist!", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - if temporaer and dauer_stunden <= 0: - embed = discord.Embed( - title="❌ Ungültige Dauer", - description="Temporäre Rollen benötigen eine Dauer > 0 Stunden!", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - self.db.add_level_role(ctx.guild.id, level, rolle.id, temporaer, dauer_stunden) - - temp_text = f" (temporär für {dauer_stunden}h)" if temporaer else "" - embed = discord.Embed( - title="✅ Level-Rolle hinzugefügt", - description=f"Die Rolle **{rolle.name}** wird nun bei **Level {level}**{temp_text} vergeben!", - color=0x00ff00 - ) - await ctx.respond(embed=embed) - - @levelrole.command(description="Fügt mehrere Rollen für ein Level hinzu") - @commands.has_permissions(manage_roles=True) - async def add_multiple(self, ctx, level: int, *roles: discord.Role): - if not roles: - await ctx.respond("Du musst mindestens eine Rolle angeben!", ephemeral=True) - return - - added_roles = [] - failed_roles = [] - - for role in roles: - if role.position >= ctx.author.top_role.position and ctx.author != ctx.guild.owner: - failed_roles.append(f"{role.name} (keine Berechtigung)") - continue - - if role.position >= ctx.guild.me.top_role.position: - failed_roles.append(f"{role.name} (Bot-Berechtigung fehlt)") - continue - - self.db.add_level_role(ctx.guild.id, level, role.id) - added_roles.append(role.name) - - embed = discord.Embed(color=0x00ff00 if added_roles else 0xff0000) - - if added_roles: - embed.title = "✅ Level-Rollen hinzugefügt" - embed.description = f"**Level {level}:** {', '.join(added_roles)}" - - if failed_roles: - if added_roles: - embed.add_field(name="❌ Fehlgeschlagen", value='\n'.join(failed_roles), inline=False) - else: - embed.title = "❌ Keine Rollen hinzugefügt" - embed.description = '\n'.join(failed_roles) - - await ctx.respond(embed=embed) - - @levelrole.command(description="Bearbeitet eine bestehende Level-Rolle") - @commands.has_permissions(manage_roles=True) - async def edit(self, ctx, - level: discord.Option(int, "Level der zu bearbeitenden Rolle", min_value=1), - neue_rolle: discord.Option(discord.Role, "Die neue Rolle")): - # Prüfen ob Level-Rolle existiert - level_roles = self.db.get_level_roles(ctx.guild.id) - if not any(l == level for l, r, t, d in level_roles): - embed = discord.Embed( - title="❌ Level-Rolle nicht gefunden", - description=f"Für Level {level} ist keine Rolle konfiguriert!", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - if neue_rolle.position >= ctx.author.top_role.position and ctx.author != ctx.guild.owner: - embed = discord.Embed( - title="❌ Keine Berechtigung", - description="Du kannst keine Rolle setzen, die höher oder gleich deiner höchsten Rolle ist!", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - if neue_rolle.position >= ctx.guild.me.top_role.position: - embed = discord.Embed( - title="❌ Bot-Berechtigung fehlt", - description="Ich kann diese Rolle nicht vergeben, da sie höher oder gleich meiner höchsten Rolle ist!", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - self.db.add_level_role(ctx.guild.id, level, neue_rolle.id) - - embed = discord.Embed( - title="✅ Level-Rolle bearbeitet", - description=f"Die Rolle für **Level {level}** wurde zu **{neue_rolle.name}** geändert!", - color=0x00ff00 - ) - await ctx.respond(embed=embed) - - @levelrole.command(description="Entfernt eine Level-Rolle") - @commands.has_permissions(manage_roles=True) - async def remove(self, ctx, level: discord.Option(int, "Level der zu entfernenden Rolle", min_value=1)): - # Prüfen ob Level-Rolle existiert - level_roles = self.db.get_level_roles(ctx.guild.id) - if not any(l == level for l, r, t, d in level_roles): - embed = discord.Embed( - title="❌ Level-Rolle nicht gefunden", - description=f"Für Level {level} ist keine Rolle konfiguriert!", - color=0xff0000 - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - self.db.remove_level_role(ctx.guild.id, level) - - embed = discord.Embed( - title="✅ Level-Rolle entfernt", - description=f"Die Level-Rolle für **Level {level}** wurde entfernt!", - color=0x00ff00 - ) - await ctx.respond(embed=embed) - - @levelrole.command(description="Zeigt alle konfigurierten Level-Rollen") - async def list(self, ctx): - level_roles = self.db.get_level_roles(ctx.guild.id) - - if not level_roles: - embed = discord.Embed( - title="📝 Level-Rollen", - description="Keine Level-Rollen konfiguriert!", - color=0x0099ff - ) - await ctx.respond(embed=embed) - return - - embed = discord.Embed( - title="📝 Level-Rollen", - color=0x0099ff, - timestamp=discord.utils.utcnow() - ) - - description = "" - for level, role_id, is_temp, duration in level_roles: - role = ctx.guild.get_role(role_id) - role_name = role.name if role else f"Gelöschte Rolle ({role_id})" - - temp_text = f" ⏰({duration}h)" if is_temp else "" - description += f"**Level {level}:** {role_name}{temp_text}\n" - - embed.description = description - embed.set_footer(text=f"Server: {ctx.guild.name}") - - await ctx.respond(embed=embed) - - # XP-Boost Commands - @xpboost.command(description="Fügt einen globalen XP-Boost hinzu") - @commands.has_permissions(manage_guild=True) - async def add_global(self, ctx, - multiplier: discord.Option(float, "XP-Multiplikator", min_value=1.1, max_value=5.0), - dauer_stunden: discord.Option(int, "Dauer in Stunden", min_value=1, max_value=168)): - - self.db.add_xp_boost(ctx.guild.id, None, multiplier, dauer_stunden) - - embed = discord.Embed( - title="🚀 Globaler XP-Boost aktiviert", - description=f"**{multiplier}x** XP-Multiplikator für **{dauer_stunden} Stunden**\n" - f"Alle Server-Mitglieder erhalten mehr XP!", - color=0x00ff00 - ) - embed.set_footer(text="Der Boost ist sofort aktiv!") - await ctx.respond(embed=embed) - - @xpboost.command(description="Fügt einen persönlichen XP-Boost hinzu") - @commands.has_permissions(manage_guild=True) - async def add_user(self, ctx, - user: discord.Option(discord.Member, "Benutzer für den Boost"), - multiplier: discord.Option(float, "XP-Multiplikator", min_value=1.1, max_value=5.0), - dauer_stunden: discord.Option(int, "Dauer in Stunden", min_value=1, max_value=168)): - - self.db.add_xp_boost(ctx.guild.id, user.id, multiplier, dauer_stunden) - - embed = discord.Embed( - title="🚀 Persönlicher XP-Boost aktiviert", - description=f"**{user.mention}** erhält **{multiplier}x** XP für **{dauer_stunden} Stunden**!", - color=0x00ff00 - ) - embed.set_footer(text="Der Boost ist sofort aktiv!") - await ctx.respond(embed=embed) - - # Konfiguration Commands - @levelconfig.command(description="Konfiguriert XP-Einstellungen") - @commands.has_permissions(manage_guild=True) - async def xp_settings(self, ctx, - min_xp: discord.Option(int, "Minimum XP pro Nachricht", default=None, min_value=1, max_value=50), - max_xp: discord.Option(int, "Maximum XP pro Nachricht", default=None, min_value=1, max_value=100), - cooldown: discord.Option(int, "Cooldown in Sekunden", default=None, min_value=5, max_value=300)): - - config_updates = {} - if min_xp is not None: - config_updates['min_xp'] = min_xp - if max_xp is not None: - config_updates['max_xp'] = max_xp - if cooldown is not None: - config_updates['xp_cooldown'] = cooldown - - if max_xp and min_xp and max_xp < min_xp: - await ctx.respond("❌ Maximum XP kann nicht kleiner als Minimum XP sein!", ephemeral=True) - return - - if not config_updates: - await ctx.respond("❌ Du musst mindestens einen Wert ändern!", ephemeral=True) - return - - self.db.set_guild_config(ctx.guild.id, **config_updates) - - current_config = self.db.get_guild_config(ctx.guild.id) - - embed = discord.Embed( - title="✅ XP-Einstellungen aktualisiert", - color=0x00ff00 - ) - - embed.add_field(name="💰 XP-Bereich", value=f"{current_config['min_xp']}-{current_config['max_xp']}", inline=True) - embed.add_field(name="⏱️ Cooldown", value=f"{current_config['xp_cooldown']}s", inline=True) - - await ctx.respond(embed=embed) - - @levelconfig.command(description="Setzt XP-Multiplikator für einen Kanal") - @commands.has_permissions(manage_guild=True) - async def channel_multiplier(self, ctx, - channel: discord.Option(discord.TextChannel, "Kanal"), - multiplier: discord.Option(float, "Multiplikator (0.0 = keine XP)", min_value=0.0, max_value=5.0)): - - self.db.set_channel_multiplier(ctx.guild.id, channel.id, multiplier) - - if multiplier == 0: - description = f"{channel.mention} gibt keine XP mehr." - color = 0xff0000 - else: - description = f"{channel.mention} hat **{multiplier}x** XP-Multiplikator." - color = 0x00ff00 - - embed = discord.Embed( - title="✅ Kanal-Multiplikator gesetzt", - description=description, - color=color - ) - await ctx.respond(embed=embed) - - @levelconfig.command(description="Fügt einen Kanal zur XP-Blacklist hinzu") - @commands.has_permissions(manage_guild=True) - async def blacklist_channel(self, ctx, - channel: discord.Option(discord.TextChannel, "Kanal zum Ausschließen")): - - self.db.add_blacklisted_channel(ctx.guild.id, channel.id) - - embed = discord.Embed( - title="✅ Kanal ausgeschlossen", - description=f"{channel.mention} wurde vom Levelsystem ausgeschlossen.", - color=0x00ff00 - ) - await ctx.respond(embed=embed) - - @levelconfig.command(description="Setzt den Level-Up Nachrichten-Kanal") - @commands.has_permissions(manage_guild=True) - async def levelup_channel(self, ctx, - channel: discord.Option(discord.TextChannel, "Kanal für Level-Up Nachrichten", default=None)): - - if channel: - self.db.set_guild_config(ctx.guild.id, level_up_channel=channel.id) - embed = discord.Embed( - title="✅ Level-Up Kanal gesetzt", - description=f"Level-Up Nachrichten werden in {channel.mention} gesendet.", - color=0x00ff00 - ) - else: - self.db.set_guild_config(ctx.guild.id, level_up_channel=None) - embed = discord.Embed( - title="✅ Level-Up Kanal zurückgesetzt", - description="Level-Up Nachrichten werden wieder im ursprünglichen Kanal gesendet.", - color=0x00ff00 - ) - - await ctx.respond(embed=embed) - - @levelconfig.command(description="Konfiguriert Prestige-Einstellungen") - @commands.has_permissions(manage_guild=True) - async def prestige_settings(self, ctx, - aktiviert: discord.Option(bool, "Prestige-System aktivieren/deaktivieren"), - min_level: discord.Option(int, "Minimum Level für Prestige", default=50, min_value=10, max_value=200)): - - self.db.set_guild_config(ctx.guild.id, prestige_enabled=aktiviert, prestige_min_level=min_level) - - embed = discord.Embed( - title="✅ Prestige-Einstellungen aktualisiert", - color=0x00ff00 - ) - - status = "aktiviert" if aktiviert else "deaktiviert" - embed.add_field(name="✨ Status", value=status.title(), inline=True) - if aktiviert: - embed.add_field(name="🎯 Minimum Level", value=str(min_level), inline=True) - - await ctx.respond(embed=embed) - - # System Commands - @levelsystem.command(description="Aktiviert das Levelsystem") - @commands.has_permissions(manage_guild=True) - async def enable(self, ctx): - if self.db.is_levelsystem_enabled(ctx.guild.id): - embed = discord.Embed( - title="ℹ️ Bereits aktiviert", - description="Das Levelsystem ist bereits aktiviert!", - color=0x0099ff - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - self.db.set_levelsystem_enabled(ctx.guild.id, True) - - embed = discord.Embed( - title="✅ Levelsystem aktiviert", - description="Das Levelsystem wurde erfolgreich aktiviert!\n\nBenutze `/levelconfig` um weitere Einstellungen vorzunehmen.", - color=0x00ff00 - ) - await ctx.respond(embed=embed) - - @levelsystem.command(description="Deaktiviert das Levelsystem") - @commands.has_permissions(manage_guild=True) - async def disable(self, ctx): - if not self.db.is_levelsystem_enabled(ctx.guild.id): - embed = discord.Embed( - title="ℹ️ Bereits deaktiviert", - description="Das Levelsystem ist bereits deaktiviert!", - color=0x0099ff - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - self.db.set_levelsystem_enabled(ctx.guild.id, False) - - embed = discord.Embed( - title="✅ Levelsystem deaktiviert", - description="Das Levelsystem wurde erfolgreich deaktiviert!\n\n*Hinweis: Alle Daten bleiben erhalten und können bei Reaktivierung wiederhergestellt werden.*", - color=0x00ff00 - ) - await ctx.respond(embed=embed) - - @levelsystem.command(description="Zeigt den detaillierten Status des Levelsystems") - async def status(self, ctx): - enabled = self.db.is_levelsystem_enabled(ctx.guild.id) - config = self.db.get_guild_config(ctx.guild.id) - - embed = discord.Embed( - title="📊 Levelsystem Status", - description=f"Das Levelsystem ist **{'aktiviert' if enabled else 'deaktiviert'}**", - color=0x00ff00 if enabled else 0xff0000, - timestamp=discord.utils.utcnow() - ) - - if enabled: - # Grundkonfiguration - embed.add_field( - name="⚙️ Konfiguration", - value=f"**XP-Bereich:** {config['min_xp']}-{config['max_xp']}\n" - f"**Cooldown:** {config['xp_cooldown']}s\n" - f"**Prestige:** {'✅' if config['prestige_enabled'] else '❌'} (Level {config['prestige_min_level']}+)", - inline=True - ) - - # Statistiken - leaderboard = self.db.get_leaderboard(ctx.guild.id, 1) - level_roles = self.db.get_level_roles(ctx.guild.id) - total_users = len(self.db.get_leaderboard(ctx.guild.id, 1000)) - - embed.add_field( - name="📈 Statistiken", - value=f"**Aktive User:** {total_users:,}\n" - f"**Level-Rollen:** {len(level_roles)}\n" - f"**XP-Boosts:** Aktiv", - inline=True - ) - - if leaderboard: - top_user = self.bot.get_user(leaderboard[0][0]) - top_username = top_user.display_name if top_user else f"User {leaderboard[0][0]}" - prestige_text = f"⭐{leaderboard[0][4]} " if leaderboard[0][4] > 0 else "" - - embed.add_field( - name="👑 Top User", - value=f"{prestige_text}**{top_username}**\nLevel {leaderboard[0][2]} ({leaderboard[0][1]:,} XP)", - inline=True - ) - - # Level-Up Kanal - if config['level_up_channel']: - channel = ctx.guild.get_channel(config['level_up_channel']) - channel_text = channel.mention if channel else "Gelöschter Kanal" - else: - channel_text = "Standard (Nachrichtenkanal)" - - embed.add_field(name="📢 Level-Up Kanal", value=channel_text, inline=True) - - embed.set_footer(text=f"Server: {ctx.guild.name}") - await ctx.respond(embed=embed) - - # Admin Commands - @levelsystem.command(description="Setzt das Level eines Users (Admin)") - @commands.has_permissions(administrator=True) - async def set_level(self, ctx, - user: discord.Option(discord.Member, "Benutzer"), - level: discord.Option(int, "Neues Level", min_value=0, max_value=1000)): - - required_xp = self.db.xp_for_level(level) - - # User in Datenbank erstellen/aktualisieren - conn = self.db.db_path - import sqlite3 - conn = sqlite3.connect(self.db.db_path) - cursor = conn.cursor() - - cursor.execute(''' - INSERT OR REPLACE INTO user_levels (user_id, guild_id, xp, level, messages, last_message, total_xp_earned) - VALUES (?, ?, ?, ?, - COALESCE((SELECT messages FROM user_levels WHERE user_id = ? AND guild_id = ?), 0), - ?, - COALESCE((SELECT total_xp_earned FROM user_levels WHERE user_id = ? AND guild_id = ?), 0) + ?) - ''', (user.id, ctx.guild.id, required_xp, level, user.id, ctx.guild.id, time.time(), user.id, ctx.guild.id, required_xp)) - - conn.commit() - conn.close() - - embed = discord.Embed( - title="✅ Level gesetzt", - description=f"{user.mention} ist jetzt **Level {level}** ({required_xp:,} XP)", - color=0x00ff00 - ) - await ctx.respond(embed=embed) - - @levelsystem.command(description="Fügt einem User XP hinzu (Admin)") - @commands.has_permissions(administrator=True) - async def add_xp(self, ctx, - user: discord.Option(discord.Member, "Benutzer"), - xp_amount: discord.Option(int, "XP-Menge", min_value=1, max_value=100000)): - - level_up, new_level = self.db.add_xp(user.id, ctx.guild.id, xp_amount, "Admin XP Grant") - - embed = discord.Embed( - title="✅ XP hinzugefügt", - description=f"{user.mention} hat **{xp_amount:,} XP** erhalten!", - color=0x00ff00 - ) - - if level_up: - embed.add_field(name="🎉 Level Up!", value=f"Neues Level: **{new_level}**", inline=False) - - await ctx.respond(embed=embed) - - @levelsystem.command(description="Setzt die Nachrichten-Anzahl eines Users (Admin)") - @commands.has_permissions(administrator=True) - async def set_messages(self, ctx, - user: discord.Option(discord.Member, "Benutzer"), - messages: discord.Option(int, "Anzahl Nachrichten", min_value=0, max_value=1000000)): - - import sqlite3 - conn = sqlite3.connect(self.db.db_path) - cursor = conn.cursor() - - cursor.execute(''' - UPDATE user_levels SET messages = ? - WHERE user_id = ? AND guild_id = ? - ''', (messages, user.id, ctx.guild.id)) - - conn.commit() - conn.close() - - embed = discord.Embed( - title="✅ Nachrichten-Anzahl gesetzt", - description=f"{user.mention} hat jetzt **{messages:,} Nachrichten**", - color=0x00ff00 - ) - await ctx.respond(embed=embed) - - @levelsystem.command(description="Löscht die Leveldaten eines Users (Admin)") - @commands.has_permissions(administrator=True) - async def reset_user(self, ctx, - user: discord.Option(discord.Member, "Benutzer zum Zurücksetzen")): - - import sqlite3 - conn = sqlite3.connect(self.db.db_path) - cursor = conn.cursor() - - cursor.execute('DELETE FROM user_levels WHERE user_id = ? AND guild_id = ?', (user.id, ctx.guild.id)) - - affected_rows = cursor.rowcount - conn.commit() - conn.close() - - if affected_rows > 0: - embed = discord.Embed( - title="✅ User zurückgesetzt", - description=f"Alle Leveldaten von {user.mention} wurden gelöscht.", - color=0x00ff00 - ) - else: - embed = discord.Embed( - title="ℹ️ Keine Daten gefunden", - description=f"{user.mention} hat keine Leveldaten auf diesem Server.", - color=0x0099ff - ) - - await ctx.respond(embed=embed, ephemeral=True) - - -def setup(bot): - bot.add_cog(LevelSystem(bot)) \ No newline at end of file diff --git a/src/cogs/Servermanament/logging.py b/src/cogs/Servermanament/logging.py deleted file mode 100644 index 818a863..0000000 --- a/src/cogs/Servermanament/logging.py +++ /dev/null @@ -1,1465 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# File: logging_cog.py - -import discord -from discord import SlashCommandGroup -from discord.ext import commands -from datetime import datetime, timedelta -from typing import Dict, Set, Optional, List -import asyncio -import logging - -# Import our separate database class -from DevTools import LoggingDatabase - -# Setup logging -logger = logging.getLogger(__name__) - -class LoggingCog(commands.Cog): - """ - Comprehensive Discord logging system with improved performance and features - """ - - def __init__(self, bot): - self.bot = bot - self.db = LoggingDatabase() - - # Improved caching system - self._edit_tasks: Dict[int, asyncio.Task] = {} - self._bulk_deletes: Dict[int, Dict[str, any]] = {} - self._voice_cache: Dict[int, Dict[int, Optional[discord.VoiceState]]] = {} - - # Configuration - self.config = { - 'edit_debounce_time': 3.0, # Sekunden - 'bulk_delete_threshold': 3, # Anzahl für Bulk-Erkennung - 'bulk_delete_window': 2.0, # Sekunden Zeitfenster - 'max_content_length': 1500, # Max Content-Länge in Embeds - 'max_embed_fields': 25, # Discord Limit - 'cleanup_interval': 300, # 5 Minuten Cache-Cleanup - 'max_attachment_display': 5, # Max Attachments in Embed - 'max_role_display': 10, # Max Roles in Embed - } - - # Performance tracking - self._stats = { - 'events_processed': 0, - 'logs_sent': 0, - 'errors': 0, - 'cache_hits': 0, - 'startup_time': datetime.utcnow(), - } - - # Start background tasks - self._cleanup_task = None - self.bot.loop.create_task(self._start_background_tasks()) - - logger.info("LoggingCog initialized successfully") - - async def _start_background_tasks(self): - """Startet Background-Tasks nachdem der Bot bereit ist""" - await self.bot.wait_until_ready() - self._cleanup_task = self.bot.loop.create_task(self._cleanup_loop()) - logger.info("Background tasks started") - - def cog_unload(self): - """Cleanup beim Entladen der Cog""" - logger.info("Unloading LoggingCog...") - - if self._cleanup_task and not self._cleanup_task.done(): - self._cleanup_task.cancel() - - # Cancel all edit tasks - for task in self._edit_tasks.values(): - if not task.done(): - task.cancel() - - # Close database connection - self.db.close() - logger.info("LoggingCog unloaded successfully") - - async def _cleanup_loop(self): - """Regelmäßige Cache-Bereinigung mit verbesserter Logik""" - while not self.bot.is_closed(): - try: - await asyncio.sleep(self.config['cleanup_interval']) - await self._cleanup_caches() - except asyncio.CancelledError: - logger.info("Cleanup loop cancelled") - break - except Exception as e: - logger.error(f"Cleanup loop error: {e}") - self._stats['errors'] += 1 - - async def _cleanup_caches(self): - """Bereinigt alle Caches""" - try: - cleanup_count = 0 - - # Edit Tasks bereinigen - completed_tasks = [ - msg_id for msg_id, task in self._edit_tasks.items() - if task.done() - ] - for msg_id in completed_tasks: - del self._edit_tasks[msg_id] - cleanup_count += 1 - - # Bulk Delete Cache bereinigen (älter als 5 Minuten) - current_time = datetime.utcnow() - expired_guilds = [] - - for guild_id, data in self._bulk_deletes.items(): - if 'timestamp' in data: - age = (current_time - data['timestamp']).total_seconds() - if age > 300: # 5 Minuten - expired_guilds.append(guild_id) - - for guild_id in expired_guilds: - del self._bulk_deletes[guild_id] - cleanup_count += 1 - - # Voice Cache für offline Mitglieder bereinigen - for guild_id in list(self._voice_cache.keys()): - guild = self.bot.get_guild(guild_id) - if not guild: - del self._voice_cache[guild_id] - cleanup_count += 1 - continue - - offline_members = [] - for member_id in self._voice_cache[guild_id]: - member = guild.get_member(member_id) - if not member or not member.voice: - offline_members.append(member_id) - - for member_id in offline_members: - del self._voice_cache[guild_id][member_id] - cleanup_count += 1 - - if cleanup_count > 0: - logger.debug(f"Cache cleanup: {cleanup_count} items removed") - - except Exception as e: - logger.error(f"Cache cleanup error: {e}") - self._stats['errors'] += 1 - - async def send_log(self, guild_id: int, embed: discord.Embed, log_type: str = "general") -> bool: - """Verbesserte Log-Versendung mit Retry-Logik""" - try: - channel_id = await self.db.get_log_channel(guild_id, log_type) - if not channel_id: - return False - - channel = self.bot.get_channel(channel_id) - if not channel: - # Channel nicht mehr vorhanden, aus DB entfernen - await self.db.remove_log_channel(guild_id, log_type) - logger.warning(f"Removed invalid channel {channel_id} for guild {guild_id}") - return False - - # Embed validieren und anpassen - if len(embed) > 6000: # Discord Limit - embed.description = "⚠️ Inhalt zu lang für Anzeige" - # Felder reduzieren falls nötig - while len(embed.fields) > 10: - embed.remove_field(-1) - - # Embed senden - await channel.send(embed=embed) - self._stats['logs_sent'] += 1 - return True - - except discord.Forbidden: - logger.warning(f"No permission for log channel in guild {guild_id}") - await self.db.remove_log_channel(guild_id, log_type) - return False - except discord.NotFound: - logger.warning(f"Log channel not found in guild {guild_id}") - await self.db.remove_log_channel(guild_id, log_type) - return False - except discord.HTTPException as e: - if e.code == 50035: # Invalid form body - logger.error(f"Invalid embed content for guild {guild_id}: {e}") - # Fallback embed senden - try: - fallback_embed = discord.Embed( - title="⚠️ Log-Fehler", - description="Originale Log-Nachricht konnte nicht angezeigt werden (zu lang oder ungültig)", - color=discord.Color.orange(), - timestamp=datetime.utcnow() - ) - await channel.send(embed=fallback_embed) - except: - pass - else: - logger.error(f"HTTP error sending log to guild {guild_id}: {e}") - except Exception as e: - logger.error(f"Unexpected error sending log to guild {guild_id}: {e}") - self._stats['errors'] += 1 - - return False - - def _create_user_embed(self, title: str, user: discord.User, color: discord.Color, - extra_fields: Dict[str, str] = None, - description: str = None) -> discord.Embed: - """Verbesserte User-Embed Erstellung""" - embed = discord.Embed( - title=title, - description=description, - color=color, - timestamp=datetime.utcnow() - ) - - # User Info - immer als erstes - embed.add_field( - name="👤 User", - value=f"{user.mention}\n`{user}`", - inline=True - ) - embed.add_field( - name="🆔 ID", - value=f"`{user.id}`", - inline=True - ) - embed.add_field( - name="📅 Erstellt", - value=f"", - inline=True - ) - - # Extra Felder hinzufügen - if extra_fields: - for name, value in extra_fields.items(): - if len(embed.fields) < self.config['max_embed_fields']: - embed.add_field(name=name, value=str(value)[:1000], inline=True) - - # Avatar und Footer - if user.display_avatar: - embed.set_thumbnail(url=user.display_avatar.url) - - embed.set_footer(text=f"User ID: {user.id}") - return embed - - def _truncate_content(self, content: str, max_length: int = None) -> str: - """Kürzt Content intelligent""" - if not content: - return "*Leer*" - - max_length = max_length or self.config['max_content_length'] - - if len(content) <= max_length: - return content - - # An Wort-Grenzen kürzen wenn möglich - truncated = content[:max_length-3] - last_space = truncated.rfind(' ') - - if last_space > max_length * 0.8: # Nur wenn nicht zu viel verloren geht - truncated = truncated[:last_space] - - return f"{truncated}..." - - def _format_content_for_embed(self, content: str, escape_markdown: bool = True) -> str: - """Formatiert Content sicher für Embeds""" - if not content: - return "*Leer*" - - content = self._truncate_content(content) - - if escape_markdown: - # Escape problematische Zeichen - content = content.replace("```", "'''") - content = content.replace("`", "'") - - return f"```\n{content}\n```" - - # ============================================================================= - # SLASH COMMANDS - Improved - # ============================================================================= - - logging = SlashCommandGroup("logging", description="Setze die Logging Systeme") - - @logging.command(name="channel", description="Setzt den Log-Channel für verschiedene Events") - @discord.default_permissions(administrator=True) - async def set_log_channel(self, ctx, - channel: discord.TextChannel, - log_type: discord.Option(str, - choices=["general", "moderation", "voice", "messages", "all"], - description="Art der Logs", - default="general")): - """Verbesserte Log-Channel Konfiguration""" - try: - # Berechtigungen prüfen - perms = channel.permissions_for(ctx.guild.me) - if not perms.send_messages: - embed = discord.Embed( - title="❌ Keine Berechtigung", - description=f"Ich kann keine Nachrichten in {channel.mention} senden.", - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - if not perms.embed_links: - embed = discord.Embed( - title="⚠️ Fehlende Berechtigung", - description=f"Ich benötige die 'Embed Links' Berechtigung in {channel.mention}.", - color=discord.Color.orange() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - if log_type == "all": - # Alle Log-Typen setzen - types = ["general", "moderation", "voice", "messages"] - for lt in types: - await self.db.set_log_channel(ctx.guild.id, channel.id, lt) - - embed = discord.Embed( - title="✅ Alle Log-Channels gesetzt", - description=f"Alle Logs werden nun in {channel.mention} gesendet.\n\n" + - f"**Konfigurierte Typen:** {', '.join(types)}", - color=discord.Color.green() - ) - else: - await self.db.set_log_channel(ctx.guild.id, channel.id, log_type) - - embed = discord.Embed( - title="✅ Log-Channel gesetzt", - description=f"**{log_type.title()}**-Logs werden nun in {channel.mention} gesendet.", - color=discord.Color.green() - ) - - embed.set_footer(text=f"Konfiguriert von {ctx.author}") - await ctx.respond(embed=embed, ephemeral=True) - - # Test-Nachricht senden - test_embed = discord.Embed( - title="🧪 Test-Nachricht", - description=f"Log-Channel für **{log_type}** erfolgreich konfiguriert!", - color=discord.Color.blue(), - timestamp=datetime.utcnow() - ) - test_embed.set_footer(text="Dies ist eine Test-Nachricht") - await self.send_log(ctx.guild.id, test_embed, "general" if log_type == "all" else log_type) - - except Exception as e: - embed = discord.Embed( - title="❌ Fehler", - description=f"Fehler beim Setzen des Log-Channels:\n```{str(e)}```", - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - logger.error(f"Error in set_log_channel: {e}") - - @logging.command(name="remove", description="Entfernt einen Log-Channel") - @discord.default_permissions(administrator=True) - async def remove_log_channel(self, ctx, - log_type: discord.Option(str, - choices=["general", "moderation", "voice", "messages", "all"], - description="Art der Logs", default="all")): - """Entfernt Log-Channel Konfiguration""" - try: - if log_type == "all": - deleted_count = await self.db.remove_all_log_channels(ctx.guild.id) - description = f"Alle Log-Channels wurden entfernt. ({deleted_count} Einträge)" - else: - deleted_count = await self.db.remove_log_channel(ctx.guild.id, log_type) - if deleted_count > 0: - description = f"{log_type.title()}-Logging wurde deaktiviert." - else: - description = f"Kein {log_type.title()}-Logging war konfiguriert." - - embed = discord.Embed( - title="🗑️ Log-Channel entfernt", - description=description, - color=discord.Color.red(), - timestamp=datetime.utcnow() - ) - embed.set_footer(text=f"Entfernt von {ctx.author}") - await ctx.respond(embed=embed, ephemeral=True) - - except Exception as e: - embed = discord.Embed( - title="❌ Fehler", - description=f"Fehler beim Entfernen des Log-Channels:\n```{str(e)}```", - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - logger.error(f"Error in remove_log_channel: {e}") - - @logging.command(name="status", description="Zeigt die aktuellen Log-Einstellungen") - @discord.default_permissions(administrator=True) - async def log_status(self, ctx): - """Verbesserter Log-Status mit mehr Details""" - try: - channels = await self.db.get_all_log_channels(ctx.guild.id) - stats = await self.db.get_statistics() - - embed = discord.Embed( - title="📊 Logging Status", - color=discord.Color.blue(), - timestamp=datetime.utcnow() - ) - - if not channels: - embed.description = "❌ Keine Log-Channels konfiguriert." - embed.add_field( - name="💡 Tipp", - value="Nutze `/setlogchannel` um Logging zu aktivieren.", - inline=False - ) - else: - status_text = f"✅ **{len(channels)}** Log-Typ(en) konfiguriert\n\n" - - for log_type, channel_id in channels.items(): - channel = self.bot.get_channel(channel_id) - if channel: - status_text += f"**{log_type.title()}:** {channel.mention}\n" - else: - status_text += f"**{log_type.title()}:** ❌ *Channel nicht gefunden* (`{channel_id}`)\n" - - embed.description = status_text - - # Bot Statistiken - uptime = datetime.utcnow() - self._stats['startup_time'] - uptime_str = f"{uptime.days}d {uptime.seconds//3600}h {(uptime.seconds%3600)//60}m" - - embed.add_field( - name="📈 Cog Statistiken", - value=f"Events verarbeitet: **{self._stats['events_processed']:,}**\n" + - f"Logs gesendet: **{self._stats['logs_sent']:,}**\n" + - f"Fehler: **{self._stats['errors']}**\n" + - f"Uptime: **{uptime_str}**", - inline=True - ) - - # Cache Info - voice_cache_size = sum(len(vc) for vc in self._voice_cache.values()) - embed.add_field( - name="🗄️ Cache Status", - value=f"Edit Tasks: **{len(self._edit_tasks)}**\n" + - f"Bulk Deletes: **{len(self._bulk_deletes)}**\n" + - f"Voice Cache: **{voice_cache_size}**", - inline=True - ) - - # Datenbank Statistiken - if stats: - embed.add_field( - name="🗃️ Datenbank", - value=f"Aktive Channels: **{stats.get('enabled_entries', 0)}**\n" + - f"Guilds mit Logging: **{stats.get('unique_guilds', 0)}**\n" + - f"Einzigartige Channels: **{stats.get('unique_channels', 0)}**", - inline=True - ) - - embed.set_footer(text=f"Guild ID: {ctx.guild.id}") - await ctx.respond(embed=embed, ephemeral=True) - - except Exception as e: - embed = discord.Embed( - title="❌ Fehler", - description=f"Fehler beim Abrufen des Status:\n```{str(e)}```", - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - logger.error(f"Error in log_status: {e}") - - @logging.command(name="backup", description="Erstellt ein Backup der Log-Konfiguration") - @discord.default_permissions(administrator=True) - async def log_backup(self, ctx): - """Erstellt ein Datenbank-Backup""" - try: - backup_path = f"data/log_channels_backup_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.db" - success = await self.db.backup_database(backup_path) - - if success: - embed = discord.Embed( - title="✅ Backup erstellt", - description=f"Datenbank-Backup wurde erfolgreich erstellt:\n`{backup_path}`", - color=discord.Color.green() - ) - else: - embed = discord.Embed( - title="❌ Backup fehlgeschlagen", - description="Backup konnte nicht erstellt werden. Prüfe die Logs für Details.", - color=discord.Color.red() - ) - - await ctx.respond(embed=embed, ephemeral=True) - - except Exception as e: - embed = discord.Embed( - title="❌ Fehler", - description=f"Fehler beim Backup:\n```{str(e)}```", - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - logger.error(f"Error in log_backup: {e}") - - # ============================================================================= - # EVENT HANDLERS - Enhanced - # ============================================================================= - - @commands.Cog.listener() - async def on_member_join(self, member: discord.Member): - """Verbessertes Member Join Logging""" - try: - self._stats['events_processed'] += 1 - - account_age = datetime.utcnow() - member.created_at - age_days = account_age.days - - # Verdächtigkeits-Score - suspicious_factors = [] - if age_days < 1: - suspicious_factors.append("Sehr neues Konto (< 1 Tag)") - elif age_days < 7: - suspicious_factors.append("Neues Konto (< 7 Tage)") - - if member.display_avatar.is_default(): - suspicious_factors.append("Standard Avatar") - - # Default Username Pattern - if len(member.name) > 10 and member.discriminator != "0": - if member.name.lower().startswith(("discord", "user", "member")): - suspicious_factors.append("Verdächtiger Username") - - # Farbe basierend auf Verdächtigkeits-Level - if member.bot: - color = discord.Color.purple() - elif len(suspicious_factors) >= 2: - color = discord.Color.red() - elif suspicious_factors: - color = discord.Color.orange() - else: - color = discord.Color.green() - - extra_fields = { - "🎂 Konto-Alter": f"{age_days} Tag{'e' if age_days != 1 else ''}", - "👥 Member #": f"{member.guild.member_count}", - } - - if suspicious_factors: - extra_fields["⚠️ Verdächtig"] = "\n".join(suspicious_factors[:3]) - - if member.bot: - extra_fields["🤖 Bot"] = "✅" - - embed = self._create_user_embed( - "📥 Member beigetreten", - member, - color, - extra_fields - ) - - await self.send_log(member.guild.id, embed, "general") - - except Exception as e: - logger.error(f"Error in on_member_join: {e}") - self._stats['errors'] += 1 - - @commands.Cog.listener() - async def on_member_remove(self, member: discord.Member): - """Verbessertes Member Leave Logging""" - try: - self._stats['events_processed'] += 1 - - extra_fields = { - "🎭 Rollen": f"{len(member.roles) - 1}", # -1 für @everyone - "👥 Member #": f"{member.guild.member_count}", - } - - if member.joined_at: - duration = datetime.utcnow() - member.joined_at - days = duration.days - hours = duration.seconds // 3600 - - if days > 0: - duration_str = f"{days} Tag{'e' if days != 1 else ''}" - elif hours > 0: - duration_str = f"{hours} Stunde{'n' if hours != 1 else ''}" - else: - minutes = duration.seconds // 60 - duration_str = f"{minutes} Minute{'n' if minutes != 1 else ''}" - - extra_fields["⏱️ Mitgliedschaftsdauer"] = duration_str - - # Top Rollen anzeigen (nicht @everyone) - top_roles = [role for role in member.roles if role.name != "@everyone"] - if top_roles: - top_roles = sorted(top_roles, key=lambda r: r.position, reverse=True)[:3] - extra_fields["🏆 Top Rollen"] = ", ".join([role.name for role in top_roles]) - - embed = self._create_user_embed( - "📤 Member verlassen", - member, - discord.Color.red(), - extra_fields - ) - - await self.send_log(member.guild.id, embed, "general") - - except Exception as e: - logger.error(f"Error in on_member_remove: {e}") - self._stats['errors'] += 1 - - @commands.Cog.listener() - async def on_message_delete(self, message: discord.Message): - """Stark verbessertes Message Delete Logging mit Bulk-Detection""" - try: - if message.author.bot or not message.guild: - return - - self._stats['events_processed'] += 1 - guild_id = message.guild.id - - # Bulk Delete Detection - current_time = datetime.utcnow() - - if guild_id not in self._bulk_deletes: - self._bulk_deletes[guild_id] = { - 'messages': set(), - 'timestamp': current_time, - 'channels': set() - } - - bulk_data = self._bulk_deletes[guild_id] - - # Reset wenn zu alt - if (current_time - bulk_data['timestamp']).total_seconds() > self.config['bulk_delete_window']: - bulk_data['messages'].clear() - bulk_data['channels'].clear() - bulk_data['timestamp'] = current_time - - bulk_data['messages'].add(message.id) - bulk_data['channels'].add(message.channel.id) - - # Kurz warten um weitere Deletes zu erfassen - await asyncio.sleep(0.3) - - # Bulk Delete Check - if len(bulk_data['messages']) >= self.config['bulk_delete_threshold']: - embed = discord.Embed( - title="🗑️ Bulk-Löschung erkannt", - description=f"**{len(bulk_data['messages'])}** Nachrichten wurden in kurzer Zeit gelöscht", - color=discord.Color.dark_red(), - timestamp=datetime.utcnow() - ) - - # Channel Info - affected_channels = [] - for ch_id in bulk_data['channels']: - channel = self.bot.get_channel(ch_id) - if channel: - affected_channels.append(channel.mention) - - if affected_channels: - embed.add_field( - name="📍 Betroffene Channels", - value="\n".join(affected_channels[:5]), - inline=False - ) - - embed.add_field(name="⏱️ Zeitfenster", value=f"< {self.config['bulk_delete_window']}s", inline=True) - embed.add_field(name="🔍 Hinweis", value="Mögliche Moderator-Aktion oder Bot-Cleanup", inline=True) - - await self.send_log(guild_id, embed, "messages") - - # Cache zurücksetzen - bulk_data['messages'].clear() - bulk_data['channels'].clear() - return - - # Normale Delete-Behandlung - if message.id not in bulk_data['messages']: - return # Bereits als Bulk verarbeitet - - embed = discord.Embed( - title="🗑️ Nachricht gelöscht", - color=discord.Color.red(), - timestamp=datetime.utcnow() - ) - - # Author Info - embed.add_field( - name="👤 Author", - value=f"{message.author.mention}\n`{message.author}`", - inline=True - ) - embed.add_field( - name="📍 Channel", - value=message.channel.mention, - inline=True - ) - embed.add_field( - name="⏰ Erstellt", - value=f"", - inline=True - ) - - # Content - if message.content: - embed.add_field( - name="💬 Inhalt", - value=self._format_content_for_embed(message.content), - inline=False - ) - - # Attachments - if message.attachments: - attachment_info = [] - for att in message.attachments[:self.config['max_attachment_display']]: - size_kb = att.size // 1024 - attachment_info.append(f"📎 `{att.filename}` ({size_kb} KB)") - - if len(message.attachments) > self.config['max_attachment_display']: - attachment_info.append(f"... und {len(message.attachments) - self.config['max_attachment_display']} weitere") - - embed.add_field( - name="📎 Anhänge", - value="\n".join(attachment_info), - inline=False - ) - - # Embeds - if message.embeds: - embed.add_field( - name="📋 Embeds", - value=f"{len(message.embeds)} Embed(s)", - inline=True - ) - - # Reactions - if message.reactions: - reaction_count = sum(r.count for r in message.reactions) - embed.add_field( - name="👍 Reaktionen", - value=f"{reaction_count} Reaktionen", - inline=True - ) - - embed.set_author(name=message.author.display_name, icon_url=message.author.display_avatar.url) - embed.set_footer(text=f"Message ID: {message.id} | User ID: {message.author.id}") - - await self.send_log(guild_id, embed, "messages") - - # Message aus bulk cache entfernen - if message.id in bulk_data['messages']: - bulk_data['messages'].discard(message.id) - - except Exception as e: - logger.error(f"Error in on_message_delete: {e}") - self._stats['errors'] += 1 - - @commands.Cog.listener() - async def on_message_edit(self, before: discord.Message, after: discord.Message): - """Verbessertes Message Edit Logging mit intelligentem Debouncing""" - try: - if (before.author.bot or not before.guild or - before.content == after.content or not before.content): - return - - self._stats['events_processed'] += 1 - message_id = before.id - - # Bestehenden Task canceln - if message_id in self._edit_tasks: - self._edit_tasks[message_id].cancel() - - # Neuen debounced Task erstellen - self._edit_tasks[message_id] = asyncio.create_task( - self._delayed_edit_log(before, after) - ) - - except Exception as e: - logger.error(f"Error in on_message_edit: {e}") - self._stats['errors'] += 1 - - async def _delayed_edit_log(self, before: discord.Message, after: discord.Message): - """Verzögertes Edit-Logging mit verbesserter Logik""" - try: - await asyncio.sleep(self.config['edit_debounce_time']) - - # Aktuellste Version der Nachricht holen - try: - fresh_message = await before.channel.fetch_message(before.id) - after = fresh_message # Aktuellste Version verwenden - except (discord.NotFound, discord.Forbidden): - pass # Nachricht wurde gelöscht oder keine Berechtigung - - await self._log_message_edit(before, after) - - except asyncio.CancelledError: - pass # Task wurde gecancelt - except Exception as e: - logger.error(f"Error in delayed edit log: {e}") - finally: - # Task aus Cache entfernen - if before.id in self._edit_tasks: - del self._edit_tasks[before.id] - - async def _log_message_edit(self, before: discord.Message, after: discord.Message): - """Internes Message Edit Logging mit Diff-Anzeige""" - try: - embed = discord.Embed( - title="✏️ Nachricht bearbeitet", - color=discord.Color.yellow(), - timestamp=datetime.utcnow() - ) - - embed.add_field( - name="👤 Author", - value=f"{before.author.mention}\n`{before.author}`", - inline=True - ) - embed.add_field( - name="📍 Channel", - value=before.channel.mention, - inline=True - ) - embed.add_field( - name="🔗 Nachricht", - value=f"[Zur Nachricht]({after.jump_url})", - inline=True - ) - - # Content Comparison - intelligenter - before_content = self._truncate_content(before.content or "", 700) - after_content = self._truncate_content(after.content or "", 700) - - if len(before_content) + len(after_content) < 2000: - embed.add_field( - name="📝 Vorher", - value=self._format_content_for_embed(before_content, escape_markdown=True), - inline=False - ) - embed.add_field( - name="📝 Nachher", - value=self._format_content_for_embed(after_content, escape_markdown=True), - inline=False - ) - else: - # Zu lang - nur Änderungsinfo - char_diff = len(after.content) - len(before.content) - diff_text = f"**Zeichen-Änderung:** {char_diff:+d}\n" - diff_text += f"**Länge:** {len(before.content)} → {len(after.content)}" - - embed.add_field( - name="📊 Änderungsinfo", - value=diff_text, - inline=False - ) - - # Timestamp der ursprünglichen Nachricht - embed.add_field( - name="🕐 Original erstellt", - value=f"", - inline=True - ) - - embed.set_author(name=before.author.display_name, icon_url=before.author.display_avatar.url) - embed.set_footer(text=f"Message ID: {before.id} | User ID: {before.author.id}") - - await self.send_log(before.guild.id, embed, "messages") - - except Exception as e: - logger.error(f"Error in _log_message_edit: {e}") - -# ============================================================================= - # VOICE STATE EVENTS - # ============================================================================= - - @commands.Cog.listener() - async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState): - """Verbessertes Voice State Logging mit intelligenter Filterung""" - try: - if member.bot: - return - - self._stats['events_processed'] += 1 - guild_id = member.guild.id - - # Cache initialisieren - if guild_id not in self._voice_cache: - self._voice_cache[guild_id] = {} - - guild_cache = self._voice_cache[guild_id] - member_id = member.id - - # Vorherigen State aus Cache holen oder setzen - cached_before = guild_cache.get(member_id) - guild_cache[member_id] = after - - # Event-Typ bestimmen - event_type = None - color = discord.Color.blue() - title = "" - - if not before.channel and after.channel: - # Join - event_type = "join" - title = "🔊 Voice Channel beigetreten" - color = discord.Color.green() - elif before.channel and not after.channel: - # Leave - event_type = "leave" - title = "🔇 Voice Channel verlassen" - color = discord.Color.red() - elif before.channel != after.channel and before.channel and after.channel: - # Move - event_type = "move" - title = "🔄 Voice Channel gewechselt" - color = discord.Color.orange() - elif before.channel == after.channel: - # State changes (mute, deafen, etc.) - changes = [] - if before.self_mute != after.self_mute: - changes.append(f"Self Mute: {'✅' if after.self_mute else '❌'}") - if before.self_deaf != after.self_deaf: - changes.append(f"Self Deaf: {'✅' if after.self_deaf else '❌'}") - if before.mute != after.mute: - changes.append(f"Server Mute: {'✅' if after.mute else '❌'}") - if before.deaf != after.deaf: - changes.append(f"Server Deaf: {'✅' if after.deaf else '❌'}") - if before.streaming != after.streaming: - changes.append(f"Streaming: {'✅' if after.streaming else '❌'}") - if before.self_video != after.self_video: - changes.append(f"Camera: {'✅' if after.self_video else '❌'}") - - if changes: - event_type = "state_change" - title = "🎛️ Voice Status geändert" - color = discord.Color.blue() - - if not event_type: - return - - embed = discord.Embed( - title=title, - color=color, - timestamp=datetime.utcnow() - ) - - # User Info - embed.add_field( - name="👤 User", - value=f"{member.mention}\n`{member}`", - inline=True - ) - - # Channel Info - if event_type == "join": - embed.add_field( - name="📍 Channel", - value=after.channel.mention, - inline=True - ) - # Wer ist noch im Channel? - other_members = [m for m in after.channel.members if m != member and not m.bot] - if other_members: - embed.add_field( - name="👥 Andere Mitglieder", - value=f"{len(other_members)} Mitglied{'er' if len(other_members) != 1 else ''}", - inline=True - ) - - elif event_type == "leave": - embed.add_field( - name="📍 Channel", - value=before.channel.mention, - inline=True - ) - # Session-Dauer berechnen wenn im Cache - if cached_before and cached_before.channel: - # Schätze Join-Zeit (grober Wert) - embed.add_field( - name="⏱️ Ungefähre Dauer", - value="Session beendet", - inline=True - ) - - elif event_type == "move": - embed.add_field( - name="📍 Von", - value=before.channel.mention, - inline=True - ) - embed.add_field( - name="📍 Nach", - value=after.channel.mention, - inline=True - ) - - elif event_type == "state_change": - embed.add_field( - name="📍 Channel", - value=after.channel.mention, - inline=True - ) - embed.add_field( - name="🔧 Änderungen", - value="\n".join(changes), - inline=False - ) - - embed.set_author(name=member.display_name, icon_url=member.display_avatar.url) - embed.set_footer(text=f"User ID: {member.id}") - - await self.send_log(guild_id, embed, "voice") - - except Exception as e: - logger.error(f"Error in on_voice_state_update: {e}") - self._stats['errors'] += 1 - - # ============================================================================= - # MEMBER UPDATE EVENTS - # ============================================================================= - - @commands.Cog.listener() - async def on_member_update(self, before: discord.Member, after: discord.Member): - """Member Update Logging mit intelligenter Filterung""" - try: - if before.bot: - return - - self._stats['events_processed'] += 1 - changes = [] - important_change = False - - # Nickname Änderung - if before.display_name != after.display_name: - changes.append({ - 'field': '🏷️ Nickname', - 'before': before.display_name or "*Kein Nickname*", - 'after': after.display_name or "*Kein Nickname*" - }) - important_change = True - - # Rollen Änderung - before_roles = set(before.roles) - after_roles = set(after.roles) - - added_roles = after_roles - before_roles - removed_roles = before_roles - after_roles - - if added_roles or removed_roles: - important_change = True - - if added_roles: - role_names = [role.name for role in added_roles if role.name != "@everyone"] - if role_names: - changes.append({ - 'field': '➕ Rollen hinzugefügt', - 'value': ", ".join(role_names[:5]) # Max 5 anzeigen - }) - - if removed_roles: - role_names = [role.name for role in removed_roles if role.name != "@everyone"] - if role_names: - changes.append({ - 'field': '➖ Rollen entfernt', - 'value': ", ".join(role_names[:5]) # Max 5 anzeigen - }) - - # Premium Status (Nitro Boost) - if hasattr(before, 'premium_since') and hasattr(after, 'premium_since'): - if before.premium_since != after.premium_since: - if after.premium_since and not before.premium_since: - changes.append({ - 'field': '💎 Server Boost', - 'value': 'Begonnen zu boosten' - }) - important_change = True - elif before.premium_since and not after.premium_since: - changes.append({ - 'field': '💎 Server Boost', - 'value': 'Boost beendet' - }) - important_change = True - - # Timeout Status - if hasattr(before, 'timed_out_until') and hasattr(after, 'timed_out_until'): - if before.timed_out_until != after.timed_out_until: - if after.timed_out_until: - changes.append({ - 'field': '⏸️ Timeout', - 'value': f"Bis " - }) - important_change = True - elif before.timed_out_until: - changes.append({ - 'field': '⏸️ Timeout', - 'value': 'Timeout aufgehoben' - }) - important_change = True - - # Nur loggen wenn wichtige Änderungen - if not important_change or not changes: - return - - embed = discord.Embed( - title="👤 Member geändert", - color=discord.Color.blue(), - timestamp=datetime.utcnow() - ) - - embed.add_field( - name="👤 Member", - value=f"{after.mention}\n`{after}`", - inline=True - ) - - # Änderungen hinzufügen - for change in changes[:self.config['max_embed_fields'] - 2]: # Platz für User und ID - if 'before' in change and 'after' in change: - value = f"**Vorher:** {change['before']}\n**Nachher:** {change['after']}" - else: - value = change['value'] - - embed.add_field( - name=change['field'], - value=value[:1024], # Discord limit - inline=False - ) - - embed.set_author(name=after.display_name, icon_url=after.display_avatar.url) - embed.set_footer(text=f"User ID: {after.id}") - - await self.send_log(after.guild.id, embed, "general") - - except Exception as e: - logger.error(f"Error in on_member_update: {e}") - self._stats['errors'] += 1 - - # ============================================================================= - # CHANNEL EVENTS - # ============================================================================= - - @commands.Cog.listener() - async def on_guild_channel_create(self, channel): - """Channel Creation Logging""" - try: - self._stats['events_processed'] += 1 - - embed = discord.Embed( - title="➕ Channel erstellt", - color=discord.Color.green(), - timestamp=datetime.utcnow() - ) - - # Channel-Typ Icon - type_icons = { - discord.ChannelType.text: "💬", - discord.ChannelType.voice: "🔊", - discord.ChannelType.category: "📁", - discord.ChannelType.news: "📢", - discord.ChannelType.stage_voice: "🎭", - discord.ChannelType.forum: "💭", - discord.ChannelType.private_thread: "🧵", - discord.ChannelType.public_thread: "🧵" - } - - icon = type_icons.get(channel.type, "📍") - embed.add_field( - name="📍 Channel", - value=f"{icon} {channel.mention}\n`{channel.name}`", - inline=True - ) - - embed.add_field( - name="📋 Typ", - value=channel.type.name.replace('_', ' ').title(), - inline=True - ) - - embed.add_field( - name="🆔 ID", - value=f"`{channel.id}`", - inline=True - ) - - # Kategorie info - if hasattr(channel, 'category') and channel.category: - embed.add_field( - name="📁 Kategorie", - value=channel.category.name, - inline=True - ) - - # Position - if hasattr(channel, 'position'): - embed.add_field( - name="📊 Position", - value=str(channel.position), - inline=True - ) - - embed.set_footer(text=f"Channel ID: {channel.id}") - await self.send_log(channel.guild.id, embed, "general") - - except Exception as e: - logger.error(f"Error in on_guild_channel_create: {e}") - self._stats['errors'] += 1 - - @commands.Cog.listener() - async def on_guild_channel_delete(self, channel): - """Channel Deletion Logging""" - try: - self._stats['events_processed'] += 1 - - embed = discord.Embed( - title="➖ Channel gelöscht", - color=discord.Color.red(), - timestamp=datetime.utcnow() - ) - - # Channel-Typ Icon - type_icons = { - discord.ChannelType.text: "💬", - discord.ChannelType.voice: "🔊", - discord.ChannelType.category: "📁", - discord.ChannelType.news: "📢", - discord.ChannelType.stage_voice: "🎭", - discord.ChannelType.forum: "💭" - } - - icon = type_icons.get(channel.type, "📍") - embed.add_field( - name="📍 Channel", - value=f"{icon} `#{channel.name}`", - inline=True - ) - - embed.add_field( - name="📋 Typ", - value=channel.type.name.replace('_', ' ').title(), - inline=True - ) - - embed.add_field( - name="🆔 ID", - value=f"`{channel.id}`", - inline=True - ) - - # Kategorie info - if hasattr(channel, 'category') and channel.category: - embed.add_field( - name="📁 Kategorie", - value=channel.category.name, - inline=True - ) - - embed.set_footer(text=f"Channel ID: {channel.id}") - await self.send_log(channel.guild.id, embed, "general") - - except Exception as e: - logger.error(f"Error in on_guild_channel_delete: {e}") - self._stats['errors'] += 1 - - # ============================================================================= - # BAN/KICK EVENTS - # ============================================================================= - - @commands.Cog.listener() - async def on_member_ban(self, guild: discord.Guild, user: discord.User): - """Member Ban Logging""" - try: - self._stats['events_processed'] += 1 - - # Versuche Ban-Info mit Grund zu holen - ban_info = None - try: - ban_info = await guild.fetch_ban(user) - except: - pass - - embed = discord.Embed( - title="🔨 Member gebannt", - color=discord.Color.dark_red(), - timestamp=datetime.utcnow() - ) - - embed.add_field( - name="👤 User", - value=f"{user.mention}\n`{user}`", - inline=True - ) - - embed.add_field( - name="🆔 ID", - value=f"`{user.id}`", - inline=True - ) - - embed.add_field( - name="📅 Account erstellt", - value=f"", - inline=True - ) - - if ban_info and ban_info.reason: - embed.add_field( - name="📝 Grund", - value=ban_info.reason[:1000], - inline=False - ) - - embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) - embed.set_footer(text=f"User ID: {user.id}") - - await self.send_log(guild.id, embed, "moderation") - - except Exception as e: - logger.error(f"Error in on_member_ban: {e}") - self._stats['errors'] += 1 - - @commands.Cog.listener() - async def on_member_unban(self, guild: discord.Guild, user: discord.User): - """Member Unban Logging""" - try: - self._stats['events_processed'] += 1 - - embed = discord.Embed( - title="🔓 Member entbannt", - color=discord.Color.green(), - timestamp=datetime.utcnow() - ) - - embed.add_field( - name="👤 User", - value=f"{user.mention}\n`{user}`", - inline=True - ) - - embed.add_field( - name="🆔 ID", - value=f"`{user.id}`", - inline=True - ) - - embed.add_field( - name="📅 Account erstellt", - value=f"", - inline=True - ) - - embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) - embed.set_footer(text=f"User ID: {user.id}") - - await self.send_log(guild.id, embed, "moderation") - - except Exception as e: - logger.error(f"Error in on_member_unban: {e}") - self._stats['errors'] += 1 - - # ============================================================================= - # INVITE EVENTS - # ============================================================================= - - @commands.Cog.listener() - async def on_invite_create(self, invite: discord.Invite): - """Invite Creation Logging""" - try: - self._stats['events_processed'] += 1 - - embed = discord.Embed( - title="🔗 Invite erstellt", - color=discord.Color.blue(), - timestamp=datetime.utcnow() - ) - - embed.add_field( - name="🔗 Invite Code", - value=f"`{invite.code}`", - inline=True - ) - - embed.add_field( - name="📍 Channel", - value=invite.channel.mention if invite.channel else "Unbekannt", - inline=True - ) - - if invite.inviter: - embed.add_field( - name="👤 Ersteller", - value=f"{invite.inviter.mention}\n`{invite.inviter}`", - inline=True - ) - - # Invite Settings - settings = [] - if invite.max_uses: - settings.append(f"Max. Nutzungen: {invite.max_uses}") - else: - settings.append("Max. Nutzungen: ∞") - - if invite.max_age: - settings.append(f"Ablauf: ") - else: - settings.append("Ablauf: Nie") - - if invite.temporary: - settings.append("Temporär: Ja") - - if settings: - embed.add_field( - name="⚙️ Einstellungen", - value="\n".join(settings), - inline=False - ) - - if invite.inviter: - embed.set_author(name=invite.inviter.display_name, icon_url=invite.inviter.display_avatar.url) - - embed.set_footer(text=f"Invite Code: {invite.code}") - await self.send_log(invite.guild.id, embed, "general") - - except Exception as e: - logger.error(f"Error in on_invite_create: {e}") - self._stats['errors'] += 1 - - @commands.Cog.listener() - async def on_invite_delete(self, invite: discord.Invite): - """Invite Deletion Logging""" - try: - self._stats['events_processed'] += 1 - - embed = discord.Embed( - title="🗑️ Invite gelöscht", - color=discord.Color.red(), - timestamp=datetime.utcnow() - ) - - embed.add_field( - name="🔗 Invite Code", - value=f"`{invite.code}`", - inline=True - ) - - embed.add_field( - name="📍 Channel", - value=invite.channel.mention if invite.channel else "Unbekannt", - inline=True - ) - - if invite.uses is not None: - embed.add_field( - name="📊 Verwendet", - value=f"{invite.uses} mal", - inline=True - ) - - embed.set_footer(text=f"Invite Code: {invite.code}") - await self.send_log(invite.guild.id, embed, "general") - - except Exception as e: - logger.error(f"Error in on_invite_delete: {e}") - self._stats['errors'] += 1 - -def setup(bot): - bot.add_cog(LoggingCog(bot)) \ No newline at end of file diff --git a/src/cogs/Servermanament/stats.py b/src/cogs/Servermanament/stats.py deleted file mode 100644 index 99c35a6..0000000 --- a/src/cogs/Servermanament/stats.py +++ /dev/null @@ -1,598 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -import discord -from discord.ext import commands, tasks -from discord import SlashCommandGroup -import logging -from typing import Optional -from DevTools import StatsDB -import asyncio -from datetime import datetime, timedelta -import math - - -logger = logging.getLogger(__name__) - - -class EnhancedStatsCog(commands.Cog): - """ - Enhanced Discord Cog for tracking user statistics with global level system. - Provides comprehensive tracking of messages, voice activity, and user progression. - """ - - def __init__(self, bot: commands.Bot): - self.bot = bot - self.db = StatsDB() - self.cleanup_task.start() - logger.info("Enhanced StatsCog initialized") - - stats = SlashCommandGroup("stats", "Statistiken") - - def cog_unload(self): - """Called when the cog is unloaded.""" - self.cleanup_task.cancel() - self.db.close() - logger.info("Enhanced StatsCog unloaded") - - @tasks.loop(hours=24) - async def cleanup_task(self): - """Daily cleanup of old data.""" - await self.db.cleanup_old_data(days=90) - - @cleanup_task.before_loop - async def before_cleanup(self): - await self.bot.wait_until_ready() - - @commands.Cog.listener() - async def on_ready(self): - """Called when the bot is ready and connected to Discord.""" - logger.info(f"Enhanced StatsCog ready - Bot connected as {self.bot.user}") - - @commands.Cog.listener() - async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, - after: discord.VoiceState): - """Track voice channel activity with enhanced features.""" - if member.bot: - return - - try: - user_id = member.id - guild_id = member.guild.id - - # User left a voice channel - if before.channel and not after.channel: - await self.db.end_voice_session(user_id, before.channel.id) - logger.debug(f"User {member.display_name} left voice channel {before.channel.name}") - - # User joined a voice channel - elif not before.channel and after.channel: - await self.db.start_voice_session(user_id, guild_id, after.channel.id) - logger.debug(f"User {member.display_name} joined voice channel {after.channel.name}") - - # User switched voice channels - elif before.channel and after.channel and before.channel.id != after.channel.id: - await self.db.end_voice_session(user_id, before.channel.id) - await self.db.start_voice_session(user_id, guild_id, after.channel.id) - logger.debug(f"User {member.display_name} switched from {before.channel.name} to {after.channel.name}") - - except Exception as e: - logger.error(f"Error handling voice state update for {member.display_name}: {e}") - - @commands.Cog.listener() - async def on_message(self, message: discord.Message): - """Track messages with enhanced analysis.""" - if message.author.bot or not message.guild: - return - - try: - # Analyze message content - word_count = len(message.content.split()) if message.content else 0 - has_attachment = len(message.attachments) > 0 - message_type = 'text' - - if message.attachments: - message_type = 'attachment' - elif message.embeds: - message_type = 'embed' - elif message.stickers: - message_type = 'sticker' - - await self.db.log_message( - user_id=message.author.id, - guild_id=message.guild.id, - channel_id=message.channel.id, - message_id=message.id, - word_count=word_count, - has_attachment=has_attachment, - message_type=message_type - ) - - logger.debug(f"Logged enhanced message {message.id} from {message.author.display_name}") - - except Exception as e: - logger.error(f"Error logging enhanced message from {message.author.display_name}: {e}") - - @stats.command( - name="statsistics", - description="Zeige deine Aktivitätsstatistiken an" - ) - async def stats_command( - self, - ctx: discord.ApplicationContext, - zeitraum: discord.Option( - str, - description="Zeitraum für die Statistiken", - choices=["24h", "7d", "30d"], - required=False, - default="24h" - ), - user: discord.Option( - discord.Member, - description="Statistiken eines anderen Users anzeigen (optional)", - required=False - ) - ): - """Enhanced stats command with more detailed information.""" - await ctx.defer() - - try: - target_user = user if user else ctx.author - time_periods = { - "24h": (24, "24 Stunden"), - "7d": (24 * 7, "7 Tagen"), - "30d": (24 * 30, "30 Tagen") - } - - hours, period_name = time_periods[zeitraum] - - # Get regular stats - message_count, voice_minutes = await self.db.get_user_stats( - target_user.id, hours, ctx.guild.id - ) - - # Get global user info - global_info = await self.db.get_global_user_info(target_user.id) - - # Format voice time - voice_hours = int(voice_minutes // 60) - voice_mins = int(voice_minutes % 60) - voice_time_str = f"{voice_hours}h {voice_mins}m" if voice_hours > 0 else f"{voice_mins}m" - - # Create main embed - embed = discord.Embed( - title=f"📊 {'Deine' if target_user == ctx.author else f'{target_user.display_name}s'} Statistiken", - description=f"Aktivität der letzten {period_name}", - color=discord.Color.blue() - ) - - # Local server stats - embed.add_field( - name="📅 Server Aktivität", - value=f"💬 **{message_count}** Nachrichten\n🎤 **{voice_time_str}** Voice-Zeit", - inline=True - ) - - # Global stats if available - if global_info: - level = global_info['level'] - xp_progress = global_info['xp_progress'] - xp_needed = global_info['xp_needed'] - progress_bar = self._create_progress_bar(xp_progress, xp_needed) - - embed.add_field( - name="🌍 Global Level", - value=f"**Level {level}** {self._get_level_emoji(level)}\n{progress_bar}\n`{int(xp_progress)}/{int(xp_needed)} XP`", - inline=True - ) - - # Global totals - total_voice_hours = int(global_info['total_voice_minutes'] // 60) - embed.add_field( - name="🏆 Global Totals", - value=f"📨 **{global_info['total_messages']:,}** Nachrichten\n" - f"🎤 **{total_voice_hours:,}** Stunden Voice\n" - f"🔥 **{global_info['daily_streak']}** Tage Streak", - inline=True - ) - - embed.set_thumbnail(url=target_user.display_avatar.url) - embed.set_footer(text=f"Angefragt von {ctx.author.display_name}") - - await ctx.followup.send(embed=embed) - - except Exception as e: - logger.error(f"Error executing enhanced stats command: {e}") - error_embed = discord.Embed( - title="❌ Fehler", - description="Es gab einen Fehler beim Abrufen der Statistiken.", - color=discord.Color.red() - ) - await ctx.followup.send(embed=error_embed, ephemeral=True) - - @stats.command( - name="globalstats", - description="Zeige deine globalen Level-Statistiken über alle Server an" - ) - async def global_stats_command( - self, - ctx: discord.ApplicationContext, - user: discord.Option( - discord.Member, - description="Global Stats eines anderen Users anzeigen", - required=False - ) - ): - """Show detailed global statistics and achievements.""" - await ctx.defer() - - try: - target_user = user if user else ctx.author - global_info = await self.db.get_global_user_info(target_user.id) - - if not global_info: - embed = discord.Embed( - title="📊 Keine Daten", - description=f"{'Du hast' if target_user == ctx.author else f'{target_user.display_name} hat'} noch keine globalen Statistiken.", - color=discord.Color.orange() - ) - await ctx.followup.send(embed=embed) - return - - level = global_info['level'] - xp = global_info['xp'] - xp_progress = global_info['xp_progress'] - xp_needed = global_info['xp_needed'] - - # Create embed - embed = discord.Embed( - title=f"🌍 {'Deine' if target_user == ctx.author else f'{target_user.display_name}s'} Globalen Stats", - description=f"Level-System über alle Server", - color=self._get_level_color(level) - ) - - # Level info - progress_bar = self._create_progress_bar(xp_progress, xp_needed) - level_emoji = self._get_level_emoji(level) - - embed.add_field( - name=f"{level_emoji} Level & XP", - value=f"**Level {level}**\n{progress_bar}\n`{int(xp_progress):,} / {int(xp_needed):,} XP`\n`Total: {int(xp):,} XP`", - inline=False - ) - - # Activity stats - total_voice_hours = int(global_info['total_voice_minutes'] // 60) - days_since_joined = (datetime.now() - datetime.fromisoformat(global_info['first_seen'])).days + 1 - avg_messages_per_day = global_info['total_messages'] / days_since_joined - - embed.add_field( - name="📈 Aktivitäts-Statistiken", - value=f"📨 **{global_info['total_messages']:,}** Nachrichten gesamt\n" - f"🎤 **{total_voice_hours:,}** Stunden in Voice\n" - f"🏢 **{global_info['total_servers']}** Server aktiv\n" - f"📊 **{avg_messages_per_day:.1f}** Nachrichten/Tag", - inline=True - ) - - # Streak info - embed.add_field( - name="🔥 Streak Statistiken", - value=f"🔥 **{global_info['daily_streak']}** Tage aktuell\n" - f"🏆 **{global_info['best_streak']}** Tage Rekord\n" - f"📅 Dabei seit **{days_since_joined}** Tagen", - inline=True - ) - - # Recent achievements - achievements = global_info['achievements'][-3:] # Last 3 achievements - if achievements: - achievement_text = "\n".join( - [f"{ach.get('icon', '🏆')} {ach.get('name', 'Unknown')}" for ach in achievements]) - embed.add_field( - name="🏆 Neueste Erfolge", - value=achievement_text, - inline=True - ) - - embed.set_thumbnail(url=target_user.display_avatar.url) - embed.set_footer(text=f"Angefragt von {ctx.author.display_name} • Globales Level-System") - - await ctx.followup.send(embed=embed) - - except Exception as e: - logger.error(f"Error executing global stats command: {e}") - error_embed = discord.Embed( - title="❌ Fehler", - description="Es gab einen Fehler beim Abrufen der globalen Statistiken.", - color=discord.Color.red() - ) - await ctx.followup.send(embed=error_embed, ephemeral=True) - - @stats.command( - name="leaderboard", - description="Zeige die Top-User Rangliste an" - ) - async def leaderboard_command( - self, - ctx: discord.ApplicationContext, - typ: discord.Option( - str, - description="Art der Rangliste", - choices=["global", "server"], - required=False, - default="server" - ), - limit: discord.Option( - int, - description="Anzahl der angezeigten User (max 20)", - min_value=5, - max_value=20, - required=False, - default=10 - ) - ): - """Show leaderboard for global or server stats.""" - await ctx.defer() - - try: - if typ == "global": - leaderboard_data = await self.db.get_leaderboard(limit) - title = "🌍 Globale Rangliste" - description = "Top User nach globalem Level & XP" - else: - leaderboard_data = await self.db.get_leaderboard(limit, ctx.guild.id) - title = f"🏢 {ctx.guild.name} Rangliste" - description = "Top User der letzten 30 Tage" - - if not leaderboard_data: - embed = discord.Embed( - title="📊 Keine Daten", - description="Keine Ranglisten-Daten verfügbar.", - color=discord.Color.orange() - ) - await ctx.followup.send(embed=embed) - return - - embed = discord.Embed( - title=title, - description=description, - color=discord.Color.gold() - ) - - leaderboard_text = "" - for i, data in enumerate(leaderboard_data, 1): - try: - user = self.bot.get_user(data[0]) - username = user.display_name if user else "Unbekannter User" - - # Position emoji - if i == 1: - pos_emoji = "🥇" - elif i == 2: - pos_emoji = "🥈" - elif i == 3: - pos_emoji = "🥉" - else: - pos_emoji = f"{i}." - - if typ == "global": - # Global leaderboard format: user_id, level, xp, messages, voice - level, xp, messages, voice = data[1], data[2], data[3], data[4] - level_emoji = self._get_level_emoji(level) - leaderboard_text += f"{pos_emoji} **{username}** {level_emoji}\n" - leaderboard_text += f" Level {level} • {int(xp):,} XP\n\n" - else: - # Server leaderboard format: user_id, messages, words - messages, words = data[1], data[2] - leaderboard_text += f"{pos_emoji} **{username}**\n" - leaderboard_text += f" {messages:,} Nachrichten • {words:,} Wörter\n\n" - - except Exception as e: - logger.error(f"Error processing leaderboard entry: {e}") - continue - - if leaderboard_text: - embed.description = leaderboard_text - else: - embed.description = "Fehler beim Laden der Rangliste." - - embed.set_footer(text=f"Angefragt von {ctx.author.display_name}") - await ctx.followup.send(embed=embed) - - except Exception as e: - logger.error(f"Error executing leaderboard command: {e}") - error_embed = discord.Embed( - title="❌ Fehler", - description="Es gab einen Fehler beim Laden der Rangliste.", - color=discord.Color.red() - ) - await ctx.followup.send(embed=error_embed, ephemeral=True) - - @stats.command( - name="achievements", - description="Zeige deine freigeschalteten Erfolge an" - ) - async def achievements_command( - self, - ctx: discord.ApplicationContext, - user: discord.Option( - discord.Member, - description="Erfolge eines anderen Users anzeigen", - required=False - ) - ): - """Show user achievements.""" - await ctx.defer() - - try: - target_user = user if user else ctx.author - global_info = await self.db.get_global_user_info(target_user.id) - - if not global_info: - embed = discord.Embed( - title="🏆 Keine Erfolge", - description=f"{'Du hast' if target_user == ctx.author else f'{target_user.display_name} hat'} noch keine Erfolge freigeschaltet.", - color=discord.Color.orange() - ) - await ctx.followup.send(embed=embed) - return - - achievements = global_info.get('achievements', []) - - if not achievements: - embed = discord.Embed( - title="🏆 Noch keine Erfolge", - description=f"{'Du hast' if target_user == ctx.author else f'{target_user.display_name} hat'} noch keine Erfolge freigeschaltet.\nWerde aktiver um Erfolge zu sammeln!", - color=discord.Color.blue() - ) - await ctx.followup.send(embed=embed) - return - - embed = discord.Embed( - title=f"🏆 {'Deine' if target_user == ctx.author else f'{target_user.display_name}s'} Erfolge", - description=f"**{len(achievements)}** Erfolge freigeschaltet", - color=discord.Color.gold() - ) - - # Group achievements by category or show all - achievement_text = "" - for ach in achievements: - icon = ach.get('icon', '🏆') - name = ach.get('name', 'Unbekannter Erfolg') - desc = ach.get('description', 'Keine Beschreibung') - unlocked = ach.get('unlocked_at', 'Unbekannt') - - achievement_text += f"{icon} **{name}**\n" - achievement_text += f" {desc}\n" - if unlocked != 'Unbekannt': - try: - unlock_date = datetime.fromisoformat(unlocked).strftime("%d.%m.%Y") - achievement_text += f" Freigeschaltet: {unlock_date}\n" - except: - pass - achievement_text += "\n" - - # Split into multiple fields if too long - if len(achievement_text) > 1024: - # Split achievements into chunks - chunks = [achievements[i:i + 5] for i in range(0, len(achievements), 5)] - for i, chunk in enumerate(chunks): - field_text = "" - for ach in chunk: - icon = ach.get('icon', '🏆') - name = ach.get('name', 'Unbekannter Erfolg') - field_text += f"{icon} **{name}**\n" - - embed.add_field( - name=f"Erfolge {i * 5 + 1}-{min((i + 1) * 5, len(achievements))}", - value=field_text, - inline=True - ) - else: - embed.description = achievement_text - - embed.set_thumbnail(url=target_user.display_avatar.url) - embed.set_footer(text=f"Angefragt von {ctx.author.display_name}") - - await ctx.followup.send(embed=embed) - - except Exception as e: - logger.error(f"Error executing achievements command: {e}") - error_embed = discord.Embed( - title="❌ Fehler", - description="Es gab einen Fehler beim Laden der Erfolge.", - color=discord.Color.red() - ) - await ctx.followup.send(embed=error_embed, ephemeral=True) - - @stats.command( - name="stats_info", - description="Informationen über das erweiterte Statistik-System" - ) - async def stats_info_command(self, ctx: discord.ApplicationContext): - """Provide information about the enhanced statistics system.""" - embed = discord.Embed( - title="ℹ️ Erweitertes Statistik-System", - description="Informationen über das Activity-Tracking & Level-System", - color=discord.Color.green() - ) - - embed.add_field( - name="📊 Was wird getrackt?", - value="• **Server-spezifisch:** Nachrichten & Voice-Zeit\n" - "• **Global:** Level, XP, Gesamtaktivität\n" - "• **Erweitert:** Wortanzahl, Anhänge, Streaks", - inline=False - ) - - embed.add_field( - name="🌍 Globales Level-System", - value="• **XP-Quellen:** Nachrichten (+1-6 XP), Voice-Chat (+0.5 XP/min)\n" - "• **Level:** Basiert auf Gesamt-XP über alle Server\n" - "• **Erfolge:** Automatisch für Meilensteine freigeschaltet", - inline=False - ) - - embed.add_field( - name="🏆 Verfügbare Kommandos", - value="• `/stats` - Server Aktivitäts-Statistiken\n" - "• `/globalstats` - Globale Level & Erfolge\n" - "• `/leaderboard` - Ranglisten (global/server)\n" - "• `/achievements` - Freigeschaltete Erfolge", - inline=False - ) - - embed.add_field( - name="🔒 Datenschutz", - value="• Nur Metadaten werden gespeichert (keine Inhalte)\n" - "• Automatische Bereinigung alter Daten nach 90 Tagen\n" - "• [Vollständige Datenschutzerklärung](https://medicopter117.github.io/ManagerX-Web/privacy.html)", - inline=False - ) - - embed.set_footer(text="Das globale Level-System funktioniert serverübergreifend!") - await ctx.respond(embed=embed, ephemeral=True) - - def _create_progress_bar(self, current: float, maximum: float, length: int = 10) -> str: - """Create a visual progress bar.""" - if maximum <= 0: - return "▓" * length - - filled = int((current / maximum) * length) - bar = "▓" * filled + "░" * (length - filled) - percentage = (current / maximum) * 100 - return f"{bar} {percentage:.1f}%" - - def _get_level_emoji(self, level: int) -> str: - """Get emoji based on user level.""" - if level >= 100: - return "👑" - elif level >= 50: - return "🏆" - elif level >= 25: - return "🏅" - elif level >= 10: - return "⭐" - elif level >= 5: - return "🌟" - else: - return "🔰" - - def _get_level_color(self, level: int) -> discord.Color: - """Get embed color based on user level.""" - if level >= 100: - return discord.Color.gold() - elif level >= 50: - return discord.Color.purple() - elif level >= 25: - return discord.Color.red() - elif level >= 10: - return discord.Color.orange() - elif level >= 5: - return discord.Color.green() - else: - return discord.Color.blue() - - -def setup(bot: commands.Bot): - """Setup function to add the enhanced cog to the bot.""" - bot.add_cog(EnhancedStatsCog(bot)) \ No newline at end of file diff --git a/src/cogs/Servermanament/tempvc.py b/src/cogs/Servermanament/tempvc.py deleted file mode 100644 index 87fb7ec..0000000 --- a/src/cogs/Servermanament/tempvc.py +++ /dev/null @@ -1,612 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -from DevTools import TempVCDatabase -import discord -from discord import slash_command, option, SlashCommandGroup -from discord.ext import commands -from discord.ui import Container -import ezcord - -db = TempVCDatabase() - - -class TempChannelControlView(discord.ui.View): - def __init__(self, channel_owner_id: int, prefix: str = "🔧"): - super().__init__(timeout=None) - self.channel_owner_id = channel_owner_id - self.prefix = prefix - - # Update button labels with custom prefix - self.rename_button.label = f"{prefix} Umbenennen" - self.limit_button.label = f"{prefix} Limit" - self.lock_button.label = f"{prefix} Sperren" - self.kick_button.label = f"{prefix} Kick" - - @discord.ui.button(label="🔧 Umbenennen", style=discord.ButtonStyle.primary, custom_id="tempvc_rename") - async def rename_button(self, button: discord.ui.Button, interaction: discord.Interaction): - if interaction.user.id != self.channel_owner_id: - container = Container() - container.add_text(f"{emoji_no} Keine Berechtigung\nDu bist nicht der Besitzer dieses Channels!") - return await interaction.response.send_message(view=container, ephemeral=True) - - modal = RenameChannelModal(interaction.channel) - await interaction.response.send_modal(modal) - - @discord.ui.button(label="🔧 Limit", style=discord.ButtonStyle.primary, custom_id="tempvc_limit") - async def limit_button(self, button: discord.ui.Button, interaction: discord.Interaction): - if interaction.user.id != self.channel_owner_id: - container = Container() - container.add_text(f"{emoji_no} Keine Berechtigung\nDu bist nicht der Besitzer dieses Channels!") - return await interaction.response.send_message(view=container, ephemeral=True) - - modal = UserLimitModal(interaction.channel) - await interaction.response.send_modal(modal) - - @discord.ui.button(label="🔧 Sperren", style=discord.ButtonStyle.secondary, custom_id="tempvc_lock") - async def lock_button(self, button: discord.ui.Button, interaction: discord.Interaction): - if interaction.user.id != self.channel_owner_id: - container = Container() - container.add_text(f"{emoji_no} Keine Berechtigung\nDu bist nicht der Besitzer dieses Channels!") - return await interaction.response.send_message(view=container, ephemeral=True) - - channel = interaction.channel - overwrites = channel.overwrites - - # Toggle lock status - is_locked = not overwrites.get(interaction.guild.default_role, discord.PermissionOverwrite()).connect - - if interaction.guild.default_role not in overwrites: - overwrites[interaction.guild.default_role] = discord.PermissionOverwrite() - - overwrites[interaction.guild.default_role].connect = not is_locked - - try: - await channel.edit(overwrites=overwrites) - status = "🔒 gesperrt" if is_locked else "🔓 entsperrt" - button.label = f"{self.prefix} {'Entsperren' if is_locked else 'Sperren'}" - button.style = discord.ButtonStyle.danger if is_locked else discord.ButtonStyle.secondary - - await interaction.response.edit_message(view=self) - - container = Container() - container.add_text(f"Channel wurde {status}!") - await interaction.followup.send(view=container, ephemeral=True) - except discord.Forbidden: - container = Container() - container.add_text(f"{emoji_no} Fehler\nFehlende Berechtigungen!") - await interaction.response.send_message(view=container, ephemeral=True) - - @discord.ui.button(label="🔧 Kick", style=discord.ButtonStyle.danger, custom_id="tempvc_kick") - async def kick_button(self, button: discord.ui.Button, interaction: discord.Interaction): - if interaction.user.id != self.channel_owner_id: - container = Container() - container.add_text(f"{emoji_no} Keine Berechtigung\nDu bist nicht der Besitzer dieses Channels!") - return await interaction.response.send_message(view=container, ephemeral=True) - - modal = KickUserModal(interaction.channel) - await interaction.response.send_modal(modal) - - -class RenameChannelModal(discord.ui.Modal): - def __init__(self, channel): - super().__init__(title="Channel umbenennen") - self.channel = channel - - self.name_input = discord.ui.InputText( - label="Neuer Channel-Name", - placeholder="Gib einen neuen Namen ein...", - value=channel.name, - max_length=100, - required=True - ) - self.add_item(self.name_input) - - async def callback(self, interaction: discord.Interaction): - new_name = self.name_input.value.strip() - - # Validate name - if len(new_name) < 1: - container = Container() - container.add_text(f"{emoji_no} Ungültiger Name\nName darf nicht leer sein!") - return await interaction.response.send_message(view=container, ephemeral=True) - - # Check for forbidden characters - forbidden_chars = ['@', '#', ':', '`', '```'] - if any(char in new_name for char in forbidden_chars): - container = Container() - container.add_text(f"{emoji_no} Ungültige Zeichen\nName enthält ungültige Zeichen!") - return await interaction.response.send_message(view=container, ephemeral=True) - - try: - old_name = self.channel.name - await self.channel.edit(name=new_name) - - container = Container() - container.add_text( - f"{emoji_yes} Channel umbenannt\n" - f"**{old_name}** → **{new_name}**" - ) - await interaction.response.send_message(view=container, ephemeral=True) - - except discord.Forbidden: - container = Container() - container.add_text(f"{emoji_no} Fehler\nFehlende Berechtigungen zum Umbenennen!") - await interaction.response.send_message(view=container, ephemeral=True) - except discord.HTTPException as e: - container = Container() - container.add_text(f"{emoji_no} Fehler\nFehler beim Umbenennen: {str(e)}") - await interaction.response.send_message(view=container, ephemeral=True) - - -class UserLimitModal(discord.ui.Modal): - def __init__(self, channel): - super().__init__(title="User-Limit setzen") - self.channel = channel - - current_limit = channel.user_limit if channel.user_limit else "Kein Limit" - - self.limit_input = discord.ui.InputText( - label="Neues User-Limit (0 = Kein Limit)", - placeholder="Gib eine Zahl zwischen 0-99 ein...", - value=str(current_limit) if isinstance(current_limit, int) else "0", - max_length=2, - required=True - ) - self.add_item(self.limit_input) - - async def callback(self, interaction: discord.Interaction): - try: - limit = int(self.limit_input.value.strip()) - - if limit < 0 or limit > 99: - container = Container() - container.add_text(f"{emoji_no} Ungültiges Limit\nLimit muss zwischen 0 und 99 liegen!") - return await interaction.response.send_message(view=container, ephemeral=True) - - # 0 means no limit in Discord - limit = None if limit == 0 else limit - - await self.channel.edit(user_limit=limit) - - limit_text = "Kein Limit" if limit is None else f"{limit} User" - - container = Container() - container.add_text( - f"{emoji_yes} User-Limit geändert\n" - f"Neues Limit: **{limit_text}**" - ) - await interaction.response.send_message(view=container, ephemeral=True) - - except ValueError: - container = Container() - container.add_text(f"{emoji_no} Ungültige Eingabe\nBitte gib eine gültige Zahl ein!") - await interaction.response.send_message(view=container, ephemeral=True) - except discord.Forbidden: - container = Container() - container.add_text(f"{emoji_no} Fehler\nFehlende Berechtigungen!") - await interaction.response.send_message(view=container, ephemeral=True) - except discord.HTTPException as e: - container = Container() - container.add_text(f"{emoji_no} Fehler\nFehler beim Setzen des Limits: {str(e)}") - await interaction.response.send_message(view=container, ephemeral=True) - - -class KickUserModal(discord.ui.Modal): - def __init__(self, channel): - super().__init__(title="User kicken") - self.channel = channel - - # Create list of current members (except bot and channel owner) - members_list = [] - for member in channel.members: - if not member.bot and db.get_temp_channel_owner(channel.id) != member.id: - members_list.append(f"{member.display_name} ({member.id})") - - members_text = "\n".join(members_list[:10]) # Limit to first 10 for display - if len(members_list) > 10: - members_text += f"\n... und {len(members_list) - 10} weitere" - - self.user_input = discord.ui.InputText( - label="User zum Kicken", - placeholder="@Username oder User-ID...", - style=discord.InputTextStyle.short, - required=True - ) - self.add_item(self.user_input) - - if members_text: - self.info_input = discord.ui.InputText( - label="Aktuelle Mitglieder:", - value=members_text if members_text else "Keine anderen Mitglieder im Channel", - style=discord.InputTextStyle.paragraph, - required=False - ) - self.add_item(self.info_input) - - async def callback(self, interaction: discord.Interaction): - user_input = self.user_input.value.strip() - - # Try to find user by mention, name or ID - target_user = None - - # Check if it's a mention - if user_input.startswith('<@') and user_input.endswith('>'): - user_id = int(user_input[2:-1].replace('!', '')) - target_user = interaction.guild.get_member(user_id) - else: - # Try by ID first - try: - user_id = int(user_input) - target_user = interaction.guild.get_member(user_id) - except ValueError: - # Try by username/display name - for member in self.channel.members: - if (member.display_name.lower() == user_input.lower() or - member.name.lower() == user_input.lower()): - target_user = member - break - - if not target_user: - container = Container() - container.add_text(f"{emoji_no} Fehler\nUser nicht gefunden!") - return await interaction.response.send_message(view=container, ephemeral=True) - - if target_user not in self.channel.members: - container = Container() - container.add_text(f"{emoji_no} Fehler\nUser ist nicht in diesem Channel!") - return await interaction.response.send_message(view=container, ephemeral=True) - - if target_user.id == db.get_temp_channel_owner(self.channel.id): - container = Container() - container.add_text(f"{emoji_no} Fehler\nDu kannst dich nicht selbst kicken!") - return await interaction.response.send_message(view=container, ephemeral=True) - - if target_user.bot: - container = Container() - container.add_text(f"{emoji_no} Fehler\nBots können nicht gekickt werden!") - return await interaction.response.send_message(view=container, ephemeral=True) - - try: - await target_user.move_to(None) # Disconnect from voice - - container = Container() - container.add_text( - f"{emoji_yes} User gekickt\n" - f"**{target_user.display_name}** wurde aus dem Channel gekickt." - ) - await interaction.response.send_message(view=container, ephemeral=True) - - except discord.Forbidden: - container = Container() - container.add_text(f"{emoji_no} Fehler\nFehlende Berechtigungen zum Kicken!") - await interaction.response.send_message(view=container, ephemeral=True) - except discord.HTTPException as e: - container = Container() - container.add_text(f"{emoji_no} Fehler\nFehler beim Kicken: {str(e)}") - await interaction.response.send_message(view=container, ephemeral=True) - - -class TempVC(ezcord.Cog): - def __init__(self, bot): - self.bot = bot - - tempvc = SlashCommandGroup("tempvc", "Verwalte temporäre Voice-Channel Systeme") - - @tempvc.command(name="create", description="Erstelle ein VC-Erstellungssystem") - @option("creator_channel", description="Channel, den Mitglieder betreten, um ihren VC zu erstellen", - channel_types=[discord.ChannelType.voice]) - @option("category", description="Kategorie, in der die Temp-Channels erstellt werden", - channel_types=[discord.ChannelType.category]) - async def tempvc_create(self, ctx: discord.ApplicationContext, creator_channel: discord.VoiceChannel, - category: discord.CategoryChannel): - if not ctx.author.guild_permissions.administrator: - container = Container() - container.add_text( - f"{emoji_no} Keine Berechtigung\n" - "Du brauchst Administratorrechte." - ) - return await ctx.respond(view=container, ephemeral=True) - - try: - db.set_tempvc_settings(ctx.guild.id, creator_channel.id, category.id) - - container = Container() - container.add_text( - f"{emoji_yes} Temp-VC System aktiviert\n" - "Das temporäre Voice-Channel System wurde erfolgreich eingerichtet!" - ) - container.add_separator() - container.add_text( - f"**🎤 Ersteller-Channel:** {creator_channel.mention}\n" - f"**📁 Kategorie:** {category.mention}\n" - "**ℹ️ Information:** Mitglieder können nun den Ersteller-Channel betreten, um automatisch einen eigenen temporären Voice-Channel zu erhalten." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - except Exception as e: - container = Container() - container.add_text( - f"{emoji_no} Fehler beim Erstellen\n" - f"```{str(e)}```" - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - - @tempvc.command(name="remove", description="Entferne das VC-Erstellungssystem") - async def tempvc_remove(self, ctx: discord.ApplicationContext): - if not ctx.author.guild_permissions.administrator: - container = Container() - container.add_text( - f"{emoji_no} Keine Berechtigung\n" - "Du brauchst Administratorrechte." - ) - view = discord.ui.View(container, timeout=None) - return await ctx.respond(view=view, ephemeral=True) - - try: - settings = db.get_tempvc_settings(ctx.guild.id) - if not settings: - container = Container() - container.add_text( - f"{emoji_no} Kein System aktiv\n" - "Es ist derzeit kein Temp-VC System auf diesem Server aktiv." - ) - view = discord.ui.View(container, timeout=None) - return await ctx.respond(view=view, ephemeral=True) - - db.remove_tempvc_settings(ctx.guild.id) - - container = Container() - container.add_text( - f"{emoji_yes} System deaktiviert\n" - "Das Temp-VC System wurde erfolgreich deaktiviert!" - ) - container.add_separator() - container.add_text( - "**ℹ️ Information:** Bestehende temporäre Channels bleiben bestehen, aber es werden keine neuen mehr erstellt." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - except Exception as e: - container = Container() - container.add_text( - f"{emoji_no} Fehler beim Entfernen\n" - f"```{str(e)}```" - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - - @tempvc.command(name="settings", description="Zeige die aktuellen Temp-VC Einstellungen") - async def tempvc_settings(self, ctx: discord.ApplicationContext): - if not ctx.author.guild_permissions.administrator: - container = Container() - container.add_text( - f"{emoji_no} Keine Berechtigung\n" - "Du brauchst Administratorrechte." - ) - view = discord.ui.View(container, timeout=None) - return await ctx.respond(view=view, ephemeral=True) - - settings = db.get_tempvc_settings(ctx.guild.id) - if not settings: - container = Container() - container.add_text( - f"{emoji_no} Kein System aktiv\n" - "Es ist derzeit kein Temp-VC System auf diesem Server aktiv." - ) - container.add_separator() - container.add_text( - "**💡 Tipp:** Verwende `/tempvc create` um ein Temp-VC System einzurichten." - ) - view = discord.ui.View(container, timeout=None) - return await ctx.respond(view=view, ephemeral=True) - - creator_channel_id, category_id, auto_delete_time = settings - creator_channel = ctx.guild.get_channel(creator_channel_id) - category = ctx.guild.get_channel(category_id) - - container = Container() - container.add_text("🎛️ **Temp-VC Einstellungen**\nAktuelle Konfiguration des temporären Voice-Channel Systems") - container.add_separator() - - container.add_text( - f"**🎤 Ersteller-Channel:**\n" - f"{creator_channel.mention if creator_channel else f'{emoji_no} Channel nicht gefunden (ID: {creator_channel_id})'}" - ) - container.add_separator() - - container.add_text( - f"**📁 Kategorie:**\n" - f"{category.mention if category else f'{emoji_no} Kategorie nicht gefunden (ID: {category_id})'}" - ) - container.add_separator() - - container.add_text(f"**⏰ Auto-Löschzeit:**\n{auto_delete_time} Minuten") - container.add_separator() - - # UI Settings - ui_settings = db.get_ui_settings(ctx.guild.id) - if ui_settings: - ui_enabled, ui_prefix = ui_settings - container.add_text( - f"**🖥️ Control-UI:**\n" - f"{'✅ Aktiviert' if ui_enabled else '❌ Deaktiviert'}" - ) - if ui_enabled: - container.add_separator() - container.add_text(f"**🏷️ UI-Prefix:**\n{ui_prefix}") - else: - container.add_text("**🖥️ Control-UI:**\n❌ Deaktiviert") - - container.add_separator() - container.add_text( - f"**ℹ️ Status:**\n" - f"{emoji_yes + ' System aktiv' if creator_channel and category else emoji_no + ' Fehlerhafte Konfiguration'}" - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - - @tempvc.command(name="ui", description="Konfiguriere das Control-UI für Temp-Channels") - @option("enabled", description="Soll das UI aktiviert sein?", choices=[ - discord.OptionChoice(name="Aktiviert", value="true"), - discord.OptionChoice(name="Deaktiviert", value="false") - ]) - @option("prefix", description="Prefix für UI-Buttons (Emoji oder Text)", required=False, default="🔧") - async def tempvc_ui(self, ctx: discord.ApplicationContext, enabled: str, prefix: str = "🔧"): - if not ctx.author.guild_permissions.administrator: - container = Container() - container.add_text( - f"{emoji_no} Keine Berechtigung\n" - "Du brauchst Administratorrechte." - ) - return await ctx.respond(view=container, ephemeral=True) - - # Check if TempVC system exists - settings = db.get_tempvc_settings(ctx.guild.id) - if not settings: - container = Container() - container.add_text( - f"{emoji_no} Kein System aktiv\n" - "Du musst zuerst ein Temp-VC System erstellen!" - ) - container.add_separator() - container.add_text( - "**💡 Tipp:** Verwende `/tempvc create` um ein Temp-VC System einzurichten." - ) - view = discord.ui.View(container, timeout=None) - return await ctx.respond(view=view, ephemeral=True) - - ui_enabled = enabled == "true" - - # Validate prefix - if len(prefix) > 10: - container = Container() - container.add_text(f"{emoji_no} Ungültiger Prefix\nPrefix darf maximal 10 Zeichen lang sein!") - return await ctx.respond(view=container, ephemeral=True) - - try: - db.set_ui_settings(ctx.guild.id, ui_enabled, prefix) - - container = Container() - container.add_text(f"{emoji_yes} UI-Einstellungen gespeichert") - container.add_separator() - container.add_text( - f"**🖥️ Control-UI:** {'✅ Aktiviert' if ui_enabled else '❌ Deaktiviert'}" - ) - if ui_enabled: - container.add_separator() - container.add_text(f"**🏷️ Prefix:** {prefix}") - container.add_separator() - container.add_text( - "**ℹ️ Information:** Das Control-UI wird nun in neu erstellten Temp-Channels angezeigt." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - - except Exception as e: - container = Container() - container.add_text( - f"{emoji_no} Fehler beim Speichern\n" - f"```{str(e)}```" - ) - await ctx.respond(view=container, ephemeral=True) - - @commands.Cog.listener() - async def on_voice_state_update(self, member, before, after): - try: - if after.channel: - await self.handle_creator_channel_join(member, after.channel) - if before.channel: - await self.handle_channel_leave(before.channel) - except Exception as e: - print(f"Error in voice state update: {e}") - - async def handle_creator_channel_join(self, member: discord.Member, channel: discord.VoiceChannel): - settings = db.get_tempvc_settings(member.guild.id) - if not settings: - return - - creator_channel_id, category_id, auto_delete_time = settings - - if channel.id != creator_channel_id: - return - - guild = member.guild - category = discord.utils.get(guild.categories, id=category_id) - if not category: - print(f"Category with ID {category_id} not found in guild {guild.id}") - return - - overwrites = { - guild.default_role: discord.PermissionOverwrite(view_channel=False), - member: discord.PermissionOverwrite( - view_channel=True, - connect=True, - manage_permissions=True, - move_members=True - ) - } - - try: - temp_channel = await guild.create_voice_channel( - name=f"🔊 {member.display_name}'s Raum", - category=category, - overwrites=overwrites - ) - db.add_temp_channel(temp_channel.id, guild.id, member.id) - await member.move_to(temp_channel) - - # Check if UI is enabled and send control panel - ui_settings = db.get_ui_settings(guild.id) - if ui_settings and ui_settings[0]: # UI enabled - ui_enabled, ui_prefix = ui_settings - - container = Container() - container.add_text( - f"## 🎛️ **Channel-Kontrolle**\n" - f"**{member.display_name}**, du bist der Besitzer dieses Channels!\n" - "Verwende die Buttons unten, um deinen Channel zu verwalten." - ) - container.add_separator() - container.add_text( - "**🔧 Verfügbare Aktionen:**\n" - "• **Umbenennen** - Ändere den Channel-Namen\n" - "• **Limit** - Setze ein User-Limit\n" - "• **Sperren** - Sperre/Entsperre den Channel\n" - "• **Kick** - Kicke User aus dem Channel" - ) - container.add_separator() - container.add_text("Diese Buttons funktionieren nur für den Channel-Besitzer.") - - control_view = TempChannelControlView(member.id, ui_prefix) - view = discord.ui.View(container, timeout=None) - await temp_channel.send(view=view) - await temp_channel.send(view=control_view) - - except discord.Forbidden: - print(f"Missing permissions to create voice channel in guild {guild.id}") - except discord.HTTPException as e: - print(f"HTTP error when creating voice channel: {e}") - except Exception as e: - print(f"Unexpected error when creating temp channel: {e}") - - async def handle_channel_leave(self, channel: discord.VoiceChannel): - if len(channel.members) > 0: - return - - if not db.is_temp_channel(channel.id): - return - - try: - db.remove_temp_channel(channel.id) - await channel.delete(reason="Temp channel cleanup - channel empty") - - except discord.Forbidden: - print(f"Missing permissions to delete channel {channel.id}") - except discord.NotFound: - db.remove_temp_channel(channel.id) - except Exception as e: - print(f"Error deleting temp channel {channel.id}: {e}") - - -def setup(bot): - bot.add_cog(TempVC(bot)) \ No newline at end of file diff --git a/src/cogs/Servermanament/welcome.py b/src/cogs/Servermanament/welcome.py deleted file mode 100644 index cc9fb69..0000000 --- a/src/cogs/Servermanament/welcome.py +++ /dev/null @@ -1,1467 +0,0 @@ -""" -Welcome System Cog -================== - -Umfassendes Welcome System mit Embed-Support, Auto-Roles, -DM-Nachrichten und Statistiken. -""" - -import discord -from discord.ext import commands -from DevTools import WelcomeDatabase -import asyncio -import json -import io -import logging -from typing import Optional, Dict, Any -import aiosqlite -from datetime import datetime -import ezcord -from discord.ui import Container - - -# Logger Setup -logger = logging.getLogger(__name__) - - -class WelcomeSystem(ezcord.Cog): - """ - Welcome System für Discord Server. - - Bietet umfassende Willkommensnachrichten mit Embed-Support, - automatischen Rollen, privaten Nachrichten und Statistiken. - - Parameters - ---------- - bot : ezcord.Bot - Die Bot-Instanz - - Attributes - ---------- - bot : ezcord.Bot - Die Bot-Instanz - db : WelcomeDatabase - Datenbank-Handler für Welcome-Einstellungen - _settings_cache : dict - Cache für Server-Einstellungen - _cache_timeout : int - Cache-Timeout in Sekunden (Standard: 300) - _rate_limit_cache : dict - Rate-Limiting Cache für Welcome-Messages - """ - - def __init__(self, bot): - """ - Initialisiert das Welcome System. - - Parameters - ---------- - bot : ezcord.Bot - Die Bot-Instanz - """ - self.bot = bot - self.db = WelcomeDatabase() - # Cache für bessere Performance - self._settings_cache = {} - self._cache_timeout = 300 # 5 Minuten Cache - self._rate_limit_cache = {} # Rate Limiting - - async def get_cached_settings(self, guild_id: int): - """ - Holt Einstellungen mit Cache-Unterstützung. - - Parameters - ---------- - guild_id : int - Discord Server ID - - Returns - ------- - dict or None - Server-Einstellungen aus Cache oder Datenbank - - Notes - ----- - Cache wird nach 5 Minuten automatisch invalidiert. - """ - now = asyncio.get_event_loop().time() - - if guild_id in self._settings_cache: - cached_data, timestamp = self._settings_cache[guild_id] - if now - timestamp < self._cache_timeout: - return cached_data - - # Aus Datenbank laden - settings = await self.db.get_welcome_settings(guild_id) - if settings: - self._settings_cache[guild_id] = (settings, now) - return settings - - def invalidate_cache(self, guild_id: int): - """ - Invalidiert Cache für einen Server. - - Parameters - ---------- - guild_id : int - Discord Server ID - - Notes - ----- - Sollte nach jeder Einstellungsänderung aufgerufen werden. - """ - if guild_id in self._settings_cache: - del self._settings_cache[guild_id] - - def check_rate_limit(self, guild_id: int) -> bool: - """ - Prüft Rate Limit für Server. - - Parameters - ---------- - guild_id : int - Discord Server ID - - Returns - ------- - bool - True wenn Rate Limit nicht erreicht, False sonst - - Notes - ----- - Erlaubt maximal eine Welcome Message alle 5 Sekunden pro Server. - """ - now = asyncio.get_event_loop().time() - if guild_id not in self._rate_limit_cache: - self._rate_limit_cache[guild_id] = now - return True - - last_time = self._rate_limit_cache[guild_id] - if now - last_time >= 5: # 5 Sekunden zwischen Welcome Messages - self._rate_limit_cache[guild_id] = now - return True - - return False - - def replace_placeholders(self, text: str, member: discord.Member, guild: discord.Guild) -> str: - """ - Erweiterte Placeholder-Ersetzung mit Rückwärtskompatibilität. - - Parameters - ---------- - text : str - Text mit Placeholders - member : discord.Member - Discord Member Objekt - guild : discord.Guild - Discord Guild Objekt - - Returns - ------- - str - Text mit ersetzten Placeholders - - Notes - ----- - Unterstützte Placeholder-Kategorien: - - User: %user%, %username%, %mention%, %tag%, %userid% - - Server: %servername%, %server%, %guild%, %serverid%, %membercount% - - Zeit: %joindate%, %jointime%, %createddate%, %createdtime%, %accountage% - - Erweitert: %roles%, %rolecount%, %highestrole%, %avatar% - - Statistiken: %onlinemembers%, %textchannels%, %voicechannels% - - Examples - -------- - >>> text = "Willkommen %mention% auf %servername%!" - >>> replace_placeholders(text, member, guild) - "Willkommen @User auf Mein Server!" - """ - if not text: - return text - - try: - # Basis Placeholder (alte Version) - placeholders = { - '%user%': member.display_name, - '%username%': member.name, - '%mention%': member.mention, - '%tag%': str(member), - '%userid%': str(member.id), - '%servername%': guild.name, - '%serverid%': str(guild.id), - '%membercount%': str(guild.member_count), - '%joindate%': member.joined_at.strftime('%d.%m.%Y') if member.joined_at else 'Unbekannt', - '%createddate%': member.created_at.strftime('%d.%m.%Y'), - '%server%': guild.name, - '%guild%': guild.name, - } - - # Erweiterte Placeholder (neue Version) - try: - # Rolleninformationen - roles = [role.name for role in member.roles if role.name != "@everyone"] - highest_role = member.top_role.name if member.top_role.name != "@everyone" else "Keine" - - # Zeitberechnungen - account_age = (discord.utils.utcnow() - member.created_at).days - - # Online-Member zählen (kann fehlschlagen bei großen Servern) - try: - online_count = sum(1 for m in guild.members if m.status != discord.Status.offline) - except: - online_count = "Unbekannt" - - extended_placeholders = { - # Zeitinformationen - '%jointime%': member.joined_at.strftime('%H:%M') if member.joined_at else 'Unbekannt', - '%createdtime%': member.created_at.strftime('%H:%M'), - '%accountage%': f"{account_age} Tage", - - # Erweiterte Infos - '%discriminator%': member.discriminator if hasattr(member, 'discriminator') else "0000", - '%roles%': ', '.join(roles) if roles else 'Keine', - '%rolecount%': str(len(roles)), - '%highestrole%': highest_role, - '%avatar%': member.display_avatar.url, - '%defaultavatar%': member.default_avatar.url, - - # Server Statistiken - '%onlinemembers%': str(online_count), - '%textchannels%': str(len(guild.text_channels)), - '%voicechannels%': str(len(guild.voice_channels)), - '%categories%': str(len(guild.categories)), - '%emojis%': str(len(guild.emojis)), - } - - placeholders.update(extended_placeholders) - - except Exception as e: - logger.warning(f"Erweiterte Placeholder fehlgeschlagen: {e}") - - except Exception as e: - logger.error(f"Placeholder Fehler: {e}") - return text - - # Placeholder ersetzen - for placeholder, value in placeholders.items(): - text = text.replace(placeholder, str(value)) - - return text - - async def send_welcome_dm(self, member: discord.Member, settings: dict): - """ - Sendet private Willkommensnachricht. - - Parameters - ---------- - member : discord.Member - Neues Mitglied - settings : dict - Server-Einstellungen - - Notes - ----- - Fehler beim DM-Versand werden geloggt aber nicht als Fehler behandelt, - da viele User DMs deaktiviert haben. - """ - try: - if not settings.get('join_dm_enabled'): - return - - dm_message = settings.get('join_dm_message', - 'Willkommen auf **%servername%**! Schön, dass du da bist! 🎉') - - processed_message = self.replace_placeholders(dm_message, member, member.guild) - - await member.send(processed_message) - logger.info(f"Welcome DM an {member} gesendet") - - except discord.Forbidden: - logger.warning(f"Konnte keine DM an {member} senden - DMs deaktiviert") - except Exception as e: - logger.error(f"Fehler beim Senden der Welcome DM: {e}") - - async def assign_auto_role(self, member: discord.Member, settings: dict): - """ - Vergibt automatische Rolle. - - Parameters - ---------- - member : discord.Member - Neues Mitglied - settings : dict - Server-Einstellungen mit auto_role_id - - Notes - ----- - Prüft automatisch Berechtigungen und Rollen-Hierarchie. - """ - try: - auto_role_id = settings.get('auto_role_id') - if not auto_role_id: - return - - role = member.guild.get_role(auto_role_id) - if not role: - logger.warning(f"Auto-Role {auto_role_id} nicht gefunden in {member.guild.name}") - return - - if role >= member.guild.me.top_role: - logger.warning(f"Auto-Role {role.name} ist höher als Bot-Rolle") - return - - await member.add_roles(role, reason="Welcome Auto-Role") - logger.info(f"Auto-Role {role.name} an {member} vergeben") - - except discord.Forbidden: - logger.error(f"Keine Berechtigung für Auto-Role") - except Exception as e: - logger.error(f"Auto-Role Fehler: {e}") - - @commands.Cog.listener() - async def on_member_join(self, member: discord.Member): - """ - Event wird ausgelöst, wenn ein neuer User dem Server beitritt. - - Parameters - ---------- - member : discord.Member - Neues Mitglied - - Notes - ----- - Führt folgende Aktionen aus (wenn aktiviert): - 1. Rate Limiting Check - 2. Einstellungen aus Cache/DB laden - 3. Auto-Role vergeben - 4. Welcome Message senden (Channel) - 5. Welcome DM senden - 6. Statistiken aktualisieren - """ - try: - # Rate Limiting prüfen - if not self.check_rate_limit(member.guild.id): - logger.info(f"Rate Limit aktiv für {member.guild.name}") - return - - settings = await self.get_cached_settings(member.guild.id) - - if not settings or not settings.get('enabled', True): - return - - # Channel validieren - channel_id = settings.get('channel_id') - if not channel_id: - logger.warning(f"Kein Welcome Channel für {member.guild.name} gesetzt") - return - - channel = self.bot.get_channel(channel_id) - if not channel: - logger.error(f"Welcome Channel {channel_id} nicht gefunden") - # Channel aus DB entfernen - await self.db.update_welcome_settings(member.guild.id, channel_id=None) - self.invalidate_cache(member.guild.id) - return - - # Permissions prüfen - perms = channel.permissions_for(member.guild.me) - if not perms.send_messages: - logger.error(f"Keine Send-Berechtigung in {channel.name}") - return - - # Auto-Role vergeben - await self.assign_auto_role(member, settings) - - # Welcome Message - welcome_message = settings.get('welcome_message', 'Willkommen %mention% auf **%servername%**! 🎉') - processed_message = self.replace_placeholders(welcome_message, member, member.guild) - - # Embed oder normale Nachricht - if settings.get('embed_enabled', False) and perms.embed_links: - await self.send_embed_welcome(channel, member, settings, processed_message) - else: - msg = await channel.send(processed_message) - await self.handle_auto_delete(msg, settings) - - # Private Nachricht senden - await self.send_welcome_dm(member, settings) - - # Statistiken aktualisieren - if settings.get('welcome_stats_enabled'): - await self.db.update_welcome_stats(member.guild.id, joins=1) - - except Exception as e: - logger.exception(f"Welcome System Fehler für {member}: {e}") - - async def send_embed_welcome(self, channel, member, settings, processed_message): - """ - Sendet Embed Welcome Message. - - Parameters - ---------- - channel : discord.TextChannel - Ziel-Channel - member : discord.Member - Neues Mitglied - settings : dict - Server-Einstellungen - processed_message : str - Verarbeitete Welcome Message (Fallback) - - Notes - ----- - Fallback auf normale Nachricht bei Embed-Fehlern. - """ - try: - embed = discord.Embed() - - # Embed Farbe - color_hex = settings.get('embed_color', '#00ff00') - try: - color = int(color_hex.replace('#', ''), 16) - embed.color = discord.Color(color) - except: - embed.color = discord.Color.green() - - # Embed Titel - embed_title = settings.get('embed_title') - if embed_title: - embed.title = self.replace_placeholders(embed_title, member, member.guild) - - # Embed Beschreibung - embed_description = settings.get('embed_description') - if embed_description: - embed.description = self.replace_placeholders(embed_description, member, member.guild) - else: - embed.description = processed_message - - # Embed Thumbnail - if settings.get('embed_thumbnail', False): - embed.set_thumbnail(url=member.display_avatar.url) - - # Embed Footer - embed_footer = settings.get('embed_footer') - if embed_footer: - embed.set_footer(text=self.replace_placeholders(embed_footer, member, member.guild)) - - # Nachricht senden - content = member.mention if settings.get('ping_user', False) else None - msg = await channel.send(content=content, embed=embed) - - await self.handle_auto_delete(msg, settings) - - except Exception as e: - logger.error(f"Embed Welcome Fehler: {e}") - # Fallback auf normale Nachricht - msg = await channel.send(processed_message) - await self.handle_auto_delete(msg, settings) - - async def handle_auto_delete(self, message, settings): - """ - Behandelt automatisches Löschen von Nachrichten. - - Parameters - ---------- - message : discord.Message - Zu löschende Nachricht - settings : dict - Server-Einstellungen mit delete_after - - Notes - ----- - Wartet die angegebene Zeit und löscht dann die Nachricht. - Fehler beim Löschen werden geloggt aber nicht weitergegeben. - """ - try: - delete_after = settings.get('delete_after', 0) - if delete_after > 0: - await asyncio.sleep(delete_after) - try: - await message.delete() - except discord.NotFound: - pass # Message bereits gelöscht - except discord.Forbidden: - logger.warning("Keine Berechtigung zum Löschen der Welcome Message") - except Exception as e: - logger.error(f"Auto-Delete Fehler: {e}") - - # Alle Commands bleiben gleich, aber mit Cache-Invalidierung - welcome = discord.SlashCommandGroup("welcome", "Welcome System Einstellungen") - - @welcome.command(name="channel", description="Setzt den Welcome Channel") - @commands.has_permissions(manage_guild=True) - async def set_welcome_channel(self, ctx, channel: discord.TextChannel): - """ - Setzt den Channel für Welcome Messages. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - channel : discord.TextChannel - Ziel-Channel für Welcome Messages - """ - success = await self.db.update_welcome_settings(ctx.guild.id, channel_id=channel.id) - self.invalidate_cache(ctx.guild.id) - - if success: - container = Container() - container.add_text( - f"{emoji_yes} Welcome Channel gesetzt" - ) - container.add_separator() - container.add_text( - f"Welcome Messages werden nun in {channel.mention} gesendet." - ) - view = discord.ui.View(container, timeout=None) - else: - container = Container() - container.add_text( - f"{emoji_no} Fehler" - ) - container.add_separator() - container.add_text( - "Der Welcome Channel konnte nicht gesetzt werden." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view) - - @welcome.command(name="message", description="Setzt die Welcome Message über ein Modal") - @commands.has_permissions(manage_guild=True) - async def set_welcome_message(self, ctx): - """ - Öffnet ein Modal zum Setzen der Welcome Message. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - - Notes - ----- - Zeigt ein Modal mit der aktuellen Message als Vorausfüllung. - Bietet nach dem Speichern eine Vorschau der neuen Message. - """ - - # Aktuelle Einstellungen laden für Vorausfüllung - current_settings = await self.get_cached_settings(ctx.guild.id) - current_message = current_settings.get('welcome_message', '') if current_settings else '' - - class WelcomeMessageModal(discord.ui.Modal): - """ - Modal für Welcome Message Konfiguration. - - Parameters - ---------- - cog : WelcomeSystem - Parent Cog Instanz - current_msg : str, optional - Aktuelle Message für Vorausfüllung - """ - - def __init__(self, cog, current_msg=""): - super().__init__(title="Welcome Message konfigurieren") - self.cog = cog - - self.message_input = discord.ui.InputText( - label="Welcome Message", - placeholder="z.B: Willkommen %mention% auf **%servername%**! 🎉", - style=discord.InputTextStyle.long, - value=current_msg, - max_length=2000, - required=True - ) - self.add_item(self.message_input) - - async def callback(self, interaction: discord.Interaction): - """ - Callback nach Modal-Submit. - - Parameters - ---------- - interaction : discord.Interaction - Modal Interaction - """ - message = self.message_input.value.strip() - - if not message: - embed = discord.Embed( - title="❌ Fehler", - description="Die Welcome Message darf nicht leer sein.", - color=discord.Color.red() - ) - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - success = await self.cog.db.update_welcome_settings(interaction.guild.id, welcome_message=message) - self.cog.invalidate_cache(interaction.guild.id) - - if success: - # Vorschau erstellen - preview = self.cog.replace_placeholders(message, interaction.user, interaction.guild) - - container = Container() - container.add_text( - "# ✅ Welcome Message gesetzt" - ) - container.add_separator() - container.add_text( - "## 💬 Neue Message\n\n" - f"```{message[:500]}{'...' if len(message) > 500 else ''}```" - ) - container.add_separator() - container.add_text( - "## 👀 Vorschau (mit deinen Daten)\n\n" - f"{preview[:500] + ("..." if len(preview) > 500 else "")}\n\n" - "-# 💡 Tipp: Verwende `/welcome test` für eine vollständige Vorschau oder `/welcome placeholders` für alle verfügbaren Optionen." - ) - view = discord.ui.View(container, timeout=None) - else: - container = Container() - container.add_text( - "# ❌ Fehler\nDie Welcome Message konnte nicht gesetzt werden." - ) - view = discord.ui.View(container, timeout=None) - await interaction.response.send_message(view=view) - - modal = WelcomeMessageModal(self, current_message) - await ctx.send_modal(modal) - - @welcome.command(name="toggle", description="Schaltet das Welcome System ein/aus") - @commands.has_permissions(manage_guild=True) - async def toggle_welcome(self, ctx): - """ - Schaltet das Welcome System ein oder aus. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - """ - new_state = await self.db.toggle_welcome(ctx.guild.id) - self.invalidate_cache(ctx.guild.id) - - if new_state is None: - container = Container() - container.add_text( - "# ❌ Fehler\nEs sind noch keine Welcome Einstellungen vorhanden. Setze zuerst einen Channel." - ) - view = discord.ui.View(container, timeout=None) - else: - status = "aktiviert" if new_state else "deaktiviert" - container = Container() - container.add_text( - f"# ✅ Welcome System {status}" - ) - container.add_separator() - container.add_text( - f"Das Welcome System wurde **{status}**." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view) - - @welcome.command(name="embed", description="Aktiviert/Deaktiviert Embed Modus") - @commands.has_permissions(manage_guild=True) - async def toggle_embed(self, ctx, enabled: bool): - """ - Aktiviert oder deaktiviert Embed Welcome Messages. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - enabled : bool - True für Embed-Modus, False für normale Nachrichten - """ - success = await self.db.update_welcome_settings(ctx.guild.id, embed_enabled=enabled) - self.invalidate_cache(ctx.guild.id) - - if success: - status = "aktiviert" if enabled else "deaktiviert" - container = Container( - f"# ✅ Embed Modus {status}" - ) - container.add_separator() - container.add_text( - f"Welcome Messages werden nun {'als Embed' if enabled else 'als normale Nachricht'} gesendet." - ) - view = discord.ui.View(container, timeout=None) - else: - container = Container() - container.add_text( - "# ❌ Fehler\nDer Embed Modus konnte nicht geändert werden." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view) - - @welcome.command(name="autorole", description="Setzt eine Rolle die automatisch vergeben wird") - @commands.has_permissions(manage_roles=True) - async def set_auto_role(self, ctx, role: discord.Role = None): - """ - Setzt eine Rolle die bei Join automatisch vergeben wird. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - role : discord.Role, optional - Rolle zum automatischen Vergeben (None zum Entfernen) - - Notes - ----- - Prüft automatisch die Rollen-Hierarchie. - """ - if role is None: - # Auto-Role entfernen - success = await self.db.update_welcome_settings(ctx.guild.id, auto_role_id=None) - self.invalidate_cache(ctx.guild.id) - - container = Container() - container.add_text( - "# ✅ Auto-Role entfernt" - ) - container.add_separator() - container.add_text( - "Neue Mitglieder erhalten keine automatische Rolle mehr." - ) - view = discord.ui.View(container, timeout=None) - - else: - # Rolle validieren - if role >= ctx.guild.me.top_role: - container = Container() - container.add_text( - "# ❌ Fehler\nDiese Rolle ist höher als meine höchste Rolle. Ich kann sie nicht vergeben." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view) - return - - success = await self.db.update_welcome_settings(ctx.guild.id, auto_role_id=role.id) - self.invalidate_cache(ctx.guild.id) - - if success: - container = Container() - container.add_text( - "# ✅ Auto-Role gesetzt" - ) - container.add_separator() - container.add_text( - f"Neue Mitglieder erhalten automatisch die Rolle {role.mention}." - ) - view = discord.ui.View(container, timeout=None) - else: - container = Container() - container.add_text( - "# ❌ Fehler\nDie Auto-Role konnte nicht gesetzt werden." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view) - - @welcome.command(name="dm", description="Aktiviert/Konfiguriert private Willkommensnachrichten") - @commands.has_permissions(manage_guild=True) - async def setup_join_dm(self, ctx, enabled: bool, *, message: str = None): - """ - Konfiguriert private Willkommensnachrichten. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - enabled : bool - True zum Aktivieren, False zum Deaktivieren - message : str, optional - Custom DM Message (verwendet Standard wenn nicht angegeben) - """ - settings = {'join_dm_enabled': enabled} - if message and enabled: - settings['join_dm_message'] = message - - success = await self.db.update_welcome_settings(ctx.guild.id, **settings) - self.invalidate_cache(ctx.guild.id) - - if success: - if enabled: - if message: - description = f"Private Welcome Messages aktiviert!\n**Nachricht:** {message[:500]}{'...' if len(message) > 500 else ''}" - else: - description = "Private Welcome Messages aktiviert! (Standard-Nachricht wird verwendet)" - else: - description = "Private Welcome Messages deaktiviert." - - container = Container() - container.add_text( - "# ✅ DM Einstellungen aktualisiert" - ) - container.add_separator() - container.add_text( - f"{description}" - ) - view = discord.ui.View(container, timeout=None) - else: - container = Container() - container.add_text( - "# ❌ Fehler\nDie DM Einstellungen konnten nicht aktualisiert werden." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view) - - @welcome.command(name="template", description="Lädt eine Vorlage") - @commands.has_permissions(manage_guild=True) - async def load_template(self, ctx, template_name: str): - """ - Lädt eine vordefinierte Vorlage. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - template_name : str - Name der Vorlage (basic, fancy, minimal, detailed) - - Notes - ----- - Verfügbare Vorlagen: - - basic: Einfache Text-Nachricht - - fancy: Embed mit Thumbnail und Farbe - - minimal: Minimalistischer Text - - detailed: Detailliertes Embed mit vielen Infos - """ - templates = { - "basic": { - "welcome_message": "Willkommen %mention% auf **%servername%**! 🎉", - "embed_enabled": False, - "template_name": "basic" - }, - "fancy": { - "welcome_message": None, - "embed_enabled": True, - "embed_title": "Willkommen auf %servername%! 🎉", - "embed_description": "Hey %user%! Du bist unser **%membercount%.** Mitglied!\n\nViel Spaß auf unserem Server! 🚀", - "embed_color": "#ff6b6b", - "embed_thumbnail": True, - "embed_footer": "Beigetreten am %joindate%", - "template_name": "fancy" - }, - "minimal": { - "welcome_message": "%user% ist dem Server beigetreten.", - "embed_enabled": False, - "template_name": "minimal" - }, - "detailed": { - "welcome_message": None, - "embed_enabled": True, - "embed_title": "🎊 Neues Mitglied!", - "embed_description": "**%mention%** ist **%servername%** beigetreten!\n\n👤 **Username:** %username%\n📅 **Account erstellt:** %createddate%\n📊 **Mitglied Nr.:** %membercount%\n⏰ **Beigetreten um:** %jointime%", - "embed_color": "#00d4ff", - "embed_thumbnail": True, - "embed_footer": "%servername% • %membercount% Mitglieder", - "template_name": "detailed" - } - } - - if template_name not in templates: - available = ", ".join(templates.keys()) - embed = discord.Embed( - title="❌ Unbekannte Vorlage", - description=f"**Verfügbare Vorlagen:** {available}", - color=discord.Color.red() - ) - await ctx.respond(embed=embed) - return - - template = templates[template_name] - success = await self.db.update_welcome_settings(ctx.guild.id, **template) - self.invalidate_cache(ctx.guild.id) - - if success: - embed = discord.Embed( - title=f"✅ Vorlage '{template_name}' geladen", - description="Die Welcome-Konfiguration wurde aktualisiert.", - color=discord.Color.green() - ) - - # Vorschau anzeigen - if template_name == "basic": - embed.add_field(name="Vorschau", value="Willkommen @User auf **Servername**! 🎉", inline=False) - elif template_name == "minimal": - embed.add_field(name="Vorschau", value="Username ist dem Server beigetreten.", inline=False) - else: - embed.add_field(name="Typ", value="Embed-Nachricht", inline=False) - else: - embed = discord.Embed( - title="❌ Fehler", - description="Die Vorlage konnte nicht geladen werden.", - color=discord.Color.red() - ) - - await ctx.respond(embed=embed) - - @welcome.command(name="config", description="Zeigt die aktuelle Konfiguration") - @commands.has_permissions(manage_messages=True) - async def show_config(self, ctx): - """ - Zeigt die aktuelle Welcome Konfiguration. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - - Notes - ----- - Zeigt alle konfigurierten Einstellungen übersichtlich an. - """ - settings = await self.get_cached_settings(ctx.guild.id) - - if not settings: - container = Container() - container.add_text( - "# ❌ Keine Konfiguration gefunden\nEs sind noch keine Welcome Einstellungen vorhanden." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view) - return - - channel = self.bot.get_channel(settings.get('channel_id')) if settings.get('channel_id') else None - auto_role = ctx.guild.get_role(settings.get('auto_role_id')) if settings.get('auto_role_id') else None - container = Container() - container.add_text( - "# ⚙️ Welcome System Konfiguration" - ) - container.add_separator() - container.add_text( - "## 📊 Status\n" - f"{'✅ Aktiviert' if settings.get('enabled') else '❌ Deaktiviert'}" - ) - - container.add_text( - "## 📢 Channel\n" - f"{channel.mention if channel else '❌ Nicht gesetzt'}" - ) - - container.add_text( - "## 🎨 Embed Modus\n" - f"{'✅ Aktiviert' if settings.get('embed_enabled') else '❌ Deaktiviert'}" - ) - - container.add_text( - "## 🏷️ Auto-Role\n" - f"{auto_role.mention if auto_role else '❌ Rolle nicht gefunden'}" - ) - - if settings.get('join_dm_enabled'): - container.add_text( - "## 💌 Private Nachricht\n✅ Aktiviert" - ) - - if settings.get('template_name'): - container.add_text( - "## 📋 Vorlage\n" - f"{settings.get('template_name').title()}" - ) - - message = settings.get('welcome_message', 'Nicht gesetzt') - if len(message) > 100: - message = message[:100] + "..." - container.add_text( - "## 💬 Welcome Message\n" - f"{message}" - ) - - if settings.get('delete_after', 0) > 0: - container.add_text( - "## 🗑️ Auto-Delete\n" - f"{settings.get('delete_after')} Sekunden" - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view) - - @welcome.command(name="test", description="Testet die Welcome Message") - @commands.has_permissions(manage_messages=True) - async def test_welcome(self, ctx): - """ - Testet die Welcome Message mit dem aktuellen User. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - - Notes - ----- - Simuliert einen Member Join mit den aktuellen Einstellungen. - Zeigt eine Vorschau ohne tatsächlich eine Welcome Message zu senden. - """ - settings = await self.get_cached_settings(ctx.guild.id) - - if not settings: - container = Container() - container.add_text( - "# ❌ Fehler\nEs sind noch keine Welcome Einstellungen vorhanden." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - return - - if not settings.get('channel_id'): - container = Container() - container.add_text( - "# ❌ Fehler\nEs ist kein Welcome Channel gesetzt." - ) - view = discord.ui.View(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - return - - # Simuliere Member Join Event - member = ctx.author - welcome_message = settings.get('welcome_message', 'Willkommen %mention% auf **%servername%**! 🎉') - processed_message = self.replace_placeholders(welcome_message, member, ctx.guild) - - embed = discord.Embed( - title="🧪 Welcome Message Test", - color=discord.Color.blue() - ) - - if settings.get('embed_enabled'): - embed.add_field( - name="Typ", - value="Embed-Nachricht", - inline=True - ) - - test_embed_title = settings.get('embed_title', 'Kein Titel') - if test_embed_title: - test_embed_title = self.replace_placeholders(test_embed_title, member, ctx.guild) - embed.add_field(name="Embed Titel", value=test_embed_title, inline=False) - - test_embed_desc = settings.get('embed_description', processed_message) - if test_embed_desc: - test_embed_desc = self.replace_placeholders(test_embed_desc, member, ctx.guild) - embed.add_field(name="Embed Beschreibung", value=test_embed_desc[:500] + ("..." if len(test_embed_desc) > 500 else ""), inline=False) - else: - embed.add_field( - name="Typ", - value="Normale Nachricht", - inline=True - ) - embed.add_field( - name="Vorschau", - value=processed_message[:500] + ("..." if len(processed_message) > 500 else ""), - inline=False - ) - - # Zusätzliche Infos - if settings.get('auto_role_id'): - auto_role = ctx.guild.get_role(settings.get('auto_role_id')) - embed.add_field( - name="🏷️ Auto-Role", - value=auto_role.mention if auto_role else "❌ Rolle nicht gefunden", - inline=True - ) - - if settings.get('join_dm_enabled'): - embed.add_field( - name="💌 Private Nachricht", - value="✅ Würde gesendet werden", - inline=True - ) - - await ctx.respond(embed=embed, ephemeral=True) - - @welcome.command(name="placeholders", description="Zeigt alle verfügbaren Placeholder") - async def show_placeholders(self, ctx): - """ - Zeigt alle verfügbaren Placeholder. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - - Notes - ----- - Liste aller unterstützten Placeholder mit Beschreibungen. - """ - embed = discord.Embed( - title="📝 Verfügbare Placeholder", - description="Diese Placeholder können in Welcome Messages verwendet werden:", - color=discord.Color.blue() - ) - - embed.add_field( - name="👤 User Informationen", - value=( - "`%user%` - Username (Display Name)\n" - "`%username%` - Echter Username\n" - "`%mention%` - User erwähnen (@User)\n" - "`%tag%` - User#1234\n" - "`%userid%` - User ID\n" - "`%discriminator%` - User Discriminator" - ), - inline=False - ) - - embed.add_field( - name="🏠 Server Informationen", - value=( - "`%servername%` - Servername\n" - "`%server%` - Servername (Alternative)\n" - "`%guild%` - Servername (Alternative)\n" - "`%serverid%` - Server ID\n" - "`%membercount%` - Mitgliederanzahl\n" - "`%onlinemembers%` - Online Mitglieder" - ), - inline=False - ) - - embed.add_field( - name="⏰ Zeit & Datum", - value=( - "`%joindate%` - Beitrittsdatum (DD.MM.YYYY)\n" - "`%jointime%` - Beitrittszeit (HH:MM)\n" - "`%createddate%` - Account Erstellung (DD.MM.YYYY)\n" - "`%createdtime%` - Account Erstellung (HH:MM)\n" - "`%accountage%` - Account Alter in Tagen" - ), - inline=False - ) - - embed.add_field( - name="🎭 Erweiterte Informationen", - value=( - "`%roles%` - Alle Rollen (außer @everyone)\n" - "`%rolecount%` - Anzahl der Rollen\n" - "`%highestrole%` - Höchste Rolle\n" - "`%avatar%` - Avatar URL\n" - "`%defaultavatar%` - Standard Avatar URL" - ), - inline=False - ) - - embed.add_field( - name="📊 Server Statistiken", - value=( - "`%textchannels%` - Anzahl Textchannels\n" - "`%voicechannels%` - Anzahl Voicechannels\n" - "`%categories%` - Anzahl Kategorien\n" - "`%emojis%` - Anzahl Emojis" - ), - inline=False - ) - - embed.set_footer(text="Beispiel: Willkommen %mention%! Du bist Mitglied #%membercount% auf %servername%") - - await ctx.respond(embed=embed, ephemeral=True) - - @welcome.command(name="export", description="Exportiert die Welcome Konfiguration") - @commands.has_permissions(administrator=True) - async def export_config(self, ctx): - """ - Exportiert die aktuelle Konfiguration als JSON-Datei. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - - Notes - ----- - Erstellt eine JSON-Datei mit allen Einstellungen. - Sensible Daten (IDs, Timestamps) werden entfernt. - """ - settings = await self.get_cached_settings(ctx.guild.id) - if not settings: - embed = discord.Embed( - title="❌ Keine Konfiguration zum Exportieren", - description="Es sind noch keine Welcome Einstellungen vorhanden.", - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - return - - try: - # Sensible Daten entfernen - export_data = {k: v for k, v in settings.items() - if k not in ['guild_id', 'created_at', 'updated_at']} - - # JSON Export erstellen - config_json = json.dumps(export_data, indent=2, ensure_ascii=False) - - # Als Datei senden - file_content = f"# Welcome System Export für {ctx.guild.name}\n# Exportiert am {datetime.now().strftime('%d.%m.%Y %H:%M')}\n\n{config_json}" - file = discord.File( - io.StringIO(file_content), - filename=f"welcome_config_{ctx.guild.name.replace(' ', '_')}.json" - ) - - embed = discord.Embed( - title="📄 Konfiguration exportiert", - description="Die aktuelle Welcome-Konfiguration wurde als Datei exportiert.", - color=discord.Color.green() - ) - - await ctx.respond(embed=embed, file=file, ephemeral=True) - - except Exception as e: - logger.error(f"Export Fehler: {e}") - embed = discord.Embed( - title="❌ Export fehlgeschlagen", - description="Es ist ein Fehler beim Exportieren aufgetreten.", - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - - @welcome.command(name="stats", description="Zeigt Welcome Statistiken") - @commands.has_permissions(manage_messages=True) - async def show_stats(self, ctx): - """ - Zeigt Welcome Statistiken für den Server. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - - Notes - ----- - Zeigt Statistiken für: - - Heute - - Diese Woche (letzte 7 Tage) - - Gesamt (seit Aktivierung) - """ - try: - await self.db.migrate_database() - - # Statistiken aktivieren falls noch nicht geschehen - settings = await self.get_cached_settings(ctx.guild.id) - if settings and not settings.get('welcome_stats_enabled'): - await self.db.update_welcome_settings(ctx.guild.id, welcome_stats_enabled=True) - self.invalidate_cache(ctx.guild.id) - - # Aktuelle Statistiken aus der DB holen - try: - async with aiosqlite.connect(self.db.db_path) as conn: - # Heute - today = datetime.now().strftime('%Y-%m-%d') - cursor = await conn.execute( - 'SELECT joins, leaves FROM welcome_stats WHERE guild_id = ? AND date = ?', - (ctx.guild.id, today) - ) - today_stats = await cursor.fetchone() - - # Letzte 7 Tage - cursor = await conn.execute(''' - SELECT SUM(joins) as total_joins, SUM(leaves) as total_leaves - FROM welcome_stats - WHERE guild_id = ? AND date >= date('now', '-7 days') - ''', (ctx.guild.id,)) - week_stats = await cursor.fetchone() - - # Gesamt - cursor = await conn.execute(''' - SELECT SUM(joins) as total_joins, SUM(leaves) as total_leaves - FROM welcome_stats - WHERE guild_id = ? - ''', (ctx.guild.id,)) - total_stats = await cursor.fetchone() - - embed = discord.Embed( - title="📊 Welcome Statistiken", - description=f"Statistiken für **{ctx.guild.name}**", - color=discord.Color.blue() - ) - - # Heute - today_joins = today_stats[0] if today_stats else 0 - today_leaves = today_stats[1] if today_stats else 0 - embed.add_field( - name="📅 Heute", - value=f"👋 **Beigetreten:** {today_joins}\n🚪 **Verlassen:** {today_leaves}", - inline=True - ) - - # Diese Woche - week_joins = week_stats[0] if week_stats and week_stats[0] else 0 - week_leaves = week_stats[1] if week_stats and week_stats[1] else 0 - embed.add_field( - name="📅 Diese Woche", - value=f"👋 **Beigetreten:** {week_joins}\n🚪 **Verlassen:** {week_leaves}", - inline=True - ) - - # Gesamt - total_joins = total_stats[0] if total_stats and total_stats[0] else 0 - total_leaves = total_stats[1] if total_stats and total_stats[1] else 0 - embed.add_field( - name="📊 Gesamt", - value=f"👋 **Beigetreten:** {total_joins}\n🚪 **Verlassen:** {total_leaves}", - inline=True - ) - - # Aktuelle Server Info - embed.add_field( - name="ℹ️ Server Info", - value=f"👥 **Aktuelle Mitglieder:** {ctx.guild.member_count}\n📈 **Netto Wachstum:** {total_joins - total_leaves}", - inline=False - ) - - embed.set_footer(text="Statistiken werden seit der Aktivierung des Systems gesammelt") - - except Exception as e: - logger.error(f"Stats DB Error: {e}") - embed = discord.Embed( - title="📊 Welcome Statistiken", - description="Statistiken werden ab sofort gesammelt und beim nächsten Aufruf angezeigt.", - color=discord.Color.blue() - ) - embed.add_field( - name="ℹ️ Server Info", - value=f"👥 **Aktuelle Mitglieder:** {ctx.guild.member_count}", - inline=False - ) - - await ctx.respond(embed=embed) - - except Exception as e: - logger.error(f"Stats Command Error: {e}") - embed = discord.Embed( - title="❌ Fehler", - description="Statistiken konnten nicht geladen werden.", - color=discord.Color.red() - ) - await ctx.respond(embed=embed, ephemeral=True) - - @welcome.command(name="reset", description="Setzt alle Welcome Einstellungen zurück") - @commands.has_permissions(administrator=True) - async def reset_welcome(self, ctx): - """ - Setzt alle Welcome Einstellungen zurück. - - Parameters - ---------- - ctx : discord.ApplicationContext - Slash Command Context - - Notes - ----- - Zeigt eine Bestätigungsabfrage vor dem Löschen. - Diese Aktion kann nicht rückgängig gemacht werden. - """ - - # Bestätigungs-View - class ConfirmView(discord.ui.View): - """ - Bestätigungs-View für Reset. - - Attributes - ---------- - confirmed : bool - Ob der Reset bestätigt wurde - """ - - def __init__(self): - super().__init__(timeout=30) - self.confirmed = False - - @discord.ui.button(label="✅ Ja, zurücksetzen", style=discord.ButtonStyle.danger) - async def confirm_button(self, button: discord.ui.Button, interaction: discord.Interaction): - """ - Bestätigung des Resets. - - Parameters - ---------- - button : discord.ui.Button - Geklickter Button - interaction : discord.Interaction - Button Interaction - """ - self.confirmed = True - self.stop() - - success = await ctx.cog.db.delete_welcome_settings(ctx.guild.id) - ctx.cog.invalidate_cache(ctx.guild.id) - - if success: - embed = discord.Embed( - title="✅ Einstellungen zurückgesetzt", - description="Alle Welcome Einstellungen wurden erfolgreich gelöscht.", - color=discord.Color.green() - ) - else: - embed = discord.Embed( - title="❌ Fehler", - description="Die Einstellungen konnten nicht zurückgesetzt werden.", - color=discord.Color.red() - ) - - await interaction.response.edit_message(embed=embed, view=None) - - @discord.ui.button(label="❌ Abbrechen", style=discord.ButtonStyle.secondary) - async def cancel_button(self, button: discord.ui.Button, interaction: discord.Interaction): - """ - Abbruch des Resets. - - Parameters - ---------- - button : discord.ui.Button - Geklickter Button - interaction : discord.Interaction - Button Interaction - """ - self.stop() - - embed = discord.Embed( - title="❌ Abgebrochen", - description="Die Einstellungen wurden nicht zurückgesetzt.", - color=discord.Color.orange() - ) - - await interaction.response.edit_message(embed=embed, view=None) - - embed = discord.Embed( - title="⚠️ Einstellungen zurücksetzen", - description="Bist du sicher, dass du **alle** Welcome Einstellungen löschen möchtest?\n\n**Diese Aktion kann nicht rückgängig gemacht werden!**", - color=discord.Color.orange() - ) - - view = ConfirmView() - await ctx.respond(embed=embed, view=view, ephemeral=True) - - # Event Listeners für Statistiken - @commands.Cog.listener() - async def on_member_remove(self, member: discord.Member): - """ - Tracking für Member Leaves. - - Parameters - ---------- - member : discord.Member - Mitglied das den Server verlassen hat - - Notes - ----- - Aktualisiert die Statistiken wenn aktiviert. - """ - try: - settings = await self.get_cached_settings(member.guild.id) - if settings and settings.get('welcome_stats_enabled'): - await self.db.update_welcome_stats(member.guild.id, leaves=1) - except Exception as e: - logger.error(f"Leave Stats Error: {e}") - - -def setup(bot): - """ - Setup-Funktion für das Cog. - - Parameters - ---------- - bot : ezcord.Bot - Bot-Instanz - - Notes - ----- - Wird automatisch von discord.py beim Laden des Cogs aufgerufen. - """ - bot.add_cog(WelcomeSystem(bot)) \ No newline at end of file diff --git a/src/cogs/fun/__init__.py b/src/cogs/fun/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/cogs/fun/gewinnt.py b/src/cogs/fun/gewinnt.py deleted file mode 100644 index 322723f..0000000 --- a/src/cogs/fun/gewinnt.py +++ /dev/null @@ -1,214 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────── -# >> Import -# ─────────────────────────────────────────────── -from discord.ui import Button, View -import discord -from discord.ext import commands -import ezcord -import yaml -from pathlib import Path - -# ─────────────────────────────────────────────── -# >> Constants -# ─────────────────────────────────────────────── -ROWS = 6 -COLUMNS = 7 - -# ─────────────────────────────────────────────── -# >> Load messages from YAML -# ─────────────────────────────────────────────── -def load_messages(lang_code: str): - """ - Lädt Nachrichten für den angegebenen Sprachcode. - Fällt auf 'en' und dann auf 'de' zurück, falls die Datei fehlt. - """ - base_path = Path("translation") / "messages" - - # 1. Versuch: Gewünschte Sprache - lang_file = base_path / f"{lang_code}.yaml" - - # 2. Versuch: Standard (Englisch) - if not lang_file.exists(): - lang_file = base_path / "en.yaml" - - # 3. Versuch: Fallback (Deutsch) - if not lang_file.exists(): - lang_file = base_path / "de.yaml" - - # Kritischer Fehler, wenn keine der drei Dateien existiert - if not lang_file.exists(): - raise FileNotFoundError(f"Missing language files: {lang_code}.yaml, en.yaml, and de.yaml") - - with open(lang_file, "r", encoding="utf-8") as f: - return yaml.safe_load(f) - -# ─────────────────────────────────────────────── -# >> Button & View -# ─────────────────────────────────────────────── -class Connect4Button(Button): - def __init__(self, column, view): - super().__init__(style=discord.ButtonStyle.secondary, label=str(column + 1)) - self.column = column - self.view_ref = view - - async def callback(self, interaction: discord.Interaction): - view = self.view_ref - msgs = view.messages - - if interaction.user != view.current_player: - await interaction.response.send_message( - msgs["cog_4gewinnt"]["error_types"]["not_your_turn"], - ephemeral=True - ) - return - - if not view.make_move(self.column): - await interaction.response.send_message( - msgs["cog_4gewinnt"]["error_types"]["this_column_full"], - ephemeral=True - ) - return - - winner = view.check_winner() - board_str = view.board_to_str() - - if winner or view.is_draw(): - for child in view.children: - child.disabled = True - - content = "" - if winner: - content = msgs["cog_4gewinnt"]["win_types"]["win"].format( - winner=view.current_player.mention, - board_str=board_str - ) - elif view.is_draw(): - content = msgs["cog_4gewinnt"]["win_types"]["draw"].format( - board_str=board_str - ) - - await interaction.response.edit_message( - content=content, - view=view - ) - view.stop() - return - - view.switch_player() - await interaction.response.edit_message( - content=msgs["cog_4gewinnt"]["message"]["player_turn"].format( - view=view, - board_str=board_str - ), - view=view - ) - -class Connect4View(View): - def __init__(self, player1, player2, messages): - super().__init__(timeout=180) - self.player1 = player1 - self.player2 = player2 - self.current_player = player1 - self.current_symbol = "🔴" - self.board = [["⚪" for _ in range(COLUMNS)] for _ in range(ROWS)] - self.messages = messages - - for col in range(COLUMNS): - self.add_item(Connect4Button(col, self)) - - def make_move(self, column): - for row in reversed(range(ROWS)): - if self.board[row][column] == "⚪": - self.board[row][column] = self.current_symbol - return True - return False - - def switch_player(self): - if self.current_player == self.player1: - self.current_player = self.player2 - self.current_symbol = "🟡" - else: - self.current_player = self.player1 - self.current_symbol = "🔴" - - def check_winner(self): - b = self.board - # horizontal - for row in range(ROWS): - for col in range(COLUMNS - 3): - line = b[row][col:col+4] - if line.count(line[0]) == 4 and line[0] != "⚪": - return True - # vertikal - for col in range(COLUMNS): - for row in range(ROWS - 3): - line = [b[row+i][col] for i in range(4)] - if line.count(line[0]) == 4 and line[0] != "⚪": - return True - # diagonal rechts unten - for row in range(ROWS - 3): - for col in range(COLUMNS - 3): - line = [b[row+i][col+i] for i in range(4)] - if line.count(line[0]) == 4 and line[0] != "⚪": - return True - # diagonal rechts oben - for row in range(3, ROWS): - for col in range(COLUMNS - 3): - line = [b[row-i][col+i] for i in range(4)] - if line.count(line[0]) == 4 and line[0] != "⚪": - return True - return None - - def is_draw(self): - return all(cell != "⚪" for row in self.board for cell in row) - - def board_to_str(self): - return "\n".join("".join(row) for row in self.board) - - -# ─────────────────────────────────────────────── -# >> Cog -# ─────────────────────────────────────────────── -class Connect4Cog(ezcord.Cog, group="fun"): - @commands.slash_command(name="connect4", description="Starte ein 4 Gewinnt Spiel mit jemandem!") - async def connect4(self, ctx: discord.ApplicationContext, opponent: discord.Member): - - try: - lang_code = self.bot.get_user_language(ctx.author.id) - except AttributeError: - lang_code = "de" - - try: - messages = load_messages(lang_code) - except FileNotFoundError as e: - print(f"CRITICAL: {e}") - messages = {"cog_4gewinnt": {"error_types": {"is_opponent_bot": "Error: Missing language file."}, - "message": {"start_game": "Error: Missing language file."}}} - - if opponent.bot: - await ctx.respond( - messages["cog_4gewinnt"]["error_types"]["is_opponent_bot"], - ephemeral=True - ) - return - if opponent == ctx.author: - await ctx.respond( - messages["cog_4gewinnt"]["error_types"]["is_opponent_self"], - ephemeral=True - ) - return - - view = Connect4View(ctx.author, opponent, messages) - - # 🟢 KORREKTUR: Stabile Formatierung - await ctx.respond( - messages["cog_4gewinnt"]["message"]["start_game"].format( - author_mention=ctx.author.mention, - opponent_mention=opponent.mention - ) + view.board_to_str(), - view=view - ) - -def setup(bot): - bot.add_cog(Connect4Cog(bot)) \ No newline at end of file diff --git a/src/cogs/fun/tictactoe.py b/src/cogs/fun/tictactoe.py deleted file mode 100644 index 65ab4e7..0000000 --- a/src/cogs/fun/tictactoe.py +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────── -# >> Import -# ─────────────────────────────────────────────── -from discord.ui import Button, View -import discord -from discord.ext import commands -import ezcord -import yaml -from pathlib import Path -# ─────────────────────────────────────────────── -# >> Hilfsfunktionen -# ─────────────────────────────────────────────── - -def load_messages(lang_code: str): - """ - Lädt Nachrichten für den angegebenen Sprachcode. - Fällt auf 'en' und dann auf 'de' zurück, falls die Datei fehlt. - """ - base_path = Path("translation") / "messages" - - # 1. Versuch: Gewünschte Sprache - lang_file = base_path / f"{lang_code}.yaml" - - # 2. Versuch: Standard (Englisch) - if not lang_file.exists(): - lang_file = base_path / "en.yaml" - - # 3. Versuch: Fallback (Deutsch) - if not lang_file.exists(): - lang_file = base_path / "de.yaml" - - # Kritischer Fehler, wenn keine der drei Dateien existiert - if not lang_file.exists(): - # Da dies nur beim Laden eines Commands passiert, keine exit() nötig - print(f"WARNUNG: Keine Sprachdatei für '{lang_code}' gefunden. Verwende leere Texte.") - return {} - - with open(lang_file, "r", encoding="utf-8") as f: - return yaml.safe_load(f) - -# 🔴 ENTFERNT: Die globale 'messages' Variable wird entfernt. -# Die Nachrichten werden jetzt in der Cog-Methode geladen. - - -class TicTacToeButton(Button): - def __init__(self, x, y): - super().__init__(style=discord.ButtonStyle.secondary, label="\u200b", row=x) - self.x = x - self.y = y - self.clicked = False - # Speichere die Nachrichten direkt im Button für den Callback - # Siehe Callback: messages werden aus der View geholt - - async def callback(self, interaction: discord.Interaction): - view: TicTacToeView = self.view - messages = view.messages # 🌟 NEU: Nachrichten aus der View abrufen - - # 🟢 Korrigierte i18n-Nutzung: Nicht dein Zug - if interaction.user != view.current_player: - await interaction.response.send_message( - messages.get("cog_tictactoe", {}).get("error_types", {}).get("not_your_turn", "Not your turn!"), - ephemeral=True - ) - return - - # 🟢 Korrigierte i18n-Nutzung: Feld belegt - if self.clicked: - await interaction.response.send_message( - messages.get("cog_tictactoe", {}).get("error_types", {}).get("this_cell_taken", "This cell is already taken!"), - ephemeral=True - ) - return - - # ... (Spiellogik bleibt gleich) ... - self.clicked = True - if view.current_turn == 0: - self.style = discord.ButtonStyle.danger # rot = X - self.label = "X" - view.board[self.x][self.y] = "X" - view.current_turn = 1 - view.current_player = view.player2 - else: - self.style = discord.ButtonStyle.success # grün = O - self.label = "O" - view.board[self.x][self.y] = "O" - view.current_turn = 0 - view.current_player = view.player1 - - winner = view.check_winner() - - if winner: - for child in view.children: - child.disabled = True - - # 🟢 Korrigierte i18n-Nutzung: Gewinn - win_msg = messages.get("cog_tictactoe", {}).get("win_types", {}).get("win", "WINNER: {winner}").format(winner=winner) - await interaction.response.edit_message(content=win_msg, view=view) - view.stop() - - elif view.is_draw(): - for child in view.children: - child.disabled = True - - # 🟢 Korrigierte i18n-Nutzung: Unentschieden - draw_msg = messages.get("cog_tictactoe", {}).get("win_types", {}).get("draw", "It's a draw!") - await interaction.response.edit_message(content=draw_msg, view=view) - view.stop() - - else: - # 🌟 NEU: I18N für den Zugwechsel - next_turn_msg = messages.get("cog_tictactoe", {}).get("message", {}).get("next_turn", "It is now {player}'s turn!").format( - player=view.current_player.mention - ) - await interaction.response.edit_message(content=next_turn_msg, view=view) - -class TicTacToeView(View): - def __init__(self, player1, player2, messages): # 🌟 NEU: Nachrichten werden übergeben - super().__init__(timeout=120) - self.player1 = player1 - self.player2 = player2 - self.current_player = player1 - self.current_turn = 0 # 0 = X (player1), 1 = O (player2) - self.board = [["" for _ in range(3)] for _ in range(3)] - self.messages = messages # 🌟 NEU: Nachrichten werden hier gespeichert - - for x in range(3): - for y in range(3): - self.add_item(TicTacToeButton(x, y)) - - # check_winner und is_draw bleiben unverändert - def check_winner(self): - # ... (Ihre bestehende Logik) ... - b = self.board - players_map = {"X": self.player1, "O": self.player2} - for i in range(3): - if b[i][0] == b[i][1] == b[i][2] != "": - winner_symbol = b[i][0] - return f"{winner_symbol} ({players_map[winner_symbol].display_name})" - for i in range(3): - if b[0][i] == b[1][i] == b[2][i] != "": - winner_symbol = b[0][i] - return f"{winner_symbol} ({players_map[winner_symbol].display_name})" - if b[0][0] == b[1][1] == b[2][2] != "": - winner_symbol = b[0][0] - return f"{winner_symbol} ({players_map[winner_symbol].display_name})" - if b[0][2] == b[1][1] == b[2][0] != "": - winner_symbol = b[0][2] - return f"{winner_symbol} ({players_map[winner_symbol].display_name})" - return None - - def is_draw(self): - return all(cell != "" for row in self.board for cell in row) - - -class fun(ezcord.Cog): - def __init__(self, bot): - self.bot = bot - - @commands.slash_command(name="tictactoe", description="Starte ein Tic Tac Toe Spiel mit jemandem!") - async def tictactoe(self, ctx: discord.ApplicationContext, opponent: discord.Member): - - # 🌟 NEU: Rufe den Sprachcode aus der Datenbank ab - # Annahme: Ihre db-Methode ist get_user_language - lang_code = self.bot.settings_db.get_user_language(ctx.author.id) - - # 🌟 NEU: Lade die korrekten Nachrichten für den Benutzer - messages = load_messages(lang_code) - - # 🟢 Korrigierte i18n-Nutzung: Gegner ist Bot - if opponent.bot: - await ctx.respond( - messages.get("cog_tictactoe", {}).get("error_types", {}).get("is_opponent_bot", "You cannot challenge a bot."), - ephemeral=True - ) - return - - # 🟢 Korrigierte i18n-Nutzung: Gegner ist man selbst - if opponent == ctx.author: - await ctx.respond( - messages.get("cog_tictactoe", {}).get("error_types", {}).get("is_opponent_self", "You cannot challenge yourself."), - ephemeral=True - ) - return - - # 🌟 NEU: Übergebe Nachrichten an die View - view = TicTacToeView(ctx.author, opponent, messages) - - # 🟢 KORREKTUR: Stabile Formatierung zur Behebung des Hängens während der Synchronisierung. - start_msg = messages.get("cog_tictactoe", {}).get("message", {}).get("start_game", "Tic Tac Toe: {author_mention} vs {opponent_mention}").format( - author_mention=ctx.author.mention, - opponent_mention=opponent.mention - ) - await ctx.respond(start_msg, view=view) - -def setup(bot): - bot.add_cog(fun(bot)) \ No newline at end of file diff --git a/src/cogs/fun/weather.py b/src/cogs/fun/weather.py deleted file mode 100644 index 8f6bb6e..0000000 --- a/src/cogs/fun/weather.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -import requests -import discord -from discord import slash_command -from discord.ui import Container -import ezcord -import os -from pathlib import Path -import yaml - -WEATHER_API = os.getenv("WEATHER_API") - -# -------------------------- -# Hilfsfunktion für Nachrichten -# -------------------------- -def load_messages(lang_code: str): - base_path = Path("translation") / "messages" - - lang_file = base_path / f"{lang_code}.yaml" - if not lang_file.exists(): - lang_file = base_path / "en.yaml" - if not lang_file.exists(): - lang_file = base_path / "de.yaml" - if not lang_file.exists(): - print(f"WARNUNG: Keine Sprachdatei für '{lang_code}' gefunden. Verwende leere Texte.") - return {} - - with open(lang_file, "r", encoding="utf-8") as f: - return yaml.safe_load(f) - -# -------------------------- -# Weather Cog -# -------------------------- -class Weather(ezcord.Cog, group="fun"): - def __init__(self, bot: ezcord.Bot): - self.bot = bot - - @slash_command(name="weather", description="Erhalte das Wetter für eine Stadt") - async def weather(self, ctx: discord.ApplicationContext, city: str): - """Get the weather for a city""" - - # 🌟 Benutzer-spezifische Sprache laden - lang_code = self.bot.settings_db.get_user_language(ctx.author.id) - messages = load_messages(lang_code) - - url = f"http://api.weatherapi.com/v1/current.json?key={WEATHER_API}&q={city}&lang={lang_code}" - - try: - response = requests.get(url, timeout=10) - response.raise_for_status() - data = response.json() - except requests.RequestException: - await ctx.respond( - messages.get("cog_weather", {}).get("error_types", {}).get( - "api_error", "Error with the weather API." - ) - ) - return - - if "error" in data: - await ctx.respond( - messages.get("cog_weather", {}).get("error_types", {}).get( - "city_not_found", f"⚠️ Error: {data['error']['message']}" - ) - ) - return - - location = data['location'] - current = data['current'] - - container = Container() - - # Übersetzbarer Header - container.add_text( - messages.get("cog_weather", {}).get("messages", {}).get( - "weather_report", "Weather report for {city}, {country}\n" - ).format(city=location['name'], country=location['country']) - ) - container.add_separator() - - # Übersetzbare Details - details = ( - messages.get("cog_weather", {}).get("messages", {}).get( - "temperature", "Temperature: {temperature}°C\n" - ).format(temperature=current['temp_c']) + - messages.get("cog_weather", {}).get("messages", {}).get( - "humidity", "Humidity: {humidity}%\n" - ).format(humidity=current['humidity']) + - messages.get("cog_weather", {}).get("messages", {}).get( - "wind_speed", "Wind speed: {wind_speed} km/h ({wind_dir})\n" - ).format(wind_speed=current['wind_kph'], wind_dir=current['wind_dir']) + - messages.get("cog_weather", {}).get("messages", {}).get( - "condition", "Condition: {condition}\n" - ).format(condition=current['condition']['text']) + - messages.get("cog_weather", {}).get("messages", {}).get( - "visibility", "Visibility: {visibility} km\n" - ).format(visibility=current['vis_km']) + - messages.get("cog_weather", {}).get("messages", {}).get( - "pressure", "Pressure: {pressure} hPa\n" - ).format(pressure=current['pressure_mb']) - ) - - container.add_text(details) - - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view) - -def setup(bot: ezcord.Bot): - bot.add_cog(Weather(bot)) diff --git a/src/cogs/fun/wikipedia/__init__.py b/src/cogs/fun/wikipedia/__init__.py deleted file mode 100644 index d708692..0000000 --- a/src/cogs/fun/wikipedia/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────────── -# >> Wikipedia Bot Package -# ─────────────────────────────────────────────────── -""" -Wikipedia Bot für Discord - -Ein umfassender Wikipedia-Bot mit Unterstützung für mehrere Sprachen, -Caching, interaktive UI-Komponenten und erweiterte Suchfunktionen. -""" - -__version__ = "2.0.0" -__author__ = "OPPRO.NET Network" - -from .cog import WikipediaCog, setup -from .config import WIKI_CONFIG, LANGUAGE_CHOICES -from .cache import WikiCache, wiki_cache -from .utils import clean_text, format_page_info -from .containers import ( - create_article_container, - create_error_container, - create_disambiguation_container, - create_loading_container, - create_random_article_container -) -from .components import ( - LanguageSelectContainer, - ArticleButtonContainer, - RandomArticleButton, - ArticleInfoButton, - RefreshArticleButton -) -from .autocomplete import enhanced_wiki_autocomplete - -__all__ = [ - # Main - 'WikipediaCog', - 'setup', - - # Config - 'WIKI_CONFIG', - 'LANGUAGE_CHOICES', - - # Cache - 'WikiCache', - 'wiki_cache', - - # Utils - 'clean_text', - 'format_page_info', - - # Containers - 'create_article_container', - 'create_error_container', - 'create_disambiguation_container', - 'create_loading_container', - 'create_random_article_container', - - # Components - 'LanguageSelectContainer', - 'ArticleButtonContainer', - 'RandomArticleButton', - 'ArticleInfoButton', - 'RefreshArticleButton', - - # Autocomplete - 'enhanced_wiki_autocomplete', -] \ No newline at end of file diff --git a/src/cogs/fun/wikipedia/autocomplete.py b/src/cogs/fun/wikipedia/autocomplete.py deleted file mode 100644 index 9c25512..0000000 --- a/src/cogs/fun/wikipedia/autocomplete.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────────── -# >> Autocomplete Functions -# ─────────────────────────────────────────────────── -import discord -import wikipedia -from .cache import wiki_cache - - -async def enhanced_wiki_autocomplete(ctx: discord.AutocompleteContext): - """ - Erweiterte Autocomplete mit Caching - - Args: - ctx: Autocomplete Context - - Returns: - Liste von Vorschlägen - """ - suchwert = ctx.value or "" - - # Standard-Vorschläge für kurze Eingaben - if len(suchwert) < 2: - return [ - "Künstliche Intelligenz", "Python (Programmiersprache)", "Discord", - "Deutschland", "Wikipedia", "Klimawandel", "Quantenphysik", "Internet" - ] - - try: - cache_key = f"autocomplete_{suchwert}_de" - cached_results = wiki_cache.get(cache_key) - - if cached_results: - return cached_results.get('suggestions', []) - - # Wikipedia-Suche - vorschlaege = wikipedia.search(suchwert, results=15) - - def relevance_score(suggestion): - """Berechnet die Relevanz eines Vorschlags""" - suggestion_lower = suggestion.lower() - suchwert_lower = suchwert.lower() - - if suchwert_lower == suggestion_lower: - return 0 - elif suggestion_lower.startswith(suchwert_lower): - return 1 - elif suchwert_lower in suggestion_lower: - return 2 - else: - return 3 + len(suggestion) - - # Nach Relevanz sortieren - vorschlaege.sort(key=relevance_score) - final_suggestions = vorschlaege[:25] - - # Im Cache speichern - wiki_cache.set(cache_key, {'suggestions': final_suggestions}) - - return final_suggestions - - except Exception: - return ["Fehler bei der Suche - bitte erneut versuchen"] \ No newline at end of file diff --git a/src/cogs/fun/wikipedia/cache.py b/src/cogs/fun/wikipedia/cache.py deleted file mode 100644 index e9a311f..0000000 --- a/src/cogs/fun/wikipedia/cache.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────────── -# >> Cache System -# ─────────────────────────────────────────────────── -from datetime import datetime, timedelta -from typing import Optional, Dict, Any - - -class WikiCache: - """Cache-System für Wikipedia-Anfragen""" - - def __init__(self): - self.cache: Dict[str, Dict[str, Any]] = {} - self.timestamps: Dict[str, datetime] = {} - - def get(self, key: str) -> Optional[Dict[str, Any]]: - """Ruft einen Wert aus dem Cache ab""" - if key in self.cache: - cache_duration = 300 # 5 Minuten - if datetime.now() - self.timestamps[key] < timedelta(seconds=cache_duration): - return self.cache[key] - else: - del self.cache[key] - del self.timestamps[key] - return None - - def set(self, key: str, value: Dict[str, Any]): - """Speichert einen Wert im Cache""" - self.cache[key] = value - self.timestamps[key] = datetime.now() - - def clear_expired(self): - """Entfernt abgelaufene Cache-Einträge""" - now = datetime.now() - cache_duration = 300 # 5 Minuten - expired_keys = [ - key for key, timestamp in self.timestamps.items() - if now - timestamp >= timedelta(seconds=cache_duration) - ] - for key in expired_keys: - self.cache.pop(key, None) - self.timestamps.pop(key, None) - - def clear(self): - """Leert den gesamten Cache""" - self.cache.clear() - self.timestamps.clear() - - @property - def size(self) -> int: - """Gibt die Anzahl der Cache-Einträge zurück""" - return len(self.cache) - - def get_expired_count(self) -> int: - """Zählt die abgelaufenen Einträge""" - now = datetime.now() - cache_duration = 300 # 5 Minuten - return sum( - 1 for timestamp in self.timestamps.values() - if now - timestamp >= timedelta(seconds=cache_duration) - ) - - -# Globale Cache-Instanz -wiki_cache = WikiCache() \ No newline at end of file diff --git a/src/cogs/fun/wikipedia/cog.py b/src/cogs/fun/wikipedia/cog.py deleted file mode 100644 index 97f428f..0000000 --- a/src/cogs/fun/wikipedia/cog.py +++ /dev/null @@ -1,461 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────────── -# >> Main Cog Class -# ─────────────────────────────────────────────────── -import discord -import ezcord -import wikipedia -import asyncio -from discord import slash_command -from discord.ui import Container -from datetime import datetime -from .config import WIKI_CONFIG, LANGUAGE_CHOICES -from .cache import wiki_cache -from .utils import format_page_info, clean_text -from .containers import ( - create_article_container, create_error_container, - create_disambiguation_container, create_random_article_container -) -from .components import ArticleButtonContainer -from .autocomplete import enhanced_wiki_autocomplete - - -class WikipediaCog(ezcord.Cog): - """Hauptklasse für Wikipedia-Bot Funktionen""" - - def __init__(self, bot): - self.bot = bot - self.current_language = 'de' - self.cleanup_task = None - self.stats = { - 'searches': 0, - 'articles_viewed': 0, - 'languages_used': set(), - 'start_time': datetime.now() - } - wikipedia.set_lang("de") - wikipedia.set_rate_limiting(True) - - @ezcord.Cog.listener() - async def on_ready(self): - """Startet den Cache-Cleanup Task""" - if self.cleanup_task is None: - self.cleanup_task = self.bot.loop.create_task(self._cleanup_cache()) - - async def _cleanup_cache(self): - """Regelmäßige Cache-Bereinigung""" - while True: - try: - await asyncio.sleep(300) - wiki_cache.clear_expired() - except: - pass - - def cog_unload(self): - """Cleanup beim Entladen des Cogs""" - if hasattr(self, 'cleanup_task') and self.cleanup_task: - self.cleanup_task.cancel() - - @slash_command(name="wiki_search", description="🔍 Durchsuche Wikipedia nach Artikeln und Informationen") - async def wikipedia_search( - self, - ctx: discord.ApplicationContext, - suchbegriff: discord.Option( - str, - "Was möchtest du auf Wikipedia nachschlagen?", - autocomplete=enhanced_wiki_autocomplete, - max_length=100 - ), - sprache: discord.Option( - str, - "Sprache für die Suche", - choices=LANGUAGE_CHOICES, - default="de", - required=False - ) - ): - await ctx.defer() - - self.stats['searches'] += 1 - self.stats['languages_used'].add(sprache) - - original_lang = self.current_language - if sprache != original_lang: - wikipedia.set_lang(sprache) - self.current_language = sprache - - try: - cache_key = f"{suchbegriff}_{sprache}" - cached_info = wiki_cache.get(cache_key) - - if cached_info: - info = cached_info - else: - page = wikipedia.page(suchbegriff) - info = format_page_info(page, sprache) - wiki_cache.set(cache_key, info) - - self.stats['articles_viewed'] += 1 - - similar_articles = wikipedia.search(suchbegriff, results=8) - similar_articles = [a for a in similar_articles if a.lower() != info['title'].lower()] - - container = create_article_container(info, ctx.author, similar_articles[:6], - suchbegriff, sprache, cog_instance=self) - view = discord.ui.DesignerView(container, timeout=WIKI_CONFIG['timeout']) - - await ctx.respond(view=view) - - except wikipedia.DisambiguationError as e: - container = create_disambiguation_container(suchbegriff, e.options[:12], sprache) - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view) - - except wikipedia.PageError: - error_text = f"Kein Wikipedia-Artikel für **'{suchbegriff}'** in {WIKI_CONFIG['languages'][sprache]['name']} gefunden." - - try: - suggestions = wikipedia.search(suchbegriff, results=5) - if suggestions: - error_text += "\n\n💡 **Meintest du vielleicht:**\n" - error_text += "\n".join([f"• {s}" for s in suggestions]) - except: - pass - - container = create_error_container("Artikel nicht gefunden", error_text) - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view) - - except Exception as e: - container = create_error_container("Unerwarteter Fehler", f"```py\n{str(e)[:800]}\n```") - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view) - - finally: - if sprache != original_lang: - wikipedia.set_lang(original_lang) - self.current_language = original_lang - - @slash_command(name="wiki_random", description="🎲 Zeige einen zufälligen Wikipedia-Artikel") - async def wiki_random( - self, - ctx: discord.ApplicationContext, - sprache: discord.Option( - str, - "Sprache für den zufälligen Artikel", - choices=LANGUAGE_CHOICES, - default="de", - required=False - ), - anzahl: discord.Option(int, "Anzahl zufälliger Artikel (1-5)", min_value=1, max_value=5, default=1) - ): - await ctx.defer() - - original_lang = self.current_language - if sprache != original_lang: - wikipedia.set_lang(sprache) - self.current_language = sprache - - try: - if anzahl == 1: - random_title = wikipedia.random() - page = wikipedia.page(random_title) - info = format_page_info(page, sprache) - - similar_articles = wikipedia.search(random_title, results=6) - similar_articles = [a for a in similar_articles if a.lower() != info['title'].lower()] - - container = create_random_article_container( - info, ctx.author, similar_articles[:4], - random_title, sprache, cog_instance=self - ) - - view = discord.ui.DesignerView(container, timeout=WIKI_CONFIG['timeout']) - await ctx.respond(view=view) - - else: - lang_info = WIKI_CONFIG['languages'][sprache] - container = Container() - container.add_text(f"🎲 **{anzahl} Zufällige Artikel**") - container.add_text(f"Entdecke neue Themen in {lang_info['flag']} {lang_info['name']}:") - container.add_separator() - - random_articles = [] - for i in range(anzahl): - try: - random_title = wikipedia.random() - summary = clean_text(wikipedia.summary(random_title, sentences=1), 200) - random_articles.append(random_title) - - container.add_text(f"**{i + 1}. {random_title}**") - container.add_text(summary) - container.add_separator() - except: - container.add_text(f"**{i + 1}. Artikel nicht verfügbar**") - container.add_text("Dieser Artikel konnte nicht geladen werden.") - container.add_separator() - - if random_articles: - container.add_text("📚 **Artikel öffnen:**") - for article in random_articles[:4]: - article_btn = ArticleButtonContainer(article, "similar", self) - container.add_item(article_btn) - - container.add_separator() - container.add_text(f"Wikipedia • {anzahl} zufällige Artikel") - - view = discord.ui.DesignerView(container, timeout=WIKI_CONFIG['timeout']) - await ctx.respond(view=view) - - except Exception as e: - container = create_error_container("Fehler beim Laden", - f"Zufällige Artikel konnten nicht geladen werden: {str(e)[:500]}") - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view) - finally: - if sprache != original_lang: - wikipedia.set_lang(original_lang) - self.current_language = original_lang - - @slash_command(name="wiki_multisearch", description="🔎 Erweiterte Wikipedia-Suche mit mehreren Ergebnissen") - async def wiki_multi_search( - self, - ctx: discord.ApplicationContext, - suchbegriff: discord.Option(str, "Suchbegriff für erweiterte Suche", max_length=100), - anzahl: discord.Option(int, "Anzahl der Ergebnisse (1-15)", min_value=1, max_value=15, default=8), - sprache: discord.Option(str, "Sprache für die Suche", choices=LANGUAGE_CHOICES, default="de", required=False) - ): - await ctx.defer() - - original_lang = self.current_language - if sprache != original_lang: - wikipedia.set_lang(sprache) - self.current_language = sprache - - try: - results = wikipedia.search(suchbegriff, results=anzahl) - - if not results: - container = create_error_container( - "Keine Ergebnisse", - f"Keine Artikel für **'{suchbegriff}'** in {WIKI_CONFIG['languages'][sprache]['name']} gefunden." - ) - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view) - return - - lang_info = WIKI_CONFIG['languages'][sprache] - container = Container() - container.add_text(f"🔍 **Suchergebnisse für '{suchbegriff}'**") - container.add_text(f"**{len(results)} Ergebnisse** in {lang_info['flag']} {lang_info['name']}:") - container.add_separator() - - for i, result in enumerate(results, 1): - try: - summary = wikipedia.summary(result, sentences=1) - summary = clean_text(summary, 150) - except: - summary = "Keine Vorschau verfügbar." - - container.add_text(f"**{i}. {result}**") - container.add_text(summary) - container.add_separator() - - container.add_text("📚 **Artikel öffnen:**") - for result in results[:4]: - article_btn = ArticleButtonContainer(result, "similar", self) - container.add_item(article_btn) - - container.add_separator() - container.add_text(f"Wikipedia • {len(results)} Ergebnisse • Sprache: {lang_info['name']}") - - view = discord.ui.DesignerView(container, timeout=WIKI_CONFIG['timeout']) - await ctx.respond(view=view) - - except Exception as e: - container = create_error_container("Suchfehler", f"Fehler bei der Suche: {str(e)[:500]}") - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view) - finally: - if sprache != original_lang: - wikipedia.set_lang(original_lang) - self.current_language = original_lang - - @slash_command(name="wiki_stats", description="📊 Zeige Bot-Statistiken und Wikipedia-Informationen") - async def wiki_statistics(self, ctx: discord.ApplicationContext): - uptime = datetime.now() - self.stats['start_time'] - uptime_str = f"{uptime.days}d {uptime.seconds // 3600}h {(uptime.seconds // 60) % 60}m" - - container = Container() - container.add_text("📊 **Wikipedia Bot Statistiken**") - container.add_separator() - - stats_text = f"🔍 **Suchanfragen:** {self.stats['searches']:,}\n" - stats_text += f"📖 **Artikel angezeigt:** {self.stats['articles_viewed']:,}\n" - stats_text += f"⏱️ **Laufzeit:** {uptime_str}" - container.add_text(stats_text) - - container.add_separator() - - lang_names = [WIKI_CONFIG['languages'][lang]['name'] for lang in self.stats['languages_used']] - if lang_names: - container.add_text(f"🌐 **Verwendete Sprachen:** {', '.join(lang_names)}") - else: - container.add_text("🌐 **Verwendete Sprachen:** Keine") - - container.add_separator() - - all_langs = [f"{info['flag']} {info['name']}" for info in WIKI_CONFIG['languages'].values()] - container.add_text("📚 **Verfügbare Sprachen:**") - container.add_text(", ".join(all_langs)) - - container.add_separator() - - tech_text = f"💾 **Cache-Einträge:** {len(wiki_cache.cache)}\n" - tech_text += f"⚡ **Rate Limiting:** Aktiviert\n" - tech_text += f"🔧 **Features:** Suche, Zufällig, Multi-Sprache, Cache" - container.add_text(tech_text) - - container.add_separator() - container.add_text("Wikipedia Bot • Erweiterte Funktionen verfügbar") - - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view) - - @slash_command(name="wiki_category", description="📂 Durchsuche Wikipedia-Kategorien") - async def wiki_category( - self, - ctx: discord.ApplicationContext, - kategorie: discord.Option(str, "Name der Kategorie", max_length=100), - sprache: discord.Option(str, "Sprache für die Kategorie-Suche", choices=LANGUAGE_CHOICES, default="de", required=False) - ): - await ctx.defer() - - original_lang = self.current_language - if sprache != original_lang: - wikipedia.set_lang(sprache) - self.current_language = sprache - - try: - search_results = wikipedia.search(f"Kategorie:{kategorie}", results=10) - if not search_results: - search_results = wikipedia.search(kategorie, results=10) - - if not search_results: - container = create_error_container( - "Kategorie nicht gefunden", - f"Keine Artikel in der Kategorie **'{kategorie}'** gefunden." - ) - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view) - return - - lang_info = WIKI_CONFIG['languages'][sprache] - container = Container() - container.add_text(f"📂 **Kategorie: {kategorie}**") - container.add_text(f"Artikel in dieser Kategorie ({lang_info['flag']} {lang_info['name']}):") - container.add_separator() - - for i, result in enumerate(search_results[:8], 1): - try: - summary = wikipedia.summary(result, sentences=1) - summary = clean_text(summary, 150) - except: - summary = "Keine Beschreibung verfügbar." - - container.add_text(f"**{i}. {result}**") - container.add_text(summary) - container.add_separator() - - container.add_text("📚 **Artikel öffnen:**") - for result in search_results[:4]: - article_btn = ArticleButtonContainer(result, "category", self) - container.add_item(article_btn) - - container.add_separator() - container.add_text(f"Wikipedia • Kategorie-Suche • {len(search_results)} Ergebnisse") - - view = discord.ui.DesignerView(container, timeout=WIKI_CONFIG['timeout']) - await ctx.respond(view=view) - - except Exception as e: - container = create_error_container("Kategorie-Fehler", - f"Fehler beim Laden der Kategorie: {str(e)[:500]}") - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view) - finally: - if sprache != original_lang: - wikipedia.set_lang(original_lang) - self.current_language = original_lang - - @slash_command(name="wiki_cache", description="🗑️ Cache-Management (nur für Administratoren)") - @discord.default_permissions(administrator=True) - async def wiki_cache_management( - self, - ctx: discord.ApplicationContext, - aktion: discord.Option( - str, - "Cache-Aktion", - choices=[ - discord.OptionChoice(name="📊 Status anzeigen", value="status"), - discord.OptionChoice(name="🗑️ Cache leeren", value="clear"), - discord.OptionChoice(name="⏰ Abgelaufene entfernen", value="cleanup") - ] - ) - ): - if not ctx.author.guild_permissions.administrator: - container = create_error_container("Berechtigung verweigert", - "Nur Administratoren können Cache-Befehle verwenden.") - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - return - - await ctx.defer(ephemeral=True) - - if aktion == "status": - total_entries = wiki_cache.size - expired_count = wiki_cache.get_expired_count() - - container = Container() - container.add_text("💾 **Cache-Status**") - container.add_separator() - - status_text = f"📊 **Gesamt-Einträge:** {total_entries}\n" - status_text += f"⏰ **Abgelaufene Einträge:** {expired_count}\n" - status_text += f"✅ **Aktive Einträge:** {total_entries - expired_count}\n" - status_text += f"⚙️ **Cache-Dauer:** {WIKI_CONFIG['cache_duration']} Sekunden" - container.add_text(status_text) - - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - - elif aktion == "clear": - old_count = wiki_cache.size - wiki_cache.clear() - - container = Container() - container.add_text("🗑️ **Cache geleert**") - container.add_separator() - container.add_text(f"**{old_count}** Einträge wurden entfernt.") - - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - - elif aktion == "cleanup": - old_count = wiki_cache.size - wiki_cache.clear_expired() - new_count = wiki_cache.size - removed = old_count - new_count - - container = Container() - container.add_text("⏰ **Cache bereinigt**") - container.add_separator() - container.add_text(f"**{removed}** abgelaufene Einträge entfernt.\n**{new_count}** Einträge verbleiben.") - - view = discord.ui.DesignerView(container, timeout=None) - await ctx.respond(view=view, ephemeral=True) - - -def setup(bot): - """Setup-Funktion für den Cog""" - bot.add_cog(WikipediaCog(bot)) \ No newline at end of file diff --git a/src/cogs/fun/wikipedia/components.py b/src/cogs/fun/wikipedia/components.py deleted file mode 100644 index 58cad61..0000000 --- a/src/cogs/fun/wikipedia/components.py +++ /dev/null @@ -1,306 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────────── -# >> UI Button Components -# ─────────────────────────────────────────────────── -import discord -import wikipedia -from discord import SelectOption -from discord.ui import Button, Select, Container -from typing import Dict, Any -from .config import WIKI_CONFIG -from .cache import wiki_cache -from .utils import format_page_info - - -class LanguageSelectContainer(Select): - """Dropdown für Sprachauswahl""" - - def __init__(self, current_term: str, current_lang: str = 'de', cog_instance=None): - self.current_term = current_term - self.current_lang = current_lang - self.cog = cog_instance - - options = [] - for code, info in WIKI_CONFIG['languages'].items(): - options.append(SelectOption( - label=info['name'], - value=code, - emoji=info['flag'], - default=(code == current_lang), - description=f"Suche auf {info['domain']}" - )) - - super().__init__( - placeholder="🌐 Sprache wählen...", - options=options, - min_values=1, - max_values=1 - ) - - async def callback(self, interaction: discord.Interaction): - from .containers import ( - create_article_container, - create_disambiguation_container, - create_error_container, - create_loading_container - ) - - await interaction.response.defer() - - selected_lang = self.values[0] - if selected_lang == self.current_lang: - error_container = Container() - error_container.add_text("Diese Sprache ist bereits ausgewählt.") - view = discord.ui.DesignerView(error_container, timeout=60) - await interaction.followup.send(view=view, ephemeral=True) - return - - original_lang = self.cog.current_language if self.cog else 'de' - if selected_lang != original_lang: - wikipedia.set_lang(selected_lang) - if self.cog: - self.cog.current_language = selected_lang - - try: - loading_container = create_loading_container( - f"Lade Artikel in {WIKI_CONFIG['languages'][selected_lang]['name']}...") - view = discord.ui.DesignerView(loading_container, timeout=None) - await interaction.edit_original_response(view=view) - - page = wikipedia.page(self.current_term) - info = format_page_info(page, selected_lang) - - similar_articles = wikipedia.search(self.current_term, results=6) - similar_articles = [a for a in similar_articles if a.lower() != info['title'].lower()] - - container = create_article_container(info, interaction.user, similar_articles[:4], - self.current_term, selected_lang, cog_instance=self.cog) - view = discord.ui.DesignerView(container, timeout=WIKI_CONFIG['timeout']) - await interaction.edit_original_response(view=view) - - except wikipedia.DisambiguationError as e: - container = create_disambiguation_container(self.current_term, e.options[:10], selected_lang) - view = discord.ui.DesignerView(container, timeout=None) - await interaction.edit_original_response(view=view) - except wikipedia.PageError: - container = create_error_container( - "Artikel nicht gefunden", - f"'{self.current_term}' existiert nicht in {WIKI_CONFIG['languages'][selected_lang]['name']}." - ) - view = discord.ui.DesignerView(container, timeout=None) - await interaction.edit_original_response(view=view) - except Exception as e: - container = create_error_container("Unerwarteter Fehler", str(e)[:500]) - view = discord.ui.DesignerView(container, timeout=None) - await interaction.edit_original_response(view=view) - finally: - if selected_lang != original_lang: - wikipedia.set_lang(original_lang) - if self.cog: - self.cog.current_language = original_lang - - -class ArticleButtonContainer(Button): - """Button zum Öffnen eines Artikels""" - - def __init__(self, article_title: str, button_type: str = "similar", cog_instance=None): - self.article_title = article_title - self.button_type = button_type - self.cog = cog_instance - - if button_type == "similar": - emoji = "📖" - style = discord.ButtonStyle.secondary - elif button_type == "category": - emoji = "📂" - style = discord.ButtonStyle.primary - else: - emoji = "📄" - style = discord.ButtonStyle.secondary - - super().__init__( - label=article_title[:80], - style=style, - emoji=emoji - ) - - async def callback(self, interaction: discord.Interaction): - from .containers import ( - create_article_container, - create_disambiguation_container, - create_error_container - ) - - await interaction.response.defer(ephemeral=True) - - try: - current_lang = self.cog.current_language if self.cog else 'de' - cache_key = f"{self.article_title}_{current_lang}" - cached_info = wiki_cache.get(cache_key) - - if cached_info: - info = cached_info - else: - page = wikipedia.page(self.article_title) - info = format_page_info(page, current_lang) - wiki_cache.set(cache_key, info) - - similar_articles = wikipedia.search(self.article_title, results=6) - similar_articles = [a for a in similar_articles if a.lower() != info['title'].lower()] - - container = create_article_container(info, interaction.user, similar_articles[:4], - self.article_title, current_lang, cog_instance=self.cog) - view = discord.ui.DesignerView(container, timeout=WIKI_CONFIG['timeout']) - await interaction.followup.send(view=view, ephemeral=True) - - except wikipedia.DisambiguationError as e: - container = create_disambiguation_container(self.article_title, e.options[:8]) - view = discord.ui.DesignerView(container, timeout=None) - await interaction.followup.send(view=view, ephemeral=True) - except wikipedia.PageError: - container = create_error_container("Artikel nicht gefunden", - f"'{self.article_title}' existiert nicht.") - view = discord.ui.DesignerView(container, timeout=None) - await interaction.followup.send(view=view, ephemeral=True) - except Exception as e: - container = create_error_container("Fehler beim Laden", str(e)[:500]) - view = discord.ui.DesignerView(container, timeout=None) - await interaction.followup.send(view=view, ephemeral=True) - - -class RandomArticleButton(Button): - """Button für zufällige Artikel""" - - def __init__(self, language: str, cog_instance=None): - self.language = language - self.cog = cog_instance - super().__init__( - label="🎲 Zufälliger Artikel", - style=discord.ButtonStyle.success - ) - - async def callback(self, interaction: discord.Interaction): - from .containers import ( - create_random_article_container, - create_loading_container, - create_error_container - ) - - await interaction.response.defer() - - try: - loading_container = create_loading_container("Lade zufälligen Artikel...") - view = discord.ui.DesignerView(loading_container, timeout=None) - await interaction.edit_original_response(view=view) - - random_title = wikipedia.random() - page = wikipedia.page(random_title) - info = format_page_info(page, self.language) - - similar_articles = wikipedia.search(random_title, results=6) - similar_articles = [a for a in similar_articles if a.lower() != info['title'].lower()] - - container = create_random_article_container( - info, interaction.user, similar_articles[:4], - random_title, self.language, cog_instance=self.cog - ) - - view = discord.ui.DesignerView(container, timeout=WIKI_CONFIG['timeout']) - await interaction.edit_original_response(view=view) - - except Exception as e: - container = create_error_container("Fehler beim Laden", - f"Zufälliger Artikel konnte nicht geladen werden: {str(e)[:300]}") - view = discord.ui.DesignerView(container, timeout=None) - await interaction.edit_original_response(view=view) - - -class ArticleInfoButton(Button): - """Button für Artikel-Informationen""" - - def __init__(self, info: Dict[str, Any], language: str): - self.info = info - self.language = language - super().__init__( - label="📊 Artikel-Info", - style=discord.ButtonStyle.primary - ) - - async def callback(self, interaction: discord.Interaction): - await interaction.response.defer(ephemeral=True) - - container = Container() - container.add_text(f"📊 **Informationen zu '{self.info['title']}'**") - container.add_separator() - - stats_text = f"🌐 **Sprache:** {WIKI_CONFIG['languages'][self.language]['name']}\n" - stats_text += f"📂 **Kategorien:** {len(self.info.get('categories', []))}\n" - stats_text += f"🔗 **Verweise:** {len(self.info.get('links', []))}" - container.add_text(stats_text) - - if self.info.get('coordinates'): - lat, lon = self.info['coordinates'] - container.add_text(f"🗺️ **Koordinaten:** {lat:.2f}°N, {lon:.2f}°E") - - if self.info.get('images'): - container.add_text(f"🖼️ **Bilder:** {len(self.info['images'])}") - - if self.info.get('categories'): - container.add_separator() - container.add_text("📚 **Hauptkategorien:**") - categories_text = "\n".join([f"• {cat}" for cat in self.info['categories'][:5]]) - container.add_text(categories_text) - - container.add_separator() - container.add_text("Wikipedia • Artikel-Statistiken") - - view = discord.ui.DesignerView(container, timeout=300) - await interaction.followup.send(view=view, ephemeral=True) - - -class RefreshArticleButton(Button): - """Button zum Aktualisieren eines Artikels""" - - def __init__(self, search_term: str, language: str, cog_instance=None): - self.search_term = search_term - self.language = language - self.cog = cog_instance - super().__init__( - label="🔄 Aktualisieren", - style=discord.ButtonStyle.secondary - ) - - async def callback(self, interaction: discord.Interaction): - from .containers import ( - create_article_container, - create_loading_container, - create_error_container - ) - - await interaction.response.defer() - - try: - cache_key = f"{self.search_term}_{self.language}" - if cache_key in wiki_cache.cache: - del wiki_cache.cache[cache_key] - del wiki_cache.timestamps[cache_key] - - loading_container = create_loading_container("Aktualisiere Artikel...") - view = discord.ui.DesignerView(loading_container, timeout=None) - await interaction.edit_original_response(view=view) - - page = wikipedia.page(self.search_term) - info = format_page_info(page, self.language) - - similar_articles = wikipedia.search(self.search_term, results=6) - similar_articles = [a for a in similar_articles if a.lower() != info['title'].lower()] - - container = create_article_container(info, interaction.user, similar_articles[:4], - self.search_term, self.language, cog_instance=self.cog) - view = discord.ui.DesignerView(container, timeout=WIKI_CONFIG['timeout']) - await interaction.edit_original_response(view=view) - - except Exception as e: - container = create_error_container("Aktualisierung fehlgeschlagen", str(e)[:500]) - view = discord.ui.DesignerView(container, timeout=None) - await interaction.edit_original_response(view=view) \ No newline at end of file diff --git a/src/cogs/fun/wikipedia/config.py b/src/cogs/fun/wikipedia/config.py deleted file mode 100644 index 1002102..0000000 --- a/src/cogs/fun/wikipedia/config.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────────── -# >> Wikipedia Bot Configuration -# ─────────────────────────────────────────────────── -import discord - -# Fallback für Farben -try: - from DevTools import INFO_COLOR, ERROR_COLOR, SUCCESS_COLOR, WARNING_COLOR -except ImportError: - INFO_COLOR = discord.Color.blue() - ERROR_COLOR = discord.Color.red() - SUCCESS_COLOR = discord.Color.green() - WARNING_COLOR = discord.Color.orange() - -# Wikipedia Konfiguration -WIKI_CONFIG = { - 'languages': { - 'de': {'name': 'Deutsch', 'flag': '\U0001F1E9\U0001F1EA', 'domain': 'de.wikipedia.org'}, - 'en': {'name': 'English', 'flag': '\U0001F1FA\U0001F1F8', 'domain': 'en.wikipedia.org'}, - 'fr': {'name': 'Français', 'flag': '\U0001F1EB\U0001F1F7', 'domain': 'fr.wikipedia.org'}, - 'es': {'name': 'Español', 'flag': '\U0001F1EA\U0001F1F8', 'domain': 'es.wikipedia.org'}, - 'it': {'name': 'Italiano', 'flag': '\U0001F1EE\U0001F1F9', 'domain': 'it.wikipedia.org'}, - 'ja': {'name': '日本語', 'flag': '\U0001F1EF\U0001F1F5', 'domain': 'ja.wikipedia.org'}, - 'ru': {'name': 'Русский', 'flag': '\U0001F1F7\U0001F1FA', 'domain': 'ru.wikipedia.org'}, - }, - 'max_summary_length': 1500, - 'max_categories': 3, - 'max_similar_articles': 6, - 'timeout': 600, - 'cache_duration': 300 -} - -# Discord Option Choices für Sprachauswahl -LANGUAGE_CHOICES = [ - discord.OptionChoice(name="DE Deutsch", value="de"), - discord.OptionChoice(name="US English", value="en"), - discord.OptionChoice(name="FR Français", value="fr"), - discord.OptionChoice(name="ES Español", value="es"), - discord.OptionChoice(name="IT Italiano", value="it"), - discord.OptionChoice(name="JP 日本語", value="ja"), - discord.OptionChoice(name="RU Русский", value="ru"), -] \ No newline at end of file diff --git a/src/cogs/fun/wikipedia/containers.py b/src/cogs/fun/wikipedia/containers.py deleted file mode 100644 index 870a87d..0000000 --- a/src/cogs/fun/wikipedia/containers.py +++ /dev/null @@ -1,228 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────────── -# >> Container Creation Functions (py-cord Designer) -# ─────────────────────────────────────────────────── -import discord -from discord.ui import Container -from typing import Dict, Any, List - - -def create_article_container( - info: Dict[str, Any], - user: discord.User, - similar_articles: List[str] = None, - search_term: str = "", - language: str = 'de', - cog_instance=None -) -> Container: - """Erstellt einen Container für einen Wikipedia-Artikel""" - from .components import ( - LanguageSelectContainer, ArticleButtonContainer, - RandomArticleButton, ArticleInfoButton, RefreshArticleButton - ) - - container = Container() - - # Header mit Titel - lang_info = { - 'de': {'name': 'Deutsch', 'flag': 'DE'}, - 'en': {'name': 'English', 'flag': 'EN'}, - 'fr': {'name': 'Français', 'flag': 'FR'}, - 'es': {'name': 'Español', 'flag': 'ES'}, - 'it': {'name': 'Italiano', 'flag': 'IT'}, - 'ja': {'name': '日本語', 'flag': 'JP'}, - 'ru': {'name': 'Русский', 'flag': 'RU'}, - } - lang_data = lang_info.get(language, {'name': 'Deutsch', 'flag': 'DE'}) - header_text = f"📖 **{info['title']}**\n[{lang_data['flag']}] {lang_data['name']} • Wikipedia" - container.add_text(header_text) - - container.add_separator() - - # Zusammenfassung - summary_text = info['summary'][:800] + ("..." if len(info['summary']) > 800 else "") - container.add_text(summary_text) - - # Kategorien falls vorhanden - if info.get('categories'): - container.add_separator() - categories_text = "📂 **Kategorien:** " + ", ".join(info['categories'][:3]) - if len(info['categories']) > 3: - categories_text += f" (+{len(info['categories']) - 3} weitere)" - container.add_text(categories_text) - - # Koordinaten falls vorhanden - if info.get('coordinates'): - lat, lon = info['coordinates'] - container.add_text(f"🗺️ **Standort:** {lat:.2f}°N, {lon:.2f}°E") - - container.add_separator() - - # Link zum vollständigen Artikel - if info.get('url'): - container.add_text(f"🔗 [Vollständigen Artikel lesen]({info['url']})") - - # Sprachauswahl - if cog_instance and search_term: - lang_select = LanguageSelectContainer(search_term, language, cog_instance) - container.add_item(lang_select) - - # Ähnliche Artikel als Buttons - if similar_articles and cog_instance: - container.add_separator() - container.add_text("📚 **Ähnliche Artikel:**") - for article in similar_articles[:4]: - article_btn = ArticleButtonContainer(article, "similar", cog_instance) - container.add_item(article_btn) - - container.add_separator() - - # Action Buttons - if cog_instance: - random_btn = RandomArticleButton(language, cog_instance) - container.add_item(random_btn) - - info_btn = ArticleInfoButton(info, language) - container.add_item(info_btn) - - if search_term: - refresh_btn = RefreshArticleButton(search_term, language, cog_instance) - container.add_item(refresh_btn) - - # Footer - container.add_separator() - footer_text = f"👤 Angefragt von {user.display_name}" - container.add_text(footer_text) - - return container - - -def create_error_container(title: str, description: str) -> Container: - """Erstellt einen Fehler-Container""" - container = Container() - container.add_text(f"❌ **{title}**") - container.add_separator() - container.add_text(description) - container.add_separator() - container.add_text("Wikipedia Bot • Fehler aufgetreten") - return container - - -def create_disambiguation_container(term: str, options: List[str], language: str = 'de') -> Container: - """Erstellt einen Mehrdeutigkeits-Container""" - lang_info = { - 'de': 'Deutsch', - 'en': 'English', - 'fr': 'Français', - 'es': 'Español', - 'it': 'Italiano', - 'ja': '日本語', - 'ru': 'Русский' - } - - container = Container() - - lang_name = lang_info.get(language, 'Deutsch') - container.add_text(f"🔀 **Mehrdeutige Suche**") - container.add_separator() - container.add_text(f"**'{term}'** kann mehrere Bedeutungen haben in {lang_name}:") - - container.add_separator() - container.add_text("📋 **Mögliche Optionen:**") - - options_text = "\n".join([f"• {opt}" for opt in options[:10]]) - container.add_text(options_text) - - container.add_separator() - container.add_text("💡 Versuche eine spezifischere Suche oder wähle eine der Optionen.") - - return container - - -def create_loading_container(title: str = "Lade Wikipedia-Artikel...") -> Container: - """Erstellt einen Lade-Container""" - container = Container() - container.add_text(f"⏳ **{title}**") - container.add_separator() - container.add_text("Dies kann einen Moment dauern...") - return container - - -def create_random_article_container( - info: Dict[str, Any], - user: discord.User, - similar_articles: List[str], - random_title: str, - language: str, - cog_instance=None -) -> Container: - """Erstellt einen Container für zufällige Artikel""" - from .components import ( - LanguageSelectContainer, ArticleButtonContainer, - RandomArticleButton, ArticleInfoButton, RefreshArticleButton - ) - - container = Container() - - lang_info = { - 'de': {'name': 'Deutsch', 'flag': 'DE'}, - 'en': {'name': 'English', 'flag': 'EN'}, - 'fr': {'name': 'Français', 'flag': 'FR'}, - 'es': {'name': 'Español', 'flag': 'ES'}, - 'it': {'name': 'Italiano', 'flag': 'IT'}, - 'ja': {'name': '日本語', 'flag': 'JP'}, - 'ru': {'name': 'Русский', 'flag': 'RU'}, - } - lang_data = lang_info.get(language, {'name': 'Deutsch', 'flag': 'DE'}) - - container.add_text(f"🎲 **Zufälliger Artikel: {info['title']}**") - container.add_text(f"[{lang_data['flag']}] {lang_data['name']} • Wikipedia") - container.add_separator() - - summary_text = info['summary'][:800] + ("..." if len(info['summary']) > 800 else "") - container.add_text(summary_text) - - if info.get('categories'): - container.add_separator() - categories_text = "📂 **Kategorien:** " + ", ".join(info['categories'][:3]) - if len(info['categories']) > 3: - categories_text += f" (+{len(info['categories']) - 3} weitere)" - container.add_text(categories_text) - - if info.get('coordinates'): - lat, lon = info['coordinates'] - container.add_text(f"🗺️ **Standort:** {lat:.2f}°N, {lon:.2f}°E") - - container.add_separator() - - if info.get('url'): - container.add_text(f"🔗 [Vollständigen Artikel lesen]({info['url']})") - - if cog_instance: - lang_select = LanguageSelectContainer(random_title, language, cog_instance) - container.add_item(lang_select) - - if similar_articles and cog_instance: - container.add_separator() - container.add_text("📚 **Ähnliche Artikel:**") - for article in similar_articles[:4]: - article_btn = ArticleButtonContainer(article, "similar", cog_instance) - container.add_item(article_btn) - - container.add_separator() - - if cog_instance: - random_btn = RandomArticleButton(language, cog_instance) - container.add_item(random_btn) - - info_btn = ArticleInfoButton(info, language) - container.add_item(info_btn) - - refresh_btn = RefreshArticleButton(random_title, language, cog_instance) - container.add_item(refresh_btn) - - container.add_separator() - container.add_text(f"👤 Angefragt von {user.display_name}") - - return container \ No newline at end of file diff --git a/src/cogs/fun/wikipedia/utils.py b/src/cogs/fun/wikipedia/utils.py deleted file mode 100644 index aedf754..0000000 --- a/src/cogs/fun/wikipedia/utils.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────────── -# >> Utility Functions -# ─────────────────────────────────────────────────── -import re -import wikipedia -from typing import Dict, Any - - -def clean_text(text: str, max_length: int = None) -> str: - """ - Erweiterte Textbereinigung - - Args: - text: Der zu bereinigende Text - max_length: Maximale Länge des Textes - - Returns: - Bereinigter Text - """ - if not text: - return "Keine Beschreibung verfügbar." - - # HTML-Tags entfernen - text = re.sub(r'<[^>]+>', '', text) - # Referenzen in eckigen Klammern entfernen - text = re.sub(r'\[.*?\]', '', text) - # Mehrfache Leerzeichen normalisieren - text = re.sub(r'\s+', ' ', text).strip() - - max_length = max_length or 1500 - if len(text) > max_length: - truncated = text[:max_length - 3] - last_sentence = truncated.rfind('.') - if last_sentence > max_length // 2: - text = truncated[:last_sentence + 1] - else: - text = truncated + "..." - - return text - - -def format_page_info(page, language: str = 'de') -> Dict[str, Any]: - """ - Erweiterte Seiteninformationen mit Fehlerbehandlung - - Args: - page: Wikipedia-Seitenobjekt - language: Sprachcode - - Returns: - Dictionary mit formatierten Seiteninformationen - """ - try: - info = { - 'title': getattr(page, 'title', 'Unbekannt'), - 'url': getattr(page, 'url', ''), - 'summary': '', - 'categories': [], - 'links': [], - 'images': [], - 'language': language, - 'coordinates': None, - 'references': [] - } - - # Zusammenfassung laden - try: - info['summary'] = clean_text(wikipedia.summary(page.title, sentences=4)) - except: - info['summary'] = "Zusammenfassung nicht verfügbar." - - # Kategorien laden - try: - info['categories'] = getattr(page, 'categories', [])[:3] - except: - pass - - # Links laden - try: - info['links'] = getattr(page, 'links', [])[:15] - except: - pass - - # Bilder laden - try: - info['images'] = getattr(page, 'images', []) - except: - pass - - # Koordinaten extrahieren - try: - content = getattr(page, 'content', '') - coord_match = re.search(r'(\d+\.?\d*)°\s*N.*?(\d+\.?\d*)°\s*[EW]', content) - if coord_match: - info['coordinates'] = (float(coord_match.group(1)), float(coord_match.group(2))) - except: - pass - - return info - - except Exception as e: - return { - 'title': 'Fehler beim Laden', - 'url': '', - 'summary': f'Informationen konnten nicht geladen werden: {str(e)}', - 'categories': [], - 'links': [], - 'images': [], - 'language': language, - 'coordinates': None, - 'references': [] - } \ No newline at end of file diff --git a/src/cogs/informationen/botstatus.py b/src/cogs/informationen/botstatus.py deleted file mode 100644 index 24f00a6..0000000 --- a/src/cogs/informationen/botstatus.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────── -# >> Imports -# ─────────────────────────────────────────────── -import discord -from discord.ext import commands, tasks -import ezcord -import math -import yaml -# ─────────────────────────────────────────────── -# >> Cogs -# ─────────────────────────────────────────────── -class StatusCog(ezcord.Cog): - def __init__(self, bot): - self.bot = bot - self.update_status.start() # Starte den Loop direkt, er pausiert automatisch, falls der Bot nicht bereit ist - - @tasks.loop(seconds=30) - async def update_status(self): - if not self.bot.is_ready(): - return - - guild_count = len(self.bot.guilds) - member_count = sum(g.member_count for g in self.bot.guilds) - - latency = self.bot.latency * 1000 - latency = 0 if math.isnan(latency) else round(latency) - - statuses = [ - f"🌍 {guild_count} | 👥 {member_count} | 🏓 {latency}ms" - ] - status_text = statuses[self.update_status.current_loop % len(statuses)] - - await self.bot.change_presence(activity=discord.CustomActivity(name=status_text)) - - @commands.Cog.listener() - async def on_ready(self): - if not self.update_status.is_running(): # Falls er aus irgendeinem Grund gestoppt wurde - self.update_status.start() - -def setup(bot): - bot.add_cog(StatusCog(bot)) \ No newline at end of file diff --git a/src/cogs/informationen/serverinfo.py b/src/cogs/informationen/serverinfo.py deleted file mode 100644 index 00a1562..0000000 --- a/src/cogs/informationen/serverinfo.py +++ /dev/null @@ -1,535 +0,0 @@ -import discord -from discord.ext import commands -from discord import SlashCommandGroup, Option -import datetime -import asyncio -from typing import Optional -import logging - - -class ServerInfoCog(commands.Cog): - def __init__(self, bot): - self.bot = bot - - server = SlashCommandGroup("server", "Server-Informationen und -Statistiken") - - @server.command(description="Zeigt umfassende Discord-Server Informationen an") - async def info(self, ctx): - """Hauptbefehl für Server-Informationen mit detaillierter Übersicht""" - guild = ctx.guild - - try: - await ctx.defer() # Mehr Zeit für komplexe Berechnungen - - # Erweiterte Mitglieder-Statistiken - members = guild.members - total_members = len(members) - - # Status-Statistiken - status_counts = { - 'online': len([m for m in members if m.status == discord.Status.online]), - 'idle': len([m for m in members if m.status == discord.Status.idle]), - 'dnd': len([m for m in members if m.status == discord.Status.dnd]), - 'offline': len([m for m in members if m.status == discord.Status.offline]) - } - - bots = len([m for m in members if m.bot]) - humans = total_members - bots - - # Kanal-Statistiken - text_channels = len(guild.text_channels) - voice_channels = len(guild.voice_channels) - stage_channels = len(guild.stage_channels) - forum_channels = len([c for c in guild.channels if isinstance(c, discord.ForumChannel)]) - categories = len(guild.categories) - total_channels = len(guild.channels) - - # Rollen und Features - roles = len(guild.roles) - 1 # Exclude @everyone - emojis = len(guild.emojis) - stickers = len(guild.stickers) - - # Boost-Informationen - boost_count = guild.premium_subscription_count or 0 - boost_tier = guild.premium_tier - boosters = len(guild.premium_subscribers) if guild.premium_subscribers else 0 - - # Haupt-Embed erstellen - embed = discord.Embed( - title=f"📊 {guild.name}", - description=guild.description or "*Keine Beschreibung verfügbar*", - color=discord.Color.blue(), - timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - - # Server-Icon und Banner - if guild.icon: - embed.set_thumbnail(url=guild.icon.url) - - if guild.banner: - embed.set_image(url=guild.banner.url) - - # Grundlegende Informationen - created_timestamp = int(guild.created_at.timestamp()) - embed.add_field( - name="ℹ️ Allgemeine Informationen", - value=f"👑 **Besitzer:** {guild.owner.mention}\n" - f"🆔 **ID:** `{guild.id}`\n" - f"📅 **Erstellt:** ()\n" - f"🌍 **Region:** {self._get_region_flag()} Automatisch", - inline=False - ) - - # Mitglieder-Statistiken - online_total = status_counts['online'] + status_counts['idle'] + status_counts['dnd'] - embed.add_field( - name="👥 Mitglieder", - value=f"**Gesamt:** {total_members:,}\n" - f"👤 **Menschen:** {humans:,}\n" - f"🤖 **Bots:** {bots:,}\n" - f"🟢 **Online:** {online_total:,}\n" - f"├ 🟢 Aktiv: {status_counts['online']:,}\n" - f"├ 🟡 Abwesend: {status_counts['idle']:,}\n" - f"├ 🔴 Beschäftigt: {status_counts['dnd']:,}\n" - f"└ ⚫ Offline: {status_counts['offline']:,}", - inline=True - ) - - # Kanal-Informationen - embed.add_field( - name="📺 Kanäle", - value=f"**Gesamt:** {total_channels}\n" - f"💬 **Text:** {text_channels}\n" - f"🔊 **Voice:** {voice_channels}\n" - f"🎭 **Stage:** {stage_channels}\n" - f"📋 **Forum:** {forum_channels}\n" - f"📁 **Kategorien:** {categories}", - inline=True - ) - - # Server-Features und Anpassungen - embed.add_field( - name="🎨 Anpassungen", - value=f"🏷️ **Rollen:** {roles}\n" - f"😀 **Emojis:** {emojis}/{guild.emoji_limit}\n" - f"🎃 **Sticker:** {stickers}\n" - f"📁 **Dateigröße:** {guild.filesize_limit // 1024 // 1024} MB", - inline=True - ) - - # Boost-Informationen - boost_benefits = self._get_boost_benefits(boost_tier) - embed.add_field( - name="💎 Nitro Boosts", - value=f"🚀 **Level:** {boost_tier}/3\n" - f"⭐ **Boosts:** {boost_count}\n" - f"👑 **Booster:** {boosters}\n" - f"🎁 **Benefits:** {boost_benefits}", - inline=True - ) - - # Sicherheit und Moderation - verification_emoji = { - discord.VerificationLevel.none: "🟢", - discord.VerificationLevel.low: "🟡", - discord.VerificationLevel.medium: "🟠", - discord.VerificationLevel.high: "🔴", - discord.VerificationLevel.highest: "🔴" - } - - nsfw_level_names = { - discord.NSFWLevel.default: "Standard", - discord.NSFWLevel.explicit: "Explizit", - discord.NSFWLevel.safe: "Sicher", - discord.NSFWLevel.age_restricted: "Altersbeschränkt" - } - - embed.add_field( - name="🛡️ Sicherheit & Moderation", - value=f"{verification_emoji.get(guild.verification_level, '❓')} **Verifikation:** {guild.verification_level.name.title()}\n" - f"🔒 **2FA:** {'✅ Aktiviert' if guild.mfa_level else '❌ Deaktiviert'}\n" - f"🔞 **NSFW Level:** {nsfw_level_names.get(guild.nsfw_level, 'Unbekannt')}\n" - f"📢 **System Channel:** {guild.system_channel.mention if guild.system_channel else 'Nicht gesetzt'}", - inline=True - ) - - # Server-Features - features = self._format_guild_features(guild.features) - if features: - embed.add_field(name="✨ Premium Features", value=features, inline=False) - - # Zusätzliche Informationen falls vorhanden - if guild.vanity_url: - embed.add_field(name="🔗 Vanity URL", value=f"discord.gg/{guild.vanity_url}", inline=True) - - if guild.rules_channel: - embed.add_field(name="📜 Regeln", value=guild.rules_channel.mention, inline=True) - - if guild.public_updates_channel: - embed.add_field(name="📢 Updates", value=guild.public_updates_channel.mention, inline=True) - - embed.set_footer( - text=f"Angefragt von {ctx.author.display_name}", - icon_url=ctx.author.display_avatar.url - ) - - await ctx.followup.send(embed=embed) - - except Exception as e: - logging.error(f"Fehler in server info command: {e}") - await ctx.followup.send("❌ Ein Fehler ist aufgetreten beim Laden der Server-Informationen.", ephemeral=True) - - @server.command(description="Zeigt Top-Rollen des Servers an") - async def roles(self, ctx, limit: Option(int, "Anzahl der anzuzeigenden Rollen (max 25)", min_value=1, max_value=25, default=15)): - """Zeigt die höchsten Rollen des Servers mit Details""" - try: - guild = ctx.guild - roles = sorted([role for role in guild.roles if role.name != "@everyone"], - key=lambda x: x.position, reverse=True) - - if not roles: - await ctx.respond("❌ Keine besonderen Rollen auf diesem Server gefunden.", ephemeral=True) - return - - embed = discord.Embed( - title=f"🏷️ Top Rollen in {guild.name}", - color=discord.Color.gold(), - timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - - role_list = [] - for i, role in enumerate(roles[:limit], 1): - member_count = len(role.members) - permissions_count = sum(1 for perm, value in role.permissions if value) - - # Spezielle Rollen-Indikatoren - indicators = [] - if role.permissions.administrator: - indicators.append("👑") - if role.permissions.manage_guild: - indicators.append("⚙️") - if role.permissions.ban_members or role.permissions.kick_members: - indicators.append("🔨") - if role.managed: - indicators.append("🤖") - if role.hoist: - indicators.append("📌") - - indicator_str = "".join(indicators) - - role_list.append( - f"`#{i:2d}` {role.mention} {indicator_str}\n" - f" 👥 {member_count} | 🔐 {permissions_count} Perms | Pos: {role.position}" - ) - - # Aufteilen in mehrere Fields falls nötig - chunk_size = 8 - for i in range(0, len(role_list), chunk_size): - chunk = role_list[i:i+chunk_size] - field_name = f"Rollen {i+1}-{min(i+chunk_size, len(role_list))}" if len(role_list) > chunk_size else "Rollen" - embed.add_field(name=field_name, value="\n".join(chunk), inline=False) - - embed.add_field( - name="📊 Statistiken", - value=f"**Gesamt:** {len(guild.roles)-1} Rollen\n" - f"**Angezeigt:** {min(limit, len(roles))}\n" - f"**Legende:** 👑 Admin | ⚙️ Management | 🔨 Moderation | 🤖 Bot | 📌 Angeheftet", - inline=False - ) - - await ctx.respond(embed=embed) - - except Exception as e: - logging.error(f"Fehler in server roles command: {e}") - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Laden der Rollen.", ephemeral=True) - - @server.command(description="Zeigt Kanal-Übersicht des Servers") - async def channels(self, ctx): - """Zeigt eine strukturierte Übersicht aller Kanäle""" - try: - guild = ctx.guild - - embed = discord.Embed( - title=f"📺 Kanäle in {guild.name}", - color=discord.Color.blue(), - timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - - # Kategorien und ihre Kanäle - categories_processed = set() - - # Kanäle ohne Kategorie - no_category = [ch for ch in guild.channels if ch.category is None and not isinstance(ch, discord.CategoryChannel)] - if no_category: - channel_list = [] - for ch in no_category: - channel_list.append(f"{self._get_channel_emoji(ch)} {ch.name}") - - if len("\n".join(channel_list)) <= 1024: - embed.add_field( - name="📁 Ohne Kategorie", - value="\n".join(channel_list) or "Keine", - inline=False - ) - - # Kategorien mit ihren Kanälen - for category in guild.categories[:10]: # Limit für Embed-Größe - if category in categories_processed: - continue - - channel_list = [] - for ch in category.channels: - channel_list.append(f"{self._get_channel_emoji(ch)} {ch.name}") - - if channel_list and len("\n".join(channel_list)) <= 1024: - embed.add_field( - name=f"📁 {category.name} ({len(category.channels)})", - value="\n".join(channel_list), - inline=True - ) - categories_processed.add(category) - - # Statistiken - stats = ( - f"📊 **Gesamt:** {len(guild.channels)}\n" - f"💬 **Text:** {len(guild.text_channels)}\n" - f"🔊 **Voice:** {len(guild.voice_channels)}\n" - f"🎭 **Stage:** {len(guild.stage_channels)}\n" - f"📋 **Forum:** {len([c for c in guild.channels if isinstance(c, discord.ForumChannel)])}\n" - f"📁 **Kategorien:** {len(guild.categories)}" - ) - - embed.add_field(name="📈 Statistiken", value=stats, inline=True) - - if len(guild.categories) > 10: - embed.set_footer(text=f"... und {len(guild.categories) - 10} weitere Kategorien") - - await ctx.respond(embed=embed) - - except Exception as e: - logging.error(f"Fehler in server channels command: {e}") - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Laden der Kanal-Übersicht.", ephemeral=True) - - @server.command(description="Zeigt Emoji-Übersicht des Servers") - async def emojis(self, ctx): - """Zeigt alle Custom Emojis des Servers""" - try: - guild = ctx.guild - emojis = guild.emojis - - if not emojis: - embed = discord.Embed( - title="😔 Keine Custom Emojis", - description="Dieser Server hat keine benutzerdefinierten Emojis.", - color=discord.Color.orange() - ) - await ctx.respond(embed=embed) - return - - # Emojis nach Typ sortieren - static_emojis = [e for e in emojis if not e.animated] - animated_emojis = [e for e in emojis if e.animated] - - embed = discord.Embed( - title=f"😀 Emojis in {guild.name}", - description=f"**{len(static_emojis)}** statische • **{len(animated_emojis)}** animierte • **{len(emojis)}/{guild.emoji_limit}** gesamt", - color=discord.Color.yellow(), - timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - - # Statische Emojis (max 25 pro Field) - if static_emojis: - emoji_chunks = [static_emojis[i:i+25] for i in range(0, len(static_emojis), 25)] - for i, chunk in enumerate(emoji_chunks[:3]): # Max 3 Chunks - emoji_display = "".join([str(emoji) for emoji in chunk]) - field_name = f"📷 Statische Emojis" if i == 0 else f"📷 Statische Emojis (Teil {i+1})" - embed.add_field(name=field_name, value=emoji_display or "Keine", inline=False) - - # Animierte Emojis - if animated_emojis: - emoji_chunks = [animated_emojis[i:i+25] for i in range(0, len(animated_emojis), 25)] - for i, chunk in enumerate(emoji_chunks[:2]): # Max 2 Chunks für animierte - emoji_display = "".join([str(emoji) for emoji in chunk]) - field_name = f"🎬 Animierte Emojis" if i == 0 else f"🎬 Animierte Emojis (Teil {i+1})" - embed.add_field(name=field_name, value=emoji_display or "Keine", inline=False) - - await ctx.respond(embed=embed) - - except Exception as e: - logging.error(f"Fehler in server emojis command: {e}") - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Laden der Emojis.", ephemeral=True) - - @server.command(description="Zeigt Server-Boosts und Premium-Features") - async def boosts(self, ctx): - """Detaillierte Boost-Informationen""" - try: - guild = ctx.guild - - embed = discord.Embed( - title=f"💎 Server Boosts - {guild.name}", - color=discord.Color.purple(), - timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - - boost_count = guild.premium_subscription_count or 0 - boost_tier = guild.premium_tier - boosters = guild.premium_subscribers or [] - - # Aktuelle Boost-Situation - embed.add_field( - name="📊 Aktuelle Situation", - value=f"🚀 **Level:** {boost_tier}/3\n" - f"⭐ **Boosts:** {boost_count}\n" - f"👑 **Booster:** {len(boosters)}\n" - f"📈 **Progress:** {self._get_boost_progress(boost_count, boost_tier)}", - inline=False - ) - - # Nächstes Level - next_level_info = self._get_next_level_info(boost_count, boost_tier) - if next_level_info: - embed.add_field(name="🎯 Nächstes Level", value=next_level_info, inline=False) - - # Aktuelle Benefits - benefits = self._get_detailed_boost_benefits(guild) - embed.add_field(name="🎁 Aktuelle Benefits", value=benefits, inline=False) - - # Top Booster (falls vorhanden) - if boosters: - booster_list = [booster.mention for booster in boosters[:10]] - embed.add_field( - name=f"👑 Booster ({len(boosters)})", - value=", ".join(booster_list) + ("..." if len(boosters) > 10 else ""), - inline=False - ) - - await ctx.respond(embed=embed) - - except Exception as e: - logging.error(f"Fehler in server boosts command: {e}") - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Laden der Boost-Informationen.", ephemeral=True) - - def _get_channel_emoji(self, channel): - """Gibt das passende Emoji für einen Kanal-Typ zurück""" - if isinstance(channel, discord.TextChannel): - return "💬" - elif isinstance(channel, discord.VoiceChannel): - return "🔊" - elif isinstance(channel, discord.StageChannel): - return "🎭" - elif isinstance(channel, discord.ForumChannel): - return "📋" - elif isinstance(channel, discord.CategoryChannel): - return "📁" - else: - return "📺" - - def _get_region_flag(self): - """Gibt eine Flagge für die Region zurück (falls erwünscht)""" - return "🌍" # Globus für automatische Region - - def _get_boost_benefits(self, tier): - """Gibt die Benefits für ein Boost-Level zurück""" - benefits = { - 0: "Keine", - 1: "50 Emoji Slots, 128kb Audio", - 2: "150 Emoji Slots, 256kb Audio, Banner", - 3: "250 Emoji Slots, 384kb Audio, Vanity URL" - } - return benefits.get(tier, "Unbekannt") - - def _get_detailed_boost_benefits(self, guild): - """Detaillierte Boost-Benefits für den aktuellen Server""" - tier = guild.premium_tier - - benefits = [ - f"😀 **Emoji Slots:** {guild.emoji_limit}", - f"📁 **Dateigröße:** {guild.filesize_limit // 1024 // 1024} MB", - f"🎵 **Audio Qualität:** {64 * (2 ** tier)} kbps" - ] - - if tier >= 2: - benefits.extend([ - "🖼️ **Server Banner:** ✅", - "🎨 **Server Icon Animation:** ✅" - ]) - - if tier >= 3: - benefits.extend([ - "🔗 **Vanity URL:** ✅", - "📺 **Go Live 1080p:** ✅" - ]) - - return "\n".join(benefits) - - def _get_boost_progress(self, current_boosts, current_tier): - """Zeigt den Progress zum nächsten Level""" - requirements = {1: 2, 2: 7, 3: 14} - - if current_tier >= 3: - return "✅ Max Level erreicht!" - - next_tier = current_tier + 1 - needed = requirements[next_tier] - progress = min(current_boosts, needed) - - bar_length = 10 - filled = int((progress / needed) * bar_length) - bar = "█" * filled + "░" * (bar_length - filled) - - return f"{bar} {progress}/{needed}" - - def _get_next_level_info(self, current_boosts, current_tier): - """Informationen über das nächste Boost-Level""" - if current_tier >= 3: - return None - - requirements = {1: 2, 2: 7, 3: 14} - next_tier = current_tier + 1 - needed = requirements[next_tier] - current_boosts - - if needed <= 0: - return f"🎉 Level {next_tier} bereits erreicht!" - - benefits = { - 1: "50 Emoji Slots, bessere Audio-Qualität (128 kbps)", - 2: "150 Emoji Slots, Server Banner, noch bessere Audio (256 kbps)", - 3: "250 Emoji Slots, Vanity URL, beste Audio-Qualität (384 kbps)" - } - - return f"**Level {next_tier}**\nNoch {needed} Boost{'s' if needed != 1 else ''} benötigt\n{benefits[next_tier]}" - - def _format_guild_features(self, features): - """Formatiert Guild-Features für die Anzeige""" - if not features: - return None - - feature_names = { - 'ANIMATED_ICON': '🎭 Animiertes Icon', - 'BANNER': '🖼️ Server Banner', - 'COMMERCE': '🛒 Commerce', - 'COMMUNITY': '🏘️ Community Server', - 'DISCOVERABLE': '🔍 Auffindbar', - 'FEATURABLE': '⭐ Auszeichnungsfähig', - 'INVITE_SPLASH': '🌊 Invite Splash', - 'MEMBER_VERIFICATION_GATE_ENABLED': '🚪 Mitglieder-Verifizierung', - 'NEWS': '📰 News Channel', - 'PARTNERED': '🤝 Partner', - 'PREVIEW_ENABLED': '👀 Preview aktiviert', - 'PUBLIC_DISABLED': '🔒 Nicht öffentlich', - 'VANITY_URL': '🔗 Vanity URL', - 'VERIFIED': '✅ Verifiziert', - 'VIP_REGIONS': '🌟 VIP Regionen', - 'WELCOME_SCREEN_ENABLED': '👋 Willkommensbildschirm' - } - - formatted_features = [] - for feature in features: - display_name = feature_names.get(feature, feature.replace('_', ' ').title()) - formatted_features.append(display_name) - - return "\n".join(formatted_features) if formatted_features else None - - -def setup(bot): - bot.add_cog(ServerInfoCog(bot)) \ No newline at end of file diff --git a/src/cogs/informationen/usermanagemt.py b/src/cogs/informationen/usermanagemt.py deleted file mode 100644 index b535872..0000000 --- a/src/cogs/informationen/usermanagemt.py +++ /dev/null @@ -1,403 +0,0 @@ -import discord -from discord import slash_command, Option, SlashCommandGroup -from discord.ext import commands -import ezcord -from datetime import datetime, timezone -import logging - -class UserManagement(ezcord.Cog): - def __init__(self, bot): - self.bot = bot - - user = SlashCommandGroup("user", "Erweiterte Benutzerverwaltung") - - @user.command(description="Zeigt detaillierte Informationen über einen Benutzer an") - async def info(self, ctx, user: Option(discord.User, "Der Benutzer, über den du Informationen erhalten möchtest", default=None)): - # Wenn kein Benutzer angegeben wurde, zeige Informationen über den Autor - target_user = user or ctx.author - - try: - # Versuche den Benutzer als Member zu bekommen für erweiterte Informationen - if isinstance(target_user, discord.User): - member = ctx.guild.get_member(target_user.id) if ctx.guild else None - else: - member = target_user - - embed = discord.Embed( - title=f"📋 Informationen über {target_user.display_name}", - color=discord.Color.blue(), - timestamp=datetime.now(timezone.utc) - ) - - # Grundlegende Informationen - embed.add_field(name="👤 Benutzername", value=f"{target_user.name}#{target_user.discriminator}", inline=True) - embed.add_field(name="🆔 ID", value=target_user.id, inline=True) - embed.add_field(name="🤖 Bot", value="Ja" if target_user.bot else "Nein", inline=True) - - # Account-Erstellung - created_at = target_user.created_at - embed.add_field( - name="📅 Account erstellt", - value=f"{created_at.strftime('%d.%m.%Y')}\n()", - inline=True - ) - - # Server-spezifische Informationen (nur wenn Member) - if member: - embed.add_field(name="📱 Status", value=str(member.status).capitalize(), inline=True) - - if member.joined_at: - joined_at = member.joined_at - embed.add_field( - name="📥 Server beigetreten", - value=f"{joined_at.strftime('%d.%m.%Y')}\n()", - inline=True - ) - - # Rollen (nur die wichtigsten anzeigen) - roles = [role for role in member.roles[1:] if role.name != "@everyone"][:5] - if roles: - embed.add_field( - name=f"🏷️ Rollen ({len(member.roles)-1} gesamt)", - value=", ".join(role.mention for role in roles) + ("..." if len(member.roles) > 6 else ""), - inline=False - ) - - # Höchste Rolle - if member.top_role.name != "@everyone": - embed.add_field(name="⭐ Höchste Rolle", value=member.top_role.mention, inline=True) - - # Nickname - if member.nick: - embed.add_field(name="📝 Nickname", value=member.nick, inline=True) - - # Gemeinsame Server - mutual_guilds = len(target_user.mutual_guilds) if hasattr(target_user, 'mutual_guilds') else "Unbekannt" - embed.add_field(name="🌐 Gemeinsame Server", value=str(mutual_guilds), inline=True) - - # Avatar - if target_user.avatar: - embed.set_thumbnail(url=target_user.avatar.url) - embed.add_field(name="🖼️ Avatar", value=f"[Link]({target_user.avatar.url})", inline=True) - else: - embed.set_thumbnail(url=target_user.default_avatar.url) - embed.add_field(name="🖼️ Avatar", value="Standard Avatar", inline=True) - - embed.set_footer( - text=f"Angefordert von {ctx.author.display_name}", - icon_url=ctx.author.display_avatar.url - ) - - await ctx.respond(embed=embed) - - except Exception as e: - logging.error(f"Fehler in user info command: {e}") - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Abrufen der Benutzerinformationen.", ephemeral=True) - - @user.command(description="Zeigt alle Rollen eines Benutzers an") - async def roles(self, ctx, user: Option(discord.Member, "Der Benutzer, dessen Rollen du sehen möchtest", default=None)): - target_user = user or ctx.author - - try: - roles = [role for role in target_user.roles[1:] if role.name != "@everyone"] - - if not roles: - embed = discord.Embed( - title="🏷️ Rollen", - description=f"{target_user.display_name} hat keine besonderen Rollen.", - color=discord.Color.orange() - ) - await ctx.respond(embed=embed) - return - - embed = discord.Embed( - title=f"🏷️ Rollen von {target_user.display_name}", - color=target_user.top_role.color or discord.Color.blue(), - timestamp=datetime.now(timezone.utc) - ) - - # Rollen nach Hierarchie sortieren - roles.sort(key=lambda x: x.position, reverse=True) - - role_list = [] - for i, role in enumerate(roles, 1): - permissions_count = sum(1 for perm, value in role.permissions if value) - role_list.append(f"{i}. {role.mention} (Pos: {role.position}, Perms: {permissions_count})") - - # Aufteilen in Chunks falls zu viele Rollen - chunk_size = 10 - for i in range(0, len(role_list), chunk_size): - chunk = role_list[i:i+chunk_size] - field_name = "Rollen" if i == 0 else f"Rollen (Fortsetzung)" - embed.add_field(name=field_name, value="\n".join(chunk), inline=False) - - embed.set_footer(text=f"Gesamt: {len(roles)} Rollen") - await ctx.respond(embed=embed) - - except Exception as e: - logging.error(f"Fehler in user roles command: {e}") - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Abrufen der Rollen.", ephemeral=True) - - @user.command(description="Setzt den Nicknamen eines Benutzers") - @discord.default_permissions(manage_nicknames=True) - async def set_nickname(self, ctx, user: Option(discord.Member, "Der Benutzer, dessen Nicknamen du ändern möchtest"), nickname: Option(str, "Der neue Nickname (leer lassen zum Entfernen)", required=False)): - try: - # Berechtigungsprüfungen - if not ctx.author.guild_permissions.manage_nicknames: - await ctx.respond("❌ Du hast keine Berechtigung, Nicknames zu verwalten.", ephemeral=True) - return - - if user.top_role >= ctx.author.top_role and ctx.author != ctx.guild.owner: - await ctx.respond("❌ Du kannst den Nickname dieses Benutzers nicht ändern (höhere Rolle).", ephemeral=True) - return - - if user == ctx.guild.owner: - await ctx.respond("❌ Der Nickname des Server-Besitzers kann nicht geändert werden.", ephemeral=True) - return - - # Nickname validieren - if nickname and len(nickname) > 32: - await ctx.respond("❌ Der Nickname ist zu lang (maximal 32 Zeichen).", ephemeral=True) - return - - old_nick = user.display_name - await user.edit(nick=nickname, reason=f"Nickname geändert von {ctx.author}") - - embed = discord.Embed( - title="✅ Nickname geändert", - color=discord.Color.green(), - timestamp=datetime.now(timezone.utc) - ) - embed.add_field(name="Benutzer", value=user.mention, inline=True) - embed.add_field(name="Vorher", value=old_nick, inline=True) - embed.add_field(name="Nachher", value=nickname or user.name, inline=True) - embed.set_footer(text=f"Geändert von {ctx.author.display_name}") - - await ctx.respond(embed=embed) - - except discord.Forbidden: - await ctx.respond("❌ Ich habe keine Berechtigung, den Nickname zu ändern.", ephemeral=True) - except Exception as e: - logging.error(f"Fehler beim Setzen des Nicknames: {e}") - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Ändern des Nicknames.", ephemeral=True) - - @user.command(description="Entfernt alle Rollen eines Benutzers") - @discord.default_permissions(manage_roles=True) - async def remove_roles(self, ctx, user: Option(discord.Member, "Der Benutzer, dessen Rollen du entfernen möchtest"), reason: Option(str, "Grund für die Aktion", required=False)): - try: - # Berechtigungsprüfungen - if not ctx.author.guild_permissions.manage_roles: - await ctx.respond("❌ Du hast keine Berechtigung, Rollen zu verwalten.", ephemeral=True) - return - - if user.top_role >= ctx.author.top_role and ctx.author != ctx.guild.owner: - await ctx.respond("❌ Du kannst die Rollen dieses Benutzers nicht verwalten (höhere Rolle).", ephemeral=True) - return - - if user == ctx.guild.owner: - await ctx.respond("❌ Die Rollen des Server-Besitzers können nicht entfernt werden.", ephemeral=True) - return - - removable_roles = [role for role in user.roles[1:] if role < ctx.me.top_role] - - if not removable_roles: - await ctx.respond("❌ Keine Rollen zum Entfernen gefunden oder Bot hat unzureichende Berechtigungen.", ephemeral=True) - return - - # Bestätigung anfordern - embed = discord.Embed( - title="⚠️ Rollen entfernen bestätigen", - description=f"Möchtest du wirklich **{len(removable_roles)} Rollen** von {user.mention} entfernen?\n\n**Rollen:** {', '.join(role.name for role in removable_roles[:5])}{'...' if len(removable_roles) > 5 else ''}", - color=discord.Color.orange() - ) - - view = ConfirmationView() - await ctx.respond(embed=embed, view=view, ephemeral=True) - await view.wait() - - if view.value: - audit_reason = f"Alle Rollen entfernt von {ctx.author}" + (f" | Grund: {reason}" if reason else "") - await user.remove_roles(*removable_roles, reason=audit_reason) - - embed = discord.Embed( - title="✅ Rollen entfernt", - description=f"**{len(removable_roles)} Rollen** wurden von {user.mention} entfernt.", - color=discord.Color.green(), - timestamp=datetime.now(timezone.utc) - ) - embed.set_footer(text=f"Entfernt von {ctx.author.display_name}") - await ctx.edit(embed=embed, view=None) - else: - embed = discord.Embed( - title="❌ Abgebrochen", - description="Die Aktion wurde abgebrochen.", - color=discord.Color.red() - ) - await ctx.edit(embed=embed, view=None) - - except discord.Forbidden: - await ctx.respond("❌ Ich habe keine Berechtigung, diese Rollen zu entfernen.", ephemeral=True) - except Exception as e: - logging.error(f"Fehler beim Entfernen der Rollen: {e}") - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Entfernen der Rollen.", ephemeral=True) - - @user.command(description="Gibt einem Benutzer eine Rolle") - @discord.default_permissions(manage_roles=True) - async def give_role(self, ctx, user: Option(discord.Member, "Der Benutzer, dem du eine Rolle geben möchtest"), role: Option(discord.Role, "Die Rolle, die du vergeben möchtest"), reason: Option(str, "Grund für die Rollenvergabe", required=False)): - try: - # Berechtigungsprüfungen - if not ctx.author.guild_permissions.manage_roles: - await ctx.respond("❌ Du hast keine Berechtigung, Rollen zu verwalten.", ephemeral=True) - return - - if role >= ctx.author.top_role and ctx.author != ctx.guild.owner: - await ctx.respond("❌ Du kannst diese Rolle nicht vergeben (Rolle ist höher als deine).", ephemeral=True) - return - - if role >= ctx.me.top_role: - await ctx.respond("❌ Ich kann diese Rolle nicht vergeben (Rolle ist höher als meine).", ephemeral=True) - return - - if role in user.roles: - await ctx.respond(f"❌ {user.display_name} hat bereits die Rolle {role.mention}.", ephemeral=True) - return - - audit_reason = f"Rolle vergeben von {ctx.author}" + (f" | Grund: {reason}" if reason else "") - await user.add_roles(role, reason=audit_reason) - - embed = discord.Embed( - title="✅ Rolle vergeben", - color=discord.Color.green(), - timestamp=datetime.now(timezone.utc) - ) - embed.add_field(name="Benutzer", value=user.mention, inline=True) - embed.add_field(name="Rolle", value=role.mention, inline=True) - if reason: - embed.add_field(name="Grund", value=reason, inline=False) - embed.set_footer(text=f"Vergeben von {ctx.author.display_name}") - - await ctx.respond(embed=embed) - - except discord.Forbidden: - await ctx.respond("❌ Ich habe keine Berechtigung, diese Rolle zu vergeben.", ephemeral=True) - except Exception as e: - logging.error(f"Fehler beim Vergeben der Rolle: {e}") - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Vergeben der Rolle.", ephemeral=True) - - @user.command(description="Entfernt eine Rolle von einem Benutzer") - @discord.default_permissions(manage_roles=True) - async def remove_role(self, ctx, user: Option(discord.Member, "Der Benutzer, von dem du eine Rolle entfernen möchtest"), role: Option(discord.Role, "Die Rolle, die du entfernen möchtest"), reason: Option(str, "Grund für die Entfernung", required=False)): - try: - # Berechtigungsprüfungen - if not ctx.author.guild_permissions.manage_roles: - await ctx.respond("❌ Du hast keine Berechtigung, Rollen zu verwalten.", ephemeral=True) - return - - if role >= ctx.author.top_role and ctx.author != ctx.guild.owner: - await ctx.respond("❌ Du kannst diese Rolle nicht entfernen (Rolle ist höher als deine).", ephemeral=True) - return - - if role >= ctx.me.top_role: - await ctx.respond("❌ Ich kann diese Rolle nicht entfernen (Rolle ist höher als meine).", ephemeral=True) - return - - if role not in user.roles: - await ctx.respond(f"❌ {user.display_name} hat die Rolle {role.mention} nicht.", ephemeral=True) - return - - audit_reason = f"Rolle entfernt von {ctx.author}" + (f" | Grund: {reason}" if reason else "") - await user.remove_roles(role, reason=audit_reason) - - embed = discord.Embed( - title="✅ Rolle entfernt", - color=discord.Color.green(), - timestamp=datetime.now(timezone.utc) - ) - embed.add_field(name="Benutzer", value=user.mention, inline=True) - embed.add_field(name="Rolle", value=role.mention, inline=True) - if reason: - embed.add_field(name="Grund", value=reason, inline=False) - embed.set_footer(text=f"Entfernt von {ctx.author.display_name}") - - await ctx.respond(embed=embed) - - except discord.Forbidden: - await ctx.respond("❌ Ich habe keine Berechtigung, diese Rolle zu entfernen.", ephemeral=True) - except Exception as e: - logging.error(f"Fehler beim Entfernen der Rolle: {e}") - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Entfernen der Rolle.", ephemeral=True) - - @user.command(description="Zeigt die Berechtigungen eines Benutzers an") - async def permissions(self, ctx, user: Option(discord.Member, "Der Benutzer, dessen Berechtigungen du sehen möchtest", default=None)): - target_user = user or ctx.author - - try: - permissions = target_user.guild_permissions - - embed = discord.Embed( - title=f"🔐 Berechtigungen von {target_user.display_name}", - color=discord.Color.blue(), - timestamp=datetime.now(timezone.utc) - ) - - # Wichtige Berechtigungen hervorheben - admin_perms = [] - mod_perms = [] - basic_perms = [] - - for perm, value in permissions: - if not value: - continue - - perm_name = perm.replace('_', ' ').title() - - if perm in ['administrator']: - admin_perms.append(perm_name) - elif perm in ['manage_guild', 'manage_roles', 'manage_channels', 'ban_members', 'kick_members', 'manage_messages']: - mod_perms.append(perm_name) - else: - basic_perms.append(perm_name) - - if admin_perms: - embed.add_field(name="👑 Administrator", value="\n".join(admin_perms), inline=False) - - if mod_perms: - embed.add_field(name="🛡️ Moderation", value="\n".join(mod_perms), inline=False) - - if basic_perms: - # Nur die ersten 10 anzeigen - basic_display = basic_perms[:10] - if len(basic_perms) > 10: - basic_display.append(f"... und {len(basic_perms) - 10} weitere") - embed.add_field(name="📝 Allgemein", value="\n".join(basic_display), inline=False) - - embed.set_footer(text=f"Gesamt: {sum(1 for _, value in permissions if value)} Berechtigungen") - await ctx.respond(embed=embed) - - except Exception as e: - logging.error(f"Fehler beim Abrufen der Berechtigungen: {e}") - await ctx.respond("❌ Ein Fehler ist aufgetreten beim Abrufen der Berechtigungen.", ephemeral=True) - - -class ConfirmationView(discord.ui.View): - def __init__(self): - super().__init__(timeout=30) - self.value = None - - @discord.ui.button(label="Bestätigen", style=discord.ButtonStyle.danger, emoji="✅") - async def confirm(self, button: discord.ui.Button, interaction: discord.Interaction): - self.value = True - self.stop() - - @discord.ui.button(label="Abbrechen", style=discord.ButtonStyle.secondary, emoji="❌") - async def cancel(self, button: discord.ui.Button, interaction: discord.Interaction): - self.value = False - self.stop() - - async def on_timeout(self): - self.value = False - self.stop() - - -def setup(bot): - bot.add_cog(UserManagement(bot)) \ No newline at end of file diff --git a/src/cogs/moderation/antispam.py b/src/cogs/moderation/antispam.py deleted file mode 100644 index 6664a44..0000000 --- a/src/cogs/moderation/antispam.py +++ /dev/null @@ -1,439 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -from collections import defaultdict -import asyncio -import discord -from discord import SlashCommandGroup -import ezcord -import datetime -from datetime import timedelta - - -from DevTools import AntiSpamDatabase as SpamDB - - -class AntiSpam(ezcord.Cog): - antispam = SlashCommandGroup( - "antispam", - "Verwalte Anti-Spam-Einstellungen und Protokolle.", - ) - - def __init__(self, bot: ezcord.Bot): - self.bot = bot - self.db = SpamDB() - # Track user message timestamps per guild - self.user_messages = defaultdict(lambda: defaultdict(list)) - # Track users currently in timeout to prevent duplicate actions - self.users_in_timeout = set() - - @ezcord.Cog.listener() - async def on_message(self, message): - """Monitor messages for spam detection.""" - # Ignore bot messages and DMs - if message.author.bot or not message.guild: - return - - # Check if user is whitelisted - if self.is_whitelisted(message.guild.id, message.author.id): - return - - # Get spam settings for this guild - settings = self.db.get_spam_settings(message.guild.id) - if not settings: - # If no settings are configured, don't process spam detection - return - - # Check if log channel is set - if not settings.get('log_channel_id'): - return - - # Record this message timestamp - user_id = message.author.id - guild_id = message.guild.id - current_time = datetime.now() - - # Add current message to tracking - self.user_messages[guild_id][user_id].append(current_time) - - # Clean old messages outside the time frame - time_threshold = current_time - timedelta(seconds=settings['time_frame']) - self.user_messages[guild_id][user_id] = [ - timestamp for timestamp in self.user_messages[guild_id][user_id] - if timestamp > time_threshold - ] - - # Check if user exceeded message limit - message_count = len(self.user_messages[guild_id][user_id]) - if message_count > settings['max_messages']: - await self.handle_spam_violation(message, settings) - - async def handle_spam_violation(self, message, settings): - """Handle a user who violated spam limits.""" - user = message.author - guild = message.guild - - # Prevent duplicate actions for the same user - user_timeout_key = f"{guild.id}_{user.id}" - if user_timeout_key in self.users_in_timeout: - return - - self.users_in_timeout.add(user_timeout_key) - - try: - # Log the spam incident - self.db.log_spam(guild.id, user.id, message.content[:100]) # Limit message length - - # Delete recent messages from this user - await self.delete_recent_messages(message.channel, user, limit=settings['max_messages']) - - # Apply timeout (5 minutes) - timeout_duration = timedelta(minutes=5) - timeout_applied = False - - try: - await user.timeout_for(timeout_duration, reason="Anti-Spam: Zu viele Nachrichten") - timeout_applied = True - except discord.Forbidden: - pass # Continue to log even if timeout fails - - # Send log to designated channel - await self.send_spam_log(guild, user, message, settings, timeout_applied) - - # Send warning message in channel - embed = discord.Embed( - title=f"{emoji_forbidden} × Anti-Spam Warnung", - description=f"{user.mention} wurde wegen zu vieler Nachrichten {'stumm geschaltet' if timeout_applied else 'verwarnt'}.", - color=ERROR_COLOR - ) - embed.add_field( - name="Limit überschritten", - value=f"Maximal {settings['max_messages']} Nachrichten in {settings['time_frame']} Sekunden erlaubt", - inline=False - ) - await message.channel.send(embed=embed, delete_after=10) - - # Clear user's message tracking after violation - if guild.id in self.user_messages and user.id in self.user_messages[guild.id]: - self.user_messages[guild_id][user.id].clear() - - # Remove from timeout tracking after delay - await asyncio.sleep(300) # 5 minutes - self.users_in_timeout.discard(user_timeout_key) - - except Exception as e: - print(f"Error handling spam violation: {e}") - self.users_in_timeout.discard(user_timeout_key) - - async def send_spam_log(self, guild, user, message, settings, timeout_applied): - """Send spam log to designated log channel.""" - log_channel_id = settings.get('log_channel_id') - if not log_channel_id: - return - - log_channel = guild.get_channel(log_channel_id) - if not log_channel: - return - - try: - embed = discord.Embed( - title=f"{emoji_warn} × Anti-Spam Verstoß", - color=discord.Color.red(), - timestamp=datetime.now() - ) - embed.add_field( - name=f"{emoji_member} × Benutzer", - value=f"{user.mention} ({user.id})", - inline=True - ) - embed.add_field( - name=f"{emoji_channel} × Kanal", - value=message.channel.mention, - inline=True - ) - embed.add_field( - name=f"{emoji_moderator} × Aktion", - value="Timeout (5 Min)" if timeout_applied else "Warnung", - inline=True - ) - embed.add_field( - name=f"{emoji_statistics} × Limit", - value=f"{settings['max_messages']} Nachrichten in {settings['time_frame']}s", - inline=True - ) - embed.add_field( - name=f"{emoji_annoattention} × Nachricht (Vorschau)", - value=f"```{message.content[:100]}{'...' if len(message.content) > 100 else ''}```", - inline=False - ) - embed.set_footer(text=f"User ID: {user.id}") - embed.set_thumbnail(url=user.display_avatar.url) - - await log_channel.send(embed=embed) - except Exception as e: - print(f"Error sending spam log: {e}") - - async def delete_recent_messages(self, channel, user, limit=5): - """Delete recent messages from a user.""" - try: - messages_to_delete = [] - async for msg in channel.history(limit=50): # Check last 50 messages - if msg.author == user and len(messages_to_delete) < limit: - messages_to_delete.append(msg) - if len(messages_to_delete) >= limit: - break - - for msg in messages_to_delete: - try: - await msg.delete() - except discord.NotFound: - pass # Message already deleted - except discord.Forbidden: - break # No permission to delete - - except Exception as e: - print(f"Error deleting messages: {e}") - - @antispam.command(name="setup", description="Richte das Anti-Spam-System ein.") - async def setup_antispam(self, ctx, log_channel: discord.TextChannel, max_messages: int = 5, time_frame: int = 10): - """Richte das Anti-Spam-System mit einem Log-Channel ein.""" - if not ctx.author.guild_permissions.manage_guild: - await ctx.respond(f"{emoji_no} × Du benötigst die 'Server verwalten' Berechtigung für diesen Befehl.", ephemeral=True) - return - - if max_messages < 1 or max_messages > 50: - await ctx.respond(f"{emoji_no} × Maximale Nachrichten müssen zwischen 1 und 50 liegen.", ephemeral=True) - return - - if time_frame < 1 or time_frame > 300: - await ctx.respond(f"{emoji_no} × Zeitrahmen muss zwischen 1 und 300 Sekunden liegen.", ephemeral=True) - return - - # Check if bot can send messages to log channel - if not log_channel.permissions_for(ctx.guild.me).send_messages: - await ctx.respond(f"{emoji_no} × Ich habe keine Berechtigung, Nachrichten in den angegebenen Log-Channel zu senden.", - ephemeral=True) - return - - self.db.set_spam_settings(ctx.guild.id, max_messages, time_frame, log_channel.id) - - embed = discord.Embed( - title=f"{emoji_yes} × Anti-Spam-System eingerichtet", - color=discord.Color.green() - ) - embed.add_field( - name=f"{emoji_channel} × Log-Channel", - value=log_channel.mention, - inline=True - ) - embed.add_field( - name=f"{emoji_annoattention} × Nachrichtenlimit", - value=f"{max_messages} Nachrichten", - inline=True - ) - embed.add_field( - name=f"{emoji_statistics} × Zeitrahmen", - value=f"{time_frame} Sekunden", - inline=True - ) - embed.add_field( - name=f"{emoji_owner} × Status", - value="🟢 Aktiv", - inline=False - ) - await ctx.respond(embed=embed, ephemeral=True) - - @antispam.command(name="set", description="Ändere Anti-Spam-Parameter.") - async def set_parameters(self, ctx, max_messages: int = None, time_frame: int = None): - """Ändere die Anti-Spam-Parameter (Log-Channel bleibt unverändert).""" - if not ctx.author.guild_permissions.manage_guild: - await ctx.respond(f"{emoji_no} × Du benötigst die 'Server verwalten' Berechtigung für diesen Befehl.", ephemeral=True) - return - - # Get current settings - current_settings = self.db.get_spam_settings(ctx.guild.id) - if not current_settings: - await ctx.respond(f"{emoji_no} × Anti-Spam-System wurde noch nicht eingerichtet. Verwende `/antispam setup` zuerst.", - ephemeral=True) - return - - # Use current values if not provided - new_max_messages = max_messages if max_messages is not None else current_settings['max_messages'] - new_time_frame = time_frame if time_frame is not None else current_settings['time_frame'] - - if new_max_messages < 5 or new_max_messages > 50: - await ctx.respond(f"{emoji_no} × Maximale Nachrichten müssen zwischen 5 und 50 liegen.", ephemeral=True) - return - - if new_time_frame < 5 or new_time_frame > 300: - await ctx.respond(f"{emoji_no} × Zeitrahmen muss zwischen 5 und 300 Sekunden liegen.", ephemeral=True) - return - - self.db.set_spam_settings(ctx.guild.id, new_max_messages, new_time_frame, current_settings['log_channel_id']) - - embed = discord.Embed( - title=f"{emoji_owner} × Anti-Spam Einstellungen aktualisiert", - description=f"Maximal **{new_max_messages}** Nachrichten in **{new_time_frame}** Sekunden erlaubt.", - color=discord.Color.green() - ) - await ctx.respond(embed=embed, ephemeral=True) - - @antispam.command(name="log-channel", description="Ändere den Log-Channel.") - async def set_log_channel(self, ctx, log_channel: discord.TextChannel): - """Ändere den Log-Channel für Anti-Spam.""" - if not ctx.author.guild_permissions.manage_guild: - await ctx.respond(f"{emoji_no} × Du benötigst die 'Server verwalten' Berechtigung für diesen Befehl.", ephemeral=True) - return - - # Check if bot can send messages to log channel - if not log_channel.permissions_for(ctx.guild.me).send_messages: - await ctx.respond(f"{emoji_no} × Ich habe keine Berechtigung, Nachrichten in den angegebenen Log-Channel zu senden.", - ephemeral=True) - return - - self.db.set_log_channel(ctx.guild.id, log_channel.id) - - embed = discord.Embed( - title=f"{emoji_owner} × Log-Channel aktualisiert", - description=f"Anti-Spam-Logs werden nun in {log_channel.mention} gesendet.", - color=discord.Color.green() - ) - await ctx.respond(embed=embed, ephemeral=True) - - @antispam.command(name="view", description="Zeige aktuelle Anti-Spam-Einstellungen an.") - async def view_settings(self, ctx): - """Zeigt die aktuellen Anti-Spam-Einstellungen an.""" - settings = self.db.get_spam_settings(ctx.guild.id) - - if settings and settings.get('log_channel_id'): - log_channel = ctx.guild.get_channel(settings['log_channel_id']) - log_channel_display = log_channel.mention if log_channel else f"{emoji_warn} × Channel nicht gefunden" - - embed = discord.Embed( - title=f"{emoji_owner} × Anti-Spam Einstellungen", - color=discord.Color.blue() - ) - embed.add_field( - name=f"{emoji_channel} × Log-Channel", - value=log_channel_display, - inline=True - ) - embed.add_field( - name=f"{emoji_annoattention} × Nachrichtenlimit", - value=f"{settings['max_messages']} Nachrichten", - inline=True - ) - embed.add_field( - name=f"{emoji_statistics} × Zeitrahmen", - value=f"{settings['time_frame']} Sekunden", - inline=True - ) - embed.add_field( - name=f"{emoji_owner} × Status", - value="🟢 Aktiv", - inline=False - ) - else: - embed = discord.Embed( - title=f"{emoji_owner} × Anti-Spam Einstellungen", - description=f"{emoji_no} × **Anti-Spam-System nicht eingerichtet**\n\nVerwende `/antispam setup` um das System zu konfigurieren.", - color=discord.Color.red() - ) - - await ctx.respond(embed=embed, ephemeral=True) - - @antispam.command(name="logs", description="Zeige Anti-Spam-Logs an.") - async def view_logs(self, ctx, limit: int = 10): - """Zeigt die Anti-Spam-Protokolle an.""" - if not ctx.author.guild_permissions.manage_guild: - await ctx.respond(f"{emoji_no} × Du benötigst die 'Server verwalten' Berechtigung für diesen Befehl.", ephemeral=True) - return - - logs = self.db.get_spam_logs(ctx.guild.id, limit) - - if logs: - embed = discord.Embed( - title=f"{emoji_statistics} × Anti-Spam Protokolle", - color=discord.Color.red() - ) - - log_text = "" - for i, log in enumerate(logs, 1): - user_id, message_preview, timestamp = log - # Try to get user mention, fallback to ID - try: - user = self.bot.get_user(user_id) - user_display = user.mention if user else f"<@{user_id}>" - except: - user_display = f"User ID: {user_id}" - - log_text += f"**{i}.** {user_display}\n" - log_text += f"📝 `{message_preview[:50]}{'...' if len(message_preview) > 50 else ''}`\n" - log_text += f"🕒 {timestamp}\n\n" - - embed.description = log_text - embed.set_footer(text=f"Zeige die letzten {len(logs)} Einträge") - else: - embed = discord.Embed( - title=f"{emoji_statistics} × Anti-Spam Protokolle", - description="Für diesen Server wurden keine Anti-Spam-Logs gefunden.", - color=discord.Color.green() - ) - - await ctx.respond(embed=embed, ephemeral=True) - - @antispam.command(name="clear", description="Lösche alle Anti-Spam-Logs für diesen Server.") - async def clear_logs(self, ctx): - """Löscht alle Anti-Spam-Protokolle für den Server.""" - if not ctx.author.guild_permissions.administrator: - await ctx.respond(f"{emoji_no} × Du benötigst Administrator-Rechte für diesen Befehl.", ephemeral=True) - return - - self.db.clear_spam_logs(ctx.guild.id) - - embed = discord.Embed( - title=f"{emoji_yes} × Protokolle gelöscht", - description="Alle Anti-Spam-Protokolle für diesen Server wurden gelöscht.", - color=discord.Color.green() - ) - await ctx.respond(embed=embed, ephemeral=True) - - @antispam.command(name="whitelist", description="Füge einen Benutzer zur Whitelist hinzu.") - async def add_whitelist(self, ctx, user: discord.Member): - """Fügt einen Benutzer zur Anti-Spam Whitelist hinzu.""" - if not ctx.author.guild_permissions.manage_guild: - await ctx.respond(f"{emoji_no} × Du benötigst die 'Server verwalten' Berechtigung für diesen Befehl.", ephemeral=True) - return - - self.db.add_to_whitelist(ctx.guild.id, user.id) - - embed = discord.Embed( - title=f"{emoji_yes} × Zur Whitelist hinzugefügt", - description=f"{user.mention} wurde zur Anti-Spam Whitelist hinzugefügt.", - color=discord.Color.green() - ) - await ctx.respond(embed=embed, ephemeral=True) - - @antispam.command(name="disable", description="Deaktiviere das Anti-Spam-System.") - async def disable_antispam(self, ctx): - """Deaktiviert das Anti-Spam-System für diesen Server.""" - if not ctx.author.guild_permissions.administrator: - await ctx.respond(f"{emoji_no} × Du benötigst Administrator-Rechte für diesen Befehl.", ephemeral=True) - return - - # Remove settings to disable the system - with self.db.conn: - self.db.conn.execute('DELETE FROM spam_settings WHERE guild_id = ?', (ctx.guild.id,)) - - embed = discord.Embed( - title=f"{emoji_delete} × Anti-Spam-System deaktiviert", - description="Das Anti-Spam-System wurde für diesen Server deaktiviert.\nVerwende `/antispam setup` um es wieder zu aktivieren.", - color=discord.Color.orange() - ) - await ctx.respond(embed=embed, ephemeral=True) - - def is_whitelisted(self, guild_id, user_id): - """Check if user is whitelisted.""" - return self.db.is_whitelisted(guild_id, user_id) - - -def setup(bot: ezcord.Bot): - bot.add_cog(AntiSpam(bot)) \ No newline at end of file diff --git a/src/cogs/moderation/moderation.py b/src/cogs/moderation/moderation.py deleted file mode 100644 index 4956073..0000000 --- a/src/cogs/moderation/moderation.py +++ /dev/null @@ -1,549 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────── -# >> Imports -# ─────────────────────────────────────────────── -import asyncio -import re -from datetime import datetime, timezone -from typing import Optional, Dict, List -import logging - -import discord -import ezcord -from discord import slash_command, option, SlashCommandGroup -import timedelta -from discord.ui import Container -# ─────────────────────────────────────────────── -# >> Cogs -# ─────────────────────────────────────────────── -class moderation(ezcord.Cog): - """Erweiterte Moderations-Cog mit verbesserter Sicherheit und Fehlerbehandlung""" - - def __init__(self, bot): - self.bot = bot - self.max_timeout_days = 28 - self._active_votes: Dict[int, Dict] = {} - self.logger = logging.getLogger(__name__) - - moderation = SlashCommandGroup("mod", "Erweiterte Moderationsbefehle") - - def _has_permission(self, member: discord.Member, permission: str) -> bool: - """Überprüft ob ein Member eine bestimmte Berechtigung hat""" - return getattr(member.guild_permissions, permission, False) - - def _can_moderate_member(self, moderator: discord.Member, target: discord.Member) -> tuple[bool, str]: - """Erweiterte Überprüfung ob ein Moderator ein Ziel-Mitglied moderieren kann""" - if target.id == target.guild.owner_id: - return False, "Der Server-Owner kann nicht moderiert werden." - if moderator.id == target.id: - return False, "Du kannst dich nicht selbst moderieren." - if target.id == self.bot.user.id: - return False, "Ich kann mich nicht selbst moderieren." - if target.bot and not moderator.guild_permissions.administrator: - return False, "Nur Administratoren können Bots moderieren." - if moderator.id != target.guild.owner_id: - if moderator.top_role <= target.top_role: - return False, "Du kannst keine Mitglieder mit gleicher oder höherer Rolle moderieren." - bot_member = target.guild.get_member(self.bot.user.id) - if bot_member and bot_member.top_role <= target.top_role: - return False, "Meine Rolle ist nicht hoch genug, um dieses Mitglied zu moderieren." - return True, "" - - def _parse_duration(self, duration_str: str) -> Optional[timedelta]: - """Erweiterte Dauer-Parsing mit mehr Formaten und besserer Validierung""" - duration_str = duration_str.lower().strip() - pattern = r'(\d+)([smhdw])' - matches = re.findall(pattern, duration_str) - if not matches: - return None - total_seconds = 0 - for amount_str, unit in matches: - try: - amount = int(amount_str) - except ValueError: - return None - if amount < 0: - return None - if unit == 's': - total_seconds += amount - elif unit == 'm': - total_seconds += amount * 60 - elif unit == 'h': - total_seconds += amount * 3600 - elif unit == 'd': - total_seconds += amount * 86400 - elif unit == 'w': - total_seconds += amount * 604800 - else: - return None - if total_seconds < 1: - return None - max_seconds = self.max_timeout_days * 86400 - if total_seconds > max_seconds: - return None - return timedelta(seconds=total_seconds) - - def _format_duration(self, duration: timedelta) -> str: - """Formatiert eine timedelta in einen lesbaren String""" - total_seconds = int(duration.total_seconds()) - weeks = total_seconds // 604800 - days = (total_seconds % 604800) // 86400 - hours = (total_seconds % 86400) // 3600 - minutes = (total_seconds % 3600) // 60 - seconds = total_seconds % 60 - parts = [] - if weeks: parts.append(f"{weeks}w") - if days: parts.append(f"{days}d") - if hours: parts.append(f"{hours}h") - if minutes: parts.append(f"{minutes}m") - if seconds: parts.append(f"{seconds}s") - return " ".join(parts) if parts else "0s" - - def _create_moderation_embed(self, action: str, moderator: discord.Member, target: discord.Member, - reason: str, duration: str = None, additional_info: str = None) -> discord.Embed: - """Erstellt ein einheitliches Moderations-Embed""" - color_map = { - "Bann": discord.Color.dark_red(), - "Kick": discord.Color.red(), - "Timeout": discord.Color.orange(), - "Timeout aufgehoben": discord.Color.green(), - "Slowmode aktiviert": discord.Color.blue(), - "Slowmode deaktiviert": discord.Color.green(), - } - embed = discord.Embed( - title=f"{emoji_yes} × {action} erfolgreich", - color=color_map.get(action, SUCCESS_COLOR), - timestamp=datetime.now(timezone.utc) - ) - embed.set_author(name=AUTHOR) - if target: - embed.add_field(name=f"{emoji_member} × Ziel", value=f"{target.mention} ({target})", inline=True) - embed.add_field(name=f"{emoji_staff} × Moderator", value=moderator.mention, inline=True) - if duration: - embed.add_field(name=f"{emoji_slowmode} × Dauer", value=duration, inline=True) - embed.add_field(name=f"{emoji_summary} × Grund", value=reason, inline=False) - if additional_info: - embed.add_field(name=f"{emoji_summary} × Zusätzlich", value=additional_info, inline=False) - if target: - embed.set_footer(text=f"User ID: {target.id}") - else: - embed.set_footer(text=FLOOTER) - return embed - - def _create_error_embed(self, title: str, description: str, additional_info: str = None) -> discord.Embed: - """Erstellt ein einheitliches Error-Embed""" - embed = discord.Embed( - title=f"{emoji_no} {title}", - description=description, - color=ERROR_COLOR, - timestamp=datetime.now(timezone.utc) - ) - embed.set_author(name=AUTHOR) - if additional_info: - embed.add_field(name=f"{emoji_summary} × Details", value=additional_info, inline=False) - embed.set_footer(text=FLOOTER) - return embed - - @moderation.command(name="ban", description="Bannt ein Mitglied vom Server") - @option("member", discord.Member, description="Das zu bannende Mitglied") - @option("reason", str, description="Grund für den Bann", max_length=500, required=False) - @option("notify_user", bool, description="User per DM benachrichtigen?", required=False, default=True) - async def ban(self, ctx: discord.ApplicationContext, member: discord.Member, - reason: str = "Kein Grund angegeben", notify_user: bool = True): - await ctx.defer(ephemeral=True) - try: - if not self._has_permission(ctx.author, BAN): - embed = self._create_error_embed("Keine Berechtigung", "Du benötigst die `Mitglieder bannen` Berechtigung.") - return await ctx.followup.send(embed=embed) - if not self._has_permission(ctx.guild.me, BAN): - embed = self._create_error_embed("Bot-Berechtigung fehlt", "Mir fehlt die `Mitglieder bannen` Berechtigung.") - return await ctx.followup.send(embed=embed) - can_moderate, error_msg = self._can_moderate_member(ctx.author, member) - if not can_moderate: - embed = self._create_error_embed("Moderation nicht möglich", error_msg) - return await ctx.followup.send(embed=embed) - notification_sent = False - if notify_user: - try: - dm_embed = discord.Embed( - title=f"{emoji_warn} × Du wurdest gebannt", - color=ERROR_COLOR, - description=f"Du wurdest von **{ctx.guild.name}** gebannt." - ) - dm_embed.add_field(name=f"{emoji_summary} × Grund", value=reason, inline=False) - dm_embed.add_field(name=f"{emoji_staff} × Moderator", value=str(ctx.author), inline=True) - dm_embed.set_footer(text="Bei Fragen wende dich an die Serverleitung.") - await member.send(embed=dm_embed) - notification_sent = True - except discord.Forbidden: - pass - ban_reason = f"{reason} | Moderator: {ctx.author} ({ctx.author.id})" - await member.ban(reason=ban_reason) - additional_info = [] - if notification_sent: - additional_info.append(f"{emoji_yes} × User per DM benachrichtigt") - elif notify_user: - additional_info.append(f"{emoji_no} × DM-Benachrichtigung fehlgeschlagen") - embed = self._create_moderation_embed("Bann", ctx.author, member, reason, additional_info="\n".join(additional_info) if additional_info else None) - await ctx.followup.send(embed=embed) - self.logger.info(f"User {member} ({member.id}) banned by {ctx.author} ({ctx.author.id}): {reason}") - except discord.Forbidden: - embed = self._create_error_embed("Berechtigung verweigert", f"Mir fehlen die Berechtigungen, um {member.mention} zu bannen.", "Stelle sicher, dass meine Rolle höher als die des Ziels ist.") - await ctx.followup.send(embed=embed) - except discord.HTTPException as e: - embed = self._create_error_embed("Discord-Fehler", f"Fehler beim Bannen: {str(e)}") - await ctx.followup.send(embed=embed) - except Exception as e: - embed = self._create_error_embed("Unerwarteter Fehler", f"Ein unerwarteter Fehler ist aufgetreten: {str(e)}") - await ctx.followup.send(embed=embed) - self.logger.error(f"Unexpected error in ban command: {e}", exc_info=True) - - @moderation.command(name="kick", description="Kickt ein Mitglied vom Server") - @option("member", discord.Member, description="Das zu kickende Mitglied") - @option("reason", str, description="Grund für den Kick", max_length=500, required=False) - @option("notify_user", bool, description="User per DM benachrichtigen?", required=False, default=True) - async def kick(self, ctx: discord.ApplicationContext, member: discord.Member, - reason: str = "Kein Grund angegeben", notify_user: bool = True): - await ctx.defer(ephemeral=True) - try: - if not self._has_permission(ctx.author, KICK): - embed = self._create_error_embed("Keine Berechtigung", "Du benötigst die `Mitglieder kicken` Berechtigung.") - return await ctx.followup.send(embed=embed) - if not self._has_permission(ctx.guild.me, KICK): - embed = self._create_error_embed("Bot-Berechtigung fehlt", "Mir fehlt die `Mitglieder kicken` Berechtigung.") - return await ctx.followup.send(embed=embed) - can_moderate, error_msg = self._can_moderate_member(ctx.author, member) - if not can_moderate: - embed = self._create_error_embed("Moderation nicht möglich", error_msg) - return await ctx.followup.send(embed=embed) - notification_sent = False - if notify_user: - try: - dm_embed = discord.Embed( - title=f"{emoji_warn} × Du wurdest gekickt", - color=ERROR_COLOR, - description=f"Du wurdest von **{ctx.guild.name}** gekickt." - ) - dm_embed.add_field(name=f"{emoji_summary} × Grund", value=reason, inline=False) - dm_embed.add_field(name=f"{emoji_staff} × Moderator", value=str(ctx.author), inline=True) - dm_embed.set_footer(text="Du kannst dem Server erneut beitreten.") - await member.send(embed=dm_embed) - notification_sent = True - except discord.Forbidden: - pass - kick_reason = f"{reason} | Moderator: {ctx.author} ({ctx.author.id})" - await member.kick(reason=kick_reason) - additional_info = None - if notification_sent: - additional_info = f"{emoji_yes} × User per DM benachrichtigt" - elif notify_user: - additional_info = f"{emoji_no} × DM-Benachrichtigung fehlgeschlagen" - embed = self._create_moderation_embed("Kick", ctx.author, member, reason, additional_info=additional_info) - await ctx.followup.send(embed=embed) - self.logger.info(f"User {member} ({member.id}) kicked by {ctx.author} ({ctx.author.id}): {reason}") - except discord.Forbidden: - embed = self._create_error_embed("Berechtigung verweigert", f"Mir fehlen die Berechtigungen, um {member.mention} zu kicken.", "Stelle sicher, dass meine Rolle höher als die des Ziels ist.") - await ctx.followup.send(embed=embed) - except discord.HTTPException as e: - embed = self._create_error_embed("Discord-Fehler", f"Fehler beim Kicken: {str(e)}") - await ctx.followup.send(embed=embed) - except Exception as e: - embed = self._create_error_embed("Unerwarteter Fehler", f"Ein unerwarteter Fehler ist aufgetreten: {str(e)}") - await ctx.followup.send(embed=embed) - self.logger.error(f"Unexpected error in kick command: {e}", exc_info=True) - - @moderation.command(name="timeout", description="Versetzt ein Mitglied in Timeout") - @option("member", discord.Member, description="Das zu mutende Mitglied") - @option("duration", str, description="Dauer (z.B. 10m, 1h30m, 2d, 1w)") - @option("reason", str, description="Grund für den Timeout", max_length=500, required=False) - @option("notify_user", bool, description="User per DM benachrichtigen?", required=False, default=True) - async def timeout(self, ctx: discord.ApplicationContext, member: discord.Member, - duration: str, reason: str = "Kein Grund angegeben", notify_user: bool = True): - await ctx.defer(ephemeral=True) - try: - if not self._has_permission(ctx.author, MODERATE): - embed = self._create_error_embed("Keine Berechtigung", "Du benötigst die `Mitglieder moderieren` Berechtigung.") - return await ctx.followup.send(embed=embed) - if not self._has_permission(ctx.guild.me, MODERATE): - embed = self._create_error_embed("Bot-Berechtigung fehlt", "Mir fehlt die `Mitglieder moderieren` Berechtigung.") - return await ctx.followup.send(embed=embed) - can_moderate, error_msg = self._can_moderate_member(ctx.author, member) - if not can_moderate: - embed = self._create_error_embed("Moderation nicht möglich", error_msg) - return await ctx.followup.send(embed=embed) - parsed_duration = self._parse_duration(duration) - if parsed_duration is None: - embed = self._create_error_embed("Ungültige Dauer", f"Konnte '{duration}' nicht als gültige Dauer erkennen.", f"Beispiele: `10m`, `1h30m`, `2d`, `1w`\nMaximum: {self.max_timeout_days} Tage") - return await ctx.followup.send(embed=embed) - if member.communication_disabled_until and member.communication_disabled_until > datetime.now(timezone.utc): - current_timeout = member.communication_disabled_until - embed = self._create_error_embed("User bereits in Timeout", f"{member.mention} ist bereits bis {discord.utils.format_dt(current_timeout, 'F')} in Timeout.", "Verwende `/moderation untimeout` um den aktuellen Timeout zu beenden.") - return await ctx.followup.send(embed=embed) - notification_sent = False - if notify_user: - try: - dm_embed = discord.Embed( - title=f"{emoji_warn} × Du wurdest in Timeout versetzt", - color=ERROR_COLOR, - description=f"Du wurdest auf **{ctx.guild.name}** in Timeout versetzt." - ) - dm_embed.add_field(name=f"{emoji_slowmode} × Dauer", value=self._format_duration(parsed_duration), inline=True) - dm_embed.add_field(name=f"{emoji_summary} × Grund", value=reason, inline=False) - dm_embed.add_field(name=f"{emoji_staff} × Moderator", value=str(ctx.author), inline=True) - end_time = datetime.now(timezone.utc) + parsed_duration - dm_embed.add_field(name="🕐 Ende", value=discord.utils.format_dt(end_time, 'F'), inline=True) - dm_embed.set_footer(text="Bitte beachte die Serverregeln.") - await member.send(embed=dm_embed) - notification_sent = True - except discord.Forbidden: - pass - timeout_reason = f"{reason} | Moderator: {ctx.author} ({ctx.author.id})" - await member.timeout_for(parsed_duration, reason=timeout_reason) - additional_info = None - if notification_sent: - additional_info = f"{emoji_yes} × User per DM benachrichtigt" - elif notify_user: - additional_info = f"{emoji_no} × DM-Benachrichtigung fehlgeschlagen" - embed = self._create_moderation_embed("Timeout", ctx.author, member, reason, duration=self._format_duration(parsed_duration), additional_info=additional_info) - end_time = datetime.now(timezone.utc) + parsed_duration - embed.add_field(name="🕐 Ende", value=discord.utils.format_dt(end_time, 'F'), inline=False) - await ctx.followup.send(embed=embed) - self.logger.info(f"User {member} ({member.id}) timed out by {ctx.author} ({ctx.author.id}) for {duration}: {reason}") - except discord.Forbidden: - embed = self._create_error_embed("Berechtigung verweigert", f"Mir fehlen die Berechtigungen, um {member.mention} zu timeouten.", "Stelle sicher, dass meine Rolle höher als die des Ziels ist.") - await ctx.followup.send(embed=embed) - except discord.HTTPException as e: - embed = self._create_error_embed("Discord-Fehler", f"Fehler beim Timeout: {str(e)}") - await ctx.followup.send(embed=embed) - except Exception as e: - embed = self._create_error_embed("Unerwarteter Fehler", f"Ein unerwarteter Fehler ist aufgetreten: {str(e)}") - await ctx.followup.send(embed=embed) - self.logger.error(f"Unexpected error in timeout command: {e}", exc_info=True) - - @moderation.command(name="untimeout", description="Hebt einen Timeout auf") - @option("member", discord.Member, description="Das Mitglied, dessen Timeout aufgehoben werden soll") - @option("reason", str, description="Grund für die Aufhebung", max_length=500, required=False) - @option("notify_user", bool, description="User per DM benachrichtigen?", required=False, default=True) - async def untimeout(self, ctx: discord.ApplicationContext, member: discord.Member, - reason: str = "Kein Grund angegeben", notify_user: bool = True): - await ctx.defer(ephemeral=True) - try: - if not self._has_permission(ctx.author, MODERATE): - embed = self._create_error_embed("Keine Berechtigung", "Du benötigst die `Mitglieder moderieren` Berechtigung.") - return await ctx.followup.send(embed=embed) - if not self._has_permission(ctx.guild.me, MODERATE): - embed = self._create_error_embed("Bot-Berechtigung fehlt", "Mir fehlt die `Mitglieder moderieren` Berechtigung.") - return await ctx.followup.send(embed=embed) - if (member.communication_disabled_until is None or member.communication_disabled_until <= datetime.now(timezone.utc)): - embed = self._create_error_embed("Kein aktiver Timeout", f"{member.mention} ist derzeit nicht in Timeout.") - return await ctx.followup.send(embed=embed) - notification_sent = False - if notify_user: - try: - dm_embed = discord.Embed( - title=f"{emoji_yes} × Dein Timeout wurde aufgehoben", - color=SUCCESS_COLOR, - description=f"Dein Timeout auf **{ctx.guild.name}** wurde vorzeitig aufgehoben." - ) - dm_embed.add_field(name=f"{emoji_summary} × Grund", value=reason, inline=False) - dm_embed.add_field(name=f"{emoji_staff} × Moderator", value=str(ctx.author), inline=True) - dm_embed.set_footer(text="Bitte beachte weiterhin die Serverregeln.") - await member.send(embed=dm_embed) - notification_sent = True - except discord.Forbidden: - pass - untimeout_reason = f"{reason} | Moderator: {ctx.author} ({ctx.author.id})" - await member.remove_timeout(reason=untimeout_reason) - additional_info = None - if notification_sent: - additional_info = f"{emoji_yes} × User per DM benachrichtigt" - elif notify_user: - additional_info = f"{emoji_no} × DM-Benachrichtigung fehlgeschlagen" - embed = self._create_moderation_embed("Timeout aufgehoben", ctx.author, member, reason, additional_info=additional_info) - await ctx.followup.send(embed=embed) - self.logger.info(f"Timeout removed from {member} ({member.id}) by {ctx.author} ({ctx.author.id}): {reason}") - except discord.Forbidden: - embed = self._create_error_embed("Berechtigung verweigert", f"Mir fehlen die Berechtigungen, um den Timeout von {member.mention} aufzuheben.") - await ctx.followup.send(embed=embed) - except discord.HTTPException as e: - embed = self._create_error_embed("Discord-Fehler", f"Fehler beim Aufheben des Timeouts: {str(e)}") - await ctx.followup.send(embed=embed) - except Exception as e: - embed = self._create_error_embed("Unerwarteter Fehler", f"Ein unerwarteter Fehler ist aufgetreten: {str(e)}") - await ctx.followup.send(embed=embed) - self.logger.error(f"Unexpected error in untimeout command: {e}", exc_info=True) - - @moderation.command(name="slowmode", description="Setzt den Slowmode für den aktuellen Channel") - @option("duration", str, description="Dauer (z.B. 10s, 5m, 1h) oder '0' zum Deaktivieren", default="0") - @option("reason", str, description="Grund für den Slowmode", max_length=500, required=False) - async def slowmode(self, ctx: discord.ApplicationContext, duration: str = "0", reason: str = "Kein Grund angegeben"): - await ctx.defer(ephemeral=True) - try: - if not ctx.author.guild_permissions.manage_channels: - embed = self._create_error_embed("Keine Berechtigung", "Du benötigst die `Kanäle verwalten` Berechtigung.") - return await ctx.followup.send(embed=embed) - if not ctx.guild.me.guild_permissions.manage_channels: - embed = self._create_error_embed("Bot-Berechtigung fehlt", "Mir fehlt die `Kanäle verwalten` Berechtigung.") - return await ctx.followup.send(embed=embed) - if duration == "0": - seconds = 0 - else: - parsed_duration = self._parse_duration(duration) - if parsed_duration is None: - embed = self._create_error_embed("Ungültige Dauer", f"Konnte '{duration}' nicht als gültige Dauer erkennen.", "Beispiele: `10s`, `5m`, `1h` oder `0` zum Deaktivieren") - return await ctx.followup.send(embed=embed) - seconds = int(parsed_duration.total_seconds()) - if seconds < 0 or seconds > 21600: - embed = self._create_error_embed("Ungültiger Zeitraum", f"Slowmode muss zwischen 0 Sekunden und 6 Stunden liegen.", f"Eingabe: {seconds} Sekunden") - return await ctx.followup.send(embed=embed) - old_slowmode = ctx.channel.slowmode_delay - slowmode_reason = f"{reason} | Moderator: {ctx.author} ({ctx.author.id})" - await ctx.channel.edit(slowmode_delay=seconds, reason=slowmode_reason) - if seconds == 0: - action = "Slowmode deaktiviert" - additional_info = f"Vorheriger Slowmode: {old_slowmode}s" if old_slowmode > 0 else None - else: - action = "Slowmode aktiviert" - additional_info = f"{emoji_slowmode} Slowmode auf {seconds} Sekunden gesetzt" - embed = self._create_moderation_embed(action, ctx.author, None, reason, duration=f"{seconds} Sekunden" if seconds > 0 else "Deaktiviert", additional_info=additional_info) - embed.add_field(name="📢 × Kanal", value=ctx.channel.mention, inline=True) - await ctx.followup.send(embed=embed) - self.logger.info(f"Slowmode set to {seconds}s in {ctx.channel} by {ctx.author} ({ctx.author.id}): {reason}") - except discord.Forbidden: - embed = self._create_error_embed("Berechtigung verweigert", "Mir fehlen die Berechtigungen, um den Slowmode zu setzen.") - await ctx.followup.send(embed=embed) - except discord.HTTPException as e: - embed = self._create_error_embed("Discord-Fehler", f"Fehler beim Setzen des Slowmodes: {str(e)}") - await ctx.followup.send(embed=embed) - except Exception as e: - embed = self._create_error_embed("Unerwarteter Fehler", f"Ein unerwarteter Fehler ist aufgetreten: {str(e)}") - await ctx.followup.send(embed=embed) - self.logger.error(f"Unexpected error in slowmode command: {e}", exc_info=True) - - @moderation.command(name="votekick", description="Startet eine Votekick-Abstimmung für ein Mitglied") - @option("member", discord.Member, description="Das zu kickende Mitglied") - @option("reason", str, description="Grund für den Kick", max_length=500, required=False) - @option("duration", int, description="Abstimmungsdauer in Minuten (Standard: 5)", min_value=1, max_value=30, required=False) - async def votekick(self, ctx: discord.ApplicationContext, member: discord.Member, reason: str = "Kein Grund angegeben", duration: int = 5): - await ctx.defer() - try: - if not self._has_permission(ctx.author, KICK): - embed = self._create_error_embed("Keine Berechtigung", "Du benötigst die `Mitglieder kicken` Berechtigung.") - return await ctx.followup.send(embed=embed, ephemeral=True) - if not self._has_permission(ctx.guild.me, KICK): - embed = self._create_error_embed("Bot-Berechtigung fehlt", "Mir fehlt die `Mitglieder kicken` Berechtigung.") - return await ctx.followup.send(embed=embed, ephemeral=True) - can_moderate, error_msg = self._can_moderate_member(ctx.author, member) - if not can_moderate: - embed = self._create_error_embed("Moderation nicht möglich", error_msg) - return await ctx.followup.send(embed=embed, ephemeral=True) - if member.id in self._active_votes: - embed = self._create_error_embed("Abstimmung bereits aktiv", f"Es läuft bereits eine Abstimmung für {member.mention}.") - return await ctx.followup.send(embed=embed, ephemeral=True) - end_time = datetime.now(timezone.utc) + timedelta(minutes=duration) - embed = discord.Embed( - title=f"🗳️ × Votekick für {member.display_name}", - description=f"{ctx.author.mention} möchte {member.mention} kicken.\n\n" - f"**{emoji_summary} Grund:** {reason}\n\n" - f"Reagiere mit {emoji_yes} zum Kicken oder {emoji_no} zum Ablehnen.\n" - f"**🕐 Ende:** {discord.utils.format_dt(end_time, 'R')}", - color=discord.Color.orange(), - timestamp=datetime.now(timezone.utc) - ) - embed.set_author(name=f"Gestartet von {ctx.author}", icon_url=ctx.author.display_avatar.url) - embed.add_field(name=f"{emoji_member} × Ziel", value=f"{member.mention}", inline=True) - embed.add_field(name=f"{emoji_slowmode} × Dauer", value=f"{duration} Minuten", inline=True) - embed.set_footer(text=f"Votekick ID: {member.id}") - message = await ctx.followup.send(embed=embed) - await message.add_reaction(emoji_yes) - await message.add_reaction(emoji_no) - self._active_votes[member.id] = { - 'message': message, - 'initiator': ctx.author, - 'target': member, - 'reason': reason, - 'end_time': end_time, - 'guild': ctx.guild - } - asyncio.create_task(self._handle_votekick(member.id, duration * 60)) - except Exception as e: - embed = self._create_error_embed("Unerwarteter Fehler", f"Fehler bei der Votekick-Abstimmung: {str(e)}") - try: - await ctx.followup.send(embed=embed, ephemeral=True) - except: - await ctx.respond(embed=embed, ephemeral=True) - self.logger.error(f"Unexpected error in votekick command: {e}", exc_info=True) - - async def _handle_votekick(self, member_id: int, duration_seconds: int): - """Verwaltet eine Votekick-Abstimmung""" - await asyncio.sleep(duration_seconds) - if member_id not in self._active_votes: - return - vote_data = self._active_votes[member_id] - message = vote_data['message'] - target = vote_data['target'] - initiator = vote_data['initiator'] - reason = vote_data['reason'] - guild = vote_data['guild'] - try: - message = await message.channel.fetch_message(message.id) - yes_count = 0 - no_count = 0 - voters = set() - for reaction in message.reactions: - if str(reaction.emoji) == emoji_yes: - async for user in reaction.users(): - if not user.bot and user.id not in voters: - yes_count += 1 - voters.add(user.id) - elif str(reaction.emoji) == emoji_no: - async for user in reaction.users(): - if not user.bot and user.id not in voters: - no_count += 1 - voters.add(user.id) - total_members = len([m for m in guild.members if not m.bot]) - required_votes = max(3, total_members // 4) - if yes_count >= required_votes and yes_count > no_count: - try: - kick_reason = f"Votekick | Grund: {reason} | Initiator: {initiator} | Ja: {yes_count}, Nein: {no_count}" - await target.kick(reason=kick_reason) - result_embed = discord.Embed( - title=f"{emoji_yes} Votekick erfolgreich", - description=f"{emoji_member} {target.mention} wurde gekickt.", - color=discord.Color.green(), - timestamp=datetime.now(timezone.utc) - ) - result_embed.add_field(name="📊 × Ergebnis", value=f"{emoji_yes} Ja: {yes_count} | {emoji_no} Nein: {no_count}\n{emoji_summary} Benötigt: {required_votes}", inline=False) - self.logger.info(f"Votekick successful: {target} ({target.id}) kicked with {yes_count} votes") - except discord.Forbidden: - result_embed = discord.Embed( - title=f"{emoji_no} × Votekick fehlgeschlagen", - description=f"Berechtigung fehlt, um {target.mention} zu kicken.", - color=discord.Color.red(), - timestamp=datetime.now(timezone.utc) - ) - except discord.HTTPException as e: - result_embed = discord.Embed( - title=f"{emoji_no} × Votekick fehlgeschlagen", - description=f"Fehler beim Kicken: {str(e)}", - color=discord.Color.red(), - timestamp=datetime.now(timezone.utc) - ) - else: - result_embed = discord.Embed( - title=f"{emoji_no} × Votekick abgelehnt", - description=f"{emoji_member} {target.mention} wurde nicht gekickt.", - color=discord.Color.red(), - timestamp=datetime.now(timezone.utc) - ) - result_embed.add_field(name="📊 × Ergebnis", value=f"{emoji_yes} Ja: {yes_count} | {emoji_no} Nein: {no_count}\n{emoji_summary} Benötigt: {required_votes}", inline=False) - await message.edit(embed=result_embed, view=None) - except Exception as e: - self.logger.error(f"Error handling votekick result: {e}", exc_info=True) - finally: - if member_id in self._active_votes: - del self._active_votes[member_id] - - -def setup(bot): - bot.add_cog(moderation(bot)) \ No newline at end of file diff --git a/src/cogs/moderation/notes.py b/src/cogs/moderation/notes.py deleted file mode 100644 index 7475c7d..0000000 --- a/src/cogs/moderation/notes.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────── -# >> Imports -# ─────────────────────────────────────────────── -import discord -from discord import SlashCommandGroup -import datetime -import ezcord -from DevTools import NotesDatabase -# ─────────────────────────────────────────────── -# >> Cog -# ─────────────────────────────────────────────── -class NotesCog(ezcord.Cog, group="moderation"): - notes = SlashCommandGroup("notes", "📝 Verwaltung von Notizen für User") - - def __init__(self, bot): - self.bot = bot - self.db = NotesDatabase("data") - - @notes.command(name="add", description="📝 Speichere eine Notiz für einen User") - async def add( - self, - ctx: discord.ApplicationContext, - user: discord.Member, - *, - content: str - ): - if not content: - return await ctx.respond("Bitte gib den Inhalt der Notiz an.", ephemeral=True) - - timestamp = datetime.datetime.now().strftime("%d.%m.%Y %H:%M") - self.db.add_note(ctx.guild.id, user.id, ctx.author.id, ctx.author.name, content, timestamp) - await ctx.respond(f"Notiz für {user.mention} gespeichert.", ephemeral=True) - - @notes.command(name="list", description="📜 Zeige alle Notizen eines Users an") - async def list(self, ctx: discord.ApplicationContext, user: discord.Member): - notes = self.db.get_notes(ctx.guild.id, user.id) - - if not notes: - return await ctx.respond(f"{emoji_no} {emoji_user}{user.mention} hat keine Notizen.", ephemeral=True) - - embed = discord.Embed(title=f"Notizen für {user.name}", color=discord.Color.blurple()) - for note in notes: - embed.add_field( - name=f"ID: {note['id']} – von {note['author_name']} am {note['timestamp']}", - value=note['content'], - inline=False - ) - - await ctx.respond(embed=embed, ephemeral=True) - - @notes.command(name="delete", description="🗑️ Lösche eine Notiz eines Users") - async def delete(self, ctx: discord.ApplicationContext, user: discord.Member, note_id: int): - notes = self.db.get_notes(ctx.guild.id, user.id) - if not notes: - return await ctx.respond(f"User {user} (ID: {user.id}) hat keine Notizen.", ephemeral=True) - - note_ids = [note['id'] for note in notes] - if note_id not in note_ids: - return await ctx.respond(f"{emoji_no} Notiz mit ID {note_id} existiert nicht für User {user}.", ephemeral=True) - - if self.db.delete_note(note_id): - await ctx.respond(f"{emoji_yes} Notiz mit ID {note_id} von User {user} wurde gelöscht.", ephemeral=True) - else: - await ctx.respond(f"{emoji_no} Fehler beim Löschen der Notiz mit ID {note_id}.", ephemeral=True) - - -def setup(bot): - bot.add_cog(NotesCog(bot)) diff --git a/src/cogs/moderation/warningsystem.py b/src/cogs/moderation/warningsystem.py deleted file mode 100644 index c56dd7f..0000000 --- a/src/cogs/moderation/warningsystem.py +++ /dev/null @@ -1,557 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# ─────────────────────────────────────────────── -# >> Imports -# ─────────────────────────────────────────────── -from DevTools import WarnDatabase -import discord -from discord import slash_command, Option -import os -import datetime -import ezcord -import asyncio -from typing import Optional - - -# ─────────────────────────────────────────────── -# >> Cogs -# ─────────────────────────────────────────────── -class WarnSystem(ezcord.Cog, group="moderation"): - """Erweiterte Warn-System Cog mit verbesserter Funktionalität""" - - def __init__(self, bot): - self.bot = bot - base_path = os.path.dirname(__file__) - self.db = WarnDatabase(base_path) - # Cache für bessere Performance - self._warn_cache = {} - - def _has_moderate_permissions(self, member: discord.Member) -> bool: - """Überprüft ob ein Member Moderationsrechte hat""" - return ( - member.guild_permissions.kick_members or - member.guild_permissions.ban_members or - member.guild_permissions.manage_messages or - member.guild_permissions.moderate_members - ) - - def _can_warn_member(self, moderator: discord.Member, target: discord.Member) -> tuple[bool, str]: - """Überprüft ob ein Moderator ein Ziel-Mitglied verwarnen kann""" - - # Server-Owner kann nicht verwarnt werden - if target.id == target.guild.owner_id: - return False, "Der Server Owner kann nicht verwarnt werden." - - # Selbst-Verwarnung verhindern - if moderator.id == target.id: - return False, "Du kannst dich nicht selbst verwarnen." - - # Bot kann nicht verwarnt werden - if target.bot: - return False, "Du kannst keine Bots verwarnen." - - # Rollen-Hierarchie prüfen (außer bei Owner) - if (moderator.top_role <= target.top_role and - moderator.id != target.guild.owner_id): - return False, "Du kannst keine Mitglieder mit gleicher oder höherer Rolle verwarnen." - - return True, "" - - def _create_warn_embed(self, action: str, moderator: discord.Member, - target: discord.Member, reason: str, - timestamp: str, warn_id: int = None) -> discord.Embed: - """Erstellt ein einheitliches Warn-Embed""" - - if action == "warn": - embed = discord.Embed( - title=f"{emoji_warn} Warnung erteilt", - color=SUCCESS_COLOR, - description=f"{target.mention} wurde erfolgreich verwarnt." - ) - elif action == "unwarn": - embed = discord.Embed( - title=f"{emoji_yes} Warnung entfernt", - color=SUCCESS_COLOR, - description=f"Warnung wurde erfolgreich entfernt." - ) - else: - embed = discord.Embed( - title=f" {action}", - color=SUCCESS_COLOR - ) - - embed.set_author(name=AUTHOR) - - if action == "warn": - embed.add_field(name=f"{emoji_member} × Verwarnter User", value=target.mention, inline=True) - embed.add_field(name=f"{emoji_staff} × Verwarnt von", value=moderator.mention, inline=True) - embed.add_field(name=f"{emoji_summary} × Grund", value=reason, inline=False) - embed.add_field(name=f"{emoji_slowmode} × Zeitstempel", value=timestamp, inline=False) - embed.set_footer(text="Powered by ManagerX") - - elif action == "unwarn": - embed.add_field(name=f" Entfernt von", value=moderator.mention, inline=True) - if warn_id: - embed.add_field(name=f" Warnung ID", value=f"`{warn_id}`", inline=True) - embed.set_footer(text=FLOOTER) - - return embed - - def _create_error_embed(self, title: str, message: str) -> discord.Embed: - """Erstellt ein einheitliches Error-Embed""" - embed = discord.Embed(title=title, color=ERROR_COLOR) - embed.set_author(name=AUTHOR) - embed.add_field(name=f"{emoji_no} {title}", value=message, inline=False) - embed.set_footer(text=FLOOTER) - return embed - - @slash_command(name="warn", description="Warnen Sie einen Benutzer") - async def warn( - self, - ctx, - user: Option(discord.Member, "User to warn"), - reason: Option(str, "Reason for the warning", max_length=500) - ): - try: - # Berechtigung prüfen - if not self._has_moderate_permissions(ctx.author): - embed = self._create_error_embed( - "Keine Berechtigung", - "Du benötigst Moderationsrechte, um Mitglieder zu verwarnen." - ) - return await ctx.respond(embed=embed, ephemeral=True) - - # Kann Mitglied verwarnt werden? - can_warn, error_msg = self._can_warn_member(ctx.author, user) - if not can_warn: - embed = self._create_error_embed("Verwarnung nicht möglich", error_msg) - return await ctx.respond(embed=embed, ephemeral=True) - - # Warn-Daten erstellen - timestamp = datetime.datetime.utcnow().strftime("%d.%m.%Y %H:%M") - - # In Datenbank speichern - try: - self.db.add_warning(ctx.guild.id, user.id, ctx.author.id, reason, timestamp) - - # Cache invalidieren - cache_key = f"{ctx.guild.id}_{user.id}" - if cache_key in self._warn_cache: - del self._warn_cache[cache_key] - - except Exception as e: - embed = self._create_error_embed( - "Datenbankfehler", - f"Fehler beim Speichern der Warnung: {str(e)}" - ) - return await ctx.respond(embed=embed, ephemeral=True) - - # Erfolgs-Embed - success_embed = self._create_warn_embed("warn", ctx.author, user, reason, timestamp) - await ctx.respond(embed=success_embed, ephemeral=True) - - # Optional: DM an verwarnten User senden - try: - dm_embed = discord.Embed( - title=f"{emoji_warn} Du wurdest verwarnt", - color=ERROR_COLOR, - description=f"Du wurdest auf **{ctx.guild.name}** verwarnt." - ) - dm_embed.add_field(name=f"{emoji_summary} × Grund", value=reason, inline=False) - dm_embed.add_field(name=f"{emoji_staff} × Moderator", value=str(ctx.author), inline=True) - dm_embed.add_field(name=f"{emoji_slowmode} × Zeitpunkt", value=timestamp, inline=True) - dm_embed.set_footer(text="Powered by ManagerX") - - await user.send(embed=dm_embed) - except discord.Forbidden: - # User hat DMs deaktiviert - ignorieren - pass - - except Exception as e: - embed = self._create_error_embed( - "Unerwarteter Fehler", - f"Ein unerwarteter Fehler ist aufgetreten: {str(e)}" - ) - await ctx.respond(embed=embed, ephemeral=True) - - @slash_command(name="warnings", description="Zeigt die Verwarnungen eines Users an") - async def warnings( - self, - ctx, - user: Option(discord.Member, "User whose warnings to show", required=False) - ): - try: - # Wenn kein User angegeben, eigene Warnungen zeigen - target_user = user if user else ctx.author - - # Cache prüfen - cache_key = f"{ctx.guild.id}_{target_user.id}" - - if cache_key in self._warn_cache: - results = self._warn_cache[cache_key] - else: - # Warnungen aus Datenbank laden - results = self.db.get_warnings(ctx.guild.id, target_user.id) - self._warn_cache[cache_key] = results - - # Überprüfung ob User Warnungen einsehen darf - if target_user != ctx.author and not self._has_moderate_permissions(ctx.author): - embed = self._create_error_embed( - "Keine Berechtigung", - "Du kannst nur deine eigenen Warnungen einsehen." - ) - return await ctx.respond(embed=embed, ephemeral=True) - - if not results: - # Keine Warnungen vorhanden - no_warnings_embed = discord.Embed( - title=f"{emoji_circleinfo} Keine Verwarnungen", - color=SUCCESS_COLOR, - description=f"{target_user.mention} hat keine Verwarnungen." - ) - no_warnings_embed.set_author(name=AUTHOR) - no_warnings_embed.set_footer(text=FLOOTER) - return await ctx.respond(embed=no_warnings_embed, ephemeral=True) - - # Warnungen-Liste aufteilen falls zu viele (max 10 pro Seite) - warnings_per_page = 10 - total_warnings = len(results) - total_pages = (total_warnings + warnings_per_page - 1) // warnings_per_page - - if total_pages == 1: - # Alle Warnungen auf einer Seite - warn_list = "\n".join([ - f"**ID `{warn_id}`** | {timestamp}\n└ **Grund:** {reason[:100]}{'...' if len(reason) > 100 else ''}" - for warn_id, reason, timestamp in results[:warnings_per_page] - ]) - - warnings_embed = discord.Embed( - title=f"{emoji_warn} Verwarnungen für {target_user.display_name}", - color=ERROR_COLOR, - description=warn_list - ) - warnings_embed.set_author(name=AUTHOR) - warnings_embed.add_field(name=f"{emoji_member} User", value=target_user.mention, inline=True) - warnings_embed.add_field(name=f"{emoji_summary} Anzahl Verwarnungen", value=str(total_warnings), inline=True) - warnings_embed.set_footer(text=FLOOTER) - - await ctx.respond(embed=warnings_embed, ephemeral=True) - else: - # Mehrere Seiten - ersten 10 zeigen mit Navigation - await self._send_paginated_warnings(ctx, target_user, results, 0) - - except Exception as e: - embed = self._create_error_embed( - "Unerwarteter Fehler", - f"Fehler beim Laden der Warnungen: {str(e)}" - ) - await ctx.respond(embed=embed, ephemeral=True) - - async def _send_paginated_warnings(self, ctx, target_user: discord.Member, - warnings: list, page: int = 0): - """Sendet paginierte Warnungen mit Navigation""" - warnings_per_page = 10 - total_pages = (len(warnings) + warnings_per_page - 1) // warnings_per_page - - start_idx = page * warnings_per_page - end_idx = min(start_idx + warnings_per_page, len(warnings)) - page_warnings = warnings[start_idx:end_idx] - - warn_list = "\n".join([ - f"**ID `{warn_id}`** | {timestamp}\n└ **Grund:** {reason[:100]}{'...' if len(reason) > 100 else ''}" - for warn_id, reason, timestamp in page_warnings - ]) - - embed = discord.Embed( - title=f"{emoji_warn} Verwarnungen für {target_user.display_name}", - color=ERROR_COLOR, - description=warn_list - ) - embed.set_author(name=AUTHOR) - embed.add_field(name=f"{emoji_member} User", value=target_user.mention, inline=True) - embed.add_field(name=f"{emoji_summary} Anzahl Verwarnungen", value=str(len(warnings)), inline=True) - embed.set_footer(text=f"Seite {page + 1}/{total_pages} • {FLOOTER}") - - # View für Navigation erstellen - view = WarningsView(self, target_user, warnings, page, total_pages) - - if ctx.response.is_done(): - await ctx.followup.send(embed=embed, view=view, ephemeral=True) - else: - await ctx.respond(embed=embed, view=view, ephemeral=True) - - @slash_command(name="unwarn", description="Löscht eine Verwarnung mit ID") - async def unwarn( - self, - ctx, - warn_id: Option(int, "Die ID der Verwarnung", min_value=1) - ): - try: - # Berechtigung prüfen - if not self._has_moderate_permissions(ctx.author): - embed = self._create_error_embed( - "Keine Berechtigung", - "Du benötigst Moderationsrechte, um Verwarnungen zu löschen." - ) - return await ctx.respond(embed=embed, ephemeral=True) - - # Warnung suchen - result = self.db.get_warning_by_id(warn_id) - if not result: - embed = self._create_error_embed( - "Verwarnung nicht gefunden", - f"Keine Verwarnung mit der ID `{warn_id}` gefunden." - ) - return await ctx.respond(embed=embed, ephemeral=True) - - # Überprüfen ob Warnung zu diesem Server gehört - warn_guild_id = result[1] # guild_id ist der zweite Wert - if warn_guild_id != ctx.guild.id: - embed = self._create_error_embed( - "Verwarnung nicht gefunden", - f"Keine Verwarnung mit der ID `{warn_id}` in diesem Server gefunden." - ) - return await ctx.respond(embed=embed, ephemeral=True) - - # Warnung löschen - success = self.db.delete_warning(warn_id) - if not success: - embed = self._create_error_embed( - "Löschfehler", - f"Fehler beim Löschen der Verwarnung `{warn_id}`." - ) - return await ctx.respond(embed=embed, ephemeral=True) - - # Cache invalidieren - user_id = result[2] # user_id ist der dritte Wert - cache_key = f"{ctx.guild.id}_{user_id}" - if cache_key in self._warn_cache: - del self._warn_cache[cache_key] - - # Erfolgs-Embed - removal_embed = self._create_warn_embed("unwarn", ctx.author, None, None, None, warn_id) - await ctx.respond(embed=removal_embed, ephemeral=True) - - except Exception as e: - embed = self._create_error_embed( - "Unerwarteter Fehler", - f"Fehler beim Löschen der Verwarnung: {str(e)}" - ) - await ctx.respond(embed=embed, ephemeral=True) - - @slash_command(name="clearwarns", description="Löscht alle Verwarnungen eines Users") - async def clearwarns( - self, - ctx, - user: Option(discord.Member, "User dessen Warnungen gelöscht werden sollen"), - reason: Option(str, "Grund für das Löschen", required=False, default="Kein Grund angegeben") - ): - try: - # Nur Administratoren können alle Warnungen löschen - if not ctx.author.guild_permissions.administrator: - embed = self._create_error_embed( - "Keine Berechtigung", - "Du benötigst Administrator-Rechte, um alle Warnungen zu löschen." - ) - return await ctx.respond(embed=embed, ephemeral=True) - - # Aktuelle Warnungen zählen - warn_count = self.db.get_warning_count(ctx.guild.id, user.id) - - if warn_count == 0: - embed = discord.Embed( - title=f"{emoji_summary} Keine Verwarnungen", - color=SUCCESS_COLOR, - description=f"{user.mention} hat keine Verwarnungen zum Löschen." - ) - embed.set_author(name=AUTHOR) - return await ctx.respond(embed=embed, ephemeral=True) - - # Bestätigung anfordern - confirm_embed = discord.Embed( - title=f"{emoji_warn} Bestätigung erforderlich", - color=ERROR_COLOR, - description=f"Möchtest du wirklich **{warn_count}** Warnungen von {user.mention} löschen?\n\n**Grund:** {reason}" - ) - confirm_embed.set_footer(text="Diese Aktion kann nicht rückgängig gemacht werden! × Powered by ManagerX") - - view = ClearWarningsConfirmView(self, user, ctx.author, reason, warn_count) - await ctx.respond(embed=confirm_embed, view=view, ephemeral=True) - - except Exception as e: - embed = self._create_error_embed( - "Unerwarteter Fehler", - f"Fehler beim Vorbereiten der Löschung: {str(e)}" - ) - await ctx.respond(embed=embed, ephemeral=True) - - async def clear_all_user_warnings(self, guild_id: int, user_id: int) -> int: - """Löscht alle Warnungen eines Users und gibt die Anzahl zurück""" - try: - # Alle Warn-IDs für den User holen - warnings = self.db.get_warnings(guild_id, user_id) - deleted_count = 0 - - for warn_id, _, _ in warnings: - if self.db.delete_warning(warn_id): - deleted_count += 1 - - # Cache invalidieren - cache_key = f"{guild_id}_{user_id}" - if cache_key in self._warn_cache: - del self._warn_cache[cache_key] - - return deleted_count - - except Exception as e: - print(f"Fehler beim Löschen aller Warnungen: {e}") - return 0 - - -class WarningsView(discord.ui.View): - """View für die Navigation durch paginierte Warnungen""" - - def __init__(self, cog, target_user: discord.Member, warnings: list, current_page: int, total_pages: int): - super().__init__(timeout=300) # 5 Minuten Timeout - self.cog = cog - self.target_user = target_user - self.warnings = warnings - self.current_page = current_page - self.total_pages = total_pages - - # Buttons aktivieren/deaktivieren - self.previous_button.disabled = current_page == 0 - self.next_button.disabled = current_page >= total_pages - 1 - - @discord.ui.button(label="◀ Vorherige", style=discord.ButtonStyle.secondary, disabled=True) - async def previous_button(self, button: discord.ui.Button, interaction: discord.Interaction): - if self.current_page > 0: - self.current_page -= 1 - await self._update_page(interaction) - - @discord.ui.button(label="Nächste ▶", style=discord.ButtonStyle.secondary) - async def next_button(self, button: discord.ui.Button, interaction: discord.Interaction): - if self.current_page < self.total_pages - 1: - self.current_page += 1 - await self._update_page(interaction) - - async def _update_page(self, interaction: discord.Interaction): - """Aktualisiert die angezeigte Seite""" - warnings_per_page = 10 - start_idx = self.current_page * warnings_per_page - end_idx = min(start_idx + warnings_per_page, len(self.warnings)) - page_warnings = self.warnings[start_idx:end_idx] - - warn_list = "\n".join([ - f"**ID `{warn_id}`** | {timestamp}\n└ **Grund:** {reason[:100]}{'...' if len(reason) > 100 else ''}" - for warn_id, reason, timestamp in page_warnings - ]) - - embed = discord.Embed( - title=f"{emoji_warn} Verwarnungen für {self.target_user.display_name}", - color=ERROR_COLOR, - description=warn_list - ) - embed.set_author(name=AUTHOR) - embed.add_field(name=f"{emoji_member} User", value=self.target_user.mention, inline=True) - embed.add_field(name=f"{emoji_summary} Anzahl Verwarnungen", value=str(len(self.warnings)), inline=True) - embed.set_footer(text=f"Seite {self.current_page + 1}/{self.total_pages} • {FLOOTER}") - - # Buttons aktualisieren - self.previous_button.disabled = self.current_page == 0 - self.next_button.disabled = self.current_page >= self.total_pages - 1 - - await interaction.response.edit_message(embed=embed, view=self) - - async def on_timeout(self): - """Deaktiviert alle Buttons nach Timeout""" - for item in self.children: - item.disabled = True - - -class ClearWarningsConfirmView(discord.ui.View): - """View für die Bestätigung beim Löschen aller Warnungen""" - - def __init__(self, cog, target_user: discord.Member, moderator: discord.Member, reason: str, warn_count: int): - super().__init__(timeout=60) # 1 Minute Timeout - self.cog = cog - self.target_user = target_user - self.moderator = moderator - self.reason = reason - self.warn_count = warn_count - - @discord.ui.button(label="✅ Bestätigen", style=discord.ButtonStyle.danger) - async def confirm_button(self, button: discord.ui.Button, interaction: discord.Interaction): - # Überprüfen ob der richtige User geantwortet hat - if interaction.user.id != self.moderator.id: - await interaction.response.send_message( - "❌ Nur der ursprüngliche Moderator kann diese Aktion bestätigen.", - ephemeral=True - ) - return - - try: - # Alle Warnungen löschen - deleted_count = await self.cog.clear_all_user_warnings( - interaction.guild.id, self.target_user.id - ) - - if deleted_count > 0: - success_embed = discord.Embed( - title=f"{emoji_yes} Warnungen gelöscht", - color=SUCCESS_COLOR, - description=f"**{deleted_count}** Warnungen von {self.target_user.mention} wurden gelöscht." - ) - success_embed.add_field(name="Grund", value=self.reason, inline=False) - success_embed.add_field(name="Moderator", value=self.moderator.mention, inline=True) - success_embed.set_footer(text=FLOOTER) - else: - success_embed = discord.Embed( - title=f"{emoji_no} Keine Warnungen gelöscht", - color=ERROR_COLOR, - description="Es konnten keine Warnungen gelöscht werden." - ) - - # View deaktivieren - for item in self.children: - item.disabled = True - - await interaction.response.edit_message(embed=success_embed, view=self) - - except Exception as e: - error_embed = discord.Embed( - title=ERROR_TITLE, - color=ERROR_COLOR, - description=f"Fehler beim Löschen: {str(e)}" - ) - await interaction.response.edit_message(embed=error_embed, view=None) - - @discord.ui.button(label="❌ Abbrechen", style=discord.ButtonStyle.secondary) - async def cancel_button(self, button: discord.ui.Button, interaction: discord.Interaction): - # Überprüfen ob der richtige User geantwortet hat - if interaction.user.id != self.moderator.id: - await interaction.response.send_message( - "❌ Nur der ursprüngliche Moderator kann diese Aktion abbrechen.", - ephemeral=True - ) - return - - cancel_embed = discord.Embed( - title=f"{emoji_yes} Abgebrochen", - color=SUCCESS_COLOR, - description="Das Löschen der Warnungen wurde abgebrochen." - ) - - # View deaktivieren - for item in self.children: - item.disabled = True - - await interaction.response.edit_message(embed=cancel_embed, view=self) - - async def on_timeout(self): - """Deaktiviert alle Buttons nach Timeout""" - for item in self.children: - item.disabled = True - - -def setup(bot): - bot.add_cog(WarnSystem(bot)) \ No newline at end of file diff --git a/src/cogs/setlang.py b/src/cogs/setlang.py deleted file mode 100644 index 37bf622..0000000 --- a/src/cogs/setlang.py +++ /dev/null @@ -1,56 +0,0 @@ -import discord -from discord.ext import commands -import ezcord - -from handler import TranslationHandler - - -class SetLangCog(ezcord.Cog): - """Cog for setting user language preferences.""" - - AVAILABLE_LANGUAGES = { - "de": "Deutsch 🇩🇪", - "en": "English 🇬🇧" - } - - @commands.slash_command( - name="set-lang", - description="Set your preferred language for bot messages." - ) - @discord.option( - "language", - description="Choose a language", - choices=[ - discord.OptionChoice(name=name, value=code) - for code, name in AVAILABLE_LANGUAGES.items() - ], - required=True - ) - async def set_language(self, ctx: discord.ApplicationContext, language: str): - """ - Set the user's preferred language. - - Args: - ctx: Discord application context - language: Selected language code - """ - # Save language preference - self.bot.settings_db.set_user_language(ctx.author.id, language) - - # Get display name for the selected language - lang_name = self.AVAILABLE_LANGUAGES.get(language, language) - - # Load response message using TranslationHandler - response_text = await TranslationHandler.get_async( - language, - "cog_setlang.message.language_set", - default="Language has been set to {language}.", - language=lang_name - ) - - await ctx.respond(response_text, ephemeral=True) - - -def setup(bot): - """Setup function to add the cog to the bot.""" - bot.add_cog(SetLangCog(bot)) \ No newline at end of file diff --git a/src/managerx/cli.py b/src/managerx/cli.py deleted file mode 100644 index 5cd9696..0000000 --- a/src/managerx/cli.py +++ /dev/null @@ -1,45 +0,0 @@ -# src/managerx/cli.py -import click -import os - -FOLDER_STRUCTURE = [ - "src/handler", - "src/DevTools/backend/utils", - "src/DevTools/backend/database", - "src/DevTools/backend/config", - "src/cogs/fun/wikipedia", - "src/cogs/information", - "src/cogs/moderation", - "src/cogs/servermanagement", - "src/managerx", -] - -@click.group() -def managerx(): - """ManagerX CLI Tool""" - pass - -@managerx.command() -def create(): - """Erstellt automatisch die komplette ManagerX-Ordnerstruktur""" - root_folder = "ManagerX" - root_path = os.path.join(os.getcwd(), root_folder) - - if os.path.exists(root_path): - click.echo(f"Ordner '{root_folder}' existiert bereits. Bitte löschen oder umbenennen!") - return - - for path in FOLDER_STRUCTURE: - full_path = os.path.join(root_path, path) - os.makedirs(full_path, exist_ok=True) - - # Automatisch __init__.py erstellen, damit Python-Pakete erkannt werden - if path.endswith("managerx") or "DevTools" in path or "cogs" in path: - init_file = os.path.join(full_path, "__init__.py") - with open(init_file, "w") as f: - f.write("# Init for package\n") - - click.echo(f"ManagerX-Ordnerstruktur wurde erfolgreich in '{root_folder}' erstellt!") - -if __name__ == "__main__": - managerx() diff --git a/translation/ez_de.json b/translation/ez_de.json deleted file mode 100644 index b4f48bb..0000000 --- a/translation/ez_de.json +++ /dev/null @@ -1,32 +0,0 @@ - -{ - "times": { - "min": "Minute", - "sec": "Sekunde", - "hour": "Stunde", - "day": "Tag" - }, - "bot": { - "error_title": "⛔ ⨯ Error", - "error": "Ein **unbekannter Fehler** ist aufgetreten. {}\nIch habe meinen Entwickler über dieses Problem informiert.", - "cooldown_title": "⌛ ⨯ Cooldown", - "cooldown": "Versuche es {} erneut.", - "no_perms_title": "\uD83D\uDEA8 ⨯ Fehlende Rechte", - "no_perms": "Mir fehlen die folgenden Berechtigungen, um diesen Befehl auszuführen.", - "no_user_perms": "Du hast keine Rechte, um diesen Befehl auszuführen." - }, - "help": { - "cmd_name": "help", - "cmd_description": "\uD83D\uDD25 Zeigt eine Liste aller Befehle an.", - "wrong_user": "Dieser Help Command gehört dir nicht!", - "placeholder": "\uD83D\uDD30 › Wähle eine Kategorie", - "embed_title": "Meine Befehle", - "default_description": "Alle Befehle der Kategorie **{}**.", - "no_commands": "Ups, ich konnte keine Befehle finden, auf die du Zugriff hast." - }, - "blacklist": { - "admin_group": "admin", - "no_perms": "Du wurdest von der Nutzung dieses Bots ausgeschlossen. Wenn du denkst, dass dies ein Fehler ist, kontaktiere meinen Owner.", - "guild_error": "Ich habe deinen Server **{}** verlassen, da du keine Rechte hast, um mich zu nutzen. Wenn du denkst, dass dies ein Fehler ist, kontaktiere meinen Owner." - } -} \ No newline at end of file diff --git a/translation/ez_en.json b/translation/ez_en.json deleted file mode 100644 index f62542d..0000000 --- a/translation/ez_en.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "times": { - "min": "minute", - "sec": "second", - "hour": "hour", - "day": "day" - }, - "bot": { - "error_title": "⛔ ⨯ Error", - "error": "An **unexpected error** has occurred. {}\nI have informed my developer about this issue.", - "cooldown_title": "⌛ ⨯ Cooldown", - "cooldown": "Try again {}.", - "no_perms_title": "\uD83D\uDEA8 ⨯ Missing permissions", - "no_perms": "I'm missing the following permissions to execute this command.", - "no_user_perms": "You do not have permission to run this command." - }, - "help": { - "cmd_name": "help", - "cmd_description": "\uD83D\uDD25 Displays a list of all commands.", - "wrong_user": "This help command does not belong to you!", - "placeholder": "\uD83D\uDD30 › Choose a category", - "embed_title": "My commands", - "default_description": "All commands of category **{}**.", - "no_commands": "Oops, I couldn't find any commands that you have access to." - }, - "blacklist": { - "admin_group": "admin", - "no_perms": "You have been banned from using this bot. If you think this is a mistake, please contact my owner.", - "guild_error": "I left your server **{}** because you don't have permissions to use me. If you think this is a mistake, please contact my owner." - } -} \ No newline at end of file diff --git a/translation/messages/de.yaml b/translation/messages/de.yaml deleted file mode 100644 index f598cf1..0000000 --- a/translation/messages/de.yaml +++ /dev/null @@ -1,116 +0,0 @@ -general: - - error_types: - no_permission: "Du hast keine Berechtigung, diesen Befehl auszuführen." - user_not_found: "Benutzer nicht gefunden." - bot_error: "Ein Fehler ist im Bot aufgetreten. Bitte versuche es später erneut." - option_missing: "Eine erforderliche Option fehlt." - bot_has_no_permission: "Der Bot hat nicht die erforderlichen Berechtigungen, um diesen Befehl auszuführen." - - -cog_setlang: - error_types: - unsupported_language: "Die angegebene Sprache wird nicht unterstützt." - same_language: "Die angegebene Sprache ist bereits eingestellt." - - message: - language_set: "Die Sprache wurde auf {language} gesetzt." -######################################################### -# Fun Cogs -######################################################### - -cog_4gewinnt: - error_types: - not_your_turn: "Es ist nicht dein Zug." - this_column_full: "Diese Spalte ist voll!" - is_opponent_bot: "Du kannst nicht gegen einen Bot spielen!" - is_opponent_self: "Du kannst nicht gegen dich selbst spielen!" - - - - win_types: - win: "Spiel vorbei! {winner} hat gewonnen!\n\n{board_str}" - draw: "Unentschieden!\n\n{board_str}" - - message: - start_game: "4 Gewinnt: {author_mention} (🔴) vs {opponent_mention} (🟡)\n{author_mention} fängt an!\n\n" - player_turn: "{view.current_player.mention} ist jetzt dran!\n\n{board_str}" - - - -cog_tictactoe: - error_types: - not_your_turn: "Es ist nicht dein Zug." - this_cell_taken: "Dieses Feld ist bereits belegt!" - is_opponent_bot: "Du kannst nicht gegen einen Bot spielen!" - is_opponent_self: "Du kannst nicht gegen dich selbst spielen!" - - - - win_types: - win: "Spiel vorbei! {winner} hat gewonnen!" - draw: "Unentschieden!" - - message: - start_game: "Tic Tac Toe: {author_mention} (X) gegen {opponent_mention} (O)\n{author_mention} fängt an!" - -cog_weather: - error_types: - city_not_found: "⚠️ Stadt nicht gefunden." - api_error: "❌ Fehler bei der Wetter-API." - - messages: - weather_report: "# 🌤️ Wetterbericht für {city}, {country}:\n\n" - temperature: "**🌡️ Temperatur:** {temperature}°C\n" - humidity: "**💧 Luftfeuchtigkeit:** {humidity}%\n" - wind_speed: "**💨 Windgeschwindigkeit:** {wind_speed} km/h ({wind_dir})\n" - condition: "**☁️ Wetterbedingungen:** {condition}\n" - visibility: "**🌫️ Sichtweite:** {visibility} km\n" - pressure: "**🧭 Luftdruck:** {pressure} hPa\n" - -cog_autorole: - error_types: - role_to_high: - title: "❌ Fehler" - desc: "Ich kann diese Rolle nicht vergeben, da sie höher oder gleich meiner höchsten Rolle ist!" - role_managed: - title: "❌ Fehler" - desc: "Diese Rolle wird von einer Integration verwaltet und kann nicht als Autorole hinzugefügt werden!" - not_found: - title: "❌ Autorole nicht gefunden" - desc: "Es existiert keine Autorole mit der ID `{autorole_id}`!" - wrong_guild: - title: "❌ Fehler" - desc: "Diese Autorole gehört nicht zu diesem Server!" - no_roles: - title: "❌ Keine Autoroles" - desc: "Es sind keine Autoroles für diesen Server eingerichtet!" - role_deleted: - title: "⚠️ Rolle nicht gefunden" - desc: "Die konfigurierte Rolle für `{autorole_id}` existiert nicht mehr!" - - messages: - add_success: - title: "✅ Autorole hinzugefügt" - desc: "Neue Mitglieder erhalten automatisch die Rolle {role}\n\n**Autorole-ID:** `{autorole_id}`" - remove_success: - title: "✅ Autorole entfernt" - desc: "Die Autorole `{autorole_id}` wurde erfolgreich entfernt!" - toggle_success: - enabled_title: "✅ Autorole aktiviert" - enabled_desc: "Die Autorole `{autorole_id}` wurde aktiviert!" - disabled_title: "✅ Autorole deaktiviert" - disabled_desc: "Die Autorole `{autorole_id}` wurde deaktiviert!" - list: - title: "📋 Autoroles Liste" - desc: "Alle Autoroles für **{guild_name}**" - role_deleted: "⚠️ **Rolle gelöscht**" - info: - title: "ℹ️ Autorole Information" - desc: "Details zur Autorole `{autorole_id}`" - - system: - audit_reason: "Autorole System" - console_log: "✅ Autoroles [{role_names}] wurden {member_name} zugewiesen" - error_forbidden: "❌ Keine Berechtigung, um Rollen zu vergeben" - error_http: "❌ Fehler beim Zuweisen der Rollen: {error}" \ No newline at end of file diff --git a/translation/messages/en.yaml b/translation/messages/en.yaml deleted file mode 100644 index 391ff35..0000000 --- a/translation/messages/en.yaml +++ /dev/null @@ -1,114 +0,0 @@ -general: - error_types: - no_permission: "You do not have permission to execute this command." - user_not_found: "User not found." - bot_error: "An error occurred within the bot. Please try again later." - option_missing: "A required option is missing." - bot_has_no_permission: "The bot does not have the necessary permissions to execute this command." - -cog_setlang: - error_types: - unsupported_language: "The specified language is not supported." - same_language: "The specified language is already set." - - message: - language_set: "Language has been set to {language}." -################################################################### -# -# Cog Folder: cogs/fun -# -#################################################################### -cog_4gewinnt: - error_types: - not_your_turn: "It's not your turn." - this_column_full: "This column is full!" - is_opponent_bot: "You cannot play against a bot!" - is_opponent_self: "You cannot play against yourself!" - - win_types: - win: "Game over! {winner} won!\n\n{board_str}" - draw: "It's a draw!\n\n{board_str}" - - message: - start_game: "Connect Four: {author_mention} (🔴) vs {opponent_mention} (🟡)\n{author_mention} starts!\n\n" - player_turn: "{view.current_player.mention}'s turn now!\n\n{board_str}" - -cog_tictactoe: - error_types: - not_your_turn: "It's not your turn." - this_cell_taken: "This cell is already taken!" - is_opponent_bot: "You cannot play against a bot!" - is_opponent_self: "You cannot play against yourself!" - - win_types: - win: "Game over! {winner} won!" - draw: "It's a draw!" - - message: - # NEU: Nur einfache Schlüssel verwenden - start_game: "Tic Tac Toe: {author_mention} (X) vs {opponent_mention} (O)\n{author_mention} starts!" - -cog_weather: - error_types: - city_not_found: "⚠️ City not found." - api_error: "❌ Error with the weather API." - - messages: - weather_report: "# 🌤️ Weather report for {city}, {country}:\n\n" - temperature: "**🌡️ Temperature:** {temperature}°C\n" - humidity: "**💧 Humidity:** {humidity}%\n" - wind_speed: "**💨 Wind:** {wind_speed} km/h ({wind_dir})\n" - condition: "**☁️ Condition:** {condition}\n" - visibility: "**🌫️ Visibility:** {visibility} km\n" - pressure: "**🧭 Pressure:** {pressure} hPa\n" - -######################################################### -# Server Management -######################################################### - -cog_autorole: - error_types: - role_to_high: - title: "❌ Error" - desc: "I cannot assign this role because it is higher than or equal to my highest role!" - role_managed: - title: "❌ Error" - desc: "This role is managed by an integration and cannot be added as an autorole!" - not_found: - title: "❌ Autorole Not Found" - desc: "There is no autorole with the ID `{autorole_id}`!" - wrong_guild: - title: "❌ Error" - desc: "This autorole does not belong to this server!" - no_roles: - title: "❌ No Autoroles" - desc: "No autoroles have been set up for this server!" - role_deleted: - title: "⚠️ Role Not Found" - desc: "The configured role for `{autorole_id}` no longer exists!" - - messages: - add_success: - title: "✅ Autorole Added" - desc: "New members will now automatically receive the role {role}\n\n**Autorole-ID:** `{autorole_id}`" - remove_success: - title: "✅ Autorole Removed" - desc: "The autorole `{autorole_id}` was successfully removed!" - toggle_success: - enabled_title: "✅ Autorole Enabled" - enabled_desc: "The autorole `{autorole_id}` has been enabled!" - disabled_title: "✅ Autorole Disabled" - disabled_desc: "The autorole `{autorole_id}` has been disabled!" - list: - title: "📋 Autoroles List" - desc: "All autoroles for **{guild_name}**" - role_deleted: "⚠️ **Role deleted**" - info: - title: "ℹ️ Autorole Information" - desc: "Details for autorole `{autorole_id}`" - - system: - audit_reason: "Autorole System" - console_log: "✅ Autoroles [{role_names}] were assigned to {member_name}" - error_forbidden: "❌ Missing permissions to assign roles" - error_http: "❌ Error while assigning roles: {error}" \ No newline at end of file