diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
index bee0d8b..c1867a5 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/static.yml
@@ -35,7 +35,11 @@ jobs:
run: npm install --legacy-peer-deps
- name: Build project
- run: npm run build # WICHTIG: Erstellt den fertigen 'dist' Ordner
+ run: npm run build
+ env:
+ VITE_API_URL: ${{ secrets.VITE_API_URL }}
+ VITE_DISCORD_CLIENT_ID: ${{ secrets.VITE_DISCORD_CLIENT_ID }}
+ VITE_DISCORD_REDIRECT_URI: ${{ secrets.VITE_DISCORD_REDIRECT_URI }}
- name: Setup Pages
uses: actions/configure-pages@v5
diff --git a/README.md b/README.md
index 8e8e4e4..56c2f57 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,10 @@
**Entwickelt von** [**ManagerX Development**](https://github.com/ManagerX-Development) **|** ⚡ **Powered by OPPRO.NET Network™**
+
+Webseite: https://managerx-bot.de
+Dokumentation: https://docs.managerx-bot.de
+API: https://api.managerx-bot.de
diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css index 4b4c030..1aea4b3 100644 --- a/docs/source/_static/custom.css +++ b/docs/source/_static/custom.css @@ -3,8 +3,6 @@ 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; @@ -13,7 +11,7 @@ --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; @@ -25,37 +23,37 @@ --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; @@ -107,7 +105,12 @@ body { } /* Improved Typography Hierarchy */ -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { font-family: var(--pst-font-family-heading); font-weight: 700; letter-spacing: -0.025em; @@ -255,8 +258,8 @@ h2::before { } /* Active Navigation Item */ -.bd-sidebar-primary .nav-item.current > a, -.bd-sidebar-primary .nav-item.active > a { +.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; @@ -264,7 +267,7 @@ h2::before { padding-left: calc(1rem - 3px); } -[data-theme="dark"] .bd-sidebar-primary .nav-item.current > a { +[data-theme="dark"] .bd-sidebar-primary .nav-item.current>a { background: linear-gradient(90deg, rgba(220, 38, 38, 0.15) 0%, transparent 100%); } @@ -301,10 +304,13 @@ article p { } @keyframes highlight-pulse { - 0%, 50% { + + 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; @@ -861,11 +867,9 @@ dt.sig { .mx-hero { text-align: center; padding: 5rem 2rem; - background: radial-gradient( - circle at center, - var(--mx-red-glow) 0%, - transparent 70% - ); + background: radial-gradient(circle at center, + var(--mx-red-glow) 0%, + transparent 70%); border-radius: var(--mx-radius-xl); margin: 3rem 0; } @@ -950,19 +954,19 @@ dt.sig { 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; } @@ -972,19 +976,19 @@ dt.sig { 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; } @@ -995,6 +999,7 @@ dt.sig { ========================================================================== */ @media print { + .bd-header, .bd-sidebar-primary, .bd-sidebar-secondary, @@ -1002,11 +1007,11 @@ dt.sig { .prev-next-area { display: none !important; } - + article { max-width: 100% !important; } - + .admonition { page-break-inside: avoid; } @@ -1044,6 +1049,7 @@ input:focus-visible { /* Reduced Motion */ @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { diff --git a/main.py b/main.py index dd2dc30..f45bafb 100644 --- a/main.py +++ b/main.py @@ -25,6 +25,12 @@ # Logger (muss existieren!) from logger import logger +# ============================================================================= +# SETUP +# ============================================================================= +BASEDIR = Path(__file__).resolve().parent +load_dotenv(dotenv_path=BASEDIR / 'config' / '.env') + # Lokale Module aus src/bot/core from src.bot.core.config import ConfigLoader, BotConfig from src.bot.core.bot_setup import BotSetup @@ -37,11 +43,6 @@ from src.api.dashboard.routes import set_bot_instance, router as dashboard_router from mx_handler import TranslationHandler -# ============================================================================= -# SETUP -# ============================================================================= -BASEDIR = Path(__file__).resolve().parent -load_dotenv(dotenv_path=BASEDIR / 'config' / '.env') colorama_init(autoreset=True) TranslationHandler.settings( @@ -104,7 +105,7 @@ async def start_webserver(): bot = bot_setup.create_bot() # Speichere Bot Start-Zeit für Uptime-Berechnung - bot.start_time = datetime.utcnow() + bot.start_time = discord.utils.utcnow() # Übergebe Bot-Instanz an die API-Routes set_bot_instance(bot) diff --git a/package-lock.json b/package-lock.json index f32aa49..10b41fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7463,4 +7463,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index fdebf70..56be7e0 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "react-hook-form": "7.71.2", "react-resizable-panels": "4.7.2", "react-router-dom": "7.13.1", - "recharts": "3.8.0", "sonner": "2.0.7", "tailwind-merge": "3.5.0", "tailwindcss-animate": "1.0.7", @@ -88,4 +87,4 @@ "vite": "8.0.0", "vitest": "4.1.0" } -} +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4cf3e08..2a2109e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ManagerX" -version = "2.0.0+build29cbfd2" +version = "2.0.0+build9b58708" description = "A powerful Discord bot for server management and fun." readme = "README.md" requires-python = ">=3.8" diff --git a/requirements/req.txt b/requirements/req.txt index b1455aa..831314b 100644 --- a/requirements/req.txt +++ b/requirements/req.txt @@ -82,4 +82,4 @@ jaraco.classes==3.4.0 jaraco.context==6.1.1 jaraco.functools==4.4.0 docutils==0.22.4 -nh3==0.3.2Ne \ No newline at end of file +nh3==0.3.2 \ No newline at end of file diff --git a/src/api/dashboard/auth_routes.py b/src/api/dashboard/auth_routes.py new file mode 100644 index 0000000..e5dec6a --- /dev/null +++ b/src/api/dashboard/auth_routes.py @@ -0,0 +1,169 @@ +from fastapi import APIRouter, Request, HTTPException, Security, status, Depends +from fastapi.responses import RedirectResponse +import httpx +import jwt +import os +import time +from urllib.parse import urlencode + +router = APIRouter( + prefix="/auth", + tags=["auth"] +) + +# JWT Setup +JWT_SECRET = os.getenv("JWT_SECRET", "fallback-secret") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days + +# Discord OAuth Setup +CLIENT_ID = os.getenv("DISCORD_CLIENT_ID") +CLIENT_SECRET = os.getenv("DISCORD_CLIENT_SECRET") +REDIRECT_URI = os.getenv("DISCORD_REDIRECT_URI", "http://localhost:8080/dash/auth/callback") +DASHBOARD_URL = os.getenv("DASHBOARD_URL", "http://localhost:8080") + +# We import bot_instance dynamically or keep a local ref if passed +# Removed top level import to prevent circular import + +def create_access_token(data: dict): + to_encode = data.copy() + expire = time.time() + (ACCESS_TOKEN_EXPIRE_MINUTES * 60) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, JWT_SECRET, algorithm=ALGORITHM) + return encoded_jwt + +def get_current_user(request: Request): + """Dependency to get the current user from the Authorization header.""" + auth_header = request.headers.get("Authorization") + if not auth_header or not auth_header.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Not authenticated") + + token = auth_header.split(" ")[1] + + try: + payload = jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM]) + user_id: str = payload.get("sub") + if user_id is None: + raise HTTPException(status_code=401, detail="Invalid token") + return {"id": user_id, "username": payload.get("username", ""), "avatar": payload.get("avatar", "")} + except jwt.ExpiredSignatureError: + raise HTTPException(status_code=401, detail="Token has expired") + except jwt.InvalidTokenError: + raise HTTPException(status_code=401, detail="Invalid token") + +@router.get("/login") +async def login(): + """Generates the Discord OAuth2 Authorization URL and redirects the user.""" + # We want to respond to the dashboard frontend, passing the code back to the frontend. + params = { + "client_id": CLIENT_ID, + "redirect_uri": REDIRECT_URI, + "response_type": "code", + "scope": "identify guilds", + "prompt": "consent" + } + url = f"https://discord.com/oauth2/authorize?{urlencode(params)}" + print(f"[DEBUG] Generated Discord URL: {url}") + return {"url": url} + +@router.post("/callback") +async def callback(request: Request): + """Exchanges code for a token and creates a JWT session.""" + data = await request.json() + code = data.get("code") + + if not code: + raise HTTPException(status_code=400, detail="No code provided") + + # Exchange code for token + async with httpx.AsyncClient() as client: + token_data = { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": REDIRECT_URI + } + + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + + try: + token_res = await client.post("https://discord.com/api/oauth2/token", data=token_data, headers=headers) + token_res.raise_for_status() + token_json = token_res.json() + access_token = token_json.get("access_token") + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to exchange token: {e}") + + # Get user info + user_res = await client.get("https://discord.com/api/users/@me", headers={ + "Authorization": f"Bearer {access_token}" + }) + user_json = user_res.json() + user_id = user_json.get("id") + + # Verify if user has admin permissions on any guild bot is in (we handle actual guilds in /me) + # For now, just generate JWT + jwt_token = create_access_token({ + "sub": user_id, + "username": user_json.get("username"), + "avatar": user_json.get("avatar") + }) + + return { + "access_token": jwt_token, + "token_type": "bearer", + "user": { + "id": str(user_id), + "username": user_json.get("username"), + "avatar": user_json.get("avatar") + }, + "discord_token": access_token # Send discord token to frontend to fetch guilds + } + +@router.get("/me") +async def get_me(request: Request, user: dict = Depends(get_current_user)): + """Returns the user along with guilds they manage that the bot is also in.""" + from src.api.dashboard.routes import bot_instance + + auth_header = request.headers.get("Authorization") + if not auth_header: + raise HTTPException(status_code=401) + + # In a real app, we'd store the Discord Access Token in a session or database. + # For now, let's assume the client might send it or we fetch it if we had it. + # To make this "really work" without a DB yet, we expect a 'X-Discord-Token' header + # or just use the one from the callback if we were to store it. + + discord_token = request.headers.get("X-Discord-Token") + user_guilds = [] + + if discord_token: + async with httpx.AsyncClient() as client: + guilds_res = await client.get("https://discord.com/api/users/@me/guilds", headers={ + "Authorization": f"Bearer {discord_token}" + }) + if guilds_res.status_code == 200: + all_guilds = guilds_res.json() + for g in all_guilds: + # check permissions (Manage Guild = 0x20) + perms = int(g.get("permissions", 0)) + is_admin = (perms & 0x20) == 0x20 or (perms & 0x8) == 0x8 + + if is_admin: + # Check if bot is in guild + guild_id = int(g.get("id")) + if bot_instance and bot_instance.get_guild(guild_id): + user_guilds.append({ + "id": str(guild_id), + "name": g.get("name"), + "icon": g.get("icon"), + "permissions": perms + }) + + return { + "user": user, + "guilds": user_guilds + } diff --git a/src/api/dashboard/routes.py b/src/api/dashboard/routes.py index a4f3b5a..bdfd4b0 100644 --- a/src/api/dashboard/routes.py +++ b/src/api/dashboard/routes.py @@ -1,11 +1,17 @@ -from fastapi import APIRouter, Request, HTTPException, Security, status +from fastapi import APIRouter, Request, HTTPException, Security, status, Depends from fastapi.security import APIKeyHeader import os +import discord +from src.api.dashboard.auth_routes import get_current_user from typing import List, Optional -from datetime import datetime +from datetime import datetime, timedelta import time # Falls du Schemas nutzt: from .schemas import ServerStatus, UserInfo +from .auth_routes import router as auth_router +from .settings_routes import router as settings_router +from .user_routes import router as user_router + # Wir erstellen einen Router, den wir später in die Haupt-App einbinden router_public = APIRouter( prefix="/v1/managerx", @@ -93,8 +99,194 @@ async def get_api_key(api_key_header: str = Security(API_KEY_HEADER)): router = APIRouter( prefix="/dashboard", - tags=["dashboard"], - dependencies=[Security(get_api_key)] + tags=["dashboard"] ) + +# Public sub-routers (no global X-API-KEY required, they manage their own like JWT) +@router.get("/guilds/{guild_id}/channels") +async def get_guild_channels(guild_id: int, user: dict = Depends(get_current_user)): + """Fetches text channels for a specific guild.""" + if bot_instance is None: + raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found or bot not in guild") + + # Check if user is in guild and has appropriate permissions (Manage Guild or Admin) + member = guild.get_member(int(user["id"])) + if not member or not (member.guild_permissions.manage_guild or member.guild_permissions.administrator): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + channels = [ + {"id": str(c.id), "name": c.name} + for c in guild.text_channels + ] + return {"channels": channels} + +@router.get("/guilds/{guild_id}/roles") +async def get_guild_roles(guild_id: int, user: dict = Depends(get_current_user)): + """Fetches manageable roles for a specific guild.""" + if bot_instance is None: + raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found or bot not in guild") + + member = guild.get_member(int(user["id"])) + if not member or not (member.guild_permissions.manage_guild or member.guild_permissions.administrator): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + roles = [ + {"id": str(r.id), "name": r.name, "color": str(r.color)} + for r in guild.roles if not r.is_default() and not r.managed + ] + return {"roles": roles} + +@router.get("/guilds/{guild_id}/categories") +async def get_guild_categories(guild_id: int, user: dict = Depends(get_current_user)): + """Fetches categories for a specific guild.""" + if bot_instance is None: + raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found or bot not in guild") + + member = guild.get_member(int(user["id"])) + if not member or not (member.guild_permissions.manage_guild or member.guild_permissions.administrator): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + categories = [ + {"id": str(c.id), "name": c.name} + for c in guild.categories + ] + return {"categories": categories} + +@router.get("/guilds/{guild_id}/voice_channels") +async def get_guild_voice_channels(guild_id: int, user: dict = Depends(get_current_user)): + """Fetches voice channels for a specific guild.""" + if bot_instance is None: + raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found or bot not in guild") + + member = guild.get_member(int(user["id"])) + if not member or not (member.guild_permissions.manage_guild or member.guild_permissions.administrator): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + channels = [ + {"id": str(c.id), "name": c.name} + for c in guild.voice_channels + ] + return {"channels": channels} + +@router.get("/guilds/{guild_id}/stats") +async def get_guild_stats(guild_id: int, user: dict = Depends(get_current_user)): + """Fetches server statistics (Daily joins, message count, member total).""" + if bot_instance is None: + raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found or bot not in guild") + + member = guild.get_member(int(user["id"])) + if not member or not (member.guild_permissions.manage_guild or member.guild_permissions.administrator): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + # Fetch daily growth/activity + today_dt = discord.utils.utcnow() + today_str = today_dt.strftime('%Y-%m-%d') + yesterday_str = (today_dt - timedelta(days=1)).strftime('%Y-%m-%d') + joined_today = 0 + joined_yesterday = 0 + messages_today = 0 + messages_yesterday = 0 + history = [] + + try: + # Pre-fetch histories + welcome_history = [] + stats_history = [] + + if hasattr(bot_instance, 'welcome_db'): + welcome_history = await bot_instance.welcome_db.get_weekly_stats(guild_id) + for day in welcome_history: + if day['date'] == today_str: + joined_today = day['joins'] + elif day['date'] == yesterday_str: + joined_yesterday = day['joins'] + + if hasattr(bot_instance, 'stats_db'): + messages_today = await bot_instance.stats_db.get_daily_messages(guild_id, today_str) + messages_yesterday = await bot_instance.stats_db.get_daily_messages(guild_id, yesterday_str) + stats_history = await bot_instance.stats_db.get_weekly_stats(guild_id) + + # 2. Combine history for the last 7 days + for i in range(6, -1, -1): + date_obj = today_dt - timedelta(days=i) + d_str = date_obj.strftime('%Y-%m-%d') + day_name = date_obj.strftime('%a') + m_count = 0 + j_count = 0 + for h in stats_history: + if h['date'] == d_str: + m_count = h['messages']; break + for h in welcome_history: + if h['date'] == d_str: + j_count = h['joins']; break + history.append({"name": day_name, "messages": m_count, "joins": j_count}) + + # Calculate Trends + def calc_trend(today, yesterday): + if today == yesterday: + return "neutral", "0%" + if yesterday == 0: + return "up", "+100%" + diff = today - yesterday + pct = round((abs(diff) / yesterday) * 100) + return ("up" if diff > 0 else "down"), f"{'+' if diff > 0 else '-'}{pct}%" + + m_trend, m_trend_val = calc_trend(messages_today, messages_yesterday) + j_trend, j_trend_val = calc_trend(joined_today, joined_yesterday) + + # Prepare final stats object + total_members = guild.member_count or len(guild.members) + online_members = 0 + if intents_working := guild.members: + online_members = sum(1 for m in guild.members if m.status != discord.Status.offline) + + stats = { + "total_members": total_members, + "online_members": online_members, + "text_channels": len(guild.text_channels), + "voice_channels": len(guild.voice_channels), + "joined_today": joined_today, + "joined_trend": j_trend, + "joined_trend_value": j_trend_val, + "messages_today": messages_today, + "messages_trend": m_trend, + "messages_trend_value": m_trend_val, + "history": history + } + return stats + except Exception as e: + print(f"Stats error: {e}") + return { + "total_members": guild.member_count, + "online_members": 0, + "text_channels": len(guild.text_channels), + "voice_channels": len(guild.voice_channels), + "joined_today": 0, + "messages_today": 0 + } + +router.include_router(auth_router) +router.include_router(settings_router) +router.include_router(user_router) router.include_router(router_public) diff --git a/src/api/dashboard/settings_routes.py b/src/api/dashboard/settings_routes.py new file mode 100644 index 0000000..c7a086c --- /dev/null +++ b/src/api/dashboard/settings_routes.py @@ -0,0 +1,411 @@ +from fastapi import APIRouter, Request, HTTPException, Security, status, Depends +from src.api.dashboard.auth_routes import get_current_user +from mx_devtools import WelcomeDatabase, AntiSpamDatabase, GlobalChatDatabase, LevelDatabase, LoggingDatabase, AutoDeleteDB, AutoRoleDatabase, TempVCDatabase +import discord +from datetime import datetime + +router = APIRouter( + prefix="/settings", + tags=["settings"], + dependencies=[Depends(get_current_user)] +) + +async def send_dashboard_notification(guild_id: int, module_name: str, user_name: str, channel_id: int = None): + """Helper to send a notification to a Discord channel when settings are saved.""" + from src.api.dashboard.routes import bot_instance + if not bot_instance: + return + + guild = bot_instance.get_guild(guild_id) + if not guild: + return + + # Try to find a suitable channel if none provided + if not channel_id: + # For general settings, we might use a system channel or first available + target_channel = guild.system_channel or guild.text_channels[0] + else: + target_channel = guild.get_channel(channel_id) + + if not target_channel: + return + + embed = discord.Embed( + title="⚙️ Dashboard Einstellungen aktualisiert", + description=f"Die Einstellungen für das Modul **{module_name}** wurden über das Dashboard geändert.", + color=discord.Color.blue(), + timestamp=datetime.now() + ) + embed.add_field(name="Administrator", value=user_name, inline=True) + embed.add_field(name="Modul", value=module_name, inline=True) + embed.set_footer(text="ManagerX Dashboard System", icon_url=bot_instance.user.avatar.url if bot_instance.user.avatar else None) + + try: + await target_channel.send(embed=embed) + except Exception as e: + print(f"Failed to send dashboard notification: {e}") + +@router.get("/{guild_id}") +async def get_settings(guild_id: int): + """Fetch settings for a specific guild.""" + from src.api.dashboard.routes import bot_instance + + if not bot_instance or not hasattr(bot_instance, 'settings_db'): + raise HTTPException(status_code=503, detail="Bot database not ready") + + try: + guild_lang = bot_instance.settings_db.get_guild_language(guild_id) if hasattr(bot_instance.settings_db, 'get_guild_language') else "de" + + return { + "success": True, + "data": { + "bot_name": bot_instance.user.name, + "prefix": "!" , + "auto_mod": True, + "welcome_message": False, + "language": guild_lang + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/{guild_id}") +async def update_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update general settings for a specific guild.""" + from src.api.dashboard.routes import bot_instance + + if not bot_instance or not hasattr(bot_instance, 'settings_db'): + raise HTTPException(status_code=503, detail="Bot database not ready") + + data = await request.json() + + try: + # Update logic... + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "Allgemein", user_name) + return {"success": True, "message": "Settings updated"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save settings: {e}") + +# --- Welcome Module Routes --- + +@router.get("/{guild_id}/channels") +async def get_guild_channels(guild_id: int): + """Returns a list of text channels for the guild.""" + from src.api.dashboard.routes import bot_instance + if not bot_instance: + raise HTTPException(status_code=503, detail="Bot not ready") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found") + + channels = [ + {"id": str(c.id), "name": c.name} + for c in guild.text_channels + ] + return {"success": True, "channels": channels} + +@router.get("/{guild_id}/welcome") +async def get_welcome_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch welcome-specific settings.""" + db = WelcomeDatabase() + try: + settings = await db.get_welcome_settings(guild_id) + if settings and "channel_id" in settings and settings["channel_id"]: + settings["channel_id"] = str(settings["channel_id"]) + if settings and "auto_role_id" in settings and settings["auto_role_id"]: + settings["auto_role_id"] = str(settings["auto_role_id"]) + + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/welcome") +async def update_welcome_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update welcome-specific settings.""" + data = await request.json() + db = WelcomeDatabase() + + if "channel_id" in data and data["channel_id"]: + data["channel_id"] = int(data["channel_id"]) + if "auto_role_id" in data and data["auto_role_id"]: + data["auto_role_id"] = int(data["auto_role_id"]) + + try: + success = await db.update_welcome_settings(guild_id, **data) + if success: + user_name = user.get("username", "Unbekannter User") + # Invalidate cache if possible + from src.api.dashboard.routes import bot_instance + if bot_instance: + cog = bot_instance.get_cog("WelcomeSystem") + if cog and hasattr(cog, 'invalidate_cache'): + cog.invalidate_cache(guild_id) + + # Send notification to the welcome channel if configured + channel_id = data.get("channel_id") + await send_dashboard_notification(guild_id, "Welcome System", user_name, channel_id) + + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save welcome settings: {e}") + +# --- AntiSpam Module Routes --- + +@router.get("/{guild_id}/antispam") +async def get_antispam_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch AntiSpam-specific settings.""" + db = AntiSpamDatabase() + try: + settings = db.get_spam_settings(guild_id) + if settings and "log_channel_id" in settings and settings["log_channel_id"]: + settings["log_channel_id"] = str(settings["log_channel_id"]) + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/antispam") +async def update_antispam_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update AntiSpam-specific settings.""" + data = await request.json() + db = AntiSpamDatabase() + + if "log_channel_id" in data and data["log_channel_id"]: + data["log_channel_id"] = int(data["log_channel_id"]) + + try: + # Use set_spam_settings with direct kwargs if possible, or mapping + success = db.set_spam_settings( + guild_id, + max_messages=data.get("max_messages", 5), + time_frame=data.get("time_frame", 10), + log_channel_id=data.get("log_channel_id") + ) + if success: + user_name = user.get("username", "Unbekannter User") + from src.api.dashboard.routes import bot_instance + if bot_instance: + cog = bot_instance.get_cog("AntiSpam") + # Add cache invalidation if AntiSpam cog supports it + + await send_dashboard_notification(guild_id, "Anti-Spam", user_name, data.get("log_channel_id")) + + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save AntiSpam settings: {e}") + +# --- GlobalChat Module Routes --- + +@router.get("/{guild_id}/globalchat") +async def get_globalchat_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch GlobalChat-specific settings.""" + db = GlobalChatDatabase() + try: + settings = db.get_guild_settings(guild_id) + channel_id = db.get_globalchat_channel(guild_id) + settings["channel_id"] = str(channel_id) if channel_id else None + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/globalchat") +async def update_globalchat_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update GlobalChat-specific settings.""" + data = await request.json() + db = GlobalChatDatabase() + + try: + success = True + user_name = user.get("username", "Unbekannter User") + + # Handle channel_id separately + new_channel_id = data.get("channel_id") + if new_channel_id: + success = db.set_globalchat_channel(guild_id, int(new_channel_id)) + + # Update other settings + for key in ["filter_enabled", "nsfw_filter", "embed_color"]: + if key in data: + db.update_guild_setting(guild_id, key, data[key]) + + if success: + await send_dashboard_notification(guild_id, "Global Chat", user_name, int(new_channel_id) if new_channel_id else None) + + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save GlobalChat settings: {e}") + +# --- LevelSystem Module Routes --- + +@router.get("/{guild_id}/levels") +async def get_level_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch LevelSystem settings.""" + db = LevelDatabase() + try: + settings = db.get_guild_settings(guild_id) + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/levels") +async def update_level_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update LevelSystem settings.""" + data = await request.json() + db = LevelDatabase() + try: + success = db.update_guild_settings(guild_id, **data) + if success: + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "Level-System", user_name) + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save level settings: {e}") + +# --- Logging Module Routes --- + +@router.get("/{guild_id}/logging") +async def get_logging_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch Logging settings.""" + db = LoggingDatabase() + try: + settings = db.get_guild_settings(guild_id) + if settings and "channel_id" in settings and settings["channel_id"]: + settings["channel_id"] = str(settings["channel_id"]) + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/logging") +async def update_logging_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update Logging settings.""" + data = await request.json() + db = LoggingDatabase() + + if "channel_id" in data and data["channel_id"]: + data["channel_id"] = int(data["channel_id"]) + + try: + success = db.update_guild_settings(guild_id, **data) + if success: + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "Server-Log", user_name, data.get("channel_id")) + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save logging settings: {e}") + +# --- AutoRole Module Routes --- + +@router.get("/{guild_id}/autorole") +async def get_autorole_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch AutoRole settings.""" + db = AutoRoleDatabase() + try: + settings = db.get_guild_settings(guild_id) + if settings and "role_id" in settings and settings["role_id"]: + settings["role_id"] = str(settings["role_id"]) + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/autorole") +async def update_autorole_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update AutoRole settings.""" + data = await request.json() + db = AutoRoleDatabase() + + if "role_id" in data and data["role_id"]: + data["role_id"] = int(data["role_id"]) + + try: + success = db.update_guild_settings(guild_id, **data) + if success: + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "Auto-Role", user_name) + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save autorole settings: {e}") + +# --- AutoDelete Module Routes --- + +@router.get("/{guild_id}/autodelete") +async def get_autodelete_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch AutoDelete settings.""" + db = AutoDeleteDB() + try: + settings = db.get_guild_settings(guild_id) + return {"success": True, "data": settings or []} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/autodelete") +async def update_autodelete_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update AutoDelete settings.""" + data = await request.json() + db = AutoDeleteDB() + try: + # Assuming db.update_guild_settings(guild_id, data) where data is a list of channel configs + success = db.update_guild_settings(guild_id, data) + if success: + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "Auto-Delete", user_name) + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save autodelete settings: {e}") +# --- TempVC Module Routes --- + +@router.get("/{guild_id}/tempvc") +async def get_tempvc_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch TempVC-specific settings.""" + db = TempVCDatabase() + try: + settings = db.get_tempvc_settings(guild_id) + if settings: + # result is tuple: (creator_channel_id, category_id, auto_delete_time) + data = { + "creator_channel_id": str(settings[0]), + "category_id": str(settings[1]), + "auto_delete_time": settings[2] + } + else: + data = {} + + # Get UI settings + ui_settings = db.get_ui_settings(guild_id) + if ui_settings: + data["ui_enabled"] = bool(ui_settings[0]) + data["ui_prefix"] = ui_settings[1] + else: + data["ui_enabled"] = False + data["ui_prefix"] = "🔧" + + return {"success": True, "data": data} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/tempvc") +async def update_tempvc_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update TempVC-specific settings.""" + data = await request.json() + db = TempVCDatabase() + + try: + # Update main settings + creator_channel_id = int(data.get("creator_channel_id")) if data.get("creator_channel_id") else 0 + category_id = int(data.get("category_id")) if data.get("category_id") else 0 + auto_delete_time = int(data.get("auto_delete_time", 0)) + + if creator_channel_id and category_id: + db.set_tempvc_settings(guild_id, creator_channel_id, category_id, auto_delete_time) + + # Update UI settings + ui_enabled = bool(data.get("ui_enabled", False)) + ui_prefix = data.get("ui_prefix", "🔧") + db.set_ui_settings(guild_id, ui_enabled, ui_prefix) + + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "TempVC System", user_name, creator_channel_id or None) + + return {"success": True} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save TempVC settings: {e}") diff --git a/src/api/dashboard/user_routes.py b/src/api/dashboard/user_routes.py new file mode 100644 index 0000000..e478667 --- /dev/null +++ b/src/api/dashboard/user_routes.py @@ -0,0 +1,47 @@ +from fastapi import APIRouter, Request, HTTPException, Depends +from src.api.dashboard.auth_routes import get_current_user +from mx_devtools import SettingsDB +import discord + +router = APIRouter( + prefix="/user", + tags=["user"], + dependencies=[Depends(get_current_user)] +) + +@router.get("/settings") +async def get_user_settings(user: dict = Depends(get_current_user)): + """Fetch user settings from SettingsDB.""" + settings_db = SettingsDB() + try: + user_id = int(user["id"]) + + # Get language setting from SettingsDB + language = settings_db.get_user_language(user_id) + + return { + "success": True, + "data": { + "user_id": str(user_id), + "language": language, + "username": user.get("username", "Unknown") + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/settings") +async def update_user_settings(request: Request, user: dict = Depends(get_current_user)): + """Update user settings in SettingsDB.""" + data = await request.json() + settings_db = SettingsDB() + try: + user_id = int(user["id"]) + + # Update language in SettingsDB if provided + if "language" in data: + settings_db.set_user_language(user_id, data["language"]) + + return {"success": True} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Update failed: {e}") diff --git a/src/bot/cogs/guild/globalchat.py b/src/bot/cogs/guild/globalchat.py index d520db9..5868a06 100644 --- a/src/bot/cogs/guild/globalchat.py +++ b/src/bot/cogs/guild/globalchat.py @@ -942,7 +942,7 @@ async def setup_globalchat( ) container.add_text(feature_text) - view = discord.ui.View(container, timeout=None) + view = discord.ui.DesignerView(container, timeout=None) await ctx.respond(view=view, ephemeral=True) except Exception as e: diff --git a/src/bot/cogs/guild/loggingsystem.py b/src/bot/cogs/guild/loggingsystem.py index fa1e9ec..4b79aba 100644 --- a/src/bot/cogs/guild/loggingsystem.py +++ b/src/bot/cogs/guild/loggingsystem.py @@ -47,7 +47,7 @@ def __init__(self, bot): 'logs_sent': 0, 'errors': 0, 'cache_hits': 0, - 'startup_time': datetime.utcnow(), + 'startup_time': discord.utils.utcnow(), } # Start background tasks @@ -106,7 +106,7 @@ async def _cleanup_caches(self): cleanup_count += 1 # Bulk Delete Cache bereinigen (älter als 5 Minuten) - current_time = datetime.utcnow() + current_time = discord.utils.utcnow() expired_guilds = [] for guild_id, data in self._bulk_deletes.items(): @@ -187,7 +187,7 @@ async def send_log(self, guild_id: int, embed: discord.Embed, log_type: str = "g title="⚠️ Log-Fehler", description="Originale Log-Nachricht konnte nicht angezeigt werden (zu lang oder ungültig)", color=discord.Color.orange(), - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) await channel.send(embed=fallback_embed) except: @@ -208,7 +208,7 @@ def _create_user_embed(self, title: str, user: discord.User, color: discord.Colo title=title, description=description, color=color, - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) # User Info - immer als erstes @@ -339,7 +339,7 @@ async def set_log_channel(self, ctx, title="🧪 Test-Nachricht", description=f"Log-Channel für **{log_type}** erfolgreich konfiguriert!", color=discord.Color.blue(), - timestamp=datetime.utcnow() + timestamp=discord.utils.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) @@ -375,7 +375,7 @@ async def remove_log_channel(self, ctx, title="🗑️ Log-Channel entfernt", description=description, color=discord.Color.red(), - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) embed.set_footer(text=f"Entfernt von {ctx.author}") await ctx.respond(embed=embed, ephemeral=True) @@ -400,7 +400,7 @@ async def log_status(self, ctx): embed = discord.Embed( title="📊 Logging Status", color=discord.Color.blue(), - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) if not channels: @@ -423,7 +423,7 @@ async def log_status(self, ctx): embed.description = status_text # Bot Statistiken - uptime = datetime.utcnow() - self._stats['startup_time'] + uptime = discord.utils.utcnow() - self._stats['startup_time'] uptime_str = f"{uptime.days}d {uptime.seconds//3600}h {(uptime.seconds%3600)//60}m" embed.add_field( @@ -472,7 +472,7 @@ async def log_status(self, ctx): 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" + backup_path = f"data/log_channels_backup_{discord.utils.utcnow().strftime('%Y%m%d_%H%M%S')}.db" success = await self.db.backup_database(backup_path) if success: @@ -509,7 +509,7 @@ async def on_member_join(self, member: discord.Member): try: self._stats['events_processed'] += 1 - account_age = datetime.utcnow() - member.created_at + account_age = discord.utils.utcnow() - member.created_at age_days = account_age.days # Verdächtigkeits-Score @@ -519,7 +519,7 @@ async def on_member_join(self, member: discord.Member): elif age_days < 7: suspicious_factors.append("Neues Konto (< 7 Tage)") - if member.display_avatar.is_default(): + if member.avatar is None: suspicious_factors.append("Standard Avatar") # Default Username Pattern @@ -573,7 +573,7 @@ async def on_member_remove(self, member: discord.Member): } if member.joined_at: - duration = datetime.utcnow() - member.joined_at + duration = discord.utils.utcnow() - member.joined_at days = duration.days hours = duration.seconds // 3600 @@ -617,7 +617,7 @@ async def on_message_delete(self, message: discord.Message): guild_id = message.guild.id # Bulk Delete Detection - current_time = datetime.utcnow() + current_time = discord.utils.utcnow() if guild_id not in self._bulk_deletes: self._bulk_deletes[guild_id] = { @@ -646,7 +646,7 @@ async def on_message_delete(self, message: discord.Message): 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() + timestamp=discord.utils.utcnow() ) # Channel Info @@ -680,7 +680,7 @@ async def on_message_delete(self, message: discord.Message): embed = discord.Embed( title="🗑️ Nachricht gelöscht", color=discord.Color.red(), - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) # Author Info diff --git a/src/bot/cogs/guild/welcome.py b/src/bot/cogs/guild/welcome.py index 77d93c7..f8973c5 100644 --- a/src/bot/cogs/guild/welcome.py +++ b/src/bot/cogs/guild/welcome.py @@ -64,6 +64,7 @@ def __init__(self, bot): """ self.bot = bot self.db = WelcomeDatabase() + self.bot.welcome_db = self.db # Cache für bessere Performance self._settings_cache = {} self._cache_timeout = 300 # 5 Minuten Cache diff --git a/src/bot/cogs/management/autodelete.py b/src/bot/cogs/management/autodelete.py index 6c79776..711b529 100644 --- a/src/bot/cogs/management/autodelete.py +++ b/src/bot/cogs/management/autodelete.py @@ -21,7 +21,7 @@ def __init__(self, bot): @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), + duration: Option(int, "Zeit in Sekunden (min: 60s max: 7d (604800s))", required=True), exclude_pinned: Option(bool, "Angepinnte Nachrichten ausschließen", default=True), exclude_bots: Option(bool, "Bot-Nachrichten ausschließen", default=False)): diff --git a/src/bot/cogs/user/stats.py b/src/bot/cogs/user/stats.py index 40d7c63..58300a3 100644 --- a/src/bot/cogs/user/stats.py +++ b/src/bot/cogs/user/stats.py @@ -22,6 +22,7 @@ class EnhancedStatsCog(ezcord.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.db = StatsDB() + self.bot.stats_db = self.db self.level_db = LevelDatabase() self.cleanup_task.start() self.monthly_reset_task.start() diff --git a/src/bot/core/__init__.py b/src/bot/core/__init__.py index 3a4f213..61d58e5 100644 --- a/src/bot/core/__init__.py +++ b/src/bot/core/__init__.py @@ -11,7 +11,6 @@ from .database import DatabaseManager from .dashboard import DashboardTask from .utils import print_logo, format_uptime, truncate_text -from .constants import * __all__ = [ 'ConfigLoader', diff --git a/src/bot/core/dashboard.py b/src/bot/core/dashboard.py index 38d7864..9b0f39c 100644 --- a/src/bot/core/dashboard.py +++ b/src/bot/core/dashboard.py @@ -7,6 +7,7 @@ """ import json +import discord from datetime import datetime from pathlib import Path from discord.ext import tasks @@ -49,7 +50,7 @@ async def _update_stats(self): "uptime": self._get_uptime(), "python_version": self._get_python_version() }, - "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "updated_at": discord.utils.utcnow().strftime("%Y-%m-%d %H:%M:%S") } # In Datei schreiben @@ -62,7 +63,7 @@ async def _update_stats(self): def _get_uptime(self) -> str: """Berechnet die Bot-Uptime""" if hasattr(self.bot, 'start_time'): - delta = datetime.now() - self.bot.start_time + delta = discord.utils.utcnow() - 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" @@ -76,7 +77,7 @@ def _get_python_version(self) -> str: def register(self): """Registriert den Task (startet ihn noch nicht)""" # Startzeit speichern - self.bot.start_time = datetime.now() + self.bot.start_time = discord.utils.utcnow() def start(self): diff --git a/src/bot/core/constants.py b/src/scripts/constants.py similarity index 100% rename from src/bot/core/constants.py rename to src/scripts/constants.py diff --git a/src/web/App.tsx b/src/web/App.tsx index 869e4db..c0356f9 100644 --- a/src/web/App.tsx +++ b/src/web/App.tsx @@ -15,10 +15,13 @@ const CommandsPage = lazy(() => import("./pages/CommandsPage")); const TeamPage = lazy(() => import("./pages/TeamPage")); const RoadmapPage = lazy(() => import("./pages/RoadmapPage")); const License = lazy(() => import("./pages/License").then(module => ({ default: module.License }))); +const LoginPage = lazy(() => import("./dashboard/LoginPage")); +const SettingsPage = lazy(() => import("./dashboard/SettingsPage")); +const UserSettingsPage = lazy(() => import("./dashboard/UserSettingsPage")); +const AuthCallback = lazy(() => import("./pages/AuthCallback")); const queryClient = new QueryClient(); -// Loading fallback component const PageLoader = () => (
Anzahl der Nachrichten, die in einem Zeitraum erlaubt sind.
+Der Zeitraum in dem die Nachrichten gezählt werden.
+Hier werden Spam-Vorfälle protokolliert.
++ Hinweis: Schärfere Einstellungen (weniger Nachrichten/längerer Zeitraum) können dazu führen, dass aktive User fälschlicherweise als Spammer erkannt werden. +
+Noch keine Kanäle konfiguriert.
+Stelle sicher, dass die Bot-Rolle über der Ziel-Rolle steht!
+Bei Beitritt
+Sofort beim Serverbeitritt zuweisen
+Nutzer benachrichtigen
+Sende eine DM nach der Zuweisung
+Wähle den Kanal, der als Global Chat fungieren soll.
+Blockiert automatisch Beleidigungen und Links.
+Blockiert nicht jugendfreie Inhalte.
+Die Farbe, in der deine Nachrichten global angezeigt werden.
+Keine Server mit Bot gefunden.
+Standard ist 1.0. Höhere Werte geben mehr XP.
+Voice XP
+XP für Zeit in Voice-Kanälen
+Ankündigungen
+Level-Ups im Chat verkünden
+Nachrichten
+Gelöschte & bearbeitete Nachrichten
+Mitglieder
+Beitritte, Verlassen, Namensänderungen
+Moderation
+Banns, Kicks, Timeouts
+Server-Updates
+Kanal- & Rollenänderungen
+