diff --git a/.replit b/.replit index 0f53797..cacbbdd 100644 --- a/.replit +++ b/.replit @@ -1,5 +1,5 @@ entrypoint = "main.py" -modules = ["python-3.10", "postgresql-16"] +modules = ["python-3.10", "postgresql-16", "nodejs-20"] hidden = [".pythonlibs"] diff --git a/sentinel-bot/.env.example b/sentinel-bot/.env.example new file mode 100644 index 0000000..d372f54 --- /dev/null +++ b/sentinel-bot/.env.example @@ -0,0 +1,21 @@ +# Discord Bot Configuration +DISCORD_TOKEN=your_bot_token_here + +# Federation Guild IDs +HUB_ID=your_hub_server_id +FORGE_ID=your_gameforge_server_id +FOUNDATION_ID=your_foundation_server_id +LABS_ID=your_labs_server_id +CORP_ID=your_corp_server_id + +# Security Configuration +WHITELISTED_USERS=user_id_1,user_id_2 + +# Dashboard Configuration +STATUS_CHANNEL_ID=voice_channel_id_for_status + +# Health Check +HEALTH_PORT=8044 + +# Database (auto-provided by Replit) +# DATABASE_URL=postgresql://... diff --git a/sentinel-bot/package-lock.json b/sentinel-bot/package-lock.json new file mode 100644 index 0000000..3a644bf --- /dev/null +++ b/sentinel-bot/package-lock.json @@ -0,0 +1,1097 @@ +{ + "name": "aethex-sentinel", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aethex-sentinel", + "version": "1.0.0", + "dependencies": { + "@prisma/client": "^5.22.0", + "@sapphire/framework": "^5.2.1", + "@sapphire/plugin-scheduled-tasks": "^10.0.1", + "discord.js": "^14.16.3", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/node": "^20.17.6", + "prisma": "^5.22.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz", + "integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.33", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/discord-utilities": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/discord-utilities/-/discord-utilities-4.0.0.tgz", + "integrity": "sha512-QAvrKNHgswz+ZX48WqSYpRiRzQcugNXXB1C3fR1qbpTJGd7Ckr2OWyFK88TyOksi3U2isrk8sMriTcAgaIe7Qg==", + "license": "MIT", + "dependencies": { + "discord-api-types": "^0.38.30" + }, + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/discord.js-utilities": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@sapphire/discord.js-utilities/-/discord.js-utilities-7.3.3.tgz", + "integrity": "sha512-WDj+zjWgNCUSvzYDD0wY3TVeTUseHq0Nhk0wVWxSDjY8z2gFEVcpY7wF8/fbTDWP44LUG5sUQ4haIrIj2OjmkQ==", + "license": "MIT", + "dependencies": { + "@sapphire/discord-utilities": "^3.5.0", + "@sapphire/duration": "^1.2.0", + "@sapphire/utilities": "^3.18.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.6.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/discord.js-utilities/node_modules/@sapphire/discord-utilities": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sapphire/discord-utilities/-/discord-utilities-3.5.0.tgz", + "integrity": "sha512-H4SY5KTVDZrqA5QG7ob6etwqhdOb3TRSY2wv56f0tiobUdIr0irlrYvdmr8Kg/FRxWU+aiHDIISWGG5vBuxOGw==", + "license": "MIT", + "dependencies": { + "discord-api-types": "^0.38.1" + }, + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/duration": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@sapphire/duration/-/duration-1.2.0.tgz", + "integrity": "sha512-LxjOAFXz81WmrI8XX9YaVcAZDjQj/1p78lZCvkAWZB1nphOwz/D0dU3CBejmhOWx5dO5CszTkLJMNR0xuCK+Zg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/framework": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@sapphire/framework/-/framework-5.4.0.tgz", + "integrity": "sha512-Gf0Ui5tOG1lRRf6Gpd/gpUDkAIMZxFXYOzq0X5kMcXRNWiX1B6iIUkyXrzCLaNunzqWRKe6x4UxzRewN5ktNWg==", + "license": "MIT", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@sapphire/discord-utilities": "^4.0.0", + "@sapphire/discord.js-utilities": "^7.3.3", + "@sapphire/lexure": "^1.1.12", + "@sapphire/pieces": "^4.4.1", + "@sapphire/ratelimits": "^2.4.11", + "@sapphire/result": "^2.8.0", + "@sapphire/stopwatch": "^1.5.4", + "@sapphire/utilities": "^3.18.2" + }, + "engines": { + "node": ">=v18", + "npm": ">=7" + } + }, + "node_modules/@sapphire/lexure": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@sapphire/lexure/-/lexure-1.1.12.tgz", + "integrity": "sha512-F7Z3QzRnAZGunRl24/qQMhzRogZU/foumu2EBBunRnQi/o/DLTCwdAbLgJATyPlvJa8N6FrJq0JJwvzM/vXoXg==", + "license": "MIT", + "dependencies": { + "@sapphire/result": "^2.8.0" + }, + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/pieces": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@sapphire/pieces/-/pieces-4.4.1.tgz", + "integrity": "sha512-2v49P++RzHGb23bdjSa9u7flkdvhrq94IUO9PneY538TILN5QiMxfaXVK+pDR2YR7jpQYBy9APwtFDes2Svykg==", + "license": "MIT", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@sapphire/utilities": "^3.18.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/plugin-scheduled-tasks": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@sapphire/plugin-scheduled-tasks/-/plugin-scheduled-tasks-10.0.4.tgz", + "integrity": "sha512-H0Z9QgDavLDK38GVyKfMDsyruOA4NHSyhNk/LCzewCDqc1d5Nxvoo5PMTB19NvadO9RFvgnPqZk7g4k3HzvAvQ==", + "license": "MIT", + "dependencies": { + "@sapphire/stopwatch": "^1.5.4", + "@sapphire/utilities": "^3.18.2", + "bullmq": "5.58.7" + }, + "engines": { + "node": ">=v18", + "npm": ">=7" + } + }, + "node_modules/@sapphire/ratelimits": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@sapphire/ratelimits/-/ratelimits-2.4.11.tgz", + "integrity": "sha512-O6FNA/P0wxU4Ve9gxL948CoZw7+sSpujyUR2CLyLLCNuNvuFGFxPCJVl5crFVLXMIyBIrc2qk+/H9bsqsyQK1Q==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/result": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@sapphire/result/-/result-2.8.0.tgz", + "integrity": "sha512-693yWouX+hR9uJm1Jgq0uSSjbSD3UrblMaxiuGbHPjSwzLCSZTcm0h3kvdVhq3o/yl4+oeAWW3hiaJ0TELuRJQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/stopwatch": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@sapphire/stopwatch/-/stopwatch-1.5.4.tgz", + "integrity": "sha512-IVI48D2yAz411bSttXyTkBH0p2vhrXoqWLn5loDDSAAEUGkM1r5KNCX2027ifQ8svdoMkUfIGjFueR+satLeWw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/utilities": { + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/@sapphire/utilities/-/utilities-3.18.2.tgz", + "integrity": "sha512-QGLdC9+pT74Zd7aaObqn0EUfq40c4dyTL65pFnkM6WO1QYN7Yg/s4CdH+CXmx0Zcu6wcfCWILSftXPMosJHP5A==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/bullmq": { + "version": "5.58.7", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.58.7.tgz", + "integrity": "sha512-rqsKV/ip76wU90q7Cxpr1vS/6PYIVbhuzqr3wgILgjS6XbsnJtWyYrK23jqWHs9+m6/NXM4+62hyf8CSBpufAw==", + "license": "MIT", + "dependencies": { + "cron-parser": "^4.9.0", + "ioredis": "^5.4.1", + "msgpackr": "^1.11.2", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^11.1.0" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.36", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.36.tgz", + "integrity": "sha512-qrbUbjjwtyeBg5HsAlm1C859epfOyiLjPqAOzkdWlCNsZCWJrertnETF/NwM8H+waMFU58xGSc5eXUfXah+WTQ==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.25.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz", + "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.33", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/discord.js/node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/ioredis": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.4.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-bytes.js": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/sentinel-bot/package.json b/sentinel-bot/package.json new file mode 100644 index 0000000..47eb34b --- /dev/null +++ b/sentinel-bot/package.json @@ -0,0 +1,27 @@ +{ + "name": "aethex-sentinel", + "version": "1.0.0", + "description": "Enterprise-grade Discord bot for managing a federation of servers", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:studio": "prisma studio" + }, + "dependencies": { + "@prisma/client": "^5.22.0", + "@sapphire/framework": "^5.2.1", + "@sapphire/plugin-scheduled-tasks": "^10.0.1", + "discord.js": "^14.16.3", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/node": "^20.17.6", + "prisma": "^5.22.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } +} diff --git a/sentinel-bot/prisma/schema.prisma b/sentinel-bot/prisma/schema.prisma new file mode 100644 index 0000000..79a6647 --- /dev/null +++ b/sentinel-bot/prisma/schema.prisma @@ -0,0 +1,65 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id + heatLevel Int @default(0) + balance Float @default(0.0) + roles String[] + lastHeatReset DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + tickets Ticket[] + heatEvents HeatEvent[] +} + +model HeatEvent { + id Int @id @default(autoincrement()) + userId String + action String + timestamp DateTime @default(now()) + user User @relation(fields: [userId], references: [id]) + + @@index([userId, timestamp]) +} + +model Ticket { + id Int @id @default(autoincrement()) + threadId String @unique + userId String + type String + status String @default("open") + transcript String? + createdAt DateTime @default(now()) + closedAt DateTime? + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) +} + +model GuildConfig { + id String @id + name String + type String + statusChannelId String? + feedChannelId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model RoleMapping { + id Int @id @default(autoincrement()) + sourceGuild String + sourceRole String + targetGuild String + targetRole String + roleName String + + @@unique([sourceGuild, sourceRole, targetGuild]) +} diff --git a/sentinel-bot/src/commands/federation.ts b/sentinel-bot/src/commands/federation.ts new file mode 100644 index 0000000..f16fbf1 --- /dev/null +++ b/sentinel-bot/src/commands/federation.ts @@ -0,0 +1,102 @@ +import { Command } from '@sapphire/framework'; +import { PermissionFlagsBits } from 'discord.js'; +import { federationManager } from '../modules/federation/FederationManager'; +import { prisma } from '../core/client'; + +export class FederationCommand extends Command { + public constructor(context: Command.LoaderContext, options: Command.Options) { + super(context, { + ...options, + name: 'federation', + description: 'Manage federation role mappings', + requiredUserPermissions: [PermissionFlagsBits.Administrator], + }); + } + + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + .addSubcommand((sub) => + sub + .setName('add-mapping') + .setDescription('Add a role mapping between guilds') + .addStringOption((opt) => + opt.setName('source-guild').setDescription('Source guild ID').setRequired(true) + ) + .addStringOption((opt) => + opt.setName('source-role').setDescription('Source role ID').setRequired(true) + ) + .addStringOption((opt) => + opt.setName('target-guild').setDescription('Target guild ID').setRequired(true) + ) + .addStringOption((opt) => + opt.setName('target-role').setDescription('Target role ID').setRequired(true) + ) + .addStringOption((opt) => + opt.setName('role-name').setDescription('Friendly name for this mapping').setRequired(true) + ) + ) + .addSubcommand((sub) => + sub.setName('list').setDescription('List all role mappings') + ) + .addSubcommand((sub) => + sub.setName('reload').setDescription('Reload mappings from database') + ) + ); + } + + public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === 'add-mapping') { + const sourceGuild = interaction.options.getString('source-guild', true); + const sourceRole = interaction.options.getString('source-role', true); + const targetGuild = interaction.options.getString('target-guild', true); + const targetRole = interaction.options.getString('target-role', true); + const roleName = interaction.options.getString('role-name', true); + + await federationManager.addMapping({ + sourceGuild, + sourceRole, + targetGuild, + targetRole, + roleName, + }); + + return interaction.reply({ + content: `✅ Added mapping: "${roleName}" from ${sourceGuild} to ${targetGuild}`, + ephemeral: true, + }); + } + + if (subcommand === 'list') { + const mappings = await prisma.roleMapping.findMany(); + + if (mappings.length === 0) { + return interaction.reply({ + content: 'No role mappings configured.', + ephemeral: true, + }); + } + + const list = mappings + .map((m) => `• **${m.roleName}**: ${m.sourceGuild}:${m.sourceRole} → ${m.targetGuild}:${m.targetRole}`) + .join('\n'); + + return interaction.reply({ + content: `**Federation Role Mappings:**\n${list}`, + ephemeral: true, + }); + } + + if (subcommand === 'reload') { + await federationManager.loadMappings(); + return interaction.reply({ + content: '✅ Reloaded role mappings from database.', + ephemeral: true, + }); + } + } +} diff --git a/sentinel-bot/src/commands/sentinel.ts b/sentinel-bot/src/commands/sentinel.ts new file mode 100644 index 0000000..007597a --- /dev/null +++ b/sentinel-bot/src/commands/sentinel.ts @@ -0,0 +1,84 @@ +import { Command } from '@sapphire/framework'; +import { PermissionFlagsBits, EmbedBuilder } from 'discord.js'; +import { heatSystem } from '../modules/security/HeatSystem'; +import { config } from '../core/config'; + +export class SentinelCommand extends Command { + public constructor(context: Command.LoaderContext, options: Command.Options) { + super(context, { + ...options, + name: 'sentinel', + description: 'Manage the Sentinel security system', + requiredUserPermissions: [PermissionFlagsBits.Administrator], + }); + } + + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + .addSubcommand((sub) => + sub.setName('status').setDescription('View Sentinel security status') + ) + .addSubcommand((sub) => + sub + .setName('clear-heat') + .setDescription('Clear heat level for a user') + .addUserOption((opt) => + opt.setName('user').setDescription('User to clear heat for').setRequired(true) + ) + ) + .addSubcommand((sub) => + sub.setName('unlock').setDescription('Lift lockdown mode on this server') + ) + ); + } + + public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === 'status') { + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle('🛡️ Sentinel Security Status') + .addFields( + { name: 'Heat Threshold', value: `${config.security.heatThreshold} actions`, inline: true }, + { name: 'Window', value: `${config.security.heatWindowMs}ms`, inline: true }, + { name: 'Whitelisted Users', value: `${config.security.whitelistedUsers.length}`, inline: true }, + { + name: 'Monitored Actions', + value: config.security.dangerousActions.join(', '), + inline: false, + } + ) + .setTimestamp(); + + return interaction.reply({ embeds: [embed], ephemeral: true }); + } + + if (subcommand === 'clear-heat') { + const user = interaction.options.getUser('user', true); + heatSystem.clearHeat(user.id); + return interaction.reply({ + content: `✅ Cleared heat for ${user.tag}`, + ephemeral: true, + }); + } + + if (subcommand === 'unlock') { + if (!interaction.guild) { + return interaction.reply({ + content: 'This command must be used in a server.', + ephemeral: true, + }); + } + + await heatSystem.unlockGuild(interaction.guild); + return interaction.reply({ + content: '🔓 Lockdown lifted. @everyone permissions restored.', + ephemeral: true, + }); + } + } +} diff --git a/sentinel-bot/src/commands/status.ts b/sentinel-bot/src/commands/status.ts new file mode 100644 index 0000000..67f01b9 --- /dev/null +++ b/sentinel-bot/src/commands/status.ts @@ -0,0 +1,55 @@ +import { Command } from '@sapphire/framework'; +import { EmbedBuilder } from 'discord.js'; +import { config } from '../core/config'; + +export class StatusCommand extends Command { + public constructor(context: Command.LoaderContext, options: Command.Options) { + super(context, { + ...options, + name: 'status', + description: 'View Sentinel bot and network status', + }); + } + + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand((builder) => + builder.setName(this.name).setDescription(this.description) + ); + } + + public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + const client = this.container.client; + + const guildIds = Object.values(config.guilds).filter(id => id); + const guilds = guildIds + .map(id => client.guilds.cache.get(id)) + .filter(g => g !== undefined); + + const totalMembers = guilds.reduce((sum, g) => sum + g!.memberCount, 0); + + const guildList = guilds + .map(g => `• **${g!.name}**: ${g!.memberCount.toLocaleString()} members`) + .join('\n') || 'No federation guilds connected'; + + const uptimeSeconds = Math.floor(process.uptime()); + const hours = Math.floor(uptimeSeconds / 3600); + const minutes = Math.floor((uptimeSeconds % 3600) / 60); + const seconds = uptimeSeconds % 60; + const uptimeStr = `${hours}h ${minutes}m ${seconds}s`; + + const embed = new EmbedBuilder() + .setColor(0x5865f2) + .setTitle('🛡️ Aethex Sentinel Status') + .setThumbnail(client.user?.displayAvatarURL() || null) + .addFields( + { name: '📡 Network', value: `${guilds.length} guilds`, inline: true }, + { name: '👥 Total Members', value: totalMembers.toLocaleString(), inline: true }, + { name: '⏱️ Uptime', value: uptimeStr, inline: true }, + { name: '🏰 Federation Guilds', value: guildList, inline: false } + ) + .setFooter({ text: 'Aethex Sentinel • Protecting the Federation' }) + .setTimestamp(); + + return interaction.reply({ embeds: [embed] }); + } +} diff --git a/sentinel-bot/src/commands/ticket.ts b/sentinel-bot/src/commands/ticket.ts new file mode 100644 index 0000000..3b9765e --- /dev/null +++ b/sentinel-bot/src/commands/ticket.ts @@ -0,0 +1,76 @@ +import { Command } from '@sapphire/framework'; +import { PermissionFlagsBits, ChannelType, TextChannel } from 'discord.js'; +import { ticketManager } from '../modules/commerce/TicketManager'; +import { prisma } from '../core/client'; + +export class TicketCommand extends Command { + public constructor(context: Command.LoaderContext, options: Command.Options) { + super(context, { + ...options, + name: 'ticket', + description: 'Manage the ticket system', + requiredUserPermissions: [PermissionFlagsBits.Administrator], + }); + } + + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + .addSubcommand((sub) => + sub + .setName('setup') + .setDescription('Set up the ticket panel in this channel') + ) + .addSubcommand((sub) => + sub.setName('stats').setDescription('View ticket statistics') + ) + ); + } + + public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === 'setup') { + if (!interaction.channel || interaction.channel.type !== ChannelType.GuildText) { + return interaction.reply({ + content: 'This command must be used in a text channel.', + ephemeral: true, + }); + } + + await interaction.deferReply({ ephemeral: true }); + await ticketManager.createSupportPanel(interaction.channel as TextChannel); + + return interaction.editReply({ + content: '✅ Ticket panel created!', + }); + } + + if (subcommand === 'stats') { + const totalTickets = await prisma.ticket.count(); + const openTickets = await prisma.ticket.count({ where: { status: 'open' } }); + const closedTickets = await prisma.ticket.count({ where: { status: 'closed' } }); + + const byType = await prisma.ticket.groupBy({ + by: ['type'], + _count: { id: true }, + }); + + const typeStats = byType + .map((t) => `• ${t.type}: ${t._count.id}`) + .join('\n') || 'No tickets yet'; + + return interaction.reply({ + content: + `**🎫 Ticket Statistics**\n\n` + + `Total: ${totalTickets}\n` + + `Open: ${openTickets}\n` + + `Closed: ${closedTickets}\n\n` + + `**By Type:**\n${typeStats}`, + ephemeral: true, + }); + } + } +} diff --git a/sentinel-bot/src/core/client.ts b/sentinel-bot/src/core/client.ts new file mode 100644 index 0000000..abbd657 --- /dev/null +++ b/sentinel-bot/src/core/client.ts @@ -0,0 +1,37 @@ +import { SapphireClient } from '@sapphire/framework'; +import { GatewayIntentBits, Partials } from 'discord.js'; +import { PrismaClient } from '@prisma/client'; + +export const prisma = new PrismaClient(); + +export class SentinelClient extends SapphireClient { + public constructor() { + super({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildModeration, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.DirectMessages, + ], + partials: [ + Partials.Channel, + Partials.Message, + Partials.GuildMember, + ], + loadMessageCommandListeners: true, + }); + } + + public override async login(token?: string): Promise { + await prisma.$connect(); + console.log('✅ Database connected'); + return super.login(token); + } + + public override async destroy(): Promise { + await prisma.$disconnect(); + return super.destroy(); + } +} diff --git a/sentinel-bot/src/core/config.ts b/sentinel-bot/src/core/config.ts new file mode 100644 index 0000000..7ca8a26 --- /dev/null +++ b/sentinel-bot/src/core/config.ts @@ -0,0 +1,41 @@ +import 'dotenv/config'; + +export const config = { + token: process.env.DISCORD_TOKEN!, + + guilds: { + hub: process.env.HUB_ID!, + gameforge: process.env.FORGE_ID!, + foundation: process.env.FOUNDATION_ID!, + labs: process.env.LABS_ID!, + corp: process.env.CORP_ID!, + }, + + security: { + heatThreshold: 3, + heatWindowMs: 5000, + dangerousActions: ['CHANNEL_DELETE', 'BAN_ADD', 'KICK', 'ROLE_DELETE', 'WEBHOOK_DELETE'], + whitelistedUsers: process.env.WHITELISTED_USERS?.split(',') || [], + }, + + dashboard: { + updateIntervalMs: 5 * 60 * 1000, + statusChannelId: process.env.STATUS_CHANNEL_ID, + }, + + health: { + port: parseInt(process.env.HEALTH_PORT || '8044'), + }, +}; + +export function validateConfig(): void { + const required = ['DISCORD_TOKEN']; + const missing = required.filter(key => !process.env[key]); + + if (missing.length > 0) { + console.error('❌ Missing required environment variables:', missing.join(', ')); + process.exit(1); + } + + console.log('✅ Configuration validated'); +} diff --git a/sentinel-bot/src/core/health.ts b/sentinel-bot/src/core/health.ts new file mode 100644 index 0000000..248cf5e --- /dev/null +++ b/sentinel-bot/src/core/health.ts @@ -0,0 +1,59 @@ +import http from 'http'; +import { SentinelClient } from './client'; +import { config } from './config'; + +export function startHealthServer(client: SentinelClient): void { + const server = http.createServer((req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Content-Type', 'application/json'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + if (req.url === '/health') { + const guilds = client.guilds.cache.size; + const commands = client.stores.get('commands')?.size || 0; + + res.writeHead(200); + res.end(JSON.stringify({ + status: client.isReady() ? 'online' : 'offline', + guilds, + commands, + uptime: Math.floor(process.uptime()), + timestamp: new Date().toISOString(), + bot: { + tag: client.user?.tag || 'Not ready', + id: client.user?.id, + }, + })); + return; + } + + if (req.url === '/stats') { + const guildStats = client.guilds.cache.map(g => ({ + id: g.id, + name: g.name, + memberCount: g.memberCount, + })); + + res.writeHead(200); + res.end(JSON.stringify({ + guilds: guildStats, + totalMembers: guildStats.reduce((sum, g) => sum + g.memberCount, 0), + uptime: Math.floor(process.uptime()), + })); + return; + } + + res.writeHead(404); + res.end(JSON.stringify({ error: 'Not found' })); + }); + + server.listen(config.health.port, () => { + console.log(`🏥 Health server running on port ${config.health.port}`); + }); +} diff --git a/sentinel-bot/src/index.ts b/sentinel-bot/src/index.ts new file mode 100644 index 0000000..385ce83 --- /dev/null +++ b/sentinel-bot/src/index.ts @@ -0,0 +1,27 @@ +import { config, validateConfig } from './core/config'; +import { SentinelClient } from './core/client'; +import { startHealthServer } from './core/health'; + +validateConfig(); + +const client = new SentinelClient(); + +client.once('ready', () => { + console.log(`✅ Aethex Sentinel logged in as ${client.user?.tag}`); + console.log(`📡 Watching ${client.guilds.cache.size} guild(s)`); + + client.user?.setActivity('🛡️ Protecting the Federation', { type: 3 }); + + startHealthServer(client); +}); + +process.on('unhandledRejection', (error: Error) => { + console.error('❌ Unhandled Promise Rejection:', error); +}); + +process.on('uncaughtException', (error: Error) => { + console.error('❌ Uncaught Exception:', error); + process.exit(1); +}); + +client.login(config.token); diff --git a/sentinel-bot/src/listeners/auditLogCreate.ts b/sentinel-bot/src/listeners/auditLogCreate.ts new file mode 100644 index 0000000..c4ccd01 --- /dev/null +++ b/sentinel-bot/src/listeners/auditLogCreate.ts @@ -0,0 +1,40 @@ +import { Listener } from '@sapphire/framework'; +import { AuditLogEvent, GuildAuditLogsEntry, Guild } from 'discord.js'; +import { heatSystem } from '../modules/security/HeatSystem'; + +type DangerousAction = 'CHANNEL_DELETE' | 'BAN_ADD' | 'KICK' | 'ROLE_DELETE' | 'WEBHOOK_DELETE'; + +const DANGEROUS_ACTIONS: Record = { + [AuditLogEvent.ChannelDelete]: 'CHANNEL_DELETE', + [AuditLogEvent.MemberBanAdd]: 'BAN_ADD', + [AuditLogEvent.MemberKick]: 'KICK', + [AuditLogEvent.RoleDelete]: 'ROLE_DELETE', + [AuditLogEvent.WebhookDelete]: 'WEBHOOK_DELETE', +}; + +export class AuditLogCreateListener extends Listener { + public constructor(context: Listener.LoaderContext, options: Listener.Options) { + super(context, { + ...options, + event: 'guildAuditLogEntryCreate', + }); + } + + public async run(entry: GuildAuditLogsEntry, guild: Guild): Promise { + const actionType = DANGEROUS_ACTIONS[entry.action]; + if (!actionType) return; + + const executorId = entry.executorId; + if (!executorId) return; + + if (executorId === this.container.client.user?.id) return; + + console.log(`⚠️ Dangerous action detected: ${actionType} by ${executorId} in ${guild.name}`); + + const triggered = await heatSystem.recordAction(executorId, actionType, guild); + + if (triggered) { + console.log(`🚨 Anti-nuke triggered for ${executorId} in ${guild.name}`); + } + } +} diff --git a/sentinel-bot/src/listeners/guildMemberUpdate.ts b/sentinel-bot/src/listeners/guildMemberUpdate.ts new file mode 100644 index 0000000..6e55e01 --- /dev/null +++ b/sentinel-bot/src/listeners/guildMemberUpdate.ts @@ -0,0 +1,28 @@ +import { Listener } from '@sapphire/framework'; +import { GuildMember, Role } from 'discord.js'; +import { federationManager } from '../modules/federation/FederationManager'; + +export class GuildMemberUpdateListener extends Listener { + public constructor(context: Listener.LoaderContext, options: Listener.Options) { + super(context, { + ...options, + event: 'guildMemberUpdate', + }); + } + + public async run(oldMember: GuildMember, newMember: GuildMember): Promise { + const oldRoles = oldMember.roles.cache; + const newRoles = newMember.roles.cache; + + const addedRoles = newRoles.filter(role => !oldRoles.has(role.id)); + const removedRoles = oldRoles.filter(role => !newRoles.has(role.id)); + + for (const role of addedRoles.values()) { + await federationManager.syncRoleGrant(newMember, role); + } + + for (const role of removedRoles.values()) { + await federationManager.syncRoleRemove(newMember, role); + } + } +} diff --git a/sentinel-bot/src/listeners/interactionCreate.ts b/sentinel-bot/src/listeners/interactionCreate.ts new file mode 100644 index 0000000..3620684 --- /dev/null +++ b/sentinel-bot/src/listeners/interactionCreate.ts @@ -0,0 +1,46 @@ +import { Listener } from '@sapphire/framework'; +import { Interaction } from 'discord.js'; +import { ticketManager } from '../modules/commerce/TicketManager'; + +export class InteractionCreateListener extends Listener { + public constructor(context: Listener.LoaderContext, options: Listener.Options) { + super(context, { + ...options, + event: 'interactionCreate', + }); + } + + public async run(interaction: Interaction): Promise { + if (!interaction.isButton()) return; + + const customId = interaction.customId; + + try { + if (customId === 'ticket_rental') { + await ticketManager.createTicket(interaction, 'rental'); + } else if (customId === 'ticket_support') { + await ticketManager.createTicket(interaction, 'support'); + } else if (customId === 'ticket_billing') { + await ticketManager.createTicket(interaction, 'billing'); + } else if (customId === 'ticket_invoice') { + await ticketManager.generateInvoice(interaction); + } else if (customId === 'ticket_close') { + await ticketManager.closeTicket(interaction); + } else if (customId === 'invoice_pay') { + await interaction.reply({ + content: '✅ Invoice marked as paid! (Mock transaction)', + ephemeral: true, + }); + } + } catch (error) { + console.error('❌ Button interaction error:', error); + + if (!interaction.replied && !interaction.deferred) { + await interaction.reply({ + content: 'An error occurred while processing your request.', + ephemeral: true, + }); + } + } + } +} diff --git a/sentinel-bot/src/listeners/ready.ts b/sentinel-bot/src/listeners/ready.ts new file mode 100644 index 0000000..23ef058 --- /dev/null +++ b/sentinel-bot/src/listeners/ready.ts @@ -0,0 +1,28 @@ +import { Listener } from '@sapphire/framework'; +import { Client } from 'discord.js'; +import { federationManager } from '../modules/federation/FederationManager'; +import { StatusUpdater } from '../modules/dashboard/StatusUpdater'; + +export class ReadyListener extends Listener { + public constructor(context: Listener.LoaderContext, options: Listener.Options) { + super(context, { + ...options, + once: true, + event: 'ready', + }); + } + + public async run(client: Client): Promise { + console.log(`✅ Aethex Sentinel ready as ${client.user.tag}`); + console.log(`📡 Connected to ${client.guilds.cache.size} guild(s)`); + + await federationManager.loadMappings(); + + const statusUpdater = new StatusUpdater(client); + statusUpdater.start(); + + client.user.setActivity('🛡️ Protecting the Federation', { type: 3 }); + + console.log('✅ All modules initialized'); + } +} diff --git a/sentinel-bot/src/modules/commerce/TicketManager.ts b/sentinel-bot/src/modules/commerce/TicketManager.ts new file mode 100644 index 0000000..e70eaad --- /dev/null +++ b/sentinel-bot/src/modules/commerce/TicketManager.ts @@ -0,0 +1,202 @@ +import { + ButtonInteraction, + ChannelType, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + TextChannel, + ThreadChannel, + Message, +} from 'discord.js'; +import { prisma } from '../../core/client'; + +export class TicketManager { + async createTicket(interaction: ButtonInteraction, type: string): Promise { + const channel = interaction.channel as TextChannel; + if (!channel || channel.type !== ChannelType.GuildText) { + await interaction.reply({ content: 'Cannot create ticket here.', ephemeral: true }); + return; + } + + await prisma.user.upsert({ + where: { id: interaction.user.id }, + update: {}, + create: { id: interaction.user.id }, + }); + + const thread = await channel.threads.create({ + name: `${type}-${interaction.user.username}-${Date.now().toString(36)}`, + type: ChannelType.PrivateThread, + invitable: false, + reason: `Ticket created by ${interaction.user.tag}`, + }); + + await thread.members.add(interaction.user.id); + + await prisma.ticket.create({ + data: { + threadId: thread.id, + userId: interaction.user.id, + type, + status: 'open', + }, + }); + + const embed = new EmbedBuilder() + .setColor(0x5865f2) + .setTitle(`🎫 ${type.charAt(0).toUpperCase() + type.slice(1)} Ticket`) + .setDescription( + `Welcome ${interaction.user}!\n\n` + + `A staff member will be with you shortly.\n` + + `Please describe your request in detail.` + ) + .setFooter({ text: `Ticket ID: ${thread.id}` }) + .setTimestamp(); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('ticket_invoice') + .setLabel('Generate Invoice') + .setStyle(ButtonStyle.Primary) + .setEmoji('📄'), + new ButtonBuilder() + .setCustomId('ticket_close') + .setLabel('Close Ticket') + .setStyle(ButtonStyle.Danger) + .setEmoji('🔒') + ); + + await thread.send({ embeds: [embed], components: [row] }); + + await interaction.reply({ + content: `✅ Your ticket has been created: ${thread}`, + ephemeral: true, + }); + + console.log(`🎫 Ticket created: ${thread.name} by ${interaction.user.tag}`); + } + + async generateInvoice(interaction: ButtonInteraction): Promise { + const invoiceId = `INV-${Date.now().toString(36).toUpperCase()}`; + + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle('📄 Invoice Generated') + .setDescription('Here is your mock invoice for the requested service.') + .addFields( + { name: 'Invoice ID', value: invoiceId, inline: true }, + { name: 'Status', value: 'Pending', inline: true }, + { name: 'Amount', value: '$XX.XX', inline: true }, + { name: 'Service', value: 'Server Rental', inline: false }, + ) + .setFooter({ text: 'This is a mock invoice for demonstration' }) + .setTimestamp(); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('invoice_pay') + .setLabel('Mark as Paid') + .setStyle(ButtonStyle.Success) + .setEmoji('💳') + ); + + await interaction.reply({ embeds: [embed], components: [row] }); + } + + async closeTicket(interaction: ButtonInteraction): Promise { + const thread = interaction.channel as ThreadChannel; + if (!thread || thread.type !== ChannelType.PrivateThread) { + await interaction.reply({ content: 'This is not a ticket thread.', ephemeral: true }); + return; + } + + const ticket = await prisma.ticket.findUnique({ + where: { threadId: thread.id }, + }); + + if (!ticket) { + await interaction.reply({ content: 'Ticket not found in database.', ephemeral: true }); + return; + } + + await interaction.reply({ content: '🔒 Closing ticket and saving transcript...' }); + + const transcript = await this.generateTranscript(thread); + + await prisma.ticket.update({ + where: { threadId: thread.id }, + data: { + status: 'closed', + transcript, + closedAt: new Date(), + }, + }); + + await thread.setLocked(true, 'Ticket closed'); + await thread.setArchived(true, 'Ticket closed'); + + console.log(`🎫 Ticket closed: ${thread.name}`); + } + + private async generateTranscript(thread: ThreadChannel): Promise { + const messages: Message[] = []; + let lastId: string | undefined; + + while (true) { + const batch = await thread.messages.fetch({ + limit: 100, + before: lastId, + }); + + if (batch.size === 0) break; + + messages.push(...batch.values()); + lastId = batch.last()?.id; + } + + messages.reverse(); + + const transcript = messages + .map(m => `[${m.createdAt.toISOString()}] ${m.author.tag}: ${m.content}`) + .join('\n'); + + return transcript; + } + + async createSupportPanel(channel: TextChannel): Promise { + const embed = new EmbedBuilder() + .setColor(0x5865f2) + .setTitle('🎫 Support Center') + .setDescription( + 'Need help? Click a button below to open a support ticket.\n\n' + + '**Available Services:**\n' + + '• Server Rental - Rent a game server\n' + + '• Technical Support - Get help with issues\n' + + '• Billing - Payment and invoice questions' + ) + .setFooter({ text: 'Aethex Sentinel Support System' }); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('ticket_rental') + .setLabel('Rent Server') + .setStyle(ButtonStyle.Primary) + .setEmoji('🖥️'), + new ButtonBuilder() + .setCustomId('ticket_support') + .setLabel('Technical Support') + .setStyle(ButtonStyle.Secondary) + .setEmoji('🔧'), + new ButtonBuilder() + .setCustomId('ticket_billing') + .setLabel('Billing') + .setStyle(ButtonStyle.Secondary) + .setEmoji('💰') + ); + + await channel.send({ embeds: [embed], components: [row] }); + } +} + +export const ticketManager = new TicketManager(); diff --git a/sentinel-bot/src/modules/dashboard/StatusUpdater.ts b/sentinel-bot/src/modules/dashboard/StatusUpdater.ts new file mode 100644 index 0000000..0319cea --- /dev/null +++ b/sentinel-bot/src/modules/dashboard/StatusUpdater.ts @@ -0,0 +1,126 @@ +import { Client, VoiceChannel, ChannelType } from 'discord.js'; +import { prisma } from '../../core/client'; +import { config } from '../../core/config'; + +export class StatusUpdater { + private client: Client; + private intervalId: NodeJS.Timeout | null = null; + + constructor(client: Client) { + this.client = client; + } + + start(): void { + if (this.intervalId) return; + + this.update(); + + this.intervalId = setInterval(() => { + this.update(); + }, config.dashboard.updateIntervalMs); + + console.log(`📊 Status updater started (interval: ${config.dashboard.updateIntervalMs}ms)`); + } + + stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + console.log('📊 Status updater stopped'); + } + } + + async update(): Promise { + try { + const guildIds = Object.values(config.guilds).filter(id => id); + let totalMembers = 0; + const guildStats: { id: string; name: string; members: number }[] = []; + + for (const guildId of guildIds) { + const guild = this.client.guilds.cache.get(guildId); + if (guild) { + totalMembers += guild.memberCount; + guildStats.push({ + id: guild.id, + name: guild.name, + members: guild.memberCount, + }); + + await prisma.guildConfig.upsert({ + where: { id: guild.id }, + update: { + name: guild.name, + updatedAt: new Date(), + }, + create: { + id: guild.id, + name: guild.name, + type: this.getGuildType(guild.id), + }, + }); + } + } + + if (config.dashboard.statusChannelId && config.guilds.hub) { + await this.updateStatusChannel(totalMembers); + } + + console.log(`📊 Network status: ${totalMembers} total members across ${guildStats.length} guilds`); + } catch (error) { + console.error('❌ Failed to update status:', error); + } + } + + private async updateStatusChannel(totalMembers: number): Promise { + try { + const hubGuild = this.client.guilds.cache.get(config.guilds.hub); + if (!hubGuild) return; + + const channel = hubGuild.channels.cache.get(config.dashboard.statusChannelId!); + if (!channel || channel.type !== ChannelType.GuildVoice) return; + + const voiceChannel = channel as VoiceChannel; + const newName = `🟢 Network: ${totalMembers.toLocaleString()} Users`; + + if (voiceChannel.name !== newName) { + await voiceChannel.setName(newName); + console.log(`📊 Updated status channel: ${newName}`); + } + } catch (error) { + console.error('❌ Failed to update status channel:', error); + } + } + + private getGuildType(guildId: string): string { + const entries = Object.entries(config.guilds); + const found = entries.find(([, id]) => id === guildId); + return found ? found[0] : 'unknown'; + } + + async getNetworkStats(): Promise<{ + totalMembers: number; + totalGuilds: number; + guilds: { id: string; name: string; members: number; type: string }[]; + }> { + const guildIds = Object.values(config.guilds).filter(id => id); + const guilds: { id: string; name: string; members: number; type: string }[] = []; + + for (const guildId of guildIds) { + const guild = this.client.guilds.cache.get(guildId); + if (guild) { + guilds.push({ + id: guild.id, + name: guild.name, + members: guild.memberCount, + type: this.getGuildType(guild.id), + }); + } + } + + return { + totalMembers: guilds.reduce((sum, g) => sum + g.members, 0), + totalGuilds: guilds.length, + guilds, + }; + } +} diff --git a/sentinel-bot/src/modules/federation/FederationManager.ts b/sentinel-bot/src/modules/federation/FederationManager.ts new file mode 100644 index 0000000..37a0b8f --- /dev/null +++ b/sentinel-bot/src/modules/federation/FederationManager.ts @@ -0,0 +1,124 @@ +import { Guild, GuildMember, Role } from 'discord.js'; +import { prisma } from '../../core/client'; +import { config } from '../../core/config'; + +interface RoleMapping { + sourceGuild: string; + sourceRole: string; + targetGuild: string; + targetRole: string; + roleName: string; +} + +export class FederationManager { + private roleMappings: RoleMapping[] = []; + + async loadMappings(): Promise { + this.roleMappings = await prisma.roleMapping.findMany(); + console.log(`📋 Loaded ${this.roleMappings.length} role mappings`); + } + + async addMapping(mapping: Omit): Promise { + await prisma.roleMapping.create({ + data: mapping, + }); + await this.loadMappings(); + } + + async syncRoleGrant(member: GuildMember, role: Role): Promise { + const sourceGuildId = member.guild.id; + + const mappings = this.roleMappings.filter( + m => m.sourceGuild === sourceGuildId && m.sourceRole === role.id + ); + + if (mappings.length === 0) return; + + console.log(`🔄 Syncing role "${role.name}" for ${member.user.tag} to ${mappings.length} guild(s)`); + + for (const mapping of mappings) { + try { + const targetGuild = member.client.guilds.cache.get(mapping.targetGuild); + if (!targetGuild) { + console.warn(`⚠️ Target guild ${mapping.targetGuild} not found`); + continue; + } + + const targetMember = await targetGuild.members.fetch(member.id).catch(() => null); + if (!targetMember) { + console.warn(`⚠️ User ${member.user.tag} not in target guild ${targetGuild.name}`); + continue; + } + + const targetRole = targetGuild.roles.cache.get(mapping.targetRole); + if (!targetRole) { + console.warn(`⚠️ Target role ${mapping.targetRole} not found in ${targetGuild.name}`); + continue; + } + + if (!targetMember.roles.cache.has(targetRole.id)) { + await targetMember.roles.add(targetRole, `Federation sync from ${member.guild.name}`); + console.log(`✅ Granted "${targetRole.name}" to ${member.user.tag} in ${targetGuild.name}`); + } + } catch (error) { + console.error(`❌ Failed to sync role to guild ${mapping.targetGuild}:`, error); + } + } + + await this.updateUserRoles(member.id); + } + + async syncRoleRemove(member: GuildMember, role: Role): Promise { + const sourceGuildId = member.guild.id; + + const mappings = this.roleMappings.filter( + m => m.sourceGuild === sourceGuildId && m.sourceRole === role.id + ); + + if (mappings.length === 0) return; + + console.log(`🔄 Removing synced role "${role.name}" for ${member.user.tag} from ${mappings.length} guild(s)`); + + for (const mapping of mappings) { + try { + const targetGuild = member.client.guilds.cache.get(mapping.targetGuild); + if (!targetGuild) continue; + + const targetMember = await targetGuild.members.fetch(member.id).catch(() => null); + if (!targetMember) continue; + + const targetRole = targetGuild.roles.cache.get(mapping.targetRole); + if (!targetRole) continue; + + if (targetMember.roles.cache.has(targetRole.id)) { + await targetMember.roles.remove(targetRole, `Federation sync removal from ${member.guild.name}`); + console.log(`✅ Removed "${targetRole.name}" from ${member.user.tag} in ${targetGuild.name}`); + } + } catch (error) { + console.error(`❌ Failed to remove synced role:`, error); + } + } + + await this.updateUserRoles(member.id); + } + + private async updateUserRoles(userId: string): Promise { + const syncedRoles = this.roleMappings + .filter(m => m.sourceGuild === config.guilds.hub) + .map(m => m.roleName); + + await prisma.user.upsert({ + where: { id: userId }, + update: { roles: syncedRoles }, + create: { id: userId, roles: syncedRoles }, + }); + } + + getGuildType(guildId: string): string | null { + const entries = Object.entries(config.guilds); + const found = entries.find(([, id]) => id === guildId); + return found ? found[0] : null; + } +} + +export const federationManager = new FederationManager(); diff --git a/sentinel-bot/src/modules/security/HeatSystem.ts b/sentinel-bot/src/modules/security/HeatSystem.ts new file mode 100644 index 0000000..c05a12a --- /dev/null +++ b/sentinel-bot/src/modules/security/HeatSystem.ts @@ -0,0 +1,131 @@ +import { Guild, GuildMember, PermissionFlagsBits } from 'discord.js'; +import { prisma } from '../../core/client'; +import { config } from '../../core/config'; + +type DangerousAction = 'CHANNEL_DELETE' | 'BAN_ADD' | 'KICK' | 'ROLE_DELETE' | 'WEBHOOK_DELETE'; + +interface HeatEvent { + userId: string; + action: DangerousAction; + timestamp: Date; +} + +export class HeatSystem { + private heatCache: Map = new Map(); + + isWhitelisted(userId: string): boolean { + return config.security.whitelistedUsers.includes(userId); + } + + async recordAction(userId: string, action: DangerousAction, guild: Guild): Promise { + if (this.isWhitelisted(userId)) { + return false; + } + + const now = new Date(); + const event: HeatEvent = { userId, action, timestamp: now }; + + await prisma.heatEvent.create({ + data: { userId, action, timestamp: now }, + }); + + const events = this.heatCache.get(userId) || []; + events.push(event); + + const windowStart = new Date(now.getTime() - config.security.heatWindowMs); + const recentEvents = events.filter(e => e.timestamp >= windowStart); + this.heatCache.set(userId, recentEvents); + + console.log(`🔥 Heat event: ${action} by ${userId} (${recentEvents.length}/${config.security.heatThreshold})`); + + if (recentEvents.length >= config.security.heatThreshold) { + console.log(`🚨 THRESHOLD EXCEEDED for ${userId}!`); + await this.triggerLockdown(userId, guild, recentEvents); + return true; + } + + return false; + } + + private async triggerLockdown(userId: string, guild: Guild, events: HeatEvent[]): Promise { + console.log(`🔒 LOCKDOWN INITIATED in ${guild.name}`); + + try { + const member = await guild.members.fetch(userId).catch(() => null); + if (member && member.bannable) { + await member.ban({ + reason: `[SENTINEL] Anti-nuke triggered: ${events.length} dangerous actions in ${config.security.heatWindowMs}ms`, + deleteMessageSeconds: 0, + }); + console.log(`🔨 Banned ${member.user.tag} for nuke attempt`); + } + } catch (error) { + console.error('❌ Failed to ban attacker:', error); + } + + try { + const everyoneRole = guild.roles.everyone; + await everyoneRole.setPermissions( + everyoneRole.permissions.remove([ + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.AddReactions, + PermissionFlagsBits.CreatePublicThreads, + PermissionFlagsBits.CreatePrivateThreads, + ]), + '[SENTINEL] Lockdown mode activated' + ); + console.log(`🔒 Locked down @everyone permissions`); + } catch (error) { + console.error('❌ Failed to modify @everyone permissions:', error); + } + + await this.sendLockdownAlert(guild, userId, events); + + await prisma.user.upsert({ + where: { id: userId }, + update: { heatLevel: events.length }, + create: { id: userId, heatLevel: events.length }, + }); + } + + private async sendLockdownAlert(guild: Guild, attackerId: string, events: HeatEvent[]): Promise { + const owner = await guild.fetchOwner().catch(() => null); + if (!owner) return; + + try { + await owner.send({ + content: `🚨 **LOCKDOWN ALERT** 🚨\n\n` + + `Server: **${guild.name}**\n` + + `Attacker ID: \`${attackerId}\`\n` + + `Actions detected: ${events.length}\n` + + `Actions: ${events.map(e => e.action).join(', ')}\n\n` + + `The attacker has been banned and @everyone permissions have been restricted.\n` + + `Please review audit logs and restore permissions when safe.`, + }); + } catch (error) { + console.error('❌ Failed to DM server owner:', error); + } + } + + async unlockGuild(guild: Guild): Promise { + try { + const everyoneRole = guild.roles.everyone; + await everyoneRole.setPermissions( + everyoneRole.permissions.add([ + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.AddReactions, + ]), + '[SENTINEL] Lockdown lifted' + ); + console.log(`🔓 Lifted lockdown in ${guild.name}`); + } catch (error) { + console.error('❌ Failed to unlock guild:', error); + } + } + + clearHeat(userId: string): void { + this.heatCache.delete(userId); + } +} + +export const heatSystem = new HeatSystem(); diff --git a/sentinel-bot/tsconfig.json b/sentinel-bot/tsconfig.json new file mode 100644 index 0000000..41d842d --- /dev/null +++ b/sentinel-bot/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}