diff --git a/README.md b/README.md index b935989..8e8e4e4 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,6 @@ Verbinde deine Community mit Servern weltweit über unser integriertes Globalcha - **Temporary Voice**: User-gesteuerte Sprachkanäle mit voller Permission-Control. - **Auto-Cleanup**: Intelligente Löschung inaktiver Kanäle spart Ressourcen. - **Live-Stats**: Echtzeit-Analysen über Voice-Aktivität und User-Engagement. -- **Toolbox**: Integrationen wie Google-Search, Wetterdaten und Wikipedia.
diff --git a/docs/source/dev_guide/getting_started/fast_setting_up.rst b/docs/source/dev_guide/getting_started/fast_setting_up.rst new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/dev_guide/index.rst b/docs/source/dev_guide/index.rst new file mode 100644 index 0000000..ca285ed --- /dev/null +++ b/docs/source/dev_guide/index.rst @@ -0,0 +1,15 @@ +Developer Guide +====================== + +Welcome to the ManagerX Developer Guide! This guide provides everything you need to understand the inner workings of ManagerX, contribute to its development, and extend its functionality. + +Whether you're looking to set up a local development environment, explore the architecture, build new features, or deploy your own instance — you'll find all the information here. + +.. toctree:: + :maxdepth: 2 + :caption: Contents + + Quick Start + architecture/index + contributing/index + deployment/index \ No newline at end of file diff --git a/main.py b/main.py index 5cac787..dd2dc30 100644 --- a/main.py +++ b/main.py @@ -113,7 +113,7 @@ async def start_webserver(): # Datenbank initialisieren db_manager = DatabaseManager() if not db_manager.initialize(bot): - logger.warning("DATABASE", "Bot läuft ohne Datenbank weiter...") + logger.warn("DATABASE", "Bot läuft ohne Datenbank weiter...") else: logger.success("DATABASE", "Datenbank erfolgreich initialisiert") diff --git a/package-lock.json b/package-lock.json index 79ce26d..22f0e08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,25 +36,25 @@ "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", - "@tanstack/react-query": "5.90.19", + "@tanstack/react-query": "5.90.21", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", "date-fns": "4.1.0", "embla-carousel-react": "8.6.0", - "framer-motion": "12.27.0", + "framer-motion": "12.34.3", "input-otp": "1.4.2", - "lucide-react": "0.462.0", - "next-themes": "0.3.0", + "lucide-react": "0.575.0", + "next-themes": "0.4.6", "react": "19.2.4", - "react-day-picker": "9.13.1", + "react-day-picker": "9.13.2", "react-dom": "19.2.4", "react-hook-form": "7.71.1", - "react-resizable-panels": "4.6.2", + "react-resizable-panels": "4.6.4", "react-router-dom": "7.13.0", "recharts": "3.7.0", "sonner": "2.0.7", - "tailwind-merge": "3.4.0", + "tailwind-merge": "3.5.0", "tailwindcss-animate": "1.0.7", "vaul": "1.1.2", "zod": "4.3.6" @@ -63,21 +63,21 @@ "@eslint/js": "10.0.1", "@tailwindcss/typography": "0.5.19", "@testing-library/jest-dom": "6.9.1", - "@testing-library/react": "16.3.1", - "@types/node": "24.10.11", - "@types/react": "19.2.13", + "@testing-library/react": "16.3.2", + "@types/node": "24.10.13", + "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@vitejs/plugin-react-swc": "4.2.3", - "autoprefixer": "10.4.23", - "eslint": "10.0.0", + "autoprefixer": "10.4.24", + "eslint": "10.0.1", "eslint-plugin-react-hooks": "7.0.1", - "eslint-plugin-react-refresh": "0.4.26", + "eslint-plugin-react-refresh": "0.5.0", "globals": "17.3.0", - "jsdom": "28.0.0", + "jsdom": "28.1.0", "postcss": "8.5.6", - "tailwindcss": "4.1.18", + "tailwindcss": "4.2.0", "typescript": "5.9.3", - "typescript-eslint": "8.53.0", + "typescript-eslint": "8.56.0", "vite": "7.3.1", "vitest": "4.0.18" } @@ -121,9 +121,9 @@ } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "6.7.8", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.8.tgz", - "integrity": "sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", "dev": true, "license": "MIT", "dependencies": { @@ -131,13 +131,13 @@ "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.5" + "lru-cache": "^11.2.6" } }, "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -421,6 +421,19 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.1.tgz", @@ -1044,15 +1057,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.1.tgz", - "integrity": "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", + "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.1", + "@eslint/object-schema": "^3.0.2", "debug": "^4.3.1", - "minimatch": "^10.1.1" + "minimatch": "^10.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -1106,9 +1119,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.1.tgz", - "integrity": "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", + "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1249,29 +1262,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3610,9 +3600,9 @@ } }, "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==", + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", "license": "MIT", "funding": { "type": "github", @@ -3620,12 +3610,12 @@ } }, "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==", + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.90.19" + "@tanstack/query-core": "5.90.20" }, "funding": { "type": "github", @@ -3656,9 +3646,9 @@ } }, "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==", + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", "dev": true, "license": "MIT", "dependencies": { @@ -3786,9 +3776,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.11.tgz", - "integrity": "sha512-/Af7O8r1frCVgOz0I62jWUtMohJ0/ZQU/ZoketltOJPZpnb17yoNc9BSoVuV9qlaIXJiPNOpsfq4ByFajSArNQ==", + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "dev": true, "license": "MIT", "dependencies": { @@ -3796,9 +3786,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", - "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", "dependencies": { @@ -3822,17 +3812,17 @@ "license": "MIT" }, "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==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", "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", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -3845,8 +3835,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.53.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -3861,16 +3851,16 @@ } }, "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==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "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", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "engines": { @@ -3881,19 +3871,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.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==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.53.0", - "@typescript-eslint/types": "^8.53.0", + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", "debug": "^4.4.3" }, "engines": { @@ -3908,14 +3898,14 @@ } }, "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==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3926,9 +3916,9 @@ } }, "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==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", "dev": true, "license": "MIT", "engines": { @@ -3943,15 +3933,15 @@ } }, "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==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/typescript-estree": "8.53.0", - "@typescript-eslint/utils": "8.53.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -3963,14 +3953,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.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==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "dev": true, "license": "MIT", "engines": { @@ -3982,16 +3972,16 @@ } }, "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==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", "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", + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", @@ -4036,16 +4026,16 @@ } }, "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==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", "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" + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4055,19 +4045,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.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==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4206,9 +4196,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -4288,9 +4278,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", "dev": true, "funding": [ { @@ -4309,7 +4299,7 @@ "license": "MIT", "dependencies": { "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", + "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -4351,6 +4341,29 @@ "require-from-string": "^2.0.2" } }, + "node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -4386,9 +4399,9 @@ } }, "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==", + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", "dev": true, "funding": [ { @@ -4523,16 +4536,16 @@ } }, "node_modules/cssstyle": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", - "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.0.1.tgz", + "integrity": "sha512-IoJs7La+oFp/AB033wBStxNOJt4+9hHMxsXUPANcoXL2b3W4DZKghlJ2cI/eyeRZIQ9ysvYEorVhjrcYctWbog==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^4.1.1", - "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "@asamuzakjp/css-color": "^4.1.2", + "@csstools/css-syntax-patches-for-csstree": "^1.0.26", "css-tree": "^3.1.0", - "lru-cache": "^11.2.4" + "lru-cache": "^11.2.5" }, "engines": { "node": ">=20" @@ -4898,15 +4911,15 @@ } }, "node_modules/eslint": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.0.tgz", - "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.1.tgz", + "integrity": "sha512-20MV9SUdeN6Jd84xESsKhRly+/vxI+hwvpBMA93s+9dAcjdCuCojn4IqUGS3lvVaqjVYGYHSRMCpeFtF2rQYxQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.0", + "@eslint/config-array": "^0.23.2", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.0", "@eslint/plugin-kit": "^0.6.0", @@ -4918,9 +4931,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.0", - "eslint-visitor-keys": "^5.0.0", - "espree": "^11.1.0", + "eslint-scope": "^9.1.1", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4931,7 +4944,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.1.1", + "minimatch": "^10.2.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4974,19 +4987,19 @@ } }, "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==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.0.tgz", + "integrity": "sha512-ZYvmh7VfVgqR/7wR71I3Zl6hK/C5CcxdWYKZSpHawS5JCNgE4efhQWg/+/WPpgGAp9Ngp/rRZYyaIwmPQBq/lA==", "dev": true, "license": "MIT", "peerDependencies": { - "eslint": ">=8.40" + "eslint": ">=9" } }, "node_modules/eslint-scope": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz", - "integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", + "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5003,22 +5016,9 @@ } }, "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/eslint/node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5029,15 +5029,15 @@ } }, "node_modules/espree": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz", - "integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", + "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.0" + "eslint-visitor-keys": "^5.0.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -5046,19 +5046,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -5236,13 +5223,13 @@ } }, "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==", + "version": "12.34.3", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.3.tgz", + "integrity": "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q==", "license": "MIT", "dependencies": { - "motion-dom": "^12.27.0", - "motion-utils": "^12.24.10", + "motion-dom": "^12.34.3", + "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { @@ -5484,16 +5471,17 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.0.0.tgz", - "integrity": "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==", + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, "license": "MIT", "dependencies": { "@acemir/cssom": "^0.9.31", - "@asamuzakjp/dom-selector": "^6.7.6", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", "@exodus/bytes": "^1.11.0", - "cssstyle": "^5.3.7", + "cssstyle": "^6.0.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", @@ -5504,7 +5492,7 @@ "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", - "undici": "^7.20.0", + "undici": "^7.21.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", @@ -5621,12 +5609,12 @@ } }, "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==", + "version": "0.575.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz", + "integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==", "license": "ISC", "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/magic-string": { @@ -5657,34 +5645,34 @@ } }, "node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "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==", + "version": "12.34.3", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.3.tgz", + "integrity": "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==", "license": "MIT", "dependencies": { - "motion-utils": "^12.24.10" + "motion-utils": "^12.29.2" } }, "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==", + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", "license": "MIT" }, "node_modules/ms": { @@ -5721,13 +5709,13 @@ "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==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", "license": "MIT", "peerDependencies": { - "react": "^16.8 || ^17 || ^18", - "react-dom": "^16.8 || ^17 || ^18" + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "node_modules/node-releases": { @@ -5938,9 +5926,9 @@ } }, "node_modules/react-day-picker": { - "version": "9.13.1", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.1.tgz", - "integrity": "sha512-9nx2lBBJ0VZw5jJekId3DishwnJLiqY1Me1JvCrIyqbWwcflBTVaEkiK+w1bre5oMNWYo722eu+8UAMXWMqktw==", + "version": "9.13.2", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.2.tgz", + "integrity": "sha512-IMPiXfXVIAuR5Yk58DDPBC8QKClrhdXV+Tr/alBrwrHUw0qDDYB1m5zPNuTnnPIr/gmJ4ChMxmtqPdxm8+R4Eg==", "license": "MIT", "dependencies": { "@date-fns/tz": "^1.4.1", @@ -6057,9 +6045,9 @@ } }, "node_modules/react-resizable-panels": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.6.2.tgz", - "integrity": "sha512-d6hyD6s7ewNAI+oINrZznR/08GUyAszrowXouUDztePEn/tQ2z/LEI2qRvrizYBe3TpgBi0cCjc10pXTTOc4jw==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.6.4.tgz", + "integrity": "sha512-E7Szs1xyaMZ7xOI2gG4TECNz4r/gmpV1AsXyZRnER6OQnfFf9uclFmrHHZR3h/iF8vQS+nQ1LKyZv9bzwGxPSg==", "license": "MIT", "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -6266,9 +6254,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -6369,9 +6357,9 @@ "license": "MIT" }, "node_modules/tailwind-merge": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", - "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", "license": "MIT", "funding": { "type": "github", @@ -6379,9 +6367,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz", + "integrity": "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==", "dev": true, "license": "MIT" }, @@ -6537,16 +6525,16 @@ } }, "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==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", "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" + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6556,7 +6544,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, diff --git a/package.json b/package.json index 6c7662d..27f6633 100644 --- a/package.json +++ b/package.json @@ -41,25 +41,25 @@ "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", - "@tanstack/react-query": "5.90.19", + "@tanstack/react-query": "5.90.21", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", "date-fns": "4.1.0", "embla-carousel-react": "8.6.0", - "framer-motion": "12.27.0", + "framer-motion": "12.34.3", "input-otp": "1.4.2", - "lucide-react": "0.462.0", - "next-themes": "0.3.0", + "lucide-react": "0.575.0", + "next-themes": "0.4.6", "react": "19.2.4", - "react-day-picker": "9.13.1", + "react-day-picker": "9.13.2", "react-dom": "19.2.4", "react-hook-form": "7.71.1", - "react-resizable-panels": "4.6.2", + "react-resizable-panels": "4.6.4", "react-router-dom": "7.13.0", "recharts": "3.7.0", "sonner": "2.0.7", - "tailwind-merge": "3.4.0", + "tailwind-merge": "3.5.0", "tailwindcss-animate": "1.0.7", "vaul": "1.1.2", "zod": "4.3.6" @@ -68,21 +68,21 @@ "@eslint/js": "10.0.1", "@tailwindcss/typography": "0.5.19", "@testing-library/jest-dom": "6.9.1", - "@testing-library/react": "16.3.1", - "@types/node": "24.10.11", - "@types/react": "19.2.13", + "@testing-library/react": "16.3.2", + "@types/node": "24.10.13", + "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@vitejs/plugin-react-swc": "4.2.3", - "autoprefixer": "10.4.23", - "eslint": "10.0.0", + "autoprefixer": "10.4.24", + "eslint": "10.0.1", "eslint-plugin-react-hooks": "7.0.1", - "eslint-plugin-react-refresh": "0.4.26", + "eslint-plugin-react-refresh": "0.5.0", "globals": "17.3.0", - "jsdom": "28.0.0", + "jsdom": "28.1.0", "postcss": "8.5.6", - "tailwindcss": "4.1.18", + "tailwindcss": "4.2.0", "typescript": "5.9.3", - "typescript-eslint": "8.53.0", + "typescript-eslint": "8.56.0", "vite": "7.3.1", "vitest": "4.0.18" } diff --git a/pyproject.toml b/pyproject.toml index ccdd299..3ac4f30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ManagerX" -version = "2.2026.01.11" +version = {attr = "src.bot.core.constants.VERSION_NUMBER"} description = "A powerful Discord bot for server management and fun." readme = "README.md" requires-python = ">=3.8" diff --git a/requirements/bot_req.txt b/requirements/bot_req.txt deleted file mode 100644 index f9914b6..0000000 --- a/requirements/bot_req.txt +++ /dev/null @@ -1,14 +0,0 @@ -ezcord==0.7.4 -py-cord==2.7.0 -aiosqlite==0.22.1 -aiohttp==3.13.3 -aiocache==0.12.3 -propcache==0.4.1 -requests==2.32.5 -wikipedia==1.4.0 -beautifulsoup4==4.14.3 -soupsieve==2.8.3 -yarl==1.22.0 -frozenlist==1.8.0 -h11==0.16.0 -multidict==6.7.1 \ No newline at end of file diff --git a/requirements/dev_req.txt b/requirements/dev_req.txt deleted file mode 100644 index f41caf2..0000000 --- a/requirements/dev_req.txt +++ /dev/null @@ -1,19 +0,0 @@ -python-dotenv==1.2.1 -click==8.3.1 -colorama==0.4.6 -typing_extensions==4.15.0 -typing-inspection==0.4.2 -attrs==25.4.0 -annotated-types==0.7.0 -anyio==4.12.1 -certifi==2026.1.4 -charset-normalizer==3.4.4 -idna==3.11 -urllib3==2.6.3 -Jinja2==3.1.6 -MarkupSafe==3.0.3 -starlette==0.52.1 -FastAPI -uvicorn -SimpleColoredLogs -timedelta==2020.12.3 \ No newline at end of file diff --git a/src/api/dashboard/routes.py b/src/api/dashboard/routes.py index 49eee8e..a4f3b5a 100644 --- a/src/api/dashboard/routes.py +++ b/src/api/dashboard/routes.py @@ -61,6 +61,13 @@ async def get_stats(request: Request): return server_status except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + +@router_public.get("/version") +async def get_version(request: Request): + return { + "pypi_version": "1.2026.2.26", + "bot_version": "v2.0.0-open-beta" + } API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False) @@ -85,8 +92,9 @@ async def get_api_key(api_key_header: str = Security(API_KEY_HEADER)): return api_key_header router = APIRouter( - prefix="/v1/managerx/dashboard", + prefix="/dashboard", tags=["dashboard"], dependencies=[Security(get_api_key)] ) router.include_router(router_public) + diff --git a/src/bot/cogs/bot/admin.py b/src/bot/cogs/bot/admin.py index be68a64..4c05b37 100644 --- a/src/bot/cogs/bot/admin.py +++ b/src/bot/cogs/bot/admin.py @@ -1,7 +1,7 @@ import discord -from discord import SlashCommandGroup +from discord import SlashCommandGroup, Option import ezcord -from discord.ui import Container, View, Button +from discord.ui import Container, View, Button, Modal, InputText import sys import os import psutil @@ -11,136 +11,363 @@ from pathlib import Path import math import subprocess +import json +from typing import Optional, List +import time ALLOWED_IDS = [1427994077332373554] -class ServerListView(View): - def __init__(self, guilds, page=0, per_page=20): - super().__init__(timeout=180) # 3 Minuten Timeout - self.guilds = guilds +# Audit Log Storage +AUDIT_LOG_FILE = Path("data/admin_audit.json") +BLACKLIST_FILE = Path("data/blacklist.json") + +class ConfirmView(View): + """Bestätigungsdialog für kritische Aktionen""" + def __init__(self, timeout=30): + super().__init__(timeout=timeout) + self.value = None + + @discord.ui.button(label="✅ Bestätigen", style=discord.ButtonStyle.danger) + async def confirm(self, button: discord.ui.Button, interaction: discord.Interaction): + self.value = True + self.stop() + await interaction.response.defer() + + @discord.ui.button(label="❌ Abbrechen", style=discord.ButtonStyle.secondary) + async def cancel(self, button: discord.ui.Button, interaction: discord.Interaction): + self.value = False + self.stop() + await interaction.response.defer() + + +class ServerListView: + """Pagination für Server-Liste mit DesignerView""" + def __init__(self, guilds, bot, page=0, per_page=20, sort_by="members", filter_text=""): + self.all_guilds = guilds + self.bot = bot self.page = page self.per_page = per_page - self.max_pages = math.ceil(len(guilds) / per_page) + self.sort_by = sort_by + self.filter_text = filter_text.lower() - self.update_buttons() - - def update_buttons(self): - # Entferne alle Buttons - self.clear_items() + # Filtern + if self.filter_text: + self.guilds = [g for g in guilds if self.filter_text in g.name.lower()] + else: + self.guilds = guilds + + # Sortieren + if sort_by == "members": + self.guilds = sorted(self.guilds, key=lambda g: g.member_count, reverse=True) + elif sort_by == "name": + self.guilds = sorted(self.guilds, key=lambda g: g.name.lower()) + elif sort_by == "joined": + self.guilds = sorted(self.guilds, key=lambda g: g.me.joined_at, reverse=True) + + self.max_pages = math.ceil(len(self.guilds) / per_page) if self.guilds else 1 + + def get_designer_view(self): + """Erstellt eine komplette DesignerView mit Container und Buttons""" + start = self.page * self.per_page + end = start + self.per_page + page_guilds = self.guilds[start:end] if self.guilds else [] + + guilds_list = [] + for i, guild in enumerate(page_guilds): + name = guild.name[:35] + "..." if len(guild.name) > 35 else guild.name + members = f"{guild.member_count:,}".replace(",", ".") + + boost_emoji = "" + if guild.premium_tier == 3: + boost_emoji = "💎" + elif guild.premium_tier == 2: + boost_emoji = "💠" + elif guild.premium_tier == 1: + boost_emoji = "🔷" + + verified_emoji = "✅" if guild.verification_level == discord.VerificationLevel.high else "" + partner_emoji = "🤝" if "PARTNERED" in guild.features else "" + status_icons = f"{boost_emoji}{verified_emoji}{partner_emoji}".strip() + + guilds_list.append(f"`{i + start + 1:3}.` **{name}** {status_icons}") + guilds_list.append(f" ID: `{guild.id}` │ 👥 {members}") + + guilds_text = "\n".join(guilds_list) if guilds_list else "*Keine Server gefunden*" + + filter_info = f" (Filter: `{self.filter_text}`)" if self.filter_text else "" + sort_name = {"members": "Mitglieder", "name": "Name", "joined": "Beitritt"}[self.sort_by] + + container = Container(color=discord.Color.blue()) + container.add_text(f"# 🌐 Server-Liste{filter_info}") + container.add_separator() + container.add_text(guilds_text) + container.add_separator() + container.add_text(f"📊 **Seite {self.page + 1}/{self.max_pages}** │ Zeige {start + 1}-{min(end, len(self.guilds))} von {len(self.guilds):,} Servern") + container.add_text(f"🔀 **Sortierung:** {sort_name} │ 💎 = Level 3, 💠 = Level 2, 🔷 = Level 1") + + # Erstelle DesignerView + view = discord.ui.DesignerView(container, timeout=180) + + # Navigation Buttons (erste Reihe) - ActionRow + nav_row = discord.ui.ActionRow() - # Erste Seite Button first_button = Button( label="⏮️", style=discord.ButtonStyle.gray, - disabled=(self.page == 0) + disabled=(self.page == 0), + custom_id=f"first_{self.page}" ) - first_button.callback = self.first_page - self.add_item(first_button) + first_button.callback = self.make_callback("first") + nav_row.add_item(first_button) - # Vorherige Seite Button prev_button = Button( label="◀️", style=discord.ButtonStyle.primary, - disabled=(self.page == 0) + disabled=(self.page == 0), + custom_id=f"prev_{self.page}" ) - prev_button.callback = self.previous_page - self.add_item(prev_button) + prev_button.callback = self.make_callback("prev") + nav_row.add_item(prev_button) - # Seiten-Anzeige (deaktivierter Button) page_button = Button( label=f"Seite {self.page + 1}/{self.max_pages}", style=discord.ButtonStyle.gray, - disabled=True + disabled=True, + custom_id=f"page_{self.page}" ) - self.add_item(page_button) + nav_row.add_item(page_button) - # Nächste Seite Button next_button = Button( label="▶️", style=discord.ButtonStyle.primary, - disabled=(self.page >= self.max_pages - 1) + disabled=(self.page >= self.max_pages - 1), + custom_id=f"next_{self.page}" ) - next_button.callback = self.next_page - self.add_item(next_button) + next_button.callback = self.make_callback("next") + nav_row.add_item(next_button) - # Letzte Seite Button last_button = Button( label="⏭️", style=discord.ButtonStyle.gray, - disabled=(self.page >= self.max_pages - 1) + disabled=(self.page >= self.max_pages - 1), + custom_id=f"last_{self.page}" ) - last_button.callback = self.last_page - self.add_item(last_button) - - def get_page_container(self): - start = self.page * self.per_page - end = start + self.per_page - page_guilds = self.guilds[start:end] + last_button.callback = self.make_callback("last") + nav_row.add_item(last_button) - guilds_text = "\n".join([ - f"**{i + start + 1}.** {guild.name}\n`ID: {guild.id}` • {guild.member_count:,} Mitglieder" - for i, guild in enumerate(page_guilds) - ]) + view.add_item(nav_row) - container = Container(color=discord.Color.blue()) - container.add_text(f"# 🌐 Server-Liste (Seite {self.page + 1}/{self.max_pages})") - container.add_separator() - container.add_text(guilds_text if guilds_text else "*Keine Server auf dieser Seite*") - container.add_separator() - container.add_text(f"**Gesamt:** {len(self.guilds):,} Server • **Zeige:** {start + 1}-{min(end, len(self.guilds))}") + # Sortierung Buttons (zweite Reihe) - ActionRow + sort_row = discord.ui.ActionRow() - return container - - async def first_page(self, interaction: discord.Interaction): - self.page = 0 - self.update_buttons() - await interaction.response.edit_message( - view=discord.ui.DesignerView(self.get_page_container(), view=self, timeout=180) + sort_members = Button( + label="👥 Mitglieder", + style=discord.ButtonStyle.success if self.sort_by == "members" else discord.ButtonStyle.secondary, + custom_id=f"sort_members_{self.page}" ) - - async def previous_page(self, interaction: discord.Interaction): - self.page = max(0, self.page - 1) - self.update_buttons() - await interaction.response.edit_message( - view=discord.ui.DesignerView(self.get_page_container(), view=self, timeout=180) + sort_members.callback = self.make_callback("sort_members") + sort_row.add_item(sort_members) + + sort_name_btn = Button( + label="📝 Name", + style=discord.ButtonStyle.success if self.sort_by == "name" else discord.ButtonStyle.secondary, + custom_id=f"sort_name_{self.page}" ) - - async def next_page(self, interaction: discord.Interaction): - self.page = min(self.max_pages - 1, self.page + 1) - self.update_buttons() - await interaction.response.edit_message( - view=discord.ui.DesignerView(self.get_page_container(), view=self, timeout=180) + sort_name_btn.callback = self.make_callback("sort_name") + sort_row.add_item(sort_name_btn) + + sort_joined = Button( + label="📅 Beitritt", + style=discord.ButtonStyle.success if self.sort_by == "joined" else discord.ButtonStyle.secondary, + custom_id=f"sort_joined_{self.page}" ) - - async def last_page(self, interaction: discord.Interaction): - self.page = self.max_pages - 1 - self.update_buttons() - await interaction.response.edit_message( - view=discord.ui.DesignerView(self.get_page_container(), view=self, timeout=180) + sort_joined.callback = self.make_callback("sort_joined") + sort_row.add_item(sort_joined) + + # Export Button + export_button = Button( + label="💾 Export", + style=discord.ButtonStyle.primary, + custom_id=f"export_{self.page}" ) - - async def on_timeout(self): - # Deaktiviere alle Buttons nach Timeout - for item in self.children: - item.disabled = True + export_button.callback = self.make_callback("export") + sort_row.add_item(export_button) + + view.add_item(sort_row) + + return view + + def make_callback(self, action): + """Erstellt eine Callback-Funktion für Button-Actions""" + async def callback(interaction: discord.Interaction): + # Navigation + if action == "first": + self.page = 0 + elif action == "prev": + self.page = max(0, self.page - 1) + elif action == "next": + self.page = min(self.max_pages - 1, self.page + 1) + elif action == "last": + self.page = self.max_pages - 1 + + # Sortierung + elif action == "sort_members": + self.sort_by = "members" + self.page = 0 + self.guilds = sorted(self.guilds, key=lambda g: g.member_count, reverse=True) + elif action == "sort_name": + self.sort_by = "name" + self.page = 0 + self.guilds = sorted(self.guilds, key=lambda g: g.name.lower()) + elif action == "sort_joined": + self.sort_by = "joined" + self.page = 0 + self.guilds = sorted(self.guilds, key=lambda g: g.me.joined_at, reverse=True) + + # Export + elif action == "export": + await self.export_data(interaction) + return + + # Update View + new_view = self.get_designer_view() + await interaction.response.edit_message(view=new_view) + + return callback + + async def export_data(self, interaction: discord.Interaction): + """Exportiert Server-Daten als JSON""" + await interaction.response.defer() + + export_data = [] + for guild in self.guilds: + export_data.append({ + "name": guild.name, + "id": str(guild.id), + "members": guild.member_count, + "boost_level": guild.premium_tier, + "boosts": guild.premium_subscription_count, + "joined": guild.me.joined_at.isoformat() if guild.me.joined_at else None, + "features": guild.features + }) + + filename = f"server_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + filepath = Path("data/exports") / filename + filepath.parent.mkdir(parents=True, exist_ok=True) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(export_data, f, indent=2, ensure_ascii=False) + + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Export erfolgreich!") + container.add_text(f"**Datei:** `{filename}`") + container.add_text(f"**Anzahl:** {len(export_data)} Server") + + await interaction.followup.send( + view=discord.ui.DesignerView(container, timeout=0), + ephemeral=True + ) + class admin(ezcord.Cog, hidden=True): def __init__(self, bot): self.bot = bot self.start_time = datetime.now() self.cogs_path = Path("src/bot/cogs") + self.data_path = Path("data") + self.data_path.mkdir(exist_ok=True) + + # Lade Blacklist + self.blacklist = self.load_blacklist() + + # Command Counter für Rate Limiting + self.command_usage = {} admin = SlashCommandGroup("admin", "Admin commands") bot = admin.create_subgroup("bot", "Bot commands") system = admin.create_subgroup("system", "System commands") server = admin.create_subgroup("server", "Server management commands") + user = admin.create_subgroup("user", "User management commands") + logs = admin.create_subgroup("logs", "Log commands") async def cog_check(self, ctx): + # Sicherheitscheck: IMMER prüfen, auch für Gruppen/Subgruppen if ctx.author.id not in ALLOWED_IDS: - await ctx.respond("Zugriff verweigert: Deine ID ist nicht autorisiert.", ephemeral=True) + await ctx.respond("❌ Zugriff verweigert: Deine ID ist nicht autorisiert.", ephemeral=True) return False + + # Nur für Leaf-Commands loggen und Rate-Limiten (nicht für Gruppen/Subgruppen) + # Das verhindert, dass z.B. "admin logs view" 3x geloggt wird + is_leaf_command = not hasattr(ctx.command, 'subcommands') or not ctx.command.subcommands + + if is_leaf_command: + # Rate Limiting Check + user_id = ctx.author.id + current_time = time.time() + + if user_id in self.command_usage: + last_time, count = self.command_usage[user_id] + if current_time - last_time < 60: # 1 Minute + if count >= 30: # Max 30 Commands pro Minute + await ctx.respond("⚠️ Rate Limit erreicht. Bitte warte einen Moment.", ephemeral=True) + return False + self.command_usage[user_id] = (last_time, count + 1) + else: + self.command_usage[user_id] = (current_time, 1) + else: + self.command_usage[user_id] = (current_time, 1) + + # Audit Log - nur einmal pro echtem Command + self.log_command(ctx) + return True + def log_command(self, ctx): + """Loggt Admin-Commands""" + AUDIT_LOG_FILE.parent.mkdir(parents=True, exist_ok=True) + + log_entry = { + "timestamp": datetime.now().isoformat(), + "user_id": str(ctx.author.id), + "user_name": str(ctx.author), + "command": ctx.command.qualified_name if ctx.command else "unknown", + "guild_id": str(ctx.guild.id) if ctx.guild else None, + "guild_name": ctx.guild.name if ctx.guild else None + } + + logs = [] + if AUDIT_LOG_FILE.exists(): + with open(AUDIT_LOG_FILE, 'r', encoding='utf-8') as f: + try: + logs = json.load(f) + except: + logs = [] + + logs.append(log_entry) + + # Behalte nur die letzten 1000 Einträge + if len(logs) > 1000: + logs = logs[-1000:] + + with open(AUDIT_LOG_FILE, 'w', encoding='utf-8') as f: + json.dump(logs, f, indent=2, ensure_ascii=False) + + def load_blacklist(self): + """Lädt die Blacklist""" + if BLACKLIST_FILE.exists(): + with open(BLACKLIST_FILE, 'r', encoding='utf-8') as f: + try: + return json.load(f) + except: + return {"guilds": [], "users": []} + return {"guilds": [], "users": []} + + def save_blacklist(self): + """Speichert die Blacklist""" + BLACKLIST_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(BLACKLIST_FILE, 'w', encoding='utf-8') as f: + json.dump(self.blacklist, f, indent=2, ensure_ascii=False) + def get_all_cogs(self): """Scannt das Cogs-Verzeichnis und gibt alle verfügbaren Cogs zurück""" cogs = [] @@ -151,7 +378,6 @@ def get_all_cogs(self): if category_dir.is_dir() and not category_dir.name.startswith('_'): for cog_file in category_dir.glob('*.py'): if not cog_file.name.startswith('_'): - # Format: category.cogname cog_path = f"{category_dir.name}.{cog_file.stem}" cogs.append(cog_path) @@ -159,46 +385,74 @@ def get_all_cogs(self): def format_cog_path(self, cog_input: str): """Formatiert den Cog-Pfad korrekt""" - # Wenn bereits im Format "category.cog", direkt verwenden if '.' in cog_input: category, cog_name = cog_input.split('.', 1) return f"src.bot.cogs.{category}.{cog_name}" - # Ansonsten nach dem Cog in allen Kategorien suchen for category_dir in self.cogs_path.iterdir(): if category_dir.is_dir() and not category_dir.name.startswith('_'): cog_file = category_dir / f"{cog_input}.py" if cog_file.exists(): return f"src.bot.cogs.{category_dir.name}.{cog_input}" - # Fallback return f"src.bot.cogs.{cog_input}" + async def cog_autocomplete(self, ctx: discord.AutocompleteContext): + """Autocomplete für Cog-Namen""" + available_cogs = self.get_all_cogs() + user_input = ctx.value.lower() + + # Filtere basierend auf Eingabe + filtered = [cog for cog in available_cogs if user_input in cog.lower()] + return filtered[:25] # Discord Limit + # ===== SYSTEM COMMANDS ===== @system.command(name="shutdown", description="Stoppt den Bot-Prozess") async def shutdown(self, ctx: discord.ApplicationContext): - container = Container(color=discord.Color.red()) - container.add_text("# ⚠️ ManagerX wird heruntergefahren...") - container.add_separator() - container.add_text("Dies kann ein paar Sekunden dauern.") + container = Container(color=discord.Color.orange()) + container.add_text("# ⚠️ Shutdown bestätigen") + container.add_text("Bist du sicher, dass du den Bot herunterfahren möchtest?") - await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + view = ConfirmView() + await ctx.respond(view=discord.ui.DesignerView(container, timeout=30), ephemeral=True) + await view.wait() - await self.bot.close() - sys.exit() + if view.value: + container = Container(color=discord.Color.red()) + container.add_text("# ⚠️ ManagerX wird heruntergefahren...") + container.add_separator() + container.add_text("Dies kann ein paar Sekunden dauern.") + await ctx.edit(view=discord.ui.DesignerView(container, timeout=0)) + await self.bot.close() + sys.exit() + else: + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Shutdown abgebrochen") + await ctx.edit(view=discord.ui.DesignerView(container, timeout=0)) @system.command(name="restart", description="Startet den Bot neu") async def restart(self, ctx: discord.ApplicationContext): container = Container(color=discord.Color.orange()) - container.add_text("# 🔄 ManagerX wird neugestartet...") - container.add_separator() - container.add_text("Der Bot sollte in wenigen Sekunden wieder online sein.") + container.add_text("# ⚠️ Restart bestätigen") + container.add_text("Bist du sicher, dass du den Bot neustarten möchtest?") - await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + view = ConfirmView() + await ctx.respond(view=discord.ui.DesignerView(container, timeout=30), ephemeral=True) + await view.wait() - await self.bot.close() - os.execv(sys.executable, ['python'] + sys.argv) + if view.value: + container = Container(color=discord.Color.orange()) + container.add_text("# 🔄 ManagerX wird neugestartet...") + container.add_separator() + container.add_text("Der Bot sollte in wenigen Sekunden wieder online sein.") + await ctx.edit(view=discord.ui.DesignerView(container, timeout=0)) + await self.bot.close() + os.execv(sys.executable, ['python'] + sys.argv) + else: + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Restart abgebrochen") + await ctx.edit(view=discord.ui.DesignerView(container, timeout=0)) @system.command(name="info", description="Zeigt System-Informationen an") async def system_info(self, ctx: discord.ApplicationContext): @@ -215,8 +469,7 @@ async def system_info(self, ctx: discord.ApplicationContext): ram_percent = ram.percent ram_available = ram.available / (1024 ** 3) - # Disk Informationen - Dynamisch für Windows und Linux - # Nutze os.path.abspath(os.sep) für das System-Laufwerk + # Disk Informationen disk_path = os.path.abspath(os.sep) try: disk = psutil.disk_usage(disk_path) @@ -230,19 +483,32 @@ async def system_info(self, ctx: discord.ApplicationContext): disk_percent = 0 disk_free_str = "N/A" - # Pfad für die Anzeige formatieren (Forward Slashes verhindern Markdown-Escaping auf Windows) display_path = disk_path.replace("\\", "/") - + # Uptime berechnen uptime = datetime.now() - self.start_time days = uptime.days hours, remainder = divmod(uptime.seconds, 3600) minutes, seconds = divmod(remainder, 60) - + # CPU Frequenz formatieren cpu_freq_current = f"{cpu_freq.current:.0f} MHz" if cpu_freq else "N/A" cpu_freq_max = f"{cpu_freq.max:.0f} MHz" if cpu_freq and cpu_freq.max > 0 else "N/A" - + + # Netzwerk Informationen + try: + net_io = psutil.net_io_counters() + bytes_sent = net_io.bytes_sent / (1024 ** 3) + bytes_recv = net_io.bytes_recv / (1024 ** 3) + net_info = f"📤 {bytes_sent:.2f} GB │ 📥 {bytes_recv:.2f} GB" + except: + net_info = "N/A" + + # Prozess Informationen + process = psutil.Process(os.getpid()) + process_memory = process.memory_info().rss / (1024 ** 2) # MB + process_threads = process.num_threads() + container = Container(color=discord.Color.blue()) container.add_text("# 🖥️ System-Informationen") container.add_separator() @@ -255,30 +521,26 @@ async def system_info(self, ctx: discord.ApplicationContext): container.add_text(f"**Python:** {platform.python_version()}") container.add_text(f"**Py-cord:** {discord.__version__}") - # CPU Modell ermitteln (speziell für Linux/vServer) + # CPU Modell ermitteln def get_cpu_model(): try: cmd = "cat /proc/cpuinfo | grep 'model name' | head -n 1 | cut -d ':' -f 2" model = subprocess.check_output(cmd, shell=True).decode().strip() return model if model else platform.processor() except: - # Fallback für Windows oder wenn cat/grep fehlt if platform.system() == "Windows": return platform.processor() - return "AMD Ryzen 9 7900" # User-Wunsch Fallback - + return "AMD Ryzen 9 7900" + cpu_model = get_cpu_model() - + # CPU Informationen container.add_text("## ⚙️ CPU") container.add_text(f"**Prozessor:** {cpu_model or 'Unbekannt'}") container.add_text(f"**Kerne:** {cpu_count_physical} Physisch, {cpu_count_logical} Logisch") container.add_text(f"**Frequenz:** {cpu_freq_current} (Max: {cpu_freq_max})") - - # CPU Auslastungs-Balken cpu_bar = "█" * int(cpu_percent / 10) + "░" * (10 - int(cpu_percent / 10)) container.add_text(f"**Auslastung:** `{cpu_bar}` {cpu_percent}%") - container.add_separator() # RAM Informationen @@ -286,11 +548,8 @@ def get_cpu_model(): container.add_text(f"**Gesamt:** {ram_total:.2f} GB") container.add_text(f"**Verwendet:** {ram_used:.2f} GB ({ram_percent}%)") container.add_text(f"**Verfügbar:** {ram_available:.2f} GB") - - # RAM Auslastungs-Balken ram_bar = "█" * int(ram_percent / 10) + "░" * (10 - int(ram_percent / 10)) container.add_text(f"`{ram_bar}` {ram_percent}%") - container.add_separator() # Disk Informationen @@ -299,11 +558,16 @@ def get_cpu_model(): container.add_text(f"**Gesamt:** {disk_total_str}") container.add_text(f"**Verwendet:** {disk_used_str} ({disk_percent}%)") container.add_text(f"**Frei:** {disk_free_str}") - - # Disk Auslastungs-Balken disk_bar = "█" * int(disk_percent / 10) + "░" * (10 - int(disk_percent / 10)) container.add_text(f"`{disk_bar}` {disk_percent}%") + container.add_separator() + # Netzwerk & Prozess + container.add_text("## 🌐 Netzwerk & Prozess") + container.add_text(f"**Netzwerk-Traffic:** {net_info}") + container.add_text(f"**Bot-RAM:** {process_memory:.2f} MB") + container.add_text(f"**Threads:** {process_threads}") + container.add_text(f"**PID:** {os.getpid()}") container.add_separator() # Uptime @@ -313,7 +577,7 @@ def get_cpu_model(): await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) # ===== BOT COMMANDS ===== - + @bot.command(name="sync", description="Synchronisiert alle Slash-Commands") async def sync(self, ctx: discord.ApplicationContext): container = Container(color=discord.Color.blue()) @@ -321,55 +585,73 @@ async def sync(self, ctx: discord.ApplicationContext): container.add_text("Befehle werden an die Discord API übertragen.") interaction = await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) - + try: await self.bot.sync_commands() container = Container(color=discord.Color.green()) container.add_separator() container.add_text("✅ **Erfolgreich synchronisiert!**") - await interaction.edit_original_response(view=discord.ui.DesignerView(container, timeout=0)) - except Exception as e: container = Container(color=discord.Color.red()) container.add_separator() container.add_text("## ❌ Synchronisierung fehlgeschlagen!") - container.add_text(f"```py\n{e}\n```") - + container.add_text(f"```py\n{e}\n```") await interaction.edit_original_response(view=discord.ui.DesignerView(container, timeout=0)) @bot.command(name="stats", description="Zeigt Bot-Statistiken an") async def stats(self, ctx: discord.ApplicationContext): - # Bot Statistiken sammeln guild_count = len(self.bot.guilds) user_count = sum(guild.member_count for guild in self.bot.guilds) text_channels = sum(len(guild.text_channels) for guild in self.bot.guilds) voice_channels = sum(len(guild.voice_channels) for guild in self.bot.guilds) - - # Latenz latency = round(self.bot.latency * 1000, 2) - + + # Zusätzliche Statistiken + total_roles = sum(len(guild.roles) for guild in self.bot.guilds) + total_emojis = sum(len(guild.emojis) for guild in self.bot.guilds) + + # Durchschnittswerte + avg_members = user_count // guild_count if guild_count > 0 else 0 + + # Größter Server + biggest_guild = max(self.bot.guilds, key=lambda g: g.member_count) if self.bot.guilds else None + container = Container(color=discord.Color.green()) container.add_text("# 📊 Bot-Statistiken") container.add_separator() - container.add_text(f"**Server:** {guild_count}") + container.add_text(f"**Server:** {guild_count:,}") container.add_text(f"**Benutzer:** {user_count:,}") - container.add_text(f"**Textkanäle:** {text_channels}") - container.add_text(f"**Sprachkanäle:** {voice_channels}") + container.add_text(f"**Ø Mitglieder/Server:** {avg_members:,}") + container.add_separator() + container.add_text(f"**Textkanäle:** {text_channels:,}") + container.add_text(f"**Sprachkanäle:** {voice_channels:,}") + container.add_text(f"**Rollen:** {total_roles:,}") + container.add_text(f"**Emojis:** {total_emojis:,}") container.add_separator() + + if biggest_guild: + container.add_text(f"**Größter Server:** {biggest_guild.name}") + container.add_text(f"**Mitglieder:** {biggest_guild.member_count:,}") + container.add_separator() + container.add_text(f"**Latenz:** {latency} ms") container.add_text(f"**Geladene Cogs:** {len(self.bot.cogs)}") await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) @bot.command(name="reload", description="Lädt einen Cog neu") - async def reload_cog(self, ctx: discord.ApplicationContext, cog: str): + async def reload_cog( + self, + ctx: discord.ApplicationContext, + cog: Option(str, "Name des Cogs", autocomplete=cog_autocomplete) + ): container = Container(color=discord.Color.blue()) container.add_text(f"## 🔄 Lade `{cog}` neu...") interaction = await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) - + try: cog_path = self.format_cog_path(cog) self.bot.reload_extension(cog_path) @@ -378,24 +660,25 @@ async def reload_cog(self, ctx: discord.ApplicationContext, cog: str): container.add_separator() container.add_text(f"✅ **`{cog}` erfolgreich neu geladen!**") container.add_text(f"*Pfad: `{cog_path}`*") - await interaction.edit_original_response(view=discord.ui.DesignerView(container, timeout=0)) - except Exception as e: container = Container(color=discord.Color.red()) container.add_separator() container.add_text(f"## ❌ Fehler beim Neuladen von `{cog}`!") container.add_text(f"```py\n{e}\n```") - await interaction.edit_original_response(view=discord.ui.DesignerView(container, timeout=0)) @bot.command(name="load", description="Lädt einen Cog") - async def load_cog(self, ctx: discord.ApplicationContext, cog: str): + async def load_cog( + self, + ctx: discord.ApplicationContext, + cog: Option(str, "Name des Cogs", autocomplete=cog_autocomplete) + ): container = Container(color=discord.Color.blue()) container.add_text(f"## 📥 Lade `{cog}`...") interaction = await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) - + try: cog_path = self.format_cog_path(cog) self.bot.load_extension(cog_path) @@ -404,31 +687,32 @@ async def load_cog(self, ctx: discord.ApplicationContext, cog: str): container.add_separator() container.add_text(f"✅ **`{cog}` erfolgreich geladen!**") container.add_text(f"*Pfad: `{cog_path}`*") - await interaction.edit_original_response(view=discord.ui.DesignerView(container, timeout=0)) - except Exception as e: container = Container(color=discord.Color.red()) container.add_separator() container.add_text(f"## ❌ Fehler beim Laden von `{cog}`!") container.add_text(f"```py\n{e}\n```") - await interaction.edit_original_response(view=discord.ui.DesignerView(container, timeout=0)) @bot.command(name="unload", description="Entlädt einen Cog") - async def unload_cog(self, ctx: discord.ApplicationContext, cog: str): + async def unload_cog( + self, + ctx: discord.ApplicationContext, + cog: Option(str, "Name des Cogs", autocomplete=cog_autocomplete) + ): if cog.lower() == "admin" or "admin" in cog.lower(): container = Container(color=discord.Color.red()) container.add_text("## ❌ Fehler!") container.add_text("Der Admin-Cog kann nicht entladen werden.") await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) return - + container = Container(color=discord.Color.blue()) container.add_text(f"## 📤 Entlade `{cog}`...") interaction = await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) - + try: cog_path = self.format_cog_path(cog) self.bot.unload_extension(cog_path) @@ -437,28 +721,47 @@ async def unload_cog(self, ctx: discord.ApplicationContext, cog: str): container.add_separator() container.add_text(f"✅ **`{cog}` erfolgreich entladen!**") container.add_text(f"*Pfad: `{cog_path}`*") - await interaction.edit_original_response(view=discord.ui.DesignerView(container, timeout=0)) - except Exception as e: container = Container(color=discord.Color.red()) container.add_separator() container.add_text(f"## ❌ Fehler beim Entladen von `{cog}`!") container.add_text(f"```py\n{e}\n```") - await interaction.edit_original_response(view=discord.ui.DesignerView(container, timeout=0)) @bot.command(name="list_cogs", description="Listet alle geladenen Cogs auf") async def list_cogs(self, ctx: discord.ApplicationContext): loaded_cogs = list(self.bot.cogs.keys()) - loaded_cogs_text = "\n".join([f"✅ `{cog}`" for cog in loaded_cogs]) + + # Gruppiere nach Kategorien + categories = {} + for ext in self.bot.extensions.keys(): + cog_name = ext.replace('src.bot.cogs.', '') + if '.' in cog_name: + category = cog_name.split('.')[0] + name = cog_name.split('.')[1] + else: + category = "Other" + name = cog_name + + if category not in categories: + categories[category] = [] + categories[category].append(name) + + # Erstelle Ausgabe + output = [] + for category, cogs in sorted(categories.items()): + output.append(f"**__{category.upper()}__**") + for cog in sorted(cogs): + output.append(f"✅ `{cog}`") + output.append("") container = Container(color=discord.Color.blue()) container.add_text("# 📦 Geladene Cogs") container.add_separator() - container.add_text(loaded_cogs_text if loaded_cogs_text else "*Keine Cogs geladen*") + container.add_text("\n".join(output) if output else "*Keine Cogs geladen*") container.add_separator() - container.add_text(f"**Gesamt:** {len(loaded_cogs)}") + container.add_text(f"**Gesamt:** {len(loaded_cogs)} Cogs in {len(categories)} Kategorien") await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) @@ -483,7 +786,7 @@ async def available_cogs(self, ctx: discord.ApplicationContext): for category, cogs in sorted(categories.items()): output.append(f"**__{category.upper()}__**") output.extend(cogs) - output.append("") # Leerzeile + output.append("") container = Container(color=discord.Color.blue()) container.add_text("# 📚 Verfügbare Cogs") @@ -497,16 +800,28 @@ async def available_cogs(self, ctx: discord.ApplicationContext): @bot.command(name="reload_all", description="Lädt alle Cogs neu") async def reload_all(self, ctx: discord.ApplicationContext): + container = Container(color=discord.Color.orange()) + container.add_text("# ⚠️ Reload All bestätigen") + container.add_text("Alle Cogs (außer Admin) werden neu geladen. Fortfahren?") + + view = ConfirmView() + await ctx.respond(view=discord.ui.DesignerView(container, timeout=30), ephemeral=True) + await view.wait() + + if not view.value: + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Reload abgebrochen") + await ctx.edit(view=discord.ui.DesignerView(container, timeout=0)) + return + container = Container(color=discord.Color.blue()) container.add_text("## 🔄 Lade alle Cogs neu...") container.add_text("Dies kann einen Moment dauern.") + await ctx.edit(view=discord.ui.DesignerView(container, timeout=0)) - interaction = await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) - success = [] failed = [] - # Liste aller geladenen Extensions (außer admin) extensions = [ext for ext in list(self.bot.extensions.keys()) if 'admin' not in ext.lower()] for ext in extensions: @@ -515,18 +830,18 @@ async def reload_all(self, ctx: discord.ApplicationContext): success.append(ext.replace('src.bot.cogs.', '')) except Exception as e: failed.append(f"{ext.replace('src.bot.cogs.', '')}: {str(e)[:50]}") - - # Ergebnis anzeigen - result_text = [] + result_text = [] if success: result_text.append("**✅ Erfolgreich neu geladen:**") - result_text.extend([f"• `{cog}`" for cog in success]) + result_text.extend([f"• `{cog}`" for cog in success[:10]]) + if len(success) > 10: + result_text.append(f"... und {len(success) - 10} weitere") if failed: result_text.append("\n**❌ Fehlgeschlagen:**") result_text.extend([f"• `{cog}`" for cog in failed]) - + container = Container(color=discord.Color.green() if not failed else discord.Color.orange()) container.add_separator() container.add_text("# 🔄 Reload abgeschlossen!") @@ -535,12 +850,62 @@ async def reload_all(self, ctx: discord.ApplicationContext): container.add_separator() container.add_text(f"**Erfolgreich:** {len(success)} | **Fehlgeschlagen:** {len(failed)}") + await ctx.edit(view=discord.ui.DesignerView(container, timeout=0)) + + @bot.command(name="reload_category", description="Lädt alle Cogs einer Kategorie neu") + async def reload_category( + self, + ctx: discord.ApplicationContext, + category: Option(str, "Kategorie-Name (z.B. 'admin', 'moderation')") + ): + container = Container(color=discord.Color.blue()) + container.add_text(f"## 🔄 Lade Kategorie `{category}` neu...") + + interaction = await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + + success = [] + failed = [] + + extensions = [ext for ext in list(self.bot.extensions.keys()) if f'.{category}.' in ext.lower()] + + if not extensions: + container = Container(color=discord.Color.red()) + container.add_text(f"## ❌ Keine Cogs in Kategorie `{category}` gefunden!") + await interaction.edit_original_response(view=discord.ui.DesignerView(container, timeout=0)) + return + + for ext in extensions: + try: + self.bot.reload_extension(ext) + success.append(ext.replace('src.bot.cogs.', '')) + except Exception as e: + failed.append(f"{ext.replace('src.bot.cogs.', '')}: {str(e)[:50]}") + + result_text = [] + if success: + result_text.append("**✅ Erfolgreich:**") + result_text.extend([f"• `{cog}`" for cog in success]) + + if failed: + result_text.append("\n**❌ Fehlgeschlagen:**") + result_text.extend([f"• `{cog}`" for cog in failed]) + + container = Container(color=discord.Color.green() if not failed else discord.Color.orange()) + container.add_separator() + container.add_text(f"# 🔄 Kategorie `{category}` neu geladen!") + container.add_separator() + container.add_text("\n".join(result_text)) + await interaction.edit_original_response(view=discord.ui.DesignerView(container, timeout=0)) # ===== SERVER COMMANDS ===== - + @server.command(name="leave", description="Verlässt einen Server") - async def leave_server(self, ctx: discord.ApplicationContext, guild_id: str): + async def leave_server( + self, + ctx: discord.ApplicationContext, + guild_id: Option(str, "Server ID") + ): try: guild = self.bot.get_guild(int(guild_id)) if guild is None: @@ -549,16 +914,32 @@ async def leave_server(self, ctx: discord.ApplicationContext, guild_id: str): container.add_text(f"Kein Server mit der ID `{guild_id}` gefunden.") await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) return - + guild_name = guild.name - await guild.leave() - - container = Container(color=discord.Color.green()) - container.add_text("## ✅ Server verlassen!") - container.add_text(f"Erfolgreich **{guild_name}** (`{guild_id}`) verlassen.") - await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) - + # Bestätigung + container = Container(color=discord.Color.orange()) + container.add_text("# ⚠️ Server verlassen bestätigen") + container.add_text(f"**Server:** {guild_name}") + container.add_text(f"**ID:** `{guild_id}`") + container.add_text(f"**Mitglieder:** {guild.member_count:,}") + + view = ConfirmView() + await ctx.respond(view=discord.ui.DesignerView(container, timeout=30), ephemeral=True) + await view.wait() + + if view.value: + await guild.leave() + + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Server verlassen!") + container.add_text(f"Erfolgreich **{guild_name}** (`{guild_id}`) verlassen.") + await ctx.edit(view=discord.ui.DesignerView(container, timeout=0)) + else: + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Abgebrochen") + await ctx.edit(view=discord.ui.DesignerView(container, timeout=0)) + except Exception as e: container = Container(color=discord.Color.red()) container.add_text("## ❌ Fehler!") @@ -566,37 +947,451 @@ async def leave_server(self, ctx: discord.ApplicationContext, guild_id: str): await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) @server.command(name="list", description="Listet alle Server auf") - async def list_servers(self, ctx: discord.ApplicationContext): - guilds = sorted(self.bot.guilds, key=lambda g: g.member_count, reverse=True) + async def list_servers( + self, + ctx: discord.ApplicationContext, + filter: Option(str, "Filter nach Name (optional)", required=False, default="") + ): + guilds = list(self.bot.guilds) - # Wenn weniger als 20 Server, normale Anzeige - if len(guilds) <= 20: - guilds_text = "\n".join([ - f"**{i + 1}.** {guild.name}\n`ID: {guild.id}` • {guild.member_count:,} Mitglieder" - for i, guild in enumerate(guilds) - ]) - + if len(guilds) <= 20 and not filter: + # Einfache Liste ohne Pagination + guilds = sorted(guilds, key=lambda g: g.member_count, reverse=True) + guilds_list = [] + + for i, guild in enumerate(guilds): + name = guild.name[:35] + "..." if len(guild.name) > 35 else guild.name + members = f"{guild.member_count:,}".replace(",", ".") + + boost_emoji = "" + if guild.premium_tier == 3: + boost_emoji = "💎" + elif guild.premium_tier == 2: + boost_emoji = "💠" + elif guild.premium_tier == 1: + boost_emoji = "🔷" + + guilds_list.append(f"`{i + 1:3}.` **{name}** {boost_emoji}") + guilds_list.append(f" ID: `{guild.id}` │ 👥 {members}") + + guilds_text = "\n".join(guilds_list) + container = Container(color=discord.Color.blue()) container.add_text("# 🌐 Server-Liste") container.add_separator() container.add_text(guilds_text if guilds_text else "*Keine Server*") container.add_separator() - container.add_text(f"**Gesamt:** {len(guilds)} Server") + container.add_text(f"📊 **Gesamt:** {len(guilds)} Server") await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) - - # Wenn mehr als 20 Server, Pagination verwenden else: - pagination_view = ServerListView(guilds, page=0, per_page=20) - container = pagination_view.get_page_container() + # Pagination mit DesignerView + pagination = ServerListView(guilds, self.bot, page=0, per_page=20, filter_text=filter) + view = pagination.get_designer_view() + await ctx.respond(view=view, ephemeral=True) + + @server.command(name="info", description="Zeigt detaillierte Informationen zu einem Server") + async def server_info( + self, + ctx: discord.ApplicationContext, + guild_id: Option(str, "Server ID") + ): + try: + guild = self.bot.get_guild(int(guild_id)) + if guild is None: + container = Container(color=discord.Color.red()) + container.add_text("## ❌ Server nicht gefunden!") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + return - await ctx.respond( - view=discord.ui.DesignerView(container, view=pagination_view, timeout=180), - ephemeral=True - ) + # Sammle Informationen + created_at = guild.created_at.strftime("%d.%m.%Y %H:%M") + joined_at = guild.me.joined_at.strftime("%d.%m.%Y %H:%M") if guild.me.joined_at else "Unbekannt" + + # Channels + text_count = len(guild.text_channels) + voice_count = len(guild.voice_channels) + category_count = len(guild.categories) + + # Rollen & Emojis + role_count = len(guild.roles) + emoji_count = len(guild.emojis) + + # Boost Info + boost_level = guild.premium_tier + boost_count = guild.premium_subscription_count + + # Verification & Features + verification = { + discord.VerificationLevel.none: "Keine", + discord.VerificationLevel.low: "Niedrig", + discord.VerificationLevel.medium: "Mittel", + discord.VerificationLevel.high: "Hoch", + discord.VerificationLevel.highest: "Höchste" + }.get(guild.verification_level, "Unbekannt") + + features = [] + if "VERIFIED" in guild.features: + features.append("✅ Verifiziert") + if "PARTNERED" in guild.features: + features.append("🤝 Partner") + if "COMMUNITY" in guild.features: + features.append("🏘️ Community") + if "DISCOVERABLE" in guild.features: + features.append("🔍 Auffindbar") + + features_text = " • ".join(features) if features else "Keine besonderen Features" + + container = Container(color=discord.Color.blue()) + container.add_text(f"# 🌐 Server-Info: {guild.name}") + container.add_separator() + + container.add_text("## 📋 Allgemein") + container.add_text(f"**ID:** `{guild.id}`") + container.add_text(f"**Owner:** {guild.owner.mention if guild.owner else 'Unbekannt'}") + container.add_text(f"**Erstellt:** {created_at}") + container.add_text(f"**Beigetreten:** {joined_at}") + container.add_separator() + + container.add_text("## 👥 Mitglieder") + container.add_text(f"**Gesamt:** {guild.member_count:,}") + container.add_text(f"**Menschen:** {len([m for m in guild.members if not m.bot]):,}") + container.add_text(f"**Bots:** {len([m for m in guild.members if m.bot]):,}") + container.add_separator() + + container.add_text("## 📺 Kanäle") + container.add_text(f"**Text:** {text_count}") + container.add_text(f"**Voice:** {voice_count}") + container.add_text(f"**Kategorien:** {category_count}") + container.add_text(f"**Gesamt:** {len(guild.channels)}") + container.add_separator() + + container.add_text("## 🎨 Weitere Infos") + container.add_text(f"**Rollen:** {role_count}") + container.add_text(f"**Emojis:** {emoji_count}/{guild.emoji_limit}") + container.add_text(f"**Boost Level:** {boost_level} ({boost_count} Boosts)") + container.add_text(f"**Verifizierung:** {verification}") + container.add_separator() + + container.add_text("## ✨ Features") + container.add_text(features_text) + + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + + except Exception as e: + container = Container(color=discord.Color.red()) + container.add_text("## ❌ Fehler!") + container.add_text(f"```py\n{e}\n```") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) - # ===== TEST COMMAND ===== + # ===== USER COMMANDS ===== + + @user.command(name="lookup", description="Findet einen User in allen Servern") + async def user_lookup( + self, + ctx: discord.ApplicationContext, + user_id: Option(str, "User ID") + ): + try: + user_id_int = int(user_id) + + # Suche User in allen Guilds + found_in = [] + user_obj = None + + for guild in self.bot.guilds: + member = guild.get_member(user_id_int) + if member: + user_obj = member + found_in.append({ + "guild": guild, + "joined": member.joined_at, + "roles": len(member.roles) - 1, # -1 für @everyone + "nickname": member.nick + }) + + if not found_in: + # Versuche User zu fetchen + try: + user_obj = await self.bot.fetch_user(user_id_int) + except: + container = Container(color=discord.Color.red()) + container.add_text("## ❌ User nicht gefunden!") + container.add_text(f"Kein User mit der ID `{user_id}` gefunden.") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + return + + # Erstelle Ausgabe + container = Container(color=discord.Color.blue()) + container.add_text(f"# 👤 User Lookup: {user_obj}") + container.add_separator() + + container.add_text("## 📋 User-Info") + container.add_text(f"**Name:** {user_obj.name}") + container.add_text(f"**ID:** `{user_obj.id}`") + container.add_text(f"**Bot:** {'Ja' if user_obj.bot else 'Nein'}") + container.add_text(f"**Erstellt:** {user_obj.created_at.strftime('%d.%m.%Y %H:%M')}") + container.add_separator() + + if found_in: + container.add_text(f"## 🌐 Gefunden in {len(found_in)} Server(n)") + for entry in found_in[:10]: # Max 10 anzeigen + guild = entry['guild'] + joined = entry['joined'].strftime('%d.%m.%Y') if entry['joined'] else 'Unbekannt' + nick = f" (Nick: {entry['nickname']})" if entry['nickname'] else "" + container.add_text(f"**{guild.name}**{nick}") + container.add_text(f" Beigetreten: {joined} │ Rollen: {entry['roles']}") + + if len(found_in) > 10: + container.add_text(f"\n... und {len(found_in) - 10} weitere Server") + else: + container.add_text("## ℹ️ Nicht in gemeinsamen Servern") + + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + + except ValueError: + container = Container(color=discord.Color.red()) + container.add_text("## ❌ Ungültige ID!") + container.add_text("Bitte gib eine gültige User-ID ein.") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + except Exception as e: + container = Container(color=discord.Color.red()) + container.add_text("## ❌ Fehler!") + container.add_text(f"```py\n{e}\n```") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + + @user.command(name="blacklist", description="Fügt einen User zur Blacklist hinzu") + async def blacklist_user( + self, + ctx: discord.ApplicationContext, + user_id: Option(str, "User ID"), + reason: Option(str, "Grund", required=False, default="Kein Grund angegeben") + ): + user_id_str = str(user_id) + + if user_id_str in [u["id"] for u in self.blacklist["users"]]: + container = Container(color=discord.Color.orange()) + container.add_text("## ⚠️ User bereits auf Blacklist!") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + return + + self.blacklist["users"].append({ + "id": user_id_str, + "reason": reason, + "added_by": str(ctx.author.id), + "added_at": datetime.now().isoformat() + }) + self.save_blacklist() + + container = Container(color=discord.Color.green()) + container.add_text("## ✅ User zur Blacklist hinzugefügt!") + container.add_text(f"**User ID:** `{user_id}`") + container.add_text(f"**Grund:** {reason}") + + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + @user.command(name="unblacklist", description="Entfernt einen User von der Blacklist") + async def unblacklist_user( + self, + ctx: discord.ApplicationContext, + user_id: Option(str, "User ID") + ): + user_id_str = str(user_id) + + # Suche den Eintrag + entry = next((u for u in self.blacklist["users"] if u["id"] == user_id_str), None) + + if not entry: + container = Container(color=discord.Color.orange()) + container.add_text("## ⚠️ User nicht auf Blacklist!") + container.add_text(f"User `{user_id}` ist nicht auf der Blacklist.") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + return + + self.blacklist["users"].remove(entry) + self.save_blacklist() + + container = Container(color=discord.Color.green()) + container.add_text("## ✅ User von Blacklist entfernt!") + container.add_text(f"**User ID:** `{user_id}`") + container.add_text(f"**Ehemaliger Grund:** {entry.get('reason', 'Unbekannt')}") + + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + + @user.command(name="blacklist_list", description="Zeigt alle geblacklisteten User an") + async def blacklist_list_users(self, ctx: discord.ApplicationContext): + users = self.blacklist.get("users", []) + + if not users: + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Keine User auf der Blacklist!") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + return + + entries = [] + for u in users: + added_at = datetime.fromisoformat(u["added_at"]).strftime("%d.%m.%Y %H:%M") if u.get("added_at") else "Unbekannt" + entries.append(f"**ID:** `{u['id']}` │ **Grund:** {u.get('reason', '-')} │ {added_at}") + + container = Container(color=discord.Color.red()) + container.add_text(f"# 🚫 User-Blacklist ({len(users)})") + container.add_separator() + container.add_text("\n".join(entries[:25])) + if len(entries) > 25: + container.add_text(f"\n... und {len(entries) - 25} weitere") + + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + + @server.command(name="blacklist", description="Fügt einen Server zur Blacklist hinzu") + async def blacklist_guild( + self, + ctx: discord.ApplicationContext, + guild_id: Option(str, "Server ID"), + reason: Option(str, "Grund", required=False, default="Kein Grund angegeben"), + auto_leave: Option(bool, "Automatisch verlassen?", required=False, default=True) + ): + guild_id_str = str(guild_id) + + if guild_id_str in [g["id"] for g in self.blacklist["guilds"]]: + container = Container(color=discord.Color.orange()) + container.add_text("## ⚠️ Server bereits auf Blacklist!") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + return + + self.blacklist["guilds"].append({ + "id": guild_id_str, + "reason": reason, + "added_by": str(ctx.author.id), + "added_at": datetime.now().isoformat() + }) + self.save_blacklist() + + # Optional: Server verlassen + if auto_leave: + guild = self.bot.get_guild(int(guild_id)) + if guild: + await guild.leave() + + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Server zur Blacklist hinzugefügt!") + container.add_text(f"**Server ID:** `{guild_id}`") + container.add_text(f"**Grund:** {reason}") + if auto_leave: + container.add_text(f"**Status:** Server verlassen") + + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + + @server.command(name="unblacklist", description="Entfernt einen Server von der Blacklist") + async def unblacklist_guild( + self, + ctx: discord.ApplicationContext, + guild_id: Option(str, "Server ID") + ): + guild_id_str = str(guild_id) + + entry = next((g for g in self.blacklist["guilds"] if g["id"] == guild_id_str), None) + + if not entry: + container = Container(color=discord.Color.orange()) + container.add_text("## ⚠️ Server nicht auf Blacklist!") + container.add_text(f"Server `{guild_id}` ist nicht auf der Blacklist.") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + return + + self.blacklist["guilds"].remove(entry) + self.save_blacklist() + + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Server von Blacklist entfernt!") + container.add_text(f"**Server ID:** `{guild_id}`") + container.add_text(f"**Ehemaliger Grund:** {entry.get('reason', 'Unbekannt')}") + + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + + @server.command(name="blacklist_list", description="Zeigt alle geblacklisteten Server an") + async def blacklist_list_guilds(self, ctx: discord.ApplicationContext): + guilds = self.blacklist.get("guilds", []) + + if not guilds: + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Keine Server auf der Blacklist!") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + return + + entries = [] + for g in guilds: + added_at = datetime.fromisoformat(g["added_at"]).strftime("%d.%m.%Y %H:%M") if g.get("added_at") else "Unbekannt" + entries.append(f"**ID:** `{g['id']}` │ **Grund:** {g.get('reason', '-')} │ {added_at}") + + container = Container(color=discord.Color.red()) + container.add_text(f"# 🚫 Server-Blacklist ({len(guilds)})") + container.add_separator() + container.add_text("\n".join(entries[:25])) + if len(entries) > 25: + container.add_text(f"\n... und {len(entries) - 25} weitere") + + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + + # ===== LOGS COMMANDS ===== + + @logs.command(name="view", description="Zeigt die letzten Admin-Logs an") + async def view_logs( + self, + ctx: discord.ApplicationContext, + limit: Option(int, "Anzahl der Einträge", required=False, default=20, min_value=1, max_value=50) + ): + if not AUDIT_LOG_FILE.exists(): + container = Container(color=discord.Color.orange()) + container.add_text("## ℹ️ Keine Logs vorhanden") + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + return + + with open(AUDIT_LOG_FILE, 'r', encoding='utf-8') as f: + logs = json.load(f) + + recent_logs = logs[-limit:] + recent_logs.reverse() + + log_text = [] + for log in recent_logs: + timestamp = datetime.fromisoformat(log["timestamp"]).strftime("%d.%m %H:%M") + user = log["user_name"] + command = log["command"] + log_text.append(f"`{timestamp}` **{user}** → `/{command}`") + + container = Container(color=discord.Color.blue()) + container.add_text(f"# 📜 Admin-Logs (Letzte {len(recent_logs)})") + container.add_separator() + container.add_text("\n".join(log_text)) + container.add_separator() + container.add_text(f"**Gesamt:** {len(logs)} Einträge") + + await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) + + @logs.command(name="clear", description="Löscht alle Admin-Logs") + async def clear_logs(self, ctx: discord.ApplicationContext): + container = Container(color=discord.Color.orange()) + container.add_text("# ⚠️ Logs löschen bestätigen") + container.add_text("Alle Admin-Logs werden permanent gelöscht!") + + view = ConfirmView() + await ctx.respond(view=discord.ui.DesignerView(container, timeout=30), ephemeral=True) + await view.wait() + + if view.value: + if AUDIT_LOG_FILE.exists(): + AUDIT_LOG_FILE.unlink() + + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Logs gelöscht!") + await ctx.edit(view=discord.ui.DesignerView(container, timeout=0)) + else: + container = Container(color=discord.Color.green()) + container.add_text("## ✅ Abgebrochen") + await ctx.edit(view=discord.ui.DesignerView(container, timeout=0)) + + # ===== TEST COMMAND ===== + @bot.command(name="test", description="Testet die Bot-Funktionalität") async def test(self, ctx: discord.ApplicationContext): container = Container(color=discord.Color.blue()) @@ -604,7 +1399,6 @@ async def test(self, ctx: discord.ApplicationContext): interaction = await ctx.respond(view=discord.ui.DesignerView(container, timeout=0), ephemeral=True) - # Simuliere eine kurze Verzögerung await asyncio.sleep(1) container = Container(color=discord.Color.green()) @@ -613,8 +1407,10 @@ async def test(self, ctx: discord.ApplicationContext): container.add_text(f"**Bot Status:** Online") container.add_text(f"**Latenz:** {round(self.bot.latency * 1000, 2)} ms") container.add_text(f"**Befehl ausgeführt von:** {ctx.author.mention}") + container.add_text(f"**Rate Limit:** {self.command_usage.get(ctx.author.id, (0, 0))[1]}/30") await interaction.edit_original_response(view=discord.ui.DesignerView(container, timeout=0)) + def setup(bot): bot.add_cog(admin(bot)) \ No newline at end of file diff --git a/src/bot/cogs/bot/join_alert.py b/src/bot/cogs/bot/join_alert.py index 178e87c..088d266 100644 --- a/src/bot/cogs/bot/join_alert.py +++ b/src/bot/cogs/bot/join_alert.py @@ -10,24 +10,33 @@ def __init__(self, bot): async def on_guild_join(self, guild: discord.Guild): owner = guild.owner.display_name if guild.owner else "Unbekannt" member_count = guild.member_count - + # Hier nutzen wir len(), um die Anzahl der Server als Zahl zu bekommen + total_servers = len(self.bot.guilds) - container = Container(color=discord.Color.green()) - container.add_text(f"## 📥 Neuer Server!") + # Container erstellen mit einer coolen Farbe + container = Container(color=discord.Color.brand_green()) + container.add_text(f"## 📥 Neuer Server beigetreten!") container.add_separator() - container.add_text(f"**Name:** {guild.name}") - container.add_text(f"**Owner:** {owner}") - container.add_text(f"**Member:** {member_count}") + # Die Infos schön untereinander mit Emojis + container.add_text( + f"**🏠 Name:** `{guild.name}`\n" + f"**👑 Owner:** `{owner}`\n" + f"**👥 Mitglieder:** `{member_count}`\n" + f"**🆔 ID:** `{guild.id}`\n" + f"**📊 Gesamtanzahl Server:** `{total_servers}`" + ) + # Die Channel-ID von dir log_channel_id = 1429163147687886889 log_channel = self.bot.get_channel(log_channel_id) if log_channel: + # Die DesignerView macht das Ganze im Discord-Chat hübsch view = DesignerView(container, timeout=None) await log_channel.send(view=view) - print(f"[+] Bot ist neu auf: {guild.name}") + print(f"[+] Bot ist neu auf: {guild.name} (Server jetzt: {total_servers})") def setup(bot): bot.add_cog(JoinAlert(bot)) \ No newline at end of file diff --git a/src/bot/cogs/bot/leave_alert.py b/src/bot/cogs/bot/leave_alert.py index 593834b..b26a546 100644 --- a/src/bot/cogs/bot/leave_alert.py +++ b/src/bot/cogs/bot/leave_alert.py @@ -5,33 +5,44 @@ class LeaveAlert(ezcord.Cog): def __init__(self, bot): self.bot = bot - # ID deines Kanals, in den die Info fließen soll + # Dein Log-Kanal für Abgänge self.log_channel_id = 1429164270435700849 @discord.Cog.listener() async def on_guild_remove(self, guild: discord.Guild): - # Container für den Abschied bauen - container = Container(color=discord.Color.red()) - container.add_text("## 📤 Bot wurde entfernt") + # Wir berechnen den neuen Serverstand direkt + total_servers = len(self.bot.guilds) + + # Container in Rot für den Abschied + container = Container(color=discord.Color.from_rgb(255, 0, 0)) # Sattes Rot + container.add_text("## 📤 Bot hat einen Server verlassen") container.add_separator() - # Falls der Name nicht mehr greifbar ist (selten), nutzen wir die ID guild_name = guild.name if guild.name else "Unbekannter Server" - container.add_text(f"**Server:** {guild_name}") - container.add_text(f"**ID:** {guild.id}") + # Schickere Formatierung mit Emojis und Backticks + info_text = ( + f"**🏠 Server:** `{guild_name}`\n" + f"**🆔 ID:** `{guild.id}`\n" + ) - # Da wir weg sind, wissen wir nicht genau, wie viele Member es zuletzt waren, - # aber wir können versuchen, den letzten Stand auszugeben if guild.member_count: - container.add_text(f"**Letzter Stand:** {guild.member_count} Mitglieder") + info_text += f"**👥 Letzte Mitgliederzahl:** `{guild.member_count}`\n" + + info_text += f"**📊 Neue Gesamtanzahl:** `{total_servers}`" + + container.add_text(info_text) + + # Versuchen, das Icon noch zu kriegen (manchmal klappt's noch kurz nach dem Leave) + if guild.icon: + container.set_thumbnail(guild.icon.url) log_channel = self.bot.get_channel(self.log_channel_id) if log_channel: view = DesignerView(container, timeout=None) await log_channel.send(view=view) - print(f"[-] Bot hat den Server verlassen: {guild_name} ({guild.id})") + print(f"[-] Bot verlassen: {guild_name} | Server verbleibend: {total_servers}") def setup(bot): bot.add_cog(LeaveAlert(bot)) \ No newline at end of file diff --git a/src/bot/cogs/guild/globalchat.py b/src/bot/cogs/guild/globalchat.py index 73caebc..d6e5e4e 100644 --- a/src/bot/cogs/guild/globalchat.py +++ b/src/bot/cogs/guild/globalchat.py @@ -2,7 +2,7 @@ import discord from discord.ext import commands, tasks from discord import slash_command, Option, SlashCommandGroup -from DevTools.backend.database.globalchat_db import GlobalChatDatabase, db +from mx_devtools.backend.database.globalchat_db import GlobalChatDatabase, db import asyncio import logging import re @@ -256,13 +256,13 @@ async def create_message_embed(self, message: discord.Message, settings: Dict, a # Embed-Farbe embed_color = self._parse_color(settings.get('embed_color', self.config.DEFAULT_EMBED_COLOR)) - # Beschreibung + # Beschreibung mit stilisiertem Layout if content: - description = content + description = f"{content}" elif message.attachments or message.stickers or attachment_data: - description = "*Medien-Nachricht*" + description = "📎 *Medien-Nachricht*" else: - description = "*Keine Beschreibung*" + description = "" # Embed erstellen embed = discord.Embed( @@ -271,15 +271,18 @@ async def create_message_embed(self, message: discord.Message, settings: Dict, a timestamp=message.created_at ) - # Author mit Badges + # Author mit Badges und Username-Tag author_text, badges = self._build_author_info(message.author) embed.set_author( name=author_text, icon_url=message.author.display_avatar.url ) - # Footer mit Server-Info UND Original-Message-ID (für Reply-Tracking) - footer_text = f"📍 {message.guild.name} • #{message.channel.name} • ID:{message.id}" + # Thumbnail mit User-Avatar für bessere Optik + embed.set_thumbnail(url=message.author.display_avatar.url) + + # Footer mit Server-Info und schönerem Layout + footer_text = f"🌐 {message.guild.name} • #{message.channel.name} • ID:{message.id}" embed.set_footer( text=footer_text, icon_url=message.guild.icon.url if message.guild.icon else None @@ -599,7 +602,7 @@ def _parse_color(self, color_hex: str) -> discord.Color: return discord.Color.blurple() def _build_author_info(self, author: discord.Member) -> Tuple[str, List[str]]: - """Baut Author-Text mit Badges""" + """Baut Author-Text mit Badges – schöneres Format""" badges = [] roles = [] # Bot Owner @@ -613,13 +616,24 @@ def _build_author_info(self, author: discord.Member) -> Tuple[str, List[str]]: elif author.guild_permissions.manage_guild: badges.append("🔧") roles.append("Mod") + + # Nitro/Booster Badge + if hasattr(author, 'premium_since') and author.premium_since: + badges.append("💎") + roles.append("Booster") badge_text = " ".join(badges) - author_text = f"{badge_text} {author.display_name}".strip() + display = author.display_name + + # Format: "👑 ⚡ DisplayName (@username)" + if badge_text: + author_text = f"{badge_text} {display} (@{author.name})" + else: + author_text = f"{display} (@{author.name})" # Hinzufügen von Discord System Badges (z.B. Bot, Verified Bot) if author.bot: - author_text += " [BOT]" + author_text += " ✦ BOT" return author_text, roles @@ -714,13 +728,9 @@ async def send_global_message(self, message: discord.Message, attachment_data: L successful_sends = 0 failed_sends = 0 - # Berechne, wie viele Tasks gleichzeitig laufen sollen (z.B. 10) + # Sende an ALLE aktiven Channels (inkl. Ursprungskanal – Original wird gelöscht) tasks = [] for channel_id in active_channels: - # Sende nicht an den Ursprungskanal zurück - if channel_id == message.channel.id: - continue - tasks.append(self._send_to_channel(channel_id, embed, files_to_upload)) results = await asyncio.gather(*tasks, return_exceptions=True) @@ -831,17 +841,16 @@ async def on_message(self, message: discord.Message): # Wenn Download fehlschlägt, Nachricht trotzdem ohne Medien senden attachment_data = [] - # Nachricht senden - successful, failed = await self.sender.send_global_message(message, attachment_data) + # Ursprüngliche Nachricht IMMER löschen (wird als Embed neu gepostet) + try: + await message.delete() + except discord.Forbidden: + logger.warning(f"⚠️ Keine Permissions zum Löschen der Original-Nachricht in {message.channel.id}") + except discord.NotFound: + pass - # Ursprüngliche Nachricht löschen, wenn Relaying erfolgreich war - if settings.get('delete_original', False): - try: - await message.delete() - except discord.Forbidden: - logger.warning(f"⚠️ Keine Permissions zum Löschen der Original-Nachricht in {message.channel.id}") - except discord.NotFound: - pass + # Nachricht als Embed an alle Channels senden (inkl. eigenen) + successful, failed = await self.sender.send_global_message(message, attachment_data) logger.info(f"🌍 GlobalChat: Nachricht von {message.guild.name} | User: {message.author.name} | ✅ {successful} | ❌ {failed}") diff --git a/src/bot/cogs/guild/levelsystem.py b/src/bot/cogs/guild/levelsystem.py index a3cce5e..76662df 100644 --- a/src/bot/cogs/guild/levelsystem.py +++ b/src/bot/cogs/guild/levelsystem.py @@ -4,7 +4,7 @@ from discord.ext import commands, tasks import time import random -from DevTools import LevelDatabase +from mx_devtools import LevelDatabase import asyncio import io import csv diff --git a/src/bot/cogs/guild/loggingsystem.py b/src/bot/cogs/guild/loggingsystem.py index 8fb1e60..fa1e9ec 100644 --- a/src/bot/cogs/guild/loggingsystem.py +++ b/src/bot/cogs/guild/loggingsystem.py @@ -10,7 +10,7 @@ import logging import ezcord # Import our separate database class -from DevTools import LoggingDatabase +from mx_devtools import LoggingDatabase # Setup logging logger = logging.getLogger(__name__) diff --git a/src/bot/cogs/guild/tempvc.py b/src/bot/cogs/guild/tempvc.py index 87fb7ec..9c42c13 100644 --- a/src/bot/cogs/guild/tempvc.py +++ b/src/bot/cogs/guild/tempvc.py @@ -1,5 +1,5 @@ # Copyright (c) 2025 OPPRO.NET Network -from DevTools import TempVCDatabase +from mx_devtools import TempVCDatabase import discord from discord import slash_command, option, SlashCommandGroup from discord.ext import commands diff --git a/src/bot/cogs/guild/welcome.py b/src/bot/cogs/guild/welcome.py index 325f154..77d93c7 100644 --- a/src/bot/cogs/guild/welcome.py +++ b/src/bot/cogs/guild/welcome.py @@ -8,7 +8,7 @@ import discord from discord.ext import commands -from DevTools import WelcomeDatabase +from mx_devtools import WelcomeDatabase import asyncio import json import io diff --git a/src/bot/cogs/legacy/secret_commands.py b/src/bot/cogs/legacy/secret_commands.py new file mode 100644 index 0000000..88b4586 --- /dev/null +++ b/src/bot/cogs/legacy/secret_commands.py @@ -0,0 +1,30 @@ +import discord +from discord.ext import commands +import ezcord +import random + +class SecretCommands(ezcord.Cog, hidden=True): + def __init__(self, bot): + self.bot = bot + + @commands.command(name="login") + async def admin_login(self, ctx): + await ctx.send("sie werden eingeloggt...") + + @commands.command(name="helau") + async def helau(self, ctx): + # Eine Liste mit verschiedenen Pegau-Vibes + responses = [ + "**🎭 PEGAU HELAU!**", + "**🎭 Ein dreifach donnerndes: PEGAU HELAU!**", + "**🎭 Die Garde steht bereit! HELAU!**", + "**🎭 Karneval in Pegau – das ist unsere Zeit!**", + "**🎭 Helau, Helau, Helau! Auf eine geile Saison!**", + "**🎭 Wer nicht hüpft, der ist kein Pegauer! HELAU!**" + ] + + # Wählt einen zufälligen Spruch aus der Liste oben + await ctx.send(random.choice(responses)) + +def setup(bot): + bot.add_cog(SecretCommands(bot)) \ No newline at end of file diff --git a/src/bot/cogs/management/autodelete.py b/src/bot/cogs/management/autodelete.py index 9ba365c..6c79776 100644 --- a/src/bot/cogs/management/autodelete.py +++ b/src/bot/cogs/management/autodelete.py @@ -1,4 +1,4 @@ -from DevTools import AutoDeleteDB +from mx_devtools import AutoDeleteDB import discord from discord.ext import tasks from discord.commands import SlashCommandGroup, Option diff --git a/src/bot/cogs/management/autorole.py b/src/bot/cogs/management/autorole.py index 3e3613c..7d30f30 100644 --- a/src/bot/cogs/management/autorole.py +++ b/src/bot/cogs/management/autorole.py @@ -1,7 +1,7 @@ import discord from discord.ext import commands from discord import option -from DevTools import AutoRoleDatabase +from mx_devtools import AutoRoleDatabase from mx_handler import TranslationHandler as TH class AutoRole(commands.Cog): diff --git a/src/bot/cogs/moderation/antispam.py b/src/bot/cogs/moderation/antispam.py index 5bdce7f..db833f0 100644 --- a/src/bot/cogs/moderation/antispam.py +++ b/src/bot/cogs/moderation/antispam.py @@ -8,7 +8,7 @@ from datetime import timedelta -from DevTools import AntiSpamDatabase as SpamDB +from mx_devtools import AntiSpamDatabase as SpamDB antispam = SlashCommandGroup("antispam") class AntiSpam(ezcord.Cog): diff --git a/src/bot/cogs/moderation/notes.py b/src/bot/cogs/moderation/notes.py index 0aeae4b..5694538 100644 --- a/src/bot/cogs/moderation/notes.py +++ b/src/bot/cogs/moderation/notes.py @@ -6,7 +6,7 @@ from discord import SlashCommandGroup import datetime import ezcord -from DevTools import NotesDatabase +from mx_devtools import NotesDatabase notes = SlashCommandGroup("notes") # ─────────────────────────────────────────────── diff --git a/src/bot/cogs/moderation/warn.py b/src/bot/cogs/moderation/warn.py index 005e161..34cfd65 100644 --- a/src/bot/cogs/moderation/warn.py +++ b/src/bot/cogs/moderation/warn.py @@ -2,7 +2,7 @@ # ─────────────────────────────────────────────── # >> Imports # ─────────────────────────────────────────────── -from DevTools import WarnDatabase +from mx_devtools import WarnDatabase import discord from discord import slash_command, Option import os diff --git a/src/bot/cogs/user/settings.py b/src/bot/cogs/user/settings.py index 80e5972..ae066bb 100644 --- a/src/bot/cogs/user/settings.py +++ b/src/bot/cogs/user/settings.py @@ -1,224 +1,115 @@ +# Copyright (c) 2026 OPPRO.NET Network import discord from discord.ext import commands from discord import SlashCommandGroup import ezcord -from discord.ui import Container - -from mx_handler import TranslationHandler -import os -from src.bot.core.constants import ERROR_COLOR, SUCCESS_COLOR, emoji_warn, emoji_delete, AUTHOR, FOOTER -from DevTools import StatsDB, WarnDatabase, NotesDatabase, LevelDatabase -import sqlite3 - +import io +import json +from datetime import datetime + +from src.bot.core.constants import ERROR_COLOR, SUCCESS_COLOR, emoji_warn, AUTHOR, FOOTER +from mx_devtools import ( + StatsDB, WarnDatabase, NotesDatabase, LevelDatabase, + ProfileDB, SettingsDB, AutoDeleteDB, + AntiSpamDatabase, TempVCDatabase +) +from mx_devtools.backend.database.globalchat_db import GlobalChatDatabase, db as global_db class Settings(ezcord.Cog): - """Cog for setting user language preferences.""" - - user = SlashCommandGroup("user", "User settings commands") + """Cog für Benutzereinstellungen, Sprache und Datenverwaltung.""" - language = user.create_subgroup( - "language") - - data = user.create_subgroup("data", "Manage your data") + user = SlashCommandGroup("user", "Benutzer-Einstellungen") + language = user.create_subgroup("language", "Spracheinstellungen") + data = user.create_subgroup("data", "Datenverwaltung (DSGVO)") 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. + # --- Spracheinstellungen --- + + @language.command(name="set", description="Setze deine bevorzugte Sprache.") + @discord.option("lang", description="Wähle eine Sprache", choices=[ + discord.OptionChoice(name="Deutsch 🇩🇪", value="de"), + discord.OptionChoice(name="English 🇬🇧", value="en") + ]) + async def set_lang(self, ctx: discord.ApplicationContext, lang: str): + db = SettingsDB() + db.set_user_language(ctx.author.id, lang) + db.close() - 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. + msg = "✅ Sprache auf **Deutsch 🇩🇪** gesetzt." if lang == "de" else "✅ Language set to **English 🇬🇧**." + await ctx.respond(msg, ephemeral=True) + + @language.command(name="show", description="Zeigt deine aktuell eingestellte Sprache.") + async def show_lang(self, ctx: discord.ApplicationContext): + db = SettingsDB() + lang = db.get_user_language(ctx.author.id) + db.close() - 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 - ) + lang_name = self.AVAILABLE_LANGUAGES.get(lang, "English 🇬🇧") + await ctx.respond(f"🌍 Deine aktuelle Sprache ist: **{lang_name}**", ephemeral=True) - await ctx.respond(response_text, ephemeral=True) + # --- Daten-Export (DSGVO Art. 15) --- - @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) - - @data.command( - name="get", - description="Zeigt alle Daten an, die ManagerX über dich gespeichert hat." - ) - async def get_all_data(self, ctx: discord.ApplicationContext): - """Sammelt alle User-Daten und zeigt sie in einem Container an.""" + @data.command(name="get", description="Erhalte eine Kopie all deiner gespeicherten Daten (JSON).") + async def get_user_data(self, ctx: discord.ApplicationContext): + """Erstellt ein Datenpaket aus allen verknüpften Datenbanken.""" await ctx.defer(ephemeral=True) + uid = ctx.author.id - user_id = ctx.author.id - guild_id = ctx.guild.id + export_data = { + "metadata": { + "user_id": uid, + "exported_at": datetime.now().isoformat(), + "source": "ManagerX Network" + }, + "content": {} + } - # 1. Datenbanken initialisieren try: - stats_db = StatsDB() - level_db = LevelDatabase() + # Daten aus den verschiedenen Modulen sammeln + # Hinweis: Manche Methoden müssen in deinen DB-Klassen existieren + export_data["content"]["settings"] = SettingsDB().get_user_language(uid) + export_data["content"]["profile"] = ProfileDB().get_user_profile(uid) + export_data["content"]["leveling"] = LevelDatabase().get_user_data(uid) + export_data["content"]["global_chat_history"] = global_db.get_user_message_history(uid, limit=50) - # WarnDatabase needs a base path that contains 'Datenbanken/warns.db' - # Based on moderation/warn.py, it takes os.path.dirname(__file__) - # which is src/bot/cogs/moderation - warn_base_path = os.path.join("src", "bot", "cogs", "moderation") - warn_db = WarnDatabase(warn_base_path) + # Moderationsdaten (nur für diesen Server) + warn_db = WarnDatabase(".") + export_data["content"]["local_warnings"] = warn_db.get_warnings(ctx.guild.id, uid) - notes_db = NotesDatabase("data") - except Exception as e: - await ctx.respond(f"Fehler beim Initialisieren der Datenbanken: {e}", ephemeral=True) - return - - # 2. Daten sammeln - # Language - lang = self.bot.settings_db.get_user_language(user_id) or "de" - lang_name = self.AVAILABLE_LANGUAGES.get(lang, lang) - - # Global Stats - global_info = await stats_db.get_global_user_info(user_id) - - # Server Stats - user_stats = level_db.get_user_stats(user_id, guild_id) - - # Moderation - warnings = warn_db.get_warnings(guild_id, user_id) - notes = notes_db.get_notes(guild_id, user_id) - - # 3. Container erstellen - container = Container() - container.add_text(f"# 👤 Datenauskunft für {ctx.author.name}") - container.add_separator() - - # Einstellungen - container.add_text(f"### ⚙️ Einstellungen") - container.add_text(f"**Bevorzugte Sprache:** {lang_name}") - container.add_separator() + notes_db = NotesDatabase(".") + export_data["content"]["local_notes"] = notes_db.get_notes(ctx.guild.id, uid) - # Globale Daten - container.add_text("### 🌍 Globale Statistiken (Serverübergreifend)") - if global_info: - container.add_text(f"**Global Level:** {global_info['level']}") - container.add_text(f"**Gesamt XP:** {int(global_info['xp']):,}") - container.add_text(f"**Nachrichten gesamt:** {global_info['total_messages']:,}") - container.add_text(f"**Voice Zeit gesamt:** {int(global_info['total_voice_minutes'] // 60)}h {int(global_info['total_voice_minutes'] % 60)}m") - container.add_text(f"**Aktivitäts-Streak:** {global_info['daily_streak']} Tage") - else: - container.add_text("*Keine globalen Daten gefunden.*") - container.add_separator() + except Exception as e: + print(f"Export-Fehler: {e}") + # Wir machen weiter, um zumindest Teil-Daten zu liefern - # Lokale Server Daten - container.add_text(f"### 🏢 Server Statistiken ({ctx.guild.name})") - if user_stats: - xp, level, messages, xp_needed, prestige, total_earned = user_stats - container.add_text(f"**Lokales Level:** {level}") - container.add_text(f"**Aktuelle XP:** {xp:,} / {xp + xp_needed:,}") - if prestige > 0: - container.add_text(f"**Prestige Rang:** ⭐ {prestige}") - else: - container.add_text("*Keine lokalen Daten in diesem Server gefunden.*") - container.add_separator() + # JSON Datei erstellen + json_str = json.dumps(export_data, indent=4, ensure_ascii=False) + file = discord.File(io.BytesIO(json_str.encode()), filename=f"managerx_data_{uid}.json") - # Moderationsdaten - container.add_text("### 🛡️ Moderationsdaten") - warn_count = len(warnings) if warnings else 0 - note_count = len(notes) if notes else 0 - - container.add_text(f"**Aktive Verwarnungen:** {warn_count}") - container.add_text(f"**Gespeicherte Notizen:** {note_count}") - - container.add_separator() - container.add_text("*Hinweis: ManagerX speichert nur Daten, die für die Bot-Funktionalitäten (Leveling, Moderation, Einstellungen) zwingend erforderlich sind. Du kannst deine persönlichen Daten jederzeit mit `/user data delete` löschen.*") + embed = discord.Embed( + title="📂 Dein Daten-Export", + description="Im Anhang findest du alle Daten, die mit deiner ID verknüpft sind.", + color=SUCCESS_COLOR + ) + embed.set_footer(text=FOOTER) + await ctx.respond(embed=embed, file=file, ephemeral=True) - view = discord.ui.DesignerView(container, timeout=0) - await ctx.respond(view=view, ephemeral=True) + # --- Daten-Löschung (DSGVO Art. 17) --- - @data.command( - name="delete", - description="Lösche alle deine Daten von ManagerX permanent." - ) + @data.command(name="delete", description="Lösche deine persönlichen Daten permanent.") async def delete_all_data(self, ctx: discord.ApplicationContext): - """Startet den doppelten Bestätigungsprozess zum Löschen aller User-Daten.""" - embed = discord.Embed( title=f"{emoji_warn} ACHTUNG: Datenlöschung", description=( - "Bist du sicher, dass du alle deine Daten löschen möchtest?\n\n" - "**Was gelöscht wird:**\n" - "• XP, Level und Statistiken (Global & Server)\n" - "• Deine persönlichen Einstellungen\n\n" - "**Was NICHT gelöscht wird:**\n" - "• Moderationsdaten (Warnungen & Notizen)\n" - "*Hinweis: ManagerX ist es nicht gestattet, Moderationsdaten zu löschen.*\n\n" - "⚠️ **WICHTIG:** Dieser Vorgang ist **unwiderruflich**. " - "Deine persönlichen Daten sind **für immer** weg!" + "Möchtest du deine persönlichen Daten wirklich löschen?\n\n" + "**Gelöscht wird:** Level, XP, Statistiken, Profile & Einstellungen.\n" + "**Bleibt bestehen:** Moderations-Daten (Warns) zum Schutz des Netzwerks (180 Tage).\n\n" + "Dieser Vorgang kann nicht rückgängig gemacht werden!" ), color=ERROR_COLOR ) @@ -228,30 +119,24 @@ async def delete_all_data(self, ctx: discord.ApplicationContext): view = DeletionView(ctx.author.id, self.bot) await ctx.respond(embed=embed, view=view, ephemeral=True) +# --- Views für den Löschprozess --- + class DeletionView(discord.ui.View): def __init__(self, user_id, bot): super().__init__(timeout=60) self.user_id = user_id self.bot = bot - @discord.ui.button(label="Daten löschen", style=discord.ButtonStyle.danger, emoji="🗑️") + @discord.ui.button(label="Fortfahren", style=discord.ButtonStyle.danger, emoji="🗑️") async def delete_button(self, button: discord.ui.Button, interaction: discord.Interaction): if interaction.user.id != self.user_id: - return await interaction.response.send_message("Das ist nicht dein Menü!", ephemeral=True) + return await interaction.response.send_message("Nicht dein Menü!", ephemeral=True) embed = discord.Embed( title="⚠️ LETZTE BESTÄTIGUNG", - description=( - "Bist du wirklich ABSOLUT sicher?\n\n" - "Alle deine Statistiken, Level und Einstellungen werden **permanent** gelöscht.\n" - "ManagerX wird alle persönlichen Informationen über dich vergessen.\n\n" - "**WICHTIG:** Moderationsdaten (Warns/Notes) dürfen vom Bot nicht gelöscht werden und bleiben erhalten." - ), + description="Willst du deine Stats und Profile wirklich unwiderruflich löschen?", color=ERROR_COLOR ) - embed.set_author(name=AUTHOR) - embed.set_footer(text=FOOTER) - view = DeletionConfirmationView(self.user_id, self.bot) await interaction.response.edit_message(embed=embed, view=view) @@ -264,64 +149,34 @@ def __init__(self, user_id, bot): @discord.ui.button(label="JA, ALLES LÖSCHEN", style=discord.ButtonStyle.danger, emoji="🔥") async def confirm_button(self, button: discord.ui.Button, interaction: discord.Interaction): if interaction.user.id != self.user_id: - return await interaction.response.send_message("Das ist nicht dein Menü!", ephemeral=True) + return await interaction.response.send_message("Nicht dein Menü!", ephemeral=True) - # Deletion logic implementation + uid = self.user_id try: - # Paths to databases - stats_db_path = "data/stats.db" - level_db_path = "data/levelsystem.db" - warn_db_path = "src/bot/cogs/moderation/Datenbanken/warns.db" - notes_db_path = "data/data/notes.db" - - # 1. Stats & Level - # StatsDB cleanup - if os.path.exists(stats_db_path): - conn = sqlite3.connect(stats_db_path) - cursor = conn.cursor() - tables = ["messages", "voice_sessions", "global_user_levels", "daily_stats", "user_achievements", "active_voice_sessions"] - for table in tables: - cursor.execute(f"DELETE FROM {table} WHERE user_id = ?", (self.user_id,)) - conn.commit() - conn.close() - - # LevelDatabase cleanup - if os.path.exists(level_db_path): - conn = sqlite3.connect(level_db_path) - cursor = conn.cursor() - tables = ["user_levels", "xp_boosts", "achievements", "temporary_roles"] - for table in tables: - cursor.execute(f"DELETE FROM {table} WHERE user_id = ?", (self.user_id,)) - conn.commit() - conn.close() - - # 2. Settings (using the existing reset method if available) - if hasattr(self.bot, 'settings_db'): - if hasattr(self.bot.settings_db, 'reset_user_settings'): - self.bot.settings_db.reset_user_settings(self.user_id) - elif hasattr(self.bot.settings_db, 'delete_user_data'): # Fallback to original code's preference - self.bot.settings_db.delete_user_data(self.user_id) - - # 3. Moderationsdaten (Warns & Notes) bleiben bestehen (User-Wunsch/Wichtigkeit) - pass + # Löschung der persönlichen Daten + await StatsDB().delete_user_data(uid) + LevelDatabase().delete_user_data(uid) + ProfileDB().delete_user_data(uid) + SettingsDB().delete_user_data(uid) + global_db.delete_user_data(uid) + AntiSpamDatabase().delete_user_data(uid) + TempVCDatabase().delete_user_data(uid) + + # Moderation (Warns/Notes) wird hier NICHT gelöscht! except Exception as e: - # Log error but proceed with message - print(f"Error during manual data deletion for {self.user_id}: {e}") + print(f"Löschfehler (User {uid}): {e}") embed = discord.Embed( - title="✅ Daten erfolgreich gelöscht", - description="Alle deine Daten wurden permanent aus unserem System entfernt.", + title="✅ Löschung erfolgreich", + description="Deine persönlichen Daten wurden entfernt. Warnungen bleiben systembedingt erhalten.", color=SUCCESS_COLOR ) - embed.set_footer(text=FOOTER) - await interaction.response.edit_message(embed=embed, view=None) @discord.ui.button(label="Abbrechen", style=discord.ButtonStyle.secondary) async def cancel_button(self, button: discord.ui.Button, interaction: discord.Interaction): - await interaction.response.edit_message(content="Vorgang abgebrochen. Deine Daten sind sicher.", embed=None, view=None) + await interaction.response.edit_message(content="Abgebrochen.", embed=None, view=None) 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/bot/cogs/user/stats.py b/src/bot/cogs/user/stats.py index 3c638f3..beedeea 100644 --- a/src/bot/cogs/user/stats.py +++ b/src/bot/cogs/user/stats.py @@ -4,7 +4,7 @@ from discord import SlashCommandGroup import logging from typing import Optional -from DevTools import StatsDB +from mx_devtools import StatsDB import asyncio from datetime import datetime, timedelta import math @@ -23,6 +23,7 @@ def __init__(self, bot: commands.Bot): self.bot = bot self.db = StatsDB() self.cleanup_task.start() + self.monthly_reset_task.start() logger.info("Enhanced StatsCog initialized") stats = SlashCommandGroup("stats", "Statistiken") @@ -31,18 +32,28 @@ def __init__(self, bot: commands.Bot): def cog_unload(self): """Called when the cog is unloaded.""" self.cleanup_task.cancel() + self.monthly_reset_task.cancel() self.db.close() logger.info("Enhanced StatsCog unloaded") @tasks.loop(hours=24) async def cleanup_task(self): - """Daily cleanup of old data.""" - await self.db.cleanup_old_data(days=90) + """Daily rolling cleanup – removes raw event data older than 30 days.""" + await self.db.cleanup_old_data(days=30) @cleanup_task.before_loop async def before_cleanup(self): await self.bot.wait_until_ready() + @tasks.loop(hours=24) + async def monthly_reset_task(self): + """Season reset – wipes messages & voice_sessions on the 1st of each month.""" + await self.db.monthly_season_reset() + + @monthly_reset_task.before_loop + async def before_monthly_reset(self): + await self.bot.wait_until_ready() + @commands.Cog.listener() async def on_ready(self): """Called when the bot is ready and connected to Discord.""" @@ -336,13 +347,13 @@ async def leaderboard_command( try: if typ == "global": - leaderboard_data = await self.db.get_leaderboard(limit) + leaderboard_data = await self.db.get_leaderboard(limit, bot=self.bot) title = "🌍 Globale Rangliste" description = "Top User nach globalem Level & XP" else: - leaderboard_data = await self.db.get_leaderboard(limit, ctx.guild.id) + leaderboard_data = await self.db.get_leaderboard(limit, ctx.guild.id, bot=self.bot) title = f"🏢 {ctx.guild.name} Rangliste" - description = "Top User der letzten 30 Tage" + description = "Top User dieser Saison (letzten 30 Tage)" if not leaderboard_data: embed = discord.Embed( @@ -543,9 +554,11 @@ async def stats_info_command(self, ctx: discord.ApplicationContext): ) embed.add_field( - name="🔒 Datenschutz", + name="🔒 Datenschutz (Privacy-First)", value="• Nur Metadaten werden gespeichert (keine Inhalte)\n" - "• Automatische Bereinigung alter Daten nach 90 Tagen\n" + "• **Monatlicher Reset:** Leaderboard startet jeden Monat neu\n" + "• **30-Tage-Cleanup:** Rohdaten werden nach 30 Tagen gelöscht\n" + "• **Anonyme Tagesstatistiken:** Keine Verknüpfung zu einzelnen Usern\n" "• [Vollständige Datenschutzerklärung](https://medicopter117.github.io/ManagerX-Web/privacy.html)", inline=False ) diff --git a/src/bot/core/bot_setup.py b/src/bot/core/bot_setup.py index 6163f2f..85df90a 100644 --- a/src/bot/core/bot_setup.py +++ b/src/bot/core/bot_setup.py @@ -29,9 +29,11 @@ def create_bot(self) -> ezcord.Bot: intents.presences = True # Bot erstellen - bot = ezcord.Bot( + bot = ezcord.PrefixBot( intents=intents, - language="de" + language="de", + command_prefix="!mx ", + help_command=None ) # Ezcord Help Command aktivieren diff --git a/src/bot/core/constants.py b/src/bot/core/constants.py index 04d4df3..9652350 100644 --- a/src/bot/core/constants.py +++ b/src/bot/core/constants.py @@ -1,38 +1,47 @@ -""" -ManagerX - Constants -==================== +import subprocess +import re +import os -Zentrale Definition von Emojis, Farben und anderen Konstanten. -""" +# --- TEIL A: Die Logik für den Bot-Betrieb --- -import discord +def get_current_version(): + """Liest die Version, die aktuell in der pyproject.toml steht.""" + try: + # Sucht die pyproject.toml im Hauptverzeichnis + with open("pyproject.toml", "r", encoding="utf-8") as f: + for line in f: + if line.strip().startswith("version ="): + # Extrahiert den String zwischen den Anführungszeichen + return line.split('"')[1] + except: + return "2.0.0-unknown" -# --- Farben --- -SUCCESS_COLOR = discord.Color.green() -ERROR_COLOR = discord.Color.red() -WARN_COLOR = discord.Color.orange() -INFO_COLOR = discord.Color.blue() +# Diese Variable nutzt du überall in deinem Bot (z.B. settings.py) +MANAGERX_VERSION = get_current_version() -# --- Emojis (Standard-Sets) --- -# Hinweis: Diese können später durch Custom-Emojis ersetzt werden -emoji_yes = "✅" -emoji_no = "❌" -emoji_warn = "⚠️" -emoji_info = "ℹ️" -emoji_forbidden = "🚫" -emoji_member = "👤" -emoji_staff = "🛡️" -emoji_summary = "📝" -emoji_slowmode = "⏳" -emoji_channel = "📁" -emoji_moderator = "👮" -emoji_statistics = "📊" -emoji_annoattention = "📣" -emoji_owner = "👑" -emoji_delete = "🗑️" -emoji_circleinfo = "ℹ️" -# --- Texte --- -AUTHOR = "ManagerX Network" -FLOOTER = "Powered by ManagerX Development" # Typo im Original ("FLOOTER"), behalte es für Kompatibilität oder korrigiere es -FOOTER = "Powered by ManagerX Development" +# --- TEIL B: Die Logik zum "Stempeln" (Nur wenn man die Datei direkt ausführt) --- + +def update_pyproject_version(): + try: + # 1. Den neuen Hash von Git holen + git_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip() + new_version = f"2.0.0+build{git_hash}" + + # 2. In die pyproject.toml schreiben + with open("pyproject.toml", "r", encoding="utf-8") as f: + content = f.read() + + # Ersetzt die alte Versionszeile durch die neue + updated_content = re.sub(r'version\s*=\s*".*?"', f'version = "{new_version}"', content) + + with open("pyproject.toml", "w", encoding="utf-8") as f: + f.write(updated_content) + + print(f"✅ pyproject.toml wurde auf {new_version} aktualisiert!") + except Exception as e: + print(f"❌ Fehler: {e}") + +if __name__ == "__main__": + # Dieser Teil läuft NUR, wenn du 'python src/bot/core/constants.py' tippst + update_pyproject_version() \ No newline at end of file diff --git a/src/bot/core/database.py b/src/bot/core/database.py index 397739a..6050f3c 100644 --- a/src/bot/core/database.py +++ b/src/bot/core/database.py @@ -9,7 +9,7 @@ from logger import logger, Category try: - from DevTools import SettingsDB + from mx_devtools import SettingsDB except ImportError as e: logger.critical(Category.DATABASE, f"SettingsDB Import fehlgeschlagen: {e}") SettingsDB = None diff --git a/src/web/App.tsx b/src/web/App.tsx index ef206d3..ffb9f5f 100644 --- a/src/web/App.tsx +++ b/src/web/App.tsx @@ -11,6 +11,7 @@ const Datenschutz = lazy(() => import("./pages/Datenschutz")); const Nutzungsbedingungen = lazy(() => import("./pages/Nutzungsbedingungen")); const PluginsPage = lazy(() => import("./pages/PluginsPage")); const Status = lazy(() => import("./pages/Status")); +const CommandsPage = lazy(() => import("./pages/CommandsPage")); const License = lazy(() => import("./pages/License").then(module => ({ default: module.License }))); const queryClient = new QueryClient(); @@ -63,6 +64,7 @@ const AppContent = () => { } /> } /> } /> + } /> } /> } /> diff --git a/src/web/components/CTA.tsx b/src/web/components/CTA.tsx index d65bd6f..00719f1 100644 --- a/src/web/components/CTA.tsx +++ b/src/web/components/CTA.tsx @@ -1,15 +1,17 @@ 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: "10+" }, - { label: "Befehle ausgeführt", value: "1000+" }, - { label: "Zufriedene User", value: "300+" }, -]; +import { useStats } from "@/hooks/useStats"; export const CTA = memo(function CTA() { + const { data, isLoading } = useStats(); + + const stats = [ + { label: "Aktive Server", value: isLoading ? "..." : `${data.guilds}` }, + { label: "Befehle", value: "90+" }, + { label: "Zufriedene User", value: isLoading ? "..." : `${data.users}` }, + ]; + return (
{/* Premium Background */} diff --git a/src/web/components/Footer.tsx b/src/web/components/Footer.tsx index 5a081bc..890ff73 100644 --- a/src/web/components/Footer.tsx +++ b/src/web/components/Footer.tsx @@ -1,7 +1,7 @@ import { memo } from "react"; import { Link } from "react-router-dom"; import { motion } from "framer-motion"; -import { Shield, Heart, Github, MessageCircle, ExternalLink, Terminal, Sparkles, Code2, Zap, Users, Rocket, Star, BarChart3, Lock, Info, FileCheck, Activity } from "lucide-react"; +import { Shield, Heart, Github, MessageCircle, ExternalLink, Terminal, Sparkles, Code2, Zap, Users, Rocket, Star, BarChart3, Lock, Info, FileCheck, Activity, LayoutGrid } from "lucide-react"; const socialLinks = [ { icon: Github, href: "https://github.com/ManagerX-Development/ManagerX", label: "GitHub" }, @@ -125,6 +125,10 @@ export const Footer = memo(function Footer() { Plugin System + + + Befehle + {/* column 2 */} diff --git a/src/web/components/Hero.tsx b/src/web/components/Hero.tsx index 00ca6f6..72139b9 100644 --- a/src/web/components/Hero.tsx +++ b/src/web/components/Hero.tsx @@ -1,16 +1,10 @@ import { memo } from "react"; import { motion } from "framer-motion"; import { Link } from "react-router-dom"; -import { Button } from "@/components/ui/button"; import { Shield, Users, MessageCircle, Sparkles, Zap, Rocket, Code2, ArrowRight } from "lucide-react"; +import { useStats } from "@/hooks/useStats"; -const stats = [ - { label: "Server", value: "10+", 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 }) => ( +const StatCard = memo(({ stat, index }: { stat: { label: string; value: string; icon: any }; index: number }) => ( {/* Premium Animated Background */} diff --git a/src/web/components/Navbar.tsx b/src/web/components/Navbar.tsx index 2c8ed1c..f56023b 100644 --- a/src/web/components/Navbar.tsx +++ b/src/web/components/Navbar.tsx @@ -1,15 +1,15 @@ import { useState, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Link, useLocation } from "react-router-dom"; -import { Button } from "@/components/ui/button"; import { - Shield, Menu, X, Sparkles, Puzzle, Activity, + Shield, Menu, X, Sparkles, Puzzle, Activity, Terminal, Newspaper // Icon für den Blog hinzugefügt } from "lucide-react"; -import { cn } from "@/lib/utils"; +import { cn } from "../lib/utils"; const navLinks = [ { label: "Features", href: "/#features", icon: Sparkles }, + { label: "Commands", href: "/commands", icon: Terminal }, { label: "Plugins", href: "/plugins", icon: Puzzle }, { label: "Status", href: "/status", icon: Activity }, ]; diff --git a/src/web/hooks/useStats.ts b/src/web/hooks/useStats.ts new file mode 100644 index 0000000..f6186d0 --- /dev/null +++ b/src/web/hooks/useStats.ts @@ -0,0 +1,59 @@ +import { useState, useEffect } from "react"; + +interface StatsData { + uptime: string; + latency: string; + guilds: number; + users: number; + bot_name: string; + bot_id: number | null; + status: string; + database: string; +} + +export const useStats = () => { + const [data, setData] = useState({ + uptime: "--", + latency: "--", + guilds: 0, + users: 0, + bot_name: "ManagerX", + bot_id: null, + status: "loading", + database: "disconnected" + }); + + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchStats = async () => { + try { + const response = await fetch("https://api.managerx-bot.de/v1/managerx/stats"); + if (!response.ok) throw new Error("Offline"); + + const result = await response.json(); + setData({ + uptime: result.uptime || "--", + latency: result.latency || "--", + guilds: result.guilds || 0, + users: result.users || 0, + bot_name: result.bot_name || "ManagerX", + bot_id: result.bot_id || null, + status: result.status || "online", + database: result.database || "disconnected" + }); + setIsLoading(false); + } catch (error) { + setData((prev) => ({ ...prev, status: "offline", latency: "--", database: "disconnected" })); + setIsLoading(false); + } + }; + + fetchStats(); + // Optional: Alle 30 Sekunden aktualisieren + const interval = setInterval(fetchStats, 30000); + return () => clearInterval(interval); + }, []); + + return { data, isLoading }; +}; diff --git a/src/web/pages/CommandsPage.tsx b/src/web/pages/CommandsPage.tsx new file mode 100644 index 0000000..d65a0d8 --- /dev/null +++ b/src/web/pages/CommandsPage.tsx @@ -0,0 +1,222 @@ +import { memo, useState, useMemo } from "react"; +import { + Search, ChevronRight, Terminal, Info, Filter, Sparkles +} from "lucide-react"; +import { Navbar } from "../components/Navbar"; +import { Footer } from "../components/Footer"; +import { motion, AnimatePresence } from "framer-motion"; +import { Input } from "../components/ui/input"; +import { Badge } from "../components/ui/badge"; +import { cn } from "../lib/utils"; +import { CATEGORIES, COMMANDS, Command } from "../data/commands"; + +const CommandCard = ({ command }: { command: Command }) => ( + +
+ +
+ +
+ {command.badges.map((badge, i) => ( + + {badge} + + ))} +
+ +

+ / + {command.name} +

+ +

+ {command.description} +

+ +
+
Usage
+ + {command.usage} + +
+
+); + +export const CommandsPage = memo(function CommandsPage() { + const [search, setSearch] = useState(""); + const [activeCategory, setActiveCategory] = useState("all"); + + const filteredCommands = useMemo(() => { + return COMMANDS.filter(cmd => { + const matchesSearch = cmd.name.toLowerCase().includes(search.toLowerCase()) || + cmd.description.toLowerCase().includes(search.toLowerCase()); + const matchesCategory = activeCategory === "all" || cmd.category === activeCategory; + return matchesSearch && matchesCategory; + }); + }, [search, activeCategory]); + + const groupedCommands = useMemo(() => { + const groups: { [key: string]: Command[] } = {}; + filteredCommands.forEach(cmd => { + if (!groups[cmd.category]) groups[cmd.category] = []; + groups[cmd.category].push(cmd); + }); + return groups; + }, [filteredCommands]); + + const visibleCategories = useMemo(() => { + return CATEGORIES.filter(cat => cat.id !== "all" && groupedCommands[cat.id]); + }, [groupedCommands]); + + return ( +
+ + +
+ {/* Header Section */} +
+ + + Command Reference + +

+ Master the Commands +

+

+ Entdecke alle verfügbaren Slash-Commands für ManagerX. Von Moderation bis hin zu AI-Games – alles auf einen Blick. +

+ + {/* Search Bar */} +
+
+
+ + + + setSearch(e.target.value)} + className="w-full h-16 pl-16 pr-6 bg-[#111318] border-white/10 rounded-[1.5rem] focus:ring-primary focus:border-primary text-white text-lg font-bold placeholder:text-slate-600 transition-all shadow-2xl" + /> +
+
+
+ + {/* Category Filters */} +
+ {CATEGORIES.map((cat) => { + const Icon = cat.icon; + return ( + + ); + })} +
+ + {/* Grouped Commands Content */} +
+ + {visibleCategories.length > 0 ? ( + visibleCategories.map((cat) => { + const Icon = cat.icon; + return ( + + {/* Category Title Header */} +
+
+ +
+
+

{cat.title}

+
+
+
+
+ + {/* Commands Grid and Category */} +
+ {groupedCommands[cat.id].map((command) => ( + + ))} +
+ + ); + }) + ) : ( + + +

Keine Befehle gefunden

+

Versuch es mit einem anderen Suchbegriff oder einer anderen Kategorie.

+
+ )} + +
+ + {/* Help Banner */} + +
+ +

Brauchst du mehr Hilfe?

+

+ Nutze /help direkt in Discord für detaillierte Anleitungen zu jedem einzelnen Befehl. +

+ + +
+ +
+ ); +}); + +export default CommandsPage; diff --git a/vite.config.ts b/vite.config.ts index 704e517..7c10f20 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,8 +21,7 @@ export default defineConfig(({ mode }) => ({ publicDir: "../../public", resolve: { alias: { - // Dein Alias zeigt auf . (jetzt src/web) - "@": path.resolve(__dirname, "src/web"), + "@": path.resolve(__dirname, "./src/web"), }, }, build: {