diff --git a/config/config.yaml b/config/config.yaml index ac07686..392a288 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -93,6 +93,13 @@ links: github: "https://github.com/ManagerX-Development/ManagerX" topgg: "https://top.gg/bot/1368201272624287754" +# Module: Uptime Kuma Discord Status +uptime_kuma: + enabled: true + base_url: "https://status.oppro-network.de" + slug: "default" + update_interval: 60 # in seconds + # Performance & Cache performance: cache_timeout: 300 @@ -150,3 +157,4 @@ features: welcome: true other: setlang: true + setlang: true diff --git a/docs/source/conf.py b/docs/source/conf.py index 17654c1..0f231d8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,7 +15,7 @@ author = 'ManagerX Development' release = '2.0.0' version = '2.0' # Kurzversion -language = 'en' +language = 'de' # -- General configuration --------------------------------------------------- extensions = [ diff --git a/docs/source/index.rst b/docs/source/index.rst index 28e92c7..beced82 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,202 +1,91 @@ -======================================== -๐Ÿค– ManagerX Documentation -======================================== - -**The Ultimate Discord Management Solution** - -Powerful. Modular. Open Source. - -.. image:: https://img.shields.io/badge/Version-2.0.0-e11d48?style=for-the-badge - :alt: Version 2.0.0 - :target: https://github.com/ManagerX-Development/ManagerX/releases - -.. image:: https://img.shields.io/badge/Python-3.11+-green.svg?style=for-the-badge - :alt: Python 3.11+ - -.. image:: https://img.shields.io/badge/License-GPL--3.0-yellow.svg?style=for-the-badge - :alt: GPL-3.0 License - :target: https://github.com/ManagerX-Development/ManagerX/blob/main/LICENSE - -.. image:: https://img.shields.io/github/stars/ManagerX-Development/ManagerX?style=for-the-badge - :alt: GitHub Stars - :target: https://github.com/ManagerX-Development/ManagerX - -ManagerX is a comprehensive Discord bot designed for modern server management. With advanced moderation, engaging entertainment features, detailed statistics, and a beautiful web dashboard, ManagerX brings your Discord server to the next level. - -โœจ **What Makes ManagerX Special:** - -- ๐ŸŽฏ **Intuitive Commands** โ€” Easy-to-use slash commands for all features -- ๐Ÿ›ก๏ธ **Powerful Moderation** โ€” Anti-spam, warnings, timeouts, and more -- ๐Ÿ“Š **Live Dashboard** โ€” Real-time server statistics and status -- ๐ŸŽฎ **Fun & Games** โ€” Entertainment commands and interactive games -- โš™๏ธ **Fully Configurable** โ€” Customize every aspect of the bot -- ๐Ÿ”ง **Developer-Friendly** โ€” Well-documented API and extensible architecture -- ๐ŸŒ **Global Ready** โ€” Multi-language support -- ๐Ÿ“œ **Active Development** โ€” Regularly updated with new features - -๐Ÿš€ **Quick Start** - -1. **New User?** Start with :doc:`user_guide/quick_start/index` -2. **Want to Contribute?** Check out :doc:`dev_guide/getting_started/index` -3. **Need Help?** Visit :doc:`user_guide/faq/index` - ---- - -๐Ÿ“š Documentation Sections -========================= - -**๐Ÿ‘ฅ User Guide** - -Everything you need to know to use ManagerX on your server: - -- Getting started and setup -- Command reference -- Feature overview -- Configuration options -- Troubleshooting - -`โ†’ Open User Guide `_ - -**๐Ÿ‘จโ€๐Ÿ’ป Developer Guide** - -For developers who want to extend or self-host ManagerX: - -- Architecture overview -- Installation & setup -- Bot development -- API development -- Testing & deployment -- Contributing guidelines - -`โ†’ Open Developer Guide `_ - ---- - -๐ŸŒŸ Core Features -================= - -.. list-table:: - :class: feature-table - :widths: 20 80 - - * - **๐Ÿ›ก๏ธ Moderation** - - Anti-spam detection, warning system, user timeouts, kicks, bans, slowmode, votekick system, comprehensive logging - * - **๐ŸŽฎ Entertainment** - - Tic Tac Toe, Connect 4, Wikipedia search, jokes, weather information, interactive games - * - **๐Ÿ“Š Statistics** - - User profiles with XP tracking, server statistics, leaderboards, achievement system, user activity monitoring - * - **โš™๏ธ Management** - - Auto-roles on join, welcome/goodbye messages, channel management, global chat networks, configuration system - * - **๐Ÿ“ˆ Insights** - - Server analytics, member insights, bot performance metrics, activity tracking - * - **๐Ÿ”Œ Integration** - - REST API, web dashboard, real-time status updates, data export capabilities - ---- - -๐Ÿ’ก How to Use This Documentation -================================= - -**For Server Owners & Moderators:** - -Start with :doc:`user_guide/quick_start/index` guide. You'll learn how to add ManagerX to your server, configure it, and use all available commands. - -**For Developers:** - -Head over to :doc:`dev_guide/getting_started/index` to set up a development environment. The :doc:`dev_guide/architecture/index` will help you understand how ManagerX is built. - -**For Contributors:** - -Read :doc:`dev_guide/contributing/index` for contribution guidelines, code style, and the pull request process. - -**For Self-Hosting:** - -Check out :doc:`dev_guide/deployment/index` for production deployment instructions. - ---- - -๐Ÿ“– Feature Categories -====================== - - - ---- - -๐Ÿ†˜ Getting Help -================ - -**Documentation Issues?** - -Found a typo or unclear section? `Report an issue `_ - -**Bug Report?** - -Create a detailed issue on `GitHub Issues `_ - -**Feature Request?** - -Suggest new features on GitHub or join our Discord community - -**Community Support?** - -Join our Discord server for real-time help and community discussion - ---- - -๐Ÿ“‹ Version Information -======================= - -- **Current Version:** 2.0.0 -- **Python Required:** 3.11 or higher -- **License:** GPL-3.0 -- **Last Updated:** January 2026 -- **Stability:** Production Ready - ---- - -Frequently Asked Questions -=========================== - -**Q: How do I invite ManagerX to my server?** - -Check :doc:`user_guide/quick_start/index` for a complete invitation guide with screenshots. - -**Q: How do I report bugs or request features?** - -Please open an issue on the `GitHub Issues page `_. Provide as much detail as possible. - -**Q: Can I self-host ManagerX?** - -Yes! See :doc:`dev_guide/deployment/index` for complete self-hosting instructions with multiple hosting options. - -**Q: Which Python version is required?** - ManagerX requires **Python 3.8** or higher to function correctly. - -Contributing ------------- - -We welcome contributions from the community! Whether it's bug reports, feature requests, or code improvements, feel free to get involved via our `GitHub Repository `_. For more details, check the **Contributing** section in the Developer Guide. - ---- - -**ยฉ 2026 ManagerX Development** -*Version 2.0.0-dev | Last Updated: December 7, 2025* - -.. toctree:: - :maxdepth: 2 - :caption: ๐Ÿ‘ฅ User Guide: - - user_guide/index - user_guide/getting_started - user_guide/moderation - user_guide/levels - user_guide/management - user_guide/dashboard - -.. toctree:: - :maxdepth: 2 - :caption: ๐Ÿ‘จโ€๐Ÿ’ป Developer Guide: - - dev_guide/index - dev_guide/architecture - dev_guide/installation \ No newline at end of file +======================================== +๐Ÿค– ManagerX Dokumentation +======================================== + +**Die ultimative Management-Lรถsung fรผr deinen Discord-Server** + +Leistungsstark. Modular. Open Source. + +.. image:: https://img.shields.io/badge/Version-2.0.0-e11d48?style=for-the-badge + :alt: Version 2.0.0 + :target: https://github.com/ManagerX-Development/ManagerX/releases + +.. image:: https://img.shields.io/badge/Python-3.11+-green.svg?style=for-the-badge + :alt: Python 3.11+ + +.. image:: https://img.shields.io/badge/Lizenz-GPL--3.0-yellow.svg?style=for-the-badge + :alt: GPL-3.0 Lizenz + :target: https://github.com/ManagerX-Development/ManagerX/blob/main/LICENSE + +ManagerX ist ein umfassender Discord-Bot, der fรผr modernes Server-Management entwickelt wurde. Mit fortschrittlicher Moderation, Entertainment-Funktionen, detaillierten Statistiken und einem modernen Web-Dashboard bringt ManagerX deinen Server auf das nรคchste Level. + +--- + +๐Ÿ“– Schnellzugriff +================ + +ManagerX ist in zwei Hauptbereiche unterteilt: + +**๐Ÿ‘ฅ Benutzer-Handbuch** +------------------------ + +Alles, was du wissen musst, um ManagerX auf deinem Server zu nutzen: + +- Erste Schritte & Installation +- Befehlsรผbersicht +- Feature-Erklรคrungen +- Dashboard-Konfiguration + +`โ†’ Zum Benutzer-Handbuch `_ + +**๐Ÿ‘จโ€๐Ÿ’ป Entwickler-Dokumentation** +---------------------------------- + +Fรผr Entwickler, die ManagerX erweitern oder selbst hosten mรถchten: + +- Architektur-รœbersicht +- Installation & Setup +- API-Referenz +- Mitwirken am Projekt + +`โ†’ Zur Entwickler-Dokumentation `_ + +--- + +๐ŸŽฎ Kern-Features +================ + +- ๐Ÿ›ก๏ธ **Moderation** โ€” Anti-Spam, Warnungen und automatische Bestrafungen. +- ๐Ÿ“Š **Detaillierte Statistiken** โ€” Level-System, Aktivitรคten und Leaderboards. +- โš™๏ธ **Einfache Verwaltung** โ€” Ein modernes Web-Dashboard fรผr alle Einstellungen. +- ๐ŸŒ **Global Chat** โ€” Vernetze deinen Server mit anderen Communities. + +--- + +๐Ÿ†˜ Hilfe & Support +================= + +Solltest du Fragen haben oder auf Probleme stoรŸen: + +* **Support-Server:** Tritt unserem `Discord-Server `_ bei. +* **Bug melden:** Erstelle ein Issue auf `GitHub `_. +* **FAQ:** Schau in unsere :doc:`user_guide/index` Sektion. + +--- + +**ยฉ 2026 ManagerX Development** +*Version 2.0.0 | Letztes Update: April 2026* + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: ๐Ÿ‘ฅ Benutzer-Handbuch: + + user_guide/index + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: ๐Ÿ‘จโ€๐Ÿ’ป Entwickler-Dokumentation: + + dev_guide/index \ No newline at end of file diff --git a/mxmariadb/connector.py b/mxmariadb/connector.py index dbd9ab6..06434d4 100644 --- a/mxmariadb/connector.py +++ b/mxmariadb/connector.py @@ -29,31 +29,30 @@ def pool(self): return MariaConnector._pool async def connect(self): - async with MariaConnector._lock: - if MariaConnector._pool is None: - if not self.user or not self.database: - logger.error(f"DB-Credentials fehlen in {env_path}") - raise RuntimeError("Datenbankzugangsdaten fehlen.") + if MariaConnector._pool is None: + if not self.user or not self.database: + logger.error(f"DB-Credentials fehlen in {env_path}") + raise RuntimeError("Datenbankzugangsdaten fehlen.") - try: - logger.info(f"[DB] Verbinde zu {self.host}:{self.port} DB='{self.database}' als '{self.user}'...") - MariaConnector._pool = await aiomysql.create_pool( - host=self.host, - user=self.user, - password=self.password, - db=self.database, - port=self.port, - autocommit=False, - minsize=2, - maxsize=15, - echo=False, - pool_recycle=1800, # Connections nach 30 Min recyclen - connect_timeout=10, # Verbindungs-Timeout - ) - logger.info("โœ… MariaDB Pool erstellt.") - except Exception as e: - logger.critical(f"โŒ Pool-Erstellung fehlgeschlagen: {e}") - raise + try: + logger.info(f"[DB] Verbinde zu {self.host}:{self.port} DB='{self.database}' als '{self.user}'...") + MariaConnector._pool = await aiomysql.create_pool( + host=self.host, + user=self.user, + password=self.password, + db=self.database, + port=self.port, + autocommit=False, + minsize=2, + maxsize=15, + echo=False, + pool_recycle=1800, # Connections nach 30 Min recyclen + connect_timeout=10, # Verbindungs-Timeout + ) + logger.info("โœ… MariaDB Pool erstellt.") + except Exception as e: + logger.critical(f"โŒ Pool-Erstellung fehlgeschlagen: {e}") + raise cls_name = type(self).__name__ if cls_name not in MariaConnector._initialized: diff --git a/mxmariadb/management_db.py b/mxmariadb/management_db.py new file mode 100644 index 0000000..8ebaf21 --- /dev/null +++ b/mxmariadb/management_db.py @@ -0,0 +1,135 @@ +# Copyright (c) 2025 OPPRO.NET Network +import aiomysql +import logging +from mxmariadb.connector import MariaConnector + +logger = logging.getLogger(__name__) + +class ManagementDatabase(MariaConnector): + """MariaDB class for Auto-Responder, News-Sync, and Applications.""" + + def __init__(self): + super().__init__() + + async def init_db(self): + """Initialize tables using the connector's lifecycle.""" + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + # Auto-Responder Table + await cur.execute(""" + CREATE TABLE IF NOT EXISTS auto_responder ( + id INT AUTO_INCREMENT PRIMARY KEY, + guild_id BIGINT NOT NULL, + keyword VARCHAR(255) NOT NULL, + response TEXT NOT NULL, + match_type VARCHAR(50) DEFAULT 'partial', + INDEX(guild_id, keyword) + ) + """) + # News-Sync Table + await cur.execute(""" + CREATE TABLE IF NOT EXISTS news_sync ( + id INT AUTO_INCREMENT PRIMARY KEY, + guild_id BIGINT NOT NULL, + channel_id BIGINT NOT NULL, + is_master BOOLEAN DEFAULT FALSE, + sync_group VARCHAR(100) DEFAULT 'default', + INDEX(guild_id, channel_id, sync_group) + ) + """) + # Application Questions Table + await cur.execute(""" + CREATE TABLE IF NOT EXISTS app_questions ( + id INT AUTO_INCREMENT PRIMARY KEY, + guild_id BIGINT NOT NULL, + question_text TEXT NOT NULL, + order_idx INT DEFAULT 0, + INDEX(guild_id) + ) + """) + # Application Submissions + await cur.execute(""" + CREATE TABLE IF NOT EXISTS app_submissions ( + id INT AUTO_INCREMENT PRIMARY KEY, + guild_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + content TEXT NOT NULL, + status VARCHAR(50) DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX(guild_id, user_id) + ) + """) + await conn.commit() + logger.info("Management tables initialized in MariaDB") + + # --- Auto-Responder Methods --- + async def add_auto_response(self, guild_id: int, keyword: str, response: str, match_type: str = 'partial'): + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO auto_responder (guild_id, keyword, response, match_type) VALUES (%s, %s, %s, %s)", + (guild_id, keyword.lower(), response, match_type) + ) + await conn.commit() + + async def remove_auto_response(self, guild_id: int, responder_id: int): + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute( + "DELETE FROM auto_responder WHERE id = %s AND guild_id = %s", + (responder_id, guild_id) + ) + await conn.commit() + + async def get_auto_responses(self, guild_id: int): + async with self.pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cur: + await cur.execute( + "SELECT id, keyword, response, match_type FROM auto_responder WHERE guild_id = %s", + (guild_id,) + ) + return await cur.fetchall() + + # --- News-Sync Methods --- + async def add_sync_channel(self, guild_id: int, channel_id: int, is_master: bool = False, sync_group: str = 'default'): + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO news_sync (guild_id, channel_id, is_master, sync_group) VALUES (%s, %s, %s, %s)", + (guild_id, channel_id, is_master, sync_group) + ) + await conn.commit() + + async def get_sync_channels(self, sync_group: str = None): + async with self.pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cur: + if sync_group: + await cur.execute("SELECT guild_id, channel_id, is_master, sync_group FROM news_sync WHERE sync_group = %s", (sync_group,)) + else: + await cur.execute("SELECT guild_id, channel_id, is_master, sync_group FROM news_sync") + return await cur.fetchall() + + # --- Application Methods --- + async def add_question(self, guild_id: int, text: str, order: int): + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO app_questions (guild_id, question_text, order_idx) VALUES (%s, %s, %s)", + (guild_id, text, order) + ) + await conn.commit() + + async def get_questions(self, guild_id: int): + async with self.pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cur: + await cur.execute( + "SELECT id, question_text FROM app_questions WHERE guild_id = %s ORDER BY order_idx ASC", + (guild_id,) + ) + return await cur.fetchall() + + async def clear_questions(self, guild_id: int): + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute("DELETE FROM app_questions WHERE guild_id = %s", (guild_id,)) + await conn.commit() diff --git a/mxmariadb/stats_db.py b/mxmariadb/stats_db.py index 6b6f0d1..56cc36d 100644 --- a/mxmariadb/stats_db.py +++ b/mxmariadb/stats_db.py @@ -105,6 +105,12 @@ async def init_db(self): start_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') + + # Migration: Sicherstellen, dass guild_id in messages existiert + try: + await cur.execute("ALTER TABLE messages ADD COLUMN IF NOT EXISTS guild_id BIGINT NOT NULL AFTER user_id") + await cur.execute("CREATE INDEX IF NOT EXISTS idx_guild_ts ON messages (guild_id, timestamp)") + except: pass await conn.commit() logger.info("StatsDB: Tabellen initialisiert.") except Exception as e: diff --git a/src/api/dashboard/management_routes.py b/src/api/dashboard/management_routes.py new file mode 100644 index 0000000..d6212eb --- /dev/null +++ b/src/api/dashboard/management_routes.py @@ -0,0 +1,109 @@ +from fastapi import APIRouter, Request, HTTPException, Depends +from src.api.dashboard.auth_routes import get_current_user +from mxmariadb import ManagementDatabase +import discord +from datetime import datetime + +router = APIRouter( + prefix="/management", + tags=["management"], + dependencies=[Depends(get_current_user)] +) + +async def send_management_notification(guild_id: int, module_name: str, user_name: str): + """Helper for dashboard notifications.""" + from src.api.dashboard.routes import bot_instance + if not bot_instance: return + guild = bot_instance.get_guild(guild_id) + if not guild: return + + target_channel = guild.system_channel or guild.text_channels[0] + if not target_channel: return + + embed = discord.Embed( + title="๐Ÿ› ๏ธ Management-Settings aktualisiert", + description=f"Das Modul **{module_name}** wurde รผber das Dashboard angepasst.", + color=discord.Color.gold(), + timestamp=datetime.now() + ) + embed.add_field(name="Administrator", value=user_name, inline=True) + embed.set_footer(text="ManagerX Dashboard System") + try: await target_channel.send(embed=embed) + except: pass + +# --- Auto-Responder --- +@router.get("/{guild_id}/autoresponder") +async def get_autoresponder(guild_id: int): + db = ManagementDatabase() + try: + await db.ensure_connection() + data = await db.get_auto_responses(guild_id) + return {"success": True, "data": data} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/{guild_id}/autoresponder") +async def add_autoresponder(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + data = await request.json() + db = ManagementDatabase() + try: + await db.ensure_connection() + await db.add_auto_response( + guild_id, + data.get("keyword"), + data.get("response"), + data.get("match_type", "partial") + ) + await send_management_notification(guild_id, "Auto-Responder", user.get("username")) + return {"success": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.delete("/{guild_id}/autoresponder/{responder_id}") +async def delete_autoresponder(guild_id: int, responder_id: int): + db = ManagementDatabase() + try: + await db.ensure_connection() + await db.remove_auto_response(guild_id, responder_id) + return {"success": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# --- News-Sync --- +@router.get("/{guild_id}/newssync") +async def get_newssync(guild_id: int): + db = ManagementDatabase() + try: + await db.ensure_connection() + all_channels = await db.get_sync_channels() + # Filter for this guild + guild_syncs = [c for c in all_channels if c['guild_id'] == guild_id] + return {"success": True, "data": guild_syncs} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# --- Applications --- +@router.get("/{guild_id}/applications") +async def get_app_questions(guild_id: int): + db = ManagementDatabase() + try: + await db.ensure_connection() + questions = await db.get_questions(guild_id) + return {"success": True, "data": questions} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/{guild_id}/applications") +async def set_app_questions(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + data = await request.json() + questions = data.get("questions", []) # List of strings + db = ManagementDatabase() + try: + await db.ensure_connection() + await db.clear_questions(guild_id) + for i, q_text in enumerate(questions): + await db.add_question(guild_id, q_text, i) + await send_management_notification(guild_id, "Bewerbungssystem", user.get("username")) + return {"success": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/src/api/dashboard/routes.py b/src/api/dashboard/routes.py index 2742885..c7c3aaa 100644 --- a/src/api/dashboard/routes.py +++ b/src/api/dashboard/routes.py @@ -11,6 +11,7 @@ from .auth_routes import router as auth_router from .settings_routes import router as settings_router from .user_routes import router as user_router +from .management_routes import router as management_router # Wir erstellen einen Router, den wir spรคter in die Haupt-App einbinden router_public = APIRouter( @@ -457,6 +458,29 @@ async def get_mega_data(guild_id: int, user: dict = Depends(get_current_user)): except: logging_active = False + # Check Management Modules + from mxmariadb import ManagementDatabase + db_m = ManagementDatabase() + await db_m.ensure_connection() + + # Check Auto-Responder + try: + ar_data = await db_m.get_auto_responses(guild_id) + autoresponder_active = len(ar_data) > 0 + except: autoresponder_active = False + + # Check Applications + try: + app_data = await db_m.get_questions(guild_id) + applications_active = len(app_data) > 0 + except: applications_active = False + + # Check NewsSync + try: + sync_data = await db_m.get_sync_channels() + newssync_active = any(c['guild_id'] == guild_id for c in sync_data) + except: newssync_active = False + guild_lang = "de" # 3. Fetch Metadata @@ -482,6 +506,9 @@ async def get_mega_data(guild_id: int, user: dict = Depends(get_current_user)): "anti_spam": antispam_active, "global_network": global_active, "logging": logging_active, + "auto_responder": autoresponder_active, + "applications": applications_active, + "news_sync": newssync_active, "economy": False }, "stats": stats, @@ -499,5 +526,6 @@ async def get_mega_data(guild_id: int, user: dict = Depends(get_current_user)): dashboard_main_router.include_router(auth_router) dashboard_main_router.include_router(settings_router) dashboard_main_router.include_router(user_router) +dashboard_main_router.include_router(management_router) # dashboard_main_router.include_router(router_public) # Move to main.py for root access diff --git a/src/bot/cogs/cogs/bot/about.py b/src/bot/cogs/bot/about.py similarity index 100% rename from src/bot/cogs/cogs/bot/about.py rename to src/bot/cogs/bot/about.py diff --git a/src/bot/cogs/cogs/bot/admin.py b/src/bot/cogs/bot/admin.py similarity index 100% rename from src/bot/cogs/cogs/bot/admin.py rename to src/bot/cogs/bot/admin.py diff --git a/src/bot/cogs/cogs/bot/botjoinevent.py b/src/bot/cogs/bot/botjoinevent.py similarity index 100% rename from src/bot/cogs/cogs/bot/botjoinevent.py rename to src/bot/cogs/bot/botjoinevent.py diff --git a/src/bot/cogs/bot/kuma_status.py b/src/bot/cogs/bot/kuma_status.py new file mode 100644 index 0000000..78350f1 --- /dev/null +++ b/src/bot/cogs/bot/kuma_status.py @@ -0,0 +1,212 @@ +# Copyright (c) 2026 ManagerX Development +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# >> Imports +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +import discord +import ezcord +from discord.ext import tasks +from discord.ui import Container, DesignerView # Strictly following example imports +import json +import os +import aiohttp +import logging +from datetime import datetime +import asyncio + +from src.bot.core.config import BotConfig +from src.bot.ui.emojis import emoji_yes, emoji_no, emoji_statistics, emoji_summary + +logger = logging.getLogger(__name__) + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# >> Constants & Hardcoded Config +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# SET YOUR CHANNEL ID HERE +STATUS_CHANNEL_ID = 0 # <--- HIER DIE CHANNEL ID EINTRAGEN + +KUMA_BASE_URL = "https://status.oppro-network.de" +KUMA_SLUG = "status" +UPDATE_INTERVAL = 60 # Sekunden + +STATE_FILE = "data/kuma_status_message.json" + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# >> Fetcher Logic +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +class KumaFetcher: + """Utility to fetch and parse Uptime Kuma status page data.""" + + def __init__(self, base_url: str, slug: str): + self.base_url = base_url.rstrip("/") + self.slug = slug + self.api_config_url = f"{self.base_url}/api/status-page/{self.slug}" + self.api_heartbeat_url = f"{self.base_url}/api/status-page/heartbeat/{self.slug}" + + async def _get(self, url): + async with aiohttp.ClientSession() as session: + try: + headers = {"User-Agent": "ManagerX/2.0.0 (Status Bot)"} + async with session.get(url, timeout=10, headers=headers) as response: + if response.status == 200: + return await response.json() + print(f"[KUMA ERROR] HTTP {response.status} von {url}") + return None + except Exception as e: + print(f"[KUMA FETCH EXCEPTION] {e}") + return None + + async def fetch_grouped_status(self): + """Fetches both config and heartbeats and returns grouped data with PING.""" + config_data = await self._get(self.api_config_url) + heartbeat_data = await self._get(self.api_heartbeat_url) + + if not config_data or not heartbeat_data: + return None + + heartbeats = heartbeat_data.get("heartbeatList", {}) + uptime_list = heartbeat_data.get("uptimeList", {}) + + grouped_data = [] + public_groups = config_data.get("publicGroupList", []) + + for group in public_groups: + group_monitors = [] + for monitor in group.get("monitorList", []): + m_id = str(monitor.get("id")) + + # Get latest heartbeat (status + ping) + m_heartbeats = heartbeats.get(m_id, []) + if m_heartbeats: + latest = m_heartbeats[-1] + status_code = latest.get("status", 0) + ping = latest.get("ping") # Ping in milliseconds + else: + status_code = 0 + ping = None + + status_map = {1: "UP", 0: "DOWN", 2: "PENDING", 3: "MAINTENANCE"} + + # Get uptime percentage + uptime_val = uptime_list.get(f"{m_id}_24", 0) + if not uptime_val: + uptime_val = next((v for k, v in uptime_list.items() if k.startswith(f"{m_id}_")), 0) + + group_monitors.append({ + "name": monitor.get("name", "Unknown"), + "status": status_map.get(status_code, "DOWN"), + "uptime": uptime_val * 100 if uptime_val else None, + "ping": ping + }) + + if group_monitors: + grouped_data.append({ + "name": group.get("name", "Unkategorisiert"), + "monitors": group_monitors + }) + + return grouped_data + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# >> Cog Logic +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +class KumaStatus(ezcord.Cog): + """Hardcoded Uptime Kuma monitoring system with Response Times.""" + + def __init__(self, bot): + self.bot = bot + self.message_id = self._load_state() + print(f"[KUMA DEBUG] Cog geladen. Ziel-Channel: {STATUS_CHANNEL_ID}") + self.update_status.start() + + def cog_unload(self): + self.update_status.cancel() + + def _load_state(self): + if os.path.exists(STATE_FILE): + try: + with open(STATE_FILE, "r") as f: + return json.load(f).get("message_id") + except Exception: return None + return None + + def _save_state(self, msg_id): + self.message_id = msg_id + os.makedirs("data", exist_ok=True) + with open(STATE_FILE, "w") as f: + json.dump({"message_id": msg_id}, f, indent=4) + + async def _create_status_view(self, groups): + """Generates the status message including response times.""" + container = Container(color=discord.Color.from_rgb(*BotConfig.ui.colors.primary)) + + container.add_text(f"## ๐ŸŒ Infrastruktur Status") + container.add_text("Echtzeit-รœberwachung der OPPRO.NET Server und Dienste.") + container.add_separator() + + overall_up = True + + for group in groups: + group_text = "" + for m in group["monitors"]: + indicator = "๐ŸŸข" if m["status"] == "UP" else "๐Ÿ”ด" + if m["status"] != "UP": overall_up = False + + # Format: Name (Uptime | Ping) + details = [] + if m.get("uptime"): details.append(f"{m['uptime']:.2f}%") + if m.get("ping") is not None: details.append(f"{m['ping']}ms") + + detail_str = f" ({' | '.join(details)})" if details else "" + group_text += f"{indicator} **{m['name']}** โ€ข `{m['status']}`{detail_str}\n" + + container.add_text(f"### ๐Ÿ“ {group['name']}") + container.add_text(group_text or "*Keine Monitore*") + container.add_separator() + + summary = "Alle Systeme laufen einwandfrei." if overall_up else "Einige Dienste sind derzeit beeintrรคchtigt." + container.add_text(f"### {emoji_statistics} Zusammenfassung") + container.add_text(f"{'โœ…' if overall_up else 'โš ๏ธ'} {summary}") + + try: + container.set_footer(text=f"{BotConfig.ui.footer_text} โ€ข Update: {UPDATE_INTERVAL}s") + except AttributeError: + container.add_separator() + container.add_text(f"*{BotConfig.ui.footer_text} โ€ข Update: {UPDATE_INTERVAL}s*") + + return DesignerView(container, timeout=0) + + @tasks.loop(seconds=UPDATE_INTERVAL) + async def update_status(self): + if STATUS_CHANNEL_ID == 0: return + + fetcher = KumaFetcher(KUMA_BASE_URL, KUMA_SLUG) + groups = await fetcher.fetch_grouped_status() + + if not groups: + print(f"[KUMA] Konnte keine gruppierten Daten abrufen.") + return + + view = await self._create_status_view(groups) + + try: + channel = self.bot.get_channel(STATUS_CHANNEL_ID) + if not channel: channel = await self.bot.fetch_channel(STATUS_CHANNEL_ID) + + if self.message_id: + try: + msg = await channel.fetch_message(self.message_id) + await msg.edit(view=view) + except discord.NotFound: + msg = await channel.send(view=view) + self._save_state(msg.id) + else: + msg = await channel.send(view=view) + self._save_state(msg.id) + except Exception as e: print(f"Error: {e}") + + @update_status.before_loop + async def before_update_status(self): + await self.bot.wait_until_ready() + +def setup(bot): + bot.add_cog(KumaStatus(bot)) diff --git a/src/bot/cogs/cogs/bot/server_join_alert.py b/src/bot/cogs/bot/server_join_alert.py similarity index 100% rename from src/bot/cogs/cogs/bot/server_join_alert.py rename to src/bot/cogs/bot/server_join_alert.py diff --git a/src/bot/cogs/cogs/bot/server_leave_alert.py b/src/bot/cogs/bot/server_leave_alert.py similarity index 100% rename from src/bot/cogs/cogs/bot/server_leave_alert.py rename to src/bot/cogs/bot/server_leave_alert.py diff --git a/src/bot/cogs/cogs/bot/status.py b/src/bot/cogs/bot/status.py similarity index 100% rename from src/bot/cogs/cogs/bot/status.py rename to src/bot/cogs/bot/status.py diff --git a/src/bot/cogs/cogs/economy/gb_economy.py b/src/bot/cogs/economy/gb_economy.py similarity index 100% rename from src/bot/cogs/cogs/economy/gb_economy.py rename to src/bot/cogs/economy/gb_economy.py diff --git a/src/bot/cogs/cogs/economy/gld_economy.py b/src/bot/cogs/economy/gld_economy.py similarity index 100% rename from src/bot/cogs/cogs/economy/gld_economy.py rename to src/bot/cogs/economy/gld_economy.py diff --git a/src/bot/cogs/cogs/fun/4gewinnt.py b/src/bot/cogs/fun/4gewinnt.py similarity index 100% rename from src/bot/cogs/cogs/fun/4gewinnt.py rename to src/bot/cogs/fun/4gewinnt.py diff --git a/src/bot/cogs/cogs/fun/tictactoe.py b/src/bot/cogs/fun/tictactoe.py similarity index 100% rename from src/bot/cogs/cogs/fun/tictactoe.py rename to src/bot/cogs/fun/tictactoe.py diff --git a/src/bot/cogs/cogs/guild/globalchat.py b/src/bot/cogs/guild/globalchat.py similarity index 95% rename from src/bot/cogs/cogs/guild/globalchat.py rename to src/bot/cogs/guild/globalchat.py index dd6c046..78e1460 100644 --- a/src/bot/cogs/cogs/guild/globalchat.py +++ b/src/bot/cogs/guild/globalchat.py @@ -429,6 +429,36 @@ async def _get_all_active_channels(self) -> List[int]: self._cached_channels = await self._fetch_all_channels() return self._cached_channels +class GlobalChatReportView(discord.ui.View): + def __init__(self, message_id: int, author_id: int, guild_id: int): + super().__init__(timeout=None) + self.message_id = message_id + self.author_id = author_id + self.guild_id = guild_id + + @discord.ui.button(label="Melden", style=discord.ButtonStyle.secondary, emoji="๐Ÿšฉ", custom_id="gc_report") + async def report_button(self, button: discord.ui.Button, interaction: discord.Interaction): + # Notify staff (owners) + owners = [1093555256689959005, 1427994077332373554] + embed = discord.Embed( + title="โš ๏ธ GlobalChat Meldung", + description=f"Eine Nachricht wurde gemeldet.\n" + f"**Sender ID:** `{self.author_id}`\n" + f"**Nachricht ID:** `{self.message_id}`\n" + f"**Server ID:** `{self.guild_id}`", + color=discord.Color.orange(), + timestamp=discord.utils.utcnow() + ) + embed.set_footer(text=f"Gemeldet von: {interaction.user} ({interaction.user.id})") + + for owner_id in owners: + try: + owner = await interaction.client.fetch_user(owner_id) + await owner.send(embed=embed) + except: pass + + await interaction.response.send_message("โœ… Danke! Die Nachricht wurde an das Moderations-Team weitergeleitet.", ephemeral=True) + async def _fetch_all_channels(self) -> List[int]: try: # โœ… await hinzugefรผgt @@ -437,7 +467,7 @@ async def _fetch_all_channels(self) -> List[int]: logger.error(f"โŒ Fehler beim Abrufen aller Channel-IDs: {e}", exc_info=True) return [] - async def _send_to_channel(self, channel_id: int, embed: discord.Embed, attachment_bytes: List[Tuple[str, bytes]]) -> bool: + async def _send_to_channel(self, channel_id: int, embed: discord.Embed, attachment_bytes: List[Tuple[str, bytes]], view: discord.ui.View = None) -> bool: try: channel = self.bot.get_channel(channel_id) if not channel: @@ -465,9 +495,9 @@ async def _send_to_channel(self, channel_id: int, embed: discord.Embed, attachme for attempt in range(max_retries): try: if files: - await channel.send(embed=embed, files=files) + await channel.send(embed=embed, files=files, view=view) else: - await channel.send(embed=embed) + await channel.send(embed=embed, view=view) return True except (ConnectionResetError, aiohttp.ClientConnectorError, asyncio.TimeoutError) as e: logger.warning(f"โŒ Sendefehler (Retry {attempt+1}/{max_retries}) in {channel_id}: {e}") @@ -488,22 +518,28 @@ async def _send_to_channel(self, channel_id: int, embed: discord.Embed, attachme return False async def send_global_message(self, message: discord.Message, attachment_data: List[Tuple[str, bytes, str]] = None) -> Tuple[int, int]: - # โœ… await hinzugefรผgt settings = await db.get_guild_settings(message.guild.id) embed, files_to_upload = await self.embed_builder.create_message_embed(message, settings, attachment_data) active_channels = await self._get_all_active_channels() successful_sends, failed_sends = 0, 0 - task_list = [self._send_to_channel(channel_id, embed, files_to_upload) for channel_id in active_channels] - results = await asyncio.gather(*task_list, return_exceptions=True) - - for result in results: - if result is True: - successful_sends += 1 - else: - failed_sends += 1 - if isinstance(result, Exception): - logger.error(f"โŒ Task-Fehler beim Senden: {result}") + # Reporting View + view = GlobalChatReportView(message.id, message.author.id, message.guild.id) + + # Batching (split into groups of 10 to reduce lag) + batch_size = 10 + for i in range(0, len(active_channels), batch_size): + current_batch = active_channels[i:i + batch_size] + task_list = [self._send_to_channel(channel_id, embed, files_to_upload, view) for channel_id in current_batch] + results = await asyncio.gather(*task_list, return_exceptions=True) + + for result in results: + if result is True: + successful_sends += 1 + else: + failed_sends += 1 + + await asyncio.sleep(0.1) # Prevents hitting rate limits too hard return successful_sends, failed_sends diff --git a/src/bot/cogs/cogs/guild/levelsystem.py b/src/bot/cogs/guild/levelsystem.py similarity index 100% rename from src/bot/cogs/cogs/guild/levelsystem.py rename to src/bot/cogs/guild/levelsystem.py diff --git a/src/bot/cogs/cogs/guild/loggingsystem.py b/src/bot/cogs/guild/loggingsystem.py similarity index 100% rename from src/bot/cogs/cogs/guild/loggingsystem.py rename to src/bot/cogs/guild/loggingsystem.py diff --git a/src/bot/cogs/guild/news_sync.py b/src/bot/cogs/guild/news_sync.py new file mode 100644 index 0000000..f96fe45 --- /dev/null +++ b/src/bot/cogs/guild/news_sync.py @@ -0,0 +1,129 @@ +# Copyright (c) 2026 ManagerX Development +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# >> Imports +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +import discord +import ezcord +from discord import slash_command, Option +from discord.ui import Container, DesignerView +from mxmariadb import ManagementDatabase +import logging +import io + +db = ManagementDatabase() +logger = logging.getLogger(__name__) + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# >> Constants & Hardcoded Config +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +DEV_MASTER_CHANNEL_ID = 1474438159465971872 + +class NewsSync(ezcord.Cog): + """Synchronizes messages across custom server networks.""" + + def __init__(self, bot): + self.bot = bot + + @ezcord.Cog.listener() + async def on_ready(self): + await db.ensure_connection() + + @ezcord.Cog.listener() + async def on_message(self, message: discord.Message): + if message.author.bot or not message.guild: + return + + await db.ensure_connection() + all_channels = await db.get_sync_channels() + + # 1. Check for Developer News (Always Global) + if message.channel.id == DEV_MASTER_CHANNEL_ID: + targets = [c for c in all_channels if c['sync_group'] == 'dev_news' and not c['is_master']] + if targets: + embed = self._build_dev_embed(message) + await self._broadcast(targets, embed, message) + return + + # 2. Check if this is a Master for ANY network + # The sync_group for regular networks is the MASTER'S GUILD ID + masters = [c for c in all_channels if c['channel_id'] == message.channel.id and c['is_master'] and c['sync_group'] != 'dev_news'] + + for master in masters: + network_id = master['sync_group'] + # Find all subscribers to THIS SPECIFIC network_id + targets = [c for c in all_channels if c['sync_group'] == network_id and not c['is_master']] + + if targets: + embed = self._build_network_embed(message) + await self._broadcast(targets, embed, message) + + def _build_dev_embed(self, message): + embed = discord.Embed( + title="๐Ÿ› ๏ธ **ManagerX Engineering Updates**", + description=message.content or "*Bild-Nachricht*", + color=discord.Color.gold(), + timestamp=message.created_at + ) + embed.set_footer(text=f"Official Developer Feed โ€ข {message.guild.name}") + if message.attachments: embed.set_image(url=message.attachments[0].url) + return embed + + def _build_network_embed(self, message): + embed = discord.Embed( + description=message.content or "*Ankรผndigung*", + color=discord.Color.blue(), + timestamp=message.created_at + ) + embed.set_author(name=f"News von {message.guild.name}", icon_url=message.guild.icon.url if message.guild.icon else None) + if message.attachments: embed.set_image(url=message.attachments[0].url) + return embed + + async def _broadcast(self, targets, embed, original_message): + for target in targets: + try: + channel = self.bot.get_channel(target['channel_id']) + if not channel: channel = await self.bot.fetch_channel(target['channel_id']) + await channel.send(embed=embed) + except: pass + + # --- Commands --- + news_sync = discord.SlashCommandGroup("newssync", "Verwalte dein eigenes Server-Netzwerk") + + @news_sync.command(name="set-master", description="Macht diesen Kanal zum Haupt-Kanal DEINES Netzwerks") + async def set_master(self, ctx: discord.ApplicationContext): + await db.ensure_connection() + if not ctx.author.guild_permissions.manage_guild: + return await ctx.respond("โŒ Berechtigung **Server verwalten** erforderlich.", ephemeral=True) + + # Network ID is the Guild ID of the master server + network_id = str(ctx.guild.id) + await db.add_sync_channel(ctx.guild.id, ctx.channel.id, is_master=True, sync_group=network_id) + + await ctx.respond(f"โœ… Dieser Kanal ist nun der Master fรผr dein Netzwerk!\n**Netzwerk-ID:** `{network_id}`\n\nAndere Server kรถnnen dein Netzwerk abonnieren mit:\n`/newssync subscribe {network_id}`", ephemeral=True) + + @news_sync.command(name="subscribe", description="Abonniere ein spezifisches Server-Netzwerk per ID") + async def subscribe(self, ctx: discord.ApplicationContext, network_id: Option(str, "Die ID des Netzwerks (Server-ID des Masters)", required=True)): + await db.ensure_connection() + if not ctx.author.guild_permissions.manage_guild: + return await ctx.respond("โŒ Berechtigung **Server verwalten** erforderlich.", ephemeral=True) + + # Check if it's the dev news ID manually redirected + if network_id == "dev": network_id = "dev_news" + + await db.add_sync_channel(ctx.guild.id, ctx.channel.id, is_master=False, sync_group=network_id) + await ctx.respond(f"โœ… Erfolg! Dieser Kanal erhรคlt nun News vom Netzwerk `{network_id}`.", ephemeral=True) + + # --- Dev News --- + dev_news = discord.SlashCommandGroup("devnews", "Offizielle ManagerX News") + + @dev_news.command(name="subscribe", description="Abonniere die offiziellen Updates der Entwickler") + async def subscribe_dev(self, ctx: discord.ApplicationContext): + await db.ensure_connection() + if not ctx.author.guild_permissions.manage_guild: + return await ctx.respond("โŒ Berechtigung erforderlich.", ephemeral=True) + + await db.add_sync_channel(ctx.guild.id, ctx.channel.id, is_master=False, sync_group="dev_news") + await ctx.respond(f"๐Ÿ› ๏ธ Du erhรคltst nun offizielle Bot-Updates!", ephemeral=True) + +def setup(bot): + bot.add_cog(NewsSync(bot)) diff --git a/src/bot/cogs/cogs/guild/tempvc.py b/src/bot/cogs/guild/tempvc.py similarity index 100% rename from src/bot/cogs/cogs/guild/tempvc.py rename to src/bot/cogs/guild/tempvc.py diff --git a/src/bot/cogs/cogs/guild/utility.py b/src/bot/cogs/guild/utility.py similarity index 100% rename from src/bot/cogs/cogs/guild/utility.py rename to src/bot/cogs/guild/utility.py diff --git a/src/bot/cogs/cogs/guild/welcome.py b/src/bot/cogs/guild/welcome.py similarity index 100% rename from src/bot/cogs/cogs/guild/welcome.py rename to src/bot/cogs/guild/welcome.py diff --git a/src/bot/cogs/cogs/legacy/secret_commands.py b/src/bot/cogs/legacy/secret_commands.py similarity index 100% rename from src/bot/cogs/cogs/legacy/secret_commands.py rename to src/bot/cogs/legacy/secret_commands.py diff --git a/src/bot/cogs/management/applications.py b/src/bot/cogs/management/applications.py new file mode 100644 index 0000000..0ee8bf9 --- /dev/null +++ b/src/bot/cogs/management/applications.py @@ -0,0 +1,146 @@ +# Copyright (c) 2026 ManagerX Development +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# >> Imports +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +import discord +import ezcord +from discord import slash_command, Option +from discord.ui import Container, DesignerView, Button, View +from mxmariadb import ManagementDatabase +import logging +import asyncio + +db = ManagementDatabase() +logger = logging.getLogger(__name__) + +# --- Database Extension for Settings --- +# Note: I'll add a simple setting fetcher for the log channel +async def get_app_log_channel(guild_id: int): + # For now, we'll use a simple approach or add it to the DB if not exists + # Alternatively, we just use the current channel if none is set. + return None + +class ApplicationActionButtons(View): + """View with Accept/Decline buttons for Admins.""" + def __init__(self, user_id: int): + super().__init__(timeout=None) + self.user_id = user_id + + @discord.ui.button(label="Annehmen", style=discord.ButtonStyle.success, emoji="โœ…", custom_id="app_accept") + async def accept(self, button: Button, interaction: discord.Interaction): + user = await interaction.client.fetch_user(self.user_id) + if user: + try: + await user.send(f"๐ŸŽ‰ **Deine Bewerbung auf {interaction.guild.name} wurde angenommen!** Ein Teammitglied wird sich in Kรผrze bei dir melden.") + except: pass + + await interaction.response.edit_message(content=f"โœ… **Bewerbung von <@{self.user_id}> angenommen durch {interaction.user.mention}**", view=None) + + @discord.ui.button(label="Ablehnen", style=discord.ButtonStyle.danger, emoji="โŒ", custom_id="app_deny") + async def deny(self, button: Button, interaction: discord.Interaction): + user = await interaction.client.fetch_user(self.user_id) + if user: + try: + await user.send(f"โŒ **Deine Bewerbung auf {interaction.guild.name} wurde leider abgelehnt.** Trotzdem danke fรผr dein Interesse!") + except: pass + + await interaction.response.edit_message(content=f"โŒ **Bewerbung von <@{self.user_id}> abgelehnt durch {interaction.user.mention}**", view=None) + +class ApplicationStartButton(View): + """The initial 'Bewerben' button view.""" + def __init__(self): + super().__init__(timeout=None) + + @discord.ui.button(label="Bewerben", style=discord.ButtonStyle.success, emoji="๐Ÿ“ฉ", custom_id="start_app") + async def start_app(self, button: Button, interaction: discord.Interaction): + await db.ensure_connection() # Tabellen sicher erstellen + questions = await db.get_questions(interaction.guild.id) + if not questions: + return await interaction.response.send_message("โŒ Bewerbungen sind derzeit nicht konfiguriert.", ephemeral=True) + + await interaction.response.send_message("๐Ÿ“ฉ Ich habe dir eine Direktnachricht geschickt!", ephemeral=True) + + try: + answers = [] + dm_channel = await interaction.user.create_dm() + + for q in questions: + container = Container(color=discord.Color.blue()) + container.add_text(f"### ๐Ÿ“ Frage: {q['question_text']}") + container.add_text("*Bitte schreibe deine Antwort in den Chat.*") + + await dm_channel.send(view=DesignerView(container, timeout=0)) + + def check(m): + return m.author == interaction.user and m.channel == dm_channel + + try: + msg = await interaction.client.wait_for('message', check=check, timeout=600.0) + answers.append((q['question_text'], msg.content)) + except asyncio.TimeoutError: + return await dm_channel.send("โฐ Zeit abgelaufen. Deine Bewerbung wurde abgebrochen.") + + await dm_channel.send("โœ… Deine Bewerbung wurde erfolgreich eingereicht!") + + # Send to Admins + container = Container(color=discord.Color.gold()) + container.add_text(f"## ๐Ÿ“ฉ Neue Bewerbung von {interaction.user}") + container.add_text(f"User ID: `{interaction.user.id}` | Erwรคhnung: {interaction.user.mention}") + container.add_separator() + + for q_text, ans_text in answers: + container.add_text(f"**{q_text}**\n> {ans_text}") + container.add_separator() + + admin_view = DesignerView(container, timeout=None) + # Add the persistent action buttons + admin_action_view = ApplicationActionButtons(interaction.user.id) + + # Log it! (In a real system, we'd use a channel ID from DB) + target_channel = interaction.channel + await target_channel.send(view=admin_view) + await target_channel.send(view=admin_action_view) + + except discord.Forbidden: + await interaction.followup.send("โŒ Ich kann dir keine DM schicken!", ephemeral=True) + +class Applications(ezcord.Cog): + """Fully functional Application system with Interactive Buttons.""" + + def __init__(self, bot): + self.bot = bot + + @ezcord.Cog.listener() + async def on_ready(self): + await db.ensure_connection() + self.bot.add_view(ApplicationStartButton()) # Register initial button + + app = discord.SlashCommandGroup("application", "Verwalte das Bewerbungssystem") + + @app.command(name="setup-questions", description="Setzt die Fragen fรผr das System (kommagetrennt)") + async def setup_questions(self, ctx: discord.ApplicationContext, questions: str): + if not ctx.author.guild_permissions.manage_guild: + return await ctx.respond("โŒ Berechtigung fehlt.", ephemeral=True) + + await db.clear_questions(ctx.guild.id) + q_list = [q.strip() for q in questions.split(",")] + + for i, q_text in enumerate(q_list): + await db.add_question(ctx.guild.id, q_text, i) + + await ctx.respond(f"โœ… {len(q_list)} Fragen gespeichert!", ephemeral=True) + + @app.command(name="post", description="Postet das Bewerbungs-Embed mit Button") + async def post_app(self, ctx: discord.ApplicationContext): + if not ctx.author.guild_permissions.manage_guild: + return await ctx.respond("โŒ Berechtigung fehlt.", ephemeral=True) + + container = Container(color=discord.Color.green()) + container.add_text("## ๐Ÿ“ฉ Team-Bewerbung") + container.add_text("Klicke auf den Button unten, um den Prozess zu starten.") + + await ctx.channel.send(view=ApplicationStartButton()) + await ctx.respond("โœ… Bewerbungsposten erstellt!", ephemeral=True) + +def setup(bot): + bot.add_cog(Applications(bot)) diff --git a/src/bot/cogs/management/auto_responder.py b/src/bot/cogs/management/auto_responder.py new file mode 100644 index 0000000..6b5ddc7 --- /dev/null +++ b/src/bot/cogs/management/auto_responder.py @@ -0,0 +1,115 @@ +# Copyright (c) 2026 ManagerX Development +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# >> Imports +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +import discord +import ezcord +from discord import slash_command, Option +from discord.ui import Container, DesignerView +from mxmariadb import ManagementDatabase +import logging + +db = ManagementDatabase() +logger = logging.getLogger(__name__) + +class AutoResponder(ezcord.Cog): + """Self-configurable Keyword-Responder using Container UI.""" + + def __init__(self, bot): + self.bot = bot + + @ezcord.Cog.listener() + async def on_ready(self): + await db.ensure_connection() + logger.info("AutoResponder DB connection ensured.") + + @ezcord.Cog.listener() + async def on_message(self, message: discord.Message): + if message.author.bot or not message.guild: + return + + await db.ensure_connection() # Sicherstellen, dass Tabellen existieren + + try: + # Fetch responses for this guild + responses = await db.get_auto_responses(message.guild.id) + if not responses: + return + + content_lower = message.content.lower() + + for res in responses: + keyword = res['keyword'].lower() + response_text = res['response'] + match_type = res['match_type'] + res_id = res['id'] + + should_respond = False + + if match_type == 'exact': + if content_lower == keyword: + should_respond = True + else: # partial + if keyword in content_lower: + should_respond = True + + if should_respond: + # Build Container Response + container = Container(color=discord.Color.blue()) + container.add_text(f"### ๐Ÿค– Automatische Antwort: `{keyword.upper()}`") + container.add_text(response_text) + container.add_separator() + container.add_text(f"*Diese Nachricht wurde automatisch basierend auf deinem Keyword gesendet.*") + + view = DesignerView(container, timeout=0) + await message.channel.send(view=view, reference=message) + break + except Exception as e: + logger.error(f"Error in AutoResponder on_message: {e}") + + # --- Commands --- + auto_respond = discord.SlashCommandGroup("autoresponder", "Verwalte den Auto-Responder") + + @auto_respond.command(name="add", description="Fรผgt ein neues Keyword hinzu") + async def add_keyword( + self, + ctx: discord.ApplicationContext, + keyword: Option(str, "Das Wort, auf das der Bot reagieren soll", required=True), + response: Option(str, "Die Antwort des Bots", required=True), + match_type: Option(str, "Wie soll das Wort erkannt werden?", choices=["Teilweise", "Exakt"], default="Teilweise") + ): + await db.ensure_connection() + if not ctx.author.guild_permissions.manage_guild: + return await ctx.respond("โŒ Du benรถtigst die Berechtigung **Server verwalten**!", ephemeral=True) + + m_type = "partial" if match_type == "Teilweise" else "exact" + await db.add_auto_response(ctx.guild.id, keyword, response, m_type) + + await ctx.respond(f"โœ… Keyword `{keyword}` wurde hinzugefรผgt!\nModus: **{match_type}**", ephemeral=True) + + @auto_respond.command(name="list", description="Zeigt alle Keywords an") + async def list_keywords(self, ctx: discord.ApplicationContext): + responses = await db.get_auto_responses(ctx.guild.id) + if not responses: + return await ctx.respond("โŒ Keine Keywords eingerichtet.", ephemeral=True) + + container = Container() + container.add_text("## ๐Ÿค– Eingerichtete Keywords") + + for res in responses: + container.add_text(f"ID: `{res['id']}` | **{res['keyword']}** ({res['match_type']})\n> {res['response'][:50]}...") + container.add_separator() + + view = DesignerView(container, timeout=None) + await ctx.respond(view=view, ephemeral=True) + + @auto_respond.command(name="remove", description="Entfernt ein Keyword per ID") + async def remove_keyword(self, ctx: discord.ApplicationContext, id: int): + if not ctx.author.guild_permissions.manage_guild: + return await ctx.respond("โŒ Du benรถtigst die Berechtigung **Server verwalten**!", ephemeral=True) + + await db.remove_auto_response(ctx.guild.id, id) + await ctx.respond(f"โœ… Keyword mit ID `{id}` wurde entfernt.", ephemeral=True) + +def setup(bot): + bot.add_cog(AutoResponder(bot)) diff --git a/src/bot/cogs/cogs/management/autodelete.py b/src/bot/cogs/management/autodelete.py similarity index 100% rename from src/bot/cogs/cogs/management/autodelete.py rename to src/bot/cogs/management/autodelete.py diff --git a/src/bot/cogs/cogs/management/autorole.py b/src/bot/cogs/management/autorole.py similarity index 100% rename from src/bot/cogs/cogs/management/autorole.py rename to src/bot/cogs/management/autorole.py diff --git a/src/bot/cogs/cogs/moderation/antispam.py b/src/bot/cogs/moderation/antispam.py similarity index 94% rename from src/bot/cogs/cogs/moderation/antispam.py rename to src/bot/cogs/moderation/antispam.py index 713ad42..3c0bf72 100644 --- a/src/bot/cogs/cogs/moderation/antispam.py +++ b/src/bot/cogs/moderation/antispam.py @@ -4,9 +4,34 @@ import discord from discord import SlashCommandGroup import ezcord -import datetime -from datetime import timedelta - +from datetime import datetime, timezone, timedelta +from src.bot.core.config import BotConfig + +# Branding & Colors (Synced from BotConfig) +SUCCESS_COLOR = discord.Color.from_rgb(*BotConfig.ui.colors.success) +ERROR_COLOR = discord.Color.from_rgb(*BotConfig.ui.colors.error) +WARN_COLOR = discord.Color.from_rgb(*BotConfig.ui.colors.warning) + +# Emojis directly from UI module +try: + from src.bot.ui.emojis import ( + emoji_yes, emoji_no, emoji_warn, emoji_member, emoji_staff, + emoji_statistics, emoji_channel, emoji_moderator, emoji_forbidden, + emoji_owner, emoji_delete, emoji_error + ) +except ImportError: + emoji_yes = "โœ…" + emoji_no = "โŒ" + emoji_warn = "โš ๏ธ" + emoji_member = "๐Ÿ‘ค" + emoji_staff = "๐Ÿ›ก๏ธ" + emoji_statistics = "๐Ÿ“Š" + emoji_channel = "#๏ธโƒฃ" + emoji_moderator = "๐Ÿ‘ฎ" + emoji_forbidden = "๐Ÿšซ" + emoji_owner = "๐Ÿ‘‘" + emoji_delete = "๐Ÿ—‘๏ธ" + emoji_error = "โŒ" from mxmariadb import AntiSpamDatabase as SpamDB @@ -33,7 +58,7 @@ async def on_message(self, message): return # Get spam settings for this guild - settings = self.db.get_spam_settings(message.guild.id) + settings = await self.db.get_spam_settings(message.guild.id) if not settings: # If no settings are configured, don't process spam detection return @@ -45,7 +70,7 @@ async def on_message(self, message): # Record this message timestamp user_id = message.author.id guild_id = message.guild.id - current_time = datetime.now() + current_time = datetime.now(timezone.utc) # Add current message to tracking self.user_messages[guild_id][user_id].append(current_time) @@ -133,7 +158,7 @@ async def send_spam_log(self, guild, user, message, settings, timeout_applied): embed = discord.Embed( title=f"{emoji_warn} ร— Anti-Spam VerstoรŸ", color=discord.Color.red(), - timestamp=datetime.now() + timestamp=datetime.now(timezone.utc) ) embed.add_field( name=f"{emoji_member} ร— Benutzer", diff --git a/src/bot/cogs/cogs/moderation/moderation.py b/src/bot/cogs/moderation/moderation.py similarity index 98% rename from src/bot/cogs/cogs/moderation/moderation.py rename to src/bot/cogs/moderation/moderation.py index 5daf98c..2855556 100644 --- a/src/bot/cogs/cogs/moderation/moderation.py +++ b/src/bot/cogs/moderation/moderation.py @@ -4,25 +4,30 @@ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ import asyncio import re -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta from typing import Optional, Dict, List import logging import discord import ezcord from discord import slash_command, option -import timedelta -from discord.ui import Container from discord import SlashCommandGroup +from src.bot.core.config import BotConfig + +# Permission Constants +BAN = "ban_members" +KICK = "kick_members" +MODERATE = "moderate_members" + # Importiere zentrale Konstanten -# Branding & Colors (Local Fallbacks) -SUCCESS_COLOR = 0x2ecc71 -ERROR_COLOR = 0xe74c3c -WARN_COLOR = 0xf39c12 -INFO_COLOR = 0x3498db -AUTHOR = "ManagerX" -FLOOTER = "ManagerX Bot" +# Branding & Colors (Synced from BotConfig) +SUCCESS_COLOR = discord.Color.from_rgb(*BotConfig.ui.colors.success) +ERROR_COLOR = discord.Color.from_rgb(*BotConfig.ui.colors.error) +WARN_COLOR = discord.Color.from_rgb(*BotConfig.ui.colors.warning) +INFO_COLOR = discord.Color.from_rgb(*BotConfig.ui.colors.info) +AUTHOR = BotConfig.bot.name +FLOOTER = BotConfig.ui.footer_text # Emojis directly from UI module try: diff --git a/src/bot/cogs/cogs/moderation/notes.py b/src/bot/cogs/moderation/notes.py similarity index 100% rename from src/bot/cogs/cogs/moderation/notes.py rename to src/bot/cogs/moderation/notes.py diff --git a/src/bot/cogs/cogs/moderation/warn.py b/src/bot/cogs/moderation/warn.py similarity index 98% rename from src/bot/cogs/cogs/moderation/warn.py rename to src/bot/cogs/moderation/warn.py index 64993c6..e31b218 100644 --- a/src/bot/cogs/cogs/moderation/warn.py +++ b/src/bot/cogs/moderation/warn.py @@ -6,20 +6,21 @@ import discord from discord import slash_command, Option import os -import datetime -import datetime +from datetime import datetime, timezone, timedelta import ezcord import asyncio from typing import Optional +from src.bot.core.config import BotConfig + # Importiere zentrale Konstanten -# Branding & Colors (Local Fallbacks) -SUCCESS_COLOR = 0x2ecc71 -ERROR_COLOR = 0xe74c3c -WARN_COLOR = 0xf39c12 -INFO_COLOR = 0x3498db -AUTHOR = "ManagerX" -FLOOTER = "ManagerX Bot" +# Branding & Colors (Synced from BotConfig) +SUCCESS_COLOR = discord.Color.from_rgb(*BotConfig.ui.colors.success) +ERROR_COLOR = discord.Color.from_rgb(*BotConfig.ui.colors.error) +WARN_COLOR = discord.Color.from_rgb(*BotConfig.ui.colors.warning) +INFO_COLOR = discord.Color.from_rgb(*BotConfig.ui.colors.info) +AUTHOR = BotConfig.bot.name +FLOOTER = BotConfig.ui.footer_text # Emojis directly from UI module try: @@ -125,7 +126,7 @@ def _create_warn_embed(self, action: str, moderator: discord.Member, def _create_error_embed(self, title: str, message: str) -> discord.Embed: """Erstellt ein einheitliches Error-Embed""" - embed = discord.Embed(title=title, color=ERROR_COLOR, timestamp=datetime.datetime.now(datetime.timezone.utc)) + embed = discord.Embed(title=title, color=ERROR_COLOR, timestamp=datetime.now(timezone.utc)) embed.set_author(name=AUTHOR) embed.add_field(name=f"{emoji_no} {title}", value=message, inline=False) embed.set_footer(text=FLOOTER) diff --git a/src/bot/cogs/cogs/user/settings.py b/src/bot/cogs/user/settings.py similarity index 100% rename from src/bot/cogs/cogs/user/settings.py rename to src/bot/cogs/user/settings.py diff --git a/src/bot/cogs/cogs/user/stats.py b/src/bot/cogs/user/stats.py similarity index 100% rename from src/bot/cogs/cogs/user/stats.py rename to src/bot/cogs/user/stats.py diff --git a/src/web/App.tsx b/src/web/App.tsx index 25f7aa7..de29f2d 100644 --- a/src/web/App.tsx +++ b/src/web/App.tsx @@ -2,6 +2,8 @@ import { lazy, Suspense } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom"; import { motion, AnimatePresence } from "framer-motion"; +import { AuthProvider } from "./components/core/AuthProvider"; +import { ErrorBoundary } from "./components/core/ErrorBoundary"; // Lazy load all route components for better performance const Index = lazy(() => import("./pages/Index")); @@ -135,8 +137,6 @@ const AppContent = () => { return ; }; -import { AuthProvider } from "./components/AuthProvider"; -import { ErrorBoundary } from "./components/ErrorBoundary"; const App = () => ( diff --git a/src/web/components/AuthProvider.tsx b/src/web/components/core/AuthProvider.tsx similarity index 80% rename from src/web/components/AuthProvider.tsx rename to src/web/components/core/AuthProvider.tsx index ebf8166..24217ca 100644 --- a/src/web/components/AuthProvider.tsx +++ b/src/web/components/core/AuthProvider.tsx @@ -13,7 +13,7 @@ interface AuthContextType { const AuthContext = createContext(undefined); -import { API_URL } from "../lib/api"; +import { API_URL } from "../../lib/api"; export const AuthProvider = ({ children }: { children: ReactNode }) => { const getSafeItem = (key: string) => { @@ -66,27 +66,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { } }; - // --- AUTOMATISCHER CALLBACK-HANDLER --- - useEffect(() => { - const params = new URLSearchParams(window.location.search); - const code = params.get("code"); - const path = params.get("p"); - - if (code && (window.location.pathname.includes("auth/callback") || path?.includes("/auth/callback"))) { - fetch(`${API_URL}/dashboard/auth/callback`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ code }) - }) - .then(res => res.ok ? res.json() : Promise.reject(res)) - .then(data => { - login(data.access_token, data.user, data.discord_token); - window.history.replaceState({}, document.title, "/"); - }) - .catch(err => console.error("Login Error:", err)); - } - }, []); - // --- USER & GUILDS LADEN --- useEffect(() => { if (token) { diff --git a/src/web/components/ErrorBoundary.tsx b/src/web/components/core/ErrorBoundary.tsx similarity index 100% rename from src/web/components/ErrorBoundary.tsx rename to src/web/components/core/ErrorBoundary.tsx diff --git a/src/web/components/CTA.tsx b/src/web/components/landing/CTA.tsx similarity index 100% rename from src/web/components/CTA.tsx rename to src/web/components/landing/CTA.tsx diff --git a/src/web/components/FAQ.tsx b/src/web/components/landing/FAQ.tsx similarity index 100% rename from src/web/components/FAQ.tsx rename to src/web/components/landing/FAQ.tsx diff --git a/src/web/components/FeatureCard.tsx b/src/web/components/landing/FeatureCard.tsx similarity index 100% rename from src/web/components/FeatureCard.tsx rename to src/web/components/landing/FeatureCard.tsx diff --git a/src/web/components/Features.tsx b/src/web/components/landing/Features.tsx similarity index 100% rename from src/web/components/Features.tsx rename to src/web/components/landing/Features.tsx diff --git a/src/web/components/Hero.tsx b/src/web/components/landing/Hero.tsx similarity index 99% rename from src/web/components/Hero.tsx rename to src/web/components/landing/Hero.tsx index 67673b7..cab96a7 100644 --- a/src/web/components/Hero.tsx +++ b/src/web/components/landing/Hero.tsx @@ -2,7 +2,7 @@ import { memo } from "react"; import { motion } from "framer-motion"; import { Link } from "react-router-dom"; import { Shield, Users, MessageCircle, Sparkles, Zap, Activity, ArrowRight } from "lucide-react"; -import { useStats } from "../hooks/useStats"; +import { useStats } from "../../hooks/useStats"; export const Hero = memo(function Hero() { const { data, isLoading } = useStats(); diff --git a/src/web/components/Testimonials.tsx b/src/web/components/landing/Testimonials.tsx similarity index 99% rename from src/web/components/Testimonials.tsx rename to src/web/components/landing/Testimonials.tsx index 5632de1..c9f2b0f 100644 --- a/src/web/components/Testimonials.tsx +++ b/src/web/components/landing/Testimonials.tsx @@ -1,7 +1,7 @@ import { memo } from "react"; import { motion } from "framer-motion"; import { Star, Users, TrendingUp } from "lucide-react"; -import { useStats } from "../hooks/useStats"; +import { useStats } from "../../hooks/useStats"; const testimonials = [ { diff --git a/src/web/components/Footer.tsx b/src/web/components/layout/Footer.tsx similarity index 100% rename from src/web/components/Footer.tsx rename to src/web/components/layout/Footer.tsx diff --git a/src/web/components/GuildSelector.tsx b/src/web/components/layout/GuildSelector.tsx similarity index 98% rename from src/web/components/GuildSelector.tsx rename to src/web/components/layout/GuildSelector.tsx index 78e178f..df9a85d 100644 --- a/src/web/components/GuildSelector.tsx +++ b/src/web/components/layout/GuildSelector.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { useAuth } from "./AuthProvider"; +import { useAuth } from "../core/AuthProvider"; import { ChevronDown, Server } from "lucide-react"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; diff --git a/src/web/components/NavLink.tsx b/src/web/components/layout/NavLink.tsx similarity index 100% rename from src/web/components/NavLink.tsx rename to src/web/components/layout/NavLink.tsx diff --git a/src/web/components/Navbar.tsx b/src/web/components/layout/Navbar.tsx similarity index 99% rename from src/web/components/Navbar.tsx rename to src/web/components/layout/Navbar.tsx index 16755c3..c5b19e7 100644 --- a/src/web/components/Navbar.tsx +++ b/src/web/components/layout/Navbar.tsx @@ -5,8 +5,8 @@ import { Shield, Menu, X, Sparkles, Puzzle, Activity, Terminal, Newspaper, Users, Milestone, ChevronDown, LayoutDashboard, User, Trophy // Added Trophy } from "lucide-react"; -import { cn } from "../lib/utils"; -import { useAuth } from "./AuthProvider"; +import { cn } from "../../lib/utils"; +import { useAuth } from "../core/AuthProvider"; const mainLinks = [ { label: "Features", href: "/#features", icon: Sparkles }, diff --git a/src/web/components/SEO.tsx b/src/web/components/layout/SEO.tsx similarity index 100% rename from src/web/components/SEO.tsx rename to src/web/components/layout/SEO.tsx diff --git a/src/web/components/AntiSpamSettings.tsx b/src/web/components/settings/AntiSpamSettings.tsx similarity index 96% rename from src/web/components/AntiSpamSettings.tsx rename to src/web/components/settings/AntiSpamSettings.tsx index 2d39616..4161494 100644 --- a/src/web/components/AntiSpamSettings.tsx +++ b/src/web/components/settings/AntiSpamSettings.tsx @@ -8,20 +8,20 @@ import { Zap, AlertTriangle } from "lucide-react"; -import { useAuth } from "../components/AuthProvider"; +import { useAuth } from "../core/AuthProvider"; import { toast } from "sonner"; -import { Button } from "./ui/button"; -import { Input } from "./ui/input"; -import { Label } from "./ui/label"; -import { Switch } from "./ui/switch"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { Label } from "../ui/label"; +import { Switch } from "../ui/switch"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; interface Channel { id: string; name: string; } -import { API_URL } from "../lib/api"; +import { API_URL } from "../../lib/api"; export default function AntiSpamSettings({ guildId }: { guildId: string }) { const { token } = useAuth(); diff --git a/src/web/components/settings/ApplicationSettings.tsx b/src/web/components/settings/ApplicationSettings.tsx new file mode 100644 index 0000000..d8ae5a2 --- /dev/null +++ b/src/web/components/settings/ApplicationSettings.tsx @@ -0,0 +1,169 @@ +import React, { useState, useEffect } from "react"; +import { motion, Reorder } from "framer-motion"; +import { + ClipboardList, + Plus, + Trash2, + Save, + GripVertical, + Send, + MessageSquare, + HelpCircle +} from "lucide-react"; +import { useAuth } from "../core/AuthProvider"; +import { toast } from "sonner"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { Label } from "../ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { API_URL } from "../../lib/api"; + +export default function ApplicationSettings({ guildId }: { guildId: string }) { + const { token } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [questions, setQuestions] = useState([]); + const [newQuestion, setNewQuestion] = useState(""); + + const fetchData = async () => { + if (!token || !guildId) return; + try { + const res = await fetch(`${API_URL}/management/${guildId}/applications`, { + headers: { "Authorization": `Bearer ${token}` } + }); + if (res.ok) { + const data = await res.json(); + setQuestions(data.data || []); + } + } catch (e) { + console.error("Fetch error", e); + } + }; + + useEffect(() => { + fetchData(); + }, [token, guildId]); + + const handleAdd = () => { + if (!newQuestion) return; + if (questions.length >= 10) { + toast.error("Maximal 10 Fragen erlaubt!"); + return; + } + setQuestions([...questions, newQuestion]); + setNewQuestion(""); + }; + + const handleRemove = (index: number) => { + const n = [...questions]; + n.splice(index, 1); + setQuestions(n); + }; + + const handleSave = async () => { + setIsLoading(true); + try { + const res = await fetch(`${API_URL}/management/${guildId}/applications`, { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ questions }) + }); + + if (res.ok) { + toast.success("Bewerbungsfragen gespeichert!"); + } + } catch (e) { + toast.error("Fehler beim Speichern."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + + + + Application System + + Automatisiere Team-Bewerbungen รผber ein interaktives DM-Interface. + + + + {/* Intro / Setup */} +
+
+ +
+
+

