diff --git a/.gitignore b/.gitignore index cd03cab..696efa8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,14 +30,17 @@ 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/* \ No newline at end of file +docs/build/* +ManagerX.egg-info/ +dist/ +node_modules 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/DevTools/__init__.py b/DevTools/__init__.py deleted file mode 100644 index 289902d..0000000 --- a/DevTools/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -from .backend import * -from .ui import * \ No newline at end of file diff --git a/DevTools/backend/__init__.py b/DevTools/backend/__init__.py deleted file mode 100644 index 0c0e07d..0000000 --- a/DevTools/backend/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .database import * - -from .config import * - -from .logging import * \ No newline at end of file diff --git a/DevTools/backend/config/__init__.py b/DevTools/backend/config/__init__.py deleted file mode 100644 index 4fdeb61..0000000 --- a/DevTools/backend/config/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -from .links import * -from .permission import * \ No newline at end of file diff --git a/DevTools/backend/config/links.py b/DevTools/backend/config/links.py deleted file mode 100644 index 543921b..0000000 --- a/DevTools/backend/config/links.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -SUPPORT_SERVER = "https://discord.gg/VWR75Tc8DR" -BOT_INVITE = "https://discord.com/oauth2/authorize?client_id=1368201272624287754&permissions=8&integration_type=0&scope=bot" - diff --git a/DevTools/backend/config/permission.py b/DevTools/backend/config/permission.py deleted file mode 100644 index 2917cd7..0000000 --- a/DevTools/backend/config/permission.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -KICK = "kick_members" -BAN = "ban_members" -ADMIN = "administrator" -MANAGE_CHANNELS = "manage_channels" -MANAGE_GUILD = "manage_guild" -ADD_REACT = "add_reactions" -VIEW_AUDIT = "view_audit_log" -PRIORITY_SPEAKER = "priority_speaker" -STREAM = "stream" -VIEW_CHANNEL = "view_channel" -SEND_MSG = "send_messages" -SEND_TTS = "send_tts_messages" -MANAGE_MSG = "manage_messages" -EMBED_LINKS = "embed_links" -ATTACH_FILES = "attach_files" -READ_HISTORY = "read_message_history" -MENTION_ALL = "mention_everyone" -USE_EXT_EMOJIS = "use_external_emojis" -VIEW_INSIGHTS = "view_guild_insights" -CONNECT = "connect" -SPEAK = "speak" -MUTE = "mute_members" -DEAFEN = "deafen_members" -MOVE = "move_members" -USE_VAD = "use_voice_activation" -CHANGE_NICK = "change_nickname" -MANAGE_NICK = "manage_nicknames" -MANAGE_ROLES = "manage_roles" -MANAGE_WEBHOOKS = "manage_webhooks" -MANAGE_EMOJIS = "manage_emojis_and_stickers" -USE_APP_COMMANDS = "use_application_commands" -REQUEST_TO_SPEAK = "request_to_speak" -MANAGE_THREADS = "manage_threads" -USE_PUBLIC_THREADS = "use_public_threads" -USE_PRIVATE_THREADS = "use_private_threads" -USE_EXT_STICKERS = "use_external_stickers" -SEND_MSG_THREADS = "send_messages_in_threads" -START_EMBED_ACT = "start_embedded_activities" -MODERATE = "moderate_members" \ No newline at end of file diff --git a/DevTools/backend/database/Stats_db.py b/DevTools/backend/database/Stats_db.py deleted file mode 100644 index 748539e..0000000 --- a/DevTools/backend/database/Stats_db.py +++ /dev/null @@ -1,476 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -import sqlite3 -import asyncio -import json -from datetime import datetime, timedelta -from typing import Optional, List, Tuple, Dict -import logging - -logger = logging.getLogger(__name__) - - -class StatsDB: - """Enhanced database handler for Discord bot statistics with global level system.""" - - def __init__(self, db_file="data/stats.db"): - self.db_file = db_file - self.conn = sqlite3.connect(db_file, check_same_thread=False) - self.cursor = self.conn.cursor() - self.lock = asyncio.Lock() - self._create_tables() - - def _create_tables(self): - """Create all necessary tables for enhanced stats tracking.""" - tables = [ - '''CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - guild_id INTEGER NOT NULL, - channel_id INTEGER NOT NULL, - message_id INTEGER NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - word_count INTEGER DEFAULT 0, - has_attachment BOOLEAN DEFAULT FALSE, - message_type TEXT DEFAULT 'text' - )''', - - '''CREATE TABLE IF NOT EXISTS voice_sessions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - guild_id INTEGER NOT NULL, - channel_id INTEGER NOT NULL, - start_time DATETIME DEFAULT CURRENT_TIMESTAMP, - end_time DATETIME, - duration_minutes REAL DEFAULT 0 - )''', - - '''CREATE TABLE IF NOT EXISTS global_user_levels ( - user_id INTEGER PRIMARY KEY, - global_level INTEGER DEFAULT 1, - global_xp INTEGER DEFAULT 0, - total_messages INTEGER DEFAULT 0, - total_voice_minutes INTEGER DEFAULT 0, - total_servers INTEGER DEFAULT 0, - first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, - last_activity DATETIME DEFAULT CURRENT_TIMESTAMP, - achievements TEXT DEFAULT '[]', - daily_streak INTEGER DEFAULT 0, - best_streak INTEGER DEFAULT 0, - last_daily_activity DATE - )''', - - '''CREATE TABLE IF NOT EXISTS daily_stats ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - guild_id INTEGER NOT NULL, - date DATE NOT NULL, - messages_count INTEGER DEFAULT 0, - voice_minutes REAL DEFAULT 0, - active_hours INTEGER DEFAULT 0, - UNIQUE(user_id, guild_id, date) - )''', - - '''CREATE TABLE IF NOT EXISTS channel_stats ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - channel_id INTEGER NOT NULL, - guild_id INTEGER NOT NULL, - date DATE NOT NULL, - total_messages INTEGER DEFAULT 0, - unique_users INTEGER DEFAULT 0, - avg_words_per_message REAL DEFAULT 0, - UNIQUE(channel_id, date) - )''', - - '''CREATE TABLE IF NOT EXISTS user_achievements ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - achievement_name TEXT NOT NULL, - unlocked_at DATETIME DEFAULT CURRENT_TIMESTAMP, - description TEXT, - icon TEXT DEFAULT '🏆' - )''', - - '''CREATE TABLE IF NOT EXISTS active_voice_sessions ( - user_id INTEGER PRIMARY KEY, - guild_id INTEGER NOT NULL, - channel_id INTEGER NOT NULL, - start_time DATETIME DEFAULT CURRENT_TIMESTAMP - )''' - ] - - for table_sql in tables: - self.cursor.execute(table_sql) - - # Create indexes for better performance - indexes = [ - 'CREATE INDEX IF NOT EXISTS idx_messages_user_timestamp ON messages(user_id, timestamp)', - 'CREATE INDEX IF NOT EXISTS idx_messages_channel_timestamp ON messages(channel_id, timestamp)', - 'CREATE INDEX IF NOT EXISTS idx_voice_user_timestamp ON voice_sessions(user_id, start_time)', - 'CREATE INDEX IF NOT EXISTS idx_daily_stats_user_date ON daily_stats(user_id, date)', - 'CREATE INDEX IF NOT EXISTS idx_global_levels_xp ON global_user_levels(global_xp DESC)' - ] - - for index_sql in indexes: - self.cursor.execute(index_sql) - - self.conn.commit() - logger.info("Enhanced Stats database initialized") - - async def log_message(self, user_id: int, guild_id: int, channel_id: int, message_id: int, - word_count: int = 0, has_attachment: bool = False, message_type: str = 'text'): - """Log a message and update global XP.""" - async with self.lock: - try: - # Insert message - self.cursor.execute(''' - INSERT INTO messages (user_id, guild_id, channel_id, message_id, word_count, has_attachment, message_type) - VALUES (?, ?, ?, ?, ?, ?, ?) - ''', (user_id, guild_id, channel_id, message_id, word_count, has_attachment, message_type)) - - # Update daily stats - today = datetime.now().date() - self.cursor.execute(''' - INSERT OR IGNORE INTO daily_stats (user_id, guild_id, date, messages_count) - VALUES (?, ?, ?, 1) - ''', (user_id, guild_id, today)) - - self.cursor.execute(''' - UPDATE daily_stats SET messages_count = messages_count + 1 - WHERE user_id = ? AND guild_id = ? AND date = ? - ''', (user_id, guild_id, today)) - - # Update global level system - await self._update_global_xp(user_id, guild_id, 'message', word_count) - - self.conn.commit() - - except Exception as e: - logger.error(f"Error logging message: {e}") - self.conn.rollback() - - async def start_voice_session(self, user_id: int, guild_id: int, channel_id: int): - """Start a voice session.""" - async with self.lock: - try: - # End any existing session first - await self._end_existing_voice_session(user_id) - - # Start new session - self.cursor.execute(''' - INSERT INTO active_voice_sessions (user_id, guild_id, channel_id) - VALUES (?, ?, ?) - ''', (user_id, guild_id, channel_id)) - - self.conn.commit() - - except Exception as e: - logger.error(f"Error starting voice session: {e}") - self.conn.rollback() - - async def end_voice_session(self, user_id: int, channel_id: int): - """End a voice session and calculate duration.""" - async with self.lock: - try: - # Get active session - self.cursor.execute(''' - SELECT guild_id, channel_id, start_time FROM active_voice_sessions - WHERE user_id = ? - ''', (user_id,)) - - session = self.cursor.fetchone() - if not session: - return - - guild_id, session_channel_id, start_time = session - start_datetime = datetime.fromisoformat(start_time) - duration_minutes = (datetime.now() - start_datetime).total_seconds() / 60 - - # Only log if session was longer than 30 seconds - if duration_minutes > 0.5: - # Insert completed session - self.cursor.execute(''' - INSERT INTO voice_sessions (user_id, guild_id, channel_id, start_time, end_time, duration_minutes) - VALUES (?, ?, ?, ?, ?, ?) - ''', (user_id, guild_id, session_channel_id, start_time, datetime.now(), duration_minutes)) - - # Update daily stats - today = datetime.now().date() - self.cursor.execute(''' - INSERT OR IGNORE INTO daily_stats (user_id, guild_id, date, voice_minutes) - VALUES (?, ?, ?, ?) - ''', (user_id, guild_id, today, duration_minutes)) - - self.cursor.execute(''' - UPDATE daily_stats SET voice_minutes = voice_minutes + ? - WHERE user_id = ? AND guild_id = ? AND date = ? - ''', (duration_minutes, user_id, guild_id, today)) - - # Update global XP - await self._update_global_xp(user_id, guild_id, 'voice', duration_minutes) - - # Remove active session - self.cursor.execute('DELETE FROM active_voice_sessions WHERE user_id = ?', (user_id,)) - self.conn.commit() - - except Exception as e: - logger.error(f"Error ending voice session: {e}") - self.conn.rollback() - - async def _end_existing_voice_session(self, user_id: int): - """Helper to end any existing voice session.""" - self.cursor.execute('SELECT channel_id FROM active_voice_sessions WHERE user_id = ?', (user_id,)) - existing = self.cursor.fetchone() - if existing: - await self.end_voice_session(user_id, existing[0]) - - async def _update_global_xp(self, user_id: int, guild_id: int, activity_type: str, value: float = 0): - """Update global XP and level system.""" - try: - # Calculate XP based on activity - xp_gain = 0 - if activity_type == 'message': - base_xp = 1 - word_bonus = min(value * 0.1, 5) # Max 5 bonus XP for long messages - xp_gain = base_xp + word_bonus - elif activity_type == 'voice': - xp_gain = value * 0.5 # 0.5 XP per minute - - # Get current user data - self.cursor.execute(''' - SELECT global_level, global_xp, total_messages, total_voice_minutes, total_servers, last_daily_activity, daily_streak - FROM global_user_levels WHERE user_id = ? - ''', (user_id,)) - - user_data = self.cursor.fetchone() - today = datetime.now().date() - - if user_data: - current_level, current_xp, total_msg, total_voice, total_servers, last_daily, daily_streak = user_data - - # Check for daily streak - if last_daily: - last_date = datetime.strptime(last_daily, '%Y-%m-%d').date() - if today == last_date + timedelta(days=1): - daily_streak += 1 - elif today != last_date: - daily_streak = 1 - else: - daily_streak = 1 - - # Update stats - new_xp = current_xp + xp_gain - new_level = self._calculate_level(new_xp) - - if activity_type == 'message': - total_msg += 1 - elif activity_type == 'voice': - total_voice += value - - # Count unique servers (simplified - you might want to track this differently) - self.cursor.execute('SELECT COUNT(DISTINCT guild_id) FROM messages WHERE user_id = ?', (user_id,)) - server_count = self.cursor.fetchone()[0] or 1 - - self.cursor.execute(''' - UPDATE global_user_levels - SET global_level = ?, global_xp = ?, total_messages = ?, total_voice_minutes = ?, - total_servers = ?, last_activity = ?, last_daily_activity = ?, daily_streak = ?, - best_streak = MAX(best_streak, ?) - WHERE user_id = ? - ''', (new_level, new_xp, total_msg, total_voice, server_count, datetime.now(), - today, daily_streak, daily_streak, user_id)) - - # Check for level up achievements - if new_level > current_level: - await self._check_level_achievements(user_id, new_level) - - else: - # Create new user - initial_level = self._calculate_level(xp_gain) - self.cursor.execute(''' - INSERT INTO global_user_levels - (user_id, global_level, global_xp, total_messages, total_voice_minutes, total_servers, - last_daily_activity, daily_streak, best_streak) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (user_id, initial_level, xp_gain, 1 if activity_type == 'message' else 0, - value if activity_type == 'voice' else 0, 1, today, 1, 1)) - - except Exception as e: - logger.error(f"Error updating global XP: {e}") - - def _calculate_level(self, xp: float) -> int: - """Calculate level based on XP using a logarithmic scale.""" - if xp < 0: - return 1 - # Level formula: level = floor(sqrt(xp/100)) + 1 - import math - return int(math.sqrt(xp / 100)) + 1 - - def _xp_for_level(self, level: int) -> int: - """Calculate XP required for a specific level.""" - return (level - 1) ** 2 * 100 - - async def get_user_stats(self, user_id: int, hours: int = 24, guild_id: Optional[int] = None) -> Tuple[int, float]: - """Get user statistics for a time period.""" - async with self.lock: - try: - cutoff_time = datetime.now() - timedelta(hours=hours) - - # Message count - if guild_id: - self.cursor.execute(''' - SELECT COUNT(*) FROM messages - WHERE user_id = ? AND guild_id = ? AND timestamp > ? - ''', (user_id, guild_id, cutoff_time)) - else: - self.cursor.execute(''' - SELECT COUNT(*) FROM messages - WHERE user_id = ? AND timestamp > ? - ''', (user_id, cutoff_time)) - - message_count = self.cursor.fetchone()[0] or 0 - - # Voice time - if guild_id: - self.cursor.execute(''' - SELECT COALESCE(SUM(duration_minutes), 0) FROM voice_sessions - WHERE user_id = ? AND guild_id = ? AND start_time > ? - ''', (user_id, guild_id, cutoff_time)) - else: - self.cursor.execute(''' - SELECT COALESCE(SUM(duration_minutes), 0) FROM voice_sessions - WHERE user_id = ? AND start_time > ? - ''', (user_id, cutoff_time)) - - voice_minutes = self.cursor.fetchone()[0] or 0 - - return message_count, voice_minutes - - except Exception as e: - logger.error(f"Error getting user stats: {e}") - return 0, 0 - - async def get_global_user_info(self, user_id: int) -> Optional[Dict]: - """Get global user information including level and achievements.""" - async with self.lock: - try: - self.cursor.execute(''' - SELECT global_level, global_xp, total_messages, total_voice_minutes, total_servers, - daily_streak, best_streak, first_seen, achievements - FROM global_user_levels WHERE user_id = ? - ''', (user_id,)) - - result = self.cursor.fetchone() - if not result: - return None - - level, xp, total_msg, total_voice, servers, streak, best_streak, first_seen, achievements = result - - # Calculate XP for next level - next_level_xp = self._xp_for_level(level + 1) - current_level_xp = self._xp_for_level(level) - xp_progress = xp - current_level_xp - xp_needed = next_level_xp - current_level_xp - - return { - 'level': level, - 'xp': xp, - 'xp_progress': xp_progress, - 'xp_needed': xp_needed, - 'total_messages': total_msg, - 'total_voice_minutes': total_voice, - 'total_servers': servers, - 'daily_streak': streak, - 'best_streak': best_streak, - 'first_seen': first_seen, - 'achievements': json.loads(achievements) if achievements else [] - } - - except Exception as e: - logger.error(f"Error getting global user info: {e}") - return None - - async def get_leaderboard(self, limit: int = 10, guild_id: Optional[int] = None) -> List[Tuple]: - """Get global or guild-specific leaderboard.""" - async with self.lock: - try: - if guild_id: - # Guild-specific leaderboard based on recent activity - self.cursor.execute(''' - SELECT user_id, COUNT(*) as messages, - COALESCE(SUM(word_count), 0) as total_words - FROM messages - WHERE guild_id = ? AND timestamp > datetime('now', '-30 days') - GROUP BY user_id - ORDER BY messages DESC - LIMIT ? - ''', (guild_id, limit)) - else: - # Global leaderboard - self.cursor.execute(''' - SELECT user_id, global_level, global_xp, total_messages, total_voice_minutes - FROM global_user_levels - ORDER BY global_xp DESC - LIMIT ? - ''', (limit,)) - - return self.cursor.fetchall() - - except Exception as e: - logger.error(f"Error getting leaderboard: {e}") - return [] - - async def _check_level_achievements(self, user_id: int, new_level: int): - """Check and award level-based achievements.""" - achievements = [] - - level_milestones = { - 5: ("Newcomer", "Reached level 5!", "🌟"), - 10: ("Regular", "Reached level 10!", "⭐"), - 25: ("Veteran", "Reached level 25!", "🏅"), - 50: ("Expert", "Reached level 50!", "🏆"), - 100: ("Legend", "Reached level 100!", "👑") - } - - for milestone, (name, desc, icon) in level_milestones.items(): - if new_level >= milestone: - # Check if already has this achievement - self.cursor.execute(''' - SELECT id FROM user_achievements - WHERE user_id = ? AND achievement_name = ? - ''', (user_id, name)) - - if not self.cursor.fetchone(): - self.cursor.execute(''' - INSERT INTO user_achievements (user_id, achievement_name, description, icon) - VALUES (?, ?, ?, ?) - ''', (user_id, name, desc, icon)) - achievements.append((name, desc, icon)) - - return achievements - - async def cleanup_old_data(self, days: int = 90): - """Clean up old data to keep database size manageable.""" - async with self.lock: - try: - cutoff_date = datetime.now() - timedelta(days=days) - - # Clean old messages (keep recent ones for stats) - self.cursor.execute('DELETE FROM messages WHERE timestamp < ?', (cutoff_date,)) - - # Clean old daily stats - self.cursor.execute('DELETE FROM daily_stats WHERE date < ?', (cutoff_date.date(),)) - - # Clean old voice sessions - self.cursor.execute('DELETE FROM voice_sessions WHERE start_time < ?', (cutoff_date,)) - - self.conn.commit() - logger.info(f"Cleaned up data older than {days} days") - - except Exception as e: - logger.error(f"Error cleaning up old data: {e}") - - def close(self): - """Close database connection.""" - if self.conn: - self.conn.close() - logger.info("Enhanced Stats database connection closed") \ No newline at end of file diff --git a/DevTools/backend/database/__init__.py b/DevTools/backend/database/__init__.py deleted file mode 100644 index 0bd4d49..0000000 --- a/DevTools/backend/database/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -from .vc_db import * -from .warn_db import * -from .spam_db import * -from .notes_db import * -from .Stats_db import * -from .globalchat_db import db as GlobalChatDatabase -from .levelsystem_db import * -from .logging_db import * -from .autodelete_db import * -from .welcome_db import * -from .lang_db import * -from .autorole_db import AutoRoleDatabase \ No newline at end of file diff --git a/DevTools/backend/database/autodelete_db.py b/DevTools/backend/database/autodelete_db.py deleted file mode 100644 index 9f4f73d..0000000 --- a/DevTools/backend/database/autodelete_db.py +++ /dev/null @@ -1,843 +0,0 @@ -import sqlite3 -import json -from datetime import datetime - - -class AutoDeleteDB: - """ - Database manager for AutoDelete functionality in Discord channels. - - Manages AutoDelete configurations, whitelists, schedules, and statistics - for automatic message deletion in Discord channels. - - Parameters - ---------- - db_file : str, optional - Path to the SQLite database file (default: "data/autodelete.db") - - Attributes - ---------- - db_file : str - Path to the database file - conn : sqlite3.Connection - Active database connection - cursor : sqlite3.Cursor - Database cursor for operations - - Examples - -------- - >>> db = AutoDeleteDB("my_database.db") - >>> db.add_autodelete(channel_id=123456, duration=3600) - >>> db.close() - - Or using context manager: - >>> with AutoDeleteDB() as db: - ... db.add_autodelete(channel_id=123456, duration=3600) - """ - - def __init__(self, db_file="data/autodelete.db"): - self.db_file = db_file - self.conn = sqlite3.connect(db_file) - self.cursor = self.conn.cursor() - self._create_tables() - - def _create_tables(self): - """ - Create all required database tables. - - Creates the following tables if they don't exist: - - autodelete: Main configuration - - autodelete_whitelist: Whitelist for roles/users - - autodelete_schedules: Time schedules - - autodelete_stats: Statistics - - Notes - ----- - This method is automatically called during initialization. - """ - self.cursor.execute(''' - CREATE TABLE IF NOT EXISTS autodelete ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - channel_id INTEGER NOT NULL UNIQUE, - duration INTEGER NOT NULL, - exclude_pinned BOOLEAN DEFAULT 1, - exclude_bots BOOLEAN DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - self.cursor.execute(''' - CREATE TABLE IF NOT EXISTS autodelete_whitelist ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - channel_id INTEGER NOT NULL, - target_id INTEGER NOT NULL, - target_type TEXT NOT NULL CHECK (target_type IN ('role', 'user')), - added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (channel_id) REFERENCES autodelete (channel_id) ON DELETE CASCADE, - UNIQUE (channel_id, target_id, target_type) - ) - ''') - - self.cursor.execute(''' - CREATE TABLE IF NOT EXISTS autodelete_schedules ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - channel_id INTEGER NOT NULL, - start_time TEXT NOT NULL, - end_time TEXT NOT NULL, - days TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (channel_id) REFERENCES autodelete (channel_id) ON DELETE CASCADE - ) - ''') - - self.cursor.execute(''' - CREATE TABLE IF NOT EXISTS autodelete_stats ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - channel_id INTEGER NOT NULL UNIQUE, - deleted_count INTEGER DEFAULT 0, - error_count INTEGER DEFAULT 0, - last_deletion TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (channel_id) REFERENCES autodelete (channel_id) ON DELETE CASCADE - ) - ''') - - self.conn.commit() - self._migrate_old_data() - - def _migrate_old_data(self): - """ - Migrate old data to new structure. - - Adds missing columns to existing autodelete table if they don't exist. - This ensures backward compatibility with older database versions. - - Notes - ----- - Errors during migration are printed to console but don't halt execution. - """ - try: - columns = [description[1] for description in - self.cursor.execute("PRAGMA table_info(autodelete)").fetchall()] - - if 'exclude_pinned' not in columns: - self.cursor.execute('ALTER TABLE autodelete ADD COLUMN exclude_pinned BOOLEAN DEFAULT 1') - if 'exclude_bots' not in columns: - self.cursor.execute('ALTER TABLE autodelete ADD COLUMN exclude_bots BOOLEAN DEFAULT 0') - if 'created_at' not in columns: - self.cursor.execute('ALTER TABLE autodelete ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP') - if 'updated_at' not in columns: - self.cursor.execute('ALTER TABLE autodelete ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP') - - self.conn.commit() - except sqlite3.Error as e: - print(f"Migration error: {e}") - - # === MAIN FUNCTIONS === - - def add_autodelete(self, channel_id, duration, exclude_pinned=True, exclude_bots=False): - """ - Add or update AutoDelete configuration for a channel. - - Parameters - ---------- - channel_id : int - Discord channel ID - duration : int - Time in seconds before messages are deleted - exclude_pinned : bool, optional - Whether to exclude pinned messages from deletion (default: True) - exclude_bots : bool, optional - Whether to exclude bot messages from deletion (default: False) - - Notes - ----- - If a configuration for the channel already exists, it will be updated. - Automatically creates a statistics entry if one doesn't exist. - - Examples - -------- - >>> db.add_autodelete(channel_id=123456, duration=3600) - >>> db.add_autodelete(channel_id=789012, duration=7200, exclude_bots=True) - """ - self.cursor.execute(''' - INSERT OR REPLACE INTO autodelete - (channel_id, duration, exclude_pinned, exclude_bots, updated_at) - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) - ''', (channel_id, duration, exclude_pinned, exclude_bots)) - - self.cursor.execute(''' - INSERT OR IGNORE INTO autodelete_stats (channel_id) - VALUES (?) - ''', (channel_id,)) - - self.conn.commit() - - def get_autodelete(self, channel_id): - """ - Get AutoDelete duration for a channel. - - Parameters - ---------- - channel_id : int - Discord channel ID - - Returns - ------- - int or None - Duration in seconds, or None if no configuration exists - - Notes - ----- - This method is for backward compatibility. Use `get_autodelete_full()` - for complete configuration details. - - Examples - -------- - >>> duration = db.get_autodelete(123456) - >>> if duration: - ... print(f"Messages deleted after {duration} seconds") - """ - self.cursor.execute("SELECT duration FROM autodelete WHERE channel_id=?", (channel_id,)) - result = self.cursor.fetchone() - return result[0] if result else None - - def get_autodelete_full(self, channel_id): - """ - Get complete AutoDelete configuration for a channel. - - Parameters - ---------- - channel_id : int - Discord channel ID - - Returns - ------- - tuple or None - Tuple of (duration, exclude_pinned, exclude_bots) or None if not found - - Examples - -------- - >>> config = db.get_autodelete_full(123456) - >>> if config: - ... duration, exclude_pinned, exclude_bots = config - ... print(f"Duration: {duration}s, Exclude pinned: {exclude_pinned}") - """ - self.cursor.execute(''' - SELECT duration, exclude_pinned, exclude_bots - FROM autodelete WHERE channel_id=? - ''', (channel_id,)) - return self.cursor.fetchone() - - def remove_autodelete(self, channel_id): - """ - Remove AutoDelete configuration and all associated data. - - Parameters - ---------- - channel_id : int - Discord channel ID - - Notes - ----- - This cascades to delete all associated whitelist entries, schedules, - and statistics for the channel due to foreign key constraints. - - Examples - -------- - >>> db.remove_autodelete(123456) - """ - self.cursor.execute("DELETE FROM autodelete WHERE channel_id=?", (channel_id,)) - self.conn.commit() - - def get_all(self): - """ - Get all AutoDelete configurations. - - Returns - ------- - list of tuple - List of tuples containing (channel_id, duration, exclude_pinned, exclude_bots) - sorted by channel_id - - Examples - -------- - >>> configs = db.get_all() - >>> for channel_id, duration, exclude_pinned, exclude_bots in configs: - ... print(f"Channel {channel_id}: {duration}s") - """ - self.cursor.execute(''' - SELECT channel_id, duration, exclude_pinned, exclude_bots - FROM autodelete ORDER BY channel_id - ''') - return self.cursor.fetchall() - - # === WHITELIST FUNCTIONS === - - def add_to_whitelist(self, channel_id, target_id, target_type): - """ - Add an entry to the whitelist. - - Parameters - ---------- - channel_id : int - Discord channel ID - target_id : int - Discord role ID or user ID - target_type : {'role', 'user'} - Type of the whitelist target - - Raises - ------ - ValueError - If target_type is not 'role' or 'user' - - Notes - ----- - Whitelisted roles/users will not have their messages auto-deleted. - Duplicate entries are silently ignored. - - Examples - -------- - >>> db.add_to_whitelist(channel_id=123456, target_id=789012, target_type='role') - >>> db.add_to_whitelist(channel_id=123456, target_id=345678, target_type='user') - """ - if target_type not in ['role', 'user']: - raise ValueError("target_type must be 'role' or 'user'") - - self.cursor.execute(''' - INSERT OR IGNORE INTO autodelete_whitelist - (channel_id, target_id, target_type) - VALUES (?, ?, ?) - ''', (channel_id, target_id, target_type)) - self.conn.commit() - - def remove_from_whitelist(self, channel_id, target_id, target_type): - """ - Remove an entry from the whitelist. - - Parameters - ---------- - channel_id : int - Discord channel ID - target_id : int - Discord role ID or user ID - target_type : {'role', 'user'} - Type of the whitelist target - - Examples - -------- - >>> db.remove_from_whitelist(channel_id=123456, target_id=789012, target_type='role') - """ - self.cursor.execute(''' - DELETE FROM autodelete_whitelist - WHERE channel_id=? AND target_id=? AND target_type=? - ''', (channel_id, target_id, target_type)) - self.conn.commit() - - def get_whitelist(self, channel_id): - """ - Get whitelist for a channel. - - Parameters - ---------- - channel_id : int - Discord channel ID - - Returns - ------- - dict - Dictionary with 'roles' and 'users' keys, each containing a list of IDs - - Examples - -------- - >>> whitelist = db.get_whitelist(123456) - >>> print(f"Whitelisted roles: {whitelist['roles']}") - >>> print(f"Whitelisted users: {whitelist['users']}") - """ - self.cursor.execute(''' - SELECT target_id, target_type FROM autodelete_whitelist - WHERE channel_id=? - ''', (channel_id,)) - - results = self.cursor.fetchall() - whitelist = {'roles': [], 'users': []} - - for target_id, target_type in results: - if target_type == 'role': - whitelist['roles'].append(target_id) - elif target_type == 'user': - whitelist['users'].append(target_id) - - return whitelist - - def clear_whitelist(self, channel_id): - """ - Clear complete whitelist for a channel. - - Parameters - ---------- - channel_id : int - Discord channel ID - - Examples - -------- - >>> db.clear_whitelist(123456) - """ - self.cursor.execute("DELETE FROM autodelete_whitelist WHERE channel_id=?", (channel_id,)) - self.conn.commit() - - # === SCHEDULE FUNCTIONS === - - def add_schedule(self, channel_id, start_time, end_time, days): - """ - Add a time schedule for AutoDelete. - - Parameters - ---------- - channel_id : int - Discord channel ID - start_time : str - Start time in HH:MM format - end_time : str - End time in HH:MM format - days : str - Days when schedule is active (e.g., "Mon,Tue,Wed") - - Notes - ----- - Schedules allow AutoDelete to only run during specific time windows. - - Examples - -------- - >>> db.add_schedule(channel_id=123456, start_time="09:00", - ... end_time="17:00", days="Mon,Tue,Wed,Thu,Fri") - """ - self.cursor.execute(''' - INSERT INTO autodelete_schedules - (channel_id, start_time, end_time, days) - VALUES (?, ?, ?, ?) - ''', (channel_id, start_time, end_time, days)) - self.conn.commit() - - def remove_schedule(self, channel_id, start_time=None): - """ - Remove schedule(s) for a channel. - - Parameters - ---------- - channel_id : int - Discord channel ID - start_time : str, optional - Specific start time to remove. If None, removes all schedules - - Examples - -------- - >>> db.remove_schedule(channel_id=123456, start_time="09:00") - >>> db.remove_schedule(channel_id=123456) # Remove all schedules - """ - if start_time: - self.cursor.execute(''' - DELETE FROM autodelete_schedules - WHERE channel_id=? AND start_time=? - ''', (channel_id, start_time)) - else: - self.cursor.execute(''' - DELETE FROM autodelete_schedules WHERE channel_id=? - ''', (channel_id,)) - self.conn.commit() - - def get_schedules(self, channel_id): - """ - Get all schedules for a channel. - - Parameters - ---------- - channel_id : int - Discord channel ID - - Returns - ------- - list of tuple - List of tuples containing (start_time, end_time, days) sorted by start_time - - Examples - -------- - >>> schedules = db.get_schedules(123456) - >>> for start, end, days in schedules: - ... print(f"{start}-{end} on {days}") - """ - self.cursor.execute(''' - SELECT start_time, end_time, days - FROM autodelete_schedules - WHERE channel_id=? - ORDER BY start_time - ''', (channel_id,)) - return self.cursor.fetchall() - - # === STATISTICS FUNCTIONS === - - def update_stats(self, channel_id, deleted_count=0, error_count=0): - """ - Update statistics for a channel. - - Parameters - ---------- - channel_id : int - Discord channel ID - deleted_count : int, optional - Number of messages deleted (default: 0) - error_count : int, optional - Number of errors encountered (default: 0) - - Notes - ----- - Counts are cumulative. The last_deletion timestamp is only updated - if deleted_count > 0. - - Examples - -------- - >>> db.update_stats(channel_id=123456, deleted_count=10) - >>> db.update_stats(channel_id=123456, error_count=1) - """ - timestamp = datetime.utcnow().timestamp() if deleted_count > 0 else None - - self.cursor.execute(''' - INSERT OR REPLACE INTO autodelete_stats - (channel_id, deleted_count, error_count, last_deletion, updated_at) - VALUES ( - ?, - COALESCE((SELECT deleted_count FROM autodelete_stats WHERE channel_id=?), 0) + ?, - COALESCE((SELECT error_count FROM autodelete_stats WHERE channel_id=?), 0) + ?, - COALESCE(?, (SELECT last_deletion FROM autodelete_stats WHERE channel_id=?)), - CURRENT_TIMESTAMP - ) - ''', (channel_id, channel_id, deleted_count, channel_id, error_count, timestamp, channel_id)) - self.conn.commit() - - def get_stats(self, channel_id): - """ - Get statistics for a channel. - - Parameters - ---------- - channel_id : int - Discord channel ID - - Returns - ------- - dict or None - Dictionary containing statistics or None if not found. - Keys: 'deleted_count', 'error_count', 'last_deletion', - 'created_at', 'updated_at' - - Examples - -------- - >>> stats = db.get_stats(123456) - >>> if stats: - ... print(f"Deleted: {stats['deleted_count']} messages") - ... print(f"Errors: {stats['error_count']}") - """ - self.cursor.execute(''' - SELECT deleted_count, error_count, last_deletion, created_at, updated_at - FROM autodelete_stats WHERE channel_id=? - ''', (channel_id,)) - - result = self.cursor.fetchone() - if result: - return { - 'deleted_count': result[0], - 'error_count': result[1], - 'last_deletion': result[2], - 'created_at': result[3], - 'updated_at': result[4] - } - return None - - def reset_stats(self, channel_id): - """ - Reset statistics for a channel. - - Parameters - ---------- - channel_id : int - Discord channel ID - - Notes - ----- - Sets deleted_count and error_count to 0, clears last_deletion timestamp. - - Examples - -------- - >>> db.reset_stats(123456) - """ - self.cursor.execute(''' - UPDATE autodelete_stats - SET deleted_count=0, error_count=0, last_deletion=NULL, updated_at=CURRENT_TIMESTAMP - WHERE channel_id=? - ''', (channel_id,)) - self.conn.commit() - - def get_global_stats(self): - """ - Get global statistics across all channels. - - Returns - ------- - dict or None - Dictionary containing global statistics or None if no data exists. - Keys: 'active_channels', 'total_deleted', 'total_errors', 'latest_deletion' - - Examples - -------- - >>> stats = db.get_global_stats() - >>> if stats: - ... print(f"Active channels: {stats['active_channels']}") - ... print(f"Total deleted: {stats['total_deleted']}") - """ - self.cursor.execute(''' - SELECT - COUNT(*) as active_channels, - SUM(deleted_count) as total_deleted, - SUM(error_count) as total_errors, - MAX(last_deletion) as latest_deletion - FROM autodelete_stats s - JOIN autodelete a ON s.channel_id = a.channel_id - ''') - - result = self.cursor.fetchone() - if result: - return { - 'active_channels': result[0], - 'total_deleted': result[1] or 0, - 'total_errors': result[2] or 0, - 'latest_deletion': result[3] - } - return None - - # === EXPORT/IMPORT FUNCTIONS === - - def export_all_settings(self): - """ - Export all AutoDelete settings. - - Returns - ------- - dict - Dictionary containing all configurations, whitelists, schedules, and stats - - Notes - ----- - The returned dictionary can be serialized to JSON and later imported - using `import_settings()`. - - Examples - -------- - >>> data = db.export_all_settings() - >>> import json - >>> with open('backup.json', 'w') as f: - ... json.dump(data, f, indent=2) - """ - data = { - 'exported_at': datetime.utcnow().isoformat(), - 'channels': [] - } - - self.cursor.execute(''' - SELECT channel_id, duration, exclude_pinned, exclude_bots, created_at, updated_at - FROM autodelete ORDER BY channel_id - ''') - - for row in self.cursor.fetchall(): - channel_id = row[0] - channel_data = { - 'channel_id': channel_id, - 'duration': row[1], - 'exclude_pinned': bool(row[2]), - 'exclude_bots': bool(row[3]), - 'created_at': row[4], - 'updated_at': row[5], - 'whitelist': self.get_whitelist(channel_id), - 'schedules': self.get_schedules(channel_id), - 'stats': self.get_stats(channel_id) - } - data['channels'].append(channel_data) - - return data - - def import_settings(self, data, overwrite=False): - """ - Import AutoDelete settings. - - Parameters - ---------- - data : dict - Dictionary containing exported settings (from `export_all_settings()`) - overwrite : bool, optional - Whether to overwrite existing configurations (default: False) - - Returns - ------- - dict - Dictionary with 'imported' and 'skipped' counts - - Notes - ----- - If overwrite is False, existing channel configurations are skipped. - If overwrite is True, existing configurations are replaced. - - Examples - -------- - >>> import json - >>> with open('backup.json', 'r') as f: - ... data = json.load(f) - >>> result = db.import_settings(data, overwrite=True) - >>> print(f"Imported: {result['imported']}, Skipped: {result['skipped']}") - """ - imported_count = 0 - skipped_count = 0 - - for channel_data in data.get('channels', []): - channel_id = channel_data['channel_id'] - - if not overwrite and self.get_autodelete(channel_id): - skipped_count += 1 - continue - - self.add_autodelete( - channel_id, - channel_data['duration'], - channel_data.get('exclude_pinned', True), - channel_data.get('exclude_bots', False) - ) - - if overwrite: - self.clear_whitelist(channel_id) - - whitelist = channel_data.get('whitelist', {}) - for role_id in whitelist.get('roles', []): - self.add_to_whitelist(channel_id, role_id, 'role') - for user_id in whitelist.get('users', []): - self.add_to_whitelist(channel_id, user_id, 'user') - - if overwrite: - self.remove_schedule(channel_id) - - for start_time, end_time, days in channel_data.get('schedules', []): - self.add_schedule(channel_id, start_time, end_time, days) - - imported_count += 1 - - return {'imported': imported_count, 'skipped': skipped_count} - - # === MAINTENANCE FUNCTIONS === - - def cleanup_orphaned_data(self): - """ - Remove orphaned data from auxiliary tables. - - Returns - ------- - int - Number of orphaned records removed - - Notes - ----- - Removes whitelist entries, schedules, and statistics that reference - non-existent AutoDelete configurations. - - Examples - -------- - >>> removed = db.cleanup_orphaned_data() - >>> print(f"Removed {removed} orphaned records") - """ - self.cursor.execute(''' - DELETE FROM autodelete_whitelist - WHERE channel_id NOT IN (SELECT channel_id FROM autodelete) - ''') - - self.cursor.execute(''' - DELETE FROM autodelete_schedules - WHERE channel_id NOT IN (SELECT channel_id FROM autodelete) - ''') - - self.cursor.execute(''' - DELETE FROM autodelete_stats - WHERE channel_id NOT IN (SELECT channel_id FROM autodelete) - ''') - - self.conn.commit() - return self.cursor.rowcount - - def vacuum_database(self): - """ - Optimize the database. - - Notes - ----- - Rebuilds the database file, repacking it into a minimal amount of disk space. - This can improve performance but may take time on large databases. - - Examples - -------- - >>> db.vacuum_database() - """ - self.cursor.execute("VACUUM") - self.conn.commit() - - def get_database_info(self): - """ - Get database information and statistics. - - Returns - ------- - dict - Dictionary containing record counts for each table and file size information - - Examples - -------- - >>> info = db.get_database_info() - >>> print(f"Database size: {info['file_size_mb']} MB") - >>> print(f"AutoDelete configs: {info['autodelete_count']}") - """ - info = {} - - tables = ['autodelete', 'autodelete_whitelist', 'autodelete_schedules', 'autodelete_stats'] - for table in tables: - self.cursor.execute(f"SELECT COUNT(*) FROM {table}") - info[f"{table}_count"] = self.cursor.fetchone()[0] - - import os - if os.path.exists(self.db_file): - info['file_size_bytes'] = os.path.getsize(self.db_file) - info['file_size_mb'] = round(info['file_size_bytes'] / 1024 / 1024, 2) - - return info - - def close(self): - """ - Close the database connection. - - Notes - ----- - Should be called when done using the database to free resources. - Not needed when using the context manager syntax. - - Examples - -------- - >>> db = AutoDeleteDB() - >>> # ... use database ... - >>> db.close() - """ - if self.conn: - self.conn.close() - - def __enter__(self): - """Context manager entry.""" - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Context manager exit.""" - self.close() \ No newline at end of file diff --git a/DevTools/backend/database/autorole_db.py b/DevTools/backend/database/autorole_db.py deleted file mode 100644 index 23f4f3c..0000000 --- a/DevTools/backend/database/autorole_db.py +++ /dev/null @@ -1,102 +0,0 @@ -import aiosqlite -import random -import string -import os - -class AutoRoleDatabase: - def __init__(self, db_path="data/autorole.db"): - self.db_path = db_path - # Erstellt den Ordner 'data', falls er fehlt - directory = os.path.dirname(self.db_path) - if directory and not os.path.exists(directory): - os.makedirs(directory, exist_ok=True) - - async def init_db(self): - """Erstellt die Tabelle, falls sie noch nicht existiert""" - async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - CREATE TABLE IF NOT EXISTS autoroles ( - autorole_id TEXT PRIMARY KEY, - guild_id INTEGER NOT NULL, - role_id INTEGER NOT NULL, - enabled INTEGER DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """) - await db.commit() - - def generate_autorole_id(self, guild_id: int, role_id: int): - guild_part = str(guild_id)[-2:].zfill(2) - role_part = str(role_id)[-2:].zfill(2) - random_part = ''.join(random.choices(string.digits, k=3)) - return f"{guild_part}-{role_part}-{random_part}" - - async def add_autorole(self, guild_id: int, role_id: int): - # WICHTIG: Erst sicherstellen, dass die Tabelle da ist! - await self.init_db() - - autorole_id = self.generate_autorole_id(guild_id, role_id) - - async with aiosqlite.connect(self.db_path) as db: - # Check ob ID existiert - while True: - async with db.execute( - "SELECT autorole_id FROM autoroles WHERE autorole_id = ?", - (autorole_id,) - ) as cursor: - if not await cursor.fetchone(): - break - autorole_id = self.generate_autorole_id(guild_id, role_id) - - await db.execute(""" - INSERT INTO autoroles (autorole_id, guild_id, role_id, enabled) - VALUES (?, ?, ?, 1) - """, (autorole_id, guild_id, role_id)) - await db.commit() - - return autorole_id - - async def get_all_autoroles(self, guild_id: int): - await self.init_db() - async with aiosqlite.connect(self.db_path) as db: - async with db.execute( - "SELECT autorole_id, role_id, enabled FROM autoroles WHERE guild_id = ?", - (guild_id,) - ) as cursor: - rows = await cursor.fetchall() - return [{"autorole_id": r[0], "role_id": r[1], "enabled": bool(r[2])} for r in rows] - - async def get_autorole(self, autorole_id: str): - await self.init_db() - async with aiosqlite.connect(self.db_path) as db: - async with db.execute( - "SELECT autorole_id, guild_id, role_id, enabled FROM autoroles WHERE autorole_id = ?", - (autorole_id,) - ) as cursor: - row = await cursor.fetchone() - return {"autorole_id": row[0], "guild_id": row[1], "role_id": row[2], "enabled": bool(row[3])} if row else None - - async def get_enabled_autoroles(self, guild_id: int): - await self.init_db() - async with aiosqlite.connect(self.db_path) as db: - async with db.execute( - "SELECT role_id FROM autoroles WHERE guild_id = ? AND enabled = 1", - (guild_id,) - ) as cursor: - rows = await cursor.fetchall() - return [r[0] for r in rows] - - async def remove_autorole(self, autorole_id: str): - await self.init_db() - async with aiosqlite.connect(self.db_path) as db: - await db.execute("DELETE FROM autoroles WHERE autorole_id = ?", (autorole_id,)) - await db.commit() - - async def toggle_autorole(self, autorole_id: str, enabled: bool): - await self.init_db() - async with aiosqlite.connect(self.db_path) as db: - await db.execute( - "UPDATE autoroles SET enabled = ? WHERE autorole_id = ?", - (1 if enabled else 0, autorole_id) - ) - await db.commit() \ No newline at end of file diff --git a/DevTools/backend/database/globalchat_db.py b/DevTools/backend/database/globalchat_db.py deleted file mode 100644 index 0fb592b..0000000 --- a/DevTools/backend/database/globalchat_db.py +++ /dev/null @@ -1,955 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -import sqlite3 -import os -import logging -from typing import Optional, List, Dict, Tuple -from datetime import datetime, timedelta -import time - -# Logger -logger = logging.getLogger(__name__) - -DB_PATH = "data/globalchat.db" - - -class GlobalChatDatabase: - """ - Database manager for Discord GlobalChat system. - - Manages channel configurations, message logging, blacklists, guild settings, - and statistics for a cross-server global chat system. - - Attributes - ---------- - DB_PATH : str - Path to the SQLite database file - - Notes - ----- - Automatically creates necessary tables and performs migrations on initialization. - Uses context managers for database connections to ensure proper resource management. - - Examples - -------- - >>> db = GlobalChatDatabase() - >>> db.set_globalchat_channel(guild_id=123456, channel_id=789012) - >>> channels = db.get_all_channels() - """ - - def __init__(self): - self._ensure_db_dir() - self.create_tables() - self.migrate_database() - - def _ensure_db_dir(self): - """ - Ensure that the data directory exists. - - Notes - ----- - Creates parent directories if they don't exist. Does not raise an error - if the directory already exists. - """ - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - - def _get_connection(self): - """ - Get a database connection. - - Returns - ------- - sqlite3.Connection - Database connection with Row factory enabled - - Notes - ----- - The connection uses sqlite3.Row as row_factory, allowing dictionary-style - access to columns. - - Examples - -------- - >>> with self._get_connection() as conn: - ... cursor = conn.cursor() - ... cursor.execute("SELECT * FROM globalchat_channels") - """ - conn = sqlite3.connect(DB_PATH) - conn.row_factory = sqlite3.Row - return conn - - def _column_exists(self, table_name: str, column_name: str) -> bool: - """ - Check if a column exists in a table. - - Parameters - ---------- - table_name : str - Name of the database table - column_name : str - Name of the column to check - - Returns - ------- - bool - True if column exists, False otherwise - - Notes - ----- - Returns False if any database error occurs during the check. - - Examples - -------- - >>> if db._column_exists('globalchat_channels', 'guild_name'): - ... print("Column exists") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - c.execute(f"PRAGMA table_info({table_name})") - columns = [row[1] for row in c.fetchall()] - return column_name in columns - except sqlite3.Error: - return False - - def migrate_database(self): - """ - Perform database migrations. - - Adds missing columns to existing tables to ensure schema compatibility - with newer versions. Migrations are idempotent and safe to run multiple times. - - Raises - ------ - sqlite3.Error - If migration fails - - Notes - ----- - Automatically called during initialization. Logs each migration step. - Critical migration: Adds 'content' column to message_log table. - - Examples - -------- - >>> db = GlobalChatDatabase() # Migrations run automatically - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - - # Migration for globalchat_channels - if not self._column_exists('globalchat_channels', 'guild_name'): - logger.info("Adding column 'guild_name' to globalchat_channels") - c.execute("ALTER TABLE globalchat_channels ADD COLUMN guild_name TEXT") - - if not self._column_exists('globalchat_channels', 'channel_name'): - logger.info("Adding column 'channel_name' to globalchat_channels") - c.execute("ALTER TABLE globalchat_channels ADD COLUMN channel_name TEXT") - - if not self._column_exists('globalchat_channels', 'created_at'): - logger.info("Adding column 'created_at' to globalchat_channels") - c.execute("ALTER TABLE globalchat_channels ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP") - - if not self._column_exists('globalchat_channels', 'last_activity'): - logger.info("Adding column 'last_activity' to globalchat_channels") - c.execute("ALTER TABLE globalchat_channels ADD COLUMN last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP") - - if not self._column_exists('globalchat_channels', 'message_count'): - logger.info("Adding column 'message_count' to globalchat_channels") - c.execute("ALTER TABLE globalchat_channels ADD COLUMN message_count INTEGER DEFAULT 0") - - if not self._column_exists('globalchat_channels', 'is_active'): - logger.info("Adding column 'is_active' to globalchat_channels") - c.execute("ALTER TABLE globalchat_channels ADD COLUMN is_active BOOLEAN DEFAULT 1") - - # CRITICAL MIGRATION: message_log content column - if not self._column_exists('message_log', 'content'): - logger.info("✹ Adding column 'content' to message_log") - c.execute("ALTER TABLE message_log ADD COLUMN content TEXT") - - conn.commit() - logger.info("✅ Database migration completed") - - except sqlite3.Error as e: - logger.error(f"❌ Migration error: {e}") - raise - - def create_tables(self): - """ - Create all required database tables. - - Creates the following tables if they don't exist: - - globalchat_channels: Channel configurations - - message_log: Message history for moderation - - globalchat_blacklist: Banned users and guilds - - guild_settings: Per-guild configuration - - daily_stats: Daily statistics - - Raises - ------ - sqlite3.Error - If table creation fails - - Notes - ----- - Safe to call multiple times. Uses IF NOT EXISTS to avoid errors. - - Examples - -------- - >>> db = GlobalChatDatabase() # Tables created automatically - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - - # GlobalChat Channels - c.execute(""" - CREATE TABLE IF NOT EXISTS globalchat_channels ( - guild_id INTEGER PRIMARY KEY, - channel_id INTEGER NOT NULL - ) - """) - - # Message Log - CORRECTED with content column - c.execute(""" - CREATE TABLE IF NOT EXISTS message_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - guild_id INTEGER NOT NULL, - channel_id INTEGER NOT NULL, - content TEXT, - attachment_urls TEXT, - timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """) - - # Blacklist System - c.execute(""" - CREATE TABLE IF NOT EXISTS globalchat_blacklist ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - entity_type TEXT NOT NULL CHECK (entity_type IN ('user', 'guild')), - entity_id INTEGER NOT NULL, - reason TEXT, - banned_by INTEGER, - banned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP, - is_permanent BOOLEAN DEFAULT 0, - UNIQUE(entity_type, entity_id) - ) - """) - - # Guild Settings - c.execute(""" - CREATE TABLE IF NOT EXISTS guild_settings ( - guild_id INTEGER PRIMARY KEY, - filter_enabled BOOLEAN DEFAULT 1, - nsfw_filter BOOLEAN DEFAULT 1, - embed_color TEXT DEFAULT '#5865F2', - custom_webhook_name TEXT, - max_message_length INTEGER DEFAULT 1900, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """) - - # Statistics - c.execute(""" - CREATE TABLE IF NOT EXISTS daily_stats ( - date DATE PRIMARY KEY, - total_messages INTEGER DEFAULT 0, - active_guilds INTEGER DEFAULT 0, - active_users INTEGER DEFAULT 0 - ) - """) - - conn.commit() - logger.info("✅ Base database tables created") - except sqlite3.Error as e: - logger.error(f"❌ Error creating tables: {e}") - raise - - def set_globalchat_channel(self, guild_id: int, channel_id: int, guild_name: str = None, channel_name: str = None) -> bool: - """ - Set a GlobalChat channel for a guild. - - Parameters - ---------- - guild_id : int - Discord guild ID - channel_id : int - Discord channel ID - guild_name : str, optional - Name of the guild (default: None) - channel_name : str, optional - Name of the channel (default: None) - - Returns - ------- - bool - True if successful, False otherwise - - Notes - ----- - Updates existing configuration if guild already has a channel set. - Automatically updates last_activity timestamp if the column exists. - - Examples - -------- - >>> db.set_globalchat_channel(guild_id=123456, channel_id=789012) - >>> db.set_globalchat_channel(guild_id=123456, channel_id=789012, - ... guild_name="My Server", channel_name="global-chat") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - - has_guild_name = self._column_exists('globalchat_channels', 'guild_name') - has_channel_name = self._column_exists('globalchat_channels', 'channel_name') - has_last_activity = self._column_exists('globalchat_channels', 'last_activity') - - if has_guild_name and has_channel_name and has_last_activity: - c.execute(""" - INSERT OR REPLACE INTO globalchat_channels - (guild_id, channel_id, guild_name, channel_name, last_activity) - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) - """, (guild_id, channel_id, guild_name, channel_name)) - else: - c.execute(""" - INSERT OR REPLACE INTO globalchat_channels - (guild_id, channel_id) - VALUES (?, ?) - """, (guild_id, channel_id)) - - conn.commit() - logger.info(f"✅ GlobalChat channel set: Guild {guild_id} -> Channel {channel_id}") - return True - except sqlite3.Error as e: - logger.error(f"❌ Error setting GlobalChat channel: {e}") - return False - - def get_all_channels(self) -> List[int]: - """ - Get all active GlobalChat channel IDs. - - Returns - ------- - list of int - List of active channel IDs - - Notes - ----- - Only returns active channels if the is_active column exists. - Returns empty list if an error occurs. - - Examples - -------- - >>> channels = db.get_all_channels() - >>> print(f"Active channels: {len(channels)}") - >>> for channel_id in channels: - ... print(f"Channel: {channel_id}") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - - if self._column_exists('globalchat_channels', 'is_active'): - c.execute("SELECT channel_id FROM globalchat_channels WHERE is_active = 1") - else: - c.execute("SELECT channel_id FROM globalchat_channels") - - result = [row['channel_id'] for row in c.fetchall()] - logger.debug(f"📊 All active channels retrieved: {len(result)} channels") - return result - except sqlite3.Error as e: - logger.error(f"❌ Error retrieving all channels: {e}") - return [] - - def get_globalchat_channel(self, guild_id: int) -> Optional[int]: - """ - Get the channel ID for a guild. - - Parameters - ---------- - guild_id : int - Discord guild ID - - Returns - ------- - int or None - Channel ID if found, None otherwise - - Notes - ----- - Only returns channel if it's marked as active (when is_active column exists). - - Examples - -------- - >>> channel_id = db.get_globalchat_channel(123456) - >>> if channel_id: - ... print(f"Guild has channel: {channel_id}") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - - if self._column_exists('globalchat_channels', 'is_active'): - c.execute("SELECT channel_id FROM globalchat_channels WHERE guild_id = ? AND is_active = 1", (guild_id,)) - else: - c.execute("SELECT channel_id FROM globalchat_channels WHERE guild_id = ?", (guild_id,)) - - result = c.fetchone() - return result['channel_id'] if result else None - except sqlite3.Error as e: - logger.error(f"❌ Error retrieving channel for guild {guild_id}: {e}") - return None - - def remove_globalchat_channel(self, guild_id: int) -> bool: - """ - Remove a GlobalChat channel configuration. - - Parameters - ---------- - guild_id : int - Discord guild ID - - Returns - ------- - bool - True if channel was removed, False if not found or error occurred - - Examples - -------- - >>> if db.remove_globalchat_channel(123456): - ... print("Channel removed successfully") - ... else: - ... print("No channel found or error occurred") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - c.execute("DELETE FROM globalchat_channels WHERE guild_id = ?", (guild_id,)) - changes = c.rowcount - conn.commit() - - if changes > 0: - logger.info(f"✅ GlobalChat channel removed for guild {guild_id}") - return True - else: - logger.warning(f"⚠ No channel found for guild {guild_id}") - return False - except sqlite3.Error as e: - logger.error(f"❌ Error removing GlobalChat channel: {e}") - return False - - def update_channel_activity(self, guild_id: int): - """ - Update last activity and increment message count. - - Parameters - ---------- - guild_id : int - Discord guild ID - - Notes - ----- - Only updates fields that exist in the schema. Safe to call even if - columns don't exist yet. - - Examples - -------- - >>> db.update_channel_activity(123456) - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - - has_last_activity = self._column_exists('globalchat_channels', 'last_activity') - has_message_count = self._column_exists('globalchat_channels', 'message_count') - - if has_last_activity and has_message_count: - c.execute(""" - UPDATE globalchat_channels - SET last_activity = CURRENT_TIMESTAMP, message_count = message_count + 1 - WHERE guild_id = ? - """, (guild_id,)) - elif has_message_count: - c.execute(""" - UPDATE globalchat_channels - SET message_count = message_count + 1 - WHERE guild_id = ? - """, (guild_id,)) - - conn.commit() - except sqlite3.Error as e: - logger.error(f"❌ Error updating activity: {e}") - - def log_message(self, user_id: int, guild_id: int, channel_id: int, content: str, attachment_urls: str = None): - """ - Log a message for moderation purposes. - - Parameters - ---------- - user_id : int - Discord user ID who sent the message - guild_id : int - Discord guild ID where message was sent - channel_id : int - Discord channel ID where message was sent - content : str - Message content - attachment_urls : str, optional - URLs of message attachments (default: None) - - Notes - ----- - Logs are used for moderation and can be retrieved with get_user_message_history(). - - Examples - -------- - >>> db.log_message(user_id=123456, guild_id=789012, channel_id=345678, - ... content="Hello world!") - >>> db.log_message(user_id=123456, guild_id=789012, channel_id=345678, - ... content="Check this out", attachment_urls="https://example.com/image.png") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - c.execute(""" - INSERT INTO message_log - (user_id, guild_id, channel_id, content, attachment_urls) - VALUES (?, ?, ?, ?, ?) - """, (user_id, guild_id, channel_id, content, attachment_urls)) - conn.commit() - logger.debug(f"📝 Message logged: User {user_id} in Guild {guild_id}") - except sqlite3.Error as e: - logger.error(f"❌ Error logging message: {e}") - - def get_user_message_history(self, user_id: int, limit: int = 10) -> List[Dict]: - """ - Get recent messages from a user. - - Parameters - ---------- - user_id : int - Discord user ID - limit : int, optional - Maximum number of messages to retrieve (default: 10) - - Returns - ------- - list of dict - List of message dictionaries, newest first. Each dictionary contains: - id, user_id, guild_id, channel_id, content, attachment_urls, timestamp - - Examples - -------- - >>> messages = db.get_user_message_history(123456, limit=5) - >>> for msg in messages: - ... print(f"{msg['timestamp']}: {msg['content']}") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - c.execute(""" - SELECT * FROM message_log - WHERE user_id = ? - ORDER BY timestamp DESC - LIMIT ? - """, (user_id, limit)) - return [dict(row) for row in c.fetchall()] - except sqlite3.Error as e: - logger.error(f"❌ Error retrieving message history: {e}") - return [] - - def add_to_blacklist(self, entity_type: str, entity_id: int, reason: str, banned_by: int, duration_hours: int = None): - """ - Add a user or guild to the blacklist. - - Parameters - ---------- - entity_type : {'user', 'guild'} - Type of entity to ban - entity_id : int - Discord ID of the user or guild - reason : str - Reason for the ban - banned_by : int - Discord user ID of the moderator issuing the ban - duration_hours : int, optional - Duration in hours (default: None for permanent ban) - - Returns - ------- - bool - True if successful, False otherwise - - Notes - ----- - If duration_hours is None, the ban is permanent. Otherwise, it expires - after the specified duration. - - Examples - -------- - >>> db.add_to_blacklist(entity_type='user', entity_id=123456, - ... reason="Spam", banned_by=789012, duration_hours=24) - >>> db.add_to_blacklist(entity_type='guild', entity_id=345678, - ... reason="Abuse", banned_by=789012) # Permanent - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - expires_at = None - is_permanent = duration_hours is None - - if duration_hours: - expires_at = datetime.now() + timedelta(hours=duration_hours) - - c.execute(""" - INSERT OR REPLACE INTO globalchat_blacklist - (entity_type, entity_id, reason, banned_by, expires_at, is_permanent) - VALUES (?, ?, ?, ?, ?, ?) - """, (entity_type, entity_id, reason, banned_by, expires_at, is_permanent)) - conn.commit() - logger.info(f"🔹 Added to blacklist: {entity_type} {entity_id}") - return True - except sqlite3.Error as e: - logger.error(f"❌ Error adding to blacklist: {e}") - return False - - def remove_from_blacklist(self, entity_type: str, entity_id: int) -> bool: - """ - Remove a user or guild from the blacklist. - - Parameters - ---------- - entity_type : {'user', 'guild'} - Type of entity to unban - entity_id : int - Discord ID of the user or guild - - Returns - ------- - bool - True if entity was removed, False if not found or error occurred - - Examples - -------- - >>> if db.remove_from_blacklist('user', 123456): - ... print("User unbanned successfully") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - c.execute("DELETE FROM globalchat_blacklist WHERE entity_type = ? AND entity_id = ?", (entity_type, entity_id)) - changes = c.rowcount - conn.commit() - - if changes > 0: - logger.info(f"✅ Removed from blacklist: {entity_type} {entity_id}") - return True - return False - except sqlite3.Error as e: - logger.error(f"❌ Error removing from blacklist: {e}") - return False - - def is_blacklisted(self, entity_type: str, entity_id: int) -> bool: - """ - Check if a user or guild is blacklisted. - - Parameters - ---------- - entity_type : {'user', 'guild'} - Type of entity to check - entity_id : int - Discord ID of the user or guild - - Returns - ------- - bool - True if blacklisted, False otherwise - - Notes - ----- - Automatically removes expired temporary bans when checking. - Permanent bans always return True. - - Examples - -------- - >>> if db.is_blacklisted('user', 123456): - ... print("User is banned") - ... else: - ... print("User is not banned") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - c.execute(""" - SELECT expires_at, is_permanent FROM globalchat_blacklist - WHERE entity_type = ? AND entity_id = ? - """, (entity_type, entity_id)) - result = c.fetchone() - - if not result: - return False - - if result['is_permanent']: - return True - - if result['expires_at']: - expires_at = datetime.fromisoformat(result['expires_at']) - if datetime.now() > expires_at: - self.remove_from_blacklist(entity_type, entity_id) - return False - return True - - return False - except sqlite3.Error as e: - logger.error(f"❌ Error checking blacklist: {e}") - return False - - def get_blacklist(self, entity_type: str = None) -> List[Dict]: - """ - Get the complete blacklist or filtered by type. - - Parameters - ---------- - entity_type : {'user', 'guild'}, optional - Type of entities to retrieve (default: None for all) - - Returns - ------- - list of dict - List of blacklist entries. Each dictionary contains: - id, entity_type, entity_id, reason, banned_by, banned_at, - expires_at, is_permanent - - Examples - -------- - >>> all_bans = db.get_blacklist() - >>> user_bans = db.get_blacklist(entity_type='user') - >>> for ban in user_bans: - ... print(f"User {ban['entity_id']}: {ban['reason']}") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - if entity_type: - c.execute("SELECT * FROM globalchat_blacklist WHERE entity_type = ?", (entity_type,)) - else: - c.execute("SELECT * FROM globalchat_blacklist") - return [dict(row) for row in c.fetchall()] - except sqlite3.Error as e: - logger.error(f"❌ Error retrieving blacklist: {e}") - return [] - - def get_guild_settings(self, guild_id: int) -> Dict: - """ - Get settings for a guild. - - Parameters - ---------- - guild_id : int - Discord guild ID - - Returns - ------- - dict - Dictionary containing guild settings. If no custom settings exist, - returns default settings. Keys: guild_id, filter_enabled, nsfw_filter, - embed_color, custom_webhook_name, max_message_length - - Examples - -------- - >>> settings = db.get_guild_settings(123456) - >>> print(f"Filter enabled: {settings['filter_enabled']}") - >>> print(f"Embed color: {settings['embed_color']}") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - c.execute("SELECT * FROM guild_settings WHERE guild_id = ?", (guild_id,)) - result = c.fetchone() - - if result: - return dict(result) - else: - return { - 'guild_id': guild_id, - 'filter_enabled': True, - 'nsfw_filter': True, - 'embed_color': '#5865F2', - 'custom_webhook_name': None, - 'max_message_length': 1900 - } - except sqlite3.Error as e: - logger.error(f"❌ Error retrieving guild settings: {e}") - return {} - - def update_guild_setting(self, guild_id: int, setting_name: str, value) -> bool: - """ - Update a guild setting. - - Parameters - ---------- - guild_id : int - Discord guild ID - setting_name : str - Name of the setting to update (must match column name) - value : Any - New value for the setting - - Returns - ------- - bool - True if successful, False otherwise - - Notes - ----- - Creates guild_settings entry if it doesn't exist. - Valid setting names: filter_enabled, nsfw_filter, embed_color, - custom_webhook_name, max_message_length - - Examples - -------- - >>> db.update_guild_setting(123456, 'filter_enabled', False) - >>> db.update_guild_setting(123456, 'embed_color', '#FF5733') - >>> db.update_guild_setting(123456, 'max_message_length', 2000) - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - c.execute("SELECT guild_id FROM guild_settings WHERE guild_id = ?", (guild_id,)) - if not c.fetchone(): - c.execute("INSERT INTO guild_settings (guild_id) VALUES (?)", (guild_id,)) - - c.execute(f"UPDATE guild_settings SET {setting_name} = ? WHERE guild_id = ?", (value, guild_id)) - conn.commit() - logger.debug(f"⚙ Setting updated: {setting_name} = {value} for Guild {guild_id}") - return True - except sqlite3.Error as e: - logger.error(f"❌ Error updating guild settings: {e}") - return False - - def get_global_stats(self) -> Dict: - """ - Get global statistics. - - Returns - ------- - dict - Dictionary containing global statistics. Keys: active_guilds, - total_messages, today_messages, banned_users, banned_guilds - - Notes - ----- - Returns empty dict if an error occurs. - - Examples - -------- - >>> stats = db.get_global_stats() - >>> print(f"Active guilds: {stats['active_guilds']}") - >>> print(f"Total messages: {stats['total_messages']}") - >>> print(f"Banned users: {stats['banned_users']}") - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - - if self._column_exists('globalchat_channels', 'is_active'): - c.execute("SELECT COUNT(*) as count FROM globalchat_channels WHERE is_active = 1") - else: - c.execute("SELECT COUNT(*) as count FROM globalchat_channels") - active_guilds = c.fetchone()['count'] - - c.execute("SELECT total_messages FROM daily_stats WHERE date = DATE('now')") - today_messages = c.fetchone() - today_messages = today_messages['total_messages'] if today_messages else 0 - - if self._column_exists('globalchat_channels', 'message_count'): - c.execute("SELECT SUM(message_count) as total FROM globalchat_channels") - total_messages = c.fetchone()['total'] or 0 - else: - total_messages = 0 - - c.execute("SELECT COUNT(*) as count FROM globalchat_blacklist WHERE entity_type = 'user'") - banned_users = c.fetchone()['count'] - - c.execute("SELECT COUNT(*) as count FROM globalchat_blacklist WHERE entity_type = 'guild'") - banned_guilds = c.fetchone()['count'] - - return { - 'active_guilds': active_guilds, - 'total_messages': total_messages, - 'today_messages': today_messages, - 'banned_users': banned_users, - 'banned_guilds': banned_guilds - } - except sqlite3.Error as e: - logger.error(f"❌ Error retrieving statistics: {e}") - return {} - - def update_daily_stats(self): - """ - Update daily statistics. - - Notes - ----- - Increments the message count for today and updates active guild count. - Creates a new daily_stats entry if one doesn't exist for today. - - Examples - -------- - >>> db.update_daily_stats() # Call after each message - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - c.execute(""" - INSERT OR REPLACE INTO daily_stats - (date, total_messages, active_guilds) - SELECT - DATE('now'), - COALESCE((SELECT total_messages FROM daily_stats WHERE date = DATE('now')), 0) + 1, - (SELECT COUNT(*) FROM globalchat_channels WHERE 1=1) - """) - conn.commit() - except sqlite3.Error as e: - logger.error(f"❌ Error updating daily stats: {e}") - - def cleanup_old_data(self, days: int = 30): - """ - Clean up old data from the database. - - Parameters - ---------- - days : int, optional - Number of days to keep message logs (default: 30) - - Notes - ----- - Performs the following cleanup: - - Removes message logs older than specified days - - Removes expired temporary bans - - Removes daily statistics older than 90 days - - Examples - -------- - >>> db.cleanup_old_data(days=30) # Keep last 30 days - >>> db.cleanup_old_data(days=7) # Keep only last week - """ - try: - with self._get_connection() as conn: - c = conn.cursor() - - c.execute("DELETE FROM message_log WHERE timestamp < datetime('now', '-{} days')".format(days)) - deleted_messages = c.rowcount - - c.execute("DELETE FROM globalchat_blacklist WHERE expires_at < datetime('now') AND is_permanent = 0") - deleted_bans = c.rowcount - - c.execute("DELETE FROM daily_stats WHERE date < date('now', '-90 days')") - deleted_stats = c.rowcount - - conn.commit() - logger.info(f"đŸ§č Cleanup: {deleted_messages} messages, {deleted_bans} bans, {deleted_stats} stats deleted") - except sqlite3.Error as e: - logger.error(f"❌ Error during cleanup: {e}") - -db = GlobalChatDatabase() \ No newline at end of file diff --git a/DevTools/backend/database/lang_db.py b/DevTools/backend/database/lang_db.py deleted file mode 100644 index c1716eb..0000000 --- a/DevTools/backend/database/lang_db.py +++ /dev/null @@ -1,46 +0,0 @@ -# src/database/settings_db.py - -import sqlite3 -import os -from datetime import datetime - -class SettingsDB: - """ - Datenbank-Klasse zur Verwaltung von Benutzer- und Servereinstellungen. - """ - def __init__(self, db_path="data/settings.db"): - self.db_path = db_path - os.makedirs(os.path.dirname(self.db_path), exist_ok=True) - self.conn = sqlite3.connect(self.db_path) - self.cursor = self.conn.cursor() - self.create_tables() - print(f"[{datetime.now().strftime('%H:%M:%S')}] [DATABASE] Settings Database initialized ✓") - - def create_tables(self): - """Erstellt die Benutzereinstellungen-Tabelle, falls sie nicht existiert.""" - self.cursor.execute(""" - CREATE TABLE IF NOT EXISTS user_settings ( - user_id INTEGER PRIMARY KEY, - language TEXT NOT NULL DEFAULT 'en' - ) - """) - self.conn.commit() - - def set_user_language(self, user_id: int, lang_code: str): - """Speichert den Sprachcode fĂŒr einen Benutzer.""" - self.cursor.execute(""" - INSERT OR REPLACE INTO user_settings (user_id, language) - VALUES (?, ?) - """, (user_id, lang_code)) - self.conn.commit() - - def get_user_language(self, user_id: int) -> str: - """Ruft den Sprachcode fĂŒr einen Benutzer ab. Standard: 'en'.""" - self.cursor.execute("SELECT language FROM user_settings WHERE user_id = ?", (user_id,)) - result = self.cursor.fetchone() - - # 'en' als gewĂŒnschter Standard, falls kein Eintrag gefunden wird - return result[0] if result else 'en' - - def close(self): - self.conn.close() \ No newline at end of file diff --git a/DevTools/backend/database/levelsystem_db.py b/DevTools/backend/database/levelsystem_db.py deleted file mode 100644 index d50a505..0000000 --- a/DevTools/backend/database/levelsystem_db.py +++ /dev/null @@ -1,758 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -import sqlite3 -import asyncio -from typing import Optional, List, Tuple, Dict, Any -import os -import logging -import time -from collections import defaultdict -import csv -import io - - -class LevelSystemLogger: - def __init__(self): - self.logger = logging.getLogger('levelsystem') - if not self.logger.handlers: - handler = logging.FileHandler('data/levelsystem.log') - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') - handler.setFormatter(formatter) - self.logger.addHandler(handler) - self.logger.setLevel(logging.INFO) - - def log_level_up(self, user_id: int, guild_id: int, old_level: int, new_level: int): - self.logger.info(f"User {user_id} in guild {guild_id} leveled up: {old_level} -> {new_level}") - - def log_xp_gain(self, user_id: int, guild_id: int, xp_gained: int, total_xp: int): - self.logger.debug(f"User {user_id} in guild {guild_id} gained {xp_gained} XP (total: {total_xp})") - - def log_prestige(self, user_id: int, guild_id: int, old_level: int): - self.logger.info(f"User {user_id} in guild {guild_id} prestiged from level {old_level}") - - -class AntiSpamDetector: - def __init__(self): - self.user_patterns = defaultdict(list) - self.user_messages = defaultdict(list) - - def is_xp_farming(self, user_id: int, message_content: str, timestamp: float) -> bool: - patterns = self.user_patterns[user_id] - - # Cleanup old patterns (Ă€lter als 10 Minuten) - patterns = [(content, ts) for content, ts in patterns if timestamp - ts < 600] - self.user_patterns[user_id] = patterns - - # Gleiche Nachricht in den letzten 5 Nachrichten - recent_messages = [content for content, ts in patterns[-5:]] - if recent_messages.count(message_content) >= 3: - return True - - # Nachricht zu kurz - if len(message_content.strip()) < 3: - return True - - patterns.append((message_content, timestamp)) - return False - - def is_spam(self, user_id: int, current_time: float, max_messages: int = 5, time_window: int = 60) -> bool: - messages = self.user_messages[user_id] - messages = [t for t in messages if current_time - t < time_window] - self.user_messages[user_id] = messages - - if len(messages) >= max_messages: - return True - - messages.append(current_time) - return False - - -class LevelDatabase: - def __init__(self, db_path: str = "data/levelsystem.db"): - self.db_path = db_path - self.logger = LevelSystemLogger() - self.anti_spam = AntiSpamDetector() - - # Cache fĂŒr bessere Performance - self.level_roles_cache = {} - self.enabled_guilds_cache = set() - self.guild_configs_cache = {} - - self.init_db() - self.load_caches() - - def init_db(self): - """Initialisiert die Datenbank und erstellt Tabellen""" - os.makedirs(os.path.dirname(self.db_path), exist_ok=True) - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - # User Levels Tabelle - cursor.execute(''' - CREATE TABLE IF NOT EXISTS user_levels ( - user_id INTEGER, - guild_id INTEGER, - xp INTEGER DEFAULT 0, - level INTEGER DEFAULT 0, - messages INTEGER DEFAULT 0, - last_message REAL DEFAULT 0, - prestige_level INTEGER DEFAULT 0, - total_xp_earned INTEGER DEFAULT 0, - PRIMARY KEY (user_id, guild_id) - ) - ''') - - # Level Roles Tabelle - cursor.execute(''' - CREATE TABLE IF NOT EXISTS level_roles ( - guild_id INTEGER, - level INTEGER, - role_id INTEGER, - is_temporary BOOLEAN DEFAULT FALSE, - duration_hours INTEGER DEFAULT 0, - PRIMARY KEY (guild_id, level, role_id) - ) - ''') - - # Guild Settings Tabelle - cursor.execute(''' - CREATE TABLE IF NOT EXISTS guild_settings ( - guild_id INTEGER PRIMARY KEY, - levelsystem_enabled BOOLEAN DEFAULT TRUE, - min_xp INTEGER DEFAULT 10, - max_xp INTEGER DEFAULT 20, - xp_cooldown INTEGER DEFAULT 30, - level_up_channel INTEGER DEFAULT NULL, - webhook_url TEXT DEFAULT NULL, - prestige_enabled BOOLEAN DEFAULT TRUE, - prestige_min_level INTEGER DEFAULT 50 - ) - ''') - - # Channel Settings Tabelle - cursor.execute(''' - CREATE TABLE IF NOT EXISTS channel_settings ( - guild_id INTEGER, - channel_id INTEGER, - xp_multiplier REAL DEFAULT 1.0, - is_blacklisted BOOLEAN DEFAULT FALSE, - PRIMARY KEY (guild_id, channel_id) - ) - ''') - - # XP Boosts Tabelle - cursor.execute(''' - CREATE TABLE IF NOT EXISTS xp_boosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id INTEGER, - user_id INTEGER, - multiplier REAL, - start_time REAL, - end_time REAL, - is_global BOOLEAN DEFAULT FALSE - ) - ''') - - # Achievements Tabelle - cursor.execute(''' - CREATE TABLE IF NOT EXISTS achievements ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id INTEGER, - user_id INTEGER, - achievement_type TEXT, - achievement_value INTEGER, - earned_at REAL DEFAULT (datetime('now')), - UNIQUE(guild_id, user_id, achievement_type, achievement_value) - ) - ''') - - # Temporary Roles Tabelle - cursor.execute(''' - CREATE TABLE IF NOT EXISTS temporary_roles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id INTEGER, - user_id INTEGER, - role_id INTEGER, - granted_at REAL, - expires_at REAL - ) - ''') - - # Performance-Indizes - cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_guild ON user_levels(user_id, guild_id)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_guild_xp ON user_levels(guild_id, xp DESC)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_level_roles ON level_roles(guild_id, level)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_channel_settings ON channel_settings(guild_id, channel_id)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_xp_boosts ON xp_boosts(guild_id, start_time, end_time)') - - conn.commit() - conn.close() - - def load_caches(self): - """LĂ€dt hĂ€ufig verwendete Daten in den Cache""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - # Level-Rollen Cache laden - cursor.execute('SELECT guild_id, level, role_id FROM level_roles') - for guild_id, level, role_id in cursor.fetchall(): - if guild_id not in self.level_roles_cache: - self.level_roles_cache[guild_id] = {} - self.level_roles_cache[guild_id][level] = role_id - - # Aktivierte Server laden - cursor.execute('SELECT guild_id FROM guild_settings WHERE levelsystem_enabled = TRUE') - self.enabled_guilds_cache = {row[0] for row in cursor.fetchall()} - - # Guild-Konfigurationen laden - cursor.execute('SELECT * FROM guild_settings') - for row in cursor.fetchall(): - guild_id = row[0] - self.guild_configs_cache[guild_id] = { - 'enabled': row[1], - 'min_xp': row[2], - 'max_xp': row[3], - 'cooldown': row[4], - 'level_up_channel': row[5], - 'webhook_url': row[6], - 'prestige_enabled': row[7] if len(row) > 7 else True, - 'prestige_min_level': row[8] if len(row) > 8 else 50 - } - - conn.close() - - def add_xp(self, user_id: int, guild_id: int, xp_amount: int, message_content: str = "") -> Tuple[bool, int]: - """FĂŒgt XP zu einem User hinzu mit Anti-Spam Schutz""" - current_time = time.time() - - # Anti-Spam Check - if self.anti_spam.is_spam(user_id, current_time): - return False, 0 - - if message_content and self.anti_spam.is_xp_farming(user_id, message_content, current_time): - return False, 0 - - # XP-Boost anwenden - xp_amount = int(xp_amount * self.get_active_xp_multiplier(guild_id, user_id)) - - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - SELECT xp, level, messages, total_xp_earned FROM user_levels - WHERE user_id = ? AND guild_id = ? - ''', (user_id, guild_id)) - - result = cursor.fetchone() - - if result: - current_xp, current_level, messages, total_earned = result - new_xp = current_xp + xp_amount - new_level = self.calculate_level(new_xp) - new_total_earned = total_earned + xp_amount - - cursor.execute(''' - UPDATE user_levels - SET xp = ?, level = ?, messages = messages + 1, last_message = ?, total_xp_earned = ? - WHERE user_id = ? AND guild_id = ? - ''', (new_xp, new_level, current_time, new_total_earned, user_id, guild_id)) - - level_up = new_level > current_level - if level_up: - self.logger.log_level_up(user_id, guild_id, current_level, new_level) - else: - new_xp = xp_amount - new_level = self.calculate_level(new_xp) - - cursor.execute(''' - INSERT INTO user_levels (user_id, guild_id, xp, level, messages, last_message, total_xp_earned) - VALUES (?, ?, ?, ?, 1, ?, ?) - ''', (user_id, guild_id, new_xp, new_level, current_time, xp_amount)) - - level_up = new_level > 0 - - conn.commit() - conn.close() - - self.logger.log_xp_gain(user_id, guild_id, xp_amount, new_xp) - - # Achievements prĂŒfen - if level_up: - self.check_achievements(user_id, guild_id, new_level) - - return level_up, new_level - - def batch_add_xp(self, updates: List[Tuple[int, int, int]]): - """FĂŒgt XP fĂŒr mehrere User in einem Batch hinzu""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - current_time = time.time() - - for user_id, guild_id, xp_amount in updates: - cursor.execute(''' - INSERT OR REPLACE INTO user_levels - (user_id, guild_id, xp, level, messages, last_message, total_xp_earned) - VALUES ( - ?, ?, - COALESCE((SELECT xp FROM user_levels WHERE user_id = ? AND guild_id = ?), 0) + ?, - ?, -- Level wird spĂ€ter berechnet - COALESCE((SELECT messages FROM user_levels WHERE user_id = ? AND guild_id = ?), 0) + 1, - ?, - COALESCE((SELECT total_xp_earned FROM user_levels WHERE user_id = ? AND guild_id = ?), 0) + ? - ) - ''', (user_id, guild_id, user_id, guild_id, xp_amount, - self.calculate_level(xp_amount), user_id, guild_id, current_time, - user_id, guild_id, xp_amount)) - - conn.commit() - conn.close() - - def get_user_stats(self, user_id: int, guild_id: int) -> Optional[Tuple[int, int, int, int, int]]: - """Holt erweiterte User-Statistiken""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - SELECT xp, level, messages, prestige_level, total_xp_earned - FROM user_levels - WHERE user_id = ? AND guild_id = ? - ''', (user_id, guild_id)) - - result = cursor.fetchone() - conn.close() - - if result: - xp, level, messages, prestige, total_earned = result - xp_needed = self.xp_for_level(level + 1) - xp - return xp, level, messages, xp_needed, prestige, total_earned - return None - - def get_leaderboard(self, guild_id: int, limit: int = 10) -> List[Tuple[int, int, int, int, int]]: - """Holt die erweiterte Leaderboard fĂŒr einen Server""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - SELECT user_id, xp, level, messages, prestige_level - FROM user_levels - WHERE guild_id = ? - ORDER BY prestige_level DESC, level DESC, xp DESC - LIMIT ? - ''', (guild_id, limit)) - - result = cursor.fetchall() - conn.close() - return result - - def get_detailed_analytics(self, guild_id: int) -> Dict[str, Any]: - """Holt detaillierte Server-Analytics""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - current_time = time.time() - today_start = current_time - (current_time % 86400) # Tagesbeginn - week_start = current_time - (7 * 86400) # Eine Woche zurĂŒck - - analytics = {} - - # Grundlegende Statistiken - cursor.execute(''' - SELECT - COUNT(*) as total_users, - AVG(level) as avg_level, - MAX(level) as max_level, - SUM(xp) as total_xp, - SUM(messages) as total_messages - FROM user_levels WHERE guild_id = ? - ''', (guild_id,)) - - result = cursor.fetchone() - if result: - analytics.update({ - 'total_users': result[0], - 'avg_level': result[1] or 0, - 'max_level': result[2] or 0, - 'total_xp': result[3] or 0, - 'total_messages': result[4] or 0 - }) - - # AktivitĂ€t heute (basierend auf last_message) - cursor.execute(''' - SELECT COUNT(*) FROM user_levels - WHERE guild_id = ? AND last_message > ? - ''', (guild_id, today_start)) - - analytics['active_today'] = cursor.fetchone()[0] - - # XP-Verteilung - cursor.execute(''' - SELECT - COUNT(CASE WHEN level BETWEEN 1 AND 10 THEN 1 END) as novice, - COUNT(CASE WHEN level BETWEEN 11 AND 25 THEN 1 END) as intermediate, - COUNT(CASE WHEN level BETWEEN 26 AND 50 THEN 1 END) as advanced, - COUNT(CASE WHEN level > 50 THEN 1 END) as expert - FROM user_levels WHERE guild_id = ? - ''', (guild_id,)) - - level_distribution = cursor.fetchone() - analytics['level_distribution'] = { - 'novice': level_distribution[0], - 'intermediate': level_distribution[1], - 'advanced': level_distribution[2], - 'expert': level_distribution[3] - } - - conn.close() - return analytics - - def set_guild_config(self, guild_id: int, **config): - """Setzt Guild-spezifische Konfiguration""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - # Aktuelle Konfiguration holen - cursor.execute('SELECT * FROM guild_settings WHERE guild_id = ?', (guild_id,)) - current = cursor.fetchone() - - if current: - # Update bestehende Konfiguration - set_clauses = [] - values = [] - for key, value in config.items(): - set_clauses.append(f"{key} = ?") - values.append(value) - values.append(guild_id) - - query = f"UPDATE guild_settings SET {', '.join(set_clauses)} WHERE guild_id = ?" - cursor.execute(query, values) - else: - # Neue Konfiguration erstellen - keys = list(config.keys()) + ['guild_id'] - values = list(config.values()) + [guild_id] - placeholders = ', '.join(['?'] * len(keys)) - - query = f"INSERT INTO guild_settings ({', '.join(keys)}) VALUES ({placeholders})" - cursor.execute(query, values) - - conn.commit() - conn.close() - - # Cache aktualisieren - if guild_id not in self.guild_configs_cache: - self.guild_configs_cache[guild_id] = {} - self.guild_configs_cache[guild_id].update(config) - - def get_guild_config(self, guild_id: int) -> Dict[str, Any]: - """Holt Guild-Konfiguration""" - if guild_id in self.guild_configs_cache: - return self.guild_configs_cache[guild_id] - - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute('SELECT * FROM guild_settings WHERE guild_id = ?', (guild_id,)) - result = cursor.fetchone() - conn.close() - - if result: - config = { - 'enabled': result[1], - 'min_xp': result[2], - 'max_xp': result[3], - 'cooldown': result[4], - 'level_up_channel': result[5], - 'webhook_url': result[6], - 'prestige_enabled': result[7] if len(result) > 7 else True, - 'prestige_min_level': result[8] if len(result) > 8 else 50 - } - else: - config = { - 'enabled': True, - 'min_xp': 10, - 'max_xp': 20, - 'cooldown': 30, - 'level_up_channel': None, - 'webhook_url': None, - 'prestige_enabled': True, - 'prestige_min_level': 50 - } - - self.guild_configs_cache[guild_id] = config - return config - - def set_channel_multiplier(self, guild_id: int, channel_id: int, multiplier: float): - """Setzt XP-Multiplikator fĂŒr einen Kanal""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - INSERT OR REPLACE INTO channel_settings (guild_id, channel_id, xp_multiplier) - VALUES (?, ?, ?) - ''', (guild_id, channel_id, multiplier)) - - conn.commit() - conn.close() - - def add_blacklisted_channel(self, guild_id: int, channel_id: int): - """FĂŒgt einen Kanal zur Blacklist hinzu""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - INSERT OR REPLACE INTO channel_settings (guild_id, channel_id, is_blacklisted) - VALUES (?, ?, TRUE) - ''', (guild_id, channel_id)) - - conn.commit() - conn.close() - - def is_channel_blacklisted(self, guild_id: int, channel_id: int) -> bool: - """PrĂŒft ob ein Kanal auf der Blacklist steht""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - SELECT is_blacklisted FROM channel_settings - WHERE guild_id = ? AND channel_id = ? - ''', (guild_id, channel_id)) - - result = cursor.fetchone() - conn.close() - return result[0] if result else False - - def get_channel_multiplier(self, guild_id: int, channel_id: int) -> float: - """Holt den XP-Multiplikator fĂŒr einen Kanal""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - SELECT xp_multiplier FROM channel_settings - WHERE guild_id = ? AND channel_id = ? - ''', (guild_id, channel_id)) - - result = cursor.fetchone() - conn.close() - return result[0] if result else 1.0 - - def add_xp_boost(self, guild_id: int, user_id: Optional[int], multiplier: float, duration_hours: int): - """FĂŒgt einen XP-Boost hinzu""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - current_time = time.time() - end_time = current_time + (duration_hours * 3600) - is_global = user_id is None - - cursor.execute(''' - INSERT INTO xp_boosts (guild_id, user_id, multiplier, start_time, end_time, is_global) - VALUES (?, ?, ?, ?, ?, ?) - ''', (guild_id, user_id, multiplier, current_time, end_time, is_global)) - - conn.commit() - conn.close() - - def get_active_xp_multiplier(self, guild_id: int, user_id: int) -> float: - """Holt den aktuell aktiven XP-Multiplikator fĂŒr einen User""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - current_time = time.time() - - cursor.execute(''' - SELECT multiplier FROM xp_boosts - WHERE guild_id = ? AND (user_id = ? OR is_global = TRUE) - AND start_time <= ? AND end_time > ? - ORDER BY multiplier DESC LIMIT 1 - ''', (guild_id, user_id, current_time, current_time)) - - result = cursor.fetchone() - conn.close() - return result[0] if result else 1.0 - - def prestige_user(self, user_id: int, guild_id: int) -> bool: - """FĂŒhrt ein Prestige fĂŒr einen User durch""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - SELECT level, prestige_level FROM user_levels - WHERE user_id = ? AND guild_id = ? - ''', (user_id, guild_id)) - - result = cursor.fetchone() - if not result or result[0] < self.get_guild_config(guild_id)['prestige_min_level']: - conn.close() - return False - - old_level, current_prestige = result - - cursor.execute(''' - UPDATE user_levels - SET level = 0, xp = 0, prestige_level = prestige_level + 1 - WHERE user_id = ? AND guild_id = ? - ''', (user_id, guild_id)) - - conn.commit() - conn.close() - - self.logger.log_prestige(user_id, guild_id, old_level) - return True - - def check_achievements(self, user_id: int, guild_id: int, level: int): - """PrĂŒft und verleiht Achievements""" - achievements_to_grant = [] - - # Level-basierte Achievements - milestone_levels = [10, 25, 50, 75, 100] - for milestone in milestone_levels: - if level >= milestone: - achievements_to_grant.append(('level_milestone', milestone)) - - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - for achievement_type, value in achievements_to_grant: - cursor.execute(''' - INSERT OR IGNORE INTO achievements (guild_id, user_id, achievement_type, achievement_value) - VALUES (?, ?, ?, ?) - ''', (guild_id, user_id, achievement_type, value)) - - conn.commit() - conn.close() - - def export_guild_data(self, guild_id: int) -> List[Tuple]: - """Exportiert alle Guild-Daten""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - SELECT user_id, xp, level, messages, prestige_level, total_xp_earned - FROM user_levels WHERE guild_id = ? - ORDER BY prestige_level DESC, level DESC, xp DESC - ''', (guild_id,)) - - result = cursor.fetchall() - conn.close() - return result - - def get_user_rank(self, user_id: int, guild_id: int) -> int: - """Holt den Rang eines Users auf dem Server""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - SELECT COUNT(*) + 1 as rank - FROM user_levels u1 - WHERE u1.guild_id = ? AND ( - u1.prestige_level > (SELECT prestige_level FROM user_levels WHERE user_id = ? AND guild_id = ?) OR - (u1.prestige_level = (SELECT prestige_level FROM user_levels WHERE user_id = ? AND guild_id = ?) AND - u1.level > (SELECT level FROM user_levels WHERE user_id = ? AND guild_id = ?)) OR - (u1.prestige_level = (SELECT prestige_level FROM user_levels WHERE user_id = ? AND guild_id = ?) AND - u1.level = (SELECT level FROM user_levels WHERE user_id = ? AND guild_id = ?) AND - u1.xp > (SELECT xp FROM user_levels WHERE user_id = ? AND guild_id = ?)) - ) - ''', (guild_id, user_id, guild_id, user_id, guild_id, user_id, guild_id, - user_id, guild_id, user_id, guild_id, user_id, guild_id)) - - result = cursor.fetchone() - conn.close() - return result[0] if result else 0 - - def add_level_role(self, guild_id: int, level: int, role_id: int, is_temporary: bool = False, duration_hours: int = 0): - """FĂŒgt eine Level-Rolle hinzu""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - INSERT OR REPLACE INTO level_roles (guild_id, level, role_id, is_temporary, duration_hours) - VALUES (?, ?, ?, ?, ?) - ''', (guild_id, level, role_id, is_temporary, duration_hours)) - - conn.commit() - conn.close() - - # Cache aktualisieren - if guild_id not in self.level_roles_cache: - self.level_roles_cache[guild_id] = {} - self.level_roles_cache[guild_id][level] = role_id - - def remove_level_role(self, guild_id: int, level: int): - """Entfernt eine Level-Rolle""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - DELETE FROM level_roles - WHERE guild_id = ? AND level = ? - ''', (guild_id, level)) - - conn.commit() - conn.close() - - # Cache aktualisieren - if guild_id in self.level_roles_cache and level in self.level_roles_cache[guild_id]: - del self.level_roles_cache[guild_id][level] - - def get_level_roles(self, guild_id: int) -> List[Tuple[int, int, bool, int]]: - """Holt alle Level-Rollen fĂŒr einen Server""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - SELECT level, role_id, is_temporary, duration_hours FROM level_roles - WHERE guild_id = ? - ORDER BY level ASC - ''', (guild_id,)) - - result = cursor.fetchall() - conn.close() - return result - - def get_role_for_level(self, guild_id: int, level: int) -> Optional[int]: - """Holt die Rolle fĂŒr ein bestimmtes Level aus Cache""" - if guild_id in self.level_roles_cache: - # Finde die höchste Rolle <= level - applicable_roles = {l: r for l, r in self.level_roles_cache[guild_id].items() if l <= level} - if applicable_roles: - highest_level = max(applicable_roles.keys()) - return applicable_roles[highest_level] - return None - - def set_levelsystem_enabled(self, guild_id: int, enabled: bool): - """Aktiviert/Deaktiviert das Levelsystem fĂŒr einen Server""" - self.set_guild_config(guild_id, levelsystem_enabled=enabled) - - # Cache aktualisieren - if enabled: - self.enabled_guilds_cache.add(guild_id) - else: - self.enabled_guilds_cache.discard(guild_id) - - def is_levelsystem_enabled(self, guild_id: int) -> bool: - """PrĂŒft ob das Levelsystem fĂŒr einen Server aktiviert ist (aus Cache)""" - if guild_id in self.enabled_guilds_cache: - return True - - # Fallback zur Datenbank wenn nicht im Cache - config = self.get_guild_config(guild_id) - enabled = config.get('enabled', True) - - if enabled: - self.enabled_guilds_cache.add(guild_id) - - return enabled - - @staticmethod - def calculate_level(xp: int) -> int: - """Berechnet das Level basierend auf XP""" - level = 0 - while xp >= LevelDatabase.xp_for_level(level + 1): - level += 1 - return level - - @staticmethod - def xp_for_level(level: int) -> int: - """Berechnet die benötigten XP fĂŒr ein Level""" - if level == 0: - return 0 - return int(100 * (level ** 1.5)) diff --git a/DevTools/backend/database/logging_db.py b/DevTools/backend/database/logging_db.py deleted file mode 100644 index 6abe517..0000000 --- a/DevTools/backend/database/logging_db.py +++ /dev/null @@ -1,413 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -# File: logging_database.py - -import sqlite3 -import asyncio -import os -from typing import Optional, Dict, List -import threading -import logging - -# Setup logging -logger = logging.getLogger(__name__) - -class LoggingDatabase: - """ - Improved database class for Discord logging system - Handles all database operations for log channel configurations - """ - - def __init__(self, db_path: str = "data/log_channels.db"): - self.db_path = db_path - self._lock = threading.Lock() - self._ensure_directory() - self.init_db() - - def _ensure_directory(self): - """Stellt sicher, dass das data/ Verzeichnis existiert""" - directory = os.path.dirname(self.db_path) - if directory and not os.path.exists(directory): - os.makedirs(directory) - - def init_db(self): - """Erstellt die Tabelle fĂŒr Log-Channels mit verbesserter Struktur""" - try: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - - # Neue Tabelle mit separaten EintrĂ€gen fĂŒr verschiedene Log-Typen - cursor.execute(''' - CREATE TABLE IF NOT EXISTS log_channels ( - guild_id INTEGER NOT NULL, - log_type TEXT NOT NULL, - channel_id INTEGER NOT NULL, - enabled BOOLEAN DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (guild_id, log_type) - ) - ''') - - # Index fĂŒr bessere Performance - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_guild_enabled - ON log_channels (guild_id, enabled) - ''') - - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_channel_id - ON log_channels (channel_id) - ''') - - # Migration von alter Struktur falls nötig - cursor.execute("PRAGMA table_info(log_channels)") - columns = [column[1] for column in cursor.fetchall()] - - if 'log_type' not in columns: - logger.info("Migrating old database structure...") - # Backup der alten Daten - cursor.execute(''' - CREATE TABLE IF NOT EXISTS log_channels_backup AS - SELECT * FROM log_channels - ''') - - # Neue Struktur erstellen - cursor.execute('DROP TABLE log_channels') - cursor.execute(''' - CREATE TABLE log_channels ( - guild_id INTEGER NOT NULL, - log_type TEXT NOT NULL DEFAULT 'general', - channel_id INTEGER NOT NULL, - enabled BOOLEAN DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (guild_id, log_type) - ) - ''') - - # Alte Daten migrieren - cursor.execute(''' - INSERT INTO log_channels (guild_id, log_type, channel_id, enabled) - SELECT guild_id, 'general', channel_id, enabled - FROM log_channels_backup - ''') - - cursor.execute('DROP TABLE log_channels_backup') - logger.info("Database migration completed successfully") - - conn.commit() - logger.info("Database initialized successfully") - - except Exception as e: - logger.error(f"Database initialization error: {e}") - raise - - async def set_log_channel(self, guild_id: int, channel_id: int, log_type: str = 'general'): - """Setzt den Log-Channel fĂŒr einen bestimmten Log-Typ""" - def _insert(): - try: - with self._lock: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - cursor.execute(''' - INSERT OR REPLACE INTO log_channels - (guild_id, log_type, channel_id, enabled, updated_at) - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) - ''', (guild_id, log_type, channel_id, True)) - conn.commit() - logger.debug(f"Set log channel: Guild {guild_id}, Type {log_type}, Channel {channel_id}") - except Exception as e: - logger.error(f"Error setting log channel: {e}") - raise - - await asyncio.get_event_loop().run_in_executor(None, _insert) - - async def get_log_channel(self, guild_id: int, log_type: str = 'general') -> Optional[int]: - """Holt die Channel-ID fĂŒr einen Server und Log-Typ""" - def _select(): - try: - with self._lock: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - cursor.execute(''' - SELECT channel_id FROM log_channels - WHERE guild_id = ? AND log_type = ? AND enabled = 1 - ''', (guild_id, log_type)) - row = cursor.fetchone() - result = row[0] if row else None - logger.debug(f"Get log channel: Guild {guild_id}, Type {log_type} -> {result}") - return result - except Exception as e: - logger.error(f"Error getting log channel: {e}") - return None - - return await asyncio.get_event_loop().run_in_executor(None, _select) - - async def get_all_log_channels(self, guild_id: int) -> Dict[str, int]: - """Holt alle konfigurierten Log-Channels fĂŒr einen Server""" - def _select_all(): - try: - with self._lock: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - cursor.execute(''' - SELECT log_type, channel_id FROM log_channels - WHERE guild_id = ? AND enabled = 1 - ORDER BY log_type - ''', (guild_id,)) - result = dict(cursor.fetchall()) - logger.debug(f"Get all log channels for guild {guild_id}: {len(result)} types") - return result - except Exception as e: - logger.error(f"Error getting all log channels: {e}") - return {} - - return await asyncio.get_event_loop().run_in_executor(None, _select_all) - - async def remove_log_channel(self, guild_id: int, log_type: str = None): - """Entfernt Log-Channel(s) fĂŒr einen Server""" - def _delete(): - try: - with self._lock: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - if log_type: - cursor.execute(''' - DELETE FROM log_channels - WHERE guild_id = ? AND log_type = ? - ''', (guild_id, log_type)) - logger.debug(f"Removed log channel: Guild {guild_id}, Type {log_type}") - else: - cursor.execute(''' - DELETE FROM log_channels WHERE guild_id = ? - ''', (guild_id,)) - logger.debug(f"Removed all log channels for guild {guild_id}") - - deleted_count = cursor.rowcount - conn.commit() - return deleted_count - except Exception as e: - logger.error(f"Error removing log channel: {e}") - return 0 - - return await asyncio.get_event_loop().run_in_executor(None, _delete) - - async def remove_all_log_channels(self, guild_id: int): - """Entfernt alle Log-Channels fĂŒr einen Server""" - return await self.remove_log_channel(guild_id) - - async def disable_logging(self, guild_id: int, log_type: str = None): - """Deaktiviert das Logging fĂŒr einen Server (alle Typen oder spezifischen Typ)""" - def _update(): - try: - with self._lock: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - if log_type: - cursor.execute(''' - UPDATE log_channels SET enabled = 0 - WHERE guild_id = ? AND log_type = ? - ''', (guild_id, log_type)) - else: - cursor.execute(''' - UPDATE log_channels SET enabled = 0 WHERE guild_id = ? - ''', (guild_id,)) - updated_count = cursor.rowcount - conn.commit() - logger.debug(f"Disabled logging: Guild {guild_id}, Type {log_type}, Count {updated_count}") - return updated_count - except Exception as e: - logger.error(f"Error disabling logging: {e}") - return 0 - - return await asyncio.get_event_loop().run_in_executor(None, _update) - - async def enable_logging(self, guild_id: int, log_type: str = None): - """Aktiviert das Logging fĂŒr einen Server (alle Typen oder spezifischen Typ)""" - def _update(): - try: - with self._lock: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - if log_type: - cursor.execute(''' - UPDATE log_channels SET enabled = 1 - WHERE guild_id = ? AND log_type = ? - ''', (guild_id, log_type)) - else: - cursor.execute(''' - UPDATE log_channels SET enabled = 1 WHERE guild_id = ? - ''', (guild_id,)) - updated_count = cursor.rowcount - conn.commit() - logger.debug(f"Enabled logging: Guild {guild_id}, Type {log_type}, Count {updated_count}") - return updated_count - except Exception as e: - logger.error(f"Error enabling logging: {e}") - return 0 - - return await asyncio.get_event_loop().run_in_executor(None, _update) - - async def channel_exists(self, guild_id: int, log_type: str) -> bool: - """PrĂŒft ob ein Log-Channel fĂŒr den Typ existiert""" - def _exists(): - try: - with self._lock: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - cursor.execute(''' - SELECT 1 FROM log_channels - WHERE guild_id = ? AND log_type = ? - ''', (guild_id, log_type)) - return cursor.fetchone() is not None - except Exception as e: - logger.error(f"Error checking channel existence: {e}") - return False - - return await asyncio.get_event_loop().run_in_executor(None, _exists) - - async def get_guilds_with_logging(self) -> List[int]: - """Holt alle Guild-IDs die Logging aktiviert haben""" - def _select_guilds(): - try: - with self._lock: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - cursor.execute(''' - SELECT DISTINCT guild_id FROM log_channels WHERE enabled = 1 - ''') - result = [row[0] for row in cursor.fetchall()] - logger.debug(f"Found {len(result)} guilds with logging enabled") - return result - except Exception as e: - logger.error(f"Error getting guilds with logging: {e}") - return [] - - return await asyncio.get_event_loop().run_in_executor(None, _select_guilds) - - async def get_channels_by_guild(self, guild_id: int) -> List[Dict]: - """Holt detaillierte Channel-Info fĂŒr eine Guild""" - def _select_detailed(): - try: - with self._lock: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - cursor.execute(''' - SELECT log_type, channel_id, enabled, created_at, updated_at - FROM log_channels - WHERE guild_id = ? - ORDER BY log_type - ''', (guild_id,)) - - columns = ['log_type', 'channel_id', 'enabled', 'created_at', 'updated_at'] - return [dict(zip(columns, row)) for row in cursor.fetchall()] - except Exception as e: - logger.error(f"Error getting detailed channels: {e}") - return [] - - return await asyncio.get_event_loop().run_in_executor(None, _select_detailed) - - async def cleanup_invalid_channels(self, valid_channel_ids: set): - """Entfernt ungĂŒltige Channel-IDs aus der Datenbank""" - def _cleanup(): - try: - with self._lock: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - - if valid_channel_ids: - placeholders = ','.join('?' * len(valid_channel_ids)) - cursor.execute(f''' - DELETE FROM log_channels - WHERE channel_id NOT IN ({placeholders}) - ''', list(valid_channel_ids)) - else: - # Wenn keine gĂŒltigen Channels vorhanden, alle löschen - cursor.execute('DELETE FROM log_channels') - - deleted = cursor.rowcount - conn.commit() - - if deleted > 0: - logger.info(f"Cleaned up {deleted} invalid channel entries") - - return deleted - except Exception as e: - logger.error(f"Error cleaning up channels: {e}") - return 0 - - return await asyncio.get_event_loop().run_in_executor(None, _cleanup) - - async def get_statistics(self) -> Dict: - """Holt Datenbankstatistiken""" - def _get_stats(): - try: - with self._lock: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - - stats = {} - - # Total entries - cursor.execute('SELECT COUNT(*) FROM log_channels') - stats['total_entries'] = cursor.fetchone()[0] - - # Enabled entries - cursor.execute('SELECT COUNT(*) FROM log_channels WHERE enabled = 1') - stats['enabled_entries'] = cursor.fetchone()[0] - - # Unique guilds - cursor.execute('SELECT COUNT(DISTINCT guild_id) FROM log_channels') - stats['unique_guilds'] = cursor.fetchone()[0] - - # Unique channels - cursor.execute('SELECT COUNT(DISTINCT channel_id) FROM log_channels') - stats['unique_channels'] = cursor.fetchone()[0] - - # Log types distribution - cursor.execute(''' - SELECT log_type, COUNT(*) FROM log_channels - WHERE enabled = 1 - GROUP BY log_type - ''') - stats['log_types'] = dict(cursor.fetchall()) - - return stats - except Exception as e: - logger.error(f"Error getting statistics: {e}") - return {} - - return await asyncio.get_event_loop().run_in_executor(None, _get_stats) - - async def backup_database(self, backup_path: str = None) -> bool: - """Erstellt ein Backup der Datenbank""" - if not backup_path: - backup_path = f"{self.db_path}.backup" - - def _backup(): - try: - with self._lock: - # Quelle öffnen - with sqlite3.connect(self.db_path) as source: - # Ziel erstellen - with sqlite3.connect(backup_path) as target: - source.backup(target) - - logger.info(f"Database backup created: {backup_path}") - return True - except Exception as e: - logger.error(f"Error creating backup: {e}") - return False - - return await asyncio.get_event_loop().run_in_executor(None, _backup) - - def close(self): - """Cleanup-Methode fĂŒr ordnungsgemĂ€ĂŸe Schließung""" - logger.debug("Database connection closed") - pass - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() \ No newline at end of file diff --git a/DevTools/backend/database/notes_db.py b/DevTools/backend/database/notes_db.py deleted file mode 100644 index 16736e4..0000000 --- a/DevTools/backend/database/notes_db.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -import sqlite3 -import os - -from colorama import Fore, Style - -class NotesDatabase: - def __init__(self, base_path): - db_path = os.path.join(base_path, "data", "notes.db") - os.makedirs(os.path.dirname(db_path), exist_ok=True) - - self.conn = sqlite3.connect(db_path) - self.cursor = self.conn.cursor() - self.cursor.execute(""" - CREATE TABLE IF NOT EXISTS notes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id INTEGER, - user_id INTEGER, - author_id INTEGER, - author_name TEXT, - note TEXT, - timestamp TEXT - ) - """) - self.conn.commit() - - def add_note(self, guild_id, user_id, author_id, author_name, note, timestamp): - self.cursor.execute( - "INSERT INTO notes (guild_id, user_id, author_id, author_name, note, timestamp) VALUES (?, ?, ?, ?, ?, ?)", - (guild_id, user_id, author_id, author_name, note, timestamp) - ) - self.conn.commit() - - def get_notes(self, guild_id, user_id): - self.cursor.execute( - "SELECT id, note, timestamp, author_name FROM notes WHERE guild_id = ? AND user_id = ?", - (guild_id, user_id) - ) - rows = self.cursor.fetchall() - return [ - {"id": row[0], "content": row[1], "timestamp": row[2], "author_name": row[3]} - for row in rows - ] - - def delete_note(self, note_id): - self.cursor.execute("DELETE FROM notes WHERE id = ?", (note_id,)) - self.conn.commit() - return self.cursor.rowcount > 0 - - def get_note_by_id(self, note_id): - self.cursor.execute("SELECT * FROM notes WHERE id = ?", (note_id,)) - return self.cursor.fetchone() - - def close(self): - self.conn.close() diff --git a/DevTools/backend/database/spam_db.py b/DevTools/backend/database/spam_db.py deleted file mode 100644 index bcc4623..0000000 --- a/DevTools/backend/database/spam_db.py +++ /dev/null @@ -1,494 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -import sqlite3 -import os -import logging -from datetime import datetime, timedelta -from typing import Optional, Dict, List, Tuple -from contextlib import contextmanager - -from colorama import Fore, Style - - -class SpamDBError(Exception): - """Custom exception for SpamDB errors""" - pass - - -class SpamDB: - def __init__(self, db_path='data/spam.db'): - """Initialize spam database with enhanced error handling and logging.""" - self.db_path = db_path - self.logger = logging.getLogger(__name__) - - try: - # Ensure data directory exists - os.makedirs(os.path.dirname(self.db_path), exist_ok=True) - self.conn = sqlite3.connect(self.db_path, check_same_thread=False) - self.conn.row_factory = sqlite3.Row # Enable dict-like access - self.create_tables() - self._migrate_database() # Add migration step - self._init_database() - except Exception as e: - self.logger.error(f"Failed to initialize database: {e}") - raise SpamDBError(f"Database initialization failed: {e}") - - @contextmanager - def get_cursor(self): - """Context manager for database operations with proper error handling.""" - cursor = self.conn.cursor() - try: - yield cursor - except sqlite3.Error as e: - self.conn.rollback() - self.logger.error(f"Database error: {e}") - raise SpamDBError(f"Database operation failed: {e}") - finally: - cursor.close() - - def _get_table_columns(self, table_name: str) -> List[str]: - """Get list of columns for a table.""" - with self.get_cursor() as cursor: - cursor.execute(f"PRAGMA table_info({table_name})") - return [row[1] for row in cursor.fetchall()] - - def _migrate_database(self): - """Handle database migrations for schema changes.""" - try: - tables = [table[0] for table in self._get_tables()] - - # Migrate spam_settings table - if 'spam_settings' in tables: - spam_settings_columns = self._get_table_columns('spam_settings') - if 'created_at' not in spam_settings_columns: - with self.get_cursor() as cursor: - cursor.execute( - 'ALTER TABLE spam_settings ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP') - - if 'updated_at' not in spam_settings_columns: - with self.get_cursor() as cursor: - cursor.execute( - 'ALTER TABLE spam_settings ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP') - - # Migrate spam_logs table - if 'spam_logs' in tables: - spam_logs_columns = self._get_table_columns('spam_logs') - if 'message_count' not in spam_logs_columns: - with self.get_cursor() as cursor: - cursor.execute('ALTER TABLE spam_logs ADD COLUMN message_count INTEGER DEFAULT 1') - - # Migrate spam_whitelist table - if 'spam_whitelist' in tables: - whitelist_columns = self._get_table_columns('spam_whitelist') - if 'added_by' not in whitelist_columns: - with self.get_cursor() as cursor: - cursor.execute('ALTER TABLE spam_whitelist ADD COLUMN added_by INTEGER') - - if 'added_at' not in whitelist_columns: - with self.get_cursor() as cursor: - cursor.execute( - 'ALTER TABLE spam_whitelist ADD COLUMN added_at DATETIME DEFAULT CURRENT_TIMESTAMP') - - if 'reason' not in whitelist_columns: - with self.get_cursor() as cursor: - cursor.execute('ALTER TABLE spam_whitelist ADD COLUMN reason TEXT') - - self.conn.commit() - self.logger.info("Database migration completed successfully") - - except Exception as e: - self.logger.error(f"Database migration failed: {e}") - # Don't raise here - let the application continue with basic functionality - - def _get_tables(self) -> List[Tuple]: - """Get list of all tables in the database.""" - with self.get_cursor() as cursor: - cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") - return cursor.fetchall() - - def create_tables(self): - """Create all necessary tables with improved schema.""" - with self.get_cursor() as cursor: - # Spam settings table with better constraints - cursor.execute(''' - CREATE TABLE IF NOT EXISTS spam_settings ( - guild_id INTEGER PRIMARY KEY, - max_messages INTEGER DEFAULT 5 CHECK (max_messages > 0), - time_frame INTEGER DEFAULT 10 CHECK (time_frame > 0), - log_channel_id INTEGER - ) - ''') - - # Spam logs with better indexing - cursor.execute(''' - CREATE TABLE IF NOT EXISTS spam_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - message TEXT NOT NULL, - message_count INTEGER DEFAULT 1, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (guild_id) REFERENCES spam_settings(guild_id) - ) - ''') - - # Whitelist with better constraints - cursor.execute(''' - CREATE TABLE IF NOT EXISTS spam_whitelist ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - UNIQUE(guild_id, user_id) - ) - ''') - - # Create indexes for better performance - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_spam_logs_guild_timestamp - ON spam_logs(guild_id, timestamp) - ''') - - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_spam_logs_user_timestamp - ON spam_logs(user_id, timestamp) - ''') - - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_whitelist_guild_user - ON spam_whitelist(guild_id, user_id) - ''') - - self.conn.commit() - - def _init_database(self): - """Initialize database with any required default data.""" - # Add any initialization logic here if needed - pass - - def set_spam_settings(self, guild_id: int, max_messages: int = 5, - time_frame: int = 10, log_channel_id: Optional[int] = None) -> bool: - """Set spam detection settings for a guild with validation.""" - if max_messages <= 0 or time_frame <= 0: - raise SpamDBError("max_messages and time_frame must be positive integers") - - with self.get_cursor() as cursor: - # Check if updated_at column exists - columns = self._get_table_columns('spam_settings') - - if 'updated_at' in columns: - cursor.execute(''' - INSERT OR REPLACE INTO spam_settings - (guild_id, max_messages, time_frame, log_channel_id, updated_at) - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) - ''', (guild_id, max_messages, time_frame, log_channel_id)) - else: - # Fallback for tables without updated_at column - cursor.execute(''' - INSERT OR REPLACE INTO spam_settings - (guild_id, max_messages, time_frame, log_channel_id) - VALUES (?, ?, ?, ?) - ''', (guild_id, max_messages, time_frame, log_channel_id)) - - self.conn.commit() - return True - - def set_log_channel(self, guild_id: int, channel_id: int) -> bool: - """Set the log channel for a guild.""" - with self.get_cursor() as cursor: - # Get current settings or use defaults - cursor.execute('SELECT max_messages, time_frame FROM spam_settings WHERE guild_id = ?', - (guild_id,)) - result = cursor.fetchone() - - if result: - max_messages, time_frame = result['max_messages'], result['time_frame'] - else: - max_messages, time_frame = 5, 10 # Default values - - # Check if updated_at column exists - columns = self._get_table_columns('spam_settings') - - if 'updated_at' in columns: - cursor.execute(''' - INSERT OR REPLACE INTO spam_settings - (guild_id, max_messages, time_frame, log_channel_id, updated_at) - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) - ''', (guild_id, max_messages, time_frame, channel_id)) - else: - cursor.execute(''' - INSERT OR REPLACE INTO spam_settings - (guild_id, max_messages, time_frame, log_channel_id) - VALUES (?, ?, ?, ?) - ''', (guild_id, max_messages, time_frame, channel_id)) - - self.conn.commit() - return True - - def get_spam_settings(self, guild_id: int) -> Optional[Dict]: - """Get spam settings for a guild.""" - with self.get_cursor() as cursor: - columns = self._get_table_columns('spam_settings') - - # Build query based on available columns - select_columns = ['max_messages', 'time_frame', 'log_channel_id'] - if 'created_at' in columns: - select_columns.append('created_at') - if 'updated_at' in columns: - select_columns.append('updated_at') - - query = f"SELECT {', '.join(select_columns)} FROM spam_settings WHERE guild_id = ?" - cursor.execute(query, (guild_id,)) - result = cursor.fetchone() - - if result: - settings = { - 'max_messages': result['max_messages'], - 'time_frame': result['time_frame'], - 'log_channel_id': result['log_channel_id'] - } - - if 'created_at' in columns: - settings['created_at'] = result.get('created_at') - if 'updated_at' in columns: - settings['updated_at'] = result.get('updated_at') - - return settings - return None - - def get_log_channel(self, guild_id: int) -> Optional[int]: - """Get the log channel ID for a guild.""" - with self.get_cursor() as cursor: - cursor.execute(''' - SELECT log_channel_id FROM spam_settings WHERE guild_id = ? - ''', (guild_id,)) - result = cursor.fetchone() - return result['log_channel_id'] if result and result['log_channel_id'] else None - - def log_spam(self, guild_id: int, user_id: int, message: str, message_count: int = 1) -> bool: - """Log a spam incident with message count.""" - with self.get_cursor() as cursor: - columns = self._get_table_columns('spam_logs') - - if 'message_count' in columns: - cursor.execute(''' - INSERT INTO spam_logs (guild_id, user_id, message, message_count) - VALUES (?, ?, ?, ?) - ''', (guild_id, user_id, message[:1000], message_count)) - else: - # Fallback for tables without message_count column - cursor.execute(''' - INSERT INTO spam_logs (guild_id, user_id, message) - VALUES (?, ?, ?) - ''', (guild_id, user_id, message[:1000])) - - self.conn.commit() - return True - - def get_spam_logs(self, guild_id: int, limit: int = 10) -> List[Dict]: - """Get recent spam logs for a guild.""" - with self.get_cursor() as cursor: - cursor.execute(''' - SELECT user_id, message, message_count, timestamp - FROM spam_logs WHERE guild_id = ? - ORDER BY timestamp DESC LIMIT ? - ''', (guild_id, limit)) - - return [ - { - 'user_id': row['user_id'], - 'message': row['message'], - 'message_count': row['message_count'], - 'timestamp': row['timestamp'] - } - for row in cursor.fetchall() - ] - - def get_user_spam_history(self, guild_id: int, user_id: int, - hours: int = 24, limit: int = 50) -> List[Dict]: - """Get spam history for a specific user within a time frame.""" - cutoff_time = datetime.now() - timedelta(hours=hours) - - with self.get_cursor() as cursor: - cursor.execute(''' - SELECT message, message_count, timestamp - FROM spam_logs - WHERE guild_id = ? AND user_id = ? AND timestamp > ? - ORDER BY timestamp DESC LIMIT ? - ''', (guild_id, user_id, cutoff_time, limit)) - - return [ - { - 'message': row['message'], - 'message_count': row['message_count'], - 'timestamp': row['timestamp'] - } - for row in cursor.fetchall() - ] - - def clear_spam_logs(self, guild_id: int, older_than_days: Optional[int] = None) -> int: - """Clear spam logs for a guild, optionally only older entries.""" - with self.get_cursor() as cursor: - if older_than_days: - cutoff_date = datetime.now() - timedelta(days=older_than_days) - cursor.execute(''' - DELETE FROM spam_logs - WHERE guild_id = ? AND timestamp < ? - ''', (guild_id, cutoff_date)) - else: - cursor.execute('DELETE FROM spam_logs WHERE guild_id = ?', (guild_id,)) - - deleted_count = cursor.rowcount - self.conn.commit() - return deleted_count - - def add_to_whitelist(self, guild_id: int, user_id: int, - added_by: Optional[int] = None, reason: Optional[str] = None) -> bool: - """Add user to spam whitelist with additional metadata.""" - with self.get_cursor() as cursor: - columns = self._get_table_columns('spam_whitelist') - - # Build query based on available columns - if 'added_by' in columns and 'reason' in columns: - cursor.execute(''' - INSERT OR IGNORE INTO spam_whitelist (guild_id, user_id, added_by, reason) - VALUES (?, ?, ?, ?) - ''', (guild_id, user_id, added_by, reason)) - else: - cursor.execute(''' - INSERT OR IGNORE INTO spam_whitelist (guild_id, user_id) - VALUES (?, ?) - ''', (guild_id, user_id)) - - success = cursor.rowcount > 0 - self.conn.commit() - return success - - def remove_from_whitelist(self, guild_id: int, user_id: int) -> bool: - """Remove user from spam whitelist.""" - with self.get_cursor() as cursor: - cursor.execute(''' - DELETE FROM spam_whitelist WHERE guild_id = ? AND user_id = ? - ''', (guild_id, user_id)) - success = cursor.rowcount > 0 - self.conn.commit() - return success - - def is_whitelisted(self, guild_id: int, user_id: int) -> bool: - """Check if user is whitelisted.""" - with self.get_cursor() as cursor: - cursor.execute(''' - SELECT 1 FROM spam_whitelist WHERE guild_id = ? AND user_id = ? - ''', (guild_id, user_id)) - return cursor.fetchone() is not None - - def get_whitelist(self, guild_id: int) -> List[Dict]: - """Get all whitelisted users for a guild with metadata.""" - with self.get_cursor() as cursor: - columns = self._get_table_columns('spam_whitelist') - - # Build query based on available columns - select_columns = ['user_id'] - if 'added_by' in columns: - select_columns.append('added_by') - if 'added_at' in columns: - select_columns.append('added_at') - if 'reason' in columns: - select_columns.append('reason') - - query = f"SELECT {', '.join(select_columns)} FROM spam_whitelist WHERE guild_id = ? ORDER BY user_id" - cursor.execute(query, (guild_id,)) - - result = [] - for row in cursor.fetchall(): - entry = {'user_id': row['user_id']} - if 'added_by' in columns: - entry['added_by'] = row.get('added_by') - if 'added_at' in columns: - entry['added_at'] = row.get('added_at') - if 'reason' in columns: - entry['reason'] = row.get('reason') - result.append(entry) - - return result - - def get_guild_stats(self, guild_id: int) -> Dict: - """Get comprehensive statistics for a guild.""" - with self.get_cursor() as cursor: - # Get spam logs count - cursor.execute('SELECT COUNT(*) as total FROM spam_logs WHERE guild_id = ?', (guild_id,)) - total_logs = cursor.fetchone()['total'] - - # Get recent spam (last 24 hours) - yesterday = datetime.now() - timedelta(hours=24) - cursor.execute(''' - SELECT COUNT(*) as recent FROM spam_logs - WHERE guild_id = ? AND timestamp > ? - ''', (guild_id, yesterday)) - recent_logs = cursor.fetchone()['recent'] - - # Get whitelist count - cursor.execute('SELECT COUNT(*) as count FROM spam_whitelist WHERE guild_id = ?', (guild_id,)) - whitelist_count = cursor.fetchone()['count'] - - # Get top spammers (last 7 days) - week_ago = datetime.now() - timedelta(days=7) - cursor.execute(''' - SELECT user_id, COUNT(*) as spam_count, SUM(message_count) as total_messages - FROM spam_logs - WHERE guild_id = ? AND timestamp > ? - GROUP BY user_id - ORDER BY spam_count DESC - LIMIT 5 - ''', (guild_id, week_ago)) - top_spammers = cursor.fetchall() - - return { - 'total_spam_logs': total_logs, - 'recent_spam_logs': recent_logs, - 'whitelist_count': whitelist_count, - 'top_spammers': [ - { - 'user_id': row['user_id'], - 'spam_incidents': row['spam_count'], - 'total_messages': row['total_messages'] - } - for row in top_spammers - ] - } - - def cleanup_old_logs(self, days_to_keep: int = 30) -> int: - """Clean up old spam logs across all guilds.""" - cutoff_date = datetime.now() - timedelta(days=days_to_keep) - - with self.get_cursor() as cursor: - cursor.execute('DELETE FROM spam_logs WHERE timestamp < ?', (cutoff_date,)) - deleted_count = cursor.rowcount - self.conn.commit() - - if deleted_count > 0: - self.logger.info(f"Cleaned up {deleted_count} old spam logs") - - return deleted_count - - def backup_database(self, backup_path: str) -> bool: - """Create a backup of the database.""" - try: - backup_conn = sqlite3.connect(backup_path) - self.conn.backup(backup_conn) - backup_conn.close() - return True - except Exception as e: - self.logger.error(f"Backup failed: {e}") - return False - - def __enter__(self): - """Context manager entry.""" - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Context manager exit with proper cleanup.""" - self.close() - - def close(self): - """Close database connection.""" - if hasattr(self, 'conn') and self.conn: - self.conn.close() \ No newline at end of file diff --git a/DevTools/backend/database/vc_db.py b/DevTools/backend/database/vc_db.py deleted file mode 100644 index 2319667..0000000 --- a/DevTools/backend/database/vc_db.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -import sqlite3 -import os -from typing import Optional, Tuple -from colorama import Fore, Style - -class TempVCDatabase: - def __init__(self, db_path: str = "data/tempvc.db"): - self.db_path = db_path - os.makedirs(os.path.dirname(self.db_path), exist_ok=True) - self.init_db() - - def init_db(self): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - CREATE TABLE IF NOT EXISTS tempvc_settings ( - guild_id INTEGER PRIMARY KEY, - creator_channel_id INTEGER NOT NULL, - category_id INTEGER NOT NULL, - auto_delete_time INTEGER DEFAULT 0 - ) - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS temp_channels ( - channel_id INTEGER PRIMARY KEY, - guild_id INTEGER NOT NULL, - owner_id INTEGER NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - # New table for UI settings - cursor.execute(''' - CREATE TABLE IF NOT EXISTS ui_settings ( - guild_id INTEGER PRIMARY KEY, - ui_enabled BOOLEAN DEFAULT 0, - ui_prefix TEXT DEFAULT '🔧' - ) - ''') - conn.commit() - conn.close() - - def set_tempvc_settings(self, guild_id: int, creator_channel_id: int, category_id: int, auto_delete_time: int = 0): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - INSERT OR REPLACE INTO tempvc_settings - (guild_id, creator_channel_id, category_id, auto_delete_time) - VALUES (?, ?, ?, ?) - ''', (guild_id, creator_channel_id, category_id, auto_delete_time)) - conn.commit() - conn.close() - - def get_tempvc_settings(self, guild_id: int) -> Optional[Tuple[int, int, int]]: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - SELECT creator_channel_id, category_id, auto_delete_time - FROM tempvc_settings - WHERE guild_id = ? - ''', (guild_id,)) - result = cursor.fetchone() - conn.close() - return result - - def remove_tempvc_settings(self, guild_id: int): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute('DELETE FROM tempvc_settings WHERE guild_id = ?', (guild_id,)) - cursor.execute('DELETE FROM temp_channels WHERE guild_id = ?', (guild_id,)) - cursor.execute('DELETE FROM ui_settings WHERE guild_id = ?', (guild_id,)) # Also remove UI settings - conn.commit() - conn.close() - - def add_temp_channel(self, channel_id: int, guild_id: int, owner_id: int): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - INSERT INTO temp_channels (channel_id, guild_id, owner_id) - VALUES (?, ?, ?) - ''', (channel_id, guild_id, owner_id)) - conn.commit() - conn.close() - - def remove_temp_channel(self, channel_id: int): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute('DELETE FROM temp_channels WHERE channel_id = ?', (channel_id,)) - conn.commit() - conn.close() - - def is_temp_channel(self, channel_id: int) -> bool: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute('SELECT 1 FROM temp_channels WHERE channel_id = ?', (channel_id,)) - result = cursor.fetchone() - conn.close() - return result is not None - - def get_temp_channel_owner(self, channel_id: int) -> Optional[int]: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute('SELECT owner_id FROM temp_channels WHERE channel_id = ?', (channel_id,)) - result = cursor.fetchone() - conn.close() - return result[0] if result else None - - def get_all_temp_channels(self, guild_id: int) -> list: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - SELECT channel_id, owner_id, created_at - FROM temp_channels - WHERE guild_id = ? - ''', (guild_id,)) - result = cursor.fetchall() - conn.close() - return result - - def update_channel_activity(self, channel_id: int): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - UPDATE temp_channels - SET last_activity = CURRENT_TIMESTAMP - WHERE channel_id = ? - ''', (channel_id,)) - conn.commit() - conn.close() - - def get_channels_to_delete(self, guild_id: int, minutes_inactive: int) -> list: - if minutes_inactive <= 0: - return [] - - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - SELECT channel_id FROM temp_channels - WHERE guild_id = ? - AND datetime(last_activity, ? || ' minutes') < datetime('now') - ''', (guild_id, str(minutes_inactive))) # Fixed SQL injection - result = [row[0] for row in cursor.fetchall()] - conn.close() - return result - - # New UI Settings methods - def set_ui_settings(self, guild_id: int, ui_enabled: bool, ui_prefix: str = "🔧"): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - INSERT OR REPLACE INTO ui_settings - (guild_id, ui_enabled, ui_prefix) - VALUES (?, ?, ?) - ''', (guild_id, ui_enabled, ui_prefix)) - conn.commit() - conn.close() - - def get_ui_settings(self, guild_id: int) -> Optional[Tuple[bool, str]]: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - SELECT ui_enabled, ui_prefix - FROM ui_settings - WHERE guild_id = ? - ''', (guild_id,)) - result = cursor.fetchone() - conn.close() - return result - - def remove_ui_settings(self, guild_id: int): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute('DELETE FROM ui_settings WHERE guild_id = ?', (guild_id,)) - conn.commit() - conn.close() \ No newline at end of file diff --git a/DevTools/backend/database/warn_db.py b/DevTools/backend/database/warn_db.py deleted file mode 100644 index 13ad680..0000000 --- a/DevTools/backend/database/warn_db.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -import os -import sqlite3 -from contextlib import contextmanager - -from colorama import Fore, Style -class WarnDatabase: - def __init__(self, base_path): - self.db_path = os.path.join(base_path, "Datenbanken", "warns.db") - os.makedirs(os.path.dirname(self.db_path), exist_ok=True) - - # Initialize the database - self._init_database() - def _init_database(self): - """Initialize the database with required tables""" - with self._get_connection() as conn: - cursor = conn.cursor() - cursor.execute(""" - CREATE TABLE IF NOT EXISTS warns ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - moderator_id INTEGER NOT NULL, - reason TEXT NOT NULL, - timestamp TEXT NOT NULL - ) - """) - conn.commit() - - @contextmanager - def _get_connection(self): - """Context manager for database connections""" - conn = sqlite3.connect(self.db_path) - try: - yield conn - finally: - conn.close() - - def add_warning(self, guild_id, user_id, moderator_id, reason, timestamp): - """Add a warning to the database""" - try: - with self._get_connection() as conn: - cursor = conn.cursor() - cursor.execute( - "INSERT INTO warns (guild_id, user_id, moderator_id, reason, timestamp) VALUES (?, ?, ?, ?, ?)", - (guild_id, user_id, moderator_id, reason, timestamp) - ) - conn.commit() - warning_id = cursor.lastrowid - print(f"Warning added successfully with ID: {warning_id}") - return warning_id - except Exception as e: - print(f"Error adding warning: {e}") - raise - - def get_warnings(self, guild_id, user_id): - """Get all warnings for a specific user in a guild""" - try: - with self._get_connection() as conn: - cursor = conn.cursor() - cursor.execute( - "SELECT id, reason, timestamp FROM warns WHERE guild_id = ? AND user_id = ? ORDER BY id DESC", - (guild_id, user_id) - ) - return cursor.fetchall() - except Exception as e: - print(f"Error getting warnings: {e}") - return [] - - def get_warning_by_id(self, warn_id): - """Get a specific warning by ID""" - try: - with self._get_connection() as conn: - cursor = conn.cursor() - cursor.execute("SELECT * FROM warns WHERE id = ?", (warn_id,)) - return cursor.fetchone() - except Exception as e: - print(f"Error getting warning by ID: {e}") - return None - - def delete_warning(self, warn_id): - """Delete a warning by ID""" - try: - with self._get_connection() as conn: - cursor = conn.cursor() - cursor.execute("DELETE FROM warns WHERE id = ?", (warn_id,)) - conn.commit() - if cursor.rowcount > 0: - print(f"Warning {warn_id} deleted successfully") - return True - else: - print(f"Warning {warn_id} not found") - return False - except Exception as e: - print(f"Error deleting warning: {e}") - return False - - def get_warning_count(self, guild_id, user_id): - """Get the total number of warnings for a user""" - try: - with self._get_connection() as conn: - cursor = conn.cursor() - cursor.execute( - "SELECT COUNT(*) FROM warns WHERE guild_id = ? AND user_id = ?", - (guild_id, user_id) - ) - return cursor.fetchone()[0] - except Exception as e: - print(f"Error getting warning count: {e}") - return 0 - - def get_total_warnings(self): - """Get total number of warnings in database (for debugging)""" - try: - with self._get_connection() as conn: - cursor = conn.cursor() - cursor.execute("SELECT COUNT(*) FROM warns") - return cursor.fetchone()[0] - except Exception as e: - print(f"Error getting total warnings: {e}") - return 0 \ No newline at end of file diff --git a/DevTools/backend/database/welcome_db.py b/DevTools/backend/database/welcome_db.py deleted file mode 100644 index 5336ad6..0000000 --- a/DevTools/backend/database/welcome_db.py +++ /dev/null @@ -1,552 +0,0 @@ -""" -Welcome Database Handler -========================= - -Datenbank-Handler fĂŒr das Welcome System mit vollstĂ€ndiger -RĂŒckwĂ€rtskompatibilitĂ€t und automatischer Migration. - -Copyright (c) 2025 OPPRO.NET Network -""" - -import sqlite3 -import aiosqlite -import asyncio -import logging -from typing import Optional, Dict, Any -from datetime import datetime - -# Logger Setup -logger = logging.getLogger(__name__) - - -class WelcomeDatabase: - """ - Datenbank-Handler fĂŒr Welcome System Einstellungen. - - Bietet synchrone und asynchrone Methoden mit automatischer - Fallback-Logik fĂŒr RĂŒckwĂ€rtskompatibilitĂ€t. UnterstĂŒtzt - automatische Datenbankmigrationen. - - Parameters - ---------- - db_path : str, optional - Pfad zur SQLite-Datenbank, by default "data/welcome.db" - - Attributes - ---------- - db_path : str - Pfad zur SQLite-Datenbank - migration_done : bool - Status der Datenbankmigrierung - - Examples - -------- - >>> db = WelcomeDatabase() - >>> await db.update_welcome_settings(123456, channel_id=789012) - True - """ - - def __init__(self, db_path: str = "data/welcome.db"): - """ - Initialisiert den Datenbank-Handler. - - Parameters - ---------- - db_path : str, optional - Pfad zur SQLite-Datenbank, by default "data/welcome.db" - - Notes - ----- - Erstellt automatisch die Basis-Tabellen wenn sie nicht existieren. - """ - self.db_path = db_path - self.migration_done = False - self.init_database() - - def init_database(self): - """ - Initialisiert die Datenbank synchron fĂŒr RĂŒckwĂ€rtskompatibilitĂ€t. - - Notes - ----- - Erstellt die Basis-Tabelle `welcome_settings` mit allen - ursprĂŒnglichen Feldern. Neue Felder werden spĂ€ter durch - `migrate_database()` hinzugefĂŒgt. - - Diese Methode ist synchron um KompatibilitĂ€t mit Ă€lteren - Versionen zu gewĂ€hrleisten. - """ - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - # Basis-Tabelle erstellen (alte Version) - cursor.execute(''' - CREATE TABLE IF NOT EXISTS welcome_settings ( - guild_id INTEGER PRIMARY KEY, - channel_id INTEGER, - welcome_message TEXT, - enabled INTEGER DEFAULT 1, - embed_enabled INTEGER DEFAULT 0, - embed_color TEXT DEFAULT '#00ff00', - embed_title TEXT, - embed_description TEXT, - embed_thumbnail INTEGER DEFAULT 0, - embed_footer TEXT, - ping_user INTEGER DEFAULT 0, - delete_after INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) - ) - ''') - - conn.commit() - conn.close() - - async def migrate_database(self): - """ - Migriert die Datenbank zu neuen Features (async). - - Notes - ----- - FĂŒgt folgende neue Spalten hinzu: - - auto_role_id: Automatische Rollenvergabe - - join_dm_enabled: Private Willkommensnachrichten - - join_dm_message: DM Nachrichtentext - - template_name: Verwendete Vorlage - - welcome_stats_enabled: Statistik-Tracking - - rate_limit_enabled: Rate-Limiting aktiv - - rate_limit_seconds: Rate-Limit Zeitfenster - - Erstellt außerdem die `welcome_stats` Tabelle fĂŒr Statistiken. - - Die Migrierung wird nur einmal pro Instanz ausgefĂŒhrt. - """ - if self.migration_done: - return - - try: - async with aiosqlite.connect(self.db_path) as conn: - # PrĂŒfe welche Spalten bereits existieren - cursor = await conn.execute("PRAGMA table_info(welcome_settings)") - columns = await cursor.fetchall() - existing_columns = [col[1] for col in columns] - - # Neue Spalten hinzufĂŒgen falls nicht vorhanden - new_columns = { - 'auto_role_id': 'INTEGER', - 'join_dm_enabled': 'INTEGER DEFAULT 0', - 'join_dm_message': 'TEXT', - 'template_name': 'TEXT', - 'welcome_stats_enabled': 'INTEGER DEFAULT 0', - 'rate_limit_enabled': 'INTEGER DEFAULT 1', - 'rate_limit_seconds': 'INTEGER DEFAULT 60' - } - - for column_name, column_def in new_columns.items(): - if column_name not in existing_columns: - try: - await conn.execute(f'ALTER TABLE welcome_settings ADD COLUMN {column_name} {column_def}') - logger.info(f"Spalte {column_name} hinzugefĂŒgt") - except sqlite3.OperationalError: - # Spalte existiert bereits - pass - - # Neue Tabelle fĂŒr Statistiken - await conn.execute(''' - CREATE TABLE IF NOT EXISTS welcome_stats ( - guild_id INTEGER, - date TEXT, - joins INTEGER DEFAULT 0, - leaves INTEGER DEFAULT 0, - PRIMARY KEY (guild_id, date) - ) - ''') - - await conn.commit() - self.migration_done = True - logger.info("Datenbankmigrierung abgeschlossen") - - except Exception as e: - logger.error(f"Fehler bei Datenbankmigrierung: {e}") - - async def set_welcome_channel(self, guild_id: int, channel_id: int) -> bool: - """ - Setzt den Welcome Channel (RĂŒckwĂ€rtskompatible Methode). - - Parameters - ---------- - guild_id : int - Discord Server ID - channel_id : int - Discord Channel ID - - Returns - ------- - bool - True bei Erfolg, False bei Fehler - - Notes - ----- - Diese Methode ist deprecated, verwende stattdessen - `update_welcome_settings(guild_id, channel_id=channel_id)`. - """ - return await self.update_welcome_settings(guild_id, channel_id=channel_id) - - async def set_welcome_message(self, guild_id: int, message: str) -> bool: - """ - Setzt die Welcome Message (RĂŒckwĂ€rtskompatible Methode). - - Parameters - ---------- - guild_id : int - Discord Server ID - message : str - Welcome Message Text - - Returns - ------- - bool - True bei Erfolg, False bei Fehler - - Notes - ----- - Diese Methode ist deprecated, verwende stattdessen - `update_welcome_settings(guild_id, welcome_message=message)`. - """ - return await self.update_welcome_settings(guild_id, welcome_message=message) - - async def update_welcome_settings(self, guild_id: int, **kwargs) -> bool: - """ - Aktualisiert Welcome Einstellungen mit Fallback auf sync. - - Parameters - ---------- - guild_id : int - Discord Server ID - **kwargs : dict - Felder zum Aktualisieren (siehe Notes) - - Returns - ------- - bool - True bei Erfolg, False bei Fehler - - Notes - ----- - GĂŒltige Felder in kwargs: - - channel_id : int - - welcome_message : str - - enabled : bool/int - - embed_enabled : bool/int - - embed_color : str (Hex-Format) - - embed_title : str - - embed_description : str - - embed_thumbnail : bool/int - - embed_footer : str - - ping_user : bool/int - - delete_after : int (Sekunden) - - auto_role_id : int - - join_dm_enabled : bool/int - - join_dm_message : str - - template_name : str - - welcome_stats_enabled : bool/int - - rate_limit_enabled : bool/int - - rate_limit_seconds : int - - Erstellt automatisch einen neuen Eintrag wenn keiner existiert. - Bei async-Fehlern wird automatisch auf sync Fallback gewechselt. - - Examples - -------- - >>> await db.update_welcome_settings( - ... 123456, - ... channel_id=789012, - ... welcome_message="Willkommen %mention%!", - ... embed_enabled=True - ... ) - True - """ - try: - await self.migrate_database() - - async with aiosqlite.connect(self.db_path) as conn: - # PrĂŒfen ob Eintrag existiert - cursor = await conn.execute('SELECT guild_id FROM welcome_settings WHERE guild_id = ?', (guild_id,)) - exists = await cursor.fetchone() - - if not exists: - # Neuen Eintrag erstellen - await conn.execute(''' - INSERT INTO welcome_settings (guild_id) VALUES (?) - ''', (guild_id,)) - - # Dynamisch die Felder aktualisieren - valid_fields = [ - 'channel_id', 'welcome_message', 'enabled', 'embed_enabled', - 'embed_color', 'embed_title', 'embed_description', 'embed_thumbnail', - 'embed_footer', 'ping_user', 'delete_after', 'auto_role_id', - 'join_dm_enabled', 'join_dm_message', 'template_name', - 'welcome_stats_enabled', 'rate_limit_enabled', 'rate_limit_seconds' - ] - - update_fields = [] - values = [] - - for key, value in kwargs.items(): - if key in valid_fields: - update_fields.append(f"{key} = ?") - values.append(value) - - if update_fields: - update_fields.append("updated_at = datetime('now')") - query = f"UPDATE welcome_settings SET {', '.join(update_fields)} WHERE guild_id = ?" - values.append(guild_id) - await conn.execute(query, values) - - await conn.commit() - return True - - except Exception as e: - logger.error(f"Async Update fehlgeschlagen, verwende Sync Fallback: {e}") - # Fallback auf synchrone Version - return self._sync_update_welcome_settings(guild_id, **kwargs) - - def _sync_update_welcome_settings(self, guild_id: int, **kwargs) -> bool: - """ - Sync Fallback fĂŒr alte Versionen. - - Parameters - ---------- - guild_id : int - Discord Server ID - **kwargs : dict - Felder zum Aktualisieren - - Returns - ------- - bool - True bei Erfolg, False bei Fehler - - Notes - ----- - UnterstĂŒtzt nur Basis-Felder fĂŒr RĂŒckwĂ€rtskompatibilitĂ€t. - Neue Felder werden ignoriert. - """ - try: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute('SELECT guild_id FROM welcome_settings WHERE guild_id = ?', (guild_id,)) - exists = cursor.fetchone() - - if not exists: - cursor.execute('INSERT INTO welcome_settings (guild_id) VALUES (?)', (guild_id,)) - - # Nur bekannte Felder fĂŒr RĂŒckwĂ€rtskompatibilitĂ€t - valid_fields = [ - 'channel_id', 'welcome_message', 'enabled', 'embed_enabled', - 'embed_color', 'embed_title', 'embed_description', 'embed_thumbnail', - 'embed_footer', 'ping_user', 'delete_after' - ] - - update_fields = [] - values = [] - - for key, value in kwargs.items(): - if key in valid_fields: - update_fields.append(f"{key} = ?") - values.append(value) - - if update_fields: - update_fields.append("updated_at = datetime('now')") - query = f"UPDATE welcome_settings SET {', '.join(update_fields)} WHERE guild_id = ?" - values.append(guild_id) - cursor.execute(query, values) - - conn.commit() - conn.close() - return True - - except Exception as e: - logger.error(f"Sync Update Fehler: {e}") - return False - - async def get_welcome_settings(self, guild_id: int) -> Optional[Dict[str, Any]]: - """ - Holt Welcome Einstellungen mit sync Fallback. - - Parameters - ---------- - guild_id : int - Discord Server ID - - Returns - ------- - dict or None - Dictionary mit allen Einstellungen oder None wenn nicht vorhanden - - Notes - ----- - Gibt ein Dictionary zurĂŒck mit allen Feldern als Keys. - Bei async-Fehlern wird automatisch auf sync Fallback gewechselt. - - Examples - -------- - >>> settings = await db.get_welcome_settings(123456) - >>> if settings: - ... print(settings['channel_id']) - ... print(settings['welcome_message']) - """ - try: - await self.migrate_database() - - async with aiosqlite.connect(self.db_path) as conn: - cursor = await conn.execute('SELECT * FROM welcome_settings WHERE guild_id = ?', (guild_id,)) - result = await cursor.fetchone() - - if result: - columns = [description[0] for description in cursor.description] - return dict(zip(columns, result)) - return None - - except Exception as e: - logger.error(f"Async Get fehlgeschlagen, verwende Sync Fallback: {e}") - return self._sync_get_welcome_settings(guild_id) - - def _sync_get_welcome_settings(self, guild_id: int) -> Optional[Dict[str, Any]]: - """ - Sync Fallback fĂŒr Einstellungen holen. - - Parameters - ---------- - guild_id : int - Discord Server ID - - Returns - ------- - dict or None - Dictionary mit Einstellungen oder None - """ - try: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute('SELECT * FROM welcome_settings WHERE guild_id = ?', (guild_id,)) - result = cursor.fetchone() - - if result: - columns = [description[0] for description in cursor.description] - return dict(zip(columns, result)) - - conn.close() - return None - - except Exception as e: - logger.error(f"Sync Get Fehler: {e}") - return None - - async def delete_welcome_settings(self, guild_id: int) -> bool: - """ - Löscht alle Welcome Einstellungen fĂŒr einen Server. - - Parameters - ---------- - guild_id : int - Discord Server ID - - Returns - ------- - bool - True bei Erfolg, False bei Fehler - - Notes - ----- - Löscht nur die Einstellungen, nicht die Statistiken. - Diese Aktion kann nicht rĂŒckgĂ€ngig gemacht werden. - """ - try: - async with aiosqlite.connect(self.db_path) as conn: - await conn.execute('DELETE FROM welcome_settings WHERE guild_id = ?', (guild_id,)) - await conn.commit() - return True - except Exception as e: - logger.error(f"Fehler beim Löschen: {e}") - return False - - async def toggle_welcome(self, guild_id: int) -> Optional[bool]: - """ - Schaltet das Welcome System ein/aus. - - Parameters - ---------- - guild_id : int - Discord Server ID - - Returns - ------- - bool or None - Neuer Status (True=aktiviert, False=deaktiviert) - oder None wenn keine Einstellungen vorhanden - - Examples - -------- - >>> new_state = await db.toggle_welcome(123456) - >>> if new_state is not None: - ... print(f"Welcome System ist jetzt {'aktiviert' if new_state else 'deaktiviert'}") - """ - try: - settings = await self.get_welcome_settings(guild_id) - if not settings: - return None - - new_state = not settings.get('enabled', True) - await self.update_welcome_settings(guild_id, enabled=new_state) - return new_state - except Exception as e: - logger.error(f"Toggle Fehler: {e}") - return None - - async def update_welcome_stats(self, guild_id: int, joins: int = 0, leaves: int = 0): - """ - Aktualisiert Welcome Statistiken fĂŒr den aktuellen Tag. - - Parameters - ---------- - guild_id : int - Discord Server ID - joins : int, optional - Anzahl neuer Beitritte hinzuzufĂŒgen, by default 0 - leaves : int, optional - Anzahl Austritte hinzuzufĂŒgen, by default 0 - - Notes - ----- - Verwendet das aktuelle Datum als SchlĂŒssel. - Summiert die Werte wenn bereits EintrĂ€ge fĂŒr heute existieren. - Erstellt automatisch einen neuen Eintrag wenn keiner vorhanden ist. - - Die Statistiken werden in der `welcome_stats` Tabelle gespeichert - mit einer Zeile pro Server pro Tag. - - Examples - -------- - >>> # Einen neuen Join tracken - >>> await db.update_welcome_stats(123456, joins=1) - >>> - >>> # Einen Leave tracken - >>> await db.update_welcome_stats(123456, leaves=1) - """ - try: - await self.migrate_database() - date = datetime.now().strftime('%Y-%m-%d') - - async with aiosqlite.connect(self.db_path) as conn: - await conn.execute(''' - INSERT OR REPLACE INTO welcome_stats (guild_id, date, joins, leaves) - VALUES (?, ?, - COALESCE((SELECT joins FROM welcome_stats WHERE guild_id = ? AND date = ?), 0) + ?, - COALESCE((SELECT leaves FROM welcome_stats WHERE guild_id = ? AND date = ?), 0) + ?) - ''', (guild_id, date, guild_id, date, joins, guild_id, date, leaves)) - await conn.commit() - except Exception as e: - logger.error(f"Stats Update Fehler: {e}") \ No newline at end of file diff --git a/DevTools/backend/logging.py b/DevTools/backend/logging.py deleted file mode 100644 index 4491903..0000000 --- a/DevTools/backend/logging.py +++ /dev/null @@ -1,240 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -""" -DevTools Backend Initialization -Initialisiert alle Datenbank-Module mit Logging -""" - -import colorama -from colorama import Fore, Style -from datetime import datetime -from typing import Callable, List -import logging - -colorama.init(autoreset=True) - -# Logger fĂŒr Backend -logger = logging.getLogger(__name__) - - -class DatabaseInitializer: - """Verwaltet Datenbank-Initialisierungen""" - - def __init__(self): - self.databases = [] - self.failed = [] - - @staticmethod - def timestamp() -> str: - """Gibt formatierten Timestamp zurĂŒck""" - return datetime.now().strftime(f"[{Fore.CYAN}%H:%M:%S{Style.RESET_ALL}]") - - @staticmethod - def log_success(db_name: str): - """Loggt erfolgreiche Initialisierung""" - print( - f"{DatabaseInitializer.timestamp()} " - f"[{Style.BRIGHT}{Fore.MAGENTA}DATABASE{Style.RESET_ALL}] " - f"{db_name} initialized ✓" - ) - - @staticmethod - def log_error(db_name: str, error: Exception): - """Loggt Fehler bei Initialisierung""" - print( - f"{DatabaseInitializer.timestamp()} " - f"[{Fore.RED}DATABASE{Style.RESET_ALL}] " - f"{db_name} initialization failed: {error}" - ) - logger.error(f"Database init failed: {db_name}", exc_info=True) - - def register(self, name: str, init_func: Callable): - """Registriert eine Datenbank fĂŒr Initialisierung""" - self.databases.append((name, init_func)) - - def init_all(self) -> bool: - """ - Initialisiert alle registrierten Datenbanken - Returns: True wenn alle erfolgreich, sonst False - """ - print(f"\n{Fore.MAGENTA}{'─' * 50}{Style.RESET_ALL}") - print(f"{Fore.MAGENTA} Initializing Databases...{Style.RESET_ALL}") - print(f"{Fore.MAGENTA}{'─' * 50}{Style.RESET_ALL}\n") - - success_count = 0 - - for db_name, init_func in self.databases: - try: - init_func() - self.log_success(db_name) - success_count += 1 - except Exception as e: - self.log_error(db_name, e) - self.failed.append((db_name, str(e))) - - # Summary - print(f"\n{Fore.MAGENTA}{'─' * 50}{Style.RESET_ALL}") - if self.failed: - print( - f"{Fore.YELLOW} ⚠ {success_count}/{len(self.databases)} databases initialized{Style.RESET_ALL}" - ) - for db_name, error in self.failed: - print(f"{Fore.RED} ✗ {db_name}: {error}{Style.RESET_ALL}") - else: - print( - f"{Fore.GREEN} ✓ All {success_count} databases initialized successfully{Style.RESET_ALL}" - ) - print(f"{Fore.MAGENTA}{'─' * 50}{Style.RESET_ALL}\n") - - return len(self.failed) == 0 - - -# ============================================================================= -# DATABASE INITIALIZATION FUNCTIONS -# ============================================================================= - -def init_spam_db(): - """Initialisiert Spam-Datenbank""" - try: - import DevTools.backend.database.spam_db - except Exception as e: - logger.debug(f"Spam DB import: {e}") - pass - - -def init_warn_db(): - """Initialisiert Warn-Datenbank""" - try: - import DevTools.backend.database.warn_db - except Exception as e: - logger.debug(f"Warn DB import: {e}") - pass - - -def init_notes_db(): - """Initialisiert Notes-Datenbank""" - try: - import DevTools.backend.database.notes_db - except Exception as e: - logger.debug(f"Notes DB import: {e}") - pass - - -def init_tempvc_db(): - """Initialisiert TempVC-Datenbank""" - try: - import DevTools.backend.database.vc_db - except Exception as e: - logger.debug(f"TempVC DB import: {e}") - pass - - -def init_stats_db(): - """Initialisiert Stats-Datenbank""" - try: - import DevTools.backend.database.Stats_db - except Exception as e: - logger.debug(f"Stats DB import: {e}") - pass - - -def init_levelsystem_db(): - """Initialisiert Levelsystem-Datenbank""" - try: - import DevTools.backend.database.levelsystem_db - except Exception as e: - logger.debug(f"Levelsystem DB import: {e}") - pass - - -def init_globalchat_db(): - """Initialisiert GlobalChat-Datenbank""" - try: - from .database.globalchat_db import GlobalChatDatabase - # Erstelle Instanz - db = GlobalChatDatabase() - except Exception as e: - logger.debug(f"GlobalChat DB import: {e}") - raise - pass - - -def init_logging_db(): - """Initialisiert Logging-Datenbank""" - try: - import DevTools.backend.database.logging_db - except Exception as e: - logger.debug(f"Logging DB import: {e}") - pass - - -def init_autodelete_db(): - """Initialisiert AutoDelete-Datenbank""" - try: - from .database.autodelete_db import db - except ImportError: - pass # Optional - - -def init_welcome_db(): - """Initialisiert Welcome-Datenbank""" - try: - from .database.welcome_db import db - except ImportError: - pass # Optional - - -# ============================================================================= -# MAIN INITIALIZATION -# ============================================================================= - -# Globaler Initializer -_initializer = DatabaseInitializer() - -# Alle Datenbanken registrieren -_initializer.register("Spam Database", init_spam_db) -_initializer.register("Warn Database", init_warn_db) -_initializer.register("Notes Database", init_notes_db) -_initializer.register("TempVC Database", init_tempvc_db) -_initializer.register("Stats Database", init_stats_db) -_initializer.register("Levelsystem Database", init_levelsystem_db) -_initializer.register("GlobalChat Database", init_globalchat_db) -_initializer.register("Logging Database", init_logging_db) -_initializer.register("AutoDelete Database", init_autodelete_db) -_initializer.register("Welcome Database", init_welcome_db) - - -def init_all() -> bool: - """ - Initialisiert alle Datenbank-Module - - Returns: - bool: True wenn alle erfolgreich initialisiert wurden - """ - return _initializer.init_all() - - -def get_failed_databases() -> List[tuple]: - """ - Gibt Liste der fehlgeschlagenen Datenbanken zurĂŒck - - Returns: - List[tuple]: Liste von (db_name, error_message) Tupeln - """ - return _initializer.failed - - -# Backwards Compatibility - Einzelne Funktionen exportieren -__all__ = [ - 'init_all', - 'init_spam_db', - 'init_warn_db', - 'init_notes_db', - 'init_tempvc_db', - 'init_stats_db', - 'init_levelsystem_db', - 'init_globalchat_db', - 'init_logging_db', - 'init_autodelete_db', - 'init_welcome_db', - 'get_failed_databases', -] \ No newline at end of file diff --git a/DevTools/backend/utils/__init__.py b/DevTools/backend/utils/__init__.py deleted file mode 100644 index d085c3a..0000000 --- a/DevTools/backend/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .config import * \ No newline at end of file diff --git a/DevTools/ui/__init__.py b/DevTools/ui/__init__.py deleted file mode 100644 index 30f00a0..0000000 --- a/DevTools/ui/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network -from .emojis import * diff --git a/DevTools/ui/emojis.py b/DevTools/ui/emojis.py deleted file mode 100644 index ed01688..0000000 --- a/DevTools/ui/emojis.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2025 OPPRO.NET Network - -# Allgemein -emoji_link = "<:link:1411749901099859998>" -emoji_slowmode = "<:slowmode:1411749933228359830>" -emoji_locked = "<:locked:1423971266737471488>" -emoji_delete = "<:delete:1411749869625802852>" -emoji_public = "<:public:1411749759319802018>" -emoji_gift = "<:gift:1411749736632946799>" -emoji_mediaadd = "<:mediaadd:1411749700620521482>" -emoji_member = "<:member:1423975509062193192>" - -# Channels & Rollen -emoji_rules = "<:rules:1411749662225989775>" -emoji_voice = "<:voicechannel:1411749608480047245>" -emoji_verified = "<:verifiedwhite:1411749568718045284>" -emoji_event = "<:event:1411749503727566858>" -emoji_pinned = "<:pinned:1411749445716152410>" -emoji_studenthub = "<:studenthub:1411749419291775117>" - -# Suche & Management -emoji_search = "<:search:1411749392943284374>" -emoji_manager = "<:manager:1411749363730092057>" -emoji_location = "<:location:1411749336492019915>" -emoji_entertainment = "<:entertainment:1411749311011881032>" -emoji_staff = "<:staff:1411749285627822203>" -emoji_moderator = "<:moderator:1411749252736094208>" -emoji_warn = "<:warn:1423974196664602704>" - -# Medien & Technik -emoji_media = "<:media:1411749223329955860>" -emoji_scienceandtech = "<:scienceandtech:1411749167038206012>" -emoji_gif = "<:gif:1411749131034300548>" -emoji_summary = "<:summary:1411749103754281081>" -emoji_upload = "<:upload:1411749080169840760>" - -# HinzufĂŒgen & Verwaltung -emoji_add = "<:add:1411749054282596564>" -emoji_channelandroles = "<:channelsandroles:1411749031293747252>" -emoji_channel = "<:channel:1411749007318974484>" -emoji_developer = "<:developer:1411748983897985075>" -emoji_announcement = "<:announcement:1411748950783955025>" -emoji_statistics = "<:statistics:1411748924359971007>" - -# Guides & Owner -emoji_serverguide = "<:serverguide:1411748884463489166>" -emoji_owner = "<:owner:1411748853585023189>" -emoji_annoattention = "<:announcementattention:1411748828738097373>" - -# Status & Sperren -emoji_forbidden = "<:forbidden:1383743601791471697>" -emoji_no = "<:no:1380796085802500148>" -emoji_yes = "<:yes:1380796058963153017>" 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 3bf5801..f90eb50 100644 --- a/README.md +++ b/README.md @@ -2,46 +2,103 @@ ![ManagerX Banner](assets/img/ManagerX_banner.png) -

đŸ€– ManagerX Discord Bot

+# đŸ€– ManagerX Discord Bot -

Der ultimative All-in-One Discord Bot fĂŒr deine Community

+### *Der ultimative All-in-One Bot fĂŒr professionelles Community Management*
- +

- Version - Next Release - Last Commit - License + + Version + + + Next Release + + + Last Commit + + + License +

+

Python Pycord + SQLite Issues Stars + Forks


```ascii -╔════════════════════════════════════════════════════════════════════╗ -║ ║ -║ đŸ€– Der professionelle Discord Bot fĂŒr deine Community 🚀 ║ -║ ║ -║ Moderation ‱ Leveling ‱ Welcome ‱ TempVC ‱ Globalchat ║ -║ ║ -╚════════════════════════════════════════════════════════════════════╝ +╔══════════════════════════════════════════════════════════════════════╗ +║ ║ +║ đŸ€– Professionelles Discord Bot Framework fĂŒr Communities 🚀 ║ +║ ║ +║ Moderation ‱ Leveling ‱ Welcome ‱ TempVC ‱ Globalchat ‱ Stats ║ +║ ║ +╚══════════════════════════════════════════════════════════════════════╝ ```
-**Entwickelt von** [**OPPRO.NET Development**](https://github.com/Oppro-net-Development) **|** **© OPPRO.NET Networkℱ** +**Entwickelt von** [**OPPRO.NET Development**](https://github.com/Oppro-net-Development) **|** ⚡ **Powered by OPPRO.NET Networkℱ**
-[đŸ“„ Installation](#-installation) ‱ [✹ Features](#-features) ‱ [📖 Docs](https://docs.oppro-network.de) ‱ [💬 Support](#-support--community) +--- + +## 📩 Quick Install + + + + + + +
+ +### 🎯 FĂŒr End-User + +```bash +# Bot direkt nutzen +pip install ManagerX +``` + +**Mit Dokumentation:** +```bash +pip install ManagerX[docs] +``` + + + +### đŸ‘šâ€đŸ’» FĂŒr Entwickler + +```bash +# Development Setup +pip install ManagerX[dev] +``` + +**VollstĂ€ndige Installation:** +```bash +pip install ManagerX[all] +``` + +
+ +
+ +

+ đŸ“„ Detaillierte Installation ‱ + ✹ Features ‱ + 📖 Dokumentation ‱ + 💬 Support +

@@ -49,45 +106,85 @@ --- +
+ ## 🎯 Was ist ManagerX? +
+ - - @@ -98,89 +195,175 @@ Active Modules: --- +
+ ## ✹ Feature-Übersicht +*Entdecke die leistungsstarken Module von ManagerX* + +
+ +
+
+ + +**ManagerX** ist ein hochmoderner, leistungsstarker Discord-Bot, der speziell fĂŒr professionelles Community-Management entwickelt wurde. Mit modernster Architektur und einer Vielzahl an Features bietet ManagerX alles, was anspruchsvolle Discord-Server benötigen. -**ManagerX** ist ein hochmoderner, leistungsstarker Discord-Bot, der speziell fĂŒr professionelles Community-Management entwickelt wurde. Mit einer Vielzahl an Features von automatisierter Moderation ĂŒber interaktive Levelsysteme bis hin zu globaler Kommunikation bietet ManagerX alles, was moderne Discord-Server benötigen. +
-### 🌟 Warum ManagerX wĂ€hlen? +### 🌟 Warum ManagerX? -- ⚡ **Performance** – Optimierte Datenbank-Architektur mit SQLite -- đŸ›Ąïž **Sicherheit** – Integriertes Anti-Spam und umfassende Moderationstools -- 🎹 **FlexibilitĂ€t** – VollstĂ€ndig konfigurierbare Module fĂŒr jeden Server -- 🌍 **KonnektivitĂ€t** – Globalchat verbindet Communities weltweit -- 📈 **Aktive Entwicklung** – RegelmĂ€ĂŸige Updates und neue Features -- 🆓 **Open Source** – Transparent und community-driven + + + + + + + + + + + + + + + + + + + + + + + + + +
⚡Blazing Fast
Optimierte SQLite-Architektur fĂŒr maximale Performance
đŸ›ĄïžEnterprise Security
Anti-Spam, Moderation-Logs und umfassende Sicherheitsfeatures
🎹Hochgradig Anpassbar
Jedes Modul vollstĂ€ndig konfigurierbar fĂŒr deine BedĂŒrfnisse
🌍Global Connected
Verbinde deine Community mit Servern weltweit via Globalchat
📈Aktive Entwicklung
RegelmĂ€ĂŸige Updates mit neuen Features und Verbesserungen
🆓100% Open Source
Transparent, community-driven und kostenlos verfĂŒgbar
+ ```yaml -Quick Stats: - Status: Production Ready - Version: 2.0.0 - Python: 3.10+ - Framework: py-cord + ezcord - Database: SQLite3 - Hosting: Linux Compatible - License: GPL-3.0 - -Active Modules: - - Moderation System - - Level & XP System - - Welcome System - - Temporary Voice Channels - - Global Chat - - Weather Integration - - Wikipedia Search - - Stats & Analytics +⚙ Technical Specifications +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Status: Production Ready ✓ +Current: v2.0.0 +Next Release: v2.1.0 (Q1 2025) + +🔧 Technology Stack +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Language: Python 3.10+ +Framework: py-cord + ezcord +Database: SQLite3 +API: Discord API v10 +Architecture: Modular Cogs System + +🌐 Deployment +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Platforms: Linux, Windows, macOS +Hosting: Cloud-Ready +Requirements: 512MB RAM (1GB+ rec.) +Uptime: 99.9%+ VerfĂŒgbarkeit + +📩 Active Modules +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +✓ Advanced Moderation System +✓ Dynamic Leveling & XP Engine +✓ Smart Welcome Automation +✓ Temporary Voice Channels +✓ Global Cross-Server Chat +✓ Real-Time Weather Data +✓ Wikipedia Integration +✓ Comprehensive Statistics +✓ Custom Command Framework ```
@@ -190,228 +373,426 @@ Active Modules: --- -## 🚀 Installation +
+ +## 🚀 Installation & Setup + +*Starte ManagerX in wenigen Minuten* + +
+ +
### 📋 Systemanforderungen +
### đŸ›Ąïž Moderation & Sicherheit -**Advanced Moderation Tools** -- ✅ Ban, Kick, Mute, Warn Befehle -- ✅ Intelligentes Anti-Spam System -- ✅ Automatisches Warning-Management -- ✅ Detaillierte Moderation-Logs -- ✅ TemporĂ€re Strafen (Timeout) -- ✅ Reason-Tracking fĂŒr alle Actions - -**Konfigurierbare Schwellenwerte** -- Nachrichten pro Zeiteinheit -- Duplicate Message Detection -- Mention Spam Protection -- Link Spam Filter -- Custom Regex Patterns +
+ +**🔒 Advanced Moderation Toolkit** + +
+ +```yaml +Moderation Commands: + /ban - Permanenter Server-Ausschluss + /kick - User vom Server entfernen + /mute - TemporĂ€res Timeout verhĂ€ngen + /warn - Verwarnungen aussprechen + /timeout - Zeitlich begrenzte Stummschaltung + /purge - Massen-Nachrichtenlöschung + +Anti-Spam Engine: + ✓ Intelligente Spam-Erkennung + ✓ Duplicate Message Detection + ✓ Mention Spam Protection + ✓ Link & URL Filter + ✓ Custom Regex Patterns + ✓ Configurable Thresholds + +Moderation Logs: + ✓ VollstĂ€ndige Action History + ✓ Reason Tracking + ✓ Moderator Attribution + ✓ Automatic Evidence Collection + ✓ Appeal System Ready +```
### 📊 Community Engagement -**Levelsystem** -- ✅ VollstĂ€ndig anpassbares XP-System -- ✅ Rollenbelohnungen fĂŒr Level-Ups -- ✅ Server & Global Leaderboards -- ✅ Individuelle Level-Up Notifications -- ✅ XP-Multiplikatoren & Boosts -- ✅ Voice-Channel XP-Tracking - -**Welcome-System** -- ✅ Automatische BegrĂŒĂŸungsnachrichten -- ✅ Custom Embed Designs -- ✅ Regel- & Informationsnachrichten -- ✅ Automatische Autorollen -- ✅ User-Counter Integration +
+ +**🎼 Gamification & Engagement** + +
+ +```yaml +Levelsystem Features: + XP System: + - VollstĂ€ndig anpassbare XP-Raten + - Text & Voice Channel XP + - XP-Multiplier & Boosts + - Daily/Weekly Bonuses + + Rewards: + - Automatische Rollenbelohnungen + - Custom Level-Up Messages + - Achievement System + - Milestone Rewards + + Leaderboards: + - Server Top Rankings + - Global Leaderboards + - Category-Specific Rankings + - Historical Statistics + +Welcome System: + ✓ Benutzerdefinierte Embed-Designs + ✓ Auto-Role Assignment + ✓ Regel- & Info-Nachrichten + ✓ User Counter Integration + ✓ Custom Welcome Images + ✓ Join/Leave Logging +```
-### 🌐 Social & Information +### 🌐 Social & Communication -**Globalchat System** -- ✅ Echtzeit-Chat mit anderen Servern -- ✅ Moderierte und sichere Kommunikation -- ✅ Blacklist-System fĂŒr Content-Filterung -- ✅ Server-ĂŒbergreifende Reputation -- ✅ Report & Block Funktionen -- ✅ Admin-Kontrolle & Moderation +
-**Wikipedia Integration** -- ✅ Direkte Wikipedia-Suche -- ✅ Formatierte Artikel-Previews -- ✅ Mehrsprachige UnterstĂŒtzung -- ✅ Schnelle Informationsabfrage +**💬 Cross-Server Communication** -**Weather System** -- ✅ Live-Wetterinformationen -- ✅ Detaillierte Vorhersagen -- ✅ Temperatur, Luftfeuchtigkeit, Wind -- ✅ Automatische Standorterkennung +
+ +```yaml +Globalchat System: + Core Features: + - Echtzeit Cross-Server Chat + - Moderierte Kommunikation + - Server-ĂŒbergreifende Community + - Report & Block Funktionen + - User Reputation System + + Safety Features: + ✓ Content-Filterung + ✓ Blacklist System + ✓ Admin-Kontrolle + ✓ Spam Prevention + ✓ Moderation Queue + ✓ Appeal Process + +Information Tools: + Wikipedia Integration: + - Direkte Artikelsuche + - Formatierte Previews + - Multi-Language Support + - Related Articles + - Quick Summaries + + Weather System: + - Live-Wetterdaten + - 5-Tage Vorhersage + - Detaillierte Metriken + - Location Auto-Detection + - Weather Alerts +```
### 🎼 Interaktive Features -**Temporary Voice Channels** -- ✅ User erstellen eigene Voice-Channel -- ✅ Individuelle Kanalverwaltung -- ✅ User-Limit & Permissions -- ✅ Auto-Delete bei InaktivitĂ€t -- ✅ Custom Namen & Kategorien +
+ +**đŸŽ™ïž Dynamic Voice & Analytics** -**Stats & Analytics** -- ✅ Server-Statistiken in Echtzeit -- ✅ User-Activity Tracking -- ✅ Command-Usage Analytics -- ✅ Performance Metriken +
+ +```yaml +Temporary Voice Channels: + User Control: + ✓ Eigene Voice-Channel erstellen + ✓ Custom Namen & Beschreibung + ✓ User-Limit Management + ✓ Permission Control + ✓ Channel Transfer + + Automation: + - Auto-Delete bei InaktivitĂ€t + - Category Organization + - Template System + - VIP Channel Options + +Stats & Analytics: + Real-Time Metrics: + - Server Activity Tracking + - User Engagement Stats + - Command Usage Analytics + - Voice Channel Statistics + - Growth Metrics + + Reports: + ✓ Daily/Weekly Summaries + ✓ Performance Dashboards + ✓ Member Insights + ✓ Trend Analysis +```
+ + + + +
+ +**Minimum Requirements** + +```yaml +Operating System: + - Linux (Ubuntu 20.04+) + - Windows 10/11 + - macOS 11+ + +Software: + - Python 3.10 oder höher + - pip (Python Package Manager) + - Git 2.0+ + +Resources: + - RAM: 512 MB minimum + - Storage: 200 MB freier Speicher + - Network: Stabile Internetverbindung +``` + + + +**Empfohlene Konfiguration** + ```yaml -Minimum Requirements: - OS: Linux / Windows / macOS - Python: 3.10 oder höher - RAM: 512 MB (empfohlen: 1 GB+) - Storage: 200 MB freier Speicher - Network: Stabile Internetverbindung - -Benötigte Services: - Discord Bot Token: discord.com/developers/applications - Database: SQLite3 (vorinstalliert) +Production Environment: + - Linux Server (Ubuntu 22.04 LTS) + - Python 3.11+ + - 1 GB+ RAM + - SSD Storage + - 24/7 Uptime Hosting + +Optional Services: + - Discord Bot Token (Required) + - Weather API Key (Optional) + - Custom Domain (Optional) + - SSL Certificate (Optional) ``` +
+
-### ⚡ Quick Start Guide +### ⚡ Installation Guide + +
+🐧 Linux / macOS Installation (Click to expand) + +
-**Linux / macOS:** ```bash -# 1. Repository klonen +# ────────────────────────────────────────────────────────── +# Step 1: Repository klonen +# ────────────────────────────────────────────────────────── git clone https://github.com/Oppro-net-Development/ManagerX.git cd ManagerX -# 2. Python Virtual Environment erstellen (empfohlen) +# ────────────────────────────────────────────────────────── +# Step 2: Virtual Environment erstellen (empfohlen) +# ────────────────────────────────────────────────────────── python3 -m venv venv source venv/bin/activate -# 3. Dependencies installieren +# ────────────────────────────────────────────────────────── +# Step 3: Dependencies installieren +# ────────────────────────────────────────────────────────── +pip install --upgrade pip pip install -r requirements.txt -# 4. Konfiguration erstellen +# ────────────────────────────────────────────────────────── +# Step 4: Konfiguration erstellen +# ────────────────────────────────────────────────────────── cp .env.example .env -nano .env # TOKEN und andere Einstellungen anpassen +nano .env # Passe TOKEN und andere Einstellungen an -# 5. Bot starten +# ────────────────────────────────────────────────────────── +# Step 5: Erste Datenbankinitialisierung +# ────────────────────────────────────────────────────────── +python -c "from utils.database import init_db; init_db()" + +# ────────────────────────────────────────────────────────── +# Step 6: Bot starten +# ────────────────────────────────────────────────────────── python main.py + +# ────────────────────────────────────────────────────────── +# Optional: Systemd Service erstellen (fĂŒr 24/7 Betrieb) +# ────────────────────────────────────────────────────────── +sudo nano /etc/systemd/system/managerx.service +# FĂŒge Service-Konfiguration hinzu (siehe Dokumentation) +sudo systemctl enable managerx +sudo systemctl start managerx ``` -**Windows:** +
+ +
+đŸȘŸ Windows Installation (Click to expand) + +
+ ```powershell -# 1. Repository klonen +# ────────────────────────────────────────────────────────── +# Step 1: Repository klonen +# ────────────────────────────────────────────────────────── git clone https://github.com/Oppro-net-Development/ManagerX.git cd ManagerX -# 2. Virtual Environment erstellen +# ────────────────────────────────────────────────────────── +# Step 2: Virtual Environment erstellen +# ────────────────────────────────────────────────────────── python -m venv venv venv\Scripts\activate -# 3. Dependencies installieren +# ────────────────────────────────────────────────────────── +# Step 3: Dependencies installieren +# ────────────────────────────────────────────────────────── +python -m pip install --upgrade pip pip install -r req.txt -# 4. Konfiguration +# ────────────────────────────────────────────────────────── +# Step 4: Konfiguration erstellen +# ────────────────────────────────────────────────────────── copy .env.example .env -notepad .env # TOKEN anpassen +notepad .env # TOKEN und Einstellungen anpassen -# 5. Bot starten +# ────────────────────────────────────────────────────────── +# Step 5: Datenbank initialisieren +# ────────────────────────────────────────────────────────── +python -c "from utils.database import init_db; init_db()" + +# ────────────────────────────────────────────────────────── +# Step 6: Bot starten +# ────────────────────────────────────────────────────────── python main.py + +# ────────────────────────────────────────────────────────── +# Optional: Als Windows Service einrichten +# ────────────────────────────────────────────────────────── +# Siehe Dokumentation fĂŒr NSSM Setup ``` +
+
-### 🔧 Konfiguration (.env) +### 🔧 Konfiguration + + + + + + +
+ +**Environment Variables (.env)** ```bash +# ════════════════════════════════════════════════ # Discord Bot Configuration +# ════════════════════════════════════════════════ DISCORD_TOKEN=your_bot_token_here -WEATHER_API=your_Weather_API_Key DISCORD_CLIENT_ID=your_client_id DISCORD_CLIENT_SECRET=your_client_secret -DISCORD_REDIRECT_URI=your_redirect_uri +DISCORD_REDIRECT_URI=http://localhost:8080/callback + +# ════════════════════════════════════════════════ +# Optional API Keys +# ════════════════════════════════════════════════ +WEATHER_API=your_openweathermap_api_key + +# ════════════════════════════════════════════════ +# Bot Settings +# ════════════════════════════════════════════════ +BOT_PREFIX=! +DEBUG_MODE=false +LOG_LEVEL=INFO + +# ════════════════════════════════════════════════ +# Database Configuration +# ════════════════════════════════════════════════ +DATABASE_PATH=./data/managerx.db +BACKUP_ENABLED=true +BACKUP_INTERVAL=24h ``` -
+
---- +**📍 Token erstellen** -## 📋 Version History & Changelog +1. Besuche [Discord Developer Portal](https://discord.com/developers/applications) +2. Klicke auf "New Application" +3. Gehe zu "Bot" → "Add Bot" +4. Kopiere den Token +5. FĂŒge ihn in `.env` ein -
+**🔑 Wichtige Berechtigungen** -| Version | Status | Highlights | -|---------|--------|------------| -| **v2.1.0** | 🔜 In Development | Bug Fixes, Performance Improvements | -| **v2.0.0** | ✅ Current | Refactored Codebase, Enhanced Stats, Globalchat v2 | -| **v1.7.1** | 📩 Stable | Enhanced Features, Bug Fixes | -| **v1.7.0** | 📩 Archived | TempVC System Implementation | -| **v1.6.0** | 📩 Archived | Levelsystem Launch | -| **v1.5.0** | 📩 Archived | Welcome System | +```yaml +Required Intents: + ✓ Server Members Intent + ✓ Message Content Intent + ✓ Presence Intent + +Bot Permissions: + ✓ Manage Roles + ✓ Manage Channels + ✓ Kick Members + ✓ Ban Members + ✓ Send Messages + ✓ Embed Links + ✓ Attach Files + ✓ Manage Messages + ✓ Read Message History +``` -[📖 View Full Changelog →](CHANGELOG.md) +**đŸŒŠïž Weather API** -
+Kostenloser API-Key: [OpenWeatherMap](https://openweathermap.org/api) + +

--- -## đŸ€ Contributing & Development +
-### đŸ’» Werde Teil des Projekts! +## 📋 Version History & Roadmap -Wir freuen uns ĂŒber Contributions von der Community. Hier sind unsere Commit-Konventionen: +*Entwicklungsgeschichte und zukĂŒnftige Features* + +
+ +
+ +### 🔄 Release Timeline + +```mermaid +gantt + title ManagerX Development Timeline + dateFormat YYYY-MM-DD + section Releases + v1.0.0 Initial Release :done, 2023-01-01, 30d + v1.5.0 Welcome System :done, 2023-05-01, 45d + v1.6.0 Levelsystem :done, 2023-08-01, 60d + v1.7.0 TempVC System :done, 2023-11-01, 45d + v2.0.0 Major Refactor :done, 2024-06-01, 90d + v2.1.0 Enhancements :active, 2025-01-01, 60d + v2.5.0 Advanced Features :2025-04-01, 90d +``` + +
- - - + + + + - - - + + + + - - - + + + + - - - + + + + - - - + + + + - - - + + + + - - - + + + +
PrÀfixBeschreibungBeispielVersionStatusDatumKey Features
FEATURE:Neue Funktion hinzugefĂŒgtFEATURE: Add weather commandv2.1.0🔜 In DevQ1 2025 +‱ Performance Optimierungen
+‱ Bug Fixes & Stability
+‱ Enhanced Error Handling
+‱ UI/UX Improvements +
UPDATE:Bestehende Funktion aktualisiertUPDATE: Improve levelsystem performancev2.0.0✅ Current2024-12 +‱ Komplettes Code Refactoring
+‱ Globalchat v2 Launch
+‱ Enhanced Statistics Module
+‱ Improved Database Architecture +
BUGFIX:Normaler Fehler behobenBUGFIX: Fix welcome message formattingv1.7.1📩 Stable2024-08 +‱ Feature Enhancements
+‱ Critical Bug Fixes
+‱ Security Updates +
HOTFIX:Kritischer Fehler behobenHOTFIX: Resolve database connection issuesv1.7.0📩 Archived2024-05 +‱ TempVC System Implementation
+‱ Dynamic Voice Channel Management
+‱ User Control Features +
DOCS:Dokumentation geĂ€ndertDOCS: Update installation guidev1.6.0📩 Archived2024-02 +‱ Advanced Levelsystem
+‱ XP & Rewards Engine
+‱ Leaderboard System +
DELETE:Datei oder Feature entferntDELETE: Remove deprecated commandv1.5.0📩 Archived2023-11 +‱ Welcome System Launch
+‱ Auto-Role Assignment
+‱ Custom Embeds +

-### 🔧 Development Workflow - -```bash -# 1. Repository forken & klonen -git clone https://github.com/YOUR_USERNAME/ManagerX.git -cd ManagerX - -# 2. Development Branch erstellen -git checkout -b feature/amazing-feature +### đŸ—ș Roadmap -# 3. Änderungen vornehmen und testen -python main.py # Testen - -# 4. Committen mit Konvention -git commit -m "FEATURE: Add amazing new feature" + + + + + + +
-# 5. Pushen -git push origin feature/amazing-feature +**🎯 v2.1.0 - Q1 2025** -# 6. Pull Request erstellen auf GitHub +```yaml +Focus: Stability & Polish + +Features: + - Performance Tuning + - Memory Optimization + - Enhanced Logging + - Bug Fixes + +Improvements: + - Error Recovery + - Database Indexing + - Command Response Time + - Resource Management ``` -
- ---- - -## 💬 Support & Community - -
+
-### 🆘 Brauchst du Hilfe? +**🚀 v2.2.0 - Q2 2025** -
+```yaml +Focus: New Features + +Planned Features: + - Ticket System + - Advanced Polls + - Music Module + - Custom Commands 2.0 + +Enhancements: + - AI Integration + - Multi-Language Support + - Enhanced Analytics + - API Webhooks +``` -[![Discord Server](https://img.shields.io/badge/Discord-Join_Community-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/tmz673WAnV) -[![Documentation](https://img.shields.io/badge/Docs-Read_More-00D9FF?style=for-the-badge&logo=gitbook&logoColor=white)](https://docs.oppro-network.de) -[![GitHub Issues](https://img.shields.io/badge/Issues-Report_Bug-EA4335?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Oppro-net-Development/ManagerX/issues) -[![Email](https://img.shields.io/badge/Email-Contact_Us-00D26A?style=for-the-badge&logo=gmail&logoColor=white)](mailto:development@oppro-network.de) +
-
+**đŸ’« v2.3.0 - Q4 2025** -| Support-Kanal | Beschreibung | Antwortzeit | -|---------------|--------------|-------------| -| 💬 **Discord** | Community Support & Diskussion | < 1 Stunde | -| 📖 **Docs** | Umfassende Anleitungen | Sofort | -| 🐛 **GitHub Issues** | Bug Reports & Feature Requests | < 24 Stunden | -| 📧 **E-Mail** | Direkter Support | < 48 Stunden | +```yaml +Focus: Major Upgrade + +Revolutionary Features: + - Web Dashboard + - Mobile App Support + - Plugin System + - Advanced AI Features + +Architecture: + - Microservices + - Cloud-Native + - Horizontal Scaling + - GraphQL API +``` - +

---- - -## 🏱 Empfohlener Hosting Partner -
-### 🚀 Professionelles Hosting fĂŒr ManagerX - -
- - - DeinServerHost - Premium Hosting - - -

- -**ZuverlĂ€ssiges, deutsches Hosting fĂŒr Discord Bots** - -✅ **24/7 Support** ‱ ✅ **99.9% Uptime** ‱ ✅ **DDoS Protection** ‱ ✅ **SSD Storage** - -
- -[![Jetzt buchen](https://img.shields.io/badge/Hosting-Jetzt_buchen-00D9FF?style=for-the-badge&logo=server&logoColor=white)](https://deinserverhost.de/store/aff.php?aff=5609) +[📖 **VollstĂ€ndiges Changelog anzeigen** →](CHANGELOG.md)
@@ -419,119 +800,163 @@ git push origin feature/amazing-feature --- -## 📊 GitHub Statistics -
-![Contributors](https://img.shields.io/github/contributors/Oppro-net-Development/ManagerX?style=for-the-badge&logo=github&logoColor=white&color=5865F2) -![Forks](https://img.shields.io/github/forks/Oppro-net-Development/ManagerX?style=for-the-badge&logo=github&logoColor=white&color=00D26A) -![Stars](https://img.shields.io/github/stars/Oppro-net-Development/ManagerX?style=for-the-badge&logo=github&logoColor=white&color=FFD700) -![Issues](https://img.shields.io/github/issues/Oppro-net-Development/ManagerX?style=for-the-badge&logo=github&logoColor=white&color=EA4335) -![Last Commit](https://img.shields.io/github/last-commit/Oppro-net-Development/ManagerX?style=for-the-badge&logo=github&logoColor=white) - -
- -
- ---- - -## 📄 Lizenz & Urheberrecht - -
- -``` -╔═══════════════════════════════════════════════════════════════╗ -║ ║ -║ This project is licensed under the GNU GPL-3.0 License ║ -║ ║ -║ Copyright © 2024 OPPRO.NET Development ║ -║ Copyright © 2025-present OPPRO.NET Networkℱ ║ -║ ║ -║ All rights reserved. ║ -║ ║ -╚═══════════════════════════════════════════════════════════════╝ -``` - -
- -[![License](https://img.shields.io/badge/License-GPL--3.0-blue?style=for-the-badge&logo=opensourceinitiative&logoColor=white)](LICENSE) +## đŸ€ Contributing & Development -**📖 VollstĂ€ndige Lizenz:** [LICENSE Datei anzeigen →](LICENSE) +*Werde Teil unseres Open-Source Projekts!*

---- +### 💡 Wie kann ich beitragen? -## 🙏 Credits & Danksagungen + + + + + + + +
-
+**🐛 Bug Reports** -| Team | Community | Frameworks | Hosting | -|:----:|:---------:|:----------:|:-------:| -| **OPPRO.NET
Development** | **Contributors
& Beta Testers** | **py-cord
ezcord** | **DeinServerHost
Premium Partner** | +Fehler gefunden?
+[Issue erstellen →](https://github.com/Oppro-net-Development/ManagerX/issues/new?template=bug_report.md) -
+
-**Besonderer Dank an alle, die dieses Projekt unterstĂŒtzen!** +**✹ Feature Requests** - +Idee fĂŒr ein Feature?
+[Feature vorschlagen →](https://github.com/Oppro-net-Development/ManagerX/issues/new?template=feature_request.md) -
+
---- +**đŸ’» Code BeitrĂ€ge** -## 🔗 Wichtige Links +Code beisteuern?
+[Pull Request →](https://github.com/Oppro-net-Development/ManagerX/pulls) -
+
-| Link | Beschreibung | -|------|--------------| -| 🌐 [**Website**](https://development.oppro-network.de/ManagerX/) | Offizielle ManagerX Website | -| 📚 [**Dokumentation**](https://docs.oppro-network.de/en/latest/) | VollstĂ€ndige Bot-Dokumentation | -| 🏠 [**OPPRO.NET**](https://oppro-network.de) | OPPRO.NET Networkℱ Hauptseite | -| 💬 [**Discord**](https://discord.gg/tmz673WAnV) | Community & Support Server | -| 📧 [**E-Mail**](mailto:development@oppro-network.de) | Direkter Kontakt | +**📖 Dokumentation** - +Docs verbessern?
+[Docs bearbeiten →](https://github.com/Oppro-net-Development/ManagerX-Docs) + +

---- +### 📝 Commit-Konventionen -
+Wir verwenden standardisierte Commit-Prefixes fĂŒr bessere Nachvollziehbarkeit: -### ⭐ Hat dir ManagerX geholfen? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PrÀfixVerwendungBeispiel
✹ FEATURE:Neue Funktion hinzugefĂŒgt✹ FEATURE: Add weather command integration
🔄 UPDATE:Bestehende Funktion verbessert🔄 UPDATE: Improve levelsystem performance by 40%
🐛 BUGFIX:Bug behoben (normal)🐛 BUGFIX: Fix welcome message formatting issue
🚑 HOTFIX:Kritischer Bug behoben🚑 HOTFIX: Resolve critical database connection error
📚 DOCS:Dokumentation aktualisiert📚 DOCS: Update installation guide with troubleshooting
đŸ—‘ïž DELETE:Code/Feature entferntđŸ—‘ïž DELETE: Remove deprecated legacy commands
🎹 STYLE:Code-Style Änderungen🎹 STYLE: Refactor code to match PEP 8 standards
♻ REFACTOR:Code-Umstrukturierung♻ REFACTOR: Restructure database module architecture
⚡ PERF:Performance-Verbesserung⚡ PERF: Optimize query execution time
đŸ§Ș TEST:Tests hinzugefĂŒgt/geĂ€ndertđŸ§Ș TEST: Add unit tests for moderation module

-**Gib uns einen Stern auf GitHub und teile das Projekt!** +### 📌 Versionierungs-Schema -
+Um maximale Transparenz und AktualitĂ€t zu gewĂ€hrleisten, nutzen wir eine duale Strategie: -[![GitHub Stars](https://img.shields.io/github/stars/Oppro-net-Development/ManagerX?style=social)](https://github.com/Oppro-net-Development/ManagerX) -[![GitHub Forks](https://img.shields.io/github/forks/Oppro-net-Development/ManagerX?style=social)](https://github.com/Oppro-net-Development/ManagerX/fork) -[![GitHub Watchers](https://img.shields.io/github/watchers/Oppro-net-Development/ManagerX?style=social)](https://github.com/Oppro-net-Development/ManagerX) +* **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` | -``` -╔═══════════════════════════════════════════════════════════╗ -║ ║ -║ Made with ❀ in Germany ║ -║ by the OPPRO.NET Development Team ║ -║ ║ -║ Bringing communities together, one bot at a time ║ -║ ║ -╚═══════════════════════════════════════════════════════════╝ -``` +### 🔧 Development Workflow -
+```bash +# ════════════════════════════════════════════════════════════════════════ +# 1. Repository forken & klonen +# ════════════════════════════════════════════════════════════════════════ +git clone https://github.com/YOUR_USERNAME/ManagerX.git +cd ManagerX -![Made with Love](https://img.shields.io/badge/Made%20with-❀-red?style=for-the-badge) -![Powered by Coffee](https://img.shields.io/badge/Powered%20by-☕-brown?style=for-the-badge) -![Made in Germany](https://img.shields.io/badge/Made%20in-đŸ‡©đŸ‡Ș%20Germany-black?style=for-the-badge) -![Built with Python](https://img.shields.io/badge/Built%20with-🐍%20Python-blue?style=for-the-badge) +# ════════════════════════════════════════════════════════════════════════ +# 2. Development Branch erstellen +# ════════════════════════════════════════════════════════════════════════ +git checkout -b feature/amazing-feature +# Oder fĂŒr Bugfixes: +git checkout -b bugfix/fix-critical-issue -
\ No newline at end of file +# ════════════════════════════════════════════════════════════════════════ +# 3. Development Environment aufsetzen +# ════════════════════════════════════════════════════════════════════════ +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate +pip install -r requirements-dev.txt + +# ════════════════════════════════════════════════════════════════════════ +# 4. Änderungen vornehmen und testen +# ════════════════════════════════════════════════════════════════════════ +# Code schreiben... +python main.py # Bot testen +pytest tests/ # Unit Tests ausfĂŒhren + +# ════════════════════════════════════════════════════════════════════════ +# 5. Code Style prĂŒfen +# \ No newline at end of file diff --git a/api.py b/api.py deleted file mode 100644 index f61f2d5..0000000 --- a/api.py +++ /dev/null @@ -1,455 +0,0 @@ -import os -import httpx -import logging -import json -import sys -from fastapi import FastAPI, HTTPException, Query, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from fastapi import Depends -from fastapi.staticfiles import StaticFiles -from pydantic import BaseModel -from typing import Optional, List, Dict, Any -from dotenv import load_dotenv -import yaml - -security = HTTPBearer() - -def get_token(credentials: HTTPAuthorizationCredentials = Depends(security)): - return credentials.credentials - -# Import deiner Datenbank-Klasse -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: - from DevTools import TempVCDatabase - # Fallback if not available - WelcomeDatabase = None - LevelDatabase = None - -# --- LOGGING SETUP (KEIN SPAM) --- -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")) - -# --- KONFIGURATION --- -load_dotenv(os.path.join("config", ".env")) - -# Lade Bot-Config fĂŒr interne PrĂŒfungen -config_path = os.path.join("config", "config.yaml") -try: - import yaml - 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 ImportError: - logger.warning("PyYAML nicht installiert, Config-PrĂŒfungen deaktiviert") - bot_config = {} -except Exception as e: - logger.error(f"Fehler beim Laden der Config: {e}") - bot_config = {} - -app = FastAPI(title="ManagerX Ultimate API") - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# --- STATISCHE DATEIEN SERVIEREN --- -app.mount("/site", StaticFiles(directory="site", html=True), name="site") - -# --- HILFSFUNKTIONEN --- -def is_feature_enabled(feature_path: str) -> bool: - """PrĂŒft, ob ein Feature in der Config aktiviert ist. z.B. 'features.cogs.server_management.tempvc'""" - keys = feature_path.split('.') - current = bot_config - try: - for key in keys: - current = current.get(key, {}) - return current if isinstance(current, bool) else True # Standard True - except: - return True - -# Datenbank Instanz (Pfad zur .db Datei) -DB_PATH = os.path.join("data", "tempvc.db") -db = TempVCDatabase(DB_PATH) - -# Welcome DB -welcome_db = WelcomeDatabase() if WelcomeDatabase else None - -# Level DB -level_db = LevelDatabase() if LevelDatabase else None - -# --- 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 - -# --- ADMIN-CHECK LOGIK --- -async def check_admin_permissions(guild_id: int, token: str): - """PrĂŒft bei Discord, ob der User wirklich Admin auf dem Server ist.""" - 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 Exception as e: - if isinstance(e, HTTPException): raise e - logger.error(f"Fehler bei Discord-Validierung: {e}") - raise HTTPException(status_code=500, detail="Discord API Kommunikationsfehler") - -# --- ALLE ENDPUNKTE --- - -# 0. ROOT REDIRECT -@app.get("/") -async def root(): - from fastapi.responses import RedirectResponse - return RedirectResponse(url="/site/index.html") - -# 1. BOT STATS (Neue & Alte Route) -@app.get("/api/managerx/stats") -@app.get("/api/v2/stats") -async def get_bot_stats(): - 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: - pass - return { - "stats": {"server_count": 50, "user_count": 15000}, - "bot_info": {"latency": 35, "status": "Online"} - } - -# 2. OAUTH2 CALLBACK (LOGIN) -@app.get("/api/auth/callback") -async def auth_callback(code: str): - 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: dict): - refresh_token = data.get('refresh_token') - if not refresh_token: - raise HTTPException(status_code=400, detail="Kein Refresh-Token") - - 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': 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')} - -# 3. GUILD LISTE (DASHBOARD) -@app.get("/api/user/guilds") -async def get_user_guilds(token: str = Depends(get_token)): - 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] - -# 3. GUILD CHANNELS (fĂŒr Dropdowns) -@app.get("/api/guild/{guild_id}/channels") -async def get_guild_channels(guild_id: int, token: str = Depends(get_token)): - await check_admin_permissions(guild_id, token) - - # Hole Guild-Info von Discord API - 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} - -# 4. TEMPVC LADEN (GET) -@app.get("/api/guild/{guild_id}/tempvc") -async def get_tempvc(guild_id: int, token: str = Depends(get_token)): - 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 in der Bot-Config deaktiviert") - - settings = db.get_tempvc_settings(guild_id) # Erwartet Tuple/List aus DB - ui = db.get_ui_settings(guild_id) # Erwartet Tuple/List aus DB - - 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 "🔧" - } - -# 5. TEMPVC SPEICHERN (POST) -@app.post("/api/guild/{guild_id}/tempvc") -async def save_tempvc(guild_id: int, data: TempVCUpdate): - # Admin-Validierung - 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 in der Bot-Config deaktiviert") - - try: - # Konvertierung zu Integer fĂŒr SQLite - 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}") - - # Datenbankbefehle ausfĂŒhren - 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 beim Schreiben: {e}") - raise HTTPException(status_code=500, detail="Interner Datenbank-Fehler") - -# 6. WELCOME LADEN (GET) -@app.get("/api/guild/{guild_id}/welcome") -async def get_welcome(guild_id: int, token: str = Depends(get_token)): - 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 in der Bot-Config 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) - } - -# 7. WELCOME SPEICHERN (POST) -@app.post("/api/guild/{guild_id}/welcome") -async def save_welcome(guild_id: int, data: WelcomeUpdate): - # Admin-Validierung - 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 in der Bot-Config deaktiviert") - - if not welcome_db: - raise HTTPException(status_code=500, detail="Welcome Database nicht verfĂŒgbar") - - try: - # Konvertierung - ch_id = int(data.channel_id) if data.channel_id else None - - logger.info(f"đŸ’Ÿ SPEICHERN WELCOME: Guild {guild_id} | Channel: {ch_id}") - - # Datenbank speichern - 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") - -# 8. LEVELSYSTEM LADEN (GET) -@app.get("/api/guild/{guild_id}/levelsystem") -async def get_levelsystem(guild_id: int, token: str = Query(...)): - 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 in der Bot-Config 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) - } - -# 9. LEVELSYSTEM SPEICHERN (POST) -@app.post("/api/guild/{guild_id}/levelsystem") -async def save_levelsystem(guild_id: int, data: LevelUpdate): - # Admin-Validierung - 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 in der Bot-Config deaktiviert") - - if not level_db: - raise HTTPException(status_code=500, detail="Levelsystem Database nicht verfĂŒgbar") - - try: - # Konvertierung - 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}") - - # Datenbank speichern - 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") - -if __name__ == "__main__": - import uvicorn - # log_level="warning" hĂ€lt die Konsole sauber - 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/components.json b/components.json new file mode 100644 index 0000000..295891d --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/site/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file 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 359b611..0000000 --- a/docs/_static/custom.css +++ /dev/null @@ -1,368 +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; -} - -/* --- 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 5b8f837..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 c635b0c..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/index.rst b/docs/dev_guide/contributing/index.rst deleted file mode 100644 index 922f427..0000000 --- a/docs/dev_guide/contributing/index.rst +++ /dev/null @@ -1,106 +0,0 @@ -Contributing to ManagerX -======================== - -Welcome to the ManagerX development community! This section provides guidelines and best practices for contributing to the ManagerX project. Whether you're fixing bugs, adding new features, or improving documentation, your contributions are valuable and appreciated. - -How to Contribute ------------------ - -1. Fork the Repository - Start by forking the ManagerX repository on GitHub to create your own copy of the project. - -2. Clone Your Fork - Clone your forked repository to your local machine using the following command:: - - git clone https://github.com/Oppro-net-Development/ManagerX.git - -3. Create a New Branch - Before making any changes, create a new branch for your work. This helps keep your changes organized and separate from the main codebase:: - - git checkout -b feature/your-feature-name - -4. Make Your Changes - Implement your changes in the codebase. Ensure that your code follows the project's coding standards and best practices. - -5. Test Your Changes - Thoroughly test your changes to ensure they work as expected and do not introduce any new issues. - -6. Commit Your Changes - Once you're satisfied with your changes, commit them to your branch with a descriptive commit message:: - - git add . - git commit -m "Add feature: your feature description" - -7. Push Your Changes - Push your branch to your forked repository on GitHub:: - - git push origin feature/your-feature-name - -8. Create a Pull Request - Navigate to the original ManagerX repository on GitHub and create a pull request from your forked repository. Provide a clear description of your changes and any relevant context. - -9. Address Feedback - Be prepared to address any feedback or requests for changes from the project maintainers. Collaboration is key to maintaining a high-quality codebase. - -10. Celebrate Your Contribution - Once your pull request is merged, celebrate your contribution to the ManagerX project! - -Issue Reporting ---------------- - -If you encounter any bugs or issues while using ManagerX, please report them on the GitHub repository: - -- Go to the `Issues `_ tab. -- Check if the issue has already been reported. -- If not, create a new issue with a **descriptive title** and detailed description. -- Include steps to reproduce the issue, expected behavior, and actual behavior. -- Attach any relevant logs or screenshots if applicable. - -Coding Standards ----------------- - -To maintain code quality, please follow these guidelines: - -- Follow PEP8 style guide for Python code. -- Use meaningful variable and function names. -- Write modular and reusable code. -- Add docstrings for all public functions and classes. -- Ensure backward compatibility wherever possible. - -Testing -------- - -- Write unit tests for new features and bug fixes. -- Ensure all existing tests pass before submitting a pull request. -- Use pytest or the built-in unittest framework. -- Document how to run tests in your pull request description. - -Documentation -------------- - -Good documentation is crucial for maintaining ManagerX: - -- Update existing documentation if your changes affect it. -- Add examples for new commands or features. -- Use the same reStructuredText format as in the `Developer Guide `_. -- Proofread for clarity and grammar. - -Resources ---------- - -- `ManagerX GitHub Repository `_ -- `Developer Guide `_ -- `Issue Tracker `_ -- `Code of Conduct `_ - -Community Guidelines -------------------- - -We value a positive and inclusive community. Please adhere to the following guidelines when contributing to ManagerX: - -- Be respectful and considerate in all interactions. -- Follow the project's code of conduct. -- Provide constructive feedback and be open to receiving it. -- Collaborate and communicate effectively with other contributors. - -Thank you for contributing to ManagerX! Your efforts help make this project better for everyone. 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 aecea91..0000000 --- a/docs/dev_guide/index.rst +++ /dev/null @@ -1,13 +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, including architecture overviews, API references, and development best practices. - -.. toctree:: - :maxdepth: 3 - :caption: Developer Guide: - - Architecture - Contributing - Database - API Reference - 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/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/source/changelog/index.rst b/docs/source/changelog/index.rst new file mode 100644 index 0000000..cfbea3d --- /dev/null +++ b/docs/source/changelog/index.rst @@ -0,0 +1,3 @@ +All Versions of ManagerX. +========================================= + 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 6e0d510..c1b17ee 100644 --- a/docs/conf.py +++ b/docs/source/conf.py @@ -7,11 +7,12 @@ import sys sys.path.insert(0, os.path.abspath('../src')) +sys.setrecursionlimit(2500) # -- 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' @@ -53,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', @@ -62,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 52a19c6..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+ @@ -114,8 +113,9 @@ We welcome contributions from the community! Whether it's bug reports, feature r User Guide Developer Guide Changelog & Releases + Plugins --- -**© 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/commands/moderation.rst b/docs/user_guide/commands/moderation.rst deleted file mode 100644 index e69de29..0000000 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/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..40f72cc --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,26 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + "@typescript-eslint/no-unused-vars": "off", + }, + }, +); diff --git a/handler/__init__.py b/handler/__init__.py deleted file mode 100644 index d21a21c..0000000 --- a/handler/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .translation_handler import TranslationHandler -from .update_checker import VersionChecker \ No newline at end of file diff --git a/handler/translation_handler.py b/handler/translation_handler.py deleted file mode 100644 index 751a79e..0000000 --- a/handler/translation_handler.py +++ /dev/null @@ -1,89 +0,0 @@ -# src/handler/translation_handler.py - -import yaml -from pathlib import Path - -class TranslationHandler: - """ - Zentrale Klasse zum Laden und Abrufen von Übersetzungen. - UnterstĂŒtzt: - - Caching - - Fallback-Sprachen - - get_for_user - - Pfad als Liste oder String - """ - - TRANSLATION_PATH = Path("translation") / "messages" - FALLBACK_LANGS = ("en", "de") - DEFAULT_LANGUAGE = "en" - _cache: dict[str, dict] = {} - - @classmethod - def load_messages(cls, lang_code: str) -> dict: - """ - LĂ€dt Sprachdateien mit Cache und Fallback. - """ - if lang_code in cls._cache: - return cls._cache[lang_code] - - for code in (lang_code, *cls.FALLBACK_LANGS): - file_path = cls.TRANSLATION_PATH / f"{code}.yaml" - - if file_path.exists(): - with open(file_path, "r", encoding="utf-8") as f: - data = yaml.safe_load(f) or {} - cls._cache[lang_code] = data - return data - - print(f"[TranslationHandler] WARNUNG: Keine Sprachdatei gefunden ({lang_code})") - cls._cache[lang_code] = {} - return {} - - @classmethod - def get(cls, lang_code: str, path, default: str = "", **placeholders) -> str: - """ - Holt eine Übersetzung ĂŒber einen Pfad (Liste oder Punkt-String) - und ersetzt Platzhalter. - """ - if isinstance(path, str): - path = path.split(".") - - messages = cls.load_messages(lang_code) - value = messages - - for key in path: - if not isinstance(value, dict): - return default - value = value.get(key) - - if not isinstance(value, str): - return default - - try: - return value.format(**placeholders) - except KeyError: - return value - - @classmethod - async def get_for_user(cls, bot, user_id: int, path, default: str = "", **placeholders) -> str: - """ - Holt die Übersetzung automatisch fĂŒr einen User. - Benötigt `bot.settings_db.get_user_language(user_id)`. - """ - lang = cls.DEFAULT_LANGUAGE - try: - lang = bot.settings_db.get_user_language(user_id) or cls.DEFAULT_LANGUAGE - except Exception: - pass - - return cls.get(lang, path, default, **placeholders) - - @classmethod - def clear_cache(cls): - """Löscht den Cache (nĂŒtzlich fĂŒr Hot-Reload im DEV).""" - cls._cache.clear() - - -# Optionaler Alias -MessagesHandler = TranslationHandler -LangHandler = TranslationHandler \ No newline at end of file diff --git a/handler/update_checker.py b/handler/update_checker.py deleted file mode 100644 index b1fa1e1..0000000 --- a/handler/update_checker.py +++ /dev/null @@ -1,218 +0,0 @@ - # Copyright (c) 2025 OPPRO.NET Network -""" -Update Checker Module for ManagerX -=================================== - -Handles version checking and update notifications for the bot. -Compares current version against remote version to detect available updates. - -Version Format: MAJOR.MINOR.PATCH[-TYPE] - - TYPE: dev, beta, alpha, or stable (default) - - Example: 1.7.2-alpha, 2.0.0, 1.5.1-beta -""" - -import re -import asyncio -import aiohttp -from typing import Optional, Tuple -from colorama import Fore, Style - -try: - from log import logger, C -except ImportError: - # Fallback if logger is not available - import logging - logger = logging.getLogger(__name__) - - -# ============================================================================= -# Configuration -# ============================================================================= - -class UpdateCheckerConfig: - """ - Configuration for the Update Checker. - - Contains all URLs and settings needed for version checking. - """ - # GitHub repository - GITHUB_REPO = "https://github.com/Oppro-net-Development/ManagerX" - - # Raw GitHub URL for version file - # Points to the version.txt file on the main branch - VERSION_URL = "https://raw.githubusercontent.com/Oppro-net-Development/ManagerX/main/config/version.txt" - - # Timeout for version check requests (in seconds) - TIMEOUT = 10 - - -# ============================================================================= -# Version Checker Class -# ============================================================================= - -class VersionChecker: - """ - Handles version parsing and update checking for ManagerX. - - Supports semantic versioning with optional pre-release type identifiers. - Automatically detects when updates are available and logs appropriate messages. - - Attributes - ---------- - GITHUB_REPO : str - Repository URL for update notifications - VERSION_URL : str - URL to the version file on GitHub - TIMEOUT : int - Timeout for requests in seconds - - Examples - -------- - >>> current = "1.7.2-alpha" - >>> latest = await VersionChecker.check_update(current, UpdateCheckerConfig.VERSION_URL) - >>> print(f"Latest version: {latest}") - """ - - GITHUB_REPO = UpdateCheckerConfig.GITHUB_REPO - VERSION_URL = UpdateCheckerConfig.VERSION_URL - TIMEOUT = UpdateCheckerConfig.TIMEOUT - - @staticmethod - def parse_version(version_str: str) -> Tuple[int, int, int, str]: - """ - Parse version string into components. - - Parses semantic versioning with optional pre-release type. - Format: MAJOR.MINOR.PATCH[-TYPE] - - Parameters - ---------- - version_str : str - Version string to parse (e.g., "1.7.2-alpha") - - Returns - ------- - tuple - Tuple of (major, minor, patch, type) - - major : int - - minor : int - - patch : int - - type : str - 'dev', 'beta', 'alpha', or 'stable' (default) - - Examples - -------- - >>> VersionChecker.parse_version("1.7.2-alpha") - (1, 7, 2, 'alpha') - - >>> VersionChecker.parse_version("2.0.0") - (2, 0, 0, 'stable') - """ - match = re.match(r"(\d+)\.(\d+)\.(\d+)(?:[-_]?(dev|beta|alpha))?", version_str) - if match: - major, minor, patch, vtype = match.groups() - return int(major), int(minor), int(patch), vtype or "stable" - return 0, 0, 0, "unknown" - - @staticmethod - async def check_update(current_version: str, version_url: str) -> Optional[str]: - """ - Check for available updates by comparing current and latest versions. - - Fetches the latest version from a remote URL and compares it with - the current version. Logs appropriate messages based on comparison: - - - Up to date: Success message - - Dev build: Info message - - Pre-release: Warning message - - Update available: Yellow alert with download link - - Error: Error message with details - - Parameters - ---------- - current_version : str - Current bot version (e.g., "1.7.2-alpha") - version_url : str - URL pointing to the latest version number - Should return plain text with only the version number - - Returns - ------- - Optional[str] - Latest version string if successfully retrieved, None if error occurred - - Raises - ------ - None - All exceptions are caught and logged - - Examples - -------- - >>> url = "https://raw.githubusercontent.com/.../version.txt" - >>> latest = await VersionChecker.check_update("1.7.2", url) - >>> if latest and latest > "1.7.2": - ... print("Update available!") - - Notes - ----- - Network timeouts are set to 10 seconds. Failed connections are - logged but do not prevent bot startup. - """ - try: - async with aiohttp.ClientSession() as session: - async with session.get(version_url, timeout=aiohttp.ClientTimeout(total=VersionChecker.TIMEOUT)) as resp: - if resp.status != 200: - logger.error(C.DEV.VER, f"Version check failed (HTTP {resp.status})") - return None - - latest_version = (await resp.text()).strip() - if not latest_version: - logger.error(C.DEV.UPDATE, "Empty response from version server") - return None - - # Parse versions for comparison - current = VersionChecker.parse_version(current_version) - latest = VersionChecker.parse_version(latest_version) - - # Compare major, minor, patch (ignore pre-release type) - current_core = current[:3] - latest_core = latest[:3] - - # Version is up to date - if current_core == latest_core and current[3] == latest[3]: - logger.success(C.DEV.VER, f"Running latest version: {current_version}") - - # Dev build newer than public release - elif current_core > latest_core: - logger.info( - C.DEV.VER, - f"Dev build detected ({current_version}) - newer than public release ({latest_version})" - ) - - # Pre-release version - elif current_core == latest_core and current[3] in ("dev", "beta", "alpha"): - logger.warn( - C.DEV.VER, - f"Pre-release version ({current_version}) - latest stable: {latest_version}" - ) - - # Update available - else: - print( - f"[{Fore.YELLOW}UPDATE AVAILABLE{Style.RESET_ALL}] " - f"Current: {current_version} → Latest: {latest_version}\n" - f" Download: {Fore.CYAN}{VersionChecker.GITHUB_REPO}{Style.RESET_ALL}" - ) - - return latest_version - - except aiohttp.ClientConnectorError: - logger.error(C.DEV.UPDATE, "Could not connect to GitHub (network issue)") - except asyncio.TimeoutError: - logger.error(C.DEV.UPDATE, "Connection to version server timed out") - except Exception as e: - logger.error(C.DEV.UPDATE, f"Unexpected error: {e}") - - return None - - -__all__ = ["VersionChecker", "UpdateCheckerConfig"] diff --git a/index.html b/index.html new file mode 100644 index 0000000..0632865 --- /dev/null +++ b/index.html @@ -0,0 +1,28 @@ + + + + + + ManagerX - Der ultimative Discord Bot + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/main.py b/main.py index 780d550..30b27d9 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,263 +10,123 @@ # 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 log 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 handler import VersionChecker - from DevTools import SettingsDB - - class BotConfig: - VERSION = "2.0.0-dev" - 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 -# ============================================================================= + # 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") -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' - } - } - - 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), - "version": BotConfig.VERSION, - "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__': - print(f"\n{Fore.CYAN}{'=' * 60}") - print(f" ManagerX Discord Bot v{BotConfig.VERSION}") - print(f"{'=' * 60}{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 --- - # EzCord's ignored_cogs filtert gegen den Dateinamen (z.B. "autocomplete") - 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!") + 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/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f15f094 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8311 @@ +{ + "name": "vite_react_shadcn_ts", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vite_react_shadcn_ts", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.83.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.26.2", + "input-otp": "^1.4.2", + "lucide-react": "^0.462.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.61.1", + "react-resizable-panels": "^2.1.9", + "react-router-dom": "^6.30.1", + "recharts": "^2.15.4", + "sonner": "^1.7.4", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "zod": "^3.25.76" + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@tailwindcss/typography": "^0.5.16", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@types/node": "^22.16.5", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react-swc": "^3.11.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "jsdom": "^20.0.3", + "lovable-tagger": "^1.1.13", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", + "vite": "^5.4.19", + "vitest": "^3.2.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.8.tgz", + "integrity": "sha512-5nZrJTF7gH+e0nZS7/QxFz6tJV4VimhQb1avEgtsJxvvIp5JilL+c58HICsKzPxghdwaDt48hEfPM1au4zGy+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", + "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.8.tgz", + "integrity": "sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.8", + "@swc/core-darwin-x64": "1.15.8", + "@swc/core-linux-arm-gnueabihf": "1.15.8", + "@swc/core-linux-arm64-gnu": "1.15.8", + "@swc/core-linux-arm64-musl": "1.15.8", + "@swc/core-linux-x64-gnu": "1.15.8", + "@swc/core-linux-x64-musl": "1.15.8", + "@swc/core-win32-arm64-msvc": "1.15.8", + "@swc/core-win32-ia32-msvc": "1.15.8", + "@swc/core-win32-x64-msvc": "1.15.8" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.8.tgz", + "integrity": "sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.8.tgz", + "integrity": "sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.8.tgz", + "integrity": "sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.8.tgz", + "integrity": "sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.8.tgz", + "integrity": "sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.8.tgz", + "integrity": "sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.8.tgz", + "integrity": "sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.8.tgz", + "integrity": "sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.8.tgz", + "integrity": "sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.8.tgz", + "integrity": "sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.19", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.19.tgz", + "integrity": "sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.19", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.19.tgz", + "integrity": "sha512-qTZRZ4QyTzQc+M0IzrbKHxSeISUmRB3RPGmao5bT+sI6ayxSRhn0FXEnT5Hg3as8SBFcRosrXXRFB+yAcxVxJQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz", + "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz", + "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/type-utils": "8.53.0", + "@typescript-eslint/utils": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.53.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz", + "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", + "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.53.0", + "@typescript-eslint/types": "^8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", + "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", + "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz", + "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/utils": "8.53.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz", + "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", + "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.0", + "@typescript-eslint/tsconfig-utils": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz", + "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", + "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", + "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.27.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.27.0.tgz", + "integrity": "sha512-gJtqOKEDJH/jrn0PpsWp64gdOjBvGX8hY6TWstxjDot/85daIEtJHl1UsiwHSXiYmJF2QXUoXP6/3gGw5xY2YA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.27.0", + "motion-utils": "^12.24.10", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lovable-tagger": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/lovable-tagger/-/lovable-tagger-1.1.13.tgz", + "integrity": "sha512-RBEYDxao7Xf8ya29L0cd+ocE7Gs80xPOIOwwck65Hoie8YDKViuXi3UYV14DoNWIvaJ7WVPf7SG3cc844nFqGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "tailwindcss": "^3.4.17" + }, + "peerDependencies": { + "vite": ">=5.0.0 <8.0.0" + } + }, + "node_modules/lucide-react": { + "version": "0.462.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.462.0.tgz", + "integrity": "sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/motion-dom": { + "version": "12.27.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.27.0.tgz", + "integrity": "sha512-oDjl0WoAsWIWKl3GCDxmh7GITrNjmLX+w5+jwk4+pzLu3VnFvsOv2E6+xCXeH72O65xlXsr84/otiOYQKW/nQA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.24.10" + } + }, + "node_modules/motion-utils": { + "version": "12.24.10", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.24.10.tgz", + "integrity": "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-themes": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", + "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "8.10.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", + "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.71.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", + "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz", + "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sonner": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", + "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.0.tgz", + "integrity": "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.53.0", + "@typescript-eslint/parser": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/utils": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vaul": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz", + "integrity": "sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e666902 --- /dev/null +++ b/package.json @@ -0,0 +1,90 @@ +{ + "name": "vite_react_shadcn_ts", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "build:dev": "vite build --mode development", + "lint": "eslint .", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.83.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.26.2", + "input-otp": "^1.4.2", + "lucide-react": "^0.462.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.61.1", + "react-resizable-panels": "^2.1.9", + "react-router-dom": "^6.30.1", + "recharts": "^2.15.4", + "sonner": "^1.7.4", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "zod": "^3.25.76" + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@tailwindcss/typography": "^0.5.16", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@types/node": "^22.16.5", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react-swc": "^3.11.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "jsdom": "^20.0.3", + "lovable-tagger": "^1.1.13", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", + "vite": "^5.4.19", + "vitest": "^3.2.4" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6e5ae71 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,139 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "ManagerX" +version = "2.2026.01.11" +description = "A powerful Discord bot for server management and fun." +readme = "README.md" +requires-python = ">=3.8" +license = {text = "GPL-3.0-or-later"} +authors = [ + {name = "OPPRO.NET Development", email = "development@oppro-network.de"}, + {name = "OPPRO.NET Network", email = "contact@oppro-network.de"} +] +classifiers = [ + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Operating System :: OS Independent" +] + +# Korrigierte Syntax: Direktes Array +dependencies = [ + "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", + "SimpleColoredLogs==1.3.0", + "FastAPI==0.100.0", + "uvicorn==0.24.0", + "Jinja2==3.1.6", + "MarkupSafe==3.0.3", + "starlette==0.50.0", + "timedelta==2020.12.3", + "ManagerX-Handler==1.2026.01.10", + "ManagerX-DevTools==1.2026.01.11.1" +] + +[project.urls] +Homepage = "https://github.com/Oppro-net-Development/ManagerX" +Documentation = "https://docs.oppro-network.de/" +Changelog = "https://docs.oppro-network.de/en/latest/releases/index.html" +BugTracker = "https://github.com/Oppro-net-Development/ManagerX/issues" + +[project.optional-dependencies] +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", + "build==1.3.0", + "twine==4.2.0" +] + +docs = [ + "sphinx<8,>=6", + "pydata-sphinx-theme", + "sphinx-autodoc-typehints", + "myst-parser<3,>=2", + "sphinx-copybutton" +] + +all = [ + "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", + "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==0.100.0", + "uvicorn==0.24.0", + "SimpleColoredLogs==1.3.0", + "timedelta==2020.12.3", + "sphinx<8,>=6", + "pydata-sphinx-theme", + "sphinx-autodoc-typehints", + "myst-parser<3,>=2", + "sphinx-copybutton", + "build==1.3.0", + "twine==4.2.0" +] + +[project.scripts] +# Standard-Weg fĂŒr CLI-Befehle +managerx-cli = "managerx.cli:main" +managerx = "managerx.__main__:main" + +[tool.setuptools] +# Definiert, wo der Quellcode liegt +package-dir = {"src" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] +include = ["managerx*", "plugins*", "DevTools*", "cogs*"] +exclude = ["tests*"] \ No newline at end of file diff --git a/requirements/bot_req.txt b/requirements/bot_req.txt index 5608281..71eee76 100644 --- a/requirements/bot_req.txt +++ b/requirements/bot_req.txt @@ -11,4 +11,4 @@ soupsieve==2.8.1 yarl==1.22.0 frozenlist==1.8.0 h11==0.16.0 -multidict==6.7.0 +multidict==6.7.0 \ No newline at end of file diff --git a/requirements/dev_req.txt b/requirements/dev_req.txt index 4d947f2..bc0bcf2 100644 --- a/requirements/dev_req.txt +++ b/requirements/dev_req.txt @@ -16,4 +16,4 @@ starlette==0.50.0 FastAPI uvicorn SimpleColoredLogs -timedelta==2020.12.3 +timedelta==2020.12.3 \ No newline at end of file diff --git a/requirements/docs_req.txt b/requirements/docs_req.txt index 57b3817..c593d1e 100644 --- a/requirements/docs_req.txt +++ b/requirements/docs_req.txt @@ -4,4 +4,4 @@ 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) + # (remove this before pushing; ReadTheDocs doesn't need it) \ 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/cogs/Servermanament/globalchat.py b/src/bot/cogs/guild/globalchat.py similarity index 99% rename from src/cogs/Servermanament/globalchat.py rename to src/bot/cogs/guild/globalchat.py index 295ad0b..daa4686 100644 --- a/src/cogs/Servermanament/globalchat.py +++ b/src/bot/cogs/guild/globalchat.py @@ -740,6 +740,7 @@ class GlobalChatCog(ezcord.Cog): """Haupt-Cog fĂŒr das GlobalChat-System""" globalchat = SlashCommandGroup("globalchat", "GlobalChat Verwaltung") + def __init__(self, bot): self.bot = bot diff --git a/src/cogs/Servermanament/levelsystem.py b/src/bot/cogs/guild/levelsystem.py similarity index 100% rename from src/cogs/Servermanament/levelsystem.py rename to src/bot/cogs/guild/levelsystem.py diff --git a/src/cogs/Servermanament/logging.py b/src/bot/cogs/guild/loggingsystem.py similarity index 100% rename from src/cogs/Servermanament/logging.py rename to src/bot/cogs/guild/loggingsystem.py diff --git a/src/cogs/Servermanament/tempvc.py b/src/bot/cogs/guild/tempvc.py similarity index 99% rename from src/cogs/Servermanament/tempvc.py rename to src/bot/cogs/guild/tempvc.py index 9c17455..87fb7ec 100644 --- a/src/cogs/Servermanament/tempvc.py +++ b/src/bot/cogs/guild/tempvc.py @@ -3,7 +3,6 @@ import discord from discord import slash_command, option, SlashCommandGroup from discord.ext import commands -from DevTools import emoji_yes, emoji_no from discord.ui import Container import ezcord diff --git a/src/cogs/Servermanament/welcome.py b/src/bot/cogs/guild/welcome.py similarity index 99% rename from src/cogs/Servermanament/welcome.py rename to src/bot/cogs/guild/welcome.py index a7c8688..cc9fb69 100644 --- a/src/cogs/Servermanament/welcome.py +++ b/src/bot/cogs/guild/welcome.py @@ -18,7 +18,6 @@ from datetime import datetime import ezcord from discord.ui import Container -from DevTools import emoji_yes, emoji_no, emoji_add # Logger Setup diff --git a/src/cogs/Servermanament/autodelete.py b/src/bot/cogs/management/autodelete.py similarity index 100% rename from src/cogs/Servermanament/autodelete.py rename to src/bot/cogs/management/autodelete.py diff --git a/src/cogs/Servermanament/autorole.py b/src/bot/cogs/management/autorole.py similarity index 99% rename from src/cogs/Servermanament/autorole.py rename to src/bot/cogs/management/autorole.py index 6d5944c..3e3613c 100644 --- a/src/cogs/Servermanament/autorole.py +++ b/src/bot/cogs/management/autorole.py @@ -2,7 +2,7 @@ from discord.ext import commands from discord import option from DevTools import AutoRoleDatabase -from handler import TranslationHandler as TH +from mx_handler import TranslationHandler as TH class AutoRole(commands.Cog): def __init__(self, bot): diff --git a/src/cogs/moderation/antispam.py b/src/bot/cogs/moderation/antispam.py similarity index 97% rename from src/cogs/moderation/antispam.py rename to src/bot/cogs/moderation/antispam.py index 955ab1a..5bdce7f 100644 --- a/src/cogs/moderation/antispam.py +++ b/src/bot/cogs/moderation/antispam.py @@ -8,29 +8,10 @@ from datetime import timedelta -from DevTools.ui import ( - emoji_yes, - emoji_no, - emoji_forbidden, - emoji_warn, - emoji_delete, - emoji_member, - emoji_channel, - emoji_moderator, - emoji_add, - emoji_statistics, - emoji_annoattention, - emoji_owner, -) - -from DevTools import SpamDB - +from DevTools import AntiSpamDatabase as SpamDB +antispam = SlashCommandGroup("antispam") class AntiSpam(ezcord.Cog): - antispam = SlashCommandGroup( - "antispam", - "Verwalte Anti-Spam-Einstellungen und Protokolle.", - ) def __init__(self, bot: ezcord.Bot): self.bot = bot diff --git a/src/cogs/moderation/moderation.py b/src/bot/cogs/moderation/moderation.py similarity index 99% rename from src/cogs/moderation/moderation.py rename to src/bot/cogs/moderation/moderation.py index 6730808..de7e7cc 100644 --- a/src/cogs/moderation/moderation.py +++ b/src/bot/cogs/moderation/moderation.py @@ -2,8 +2,6 @@ # ─────────────────────────────────────────────── # >> Imports # ─────────────────────────────────────────────── -from DevTools.ui import emoji_yes, emoji_no, emoji_member, emoji_warn, emoji_summary, emoji_staff, emoji_slowmode - import asyncio import re from datetime import datetime, timezone @@ -12,13 +10,14 @@ import discord import ezcord -from discord import slash_command, option, SlashCommandGroup +from discord import slash_command, option import timedelta from discord.ui import Container +from discord import SlashCommandGroup # ─────────────────────────────────────────────── # >> Cogs # ─────────────────────────────────────────────── -class moderation(ezcord.Cog): +class moderationCog(ezcord.Cog): """Erweiterte Moderations-Cog mit verbesserter Sicherheit und Fehlerbehandlung""" def __init__(self, bot): @@ -26,8 +25,8 @@ def __init__(self, bot): self.max_timeout_days = 28 self._active_votes: Dict[int, Dict] = {} self.logger = logging.getLogger(__name__) + moderation = SlashCommandGroup("mod") - moderation = SlashCommandGroup("mod", "Erweiterte Moderationsbefehle") def _has_permission(self, member: discord.Member, permission: str) -> bool: """ÜberprĂŒft ob ein Member eine bestimmte Berechtigung hat""" @@ -548,4 +547,4 @@ async def _handle_votekick(self, member_id: int, duration_seconds: int): def setup(bot): - bot.add_cog(moderation(bot)) \ No newline at end of file + bot.add_cog(moderationCog(bot)) \ No newline at end of file diff --git a/src/cogs/moderation/notes.py b/src/bot/cogs/moderation/notes.py similarity index 96% rename from src/cogs/moderation/notes.py rename to src/bot/cogs/moderation/notes.py index fcf7d6c..0aeae4b 100644 --- a/src/cogs/moderation/notes.py +++ b/src/bot/cogs/moderation/notes.py @@ -7,12 +7,12 @@ import datetime import ezcord from DevTools import NotesDatabase -from DevTools import emoji_no, emoji_yes + +notes = SlashCommandGroup("notes") # ─────────────────────────────────────────────── # >> Cog # ─────────────────────────────────────────────── class NotesCog(ezcord.Cog, group="moderation"): - notes = SlashCommandGroup("notes", "📝 Verwaltung von Notizen fĂŒr User") def __init__(self, bot): self.bot = bot diff --git a/src/cogs/moderation/warningsystem.py b/src/bot/cogs/moderation/warn.py similarity index 99% rename from src/cogs/moderation/warningsystem.py rename to src/bot/cogs/moderation/warn.py index 70235be..c56dd7f 100644 --- a/src/cogs/moderation/warningsystem.py +++ b/src/bot/cogs/moderation/warn.py @@ -2,9 +2,6 @@ # ─────────────────────────────────────────────── # >> Imports # ─────────────────────────────────────────────── -from DevTools import ( - emoji_no, emoji_yes, emoji_warn, emoji_member, emoji_staff, emoji_slowmode, emoji_summary -) from DevTools import WarnDatabase import discord from discord import slash_command, Option diff --git a/src/bot/cogs/user/settings.py b/src/bot/cogs/user/settings.py new file mode 100644 index 0000000..381ff7b --- /dev/null +++ b/src/bot/cogs/user/settings.py @@ -0,0 +1,108 @@ +import discord +from discord.ext import commands +from discord import SlashCommandGroup +import ezcord + +from mx_handler import TranslationHandler + + +class Settings(ezcord.Cog): + """Cog for setting user language preferences.""" + + user = SlashCommandGroup("user", "User settings commands") + + language = user.create_subgroup( + "language") + + AVAILABLE_LANGUAGES = { + "de": "Deutsch đŸ‡©đŸ‡Ș", + "en": "English 🇬🇧" + } + + @language.command( + name="set", + 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_settings.language.message.language_set", + default="Language has been set to {language}.", + language=lang_name + ) + + await ctx.respond(response_text, ephemeral=True) + + + @language.command() + async def get(self, ctx: discord.ApplicationContext): + """ + Get the user's current preferred language. + + Args: + ctx: Discord application context + """ + # Retrieve user's language preference + language = self.bot.settings_db.get_user_language(ctx.author.id) + + if not language: + response_text = await TranslationHandler.get_async( + "en", + "cog_settings.language.error_types.language_not_set", + default="You have not set a preferred language yet." + ) + else: + lang_name = self.AVAILABLE_LANGUAGES.get(language, language) + response_text = await TranslationHandler.get_async( + language, + "cog_settings.language.message.current_language", + default="Your current preferred language is {language}.", + language=lang_name + ) + + await ctx.respond(response_text, ephemeral=True) + + @language.command( + name="list", + description="List all available languages." + ) + + async def list_languages(self, ctx: discord.ApplicationContext): + """ + List all available languages. + + Args: + ctx: Discord application context + """ + languages_list = "\n".join( + f"{code}: {name}" for code, name in self.AVAILABLE_LANGUAGES.items() + ) + response_text = f"**Available Languages:**\n{languages_list}" + await ctx.respond(response_text, ephemeral=True) + +def setup(bot): + """Setup function to add the cog to the bot.""" + bot.add_cog(Settings(bot)) \ No newline at end of file diff --git a/src/cogs/Servermanament/stats.py b/src/bot/cogs/user/stats.py similarity index 100% rename from src/cogs/Servermanament/stats.py rename to src/bot/cogs/user/stats.py diff --git a/src/bot/core/__init__.py b/src/bot/core/__init__.py new file mode 100644 index 0000000..61d58e5 --- /dev/null +++ b/src/bot/core/__init__.py @@ -0,0 +1,25 @@ +""" +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 + +__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/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/setlang.py b/src/cogs/setlang.py deleted file mode 100644 index d838330..0000000 --- a/src/cogs/setlang.py +++ /dev/null @@ -1,46 +0,0 @@ -import discord -from discord.ext import commands -import ezcord - -from handler import TranslationHandler - -class SetLangCog(ezcord.Cog, group="informationen"): - - AVAILABLE_LANGUAGES = { - "de": "Deutsch đŸ‡©đŸ‡Ș", - "en": "English 🇬🇧" - } - - @commands.slash_command( - name="set-lang", - description="Stelle deine bevorzugte Sprache fĂŒr Bot-Nachrichten ein." - ) - @discord.option( - "language", - description="WĂ€hle eine Sprache", - 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): - # Sprache speichern - self.bot.settings_db.set_user_language(ctx.author.id, language) - - # Name fĂŒr Anzeige - lang_name = self.AVAILABLE_LANGUAGES.get(language, language) - - # Nachricht laden ĂŒber TranslationHandler - response_text = TranslationHandler.get( - 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): - bot.add_cog(SetLangCog(bot)) diff --git a/src/site/App.css b/src/site/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/src/site/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/src/site/App.tsx b/src/site/App.tsx new file mode 100644 index 0000000..f432120 --- /dev/null +++ b/src/site/App.tsx @@ -0,0 +1,33 @@ +import { Toaster } from "@/components/ui/toaster"; +import { Toaster as Sonner } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Index from "./pages/Index"; +import NotFound from "./pages/NotFound"; +import Impressum from "./pages/Impressum"; +import Datenschutz from "./pages/Datenschutz"; +import Nutzungsbedingungen from "./pages/Nutzungsbedingungen"; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + + + } /> + } /> + } /> + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + +); + +export default App; diff --git a/src/site/components/CTA.tsx b/src/site/components/CTA.tsx new file mode 100644 index 0000000..d5b5923 --- /dev/null +++ b/src/site/components/CTA.tsx @@ -0,0 +1,70 @@ +import { memo } from "react"; +import { motion } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { ArrowRight, Sparkles } from "lucide-react"; + +const stats = [ + { label: "Aktive Server", value: "10K+" }, + { label: "Befehle ausgefĂŒhrt", value: "1M+" }, + { label: "Zufriedene User", value: "50K+" }, +]; + +export const CTA = memo(function CTA() { + return ( +
+ {/* Simple Background */} +
+ +
+ +
+ + 100% Kostenlos +
+ +

+ Bereit fĂŒr das + nĂ€chste + Level + ? +

+ +

+ FĂŒge ManagerX jetzt zu deinem Server hinzu und erlebe die Zukunft + der Discord Server-Verwaltung. +

+ + + + {/* Bottom Stats */} +
+ {stats.map((stat) => ( +
+
{stat.value}
+
{stat.label}
+
+ ))} +
+
+
+
+ ); +}); diff --git a/src/site/components/FAQ.tsx b/src/site/components/FAQ.tsx new file mode 100644 index 0000000..780ff24 --- /dev/null +++ b/src/site/components/FAQ.tsx @@ -0,0 +1,104 @@ +import { memo } from "react"; +import { motion } from "framer-motion"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { HelpCircle, Sparkles } from "lucide-react"; + +const faqs = [ + { + question: "Wie fĂŒge ich ManagerX zu meinem Server hinzu?", + answer: "Klicke einfach auf den 'Zum Server hinzufĂŒgen' Button oben auf der Seite. Du wirst zu Discord weitergeleitet, wo du den Server auswĂ€hlen kannst, auf dem du ManagerX installieren möchtest. Stelle sicher, dass du Administrator-Rechte auf diesem Server hast." + }, + { + question: "Ist ManagerX kostenlos?", + answer: "Ja! ManagerX ist vollstĂ€ndig kostenlos nutzbar. Alle Kernfunktionen wie Moderation, Levelsystem, Globalchat und mehr stehen dir ohne EinschrĂ€nkungen zur VerfĂŒgung." + }, + { + question: "Wie funktioniert das Levelsystem?", + answer: "Das Levelsystem vergibt automatisch XP fĂŒr Nachrichten und Voice-Chat-AktivitĂ€t. Du kannst XP-Raten, Level-Rollen und Benachrichtigungen vollstĂ€ndig anpassen. Server-weite und globale Leaderboards zeigen die aktivsten Mitglieder." + }, + { + question: "Was ist der Globalchat?", + answer: "Der Globalchat verbindet deinen Server mit anderen ManagerX-Servern in Echtzeit. Nachrichten werden moderiert und gefiltert. Du hast volle Kontrolle ĂŒber Blacklists und kannst User blockieren oder reporten." + }, + { + question: "Wie kann ich Support erhalten?", + answer: "Tritt unserem Support-Server bei! Dort findest du eine aktive Community und unser Team, das dir bei allen Fragen hilft. Du kannst auch die Dokumentation und FAQ auf unserer Website nutzen." + }, + { + question: "Kann ich die Bot-Befehle anpassen?", + answer: "Absolut! Du kannst PrĂ€fixe Ă€ndern, Befehle aktivieren/deaktivieren, Berechtigungen fĂŒr bestimmte Rollen festlegen und vieles mehr. Die meisten Einstellungen sind ĂŒber das Dashboard oder Slash-Commands konfigurierbar." + }, +]; + +export const FAQ = memo(function FAQ() { + return ( +
+ {/* Simple Background */} +
+ +
+ {/* Section Header */} + +
+ + HĂ€ufige Fragen +
+ +

+ Hast du + + Fragen + + + ? +

+

+ Hier findest du Antworten auf die hÀufigsten Fragen zu ManagerX. +

+
+ + {/* FAQ Accordion */} + + + {faqs.map((faq, index) => ( + + + + + {index + 1} + + {faq.question} + + + + {faq.answer} + + + ))} + + +
+
+ ); +}); diff --git a/src/site/components/FeatureCard.tsx b/src/site/components/FeatureCard.tsx new file mode 100644 index 0000000..88e71ef --- /dev/null +++ b/src/site/components/FeatureCard.tsx @@ -0,0 +1,75 @@ +import { memo } from "react"; +import { motion } from "framer-motion"; +import { LucideIcon, Check } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface FeatureCardProps { + icon: LucideIcon; + title: string; + features: string[]; + category: "moderation" | "community" | "social" | "interactive"; + delay?: number; +} + +const categoryColors = { + moderation: "text-red-400", + community: "text-purple-400", + social: "text-blue-400", + interactive: "text-yellow-400", +}; + +const categoryBgColors = { + moderation: "bg-red-500/10", + community: "bg-purple-500/10", + social: "bg-blue-500/10", + interactive: "bg-yellow-500/10", +}; + +export const FeatureCard = memo(function FeatureCard({ + icon: Icon, + title, + features, + category, + delay = 0 +}: FeatureCardProps) { + return ( + + {/* Header */} +
+
+ +
+
+ +

+ {title} +

+ +
    + {features.map((feature, index) => ( +
  • +
    + +
    + {feature} +
  • + ))} +
+
+ ); +}); diff --git a/src/site/components/Features.tsx b/src/site/components/Features.tsx new file mode 100644 index 0000000..da08024 --- /dev/null +++ b/src/site/components/Features.tsx @@ -0,0 +1,112 @@ +import { memo } from "react"; +import { motion } from "framer-motion"; +import { FeatureCard } from "./FeatureCard"; +import { + Shield, + Award, + Globe, + Gamepad2, + Sparkles +} from "lucide-react"; + +const featureCategories = [ + { + icon: Shield, + title: "Moderation & Sicherheit", + category: "moderation" as const, + features: [ + "Ban, Kick, Mute, Warn Befehle", + "Intelligentes Anti-Spam System", + "Automatisches Warning-Management", + "Detaillierte Moderation-Logs", + "TemporĂ€re Strafen (Timeout)", + "Reason-Tracking fĂŒr alle Actions", + ], + }, + { + icon: Award, + title: "Community Engagement", + category: "community" as const, + features: [ + "VollstĂ€ndig anpassbares XP-System", + "Rollenbelohnungen fĂŒr Level-Ups", + "Server & Global Leaderboards", + "XP-Multiplikatoren & Boosts", + "Voice-Channel XP-Tracking", + "Automatische BegrĂŒĂŸungsnachrichten", + ], + }, + { + icon: Globe, + title: "Social & Information", + category: "social" as const, + features: [ + "Echtzeit-Chat mit anderen Servern", + "Wikipedia Integration", + "Live-Wetterinformationen", + "Server-ĂŒbergreifende Reputation", + "Mehrsprachige UnterstĂŒtzung", + "Report & Block Funktionen", + ], + }, + { + icon: Gamepad2, + title: "Interaktive Features", + category: "interactive" as const, + features: [ + "Temporary Voice Channels", + "Individuelle Kanalverwaltung", + "Server-Statistiken in Echtzeit", + "User-Activity Tracking", + "Command-Usage Analytics", + "Auto-Delete bei InaktivitĂ€t", + ], + }, +]; + +export const Features = memo(function Features() { + return ( +
+ {/* Simple Background */} +
+ +
+ {/* Section Header */} + +
+ + Über 90 Befehle +
+ +

+ Alles was du + brauchst +

+

+ Ein Bot fĂŒr alle deine Server-BedĂŒrfnisse. Moderation, Engagement, Social Features und mehr. +

+
+ + {/* Feature Cards Grid */} +
+ {featureCategories.map((category, index) => ( + + ))} +
+
+
+ ); +}); diff --git a/src/site/components/Footer.tsx b/src/site/components/Footer.tsx new file mode 100644 index 0000000..72e435d --- /dev/null +++ b/src/site/components/Footer.tsx @@ -0,0 +1,140 @@ +import { memo } from "react"; +import { Link } from "react-router-dom"; +import { motion } from "framer-motion"; +import { Shield, Heart, Github, MessageCircle, ExternalLink } from "lucide-react"; + +const socialLinks = [ + { icon: Github, href: "https://github.com", label: "GitHub" }, + { icon: MessageCircle, href: "https://discord.gg", label: "Discord" }, +]; + +const footerLinks = [ + { label: "Features", href: "#features" }, + { label: "Commands", href: "#commands" }, + { label: "Dokumentation", href: "https://docs.oppro-network.de", external: true }, + { label: "Support", href: "#support" }, +]; + +const legalLinks = [ + { label: "Datenschutz", href: "/datenschutz" }, + { label: "Impressum", href: "/impressum" }, + { label: "Nutzungsbedingungen", href: "/nutzungsbedingungen" }, +]; + +export const Footer = memo(function Footer() { + return ( +
+ {/* Background Gradient */} +
+ +
+
+ {/* Brand */} + +
+
+ +
+ + ManagerX + +
+

+ Der ultimative Discord Bot fĂŒr Moderation, Community-Engagement und vieles mehr. +

+
+ {socialLinks.map((link) => ( + + + + ))} +
+
+ + {/* Quick Links */} + +

Navigation

+ +
+ + {/* Legal */} + +

Rechtliches

+
    + {legalLinks.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+
+ + {/* Bottom Bar */} + +
+ Made with + + + + in Germany +
+ +
+ © {new Date().getFullYear()} ManagerX. Alle Rechte vorbehalten. +
+
+
+
+ ); +}); \ No newline at end of file diff --git a/src/site/components/Hero.tsx b/src/site/components/Hero.tsx new file mode 100644 index 0000000..37cf158 --- /dev/null +++ b/src/site/components/Hero.tsx @@ -0,0 +1,135 @@ +import { memo } from "react"; +import { motion } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { Shield, Users, MessageCircle, Sparkles } from "lucide-react"; + +const stats = [ + { label: "Server", value: "10,000+", icon: Users }, + { label: "Befehle", value: "90+", icon: MessageCircle }, + { label: "Uptime", value: "99.9%", icon: Sparkles }, +]; + +const StatCard = memo(({ stat, index }: { stat: typeof stats[0]; index: number }) => ( + +
+ + {stat.value} +
+ {stat.label} +
+)); + +StatCard.displayName = "StatCard"; + +export const Hero = memo(function Hero() { + return ( +
+ {/* Static Background - no blur for performance */} +
+ + {/* Simple gradient orbs without blur */} +
+
+ + {/* Grid Pattern - static */} +
+ +
+
+ {/* Badge */} + + + Version 2.0 jetzt verfĂŒgbar + + + + {/* Logo */} + +
+
+ +
+
+ +
+
+
+ + {/* Title */} + + Manager + X + + + {/* Description */} + + Der ultimative Discord Bot fĂŒr{" "} + Moderation,{" "} + Levelsystem,{" "} + Globalchat und mehr. + + + {/* CTA Buttons */} + + + + + + {/* Stats */} +
+ {stats.map((stat, index) => ( + + ))} +
+
+
+ + {/* Bottom Gradient */} +
+
+ ); +}); diff --git a/src/site/components/NavLink.tsx b/src/site/components/NavLink.tsx new file mode 100644 index 0000000..a561a95 --- /dev/null +++ b/src/site/components/NavLink.tsx @@ -0,0 +1,28 @@ +import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom"; +import { forwardRef } from "react"; +import { cn } from "@/lib/utils"; + +interface NavLinkCompatProps extends Omit { + className?: string; + activeClassName?: string; + pendingClassName?: string; +} + +const NavLink = forwardRef( + ({ className, activeClassName, pendingClassName, to, ...props }, ref) => { + return ( + + cn(className, isActive && activeClassName, isPending && pendingClassName) + } + {...props} + /> + ); + }, +); + +NavLink.displayName = "NavLink"; + +export { NavLink }; diff --git a/src/site/components/Navbar.tsx b/src/site/components/Navbar.tsx new file mode 100644 index 0000000..284f147 --- /dev/null +++ b/src/site/components/Navbar.tsx @@ -0,0 +1,149 @@ +import { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { Shield, Menu, X, Sparkles } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const navLinks = [ + { label: "Features", href: "#features" }, + { label: "Commands", href: "#commands" }, + { label: "Support", href: "#support" }, +]; + +export function Navbar() { + const [isScrolled, setIsScrolled] = useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 20); + }; + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + return ( + +
+ + + {/* Mobile Menu */} + + {isMobileMenuOpen && ( + +
+
+ {navLinks.map((link, index) => ( + setIsMobileMenuOpen(false)} + initial={{ opacity: 0, x: -20 }} + animate={{ opacity: 1, x: 0 }} + transition={{ delay: index * 0.1 }} + className="text-muted-foreground hover:text-foreground transition-colors py-3 px-4 rounded-xl hover:bg-card/50 font-medium" + > + {link.label} + + ))} + + + +
+
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/site/components/Testimonials.tsx b/src/site/components/Testimonials.tsx new file mode 100644 index 0000000..2dca055 --- /dev/null +++ b/src/site/components/Testimonials.tsx @@ -0,0 +1,168 @@ +import { memo } from "react"; +import { motion } from "framer-motion"; +import { Star, MessageSquare, Users } from "lucide-react"; + +const testimonials = [ + { + name: "Max Mustermann", + role: "Server Admin", + server: "Gaming Community DE", + members: "15.000+", + avatar: "M", + rating: 5, + text: "ManagerX hat unseren Server komplett transformiert. Das Levelsystem motiviert unsere Mitglieder unglaublich und die Moderation ist ein Traum!", + }, + { + name: "Sarah Schmidt", + role: "Community Manager", + server: "Creative Hub", + members: "8.000+", + avatar: "S", + rating: 5, + text: "Der Globalchat verbindet uns mit anderen Communities - das ist einzigartig! Und der Support ist super schnell und hilfsbereit.", + }, + { + name: "Tom Weber", + role: "GrĂŒnder", + server: "Tech Talk Germany", + members: "25.000+", + avatar: "T", + rating: 5, + text: "Wir haben viele Bots getestet, aber ManagerX ist der beste. Alle Features funktionieren perfekt zusammen und die Konfiguration ist super einfach.", + }, + { + name: "Lisa MĂŒller", + role: "Moderatorin", + server: "Anime World", + members: "12.000+", + avatar: "L", + rating: 5, + text: "Das Anti-Spam System und die Moderations-Tools machen meinen Job so viel einfacher. Endlich ein Bot, der wirklich durchdacht ist!", + }, + { + name: "Jan Hoffmann", + role: "Server Owner", + server: "Music Lounge", + members: "5.000+", + avatar: "J", + rating: 5, + text: "Die Temporary Voice Channels sind genial! Unsere Mitglieder lieben es, eigene RĂ€ume erstellen zu können. Absolute Empfehlung!", + }, + { + name: "Emma Fischer", + role: "Admin Team Lead", + server: "Study Together", + members: "20.000+", + avatar: "E", + rating: 5, + text: "ManagerX ist stabil, schnell und hat alles was wir brauchen. Das Dashboard ist ĂŒbersichtlich und die Docs sind hervorragend.", + }, +]; + +const TestimonialCard = memo(({ testimonial, index }: { testimonial: typeof testimonials[0]; index: number }) => ( + + {/* Stars */} +
+ {[...Array(testimonial.rating)].map((_, i) => ( + + ))} +
+ + {/* Text */} +

+ "{testimonial.text}" +

+ + {/* Author */} +
+
+ {testimonial.avatar} +
+
+
{testimonial.name}
+
{testimonial.role}
+
+
+ + {/* Server Info */} +
+ {testimonial.server} + + + {testimonial.members} + +
+
+)); + +TestimonialCard.displayName = "TestimonialCard"; + +export const Testimonials = memo(function Testimonials() { + return ( +
+ {/* Simple Background */} +
+ +
+ {/* Section Header */} + +
+ + Testimonials +
+ +

+ Was unsere + Community + sagt +

+

+ Tausende Server-Admins vertrauen ManagerX. Hier sind ihre Erfahrungen. +

+
+ + {/* Testimonials Grid */} +
+ {testimonials.map((testimonial, index) => ( + + ))} +
+ + {/* Stats Bar */} + + {[ + { value: "4.9/5", label: "Durchschnittliche Bewertung", icon: Star }, + { value: "10.000+", label: "Zufriedene Server", icon: Users }, + { value: "99%", label: "WĂŒrden uns empfehlen", icon: MessageSquare }, + ].map((stat, index) => ( +
+
+ + {stat.value} +
+ {stat.label} +
+ ))} +
+
+
+ ); +}); diff --git a/src/site/components/ui/accordion.tsx b/src/site/components/ui/accordion.tsx new file mode 100644 index 0000000..1e7878c --- /dev/null +++ b/src/site/components/ui/accordion.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/src/site/components/ui/alert-dialog.tsx b/src/site/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..6dfbfb4 --- /dev/null +++ b/src/site/components/ui/alert-dialog.tsx @@ -0,0 +1,104 @@ +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +AlertDialogHeader.displayName = "AlertDialogHeader"; + +const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/src/site/components/ui/alert.tsx b/src/site/components/ui/alert.tsx new file mode 100644 index 0000000..2efc3c8 --- /dev/null +++ b/src/site/components/ui/alert.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/site/components/ui/aspect-ratio.tsx b/src/site/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c9e6f4b --- /dev/null +++ b/src/site/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; + +const AspectRatio = AspectRatioPrimitive.Root; + +export { AspectRatio }; diff --git a/src/site/components/ui/avatar.tsx b/src/site/components/ui/avatar.tsx new file mode 100644 index 0000000..68d21bb --- /dev/null +++ b/src/site/components/ui/avatar.tsx @@ -0,0 +1,38 @@ +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "@/lib/utils"; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/site/components/ui/badge.tsx b/src/site/components/ui/badge.tsx new file mode 100644 index 0000000..0853c44 --- /dev/null +++ b/src/site/components/ui/badge.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps extends React.HTMLAttributes, VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
; +} + +export { Badge, badgeVariants }; diff --git a/src/site/components/ui/breadcrumb.tsx b/src/site/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..ca91ff5 --- /dev/null +++ b/src/site/components/ui/breadcrumb.tsx @@ -0,0 +1,90 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode; + } +>(({ ...props }, ref) =>