Bewerbungs-Post erstellen

+

Poste eine Nachricht mit einem Button in deinen Kanal, um Bewerbungen zu starten.

+
+ +
+ + {/* Question Editor */} +
+
+

+ Interview-Fragen ({questions.length}/10) +

+
+ +
+ {questions.map((q, idx) => ( + +
+ {idx + 1} +
+
{q}
+ +
+ ))} + +
+ setNewQuestion(e.target.value)} + placeholder="Neue Frage hinzufรผgen..." + className="bg-black/20 border-white/10 h-12 rounded-xl flex-1" + onKeyDown={(e) => e.key === 'Enter' && handleAdd()} + /> + +
+
+ +
+ +
+
+
+
+
+ ); +} diff --git a/src/web/components/AutoDeleteSettings.tsx b/src/web/components/settings/AutoDeleteSettings.tsx similarity index 96% rename from src/web/components/AutoDeleteSettings.tsx rename to src/web/components/settings/AutoDeleteSettings.tsx index a475a78..bc0fdbf 100644 --- a/src/web/components/AutoDeleteSettings.tsx +++ b/src/web/components/settings/AutoDeleteSettings.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from "react"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "./ui/card"; -import { Label } from "./ui/label"; -import { Switch } from "./ui/switch"; -import { Input } from "./ui/input"; -import { Button } from "./ui/button"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../ui/card"; +import { Label } from "../ui/label"; +import { Switch } from "../ui/switch"; +import { Input } from "../ui/input"; +import { Button } from "../ui/button"; import { Trash2, Save, @@ -14,7 +14,7 @@ import { Search } from "lucide-react"; import { toast } from "sonner"; -import { SearchableSelect } from "./ui/SearchableSelect"; +import { SearchableSelect } from "../ui/SearchableSelect"; interface AutoDeleteSettingsProps { guildId: string; @@ -26,7 +26,7 @@ interface ChannelConfig { delay: number; } -import { API_URL } from "../lib/api"; +import { API_URL } from "../../lib/api"; export default function AutoDeleteSettings({ guildId, channels }: AutoDeleteSettingsProps) { const [loading, setLoading] = useState(true); diff --git a/src/web/components/settings/AutoResponderSettings.tsx b/src/web/components/settings/AutoResponderSettings.tsx new file mode 100644 index 0000000..c0543cc --- /dev/null +++ b/src/web/components/settings/AutoResponderSettings.tsx @@ -0,0 +1,219 @@ +import React, { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + MessageSquare, + Plus, + Trash2, + Zap, + Save, + Search, + Brain +} from "lucide-react"; +import { useAuth } from "../core/AuthProvider"; +import { toast } from "sonner"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { Label } from "../ui/label"; +import { Switch } from "../ui/switch"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { API_URL } from "../../lib/api"; + +interface AutoResponse { + id: number; + keyword: string; + response: string; + match_type: string; +} + +export default function AutoResponderSettings({ guildId }: { guildId: string }) { + const { token } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [responses, setResponses] = useState([]); + + // New Response Form + const [newKeyword, setNewKeyword] = useState(""); + const [newResponse, setNewResponse] = useState(""); + const [matchType, setMatchType] = useState("partial"); + + const fetchData = async () => { + if (!token || !guildId) return; + try { + const res = await fetch(`${API_URL}/management/${guildId}/autoresponder`, { + headers: { "Authorization": `Bearer ${token}` } + }); + if (res.ok) { + const data = await res.json(); + setResponses(data.data || []); + } + } catch (e) { + console.error("Fetch error", e); + } + }; + + useEffect(() => { + fetchData(); + }, [token, guildId]); + + const handleAdd = async () => { + if (!newKeyword || !newResponse) { + toast.error("Bitte Keyword und Antwort ausfรผllen!"); + return; + } + + setIsLoading(true); + try { + const res = await fetch(`${API_URL}/management/${guildId}/autoresponder`, { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + keyword: newKeyword, + response: newResponse, + match_type: matchType + }) + }); + + if (res.ok) { + toast.success("Keyword erfolgreich hinzugefรผgt!"); + setNewKeyword(""); + setNewResponse(""); + fetchData(); + } + } catch (e) { + toast.error("Fehler beim Hinzufรผgen."); + } finally { + setIsLoading(false); + } + }; + + const handleDelete = async (id: number) => { + try { + const res = await fetch(`${API_URL}/management/${guildId}/autoresponder/${id}`, { + method: "DELETE", + headers: { "Authorization": `Bearer ${token}` } + }); + if (res.ok) { + toast.success("Keyword gelรถscht."); + fetchData(); + } + } catch (e) { + toast.error("Fehler beim Lรถschen."); + } + }; + + return ( +
+ + +
+
+ + + Smart Auto-Responder + + Lass den Bot automatisch auf hรคufige Fragen antworten. +
+
+
+ + + {/* Add New Keyword */} +
+

+ Neues Keyword erstellen +

+ +
+
+ + setNewKeyword(e.target.value)} + className="bg-black/20 border-white/10 h-12 rounded-xl" + placeholder="z.B. IP, Regeln, Hilfe" + /> +
+
+ + +
+
+ +
+ +