diff --git a/.env.sample b/.env.sample index 770101de..447178b8 100644 --- a/.env.sample +++ b/.env.sample @@ -1,7 +1,12 @@ # Required for minimum function # Discord Bot Token from Dev portal -DISCORD_TOKEN = BOT_TOKEN -# Mongodb URI for DB login and auth -MONGODB_URI = URI +DISCORD_TOKEN = "" + # Express server port -PORT = 3000 +PORT = "" + +# host address (including port) for the api +API_HOST_ADDR = "" + +# guild id of the target server +PV_GUILD_ID = "" diff --git a/.gitignore b/.gitignore index cbf43ff1..8cd14190 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,7 @@ package-lock.json # Documentation docs/ + +# Temp Folder +assets/temp/* +!assets/temp/.gitkeep diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..75fa1341 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "tabWidth": 2 +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 24f48dc1..638f4549 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,8 +6,6 @@ "christian-kohler.path-intellisense", "macabeus.vscode-fluent", "ms-vsliveshare.vsliveshare", - "visualstudioexptteam.vscodeintellicode", - "visualstudioexptteam.intellicode-api-usage-examples", "eamodio.gitlens", ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index ad1d5de9..11f9ac97 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,17 +1,17 @@ { - "files.eol": "\n", - "files.insertFinalNewline": true, - "editor.tabSize": 4, - "editor.insertSpaces": false, - "editor.detectIndentation": true, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" - }, - "eslint.useFlatConfig": true, - "eslint.workingDirectories": [ - { - "mode": "auto" - } - ], + "files.eol": "\n", + "files.insertFinalNewline": true, + "editor.tabSize": 2, + "editor.indentSize": "tabSize", + "editor.insertSpaces": true, + "editor.detectIndentation": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ] } diff --git a/assets/temp/.gitkeep b/assets/temp/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/assets/temp/attendees.csv b/assets/temp/attendees.csv deleted file mode 100644 index a49a3ff1..00000000 --- a/assets/temp/attendees.csv +++ /dev/null @@ -1,2 +0,0 @@ -Tue Nov 18 2025 16:01:01 GMT-0700 (Mountain Standard Time);213812474670088202;sH3llH0und;true -Tue Nov 18 2025 16:02:15 GMT-0700 (Mountain Standard Time);213812474670088202;sH3llH0und;false diff --git a/assets/temp/attendees.txt b/assets/temp/attendees.txt deleted file mode 100644 index 90af813c..00000000 --- a/assets/temp/attendees.txt +++ /dev/null @@ -1 +0,0 @@ -sH3llH0und joined at 13:24:0 UTC+7,Matt joined at 13:24:0 UTC+7,Matt left at 13:24:23 UTC+7,Matt joined at 13:24:25 UTC+7,Matt left at 13:24:26 UTC+7,Matt joined at 13:24:27 UTC+7,Matt left at 13:24:28 UTC+7,Matt joined at 13:24:29 UTC+7,Matt left at 13:24:30 UTC+7,Matt joined at 13:24:31 UTC+7,Matt left at 13:24:32 UTC+7,Matt joined at 13:24:33 UTC+7,Matt left at 13:24:34 UTC+7,Matt joined at 13:24:35 UTC+7,Matt left at 13:24:36 UTC+7,Matt joined at 13:24:37 UTC+7,Matt left at 13:24:41 UTC+7,Matt joined at 13:24:53 UTC+7,Matt left at 13:24:57 UTC+7,Matt joined at 13:24:58 UTC+7,Matt left at 13:24:59 UTC+7,Matt joined at 13:24:59 UTC+7,Matt left at 13:25:0 UTC+7,Matt joined at 13:25:1 UTC+7,Matt left at 13:25:2 UTC+7,Matt joined at 13:25:2 UTC+7,Matt left at 13:25:4 UTC+7,Matt joined at 13:25:4 UTC+7,Matt left at 13:25:5 UTC+7,Matt joined at 13:25:6 UTC+7,Matt left at 13:25:7 UTC+7,Matt joined at 13:25:8 UTC+7 \ No newline at end of file diff --git a/esbuild.config.js b/esbuild.config.js new file mode 100644 index 00000000..4088ab23 --- /dev/null +++ b/esbuild.config.js @@ -0,0 +1,23 @@ +import esbuild from "esbuild"; +import path, { dirname } from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +await esbuild.build({ + platform: "node", + target: "esnext", + format: "esm", + entryPoints: ["./src/index.ts"], + outfile: "./dist/index.js", + sourcemap: true, + minify: true, + bundle: true, + legalComments: "external", + packages: "external", + alias: { + "~": path.resolve(__dirname, "src"), + "@": path.resolve(__dirname, "src"), + }, +}); diff --git a/eslint.config.js b/eslint.config.js index adf00fb9..ef6127c9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,19 +1,23 @@ import pluginJs from "@eslint/js"; import parserTs from "@typescript-eslint/parser"; -import tsdoc from 'eslint-plugin-tsdoc'; +import eslintConfigPrettier from "eslint-config-prettier/flat"; +import tsdoc from "eslint-plugin-tsdoc"; +import { defineConfig } from "eslint/config"; import globals from "globals"; import tseslint from "typescript-eslint"; -import eslintConfigPrettier from "eslint-config-prettier/flat"; -export default tseslint.config( +export default defineConfig([ + { + ignores: ["dist/**", "docs/**"], + }, pluginJs.configs.recommended, tseslint.configs.recommended, { files: ["**/*.ts", "**/*.tsx"], plugins: { tsdoc }, rules: { - 'tsdoc/syntax': 'warn', - } + "tsdoc/syntax": "warn", + }, }, { languageOptions: { @@ -31,5 +35,5 @@ export default tseslint.config( files: ["**/*.js"], extends: [tseslint.configs.disableTypeChecked], }, - eslintConfigPrettier, -); + eslintConfigPrettier, +]); diff --git a/package.json b/package.json index f8be98a4..10c7de06 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,14 @@ "type": "module", "main": "./dist/index.js", "scripts": { - "build": "pnpm format && tsc --project tsconfig.json", + "build": "pnpm format && node esbuild.config.js", + "predev": "pnpm build", "dev": "node --env-file=.env .", + "predev-deploy": "pnpm build", + "dev-deploy": "node --env-file=.env . --deploy", "start": "node .", - "predev": "pnpm build", - "lint": "eslint \"src/**/*.{js,ts,jsx,tsx}\" --no-error-on-unmatched-pattern", - "lint:fix": "eslint --fix \"src/**/*.{js,ts,jsx,tsx}\" --no-error-on-unmatched-pattern", + "lint": "eslint", + "lint:fix": "eslint --fix", "format": "prettier -w src", "doc": "typedoc" }, @@ -31,24 +33,27 @@ }, "dependencies": { "@fluent/bundle": "^0.19.1", + "@sapphire/snowflake": "^3.5.5", "csv-writer": "^1.6.0", - "discord.js": "^14.22.1", - "express": "^5.1.0", - "mongoose": "^8.18.0" + "discord.js": "^14.25.1", + "esbuild": "^0.27.2", + "express": "^5.2.1", + "ts-transformer-keys": "^0.4.4", + "zod": "^4.3.6" }, "devDependencies": { - "@eslint/js": "^9.34.0", - "@types/express": "^5.0.3", - "@types/node": "^22.18.0", - "@typescript-eslint/parser": "^8.41.0", - "eslint": "^9.34.0", + "@eslint/js": "^9.39.2", + "@types/express": "^5.0.6", + "@types/node": "^22.19.7", + "@typescript-eslint/parser": "^8.53.1", + "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-tsdoc": "^0.4.0", - "globals": "^16.3.0", - "prettier": "^3.6.2", - "typedoc": "^0.28.11", - "typedoc-plugin-markdown": "^4.8.1", - "typescript": "^5.9.2", - "typescript-eslint": "^8.41.0" + "globals": "^16.5.0", + "prettier": "^3.8.1", + "typedoc": "^0.28.16", + "typedoc-plugin-markdown": "^4.9.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a47a3363..9e555a04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,63 +11,72 @@ importers: '@fluent/bundle': specifier: ^0.19.1 version: 0.19.1 + '@sapphire/snowflake': + specifier: ^3.5.5 + version: 3.5.5 csv-writer: specifier: ^1.6.0 version: 1.6.0 discord.js: - specifier: ^14.22.1 - version: 14.22.1 + specifier: ^14.25.1 + version: 14.25.1 + esbuild: + specifier: ^0.27.2 + version: 0.27.2 express: - specifier: ^5.1.0 - version: 5.1.0 - mongoose: - specifier: ^8.18.0 - version: 8.18.0 + specifier: ^5.2.1 + version: 5.2.1 + ts-transformer-keys: + specifier: ^0.4.4 + version: 0.4.4(typescript@5.9.3) + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@eslint/js': - specifier: ^9.34.0 - version: 9.34.0 + specifier: ^9.39.2 + version: 9.39.2 '@types/express': - specifier: ^5.0.3 - version: 5.0.3 + specifier: ^5.0.6 + version: 5.0.6 '@types/node': - specifier: ^22.18.0 - version: 22.18.0 + specifier: ^22.19.7 + version: 22.19.7 '@typescript-eslint/parser': - specifier: ^8.41.0 - version: 8.41.0(eslint@9.34.0)(typescript@5.9.2) + specifier: ^8.53.1 + version: 8.53.1(eslint@9.39.2)(typescript@5.9.3) eslint: - specifier: ^9.34.0 - version: 9.34.0 + specifier: ^9.39.2 + version: 9.39.2 eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.34.0) + version: 10.1.8(eslint@9.39.2) eslint-plugin-tsdoc: specifier: ^0.4.0 version: 0.4.0 globals: - specifier: ^16.3.0 - version: 16.3.0 + specifier: ^16.5.0 + version: 16.5.0 prettier: - specifier: ^3.6.2 - version: 3.6.2 + specifier: ^3.8.1 + version: 3.8.1 typedoc: - specifier: ^0.28.11 - version: 0.28.11(typescript@5.9.2) + specifier: ^0.28.16 + version: 0.28.16(typescript@5.9.3) typedoc-plugin-markdown: - specifier: ^4.8.1 - version: 4.8.1(typedoc@0.28.11(typescript@5.9.2)) + specifier: ^4.9.0 + version: 4.9.0(typedoc@0.28.16(typescript@5.9.3)) typescript: - specifier: ^5.9.2 - version: 5.9.2 + specifier: ^5.9.3 + version: 5.9.3 typescript-eslint: - specifier: ^8.41.0 - version: 8.41.0(eslint@9.34.0)(typescript@5.9.2) + specifier: ^8.53.1 + version: 8.53.1(eslint@9.39.2)(typescript@5.9.3) packages: - '@discordjs/builders@1.11.3': - resolution: {integrity: sha512-p3kf5eV49CJiRTfhtutUCeivSyQ/l2JlKodW1ZquRwwvlOWmG9+6jFShX6x8rUiYhnP6wKI96rgN/SXMy5e5aw==} + '@discordjs/builders@1.13.1': + resolution: {integrity: sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==} engines: {node: '>=16.11.0'} '@discordjs/collection@1.5.3': @@ -78,83 +87,235 @@ packages: resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} engines: {node: '>=18'} - '@discordjs/formatters@0.6.1': - resolution: {integrity: sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==} + '@discordjs/formatters@0.6.2': + resolution: {integrity: sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==} engines: {node: '>=16.11.0'} '@discordjs/rest@2.6.0': resolution: {integrity: sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==} engines: {node: '>=18'} - '@discordjs/util@1.1.1': - resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} + '@discordjs/util@1.2.0': + resolution: {integrity: sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==} engines: {node: '>=18'} '@discordjs/ws@1.2.3': resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==} engines: {node: '>=16.11.0'} - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.0': - resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.3.1': - resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.15.2': - resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.34.0': - resolution: {integrity: sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==} + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.5': - resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@fluent/bundle@0.19.1': resolution: {integrity: sha512-SWJLZrPamDPsJlFFOW1nkgN0j0rbPbmSdmK0XAoXlyqKieLtMVl4vzng3aR5pwKoUx0scug8+YY2oct3fdfy9A==} engines: {node: '>=18.0.0', npm: '>=7.0.0'} - '@gerrit0/mini-shiki@3.12.0': - resolution: {integrity: sha512-CF1vkfe2ViPtmoFEvtUWilEc4dOCiFzV8+J7/vEISSsslKQ97FjeTPNMCqUhZEiKySmKRgK3UO/CxtkyOp7DvA==} + '@gerrit0/mini-shiki@3.21.0': + resolution: {integrity: sha512-9PrsT5DjZA+w3lur/aOIx3FlDeHdyCEFlv9U+fmsVyjPZh61G5SYURQ/1ebe2U63KbDmI2V8IhIUegWb8hjOyg==} '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.6': - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.1': - resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} - engines: {node: '>=18.18'} - '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} @@ -165,21 +326,6 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} - '@mongodb-js/saslprep@1.3.0': - resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@sapphire/async-queue@1.5.5': resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} @@ -192,17 +338,21 @@ packages: resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - '@shikijs/engine-oniguruma@3.12.0': - resolution: {integrity: sha512-IfDl3oXPbJ/Jr2K8mLeQVpnF+FxjAc7ZPDkgr38uEw/Bg3u638neSrpwqOTnTHXt1aU0Fk1/J+/RBdst1kVqLg==} + '@sapphire/snowflake@3.5.5': + resolution: {integrity: sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@shikijs/engine-oniguruma@3.21.0': + resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==} - '@shikijs/langs@3.12.0': - resolution: {integrity: sha512-HIca0daEySJ8zuy9bdrtcBPhcYBo8wR1dyHk1vKrOuwDsITtZuQeGhEkcEfWc6IDyTcom7LRFCH6P7ljGSCEiQ==} + '@shikijs/langs@3.21.0': + resolution: {integrity: sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==} - '@shikijs/themes@3.12.0': - resolution: {integrity: sha512-/lxvQxSI5s4qZLV/AuFaA4Wt61t/0Oka/P9Lmpr1UV+HydNCczO3DMHOC/CsXCCpbv4Zq8sMD0cDa7mvaVoj0Q==} + '@shikijs/themes@3.21.0': + resolution: {integrity: sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==} - '@shikijs/types@3.12.0': - resolution: {integrity: sha512-jsFzm8hCeTINC3OCmTZdhR9DOl/foJWplH2Px0bTi4m8z59fnsueLsweX82oGcjRQ7mfQAluQYKGoH2VzsWY4A==} + '@shikijs/types@3.21.0': + resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -216,11 +366,11 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@5.0.7': - resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} - '@types/express@5.0.3': - resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -231,11 +381,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - - '@types/node@22.18.0': - resolution: {integrity: sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==} + '@types/node@22.19.7': + resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -243,85 +390,79 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/send@0.17.5': - resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - '@types/serve-static@1.15.8': - resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/webidl-conversions@7.0.3': - resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} - - '@types/whatwg-url@11.0.5': - resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} - '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.41.0': - resolution: {integrity: sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==} + '@typescript-eslint/eslint-plugin@8.53.1': + resolution: {integrity: sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.41.0 + '@typescript-eslint/parser': ^8.53.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.41.0': - resolution: {integrity: sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==} + '@typescript-eslint/parser@8.53.1': + resolution: {integrity: sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.41.0': - resolution: {integrity: sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==} + '@typescript-eslint/project-service@8.53.1': + resolution: {integrity: sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.41.0': - resolution: {integrity: sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==} + '@typescript-eslint/scope-manager@8.53.1': + resolution: {integrity: sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.41.0': - resolution: {integrity: sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==} + '@typescript-eslint/tsconfig-utils@8.53.1': + resolution: {integrity: sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.41.0': - resolution: {integrity: sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==} + '@typescript-eslint/type-utils@8.53.1': + resolution: {integrity: sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.41.0': - resolution: {integrity: sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==} + '@typescript-eslint/types@8.53.1': + resolution: {integrity: sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.41.0': - resolution: {integrity: sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==} + '@typescript-eslint/typescript-estree@8.53.1': + resolution: {integrity: sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.41.0': - resolution: {integrity: sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==} + '@typescript-eslint/utils@8.53.1': + resolution: {integrity: sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.41.0': - resolution: {integrity: sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==} + '@typescript-eslint/visitor-keys@8.53.1': + resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vladfrangu/async_event_emitter@2.4.6': - resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} + '@vladfrangu/async_event_emitter@2.4.7': + resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} accepts@2.0.0: @@ -354,8 +495,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} brace-expansion@1.1.12: @@ -364,14 +505,6 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - bson@6.10.4: - resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} - engines: {node: '>=16.20.1'} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -402,9 +535,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} - engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} @@ -425,8 +558,8 @@ packages: csv-writer@1.6.0: resolution: {integrity: sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==} - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -441,11 +574,11 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - discord-api-types@0.38.22: - resolution: {integrity: sha512-2gnYrgXN3yTlv2cKBISI/A8btZwsSZLwKpIQXeI1cS8a7W7wP3sFVQOm3mPuuinTD8jJCKGPGNH399zE7Un1kA==} + discord-api-types@0.38.37: + resolution: {integrity: sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w==} - discord.js@14.22.1: - resolution: {integrity: sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w==} + discord.js@14.25.1: + resolution: {integrity: sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==} engines: {node: '>=18'} dunder-proto@1.0.1: @@ -475,6 +608,11 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -503,8 +641,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.34.0: - resolution: {integrity: sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==} + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -517,8 +655,8 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -537,37 +675,35 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} - engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -599,10 +735,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -611,17 +743,14 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@16.3.0: - resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -634,12 +763,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} ignore@5.3.2: @@ -677,10 +806,6 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -690,8 +815,8 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true json-buffer@3.0.1: @@ -706,10 +831,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - kareem@2.6.3: - resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} - engines: {node: '>=12.0.0'} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -730,14 +851,14 @@ packages: lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} - magic-bytes.js@1.12.1: - resolution: {integrity: sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==} + magic-bytes.js@1.13.0: + resolution: {integrity: sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==} markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} @@ -754,28 +875,17 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} - memory-pager@1.5.0: - resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} - merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -784,48 +894,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - mongodb-connection-string-url@3.0.2: - resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} - - mongodb@6.18.0: - resolution: {integrity: sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==} - engines: {node: '>=16.20.1'} - peerDependencies: - '@aws-sdk/credential-providers': ^3.188.0 - '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 - gcp-metadata: ^5.2.0 - kerberos: ^2.0.1 - mongodb-client-encryption: '>=6.0.0 <7' - snappy: ^7.2.2 - socks: ^2.7.1 - peerDependenciesMeta: - '@aws-sdk/credential-providers': - optional: true - '@mongodb-js/zstd': - optional: true - gcp-metadata: - optional: true - kerberos: - optional: true - mongodb-client-encryption: - optional: true - snappy: - optional: true - socks: - optional: true - - mongoose@8.18.0: - resolution: {integrity: sha512-3TixPihQKBdyaYDeJqRjzgb86KbilEH07JmzV8SoSjgoskNTpa6oTBmDxeoF9p8YnWQoz7shnCyPkSV/48y3yw==} - engines: {node: '>=16.20.1'} - - mpath@0.9.0: - resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} - engines: {node: '>=4.0.0'} - - mquery@5.0.0: - resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} - engines: {node: '>=14.0.0'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -878,20 +946,19 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-to-regexp@8.2.0: - resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} - engines: {node: '>=16'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} hasBin: true @@ -907,20 +974,17 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} - engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} @@ -930,39 +994,29 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} hasBin: true - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} setprototypeof@1.2.0: @@ -992,16 +1046,6 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} - sift@17.1.3: - resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} - - sparse-bitfield@3.0.3: - resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} - - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -1018,20 +1062,16 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} - - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -1039,6 +1079,11 @@ packages: ts-mixer@6.0.4: resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + ts-transformer-keys@0.4.4: + resolution: {integrity: sha512-LrqgvaFvar01/5mbunRyeLTSIkqoC2xfcpL/90aDY6vR07DGyH+UaYGdIEsUudnlAw2Sr0pxFgdZvE0QIyI4qA==} + peerDependencies: + typescript: '>=2.4.1' + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1050,28 +1095,28 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - typedoc-plugin-markdown@4.8.1: - resolution: {integrity: sha512-ug7fc4j0SiJxSwBGLncpSo8tLvrT9VONvPUQqQDTKPxCoFQBADLli832RGPtj6sfSVJebNSrHZQRUdEryYH/7g==} + typedoc-plugin-markdown@4.9.0: + resolution: {integrity: sha512-9Uu4WR9L7ZBgAl60N/h+jqmPxxvnC9nQAlnnO/OujtG2ubjnKTVUFY1XDhcMY+pCqlX3N2HsQM2QTYZIU9tJuw==} engines: {node: '>= 18'} peerDependencies: typedoc: 0.28.x - typedoc@0.28.11: - resolution: {integrity: sha512-1FqgrrUYGNuE3kImAiEDgAVVVacxdO4ZVTKbiOVDGkoeSB4sNwQaDpa8mta+Lw5TEzBFmGXzsg0I1NLRIoaSFw==} + typedoc@0.28.16: + resolution: {integrity: sha512-x4xW77QC3i5DUFMBp0qjukOTnr/sSg+oEs86nB3LjDslvAmwe/PUGDWbe3GrIqt59oTqoXK5GRK9tAa0sYMiog==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x - typescript-eslint@8.41.0: - resolution: {integrity: sha512-n66rzs5OBXW3SFSnZHr2T685q1i4ODm2nulFJhMZBotaTavsS8TrI3d7bDlRSs9yWo7HmyWrN9qDu14Qv7Y0Dw==} + typescript-eslint@8.53.1: + resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - typescript@5.9.2: - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -1096,14 +1141,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1116,8 +1153,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -1128,8 +1165,8 @@ packages: utf-8-validate: optional: true - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true @@ -1137,14 +1174,17 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + snapshots: - '@discordjs/builders@1.11.3': + '@discordjs/builders@1.13.1': dependencies: - '@discordjs/formatters': 0.6.1 - '@discordjs/util': 1.1.1 + '@discordjs/formatters': 0.6.2 + '@discordjs/util': 1.2.0 '@sapphire/shapeshift': 4.0.0 - discord-api-types: 0.38.22 + discord-api-types: 0.38.37 fast-deep-equal: 3.1.3 ts-mixer: 6.0.4 tslib: 2.8.1 @@ -1153,104 +1193,184 @@ snapshots: '@discordjs/collection@2.1.1': {} - '@discordjs/formatters@0.6.1': + '@discordjs/formatters@0.6.2': dependencies: - discord-api-types: 0.38.22 + discord-api-types: 0.38.37 '@discordjs/rest@2.6.0': dependencies: '@discordjs/collection': 2.1.1 - '@discordjs/util': 1.1.1 + '@discordjs/util': 1.2.0 '@sapphire/async-queue': 1.5.5 - '@sapphire/snowflake': 3.5.3 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.38.22 - magic-bytes.js: 1.12.1 + '@sapphire/snowflake': 3.5.5 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.37 + magic-bytes.js: 1.13.0 tslib: 2.8.1 undici: 6.21.3 - '@discordjs/util@1.1.1': {} + '@discordjs/util@1.2.0': + dependencies: + discord-api-types: 0.38.37 '@discordjs/ws@1.2.3': dependencies: '@discordjs/collection': 2.1.1 '@discordjs/rest': 2.6.0 - '@discordjs/util': 1.1.1 + '@discordjs/util': 1.2.0 '@sapphire/async-queue': 1.5.5 '@types/ws': 8.18.1 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.38.22 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.37 tslib: 2.8.1 - ws: 8.18.3 + ws: 8.19.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@eslint-community/eslint-utils@4.7.0(eslint@9.34.0)': + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)': dependencies: - eslint: 9.34.0 + eslint: 9.39.2 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.0': + '@eslint/config-array@0.21.1': dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.1 + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.3.1': {} + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 - '@eslint/core@0.15.2': + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 - debug: 4.4.1 + debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@9.34.0': {} + '@eslint/js@9.39.2': {} - '@eslint/object-schema@2.1.6': {} + '@eslint/object-schema@2.1.7': {} - '@eslint/plugin-kit@0.3.5': + '@eslint/plugin-kit@0.4.1': dependencies: - '@eslint/core': 0.15.2 + '@eslint/core': 0.17.0 levn: 0.4.1 '@fluent/bundle@0.19.1': {} - '@gerrit0/mini-shiki@3.12.0': + '@gerrit0/mini-shiki@3.21.0': dependencies: - '@shikijs/engine-oniguruma': 3.12.0 - '@shikijs/langs': 3.12.0 - '@shikijs/themes': 3.12.0 - '@shikijs/types': 3.12.0 + '@shikijs/engine-oniguruma': 3.21.0 + '@shikijs/langs': 3.21.0 + '@shikijs/themes': 3.21.0 + '@shikijs/types': 3.21.0 '@shikijs/vscode-textmate': 10.0.2 '@humanfs/core@0.19.1': {} - '@humanfs/node@0.16.6': + '@humanfs/node@0.16.7': dependencies: '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.1': {} - '@humanwhocodes/retry@0.4.3': {} '@microsoft/tsdoc-config@0.17.1': @@ -1258,49 +1378,35 @@ snapshots: '@microsoft/tsdoc': 0.15.1 ajv: 8.12.0 jju: 1.4.0 - resolve: 1.22.10 + resolve: 1.22.11 '@microsoft/tsdoc@0.15.1': {} - '@mongodb-js/saslprep@1.3.0': - dependencies: - sparse-bitfield: 3.0.3 - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - '@sapphire/async-queue@1.5.5': {} '@sapphire/shapeshift@4.0.0': dependencies: fast-deep-equal: 3.1.3 - lodash: 4.17.21 + lodash: 4.17.23 '@sapphire/snowflake@3.5.3': {} - '@shikijs/engine-oniguruma@3.12.0': + '@sapphire/snowflake@3.5.5': {} + + '@shikijs/engine-oniguruma@3.21.0': dependencies: - '@shikijs/types': 3.12.0 + '@shikijs/types': 3.21.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.12.0': + '@shikijs/langs@3.21.0': dependencies: - '@shikijs/types': 3.12.0 + '@shikijs/types': 3.21.0 - '@shikijs/themes@3.12.0': + '@shikijs/themes@3.21.0': dependencies: - '@shikijs/types': 3.12.0 + '@shikijs/types': 3.21.0 - '@shikijs/types@3.12.0': + '@shikijs/types@3.21.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -1310,26 +1416,26 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.18.0 + '@types/node': 22.19.7 '@types/connect@3.4.38': dependencies: - '@types/node': 22.18.0 + '@types/node': 22.19.7 '@types/estree@1.0.8': {} - '@types/express-serve-static-core@5.0.7': + '@types/express-serve-static-core@5.1.1': dependencies: - '@types/node': 22.18.0 + '@types/node': 22.19.7 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 - '@types/send': 0.17.5 + '@types/send': 1.2.1 - '@types/express@5.0.3': + '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 5.0.7 - '@types/serve-static': 1.15.8 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 '@types/hast@3.0.4': dependencies: @@ -1339,9 +1445,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/mime@1.3.5': {} - - '@types/node@22.18.0': + '@types/node@22.19.7': dependencies: undici-types: 6.21.0 @@ -1349,127 +1453,117 @@ snapshots: '@types/range-parser@1.2.7': {} - '@types/send@0.17.5': + '@types/send@1.2.1': dependencies: - '@types/mime': 1.3.5 - '@types/node': 22.18.0 + '@types/node': 22.19.7 - '@types/serve-static@1.15.8': + '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.18.0 - '@types/send': 0.17.5 + '@types/node': 22.19.7 '@types/unist@3.0.3': {} - '@types/webidl-conversions@7.0.3': {} - - '@types/whatwg-url@11.0.5': - dependencies: - '@types/webidl-conversions': 7.0.3 - '@types/ws@8.18.1': dependencies: - '@types/node': 22.18.0 + '@types/node': 22.19.7 - '@typescript-eslint/eslint-plugin@8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0)(typescript@5.9.2))(eslint@9.34.0)(typescript@5.9.2)': + '@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.41.0(eslint@9.34.0)(typescript@5.9.2) - '@typescript-eslint/scope-manager': 8.41.0 - '@typescript-eslint/type-utils': 8.41.0(eslint@9.34.0)(typescript@5.9.2) - '@typescript-eslint/utils': 8.41.0(eslint@9.34.0)(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.41.0 - eslint: 9.34.0 - graphemer: 1.4.0 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.53.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.1 + eslint: 9.39.2 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.41.0(eslint@9.34.0)(typescript@5.9.2)': + '@typescript-eslint/parser@8.53.1(eslint@9.39.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.41.0 - '@typescript-eslint/types': 8.41.0 - '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.41.0 - debug: 4.4.1 - eslint: 9.34.0 - typescript: 5.9.2 + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.1 + debug: 4.4.3 + eslint: 9.39.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.41.0(typescript@5.9.2)': + '@typescript-eslint/project-service@8.53.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.9.2) - '@typescript-eslint/types': 8.41.0 - debug: 4.4.1 - typescript: 5.9.2 + '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3) + '@typescript-eslint/types': 8.53.1 + debug: 4.4.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.41.0': + '@typescript-eslint/scope-manager@8.53.1': dependencies: - '@typescript-eslint/types': 8.41.0 - '@typescript-eslint/visitor-keys': 8.41.0 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/visitor-keys': 8.53.1 - '@typescript-eslint/tsconfig-utils@8.41.0(typescript@5.9.2)': + '@typescript-eslint/tsconfig-utils@8.53.1(typescript@5.9.3)': dependencies: - typescript: 5.9.2 + typescript: 5.9.3 - '@typescript-eslint/type-utils@8.41.0(eslint@9.34.0)(typescript@5.9.2)': + '@typescript-eslint/type-utils@8.53.1(eslint@9.39.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.41.0 - '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.41.0(eslint@9.34.0)(typescript@5.9.2) - debug: 4.4.1 - eslint: 9.34.0 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.41.0': {} + '@typescript-eslint/types@8.53.1': {} - '@typescript-eslint/typescript-estree@8.41.0(typescript@5.9.2)': + '@typescript-eslint/typescript-estree@8.53.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.41.0(typescript@5.9.2) - '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.9.2) - '@typescript-eslint/types': 8.41.0 - '@typescript-eslint/visitor-keys': 8.41.0 - debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 + '@typescript-eslint/project-service': 8.53.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3) + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/visitor-keys': 8.53.1 + debug: 4.4.3 minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.41.0(eslint@9.34.0)(typescript@5.9.2)': + '@typescript-eslint/utils@8.53.1(eslint@9.39.2)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0) - '@typescript-eslint/scope-manager': 8.41.0 - '@typescript-eslint/types': 8.41.0 - '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2) - eslint: 9.34.0 - typescript: 5.9.2 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + eslint: 9.39.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.41.0': + '@typescript-eslint/visitor-keys@8.53.1': dependencies: - '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/types': 8.53.1 eslint-visitor-keys: 4.2.1 - '@vladfrangu/async_event_emitter@2.4.6': {} + '@vladfrangu/async_event_emitter@2.4.7': {} accepts@2.0.0: dependencies: - mime-types: 3.0.1 + mime-types: 3.0.2 negotiator: 1.0.0 acorn-jsx@5.3.2(acorn@8.15.0): @@ -1500,16 +1594,16 @@ snapshots: balanced-match@1.0.2: {} - body-parser@2.2.0: + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1 - http-errors: 2.0.0 - iconv-lite: 0.6.3 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.0 + qs: 6.14.1 + raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: - supports-color @@ -1523,12 +1617,6 @@ snapshots: dependencies: balanced-match: 1.0.2 - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - bson@6.10.4: {} - bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -1556,9 +1644,7 @@ snapshots: concat-map@0.0.1: {} - content-disposition@1.0.0: - dependencies: - safe-buffer: 5.2.1 + content-disposition@1.0.1: {} content-type@1.0.5: {} @@ -1574,7 +1660,7 @@ snapshots: csv-writer@1.6.0: {} - debug@4.4.1: + debug@4.4.3: dependencies: ms: 2.1.3 @@ -1582,21 +1668,21 @@ snapshots: depd@2.0.0: {} - discord-api-types@0.38.22: {} + discord-api-types@0.38.37: {} - discord.js@14.22.1: + discord.js@14.25.1: dependencies: - '@discordjs/builders': 1.11.3 + '@discordjs/builders': 1.13.1 '@discordjs/collection': 1.5.3 - '@discordjs/formatters': 0.6.1 + '@discordjs/formatters': 0.6.2 '@discordjs/rest': 2.6.0 - '@discordjs/util': 1.1.1 + '@discordjs/util': 1.2.0 '@discordjs/ws': 1.2.3 '@sapphire/snowflake': 3.5.3 - discord-api-types: 0.38.22 + discord-api-types: 0.38.37 fast-deep-equal: 3.1.3 lodash.snakecase: 4.1.1 - magic-bytes.js: 1.12.1 + magic-bytes.js: 1.13.0 tslib: 2.8.1 undici: 6.21.3 transitivePeerDependencies: @@ -1623,13 +1709,42 @@ snapshots: dependencies: es-errors: 1.3.0 + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + escape-html@1.0.3: {} escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.34.0): + eslint-config-prettier@10.1.8(eslint@9.39.2): dependencies: - eslint: 9.34.0 + eslint: 9.39.2 eslint-plugin-tsdoc@0.4.0: dependencies: @@ -1645,30 +1760,29 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.34.0: - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.0 - '@eslint/config-helpers': 0.3.1 - '@eslint/core': 0.15.2 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.34.0 - '@eslint/plugin-kit': 0.3.5 - '@humanfs/node': 0.16.6 + eslint@9.39.2: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - esquery: 1.6.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -1691,7 +1805,7 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.1 - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -1705,32 +1819,33 @@ snapshots: etag@1.8.1: {} - express@5.1.0: + express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.0 - content-disposition: 1.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.1 + debug: 4.4.3 + depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.0 + finalhandler: 2.1.1 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 2.0.0 - mime-types: 3.0.1 + mime-types: 3.0.2 on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.14.1 range-parser: 1.2.1 router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 @@ -1739,33 +1854,21 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} - fastq@1.19.1: - dependencies: - reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 - fill-range@7.1.1: + finalhandler@2.1.1: dependencies: - to-regex-range: 5.0.1 - - finalhandler@2.1.0: - dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -1810,22 +1913,16 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 globals@14.0.0: {} - globals@16.3.0: {} + globals@16.5.0: {} gopd@1.2.0: {} - graphemer@1.4.0: {} - has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -1834,15 +1931,15 @@ snapshots: dependencies: function-bind: 1.1.2 - http-errors@2.0.0: + http-errors@2.0.1: dependencies: depd: 2.0.0 inherits: 2.0.4 setprototypeof: 1.2.0 - statuses: 2.0.1 + statuses: 2.0.2 toidentifier: 1.0.1 - iconv-lite@0.6.3: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -1871,15 +1968,13 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-number@7.0.0: {} - is-promise@4.0.0: {} isexe@2.0.0: {} jju@1.4.0: {} - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -1891,8 +1986,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - kareem@2.6.3: {} - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -1914,11 +2007,11 @@ snapshots: lodash.snakecase@4.1.1: {} - lodash@4.17.21: {} + lodash@4.17.23: {} lunr@2.3.9: {} - magic-bytes.js@1.12.1: {} + magic-bytes.js@1.13.0: {} markdown-it@14.1.0: dependencies: @@ -1935,20 +2028,11 @@ snapshots: media-typer@1.1.0: {} - memory-pager@1.5.0: {} - merge-descriptors@2.0.0: {} - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - mime-db@1.54.0: {} - mime-types@3.0.1: + mime-types@3.0.2: dependencies: mime-db: 1.54.0 @@ -1960,44 +2044,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - mongodb-connection-string-url@3.0.2: - dependencies: - '@types/whatwg-url': 11.0.5 - whatwg-url: 14.2.0 - - mongodb@6.18.0: - dependencies: - '@mongodb-js/saslprep': 1.3.0 - bson: 6.10.4 - mongodb-connection-string-url: 3.0.2 - - mongoose@8.18.0: - dependencies: - bson: 6.10.4 - kareem: 2.6.3 - mongodb: 6.18.0 - mpath: 0.9.0 - mquery: 5.0.0 - ms: 2.1.3 - sift: 17.1.3 - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - gcp-metadata - - kerberos - - mongodb-client-encryption - - snappy - - socks - - supports-color - - mpath@0.9.0: {} - - mquery@5.0.0: - dependencies: - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - ms@2.1.3: {} natural-compare@1.4.0: {} @@ -2043,13 +2089,13 @@ snapshots: path-parse@1.0.7: {} - path-to-regexp@8.2.0: {} + path-to-regexp@8.3.0: {} - picomatch@2.3.1: {} + picomatch@4.0.3: {} prelude-ls@1.2.1: {} - prettier@3.6.2: {} + prettier@3.8.1: {} proxy-addr@2.0.7: dependencies: @@ -2060,62 +2106,52 @@ snapshots: punycode@2.3.1: {} - qs@6.14.0: + qs@6.14.1: dependencies: side-channel: 1.1.0 - queue-microtask@1.2.3: {} - range-parser@1.2.1: {} - raw-body@3.0.0: + raw-body@3.0.2: dependencies: bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 unpipe: 1.0.0 require-from-string@2.0.2: {} resolve-from@4.0.0: {} - resolve@1.22.10: + resolve@1.22.11: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - reusify@1.1.0: {} - router@2.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 - path-to-regexp: 8.2.0 + path-to-regexp: 8.3.0 transitivePeerDependencies: - supports-color - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - safe-buffer@5.2.1: {} - safer-buffer@2.1.2: {} - semver@7.7.2: {} + semver@7.7.3: {} - send@1.2.0: + send@1.2.1: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 fresh: 2.0.0 - http-errors: 2.0.0 - mime-types: 3.0.1 + http-errors: 2.0.1 + mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 @@ -2123,12 +2159,12 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.0: + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.0 + send: 1.2.1 transitivePeerDependencies: - supports-color @@ -2168,14 +2204,6 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - sift@17.1.3: {} - - sparse-bitfield@3.0.3: - dependencies: - memory-pager: 1.5.0 - - statuses@2.0.1: {} - statuses@2.0.2: {} strip-json-comments@3.1.1: {} @@ -2186,22 +2214,23 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - to-regex-range@5.0.1: + tinyglobby@0.2.15: dependencies: - is-number: 7.0.0 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 toidentifier@1.0.1: {} - tr46@5.1.1: - dependencies: - punycode: 2.3.1 - - ts-api-utils@2.1.0(typescript@5.9.2): + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: - typescript: 5.9.2 + typescript: 5.9.3 ts-mixer@6.0.4: {} + ts-transformer-keys@0.4.4(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + tslib@2.8.1: {} type-check@0.4.0: @@ -2212,33 +2241,33 @@ snapshots: dependencies: content-type: 1.0.5 media-typer: 1.1.0 - mime-types: 3.0.1 + mime-types: 3.0.2 - typedoc-plugin-markdown@4.8.1(typedoc@0.28.11(typescript@5.9.2)): + typedoc-plugin-markdown@4.9.0(typedoc@0.28.16(typescript@5.9.3)): dependencies: - typedoc: 0.28.11(typescript@5.9.2) + typedoc: 0.28.16(typescript@5.9.3) - typedoc@0.28.11(typescript@5.9.2): + typedoc@0.28.16(typescript@5.9.3): dependencies: - '@gerrit0/mini-shiki': 3.12.0 + '@gerrit0/mini-shiki': 3.21.0 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 - typescript: 5.9.2 - yaml: 2.8.1 + typescript: 5.9.3 + yaml: 2.8.2 - typescript-eslint@8.41.0(eslint@9.34.0)(typescript@5.9.2): + typescript-eslint@8.53.1(eslint@9.39.2)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0)(typescript@5.9.2))(eslint@9.34.0)(typescript@5.9.2) - '@typescript-eslint/parser': 8.41.0(eslint@9.34.0)(typescript@5.9.2) - '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.41.0(eslint@9.34.0)(typescript@5.9.2) - eslint: 9.34.0 - typescript: 5.9.2 + '@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2)(typescript@5.9.3) + eslint: 9.39.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - typescript@5.9.2: {} + typescript@5.9.3: {} uc.micro@2.1.0: {} @@ -2254,13 +2283,6 @@ snapshots: vary@1.1.2: {} - webidl-conversions@7.0.0: {} - - whatwg-url@14.2.0: - dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -2269,8 +2291,10 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.3: {} + ws@8.19.0: {} - yaml@2.8.1: {} + yaml@2.8.2: {} yocto-queue@0.1.0: {} + + zod@4.3.6: {} diff --git a/src/Classes/API/ApiConnService/ApiConnService.ts b/src/Classes/API/ApiConnService/ApiConnService.ts new file mode 100644 index 00000000..eba62387 --- /dev/null +++ b/src/Classes/API/ApiConnService/ApiConnService.ts @@ -0,0 +1,144 @@ +import { ZodType } from "zod"; +import { + ApiConnServiceOptions, + InternalRequest, + RequestData, + RequestMethod, + RouteLike, +} from "./types"; +import { parseResponse } from "./utils"; + +export class ApiConnService { + jwt: string | null = null; + + host: string; + + constructor(options: ApiConnServiceOptions) { + this.host = options.host; + } + + async auth(token: string) { + console.log("Bot " + token); + + await fetch(`${this.host}/auth`, { + //process.env.API_HOST_ADDR + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + discordToken: "Bot " + token, + }), + }) + .then(async (res) => { + if (!res.ok) throw Error("Can't fucking connect to API dawg"); + const { accessToken } = await res.json(); + this.jwt = accessToken; + }) + .catch(console.error); + } + + async request( + options: InternalRequest, + schema?: ZodType, + ): Promise { + if (!this.jwt) throw Error("run auth function"); + // console.log(this.host, options.fullRoute, options.query?.toString()); + const url = new URL(this.host + options.fullRoute); + options.query?.forEach((val: string, key: string) => { + url.searchParams.append(key, val); + }); + const res = await fetch(url, { + method: options.method, + body: options.body, + headers: { + ...options.headers, + Authorization: "Bot " + this.jwt, + }, + }); + if (res.status === 401 && options.attempt && options.attempt > 2) { + this.jwt = null; + options.attempt++; + return this.request(options, schema); + } + + if (!res.ok) + throw Error( + `API threw exception: ${res.status} ${res.statusText}${res.body ? "\n" + (await res.text()) : ""}`, + ); + + return parseResponse(res, schema); + } + + /** + * Runs a get request from the api + * + * @param fullRoute - The full route to query + * @param options - Optional request options + */ + public async get( + fullRoute: RouteLike, + schema: ZodType, + options: RequestData = {}, + ): Promise { + return (await this.request( + { + ...options, + fullRoute, + method: RequestMethod.Get, + }, + schema, + )) as R; + } + + /** + * Runs a delete request from the api + * + * @param fullRoute - The full route to query + * @param options - Optional request options + */ + public async delete(fullRoute: RouteLike, options: RequestData = {}) { + return this.request({ + ...options, + fullRoute, + method: RequestMethod.Delete, + }); + } + + /** + * Runs a post request from the api + * + * @param fullRoute - The full route to query + * @param options - Optional request options + */ + public async post( + fullRoute: RouteLike, + options: RequestData = {}, + schema?: ZodType, + ) { + return (await this.request( + { ...options, fullRoute, method: RequestMethod.Post }, + schema, + )) as R; + } + + /** + * Runs a put request from the api + * + * @param fullRoute - The full route to query + * @param options - Optional request options + */ + public async put(fullRoute: RouteLike, options: RequestData = {}) { + return this.request({ ...options, fullRoute, method: RequestMethod.Put }); + } + + /** + * Runs a patch request from the api + * + * @param fullRoute - The full route to query + * @param options - Optional request options + */ + public async patch(fullRoute: RouteLike, options: RequestData = {}) { + return this.request({ ...options, fullRoute, method: RequestMethod.Patch }); + } +} diff --git a/src/Classes/API/ApiConnService/WarnSearchmanager.ts b/src/Classes/API/ApiConnService/WarnSearchmanager.ts new file mode 100644 index 00000000..f31f6c31 --- /dev/null +++ b/src/Classes/API/ApiConnService/WarnSearchmanager.ts @@ -0,0 +1,161 @@ +import { DiscordSnowflake } from "@sapphire/snowflake"; +import { Collection, GuildMember, Snowflake, User } from "discord.js"; +import z from "zod"; +import { Warn } from "../Warn"; +import { ApiConnService } from "./ApiConnService"; +import { Routes } from "./routes"; +import { APIWarn, APIWarnPage, zAPIWarnPage } from "./types"; + +export enum WarnSortOption { + Descending = "desc", + Ascending = "asc", +} +export interface FetchWarnOptions { + moderatorId?: Snowflake; + targetId?: Snowflake; + monthsAgo?: number; + limit?: number; + sort?: WarnSortOption; + page?: number; +} + +interface CreateWarnOptions { + moderatorId: Snowflake; + targetId: Snowflake; + reason: string; + expires: Date; +} + +export class WarnSearch { + private lastRead = new Date(); + readonly currentPageWarns = new Collection(); + private count: number | null = null; + constructor( + readonly id: Snowflake, + readonly searcher: GuildMember | User, + readonly client: ApiConnService, + private options: FetchWarnOptions, + ) {} + + get guild() { + if (this.searcher instanceof GuildMember) return this.searcher.guild; + return null; + } + get page() { + return this.options.page; + } + + get limit() { + return this.options.limit; + } + + toQuery() { + const query = new URLSearchParams(); + if (this.options.moderatorId) + query.set("mod_discord_id", this.options.moderatorId); + if (this.options.targetId) + query.set("tgt_discord_id", this.options.targetId); + if (this.options.monthsAgo) { + const timeWindowDate = new Date( + Date.now() - Math.abs(this.options.monthsAgo) * 2_592_000_000, + ); + query.set("time_window", timeWindowDate.toISOString()); + } + query.set("sort", this.options.sort ?? WarnSortOption.Descending); + query.set("limit", this.options.limit?.toString() ?? String(3)); + query.set("page", this.options.page?.toString() ?? "0"); + return query; + } + + async fetchPage() { + // console.log(this.toQuery()); + const page = await this.client.get( + Routes.discordWarns, + zAPIWarnPage, + { + query: this.toQuery(), + }, + ); + + this.currentPageWarns.clear(); + // console.log(this.toQuery(), page); + page.data.forEach((warn) => + this.currentPageWarns.set(warn.id.toString(), warn), + ); + + this.options.page ??= page.page; + this.lastRead = new Date(); + this.count = page.count; + this.options.limit = page.limit; + return page; + } + async fetchNextPage() { + this.options.page!++; + return this.fetchPage(); + } + + async fetchLastPage() { + this.options.page!--; + return this.fetchPage(); + } + + get lastQuery() { + return this.lastRead; + } + + get totalWarns() { + return this.count; + } +} +export class WarnSearchManager { + public cache = new Collection(); + + constructor(readonly client: ApiConnService) {} + + newSearch(member: GuildMember | User, options: FetchWarnOptions) { + const id = DiscordSnowflake.generate().toString(); + const warn = new WarnSearch(id, member, this.client, options); + this.cache.set(id, warn); + this.sweep(); + return warn; + } + + private async sweep() { + this.cache.forEach((search) => { + const now = new Date(); + now.setMinutes(now.getMinutes() - 30); + if (search.lastQuery <= now) { + this.cache.delete(search.id); + } + }); + } + + async createWarn(options: CreateWarnOptions) { + const id = await this.client.post( + Routes.discordWarns, + { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + mod_discord_id: options.moderatorId, + tgt_discord_id: options.targetId, + reason: options.reason, + expires_at_utc: options.expires.toISOString(), + }), + }, + z.number(), + ); + + // console.log(res); + + const now = new Date().toISOString(); + return new Warn(this.client, { + id, + moderatorDiscordId: options.moderatorId, + userWarnedDiscordId: options.targetId, + reason: options.reason, + expiresAtUtc: options.expires.toISOString(), + createdAtUtc: now, + updatedAtUtc: now, + }); + } +} diff --git a/src/Classes/API/ApiConnService/index.ts b/src/Classes/API/ApiConnService/index.ts new file mode 100644 index 00000000..1c553ed8 --- /dev/null +++ b/src/Classes/API/ApiConnService/index.ts @@ -0,0 +1,3 @@ +export * from "./ApiConnService"; +export * from "./routes"; +export type * from "./types"; diff --git a/src/Classes/API/ApiConnService/routes.ts b/src/Classes/API/ApiConnService/routes.ts new file mode 100644 index 00000000..c667b49b --- /dev/null +++ b/src/Classes/API/ApiConnService/routes.ts @@ -0,0 +1,33 @@ +export const Routes = { + discordWarns: "/discord/warns" as const, + + discordWarn(warnId: string): `/discord/warns/${string}` { + return `/discord/warns/${warnId}`; + }, + + discordEvents: "/discordEvents" as const, + + latestDiscordEvent( + eventDiscordId: string, + ): `/discordEvents/byDiscordId/${string}/latest` { + return `/discordEvents/byDiscordId/${eventDiscordId}/latest`; + }, + + discordEvent(id: number): `/discordEvents/${string}` { + return `/discordEvents/${id}`; + }, + + discordEventAttendance( + eventId: number, + ): `/discordEvents/${string}/attendance` { + return `/discordEvents/${eventId}/attendance`; + }, + + setting(name: string): `/settings/${string}` { + return `/settings/${name}`; + }, + + discordStateRole(abbr: string): `/discord/state-roles/${string}` { + return `/discord/state-roles/${abbr}`; + }, +}; diff --git a/src/Classes/API/ApiConnService/types.ts b/src/Classes/API/ApiConnService/types.ts new file mode 100644 index 00000000..e45e655b --- /dev/null +++ b/src/Classes/API/ApiConnService/types.ts @@ -0,0 +1,80 @@ +import { Readable } from "node:stream"; +import z from "zod"; + +export interface ApiConnServiceOptions { + host: string; +} + +export interface RequestData { + body?: BodyInit | undefined; + /** + * Additional headers to add to this request + */ + headers?: Record; + /** + * Query string parameters to append to the called endpoint + */ + query?: URLSearchParams; + /** + * The signal to abort the queue entry or the REST call, where applicable + */ + signal?: AbortSignal | undefined; + + attempt?: number; +} + +export type RouteLike = `/${string}`; + +/** + * Possible API methods to be used when doing requests + */ +export enum RequestMethod { + Delete = "DELETE", + Get = "GET", + Patch = "PATCH", + Post = "POST", + Put = "PUT", +} + +/** + * Internal request options + */ +export interface InternalRequest extends RequestData { + fullRoute: RouteLike; + method: RequestMethod; +} + +export interface ResponseLike extends Pick< + Response, + | "arrayBuffer" + | "bodyUsed" + | "headers" + | "json" + | "ok" + | "status" + | "statusText" + | "text" +> { + body: Readable | ReadableStream | null; +} + +export const zAPIWarn = z.object({ + id: z.number(), + userWarnedDiscordId: z.string(), + moderatorDiscordId: z.string(), + reason: z.string(), + createdAtUtc: z.string(), + expiresAtUtc: z.string(), + updatedAtUtc: z.string(), +}); + +export type APIWarn = z.infer; + +export const zAPIWarnPage = z.object({ + page: z.number(), + limit: z.number(), + count: z.number(), + data: z.array(zAPIWarn), +}); + +export type APIWarnPage = z.infer; diff --git a/src/Classes/API/ApiConnService/utils.ts b/src/Classes/API/ApiConnService/utils.ts new file mode 100644 index 00000000..891dc785 --- /dev/null +++ b/src/Classes/API/ApiConnService/utils.ts @@ -0,0 +1,14 @@ +import z, { ZodType } from "zod"; + +/** + * Converts the response to usable data + * + * @param res - The fetch response + */ +export async function parseResponse(res: Response, schema?: ZodType) { + if (!res.body) return; + const data = (await res.json()) as unknown; + + if (!schema) return data as R; + return z.parse(schema, data) as R; +} diff --git a/src/Classes/API/Warn.ts b/src/Classes/API/Warn.ts new file mode 100644 index 00000000..d1b2d4cd --- /dev/null +++ b/src/Classes/API/Warn.ts @@ -0,0 +1,40 @@ +import { ApiConnService } from "./ApiConnService/ApiConnService"; +import { APIWarn } from "./ApiConnService/types"; + +export class Warn { + data: APIWarn; + api: ApiConnService; + + constructor(api: ApiConnService, data: APIWarn) { + this.data = data; + this.api = api; + } + + get id() { + return this.data.id; + } + + get reason() { + return this.data.reason; + } + + get targetId() { + return this.data.userWarnedDiscordId; + } + + get moderatorId() { + return this.data.moderatorDiscordId; + } + + get createdAt() { + return new Date(this.data.createdAtUtc); + } + + get expiresAt() { + return new Date(this.data.expiresAtUtc); + } + + get updatedAt() { + return new Date(this.data.updatedAtUtc); + } +} diff --git a/src/Classes/Client/Caches/EventLogMessageCache.ts b/src/Classes/Client/Caches/EventLogMessageCache.ts new file mode 100644 index 00000000..8bd70742 --- /dev/null +++ b/src/Classes/Client/Caches/EventLogMessageCache.ts @@ -0,0 +1,46 @@ +import { DiscordEvent } from "@/contracts/data/DiscordEvent"; + +export class EventLogMessageCache { + private cache: Record = {}; + + push(logChannelId: string, event: DiscordEvent) { + this.cache[logChannelId] = event; + } + + fetch(logMessageId: string): DiscordEvent | undefined; + fetch(eventId: number): string | undefined; + fetch(arg: string | number): DiscordEvent | string | undefined { + if (typeof arg === "string") { + try { + return this.cache[arg]; + } catch { + return undefined; + } + } else { + return Object.keys(this.cache).find((x) => this.cache[x].id === arg); + } + } + + delete(logMessageId: string): boolean; + delete(eventId: number): boolean; + delete(arg: string | number): boolean { + if (typeof arg === "string") { + try { + delete this.cache[arg]; + return true; + } catch { + return false; + } + } else { + const record = Object.keys(this.cache).find( + (x) => this.cache[x].id === arg, + ); + + if (!record) return false; + + delete this.cache[record]; + + return true; + } + } +} diff --git a/src/Classes/Client/Client.ts b/src/Classes/Client/Client.ts index 3b47c82e..855a09ae 100644 --- a/src/Classes/Client/Client.ts +++ b/src/Classes/Client/Client.ts @@ -1,10 +1,6 @@ import { Client, Events } from "discord.js"; -import { - CommandHandler, - EventHandler, - InteractionHandler, -} from "../Handlers/index.js"; -import { ExtendedClientOptions } from "./interfaces.js"; +import { CommandHandler, EventHandler, InteractionHandler } from "../Handlers"; +import { ExtendedClientOptions } from "./interfaces"; /** * Client is extended from the {@link Client}. diff --git a/src/Classes/Client/index.ts b/src/Classes/Client/index.ts index 2a489d5e..851f981c 100644 --- a/src/Classes/Client/index.ts +++ b/src/Classes/Client/index.ts @@ -1,5 +1,5 @@ -export { ExtendedClient as Client } from "./Client.js"; +export { ExtendedClient as Client } from "./Client"; -export { ExtraColor } from "./types.js"; +export { ExtraColor } from "./types"; -export type { ExtendedClientOptions } from "./interfaces.js"; +export type { ExtendedClientOptions } from "./interfaces"; diff --git a/src/Classes/Client/types.ts b/src/Classes/Client/types.ts index fcfd713e..10c981dd 100644 --- a/src/Classes/Client/types.ts +++ b/src/Classes/Client/types.ts @@ -1,5 +1,5 @@ import { time, TimestampStyles, TimestampStylesString } from "discord.js"; -import { ExtendedClient } from "./Client.js"; +import { ExtendedClient } from "./Client"; export const ExtraColor = { EmbedGray: 0x2b2d31, diff --git a/src/Classes/Commands/BaseCommand.ts b/src/Classes/Commands/BaseCommand.ts index cd622941..c0c77831 100644 --- a/src/Classes/Commands/BaseCommand.ts +++ b/src/Classes/Commands/BaseCommand.ts @@ -4,7 +4,7 @@ import { ContextMenuCommandInteraction, Snowflake, } from "discord.js"; -import { AnySlashCommandBuilder } from "./types.js"; +import { AnySlashCommandBuilder } from "./types"; /** * BaseCommand represents a command that the PV bot can handle. This is a combination of a @@ -27,7 +27,7 @@ export class BaseCommand< protected _guildIds: Snowflake[]; - protected _execute?: (interaction: TypeInteraction) => void; + protected _execute?: (interaction: TypeInteraction) => Promise; get name() { return this.builder.name; @@ -63,7 +63,7 @@ export class BaseCommand< * @param execute - the interaction handler * @returns The modified object */ - setExecute(execute: (interaction: TypeInteraction) => void): this { + setExecute(execute: (interaction: TypeInteraction) => Promise): this { this._execute = execute; return this; } diff --git a/src/Classes/Commands/Commands.ts b/src/Classes/Commands/Commands.ts index f291f601..ca2a0105 100644 --- a/src/Classes/Commands/Commands.ts +++ b/src/Classes/Commands/Commands.ts @@ -7,8 +7,8 @@ import { ContextMenuCommandType, SlashCommandBuilder, } from "discord.js"; -import { BaseCommand } from "./BaseCommand.js"; -import { AnySlashCommandBuilder } from "./types.js"; +import { BaseCommand } from "./BaseCommand"; +import { AnySlashCommandBuilder } from "./types"; /** * Represents a PV Bot command that is invoked via a slash command and provides an interaction interface @@ -22,7 +22,9 @@ export class ChatInputCommand extends BaseCommand< * Runs when client receives and Autocomplete interaction * @param interaction - Autocomplete interaction received by the client */ - protected _autocomplete?: (interaction: AutocompleteInteraction) => void; + protected _autocomplete?: ( + interaction: AutocompleteInteraction, + ) => Promise; get autocomplete() { if (this._autocomplete === undefined) @@ -58,7 +60,7 @@ export class ChatInputCommand extends BaseCommand< * @returns The modified object */ public setAutocomplete( - autocomplete: (interaction: AutocompleteInteraction) => void, + autocomplete: (interaction: AutocompleteInteraction) => Promise, ) { this._autocomplete = autocomplete; return this; diff --git a/src/Classes/Commands/index.ts b/src/Classes/Commands/index.ts index ab8f6e24..aa296d9f 100644 --- a/src/Classes/Commands/index.ts +++ b/src/Classes/Commands/index.ts @@ -1,5 +1,5 @@ -export { BaseCommand } from "./BaseCommand.js"; +export { BaseCommand } from "./BaseCommand"; -export { ChatInputCommand, ContextMenuCommand } from "./Commands.js"; +export { ChatInputCommand, ContextMenuCommand } from "./Commands"; -export type { AnySlashCommandBuilder, builders, TypeCommand } from "./types.js"; +export type { AnySlashCommandBuilder, TypeCommand, builders } from "./types"; diff --git a/src/Classes/Commands/types.ts b/src/Classes/Commands/types.ts index 2053ab68..041f3fd2 100644 --- a/src/Classes/Commands/types.ts +++ b/src/Classes/Commands/types.ts @@ -6,7 +6,7 @@ import { SlashCommandOptionsOnlyBuilder, SlashCommandSubcommandsOnlyBuilder, } from "discord.js"; -import { BaseCommand } from "./BaseCommand.js"; +import { BaseCommand } from "./BaseCommand"; export type builders = SlashCommandBuilder | ContextMenuCommandBuilder; diff --git a/src/Classes/Handlers/CommandHandler.ts b/src/Classes/Handlers/CommandHandler.ts index fc436fec..8eb2a7f5 100644 --- a/src/Classes/Handlers/CommandHandler.ts +++ b/src/Classes/Handlers/CommandHandler.ts @@ -10,8 +10,8 @@ import { Snowflake, UserContextMenuCommandInteraction, } from "discord.js"; -import { Client } from "../Client/index.js"; -import { ChatInputCommand, ContextMenuCommand } from "../Commands/index.js"; +import { Client } from "../Client"; +import { ChatInputCommand, ContextMenuCommand } from "../Commands"; /** * Manages all chat and context menu commands that the PV bot supports. The {@link CommandHandler}: diff --git a/src/Classes/Handlers/EventHandler.ts b/src/Classes/Handlers/EventHandler.ts index ad7fe519..1b33a6f0 100644 --- a/src/Classes/Handlers/EventHandler.ts +++ b/src/Classes/Handlers/EventHandler.ts @@ -1,5 +1,5 @@ import { Client, Collection } from "discord.js"; -import { Event } from "../Event.js"; +import { Event } from "../Event"; /** * Manages all events that the PV bot supports. The EventHandler: diff --git a/src/Classes/Handlers/InteractionHandler.ts b/src/Classes/Handlers/InteractionHandler.ts index 9e0388b4..9d50e837 100644 --- a/src/Classes/Handlers/InteractionHandler.ts +++ b/src/Classes/Handlers/InteractionHandler.ts @@ -4,8 +4,8 @@ import { Collection, ModalSubmitInteraction, } from "discord.js"; -import { Client } from "../Client/index.js"; -import { Interaction } from "../Interaction.js"; +import { Client } from "../Client"; +import { Interaction } from "../Interaction"; /** * Manages all interactions that the PV bot supports. The {@link InteractionHandler}: diff --git a/src/Classes/Handlers/index.ts b/src/Classes/Handlers/index.ts index 4438c22d..6ff1649e 100644 --- a/src/Classes/Handlers/index.ts +++ b/src/Classes/Handlers/index.ts @@ -1,5 +1,5 @@ -export { CommandHandler } from "./CommandHandler.js"; +export { CommandHandler } from "./CommandHandler"; -export { EventHandler } from "./EventHandler.js"; +export { EventHandler } from "./EventHandler"; -export { InteractionHandler } from "./InteractionHandler.js"; +export { InteractionHandler } from "./InteractionHandler"; diff --git a/src/Classes/i18n/i18n.ts b/src/Classes/i18n/i18n.ts index 2527782a..6e74f1f4 100644 --- a/src/Classes/i18n/i18n.ts +++ b/src/Classes/i18n/i18n.ts @@ -2,9 +2,9 @@ import { FluentBundle, FluentResource } from "@fluent/bundle"; import { Collection } from "discord.js"; import { readFileSync, readdirSync } from "fs"; import { join } from "path"; -import { i18nOptions } from "./interface.js"; -import { LocaleBundle } from "./locale.js"; -import { Locale, LocalizationMap, fluentVariables } from "./types.js"; +import { i18nOptions } from "./interface"; +import { LocaleBundle } from "./locale"; +import { Locale, LocalizationMap, fluentVariables } from "./types"; /** * Manages localization logic for the PV bot. Includes: diff --git a/src/Classes/i18n/index.ts b/src/Classes/i18n/index.ts index dd6c69e5..862112d4 100644 --- a/src/Classes/i18n/index.ts +++ b/src/Classes/i18n/index.ts @@ -1,9 +1,9 @@ -export { i18n } from "./i18n.js"; +export { i18n } from "./i18n"; -export { LocaleBundle } from "./locale.js"; +export { LocaleBundle } from "./locale"; -export { Locale } from "./types.js"; +export { Locale } from "./types"; -export type { fluentVariables } from "./types.js"; +export type { fluentVariables } from "./types"; -export type { i18nOptions } from "./interface.js"; +export type { i18nOptions } from "./interface"; diff --git a/src/Classes/i18n/locale.ts b/src/Classes/i18n/locale.ts index 4bc7b94c..f874b8a5 100644 --- a/src/Classes/i18n/locale.ts +++ b/src/Classes/i18n/locale.ts @@ -1,7 +1,7 @@ import { FluentBundle, Message } from "@fluent/bundle"; import { Collection } from "discord.js"; -import { i18n } from "./i18n.js"; -import { Locale, common, fluentVariables } from "./types.js"; +import { i18n } from "./i18n"; +import { Locale, common, fluentVariables } from "./types"; export class LocaleBundle { /** diff --git a/src/Classes/index.ts b/src/Classes/index.ts index 0d21f9dc..b50166f4 100644 --- a/src/Classes/index.ts +++ b/src/Classes/index.ts @@ -1,9 +1,9 @@ -export { ChatInputCommand, ContextMenuCommand } from "./Commands/index.js"; +export { ChatInputCommand, ContextMenuCommand } from "./Commands"; -export { Interaction } from "./Interaction.js"; +export { Interaction } from "./Interaction"; -export { Client, ExtraColor } from "./Client/index.js"; +export { Client, ExtraColor } from "./Client"; -export { Event } from "./Event.js"; +export { Event } from "./Event"; -export { i18n, LocaleBundle } from "./i18n/index.js"; +export { LocaleBundle, i18n } from "./i18n"; diff --git a/src/commands/chat/README.md b/src/commands/chat/README.md index efb46f47..6f132718 100644 --- a/src/commands/chat/README.md +++ b/src/commands/chat/README.md @@ -19,7 +19,7 @@ Additionally, you may want to group commands together. This is done with `Subcom ```ts // src/commands/chat/ping.ts - import { ChatInputCommand } from "../../Classes/index.js"; + import { ChatInputCommand } from "@/Classes"; export default new ChatInputCommand() .setBuilder((builder) => @@ -34,15 +34,15 @@ Additionally, you may want to group commands together. This is done with `Subcom ```ts // src/commands/index.ts - export { default as ping } from "./chat/ping.js"; + export { default as ping } from "./chat/ping"; ``` -3. Register the commands with the client in the root [`index.ts`](../../index.ts). +3. Register the commands with the client in the root [`index.ts`](@/index.ts). ```ts // src/index.ts - import { Client } from "./Classes/index.js"; - import * as commands from "./commands/index.js"; + import { Client } from "./Classes"; + import * as commands from "./commands"; const client = new Client(); diff --git a/src/commands/chat/feedback.ts b/src/commands/chat/feedback.ts index b8e235b1..7bf5da2a 100644 --- a/src/commands/chat/feedback.ts +++ b/src/commands/chat/feedback.ts @@ -1,11 +1,11 @@ +import { ChatInputCommand } from "@/Classes"; +import { localize } from "@/i18n"; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags, } from "discord.js"; -import { ChatInputCommand } from "../../Classes/index.js"; -import { localize } from "../../i18n.js"; export const ns = "feedback"; diff --git a/src/commands/chat/moderation.ts b/src/commands/chat/moderation.ts deleted file mode 100644 index cd6e296d..00000000 --- a/src/commands/chat/moderation.ts +++ /dev/null @@ -1,356 +0,0 @@ -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - ChatInputCommandInteraction, - inlineCode, - InteractionContextType, - InteractionReplyOptions, - MessageFlags, - PermissionFlagsBits, - SlashCommandBuilder, - SlashCommandStringOption, -} from "discord.js"; -import { FilterQuery } from "mongoose"; -import { ChatInputCommand } from "../../Classes/index.js"; -import { - modViewWarningHistory, - updateWarnById, -} from "../../features/moderation/buttons.js"; -import { - viewWarningEmbed, - viewWarningEmbeds, -} from "../../features/moderation/embeds.js"; -import { - dateDiffInDays, - WARN_MAX_CHAR, -} from "../../features/moderation/index.js"; -import { warnModal } from "../../features/moderation/modals.js"; -import { - WarnButtonsPrefixes, - WarnModalPrefixes, -} from "../../features/moderation/types.js"; -import { warnSearch } from "../../features/moderation/warnSearch.js"; -import { Warn, WarningRecord } from "../../models/Warn.js"; -import { WarningSearch } from "../../models/WarnSearch.js"; -import { AddSplitCustomId, isGuildMember } from "../../util/index.js"; -// import { localize } from "../../i18n.js"; - -export const ns = "moderation"; -const idOptions = new SlashCommandStringOption() - .setName("id") - .setDescription("Record Id") - .setMinLength(24) - .setMaxLength(24) - .setRequired(true); - -/** - * The `warn` mod command allows an admin to issue a warning to a guild member. It exposes - * the following subcommands: - *
    - *
  • `create` - create a warning for the specified guild member for the
  • - *
  • `update` - update a warning
  • - *
  • `remove` - remove a warning
  • - *
  • `view` - view warnings, optionally filtering by the recipient, issuer, or the time scope
  • - *
- */ -export const warn = new ChatInputCommand({ - builder: new SlashCommandBuilder() - .setName("warn") - .setDescription("Moderation commands") - .setContexts(InteractionContextType.Guild) - .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) - .addSubcommand((subCommand) => - subCommand - .setName("create") - .setDescription("Add warning to a member") - .addUserOption((option) => - option - .setName("member") - .setDescription("The member that will receive the warning") - .setRequired(true), - ) - .addStringOption((option) => - option - .setName("reason") - .setDescription("Add reason for the warning") - .setMaxLength(WARN_MAX_CHAR) - .setRequired(false), - ) - .addIntegerOption((option) => - option - .setName("duration") - .setDescription("Number of days, the warning till end of the warn") - .setMinValue(0) - .setMaxValue(999) - .setRequired(false), - ), - ) - .addSubcommand((subCommand) => - subCommand - .setName("update") - .setDescription("Update Warning") - .addStringOption(idOptions.setRequired(true)), - ) - .addSubcommand((subCommand) => - subCommand - .setName("remove") - .setDescription("Remove warn") - .addStringOption(idOptions.setRequired(true)) - .addBooleanOption((option) => - option - .setName("delete") - .setDescription("To delete the record") - .setRequired(false), - ), - ) - .addSubcommand((subCommand) => - subCommand - .setName("view") - .setDescription("View warnings") - .addStringOption( - idOptions - .setDescription( - "Search by Record Id. Overrides other search options", - ) - .setRequired(false), - ) - .addUserOption((option) => - option - .setName("recipient") - .setDescription("Filter be the member who received the warning") - .setRequired(false), - ) - .addUserOption((option) => - option - .setName("issuer") - .setDescription("Filter be the member who issued the warning") - .setRequired(false), - ) - .addIntegerOption((option) => - option - .setName("scope") - .setDescription("Filter warnings by date issued the last x months") - .addChoices( - { name: "All", value: 0 }, - { name: "3 Months", value: 3 }, - { name: "6 Months", value: 6 }, - { name: "9 Months", value: 9 }, - { name: "1 year", value: 12 }, - ) - .setRequired(false), - ), - ), - execute: async (interaction) => { - const subcommand = interaction.options.getSubcommand(true); - - switch (subcommand) { - case "create": - chatAdd(interaction); - break; - case "update": - update(interaction); - break; - case "remove": - remove(interaction); - break; - case "view": - viewWarning(interaction); - break; - default: - throw Error("Unexpected Warn subcommand"); - } - - return undefined; - }, -}); - -/** - * Send modal to add warning to user - * @param interaction - command interaction from user - */ -function chatAdd(interaction: ChatInputCommandInteraction) { - const target = interaction.options.getMember("member"); - if (!isGuildMember(target)) return; - const chatDuration = interaction.options.getInteger("duration") ?? undefined; - const chatReason = interaction.options.getString("reason") ?? undefined; - if (target == interaction.member) { - interaction.reply({ - flags: MessageFlags.Ephemeral, - content: "You can not warn your self", - }); - return; - } else if (target.user.bot) { - interaction.reply({ - flags: MessageFlags.Ephemeral, - content: "You can not issue a warning to a bot", - }); - return; - } - - const modal = warnModal( - AddSplitCustomId(WarnModalPrefixes.createWarning, target.id), - "Create Warning", - chatReason, - chatDuration, - ); - - interaction.showModal(modal); -} - -/** - * view warning(s) - * @param interaction - command interaction from user - */ -async function viewWarning(interaction: ChatInputCommandInteraction) { - const mod = interaction.options.getUser("issuer"); - const target = interaction.options.getUser("recipient"); - const monthsAgo = interaction.options.getInteger("scope") ?? -1; - const id = interaction.options.getString("id"); - const filter: FilterQuery = {}; - - if (id) { - const record = await Warn.findById(id); - if (record) { - const embeds = await viewWarningEmbeds([record], true); - const actionRow = new ActionRowBuilder(); - if (record.expireAt > new Date()) { - actionRow.addComponents(updateWarnById(record)); - } - - actionRow.addComponents(modViewWarningHistory(record.target.discordId)); - - interaction.reply({ - flags: MessageFlags.Ephemeral, - embeds, - components: [actionRow], - }); - return; - } else if (!mod && !target && monthsAgo === -1) { - interaction.reply({ - flags: MessageFlags.Ephemeral, - content: - "No search record found. Please let an admin know if you see this message", - }); - return; - } - } - - let expireAfter: Date | undefined = undefined; - - if (monthsAgo === -1) { - expireAfter = new Date(); - filter.expireAt = { $gte: expireAfter }; - } else if (monthsAgo > 0) { - expireAfter = new Date(); - expireAfter.setMonth(-monthsAgo); - filter.expireAt = { $gte: expireAfter }; - } - - const searchRecord = await WarningSearch.create({ - guildId: interaction.guild?.id, - searcher: { - discordId: interaction.user.id, - username: interaction.user.username, - }, - targetDiscordId: target?.id, - moderatorDiscordId: mod?.id, - expireAfter, - pageStart: 0, - }); - - const reply: InteractionReplyOptions = await warnSearch( - searchRecord, - true, - undefined, - true, - ); - reply.flags = MessageFlags.Ephemeral; - - interaction.reply(reply); -} - -/** - * Update a warning - * @param interaction - command interaction from user - */ -async function update(interaction: ChatInputCommandInteraction) { - const warnId = interaction.options.getString("id", true); - const record = await Warn.findById(warnId); - - if (!record) { - interaction.reply({ - content: "Warning could not be found", - flags: MessageFlags.Ephemeral, - }); - } else { - interaction.showModal( - warnModal( - AddSplitCustomId(WarnModalPrefixes.updateById, warnId), - "Update Warning", - record.reason, - dateDiffInDays(new Date(), record.expireAt), - ), - ); - } -} - -/** - * Remove a warning - * @param interaction - command interaction from user - */ -async function remove(interaction: ChatInputCommandInteraction) { - const warnId = interaction.options.getString("id", true); - const record = await Warn.findById(warnId); - const del = interaction.options.getBoolean("delete") ?? false; - const reply: InteractionReplyOptions = { flags: MessageFlags.Ephemeral }; - if (!record) { - reply.content = "Warn could not be found. Check your the warn id"; - interaction.reply(reply); - return; - } - const embed = await viewWarningEmbed(record, true); - if (!embed) { - reply.content = "An Error occurred. Please contact an admin"; - interaction.reply(reply); - return; - } - - reply.embeds = [embed]; - - const cancelButton: ButtonBuilder = new ButtonBuilder() - .setLabel("Cancel") - .setStyle(ButtonStyle.Success); - const approveButton: ButtonBuilder = new ButtonBuilder().setStyle( - ButtonStyle.Danger, - ); - - if (del) { - reply.content = `Are you sure you want to delete warning: ${inlineCode(warnId)}`; - - cancelButton.setCustomId( - AddSplitCustomId(WarnButtonsPrefixes.deleteWarnNo, warnId), - ); - approveButton - .setLabel("Delete") - .setCustomId(AddSplitCustomId(WarnButtonsPrefixes.deleteWarnYes, warnId)); - } else { - reply.content = `Are you sure you want to end warning: ${inlineCode(warnId)}`; - - cancelButton.setCustomId( - AddSplitCustomId(WarnButtonsPrefixes.removeWarnNo, warnId), - ); - approveButton - .setLabel("End") - .setCustomId(AddSplitCustomId(WarnButtonsPrefixes.removeWarnYes, warnId)); - } - - reply.components = [ - new ActionRowBuilder().addComponents( - cancelButton, - approveButton, - ), - ]; - - interaction.reply(reply); -} diff --git a/src/commands/chat/moderation/create.ts b/src/commands/chat/moderation/create.ts new file mode 100644 index 00000000..a5a190de --- /dev/null +++ b/src/commands/chat/moderation/create.ts @@ -0,0 +1,38 @@ +import { WARN_MAX_CHAR } from "@/features/moderation"; +import { + ChatInputCommandInteraction, + LabelBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + UserSelectMenuBuilder, +} from "discord.js"; + +export async function create(interaction: ChatInputCommandInteraction) { + const targetMenu = new UserSelectMenuBuilder() + .setCustomId("member") + .setRequired(true); + const targetUser = interaction.options.getUser("member"); + + if (targetUser) targetMenu.setDefaultUsers(targetUser?.id); + + const newWarn = new ModalBuilder() + .setCustomId("nw") + .setTitle("New Warning") + .addLabelComponents( + new LabelBuilder() + .setLabel("Target Member") + .setDescription("Member to whom this is issued to") + .setUserSelectMenuComponent(targetMenu), + new LabelBuilder() + .setLabel("Reason") + .setTextInputComponent( + new TextInputBuilder() + .setCustomId("reason") + .setMaxLength(WARN_MAX_CHAR) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true), + ), + ); + await interaction.showModal(newWarn); +} diff --git a/src/commands/chat/moderation/index.ts b/src/commands/chat/moderation/index.ts new file mode 100644 index 00000000..43de6d5d --- /dev/null +++ b/src/commands/chat/moderation/index.ts @@ -0,0 +1,96 @@ +import { ChatInputCommand } from "@/Classes"; +import { + InteractionContextType, + PermissionFlagsBits, + SlashCommandBuilder, +} from "discord.js"; +// import { create } from "./create"; +import { create } from "./create"; +import { view } from "./view"; + +/** + * The `warn` mod command allows an admin to issue a warning to a guild member. It exposes + * the following subcommands: + *
    + *
  • `create` - create a warning for the specified guild member for the
  • + *
  • `view` - view warnings, optionally filtering by the recipient, issuer, or the time scope
  • + *
+ */ +export const warn = new ChatInputCommand({ + builder: new SlashCommandBuilder() + .setName("warn") + .setDescription("Moderation commands") + .setContexts(InteractionContextType.Guild) + .setDefaultMemberPermissions( + PermissionFlagsBits.KickMembers | PermissionFlagsBits.BanMembers, + ) + + .addSubcommand((subCommand) => + subCommand + .setName("create") + .setDescription("Add warning to a member") + + .addUserOption((option) => + option + .setName("member") + .setDescription("The member that will receive the warning"), + ), + ) + .addSubcommand((subCommand) => + subCommand + .setName("view") + .setDescription("View warnings") + + .addUserOption((option) => + option + .setName("recipient") + .setDescription("Filter by the member who received the warning") + .setRequired(false), + ) + .addUserOption((option) => + option + .setName("moderator") + .setDescription("Filter by the member who issued the warning") + .setRequired(false), + ) + .addIntegerOption((option) => + option + .setName("scope") + .setDescription( + "Filter warnings by date issued in the last x months", + ) + .addChoices( + { name: "All", value: 0 }, + { name: "3 Months", value: 3 }, + { name: "6 Months", value: 6 }, + { name: "9 Months", value: 9 }, + { name: "1 year", value: 12 }, + ) + .setRequired(false), + ) + .addStringOption((option) => + option + .setName("order") + .setDescription("The order in which warns are displayed") + .setChoices( + { name: "Ascending", value: "asc" }, + { name: "Descending", value: "desc" }, + ) + .setRequired(false), + ), + ), + execute: async (interaction) => { + const subcommand = interaction.options.getSubcommand(true); + + switch (subcommand) { + case "create": + create(interaction); + break; + case "view": + view(interaction); + break; + default: + throw Error("Unexpected Warn subcommand"); + } + }, +}); diff --git a/src/commands/chat/moderation/view.ts b/src/commands/chat/moderation/view.ts new file mode 100644 index 00000000..d1ef94e0 --- /dev/null +++ b/src/commands/chat/moderation/view.ts @@ -0,0 +1,34 @@ +import { WarnSortOption } from "@/Classes/API/ApiConnService/WarnSearchmanager"; +import { warnPage } from "@/features/moderation/warn-render"; +import { warnSearchManger } from "@/util/api/pvapi"; +import { ChatInputCommandInteraction, MessageFlags } from "discord.js"; + +export async function view(interaction: ChatInputCommandInteraction) { + await interaction.deferReply({ + flags: MessageFlags.Ephemeral, + // withResponse: true, + }); + if (!interaction.inCachedGuild()) return; + + const mod = interaction.options.getUser("moderator"); + const target = interaction.options.getUser("recipient"); + const monthsAgo = interaction.options.getInteger("scope") ?? 0; + const order = + (interaction.options.getString("order") as WarnSortOption) ?? + WarnSortOption.Descending; + + const search = warnSearchManger.newSearch(interaction.member, { + moderatorId: mod?.id, + targetId: target?.id, + monthsAgo: monthsAgo, + sort: order, + }); + + await search.fetchPage(); + + await interaction.editReply({ + flags: MessageFlags.IsComponentsV2, + components: await warnPage(search), + allowedMentions: {}, + }); +} diff --git a/src/commands/chat/move.ts b/src/commands/chat/move.ts index 85686af7..c8455117 100644 --- a/src/commands/chat/move.ts +++ b/src/commands/chat/move.ts @@ -1,3 +1,6 @@ +import { ChatInputCommand } from "@/Classes"; +import { localize } from "@/i18n"; +import { getMember } from "@/util"; import { ActionRowBuilder, ChannelType, @@ -5,10 +8,7 @@ import { PermissionsBitField, UserSelectMenuBuilder, } from "discord.js"; -import { ChatInputCommand } from "../../Classes/index.js"; -import { localize } from "../../i18n.js"; -import { client } from "../../index.js"; -import { getMember } from "../../util/index.js"; +import { client } from "../.."; export const ns = "move"; diff --git a/src/commands/chat/mute.ts b/src/commands/chat/mute.ts index 74c8e2eb..ecbae940 100644 --- a/src/commands/chat/mute.ts +++ b/src/commands/chat/mute.ts @@ -1,3 +1,11 @@ +import { ChatInputCommand } from "@/Classes"; +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { + SettingsResponse, + zSettingsResponse, +} from "@/contracts/responses/SettingsResponse"; +import { getGuildChannel } from "@/util"; +import { apiConnService } from "@/util/api/pvapi"; import { ContainerBuilder, EmbedBuilder, @@ -15,9 +23,6 @@ import { time, TimestampStyles, } from "discord.js"; -import { ChatInputCommand } from "../../Classes/index.js"; -import { GuildSetting } from "../../models/Setting.js"; -import { getGuildChannel } from "../../util/index.js"; const MUTE_COLOR = 0x7c018c; @@ -104,6 +109,12 @@ export const mute = new ChatInputCommand({ mutingMember = await guild.members.fetch(interaction.user); } + if (!targetMember.voice.channel) + return interaction.reply({ + flags: MessageFlags.Ephemeral, + content: "User is not in a vc.", + }); + // and for how long const durationMinutes = interaction.options.getInteger("duration", true); const reason = interaction.options.getString("reason", true); @@ -144,15 +155,18 @@ async function logMessage( reason: string, ) { // check if log channel is set - const settings = await GuildSetting.findOne({ - guildId: targetMember.guild.id, - }); - if (!settings?.logging.timeoutChannelId) return; + const res = await apiConnService.get( + Routes.setting("timeout_log_channel_id"), + zSettingsResponse, + ); + + const timeoutLogChannelId = res.data; + if (timeoutLogChannelId) return; // check that channel is real const timeoutChannel = await getGuildChannel( targetMember.guild, - settings.logging.timeoutChannelId, + timeoutLogChannelId, ); if (!timeoutChannel?.isSendable()) return; diff --git a/src/commands/chat/search-events.ts b/src/commands/chat/search-events.ts index 60a8be90..b48d883b 100644 --- a/src/commands/chat/search-events.ts +++ b/src/commands/chat/search-events.ts @@ -1,13 +1,14 @@ +import { ChatInputCommand } from "@/Classes"; import { ChatInputCommandInteraction, Guild, GuildScheduledEvent, + GuildScheduledEventStatus, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder, } from "discord.js"; -import { ChatInputCommand } from "../../Classes/index.js"; export const searchEvents = new ChatInputCommand({ builder: new SlashCommandBuilder() @@ -157,7 +158,7 @@ async function findEventsMatchingQuery( async function directMessageEvents( interaction: ChatInputCommandInteraction, - events: any[] | null, + events: GuildScheduledEvent[] | null, ) { if (events === null || events.length === 0) { await interaction.followUp({ diff --git a/src/commands/chat/setting.ts b/src/commands/chat/setting.ts index f615088a..075309d9 100644 --- a/src/commands/chat/setting.ts +++ b/src/commands/chat/setting.ts @@ -1,3 +1,6 @@ +import { ChatInputCommand } from "@/Classes"; +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { apiConnService } from "@/util/api/pvapi"; import { ChannelType, inlineCode, @@ -8,9 +11,6 @@ import { SlashCommandBuilder, SlashCommandChannelOption, } from "discord.js"; -import { UpdateQuery } from "mongoose"; -import { ChatInputCommand } from "../../Classes/index.js"; -import { GuildSetting, ISettings } from "../../models/Setting.js"; const channel = new SlashCommandChannelOption() .setName("channel") @@ -42,7 +42,7 @@ export const settings = new ChatInputCommand({ .addSubcommandGroup((subcommandGroup) => subcommandGroup .setName("warn") - .setDescription("configure wanning system") + .setDescription("configure warning system") .addSubcommand((subCommand) => subCommand .setName("channels") @@ -52,7 +52,7 @@ export const settings = new ChatInputCommand({ .setName("setting") .setDescription("Setting to edit") .setChoices( - { name: "log", value: "warn.logChannelId" }, + { name: "log", value: "warn_log_channel_id" }, // { name: 'appeal', value: 'warn.appealChannelId' }, ) .setRequired(true), @@ -72,7 +72,7 @@ export const settings = new ChatInputCommand({ option .setName("setting") .setDescription("Setting to edit") - .setChoices({ name: "log", value: "report.logChannelId" }) + .setChoices({ name: "log", value: "report_log_channel_id" }) .setRequired(true), ) .addChannelOption(channel), @@ -108,21 +108,21 @@ export const settings = new ChatInputCommand({ .setName("setting") .setDescription("Setting to edit") .setChoices( - { name: "timeouts", value: "logging.timeoutChannelId" }, - { name: "leaves", value: "logging.leaveChannelId" }, + { name: "timeouts", value: "timeout_log_channel_id" }, + { name: "leaves", value: "leave_log_channel_id" }, { name: "channel updates", - value: "logging.channelUpdatesChannelId", + value: "channel_updates_log_channel_id", }, { name: "vc updates", - value: "logging.voiceUpdatesChannelId", + value: "voice_updates_log_channel_id", }, { name: "nickname updates", - value: "logging.nicknameUpdatesChannelId", + value: "nickname_updates_log_channel_id", }, - { name: "event logs", value: "logging.eventLogChannelId" }, + { name: "event logs", value: "event_log_channel_id" }, ) .setRequired(true), ) @@ -140,11 +140,20 @@ export const settings = new ChatInputCommand({ ChannelType.GuildText, ChannelType.PublicThread, ]); - await GuildSetting.findOneAndUpdate( - { guildId: interaction.guildId }, - { "welcome.channelId": channel.id }, - ); - reply.content = `welcome channel set to ${channel}`; + + try { + await apiConnService.put(Routes.setting("welcome_channel_id"), { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ value: channel.id }), + }); + + reply.content = `welcome channel set to ${channel}`; + } catch (err) { + console.error(err); + reply.content = `failed to modify welcome channel to ${channel}`; + } } // else if (subCommand === 'role') { @@ -166,17 +175,24 @@ export const settings = new ChatInputCommand({ ChannelType.PublicThread, ]); - const update: UpdateQuery = {}; - update[setting] = channel.id; + let msg; + try { + await apiConnService.put(Routes.setting(setting), { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ value: channel.id }), + }); - await GuildSetting.findOneAndUpdate( - { guildId: interaction.guildId }, - update, - ); + msg = `${inlineCode(setting)} has been updated to ${channel}`; + } catch (err) { + console.error(err); + msg = `${inlineCode(setting)} has encountered an error and has not been updated to ${channel}`; + } interaction.reply({ flags: MessageFlags.Ephemeral, - content: `${inlineCode(setting)} has been updated to ${channel}`, + content: msg, }); } }, diff --git a/src/commands/chat/state-admin.ts b/src/commands/chat/state-admin.ts index 4665e0f9..a3c347dc 100644 --- a/src/commands/chat/state-admin.ts +++ b/src/commands/chat/state-admin.ts @@ -1,3 +1,8 @@ +import { ChatInputCommand } from "@/Classes"; +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { apiConnService } from "@/util/api/pvapi"; +import { IDiscordStateRole } from "@/util/states/discordStateRole"; +import { isStateAbbreviations, stateNames, states } from "@/util/states/types"; import { ApplicationCommandOptionType, ChannelType, @@ -6,13 +11,6 @@ import { MessageFlags, PermissionFlagsBits, } from "discord.js"; -import { ChatInputCommand } from "../../Classes/index.js"; -import { States } from "../../models/State.js"; -import { - isStateAbbreviations, - stateNames, - states, -} from "../../util/states/types.js"; /** * The `state-admin` allows a guild manager to configure the state system: @@ -82,7 +80,7 @@ export const stateAdmin = new ChatInputCommand() .setExecute(async (interaction) => { if (!interaction.inGuild()) return; - const { guildId, options } = interaction; + const { options } = interaction; const abbreviation = options.getString("state", true); if (!isStateAbbreviations(abbreviation)) { @@ -105,20 +103,34 @@ export const stateAdmin = new ChatInputCommand() const message: InteractionReplyOptions = { flags: MessageFlags.Ephemeral }; const name = stateNames.get(abbreviation)?.name; - let state = await States.findOne({ guildId, abbreviation, name }); - if (!state) state = new States({ guildId, abbreviation, name }); + const updatePkg: Partial = + new Object() as Partial; + if (!role && !channel) { message.content = `No update made to ${name} settings`; } else if (subcommandGroup === "team") { - if (role?.id) state.team.roleId = role.id; - if (channel?.id) state.team.channelId = channel.id; + if (role?.id) updatePkg.teamRoleId = role.id; + if (channel?.id) updatePkg.teamChannelId = channel.id; message.content = `updated state team for ${name} settings`; } else { - if (role?.id) state.roleId = role.id; - if (channel?.id) state.channelId = channel.id; + if (role?.id) updatePkg.memberRoleId = role.id; + if (channel?.id) updatePkg.memberChannelId = channel.id; message.content = `updated state for ${name} settings`; } - await state.save(); + + try { + await apiConnService.patch(Routes.discordStateRole(abbreviation), { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updatePkg), + }); + } catch (err) { + console.error(err); + //@ts-expect-error can't type error catches + return interaction.reply(err.message); + } + interaction.reply(message); }) .setAutocomplete((interaction) => { diff --git a/src/commands/chat/state.ts b/src/commands/chat/state.ts index 55170141..893c2197 100644 --- a/src/commands/chat/state.ts +++ b/src/commands/chat/state.ts @@ -1,16 +1,13 @@ +import { ChatInputCommand } from "@/Classes"; +import { lead } from "@/features/state"; +import { messageMaxLength, titleMaxLength } from "@/features/state/constants"; +import { localize } from "@/i18n"; +import { states } from "@/util/states/types"; import { ApplicationCommandOptionType, InteractionContextType, PermissionFlagsBits, } from "discord.js"; -import { ChatInputCommand } from "../../Classes/index.js"; -import { - messageMaxLength, - titleMaxLength, -} from "../../features/state/constants.js"; -import { lead } from "../../features/state/index.js"; -import { localize } from "../../i18n.js"; -import { states } from "../../util/states/types.js"; export const ns = "state"; diff --git a/src/commands/chat/timeout.ts b/src/commands/chat/timeout.ts index b6d7bdc7..ebead850 100644 --- a/src/commands/chat/timeout.ts +++ b/src/commands/chat/timeout.ts @@ -1,15 +1,22 @@ +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { ChatInputCommand } from "@/Classes/index"; import { + SettingsResponse, + zSettingsResponse, +} from "@/contracts/responses/SettingsResponse"; +import { timeoutEmbed } from "@/features/timeout"; +import { localize } from "@/i18n"; +import { getGuildChannel, isGuildMember } from "@/util"; +import { apiConnService } from "@/util/api/pvapi"; +import { + DiscordAPIError, Events, GuildMember, + inlineCode, InteractionContextType, MessageFlags, PermissionFlagsBits, } from "discord.js"; -import { ChatInputCommand } from "../../Classes/index.js"; -import { timeoutEmbed } from "../../features/timeout.js"; -import { localize } from "../../i18n.js"; -import { GuildSetting } from "../../models/Setting.js"; -import { getGuildChannel, isGuildMember } from "../../util/index.js"; export const ns = "timeout"; @@ -101,38 +108,45 @@ export const timeout = new ChatInputCommand() "received APIInteractionDataResolvedGuildMember when expecting guild member", ), ); - return interaction.reply({ + await interaction.reply({ content: localize.t("reply_error", ns, locale), flags: MessageFlags.Ephemeral, }); + return; } const reason = options.getString("reason", false) ?? "No reason given"; const duration = options.getNumber("duration", true); // const endNumber = Math.floor(new Date().getTime() / 1000) + duration; + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + try { + target = await target.timeout( + duration * 1000, + `Member was timed out by ${user.username} for ${reason}`, + ); + } catch (error) { + if (!(error instanceof DiscordAPIError)) throw error; + await interaction.editReply({ + content: `Command could not be completed because: ${inlineCode(error.message)}`, + }); + return; + } - target = await target.timeout( - duration * 1000, - `Member was timed out by ${user.username} for ${reason}`, - ); - - interaction.reply({ + await interaction.editReply({ content: localize.t("reply_timeout", ns, locale, { member: target.toString(), endDate: durationText[duration.toString() as durationValue], }), - flags: MessageFlags.Ephemeral, }); - const settings = await GuildSetting.findOne({ - guildId: interaction.guild?.id, - }); - if (!settings?.logging.timeoutChannelId || !guild) return; - - const timeoutChannel = await getGuildChannel( - guild, - settings.logging.timeoutChannelId, + const timeoutLogChannelId = await apiConnService.get( + Routes.setting("timeout_log_channel_id"), + zSettingsResponse, ); + console.log("api response", timeoutLogChannelId); + if (timeoutLogChannelId || !guild) return; + + const timeoutChannel = await getGuildChannel(guild, timeoutLogChannelId); if (!timeoutChannel?.isSendable() || !(member instanceof GuildMember)) return; diff --git a/src/commands/context_menu/README.md b/src/commands/context_menu/README.md index 15faba91..4920cc2e 100644 --- a/src/commands/context_menu/README.md +++ b/src/commands/context_menu/README.md @@ -14,7 +14,7 @@ ```ts // src/commands/context_menu/user.ts import { ApplicationCommandType } from "discord.js"; - import { ContextMenuCommand } from "../../Classes/index.js"; + import { ContextMenuCommand } from "@/Classes"; export default new ContextMenuCommand() .setBuilder((builder) => @@ -29,15 +29,15 @@ ```ts // src/commands/index.ts - export { default as user } from "./context_menu/user.js"; + export { default as user } from "./context_menu/user"; ``` -3. Register the commands with the client in the root [`index.ts`](../../index.ts). +3. Register the commands with the client in the root [`index.ts`](@/index.ts). ```ts // src/index.ts - import { Client } from "./Classes/index.js"; - import * as commands from "./commands/index.js"; + import { Client } from "./Classes"; + import * as commands from "./commands"; const client = new Client(); diff --git a/src/commands/context_menu/profile.ts b/src/commands/context_menu/profile.ts index 0741c243..43c6a808 100644 --- a/src/commands/context_menu/profile.ts +++ b/src/commands/context_menu/profile.ts @@ -1,11 +1,11 @@ +import { ContextMenuCommand } from "@/Classes"; +import { isGuildMember } from "@/util"; import { ApplicationCommandType, ContextMenuCommandBuilder, InteractionContextType, UserContextMenuCommandInteraction, } from "discord.js"; -import { ContextMenuCommand } from "../../Classes/index.js"; -import { isGuildMember } from "../../util/index.js"; /** * The `View Profile` context menu command allows guild managers to see the guild profile diff --git a/src/commands/context_menu/report.ts b/src/commands/context_menu/report.ts index e8988b1d..d8892df7 100644 --- a/src/commands/context_menu/report.ts +++ b/src/commands/context_menu/report.ts @@ -1,3 +1,6 @@ +import { ContextMenuCommand } from "@/Classes"; +import { reportModalPrefix, reportModel } from "@/features/report"; +import { AddSplitCustomId } from "@/util"; import { ApplicationCommandType, ContextMenuCommandBuilder, @@ -6,9 +9,6 @@ import { MessageFlags, UserContextMenuCommandInteraction, } from "discord.js"; -import { ContextMenuCommand } from "../../Classes/index.js"; -import { reportModalPrefix, reportModel } from "../../features/report.js"; -import { AddSplitCustomId } from "../../util/index.js"; /** * The `Report User` context menu command allows a user to report another non-bot user diff --git a/src/commands/index.ts b/src/commands/index.ts index e7d10c8b..3558acbc 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,19 +1,19 @@ -export { default as state } from "./chat/state.js"; +export { default as state } from "./chat/state"; -export { default as feedback } from "./chat/feedback.js"; +export { default as feedback } from "./chat/feedback"; -export { warn } from "./chat/moderation.js"; +export { warn } from "./chat/moderation"; -// export { userProfile } from './context_menu/profile.js'; +// export { userProfile } from "./context_menu/profile"; -export { settings } from "./chat/setting.js"; +export { settings } from "./chat/setting"; -export { reportMessage, reportUser } from "./context_menu/report.js"; +export { reportMessage, reportUser } from "./context_menu/report"; -export { timeout } from "./chat/timeout.js"; +export { timeout } from "./chat/timeout"; -export { stateAdmin } from "./chat/state-admin.js"; +export { stateAdmin } from "./chat/state-admin"; -export { searchEvents } from "./chat/search-events.js"; +export { searchEvents } from "./chat/search-events"; -export { mute } from "./chat/mute.js"; +export { mute } from "./chat/mute"; diff --git a/src/contracts/data/DiscordEvent.ts b/src/contracts/data/DiscordEvent.ts new file mode 100644 index 00000000..984983ca --- /dev/null +++ b/src/contracts/data/DiscordEvent.ts @@ -0,0 +1,24 @@ +import { zDiscordEventAttendee } from "./DiscordEventAttendee"; +import { zDiscordEventStatus } from "./DiscordEventStatus"; +import z from "zod"; + +export const zDiscordEvent = z.object({ + id: z.number(), + discordId: z.string().nonempty(), + channelId: z.string().nonempty(), + name: z.string().nonempty(), + description: z.string().nullable(), + status: zDiscordEventStatus.nullable(), + recurrent: z.boolean(), + userCount: z.number().nullable(), + thumbnailUrl: z.string().nonempty(), + createdAtUtc: z.coerce.date(), + creatorDiscordId: z.string().nonempty(), + scheduledStartUtc: z.coerce.date(), + startedAtUtc: z.coerce.date().nullable(), + scheduledEndUtc: z.coerce.date().nullable(), + endedAtUtc: z.coerce.date().nullable(), + attendees: z.array(zDiscordEventAttendee).optional(), +}); + +export type DiscordEvent = z.infer; diff --git a/src/contracts/data/DiscordEventAttendee.ts b/src/contracts/data/DiscordEventAttendee.ts new file mode 100644 index 00000000..7afc933c --- /dev/null +++ b/src/contracts/data/DiscordEventAttendee.ts @@ -0,0 +1,11 @@ +import z from "zod"; + +export const zDiscordEventAttendee = z.object({ + id: z.number(), + userDiscordId: z.string().nonempty(), + eventId: z.number(), + dateAttendedUtc: z.coerce.date(), + isJoin: z.boolean(), +}); + +export type DiscordEventAttendee = z.infer; diff --git a/src/contracts/data/DiscordEventStatus.ts b/src/contracts/data/DiscordEventStatus.ts new file mode 100644 index 00000000..598491bf --- /dev/null +++ b/src/contracts/data/DiscordEventStatus.ts @@ -0,0 +1,10 @@ +import z from "zod"; + +export enum DiscordEventStatus { + Scheduled = 1, + Active = 2, + Completed = 3, + Cancelled = 4, +} + +export const zDiscordEventStatus = z.enum(DiscordEventStatus); diff --git a/src/contracts/data/index.ts b/src/contracts/data/index.ts new file mode 100644 index 00000000..eae2e876 --- /dev/null +++ b/src/contracts/data/index.ts @@ -0,0 +1,3 @@ +export * from "./DiscordEvent"; +export * from "./DiscordEventAttendee"; +export * from "./DiscordEventStatus"; diff --git a/src/contracts/requests/CreateDiscordEventAttendeeRequest.ts b/src/contracts/requests/CreateDiscordEventAttendeeRequest.ts new file mode 100644 index 00000000..401965f7 --- /dev/null +++ b/src/contracts/requests/CreateDiscordEventAttendeeRequest.ts @@ -0,0 +1,11 @@ +import z from "zod"; + +export const zCreateDiscordEventAttendeeRequest = z.object({ + userDiscordId: z.string().nonempty(), + dateAttendedUtc: z.coerce.date(), + isJoin: z.boolean(), +}); + +export type CreateDiscordEventAttendeeRequest = z.infer< + typeof zCreateDiscordEventAttendeeRequest +>; diff --git a/src/contracts/requests/CreateDiscordEventRequest.ts b/src/contracts/requests/CreateDiscordEventRequest.ts new file mode 100644 index 00000000..a064eeb2 --- /dev/null +++ b/src/contracts/requests/CreateDiscordEventRequest.ts @@ -0,0 +1,23 @@ +import z from "zod"; +import { zDiscordEventStatus } from "../data"; + +export const zCreateDiscordEventRequest = z.object({ + discordId: z.string().nonempty(), + channelId: z.string().nonempty(), + name: z.string().nonempty(), + description: z.string().nullable(), + status: zDiscordEventStatus.nullable(), + recurrent: z.boolean(), + userCount: z.number().nullable(), + thumbnailUrl: z.string().nonempty(), + createdAtUtc: z.coerce.date(), + creatorDiscordId: z.string().nonempty(), + scheduledStartUtc: z.coerce.date(), + startedAtUtc: z.coerce.date().nullable(), + scheduledEndUtc: z.coerce.date().nullable(), + endedAtUtc: z.coerce.date().nullable(), +}); + +export type CreateDiscordEventRequest = z.infer< + typeof zCreateDiscordEventRequest +>; diff --git a/src/contracts/requests/index.ts b/src/contracts/requests/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/contracts/responses/SettingsResponse.ts b/src/contracts/responses/SettingsResponse.ts new file mode 100644 index 00000000..2f37a1ec --- /dev/null +++ b/src/contracts/responses/SettingsResponse.ts @@ -0,0 +1,7 @@ +import z from "zod"; + +export const zSettingsResponse = z.object({ + data: z.string().nonempty(), +}); + +export type SettingsResponse = z.infer; diff --git a/src/contracts/responses/index.ts b/src/contracts/responses/index.ts new file mode 100644 index 00000000..238bbe0c --- /dev/null +++ b/src/contracts/responses/index.ts @@ -0,0 +1 @@ +export * from "./SettingsResponse"; diff --git a/src/events/README.md b/src/events/README.md index 38a9898b..17103537 100644 --- a/src/events/README.md +++ b/src/events/README.md @@ -11,7 +11,7 @@ Client Events are how the bot interacts with Discord. The PV bot has a built-in ```ts // src/events/messageCreate.ts import { Events, Message } from "discord.js"; -import { Client, Event } from "../Classes/index.js"; +import { Client, Event } from "../Classes"; export default new Event({ name: Events.MessageCreate, @@ -26,15 +26,15 @@ export default new Event({ ```ts // src/events/index.ts -export { default as messageCreate } from "./messageCreate.js"; +export { default as messageCreate } from "./messageCreate"; ``` 3. In the entry point (`src/index.ts`) make sure the following is present. This registers the event handlers with the client: ```ts // src/index.ts -import { Client } from "./Classes/index.js"; -import * as events from "./events/index.js"; +import { Client } from "./Classes"; +import * as events from "./events"; export const client = new Client(); diff --git a/src/events/client/debug.ts b/src/events/client/debug.ts index a0a2bc3d..f46510b4 100644 --- a/src/events/client/debug.ts +++ b/src/events/client/debug.ts @@ -1,5 +1,5 @@ +import { Event } from "@/Classes"; import { Events } from "discord.js"; -import { Event } from "../../Classes/index.js"; /** * The `debug` {@link Event} handles emission of DEBUG logs diff --git a/src/events/client/error.ts b/src/events/client/error.ts index 1208b96c..ddcb5877 100644 --- a/src/events/client/error.ts +++ b/src/events/client/error.ts @@ -1,5 +1,5 @@ +import { Event } from "@/Classes"; import { Events } from "discord.js"; -import { Event } from "../../Classes/index.js"; /** * The `error` {@link Event} handles emission of ERROR logs diff --git a/src/events/client/ready.ts b/src/events/client/ready.ts index c516841e..5d83a451 100644 --- a/src/events/client/ready.ts +++ b/src/events/client/ready.ts @@ -1,5 +1,5 @@ +import { Event } from "@/Classes"; import { Events } from "discord.js"; -import { Event } from "../../Classes/index.js"; /** * The `ready` event {@link Event} registers a event handler for the diff --git a/src/events/client/warn.ts b/src/events/client/warn.ts index 54c69b1c..7b35ed44 100644 --- a/src/events/client/warn.ts +++ b/src/events/client/warn.ts @@ -1,5 +1,5 @@ +import { Event } from "@/Classes"; import { Events } from "discord.js"; -import { Event } from "../../Classes/index.js"; /** * The `warn` {@link Event} handles emission of WARN logs diff --git a/src/events/guild_audit_log/guildAuditLogEntryCreate.ts b/src/events/guild_audit_log/guildAuditLogEntryCreate.ts index bdbf6e4d..d6389fce 100644 --- a/src/events/guild_audit_log/guildAuditLogEntryCreate.ts +++ b/src/events/guild_audit_log/guildAuditLogEntryCreate.ts @@ -1,3 +1,12 @@ +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import Event from "@/Classes/Event"; +import { + SettingsResponse, + zSettingsResponse, +} from "@/contracts/responses/SettingsResponse"; +import { timeoutEmbed } from "@/features/timeout"; +import { getGuildChannel } from "@/util"; +import { apiConnService } from "@/util/api/pvapi"; import { AuditLogEvent, Events, @@ -5,10 +14,6 @@ import { GuildAuditLogsEntry, User, } from "discord.js"; -import Event from "../../Classes/Event.js"; -import { timeoutEmbed } from "../../features/timeout.js"; -import { GuildSetting } from "../../models/Setting.js"; -import { getGuildChannel } from "../../util/index.js"; /** * `guildAuditLogEntryCreate` handles the {@link Events.GuildAuditLogEntryCreate} {@link Event} @@ -18,9 +23,13 @@ export const guildAuditLogEntryCreate = new Event({ name: Events.GuildAuditLogEntryCreate, execute: async (auditLogEntry: GuildAuditLogsEntry, guild: Guild) => { const { executorId, target, changes } = auditLogEntry; - const settings = await GuildSetting.findOne({ guildId: guild.id }); + const res = await apiConnService.get( + Routes.setting("timeout_log_channel_id"), + zSettingsResponse, + ); + + const timeoutChannelId = res.data; if ( - settings?.logging.timeoutChannelId && auditLogEntry.action == AuditLogEvent.MemberUpdate && changes[0].key == "communication_disabled_until" && target instanceof User && @@ -31,10 +40,7 @@ export const guildAuditLogEntryCreate = new Event({ if (executorMember?.user.bot || !(targetMember && executorMember)) return; - const timeoutChannel = await getGuildChannel( - guild, - settings.logging.timeoutChannelId, - ); + const timeoutChannel = await getGuildChannel(guild, timeoutChannelId); if (!timeoutChannel?.isSendable()) return; diff --git a/src/events/guild_member/guildMemberAdd.ts b/src/events/guild_member/guildMemberAdd.ts index 7e40a1a7..fc2acc68 100644 --- a/src/events/guild_member/guildMemberAdd.ts +++ b/src/events/guild_member/guildMemberAdd.ts @@ -1,5 +1,5 @@ +import Event from "@/Classes/Event"; import { Events } from "discord.js"; -import Event from "../../Classes/Event.js"; /** * `guildMemberAdd` handles the {@link Events.GuildMemberAdd} {@link Event}. Currently, diff --git a/src/events/guild_member/guildMemberRemove.ts b/src/events/guild_member/guildMemberRemove.ts index 26619bb3..ea1e85ef 100644 --- a/src/events/guild_member/guildMemberRemove.ts +++ b/src/events/guild_member/guildMemberRemove.ts @@ -1,3 +1,12 @@ +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import Event from "@/Classes/Event"; +import { + SettingsResponse, + zSettingsResponse, +} from "@/contracts/responses/SettingsResponse"; +import { getGuildChannel } from "@/util"; +import { apiConnService } from "@/util/api/pvapi"; +import { footer } from "@/util/components"; import { bold, Colors, @@ -13,10 +22,6 @@ import { ThumbnailBuilder, TimestampStyles, } from "discord.js"; -import Event from "../../Classes/Event.js"; -import { GuildSetting } from "../../models/Setting.js"; -import { footer } from "../../util/components.js"; -import { getGuildChannel } from "../../util/index.js"; /** * `GuildMemberRemove` handles the {@link Events.GuildMemberRemove} {@link Event}. @@ -26,11 +31,12 @@ export const GuildMemberRemove = new Event({ name: Events.GuildMemberRemove, execute: async (member) => { const { guild } = member; - const settings = await GuildSetting.findOne({ guildId: guild.id }); + const res = await apiConnService.get( + Routes.setting("leave_log_channel_id"), + zSettingsResponse, + ); - // check that leave channel ID is set - const leaveChannelId = settings?.logging.leaveChannelId; - if (!leaveChannelId) return; + const leaveChannelId = res.data; // check that Join channel exists in guild const leaveChannel = await getGuildChannel(guild, leaveChannelId); diff --git a/src/events/guild_member/guildMemberUpdate.ts b/src/events/guild_member/guildMemberUpdate.ts index 00ad6d67..31f7c92b 100644 --- a/src/events/guild_member/guildMemberUpdate.ts +++ b/src/events/guild_member/guildMemberUpdate.ts @@ -16,11 +16,16 @@ import { TimestampStyles, } from "discord.js"; -import Event from "../../Classes/Event.js"; -import { welcomeButton, welcomeColors } from "../../features/welcome.js"; -import { GuildSetting } from "../../models/Setting.js"; -import { footer } from "../../util/components.js"; -import { getGuildChannel } from "../../util/index.js"; +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import Event from "@/Classes/Event"; +import { + SettingsResponse, + zSettingsResponse, +} from "@/contracts/responses/SettingsResponse"; +import { welcomeButton, welcomeColors } from "@/features/welcome"; +import { getGuildChannel } from "@/util"; +import { apiConnService } from "@/util/api/pvapi"; +import { footer } from "@/util/components"; /** * `guildMemberUpdate` handles the {@link Events.GuildMemberUpdate} {@link Event}. There are two cases: @@ -37,10 +42,12 @@ export const guildMemberUpdate = new Event({ execute: async (oldMember, newMember) => { if (oldMember.pending && oldMember.pending !== newMember.pending) { const { guild } = newMember; - const settings = await GuildSetting.findOne({ guildId: guild.id }); - // check that Join channel ID is set - const joinChannelId = settings?.welcome.channelId; - if (!joinChannelId) return; + const res = await apiConnService.get( + Routes.setting("welcome_channel_id"), + zSettingsResponse, + ); + + const joinChannelId = res.data; // check that Join channel exists in guild const joinChannel = await getGuildChannel(guild, joinChannelId); @@ -89,11 +96,12 @@ export const guildMemberUpdate = new Event({ if (oldMember.nickname !== newMember.nickname) { const { guild } = newMember; - const settings = await GuildSetting.findOne({ guildId: guild.id }); + const res = await apiConnService.get( + Routes.setting("nickname_updates_log_channel_id"), + zSettingsResponse, + ); - const nicknameUpdatesChannelId = - settings?.logging.nicknameUpdatesChannelId; - if (!nicknameUpdatesChannelId) return; + const nicknameUpdatesChannelId = res.data; const nicknameLogChannel = await getGuildChannel( guild, diff --git a/src/events/guild_member/guildMemberVoiceUpdate.ts b/src/events/guild_member/guildMemberVoiceUpdate.ts index 80633bbd..216de614 100644 --- a/src/events/guild_member/guildMemberVoiceUpdate.ts +++ b/src/events/guild_member/guildMemberVoiceUpdate.ts @@ -1,3 +1,12 @@ +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import Event from "@/Classes/Event"; +import { + SettingsResponse, + zSettingsResponse, +} from "@/contracts/responses/SettingsResponse"; +import { getGuildChannel } from "@/util"; +import { apiConnService } from "@/util/api/pvapi"; +import { markAttendance } from "@/util/events/markAttendance"; import { channelMention, ColorResolvable, @@ -5,16 +14,9 @@ import { EmbedBuilder, Events, GuildMember, + GuildScheduledEventStatus, inlineCode, } from "discord.js"; -import Event from "../../Classes/Event.js"; -import { - IScheduledEvent, - ScheduledEvent, -} from "../../models/ScheduledEvent.js"; -import { GuildSetting } from "../../models/Setting.js"; -import { getGuildChannel } from "../../util/index.js"; -import dbConnect from "../../util/libmongo.js"; /** * `guildMemberVoiceUpdate` handles the {@link Events.VoiceStateUpdate} {@link Event}. @@ -25,12 +27,24 @@ export const guildMemberVoiceUpdate = new Event({ execute: async (oldState, newState) => { // console.log(oldState.toJSON(), newState.toJSON()) - const { guild } = newState; + let guild = newState.guild; + if (!guild) guild = oldState.guild; const member = newState.member === null ? await guild.members.fetch(newState.id).catch(console.error) : newState.member; if (!member) return; + const events = await guild.scheduledEvents.fetch(); + const oldChannelEv = events.find( + (x) => + x.channelId === oldState.channelId && + x.status === GuildScheduledEventStatus.Active, + ); + const newChannelEv = events.find( + (x) => + x.channelId === newState.channelId && + x.status === GuildScheduledEventStatus.Active, + ); const newStateChannelMention = channelMention( newState.channelId ?? "error", @@ -61,7 +75,10 @@ export const guildMemberVoiceUpdate = new Event({ } else return; } else { if (oldState.channelId === null && newState.channelId !== null) { - markAttendance(newState.channelId, member, newState.channelId); + if (newChannelEv) { + console.log("joined", member.displayName, member.id, true); + await markAttendance(newChannelEv, member, true); + } embed = vcLogEmbed( member, "Joined Voice Channel", @@ -69,7 +86,10 @@ export const guildMemberVoiceUpdate = new Event({ Colors.Green, ); } else if (oldState.channelId !== null && newState.channelId === null) { - markAttendance(oldState.channelId, member, null); + if (oldChannelEv) { + console.log("left", member.displayName, member.id, false); + await markAttendance(oldChannelEv, member, false); + } embed = vcLogEmbed( member, "Left Voice Channel", @@ -77,26 +97,38 @@ export const guildMemberVoiceUpdate = new Event({ Colors.Red, ); } else { + if (oldState.channelId !== null && oldChannelEv) { + console.log("switched off", member.displayName, member.id, false); + await markAttendance(oldChannelEv, member, false); + } + if (newState.channelId !== null && newChannelEv) { + console.log("switched on", member.displayName, member.id, true); + await markAttendance(newChannelEv, member, true); + } embed = vcLogEmbed( member, "Switched Voice Channel", `${member}${inlineCode(member.displayName)} switched from ${oldStateChannelMention} to ${newStateChannelMention}`, Colors.Blue, ); - if (newState.channelId) - markAttendance(newState.channelId, member, newState.channelId); } } - const settings = await GuildSetting.findOne({ guildId: guild.id }); - // check that logging channel ID is set - const loggingChannelId = settings?.logging.voiceUpdatesChannelId; - if (!loggingChannelId) return; + const res = await apiConnService.get( + Routes.setting("voice_updates_log_channel_id"), + zSettingsResponse, + ); + + console.log("res", res); + + const loggingChannelId = res.data; // check that logging channel exists in guild const loggingChannel = await getGuildChannel(guild, loggingChannelId); if (!loggingChannel?.isSendable()) return; + console.log("sending vc update"); + loggingChannel.send({ embeds: [embed] }); }, }); @@ -123,36 +155,3 @@ function vcLogEmbed( .setFooter({ text: `User ID: ${member.id}` }) .setColor(color); } - -/** - * - * @param channelId - * @param member - */ -async function markAttendance( - channelId: string, - member: GuildMember, - newChannel: string | null, -) { - try { - await dbConnect(); - const res: IScheduledEvent = (await ScheduledEvent.findOne({ - channelId: channelId, - status: 2, - }) - .sort({ _id: -1 }) - .exec()) as IScheduledEvent; - if (!res) return; - console.log( - `Marking Attendance:\nUser Id: ${member.id}\nEvent Id: ${res.eventId}`, - ); - res.attendees.push({ - id: member.id, - join: newChannel && newChannel === res.channelId ? true : false, - timestamp: new Date(Date.now()), - }); - await res.save(); - } catch (e) { - console.error(e); - } -} diff --git a/src/events/guild_scheduled_event/guildScheduledEventDelete.ts b/src/events/guild_scheduled_event/guildScheduledEventDelete.ts deleted file mode 100644 index 89421c4a..00000000 --- a/src/events/guild_scheduled_event/guildScheduledEventDelete.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Events, GuildScheduledEventStatus } from "discord.js"; -import { Event } from "../../Classes/Event.js"; -import { logScheduledEvent } from "../../features/logging/scheduledEvent.js"; -import { - IScheduledEvent, - ScheduledEvent, -} from "../../models/ScheduledEvent.js"; -import dbConnect from "../../util/libmongo.js"; - -export const guildScheduledEventDelete = new Event({ - name: Events.GuildScheduledEventDelete, - execute: async (event) => { - console.log("deleting"); - await dbConnect(); - const res: IScheduledEvent = ( - await ScheduledEvent.find({ eventId: event.id }).sort({ _id: -1 }).exec() - )[0] as IScheduledEvent; - res.status = GuildScheduledEventStatus.Canceled; - res.save(); - await logScheduledEvent(res); - }, -}); diff --git a/src/events/guild_scheduled_event/guildScheduledEventUpdate.ts b/src/events/guild_scheduled_event/guildScheduledEventUpdate.ts index 9becb159..c5f9dee7 100644 --- a/src/events/guild_scheduled_event/guildScheduledEventUpdate.ts +++ b/src/events/guild_scheduled_event/guildScheduledEventUpdate.ts @@ -1,102 +1,112 @@ -import { Events, VoiceBasedChannel } from "discord.js"; -import { Event } from "../../Classes/Event.js"; -import { logScheduledEvent } from "../../features/logging/scheduledEvent.js"; +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { Event } from "@/Classes/Event"; import { - IScheduledEvent, - ScheduledEvent, -} from "../../models/ScheduledEvent.js"; -import dbConnect from "../../util/libmongo.js"; + DiscordEvent, + DiscordEventStatus, + zDiscordEvent, +} from "@/contracts/data"; +import { + CreateDiscordEventRequest, + zCreateDiscordEventRequest, +} from "@/contracts/requests/CreateDiscordEventRequest"; +import { logScheduledEvent } from "@/features/logging/scheduledEvent"; +import { apiConnService } from "@/util/api/pvapi"; +import { markAttendance } from "@/util/events/markAttendance"; +import { Events } from "discord.js"; +import z from "zod"; export const guildScheduledEventUpdate = new Event({ name: Events.GuildScheduledEventUpdate, execute: async (oldEvent, newEvent) => { try { - console.log("Updating Event ID: " + newEvent.id); - if (!oldEvent) throw Error("No old event reported"); - await dbConnect(); + console.log(+newEvent.status); + + // map event interface + if (!newEvent.channelId) + throw Error("No channel id specified for event: " + newEvent.id); + if (!newEvent.creatorId) + throw Error("No creator specified for event: " + newEvent.id); + if (!newEvent.scheduledStartAt) + throw Error("No start time specified for event: " + newEvent.id); - let res; + const eventCreateRequest: CreateDiscordEventRequest = { + discordId: newEvent.id, + channelId: newEvent.channelId, + name: newEvent.name, + description: newEvent.description ?? null, + status: newEvent.status as number as DiscordEventStatus, + recurrent: newEvent.recurrenceRule ? true : false, + userCount: null, + startedAtUtc: new Date(), + endedAtUtc: null, + thumbnailUrl: newEvent.coverImageURL() ?? "attachment://image.jpg", + createdAtUtc: newEvent.createdAt, + creatorDiscordId: newEvent.creatorId, + scheduledStartUtc: newEvent.scheduledStartAt, + scheduledEndUtc: newEvent.scheduledEndAt ?? null, + }; + // Event Started if (oldEvent.isScheduled() && newEvent.isActive()) { - console.log("Starting Event: " + newEvent.id); - await new Promise((r) => setTimeout(r, 2000)); - const evChannel = - (await newEvent.channel?.fetch()) as VoiceBasedChannel; - res = (await ScheduledEvent.insertOne({ - thumbnailUrl: newEvent.coverImageURL() ?? "attachment://image.jpg", - eventUrl: newEvent.url, - recurrence: newEvent.recurrenceRule ? true : false, - guildId: newEvent.guildId, - eventId: newEvent.id, - channelId: newEvent.channelId, - createdAt: newEvent.createdAt, - startedAt: new Date(Date.now()), - description: newEvent.description, - creatorId: newEvent.creatorId, - scheduledEnd: newEvent.scheduledEndAt, - scheduledStart: newEvent.scheduledStartAt, - name: newEvent.name, - status: newEvent.status, - attendees: evChannel.members.map((usr) => { - return { id: usr.id, join: true, timestamp: new Date(Date.now()) }; - }), - })) as IScheduledEvent; + eventCreateRequest.startedAtUtc = new Date(); + eventCreateRequest.status = DiscordEventStatus.Active; - await logScheduledEvent(res); - } else { - res = ( - await ScheduledEvent.find({ eventId: newEvent.id }) - .sort({ _id: -1 }) - .exec() - )[0] as IScheduledEvent; - if (!res) { - res = (await ScheduledEvent.insertOne({ - thumbnailUrl: newEvent.coverImageURL() ?? "attachment://image.jpg", - eventUrl: newEvent.url, - recurrence: newEvent.recurrenceRule ? true : false, - guildId: newEvent.guildId, - eventId: newEvent.id, - channelId: newEvent.channelId, - createdAt: newEvent.createdAt, - description: newEvent.description, - creatorId: newEvent.creatorId, - scheduledEnd: newEvent.scheduledEndAt, - scheduledStart: newEvent.scheduledStartAt, - name: newEvent.name, - status: newEvent.status, - })) as IScheduledEvent; //maybe this should return null - } else { - res.recurrence = newEvent.recurrenceRule ? true : false; - res.thumbnailUrl = - newEvent.coverImageURL() ?? "attachment://image.jpg"; - res.channelId = newEvent.channelId ?? undefined; - res.name = newEvent.name; - res.description = newEvent.description ?? ""; - res.scheduledEnd = newEvent.scheduledEndAt ?? undefined; - res.scheduledStart = newEvent.scheduledStartAt ?? undefined; - res.status = newEvent.status; - res.userCount = newEvent.userCount ?? undefined; - } + const myWholeEvent = await apiConnService.post( + Routes.discordEvents, + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify( + z.parse(zCreateDiscordEventRequest, eventCreateRequest), + ), + }, + zDiscordEvent, + ); + + await logScheduledEvent(myWholeEvent, true); + + console.log(myWholeEvent.id); + + const channelFresh = await newEvent.channel?.fetch(); + + channelFresh?.members.forEach(async (usr) => { + await markAttendance(newEvent, usr, true, true); + }); } - if (!res.recurrence) { - if (oldEvent.isActive() && newEvent.isCompleted()) { - console.log("ending one time event: " + newEvent.id); - res.endedAt = new Date(Date.now()); + // Event Ended + else if (oldEvent.isActive() && !newEvent.isActive()) { + const data: DiscordEvent = await apiConnService.get( + Routes.latestDiscordEvent(newEvent.id), + zDiscordEvent, + ); - await logScheduledEvent(res); + data.endedAtUtc = new Date(); + switch (newEvent.status) { + case 1: + data.status = DiscordEventStatus.Completed; + break; + case 3: + data.status = DiscordEventStatus.Completed; + break; + case 4: + data.status = DiscordEventStatus.Cancelled; + break; } - } else { - if (oldEvent.isActive() && newEvent.isScheduled()) { - console.log("ending recurring event: " + newEvent.id); - res.endedAt = new Date(Date.now()); - await logScheduledEvent(res); - } - } + const myWholeEvent = z.parse(zDiscordEvent, data); + + await apiConnService.patch(Routes.discordEvent(myWholeEvent.id), { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(myWholeEvent), + }); - await res.save(); + logScheduledEvent(myWholeEvent, false); + } } catch (e) { console.error(e); } diff --git a/src/events/index.ts b/src/events/index.ts index a62c47a0..c852a11e 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -1,23 +1,23 @@ -export { ready } from "./client/ready.js"; +export { ready } from "./client/ready"; -export { warn } from "./client/warn.js"; +export { warn } from "./client/warn"; -export { debug } from "./client/debug.js"; +export { debug } from "./client/debug"; -export { error } from "./client/error.js"; +export { error } from "./client/error"; -export { interactionCreate } from "./interactionCreate.js"; +export { interactionCreate } from "./interactionCreate"; -export { guildAuditLogEntryCreate } from "./guild_audit_log/guildAuditLogEntryCreate.js"; +export { guildAuditLogEntryCreate } from "./guild_audit_log/guildAuditLogEntryCreate"; -// export { guildMemberAdd } from './guild_member/guildMemberAdd.js'; +// export { guildMemberAdd } from "./guild_member/guildMemberAdd"; -export { GuildMemberRemove } from "./guild_member/guildMemberRemove.js"; +export { GuildMemberRemove } from "./guild_member/guildMemberRemove"; -export { guildMemberUpdate } from "./guild_member/guildMemberUpdate.js"; +export { guildMemberUpdate } from "./guild_member/guildMemberUpdate"; -export { guildMemberVoiceUpdate } from "./guild_member/guildMemberVoiceUpdate.js"; +export { guildMemberVoiceUpdate } from "./guild_member/guildMemberVoiceUpdate"; -export { guildScheduledEventDelete } from "./guild_scheduled_event/guildScheduledEventDelete.js"; +//export { guildScheduledEventDelete } from "./guild_scheduled_event/guildScheduledEventDelete"; -export { guildScheduledEventUpdate } from "./guild_scheduled_event/guildScheduledEventUpdate.js"; +export { guildScheduledEventUpdate } from "./guild_scheduled_event/guildScheduledEventUpdate"; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index cdb731ca..d8be8d41 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -2,11 +2,12 @@ import { ApplicationCommandType, DiscordAPIError, Events, + inlineCode, Interaction, InteractionType, + MessageFlags, } from "discord.js"; -import { Event } from "../Classes/Event.js"; -import { GuildSetting } from "../models/Setting.js"; +import { Event } from "../Classes/Event"; /** * Handles the creation of a new interaction. @@ -14,24 +15,10 @@ import { GuildSetting } from "../models/Setting.js"; */ async function onInteractionCreate(interaction: Interaction): Promise { const { client, type } = interaction; - const { commands, interactions, errorMessage, replyOnError } = client; - - if (interaction.inGuild()) { - const setting = await GuildSetting.findOne({ - guildId: interaction.guildId, - }); - if (!setting) - GuildSetting.create({ - guildId: interaction.guildId, - guildName: interaction.guild?.name, - }); - else if (interaction.guild?.name !== setting.guildName) { - setting.guildName = interaction.guild?.name ?? "Name Unknown"; - setting.save(); - } - } + const { commands, interactions } = client; client.emit(Events.Debug, interaction.toString()); + try { switch (type) { case InteractionType.ApplicationCommandAutocomplete: @@ -75,24 +62,28 @@ async function onInteractionCreate(interaction: Interaction): Promise { break; } } catch (error) { - if (interaction.isRepliable()) { - // If the interaction is repliable, handle the error with a reply - if (error instanceof DiscordAPIError) client.emit(Events.Error, error); - else if (error instanceof Error) { - client.emit(Events.Error, error); - - if (!replyOnError) return; - - if (interaction.deferred) - // If the interaction is deferred, follow up with an ephemeral error message - void interaction.followUp({ content: errorMessage, ephemeral: true }); - else - // If the interaction is not deferred, reply with an ephemeral error message - void interaction.reply({ content: errorMessage, ephemeral: true }); + console.log(error, error instanceof DiscordAPIError); + if (!(error instanceof DiscordAPIError)) throw error; + client.emit(Events.Error, error); + if (interaction.isAutocomplete()) { + await interaction.respond([]); + return; + } + // If the interaction is repliable, handle the error with a reply + else if (interaction.isRepliable()) { + if (interaction.deferred) { + // If the interaction is deferred, editReply with error + interaction.editReply({ + content: `Interaction could not be completed because: ${inlineCode(error.message)}`, + }); + } else { + // If the interaction is not deferred, reply with an ephemeral error message + interaction.reply({ + content: `Interaction could not be completed because: ${inlineCode(error.message)}`, + flags: MessageFlags.Ephemeral, + }); } - } else - // If the interaction is not repliable - throw error; + } } } diff --git a/src/features/logging/scheduledEvent.ts b/src/features/logging/scheduledEvent.ts index cfc0dd22..503e1610 100644 --- a/src/features/logging/scheduledEvent.ts +++ b/src/features/logging/scheduledEvent.ts @@ -1,3 +1,12 @@ +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { DiscordEvent } from "@/contracts/data"; +import { + SettingsResponse, + zSettingsResponse, +} from "@/contracts/responses/SettingsResponse"; +import { apiConnService } from "@/util/api/pvapi"; +import { eventLogMessageCache } from "@/util/cache/eventLogMessageCache"; +import { ScheduledEventWrapper } from "@/util/scheduledEventWrapper"; import { AttachmentBuilder, ButtonBuilder, @@ -17,148 +26,194 @@ import { TextDisplayBuilder, ThumbnailBuilder, } from "discord.js"; -import { client } from "../../index.js"; -import { IScheduledEvent } from "../../models/ScheduledEvent.js"; -import { GuildSetting } from "../../models/Setting.js"; -import dbConnect from "../../util/libmongo.js"; -import { ScheduledEventWrapper } from "../../util/scheduledEventWrapper.js"; +import { client } from "../.."; -/** - * - * @param event - * @param guild - * @param forceNew - */ -export async function logScheduledEvent(event: IScheduledEvent) { - await dbConnect(); - const guild: Guild = await client.guilds.fetch(event.guildId); - const settings = await GuildSetting.findOne({ guildId: guild.id }).exec(); +export async function logScheduledEvent(event: DiscordEvent, init: boolean) { + try { + if (!process.env.PV_GUILD_ID) + throw Error("Set 'PV_GUILD_ID' in the env file"); + const guild: Guild = await client.guilds.fetch(process.env.PV_GUILD_ID); - const logChannelId = settings?.logging.eventLogChannelId; - if (!logChannelId) return; - let logChannel = guild.channels.cache.get(logChannelId); - if (!logChannel) { - logChannel = (await guild.channels.fetch(logChannelId)) ?? undefined; - } + const setting = "event_log_channel_id"; - if (logChannel?.type !== ChannelType.GuildText) return; - let existingPost = undefined; - if (event.logMessageId) { - //console.log("finding existing post"); - existingPost = logChannel.messages.cache.get(event.logMessageId); - if (!existingPost) { - existingPost = await logChannel.messages - .fetch(event.logMessageId) - .catch((e) => { - if ( - e instanceof DiscordAPIError && - e.code === RESTJSONErrorCodes.UnknownMessage - ) { - return undefined; - } - throw e; - }); + const res = await apiConnService.get( + Routes.setting(setting), + zSettingsResponse, + ); + + const logChannelId = res.data; + if (!logChannelId) throw Error("Set the log channel id in the settings!"); + let logChannel = guild.channels.cache.get(logChannelId); + if (!logChannel) { + logChannel = (await guild.channels.fetch(logChannelId)) ?? undefined; } - } - //console.log("fetched post"); - //console.log(existingPost); + if (logChannel?.type !== ChannelType.GuildText) return; + let existingPost = undefined; + const logMessageId = eventLogMessageCache.fetch(event.id); + if (logMessageId) { + existingPost = logChannel.messages.cache.get(logMessageId); + if (!existingPost) { + existingPost = await logChannel.messages + .fetch(logMessageId) + .catch((e) => { + if ( + e instanceof DiscordAPIError && + e.code === RESTJSONErrorCodes.UnknownMessage + ) { + return undefined; + } + throw e; + }); + } + } - if (existingPost) { - //console.log("editing existing post..."); - //console.log("event ended at: " + event.endedAt); - const { cont } = await logContainer(event); - const files = []; - if (event.thumbnailUrl === "attachment://image.jpg") - files.push(new AttachmentBuilder("./assets/image.jpg")); - files.push(new AttachmentBuilder("./assets/temp/attendees.csv")); - await existingPost.edit({ - components: [cont], - files: files, - flags: MessageFlags.IsComponentsV2, - allowedMentions: { parse: [] }, - }); - } else { - const { cont } = await logContainer(event); - const files = []; - if (event.thumbnailUrl === "attachment://image.jpg") - files.push(new AttachmentBuilder("./assets/image.jpg")); - files.push(new AttachmentBuilder("./assets/temp/attendees.csv")); - const post = await logChannel.send({ - components: [cont], - flags: MessageFlags.IsComponentsV2, - files: files, - allowedMentions: { parse: [] }, - }); - event.logMessageId = post.id; - //console.log("event log message id: " + event.logMessageId); - await event.save(); + if (existingPost) { + const { cont } = await logContainer(event, init); + const files = []; + if (event.thumbnailUrl === "attachment://image.jpg") + files.push(new AttachmentBuilder("./assets/image.jpg")); + if (!init) + files.push(new AttachmentBuilder("./assets/temp/attendees.csv")); + await existingPost.edit({ + components: [cont], + files: files, + flags: MessageFlags.IsComponentsV2, + allowedMentions: { parse: [] }, + }); + if (logMessageId) eventLogMessageCache.delete(logMessageId); + } else { + const { cont } = await logContainer(event, init); + const files = []; + if (event.thumbnailUrl === "attachment://image.jpg") + files.push(new AttachmentBuilder("./assets/image.jpg")); + if (!init) + files.push(new AttachmentBuilder("./assets/temp/attendees.csv")); + const post = await logChannel.send({ + components: [cont], + flags: MessageFlags.IsComponentsV2, + files: files, + allowedMentions: { parse: [] }, + }); + eventLogMessageCache.push(post.id, event); + } + } catch (err) { + console.error(err); } } -/** - * - * @param event - */ -async function logContainer(event: IScheduledEvent) { +// rewrite this function +async function logContainer(event: DiscordEvent, init: boolean) { const wrapper = new ScheduledEventWrapper(event); - let attendees = wrapper.attendancePercentages(); - const attendeesCount = wrapper.uniqueAttendees(); - await wrapper.writeCsvDump(); - //if attendees.length > 30 then replace inline list with text file - //todo: figure out how to generate text file - //todo: add some file output for attachments in this function; wire it up to the main log function - const attendeesStr = - attendees.length > 0 && attendees.length < 30 - ? attendees - .map((usr) => { - return `\n- ${usr}`; - }) - .toString() - : ""; + let attendeesCount; + let attendeesStr; + if (!init) { + const attendees = wrapper.attendancePercentages(); + attendeesCount = wrapper.uniqueAttendees(); + await wrapper.writeCsvDump(); + //if attendees.length > 30 then replace inline list with text file + //todo: figure out how to generate text file + //todo: add some file output for attachments in this function; wire it up to the main log function + attendeesStr = + attendees.length > 0 && attendees.length < 30 + ? attendees + .map((usr) => { + return `\n- ${usr}`; + }) + .toString() + : ""; + } const separator = new SeparatorBuilder() .setSpacing(SeparatorSpacingSize.Small) .setDivider(true); - return { - cont: new ContainerBuilder() - .setAccentColor(wrapper.statusColor()) - .addSectionComponents( - new SectionBuilder() - .setThumbnailAccessory( - new ThumbnailBuilder().setURL(wrapper.thumbnail()), - ) - .addTextDisplayComponents( - new TextDisplayBuilder().setContent( - heading(wrapper.name(), HeadingLevel.Three), - ), - new TextDisplayBuilder().setContent("Date: " + wrapper.startDate()), - new TextDisplayBuilder().setContent( - `Time: ${wrapper.startTime()} - ${wrapper.endTime()}`, + if (init) { + return { + cont: new ContainerBuilder() + .setAccentColor(wrapper.statusColor()) + .addSectionComponents( + new SectionBuilder() + .setThumbnailAccessory( + new ThumbnailBuilder().setURL(wrapper.thumbnail()), + ) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent( + heading(wrapper.name(), HeadingLevel.Three), + ), + new TextDisplayBuilder().setContent( + "Date: " + wrapper.startDate(), + ), + new TextDisplayBuilder().setContent(`Time: N/A`), ), + ) + .addSeparatorComponents(separator) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent( + "Description:\n" + wrapper.description(), ), - ) - .addSeparatorComponents(separator) - .addTextDisplayComponents( - new TextDisplayBuilder().setContent( - "Description:\n" + wrapper.description(), + new TextDisplayBuilder().setContent("Attendees: N/A"), + ) + .addSeparatorComponents(separator) + .addSectionComponents( + new SectionBuilder() + .setButtonAccessory( + new ButtonBuilder() + .setStyle(ButtonStyle.Link) + .setLabel("Event Link") + .setURL(await wrapper.eventLink()), + ) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `N/A Minutes • N/A Attendees • ${wrapper.recurrence()}`, + ), + ), ), - new TextDisplayBuilder().setContent("Attendees: " + attendeesStr), - ) - .addFileComponents(new FileBuilder().setURL("attachment://attendees.csv")) - .addSeparatorComponents(separator) - .addSectionComponents( - new SectionBuilder() - .setButtonAccessory( - new ButtonBuilder() - .setStyle(ButtonStyle.Link) - .setLabel("Event Link") - .setURL(wrapper.eventLink()), - ) - .addTextDisplayComponents( - new TextDisplayBuilder().setContent( - `${wrapper.duration()} Minutes • ${attendeesCount} Attendees • ${wrapper.recurrence()}`, + }; + } else { + return { + cont: new ContainerBuilder() + .setAccentColor(wrapper.statusColor()) + .addSectionComponents( + new SectionBuilder() + .setThumbnailAccessory( + new ThumbnailBuilder().setURL(wrapper.thumbnail()), + ) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent( + heading(wrapper.name(), HeadingLevel.Three), + ), + new TextDisplayBuilder().setContent( + "Date: " + wrapper.startDate(), + ), + new TextDisplayBuilder().setContent( + `Time: ${wrapper.startTime()} - ${wrapper.endTime()}`, + ), ), + ) + .addSeparatorComponents(separator) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent( + "Description:\n" + wrapper.description(), ), - ), - }; + new TextDisplayBuilder().setContent("Attendees: " + attendeesStr), + ) + .addFileComponents( + new FileBuilder().setURL("attachment://attendees.csv"), + ) + .addSeparatorComponents(separator) + .addSectionComponents( + new SectionBuilder() + .setButtonAccessory( + new ButtonBuilder() + .setStyle(ButtonStyle.Link) + .setLabel("Event Link") + .setURL(await wrapper.eventLink()), + ) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `${wrapper.duration()} Minutes • ${attendeesCount} Attendees • ${wrapper.recurrence()}`, + ), + ), + ), + }; + } } diff --git a/src/features/moderation/buttons.ts b/src/features/moderation/buttons.ts index fbb47969..f5bac59e 100644 --- a/src/features/moderation/buttons.ts +++ b/src/features/moderation/buttons.ts @@ -1,70 +1,13 @@ +import { APIWarn } from "@/Classes/API/ApiConnService/types"; +import { AddSplitCustomId } from "@/util"; import { ButtonBuilder, ButtonStyle, Guild, Snowflake } from "discord.js"; -import { HydratedDocument } from "mongoose"; -import { IWarn, WarningRecord } from "../../models/Warn.js"; -import { IWarnSearch } from "../../models/WarnSearch.js"; -import { AddSplitCustomId } from "../../util/index.js"; -import { numberOfWarnEmbedsOnPage, WarnButtonsPrefixes } from "./types.js"; +import { WarnButtonsPrefixes } from "./types"; /** * Create move left button for viewing warnings * @param searchRecord - Warning Search document * @returns ButtonBuilder for the move left button */ -export function leftButton(searchRecord: HydratedDocument) { - return new ButtonBuilder() - .setCustomId( - AddSplitCustomId(WarnButtonsPrefixes.viewWarningsLeft, searchRecord.id), - ) - .setEmoji("⬅️") - .setStyle(ButtonStyle.Secondary) - .setDisabled(searchRecord.pageStart === 0); -} - -/** - * Create move right button for viewing warnings - * @param searchRecord - Warning Search document - * @param records - Array of warn documents - * @returns ButtonBuilder - */ -export function rightButton( - searchRecord: HydratedDocument, - records: HydratedDocument[], -) { - // button is disabled if the start page plus the number of warn embeds on page is greater than the number of records - const isDisabled = - searchRecord.pageStart + numberOfWarnEmbedsOnPage >= records.length; - return new ButtonBuilder() - .setCustomId( - AddSplitCustomId(WarnButtonsPrefixes.viewWarningsRight, searchRecord.id), - ) - .setEmoji("➡️") - .setStyle(ButtonStyle.Secondary) - .setDisabled(isDisabled); -} - -/** - * Creates button for viewing page number - * @param searchRecord - warn search document - * @param records - Array of warn documents - * @returns ButtonBuilder - */ -export function pageNumber( - searchRecord: HydratedDocument, - records: HydratedDocument[], -) { - const currentPage = - (searchRecord.pageStart + numberOfWarnEmbedsOnPage) / - numberOfWarnEmbedsOnPage; - - // round up the record length divided by the number of warn embeds on page - const totalPages = Math.ceil(records.length / numberOfWarnEmbedsOnPage); - - return new ButtonBuilder() - .setDisabled(true) - .setCustomId("Button does not use ID") - .setLabel(`${currentPage}/${totalPages}`) - .setStyle(ButtonStyle.Primary); -} /** * Provide a button to allow mods to view the warn history of another guild member @@ -107,7 +50,7 @@ function viewWarnHistory() { * @param record - the warning object witch to update * @returns {@link ButtonBuilder} object */ -export function warnUpdateFromIssue(record: WarningRecord) { +export function warnUpdateFromIssue(record: APIWarn) { return updateWarn(record, WarnButtonsPrefixes.updateWarnById); } @@ -116,7 +59,7 @@ export function warnUpdateFromIssue(record: WarningRecord) { * @param record - the warning object witch to update * @returns {@link ButtonBuilder} object */ -export function warnUpdateFromLog(record: WarningRecord) { +export function warnUpdateFromLog(record: APIWarn) { return updateWarn(record, WarnButtonsPrefixes.updateWarnById); } @@ -125,7 +68,7 @@ export function warnUpdateFromLog(record: WarningRecord) { * @param code - The {@link WarnButtonsPrefixes} denoting the type of update to perform * @returns a button used to trigger the event to update a user's warning */ -function updateWarn(record: WarningRecord, code: WarnButtonsPrefixes) { +function updateWarn(record: APIWarn, code: WarnButtonsPrefixes) { return new ButtonBuilder() .setCustomId(AddSplitCustomId(code, record.id)) .setEmoji("📝") @@ -138,7 +81,7 @@ function updateWarn(record: WarningRecord, code: WarnButtonsPrefixes) { * @param record - record of the warning * @returns ButtonBuilder */ -export function appealWarn(record: WarningRecord) { +export function appealWarn(record: APIWarn) { return new ButtonBuilder() .setCustomId(AddSplitCustomId(WarnButtonsPrefixes.appealWarn, record.id)) .setLabel("Appeal") @@ -164,7 +107,7 @@ export function appealDmSubmitted() { * @param record - The {@link WarningRecord} that contains the information of the warning to update * @returns a {@link ButtonBuilder} instance that can be used to construct an update-warn-by-ID button */ -export function updateWarnById(record: WarningRecord) { +export function updateWarnById(record: APIWarn) { return new ButtonBuilder() .setCustomId( AddSplitCustomId(WarnButtonsPrefixes.updateWarnById, record.id), diff --git a/src/features/moderation/embeds.ts b/src/features/moderation/embeds.ts index dd02df1c..30e86132 100644 --- a/src/features/moderation/embeds.ts +++ b/src/features/moderation/embeds.ts @@ -1,3 +1,5 @@ +import { APIWarn } from "@/Classes/API/ApiConnService/types"; +import { getMember } from "@/util"; import { APIEmbedField, ColorResolvable, @@ -8,13 +10,12 @@ import { GuildMember, ImageURLOptions, inlineCode, + Snowflake, TimestampStyles, User, } from "discord.js"; -import { client } from "../../index.js"; -import { WarningRecord } from "../../models/Warn.js"; -import { getMember } from "../../util/index.js"; -import { numberOfWarnEmbedsOnPage, WarnEmbedColor } from "./types.js"; +import { client } from "../.."; +import { numberOfWarnEmbedsOnPage, WarnEmbedColor } from "./types"; /** * Utility to create an embed for a moderator creating a new warning @@ -24,7 +25,7 @@ import { numberOfWarnEmbedsOnPage, WarnEmbedColor } from "./types.js"; * @returns the created new warning {@link EmbedBuilder} */ export function newWarnModEmbed( - record: WarningRecord, + record: APIWarn, moderator: GuildMember, target: GuildMember, ) { @@ -51,7 +52,7 @@ export function newWarnModEmbed( * @returns the created warning update {@link EmbedBuilder} */ export function warnLogUpdateEmbed( - record: WarningRecord, + record: APIWarn, moderator: GuildMember, target: GuildMember, updater: GuildMember, @@ -80,10 +81,7 @@ export function warnLogUpdateEmbed( * @param target - the {@link GuildMember} to whom the warning was issued * @returns the created {@link EmbedBuilder} */ -export function warnIssueUpdateEmbed( - record: WarningRecord, - target: GuildMember, -) { +export function warnIssueUpdateEmbed(record: APIWarn, target: GuildMember) { const embed = new EmbedBuilder() .setTitle("Warning updated") .setColor(WarnEmbedColor.updated) @@ -106,7 +104,7 @@ export function warnIssueUpdateEmbed( * @returns the created {@link EmbedBuilder} */ export function newWarningDmEmbed( - record: WarningRecord, + record: APIWarn, count: number, guild: Guild, ) { @@ -131,7 +129,7 @@ export function newWarningDmEmbed( * @returns an {@link EmbedBuilder} that acts as a notification that the warning was created */ export function newWarningLogEmbed( - record: WarningRecord, + record: APIWarn, moderator: GuildMember, target: GuildMember, ) { @@ -147,7 +145,7 @@ export function newWarningLogEmbed( expireAtField(record), ) .setFooter(documentIdFooter(record)) - .setTimestamp(record.createdAt); + .setTimestamp(new Date(record.createdAtUtc)); return embed; } @@ -159,7 +157,8 @@ export function newWarningLogEmbed( * @returns Array of EmbedBuilders */ export async function viewWarningEmbeds( - records: WarningRecord[], + guildId: Snowflake, + records: APIWarn[], isMod: boolean, start: number = 0, ) { @@ -177,12 +176,12 @@ export async function viewWarningEmbeds( // Embed color based on the status of the warning const color = - record.expireAt > new Date() + new Date(record.expiresAtUtc) > new Date() ? WarnEmbedColor.Active : WarnEmbedColor.Inactive; // Render embed - const embed = await viewWarningEmbed(record, isMod, color); + const embed = await viewWarningEmbed(guildId, record, isMod, color); // If embed is undefined is is not added to the embeds array if (!embed) continue; @@ -200,14 +199,15 @@ export async function viewWarningEmbeds( * @returns EmbedBuilder or undefined */ export async function viewWarningEmbed( - record: WarningRecord, + guildId: Snowflake, + record: APIWarn, isMod: boolean, embedColor: ColorResolvable = WarnEmbedColor.updated, ) { // Get guild from cache or fetch const guild = - client.guilds.cache.get(record.guildId) ?? - (await client.guilds.fetch(record.guildId).catch(console.error)); + client.guilds.cache.get(guildId) ?? + (await client.guilds.fetch(guildId).catch(console.error)); if (!guild) return; const embed = new EmbedBuilder() @@ -216,16 +216,16 @@ export async function viewWarningEmbed( .setFooter(documentIdFooter(record)); if (isMod) { // Get target from cache or fetch - const target = await getMember(guild, record.target.discordId); + const target = await getMember(guild, record.userWarnedDiscordId); // Get moderator from cache or fetch - const moderator = await getMember(guild, record.moderator.discordId); + const moderator = await getMember(guild, record.moderatorDiscordId); const targetFieldName = "Member"; // Check if target present if target is not present uses username from warning document if (!target) - embed.addFields(userField(targetFieldName, record.target.username)); + embed.addFields(userField(targetFieldName, record.userWarnedDiscordId)); // if target is present use GuildMember for username and avatar else embed @@ -237,25 +237,14 @@ export async function viewWarningEmbed( // Check if target present if moderator is not present uses username from warning document if (!moderator) - embed.addFields(userField(moderatorFieldName, record.moderator.username)); + embed.addFields(userField(moderatorFieldName, record.moderatorDiscordId)); // if moderator is present use GuildMember for username else embed.addFields(userField(moderatorFieldName, moderator.user)); - - // Check if warning document updater fields present - if (record.updater?.discordId && record.updater?.username) { - // Get updater from cache or fetch - const updater = await getMember(guild, record.updater.discordId); - const updaterFieldName = "Last Updated By"; - - // If updater is present add felid to embed - if (updater) embed.addFields(userField(updaterFieldName, updater.user)); - else - embed.addFields(userField(updaterFieldName, record.updater.username)); - } - embed.addFields(expireAtField(record)).setTimestamp(record.createdAt); - } else { - embed.setTimestamp(record.createdAt); } + embed + .addFields(expireAtField(record)) + .setTimestamp(new Date(record.createdAtUtc)); + embed.setTimestamp(new Date(record.createdAtUtc)); return embed; } @@ -265,7 +254,7 @@ export async function viewWarningEmbed( * @param record - Warning record * @returns footer option */ -export function documentIdFooter(record: WarningRecord): EmbedFooterOptions { +export function documentIdFooter(record: APIWarn): EmbedFooterOptions { return { text: `Warn ID: ${record.id}` }; } @@ -275,12 +264,12 @@ export function documentIdFooter(record: WarningRecord): EmbedFooterOptions { * @returns an embed field showing the time until a warning expires */ export function expireAtField( - record: WarningRecord, + record: APIWarn, inline: boolean = false, ): APIEmbedField { return { name: "Remaining Time", - value: `Expire ${record.expireAt.toDiscordString(TimestampStyles.RelativeTime)} On ${record.expireAt.toDiscordString(TimestampStyles.LongDate)}`, + value: `Expire ${new Date(record.expiresAtUtc).toDiscordString(TimestampStyles.RelativeTime)} On ${new Date(record.expiresAtUtc).toDiscordString(TimestampStyles.LongDate)}`, inline, }; } diff --git a/src/features/moderation/modals.ts b/src/features/moderation/modals.ts index e5714a1b..0266dfc5 100644 --- a/src/features/moderation/modals.ts +++ b/src/features/moderation/modals.ts @@ -4,8 +4,8 @@ import { TextInputBuilder, TextInputStyle, } from "discord.js"; -import { WARN_MAX_CHAR } from "./index.js"; -import { defaultNumberOfDaysBeforeExpiration } from "./types.js"; +import { WARN_MAX_CHAR } from "."; +import { defaultNumberOfDaysBeforeExpiration } from "./types"; /** * Create Modal for creating or updating warn diff --git a/src/features/moderation/warn-render.ts b/src/features/moderation/warn-render.ts new file mode 100644 index 00000000..b652485d --- /dev/null +++ b/src/features/moderation/warn-render.ts @@ -0,0 +1,269 @@ +import { APIWarn } from "@/Classes/API/ApiConnService"; +import { WarnSearch } from "@/Classes/API/ApiConnService/WarnSearchmanager"; +import { Warn } from "@/Classes/API/Warn"; +import { fetchMemberOrUser, getNameToDisplay } from "@/util"; +import { apiConnService } from "@/util/api/pvapi"; +import { + APIMessageTopLevelComponent, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + Client, + Colors, + ContainerBuilder, + Guild, + HeadingLevel, + JSONEncodable, + SeparatorSpacingSize, + Snowflake, + bold, + heading, + inlineCode, + resolveColor, + subtext, + time, +} from "discord.js"; +import { WarnEmbedColor } from "./types"; + +export async function warnPage(search: WarnSearch) { + const components: JSONEncodable[] = + await Promise.all( + search.currentPageWarns.map((warn) => + warnContainer(search.searcher.client, warn, search.id), + ), + ); + return components.toReversed().concat(viewPageRow(search)); +} + +export async function warnContainer( + client: Client, + warn: APIWarn, + searchId: Snowflake, +) { + const guild = client.guilds.cache.get(process.env.PV_GUILD_ID!)!; + const record = new Warn(apiConnService, warn); + + // color of the container + const color = Colors.Red; + + // member or user of related to the action + const [target, moderator] = await Promise.all([ + fetchMemberOrUser(record.targetId, guild), + fetchMemberOrUser(record.moderatorId, guild), + ]); + + // if name is too long shorten it + const printableReason = + record.reason.length >= 1000 + ? record.reason.substring(0, 997).concat("...") + : record.reason; + + return ( + new ContainerBuilder() + .setAccentColor(color) + .addSectionComponents((top) => + top + .setThumbnailAccessory((pfp) => pfp.setURL(target.displayAvatarURL())) + .addTextDisplayComponents((text) => + text.setContent( + [ + heading(`Member Warn`), + `${bold("Member")}: ${[target.toString(), inlineCode(getNameToDisplay(target))].join(" ")}`, + `${bold("Moderator")}: ${[moderator.toString(), inlineCode(getNameToDisplay(moderator))].join(" ")}`, + ].join("\n"), + ), + ), + ) + // .addSeparatorComponents((space) => + // space.setDivider(true).setSpacing(SeparatorSpacingSize.Large), + // ) + .addTextDisplayComponents((reason) => + reason.setContent( + `${heading("Reason", HeadingLevel.Three)}\n${printableReason}`, + ), + ) + .addSeparatorComponents((line) => + line.setDivider(true).setSpacing(SeparatorSpacingSize.Small), + ) + .addSectionComponents((footer) => + footer + .addTextDisplayComponents((text) => + text.setContent( + [ + `Created At: ${time(record.createdAt)} | Warn Id: ${inlineCode(record.id.toString())}`, + ] + .map(subtext) + .join("\n"), + ), + ) + .setButtonAccessory((view) => + view + .setCustomId(`wv_${record.id}_${searchId}`) + .setLabel(`View Warn`) + .setStyle(ButtonStyle.Primary), + ), + ) + ); +} + +export function viewPageRow(search: WarnSearch) { + const { totalWarns, page, limit, id } = search; + const totalPages = Math.ceil((totalWarns ?? 0) / limit!); + return new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setDisabled(page! <= 0) + .setCustomId(`wp_${id}_l`) + .setEmoji("⬅️") + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setDisabled(true) + .setCustomId("Button does not use ID") + .setLabel(`${page! + 1}\\${totalPages}`) + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setDisabled(totalPages <= page! + 1) + .setCustomId(`wp_${id}_n`) + .setEmoji("➡️") + .setStyle(ButtonStyle.Secondary), + ); +} + +export async function soloWarn( + searchId: string, + warn: APIWarn, + client: Client, +) { + const guild = client.guilds.cache.get(process.env.PV_GUILD_ID!)!; + const record = new Warn(apiConnService, warn); + + // color of the container + const color = Colors.Red; + + // member or user of related to the action + const [target, moderator] = await Promise.all([ + fetchMemberOrUser(record.targetId, guild), + fetchMemberOrUser(record.moderatorId, guild), + ]); + + const container = new ContainerBuilder() + .setAccentColor(color) + .addSectionComponents((top) => + top + .setThumbnailAccessory((pfp) => pfp.setURL(target.displayAvatarURL())) + .addTextDisplayComponents((text) => + text.setContent( + [ + heading(`Member Warn`), + `${bold("Member")}: ${[target.toString(), inlineCode(getNameToDisplay(target))].join(" ")}`, + `${bold("Moderator")}: ${[moderator.toString(), inlineCode(getNameToDisplay(moderator))].join(" ")}`, + ].join("\n"), + ), + ), + ) + // .addSeparatorComponents((space) => + // space.setDivider(true).setSpacing(SeparatorSpacingSize.Large), + // ) + .addTextDisplayComponents((reason) => + reason.setContent( + `${heading("Reason", HeadingLevel.Three)}\n${record.reason}`, + ), + ) + .addSeparatorComponents((line) => + line.setDivider(true).setSpacing(SeparatorSpacingSize.Small), + ) + .addTextDisplayComponents((text) => + text.setContent( + [ + `Last Updated: ${time(record.updatedAt)} | Warn Id: ${inlineCode(record.id.toString())}`, + ] + .map(subtext) + .join("\n"), + ), + ); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`wp_${searchId}`) + .setLabel("Back to Search") + .setStyle(ButtonStyle.Secondary), + ); + return [container, row]; +} + +export function warnDMContainer(record: Warn) { + return new ContainerBuilder() + .setAccentColor(resolveColor(WarnEmbedColor.updated)) + .addTextDisplayComponents((text) => + text.setContent( + [ + heading("You Have Received a Warning"), + heading("Reason", HeadingLevel.Three), + `${record.reason}`, + ].join("\n"), + ), + ) + .addSeparatorComponents((line) => + line.setDivider(true).setSpacing(SeparatorSpacingSize.Small), + ) + .addTextDisplayComponents((footer) => + footer.setContent( + [`Issued At: ${time(record.createdAt)}`].map(subtext).join("\n"), + ), + ); +} + +export async function warnModContainer( + record: Warn, + guild: Guild, + receivedByUser: boolean, +) { + // member or user of related to the action + const [target, moderator] = await Promise.all([ + fetchMemberOrUser(record.targetId, guild), + fetchMemberOrUser(record.moderatorId, guild), + ]); + const topText = [ + heading(`Member Warn`), + `${bold("Member")}: ${[target.toString(), inlineCode(getNameToDisplay(target))].join(" ")}`, + `${bold("Moderator")}: ${[moderator.toString(), inlineCode(getNameToDisplay(moderator))].join(" ")}`, + ]; + if (!receivedByUser) { + topText.splice( + 1, + 0, + subtext("Member did not receive DM of warn due to privacy settings"), + ); + } + + return new ContainerBuilder() + .setAccentColor(resolveColor(WarnEmbedColor.updated)) + .addSectionComponents((top) => + top + .setThumbnailAccessory((pfp) => pfp.setURL(target.displayAvatarURL())) + .addTextDisplayComponents((text) => + text.setContent(topText.join("\n")), + ), + ) + .addTextDisplayComponents((reason) => + reason.setContent( + [heading("Reason", HeadingLevel.Three), record.reason].join("\n"), + ), + ) + .addSeparatorComponents((line) => + line.setDivider(true).setSpacing(SeparatorSpacingSize.Small), + ) + .addSectionComponents((footer) => + footer + .addTextDisplayComponents((text) => + text.setContent( + [`Issued At: ${time(record.createdAt)}`].map(subtext).join("\n"), + ), + ) + .setButtonAccessory((button) => + button + .setCustomId(`vumw_${target.id}`) + .setLabel("View Member Warns") + .setStyle(ButtonStyle.Secondary), + ), + ); +} diff --git a/src/features/moderation/warnSearch.ts b/src/features/moderation/warnSearch.ts index 8162af15..bbdc2880 100644 --- a/src/features/moderation/warnSearch.ts +++ b/src/features/moderation/warnSearch.ts @@ -1,10 +1,10 @@ -import { ActionRowBuilder, ButtonBuilder, italic } from "discord.js"; +/*import { ActionRowBuilder, ButtonBuilder, italic } from "discord.js"; import { FilterQuery } from "mongoose"; -import { Warn, WarningRecord } from "../../models/Warn.js"; -import { WarningSearch, WarnSearch } from "../../models/WarnSearch.js"; -import { leftButton, pageNumber, rightButton } from "./buttons.js"; -import { viewWarningEmbeds } from "./embeds.js"; -import { numberOfWarnEmbedsOnPage } from "./types.js"; +import { Warn, WarningRecord } from "@/models/Warn"; +import { WarningSearch, WarnSearch } from "@/models/WarnSearch"; +import { leftButton, pageNumber, rightButton } from "./buttons"; +import { viewWarningEmbeds } from "./embeds"; +import { numberOfWarnEmbedsOnPage } from "./types"; /** * Render message for search of warnings @@ -14,7 +14,7 @@ import { numberOfWarnEmbedsOnPage } from "./types.js"; * @param isStart - is this the initiation of a search * @returns partial message object compatible with Interaction reply and update */ -export async function warnSearch( +/*export async function warnSearch( record: WarnSearch | string, isMod: boolean, isRightMove: boolean = false, @@ -84,4 +84,4 @@ export async function warnSearch( ), ], }; -} +}*/ diff --git a/src/features/profile.ts b/src/features/profile.ts index 8af8c8ff..643931cd 100644 --- a/src/features/profile.ts +++ b/src/features/profile.ts @@ -5,7 +5,7 @@ import { GuildMember, TimestampStyles, } from "discord.js"; -import { getAuthorOptions } from "./moderation/embeds.js"; +import { getAuthorOptions } from "./moderation/embeds"; /** * @param member - The member to display the information of diff --git a/src/features/state/index.ts b/src/features/state/index.ts index 51512139..d29637ba 100644 --- a/src/features/state/index.ts +++ b/src/features/state/index.ts @@ -1,11 +1,11 @@ +import { isGuildMember } from "@/util"; +import { getStatesFromMember } from "@/util/states"; import { AutocompleteInteraction, ChatInputCommandInteraction, } from "discord.js"; -import { isGuildMember } from "../../util/index.js"; -import { getStatesFromMember } from "../../util/states/index.js"; -import { memberList } from "./member-list.js"; -import ping from "./ping.js"; +import { memberList } from "./member-list"; +import ping from "./ping"; /** * Executes the lead command based on the subcommand and subcommand group provided in the interaction options. diff --git a/src/features/state/member-list.ts b/src/features/state/member-list.ts index 6652a5d7..eee5d740 100644 --- a/src/features/state/member-list.ts +++ b/src/features/state/member-list.ts @@ -1,40 +1,47 @@ -import { AttachmentBuilder, ChatInputCommandInteraction, MessageFlags } from "discord.js"; -import { ns } from "../../commands/chat/state.js"; -import { localize } from "../../i18n.js"; +import { ns } from "@/commands/chat/state"; +import { localize } from "@/i18n"; +import { + AttachmentBuilder, + ChatInputCommandInteraction, + MessageFlags, +} from "discord.js"; /** * Executes a chat input command interaction to export role members to a CSV file. * @param interaction - The chat input command interaction object. */ export async function memberList(interaction: ChatInputCommandInteraction) { - - // Defer the reply to indicate that the bot is processing the command. - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - - if(!interaction.inCachedGuild()) return - - // Extract the locale and options from the interaction. - const localeBundle = localize.getLocale(interaction.locale); - const options = interaction.options + // Defer the reply to indicate that the bot is processing the command. + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - // Get the role from interaction options using true to make the argument required - const role = options.getRole("role", true) + if (!interaction.inCachedGuild()) return; - // Create a CSV attachment using the AttachmentBuilder class. - const csv = new AttachmentBuilder( - // Construct the CSV content using the role's members. - Buffer.from(`Display Name,Username,Id\n${ - role.members.map((member) => - `${member.displayName},${member.user.username},${member.id}` - ).join("\n")}`), - // Set the file name for the CSV attachment based on the role name and interaction ID. - { name: `${role.name.replace(" ", "-")}.csv` } - ); + // Extract the locale and options from the interaction. + const localeBundle = localize.getLocale(interaction.locale); + const options = interaction.options; + + // Get the role from interaction options using true to make the argument required + const role = options.getRole("role", true); + + // Create a CSV attachment using the AttachmentBuilder class. + const csv = new AttachmentBuilder( + // Construct the CSV content using the role's members. + Buffer.from( + `Display Name,Username,Id\n${role.members + .map( + (member) => + `${member.displayName},${member.user.username},${member.id}`, + ) + .join("\n")}`, + ), + // Set the file name for the CSV attachment based on the role name and interaction ID. + { name: `${role.name.replace(" ", "-")}.csv` }, + ); // Send a follow-up message with a content and the CSV file attached. await interaction.editReply({ content: localeBundle?.t("member-list-message-followup", ns, { - role: role.toString(), + role: role.toString(), }), files: [csv], }); diff --git a/src/features/state/ping.ts b/src/features/state/ping.ts index 94b12665..54a0610e 100644 --- a/src/features/state/ping.ts +++ b/src/features/state/ping.ts @@ -1,3 +1,11 @@ +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { AddSplitCustomId, getGuildChannel } from "@/util"; +import { apiConnService } from "@/util/api/pvapi"; +import { + IDiscordStateRole, + zDiscordStateRole, +} from "@/util/states/discordStateRole"; +import { isStateAbbreviations } from "@/util/states/types"; import { ActionRowBuilder, ButtonBuilder, @@ -7,6 +15,7 @@ import { Guild, GuildMember, heading, + LabelBuilder, Message, MessageCreateOptions, MessageFlags, @@ -20,10 +29,7 @@ import { TextInputStyle, userMention, } from "discord.js"; -import { States } from "../../models/State.js"; -import { AddSplitCustomId, getGuildChannel } from "../../util/index.js"; -import { isStateAbbreviations } from "../../util/states/types.js"; -import { messageMaxLength, titleMaxLength } from "./constants.js"; +import { messageMaxLength, titleMaxLength } from "./constants"; /** * Executes the ping command to send a message to a channel. @@ -70,35 +76,36 @@ export default async function ping(interaction: ChatInputCommandInteraction) { const legacyOption = !(options.getBoolean("usecomponents") ?? false); if (!messageOption) { - const title = new TextInputBuilder() + const titleInput = new TextInputBuilder() .setCustomId("title") - .setLabel("Title") .setMaxLength(titleMaxLength) .setPlaceholder(`State Announcement`) .setRequired(true) .setStyle(TextInputStyle.Short); - if (titleOption) title.setValue(titleOption); + if (titleOption) titleInput.setValue(titleOption); - const message = new TextInputBuilder() + const messageInput = new TextInputBuilder() .setCustomId("message") - .setLabel("Message") .setPlaceholder(`Your message to state member`) .setMaxLength(messageMaxLength) .setRequired(true) .setStyle(TextInputStyle.Paragraph); - const titleRow = new ActionRowBuilder().setComponents( - title, - ); - const messageRow = new ActionRowBuilder().setComponents( - message, - ); + if (messageOption) messageInput.setValue(messageOption); + + const titleLabel = new LabelBuilder() + .setLabel("Title") + .setTextInputComponent(titleInput); + + const messageLabel = new LabelBuilder() + .setLabel("Message") + .setTextInputComponent(messageInput); const modal = new ModalBuilder() .setCustomId(AddSplitCustomId("sp", stateAbbreviation, legacyOption)) .setTitle("State Ping Message") - .setComponents(titleRow, messageRow); + .addLabelComponents(titleLabel, messageLabel); await interaction.showModal(modal); return; @@ -106,38 +113,55 @@ export default async function ping(interaction: ChatInputCommandInteraction) { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - const state = await States.findOne({ - guildId: interaction.guildId, - abbreviation: stateAbbreviation, - }).catch(console.error); - if (!state || !state.roleId || !state.channelId) return; + let state: IDiscordStateRole | undefined = undefined; + + try { + state = await apiConnService.get( + Routes.discordStateRole(stateAbbreviation), + zDiscordStateRole, + ); + + // console.log(state); + } catch (err) { + console.error(err); + if ( + typeof err === "object" && + err && + "message" in err && + typeof err.message === "string" + ) + return interaction.reply(err.message); + } + + if (!state) return; // check to see if the person trying to use the command has the role being pinged - if (!member.roles.cache.has(state.roleId)) { + if (!member.roles.cache.has(state.memberRoleId)) { await interaction.editReply({ - content: `You are not allowed to run this command to ${state.name}`, + content: `You are missing the ${roleMention(state.memberRoleId)} state role.`, + allowedMentions: {}, }); return; } - const channel = await getGuildChannel(guild, state.channelId); + const channel = await getGuildChannel(guild, state.memberChannelId); if (!channel || !channel.isSendable()) return; let stateMessageCreateOptions: MessageCreateOptions; if (messageOption) { if (legacyOption) stateMessageCreateOptions = legacyStateMessageCreate( - state.roleId, + state.memberRoleId, member.id, messageOption, - titleOption ?? `${state.name} Announcement`, + titleOption ?? `${state.stateName} Announcement`, ); else stateMessageCreateOptions = stateMessageCreate( - state.roleId, + state.memberRoleId, member.id, messageOption, - titleOption ?? `${state.name} Announcement`, + titleOption ?? `${state.stateName} Announcement`, ); const pingMessage = await channel.send(stateMessageCreateOptions); diff --git a/src/features/test/test.ts b/src/features/test/test.ts deleted file mode 100644 index d2fc8ef6..00000000 --- a/src/features/test/test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ChatInputCommandInteraction } from "discord.js"; - -export default async function (interaction: ChatInputCommandInteraction) {} diff --git a/src/features/timeout.ts b/src/features/timeout.ts index 77df0d34..cc9152a0 100644 --- a/src/features/timeout.ts +++ b/src/features/timeout.ts @@ -6,11 +6,7 @@ import { inlineCode, TimestampStyles, } from "discord.js"; -import { - getAuthorOptions, - reasonField, - userField, -} from "./moderation/embeds.js"; +import { getAuthorOptions, reasonField, userField } from "./moderation/embeds"; const timeoutEmbedColor: ColorResolvable = Colors.LuminousVividPink; diff --git a/src/i18n.ts b/src/i18n.ts index 425dfc20..7068fb74 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,5 +1,5 @@ import { Locale } from "discord.js"; -import { i18n } from "./Classes/index.js"; +import { i18n } from "./Classes"; // Load locales // Note: setGlobalResource should always be set first diff --git a/src/index.ts b/src/index.ts index c7e4a90d..7832b10b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,15 +4,12 @@ import { Partials, } from "discord.js"; import express from "express"; -import { Client, Interaction } from "./Classes/index.js"; -import * as commands from "./commands/index.js"; -import * as events from "./events/index.js"; -import * as buttons from "./interactions/buttons/index.js"; -import * as modals from "./interactions/modals/index.js"; -import * as selectMenus from "./interactions/select_menus/index.js"; -import dbConnect from "./util/libmongo.js"; - -dbConnect(); +import { Client, Interaction } from "./Classes"; +import * as commands from "./commands"; +import * as events from "./events"; +import * as buttons from "./interactions/buttons"; +import * as modals from "./interactions/modals"; +import * as selectMenus from "./interactions/select_menus"; // Initialization (specify intents and partials) export const client = new Client({ @@ -55,7 +52,7 @@ for (const selectMenu of Object.values(selectMenus)) // Bot logins to Discord services void client.login(process.env.DISCORD_TOKEN).then(() => { // Skip if no-deployment flag is set, else deploys command - if (!process.argv.includes("--no-deployment")) + if (process.argv.includes("--deploy")) // removes guild command from set guild // client.commands.deregisterGuildCommands(process.env.GUILDID); // deploys commands diff --git a/src/interactions/buttons/README.md b/src/interactions/buttons/README.md index 3710f314..ab0a8543 100644 --- a/src/interactions/buttons/README.md +++ b/src/interactions/buttons/README.md @@ -13,7 +13,7 @@ Interactions start with the bot [sending the button](https://discordjs.guide/int ```ts // src/interactions/button/button.ts import { ButtonInteraction } from "discord.js"; - import { Interaction } from "../../../Classes/index.js"; + import { Interaction } from "@/Classes"; export default new Interaction({ customIdPrefix: "button", @@ -27,15 +27,15 @@ Interactions start with the bot [sending the button](https://discordjs.guide/int ```ts // src/interactions/button/index.ts - export { default as string } from "./button.js"; + export { default as string } from "./button"; ``` -3. In the root [`index.ts`](../../index.ts), make sure the following is present: +3. In the root [`index.ts`](@/index.ts), make sure the following is present: ```ts // src/index.ts - import { Client } from "./Classes/index.js"; - import * as buttons from "./interactions/buttons/index.js"; + import { Client } from "./Classes"; + import * as buttons from "./interactions/buttons"; export const client = new Client({ receiveMessageComponents: true, // enables the usage of message components diff --git a/src/interactions/buttons/index.ts b/src/interactions/buttons/index.ts index 8b8662bb..41289350 100644 --- a/src/interactions/buttons/index.ts +++ b/src/interactions/buttons/index.ts @@ -1,14 +1,2 @@ -export { - banAppeal, - warnViewLeft, - warnViewRight, - warnViewUser, -} from "./warn.js"; - -export { - deleteWarnNo, - deleteWarnYes, - removeWarnNo, - removeWarnYes, -} from "./moderation/remove.js"; -export { unwelcome, welcomed } from "./welcome.js"; +export * from "./moderation/view"; +export { unwelcome, welcomed } from "./welcome"; diff --git a/src/interactions/buttons/moderation/remove.ts b/src/interactions/buttons/moderation/remove.ts index ae3c92d8..1cbdbb0b 100644 --- a/src/interactions/buttons/moderation/remove.ts +++ b/src/interactions/buttons/moderation/remove.ts @@ -1,4 +1,4 @@ -import { +/*import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, @@ -6,26 +6,27 @@ import { GuildMember, inlineCode, } from "discord.js"; -import { Interaction } from "../../../Classes/Interaction.js"; -import { modViewWarningHistory } from "../../../features/moderation/buttons.js"; +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { APIWarn } from "@/Classes/API/ApiConnService/types"; +import { Interaction } from "@/Classes/Interaction"; +import { modViewWarningHistory } from "@/features/moderation/buttons"; import { getAuthorOptions, userField, warnLogUpdateEmbed, -} from "../../../features/moderation/embeds.js"; +} from "@/features/moderation/embeds"; import { WarnButtonsPrefixes, WarnEmbedColor, -} from "../../../features/moderation/types.js"; -import { GuildSetting } from "../../../models/Setting.js"; -import { Warn } from "../../../models/Warn.js"; -import { getGuildChannel, getMember } from "../../../util/index.js"; +} from "@/features/moderation/types"; +import { apiConnService } from "@/util/api/pvapi"; +import { getGuildChannel, getMember } from "@/util"; export const removeWarnYes = new Interaction({ customIdPrefix: WarnButtonsPrefixes.removeWarnYes, run: async (interaction: ButtonInteraction) => { const record = await getWarnRecord(interaction); - const { user, member, guild, guildId } = interaction; + const { user, member, guild } = interaction; if (!record || !guild) return; const target = await getMember(guild, record.target.discordId); @@ -52,7 +53,9 @@ export const removeWarnYes = new Interaction({ const embed = warnLogUpdateEmbed(record, mod, target, updater); - const settings = await GuildSetting.findOne({ guildId }); + const warnChannelId = (await apiConnService.get( + Routes.setting("warn_log_channel_id"), + )) as string; const row = new ActionRowBuilder().addComponents( modViewWarningHistory(target.id), @@ -64,8 +67,8 @@ export const removeWarnYes = new Interaction({ components: [row], }); - if (settings?.warn.logChannelId) { - const log = await getGuildChannel(guild, settings.warn.logChannelId); + if (warnChannelId) { + const log = await getGuildChannel(guild, warnChannelId); if (log?.isSendable()) { log.send({ embeds: [embed.setAuthor(getAuthorOptions(updater))], @@ -113,9 +116,9 @@ export const deleteWarnYes = new Interaction({ embed.setThumbnail(target.displayAvatarURL({ forceStatic: true })); } - const settings = await GuildSetting.findOne({ - guildId: interaction.guildId, - }); + const warnChannelId = (await apiConnService.get( + Routes.setting("warn_log_channel_id"), + )) as string; record?.deleteOne(); @@ -129,8 +132,8 @@ export const deleteWarnYes = new Interaction({ components: [row], }); - if (settings?.warn.logChannelId) { - const log = await getGuildChannel(guild, settings.warn.logChannelId); + if (warnChannelId) { + const log = await getGuildChannel(guild, warnChannelId); if (log?.isSendable()) { let member = interaction.member ?? undefined; if (!(member instanceof GuildMember)) { @@ -164,13 +167,15 @@ export const deleteWarnNo = new Interaction({ * @param interaction - button interaction * @returns warn document or undefined */ -async function getWarnRecord(interaction: ButtonInteraction) { +/*async function getWarnRecord(interaction: ButtonInteraction) { const { customId, client } = interaction; const warnId = customId.split(client.splitCustomIdOn!)[1]; // check that warning exists - const record = await Warn.findById(warnId); + const record = (await apiConnService.get( + Routes.discordWarn(warnId), + )) as APIWarn; if (!record) { interaction.update({ content: `Unable to locate warning check warn Id: ${inlineCode(warnId)}.\nPlease notify an admin `, @@ -180,4 +185,4 @@ async function getWarnRecord(interaction: ButtonInteraction) { } return record; -} +}*/ diff --git a/src/interactions/buttons/moderation/view.ts b/src/interactions/buttons/moderation/view.ts new file mode 100644 index 00000000..2eb542d5 --- /dev/null +++ b/src/interactions/buttons/moderation/view.ts @@ -0,0 +1,93 @@ +import { Interaction } from "@/Classes"; +import { soloWarn, warnPage } from "@/features/moderation/warn-render"; +import { warnSearchManger } from "@/util/api/pvapi"; +import { + ButtonInteraction, + MessageFlags, + TextDisplayBuilder, +} from "discord.js"; + +export const warnView = new Interaction({ + customIdPrefix: "wv", + run: async (interaction: ButtonInteraction) => { + const args = interaction.customId.split( + interaction.client.splitCustomIdOn!, + ); + const warnId = args[1]; + const searchId = args[2]; + + const search = warnSearchManger.cache.get(searchId); + + if (!search) + return interaction.update({ + components: [ + new TextDisplayBuilder().setContent( + "Search has ended please restart search", + ), + ], + }); + + const warn = search.currentPageWarns.get(warnId); + if (!warn) + return interaction.update({ + components: [ + new TextDisplayBuilder({ + content: "Warn could not be found on page", + }), + ], + }); + + await interaction.update({ + components: await soloWarn(search.id, warn, interaction.client), + }); + }, +}); + +export const warnPageButton = new Interaction({ + customIdPrefix: "wp", + run: async (interaction: ButtonInteraction) => { + const args = interaction.customId.split( + interaction.client.splitCustomIdOn!, + ); + const searchId = args[1]; + await interaction.deferUpdate(); + const search = warnSearchManger.cache.get(searchId); + // console.log(args); + switch (args[2]) { + case "l": + await search?.fetchLastPage(); + break; + case "n": + await search?.fetchNextPage(); + break; + default: + break; + } + // console.log(search?.currentPageWarns); + await interaction.editReply({ + components: await warnPage(search!), + allowedMentions: {}, + }); + }, +}); + +export const viewUserWarns = new Interaction({ + customIdPrefix: "vumw", + run: async (interaction) => { + if (!interaction.inCachedGuild()) return; + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + const args = interaction.customId.split( + interaction.client.splitCustomIdOn!, + ); + const targetId = args[1]; + + const search = warnSearchManger.newSearch(interaction.member, { targetId }); + await search.fetchPage(); + + await interaction.editReply({ + flags: MessageFlags.IsComponentsV2, + components: await warnPage(search), + allowedMentions: {}, + }); + }, +}); diff --git a/src/interactions/buttons/warn.ts b/src/interactions/buttons/warn.ts index 7831149a..c9acbb51 100644 --- a/src/interactions/buttons/warn.ts +++ b/src/interactions/buttons/warn.ts @@ -1,4 +1,4 @@ -import { +/*import { ActionRow, ActionRowBuilder, ButtonBuilder, @@ -7,19 +7,19 @@ import { MessageFlags, Snowflake, } from "discord.js"; -import { Interaction } from "../../Classes/index.js"; -import { appealDmSubmitted } from "../../features/moderation/buttons.js"; -import { dateDiffInDays } from "../../features/moderation/index.js"; -import { warnModal } from "../../features/moderation/modals.js"; +import { Interaction } from "@/Classes"; +import { appealDmSubmitted } from "@/features/moderation/buttons"; +import { dateDiffInDays } from "@/features/moderation"; +import { warnModal } from "@/features/moderation/modals"; import { WarnButtonsPrefixes, WarnModalPrefixes, -} from "../../features/moderation/types.js"; -import { warnSearch } from "../../features/moderation/warnSearch.js"; -import { GuildSetting } from "../../models/Setting.js"; -import { Warn } from "../../models/Warn.js"; -import { WarningSearch } from "../../models/WarnSearch.js"; -import { AddSplitCustomId, getGuildChannel } from "../../util/index.js"; +} from "@/features/moderation/types"; +import { warnSearch } from "@/features/moderation/warnSearch"; +import { AddSplitCustomId, getGuildChannel } from "@/util"; +import { apiConnService } from "@/util/api/pvapi"; +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { APIWarn } from "@/Classes/API/ApiConnService/types"; // button to move warn view left export const warnViewLeft = new Interaction({ @@ -58,7 +58,7 @@ export const updateById = new Interaction({ const warnId = customId.split(client.splitCustomIdOn!)[1]; // check that warning exists - const record = await Warn.findById(warnId); + const record = await apiConnService.get(Routes.discordWarn(warnId)) as APIWarn if (!record) { interaction.update({ content: "Warning does not exist", @@ -71,7 +71,7 @@ export const updateById = new Interaction({ AddSplitCustomId(WarnModalPrefixes.updateById, warnId), "Update Warning", record.reason, - dateDiffInDays(new Date(), record.expireAt), + dateDiffInDays(new Date(), new Date(record.expiresAtUtc)), ); interaction.showModal(modal); @@ -161,4 +161,4 @@ export const banAppeal = new Interaction({ components: [actionRow], }); }, -}); +});*/ diff --git a/src/interactions/buttons/welcome.ts b/src/interactions/buttons/welcome.ts index cc760921..e86b6c19 100644 --- a/src/interactions/buttons/welcome.ts +++ b/src/interactions/buttons/welcome.ts @@ -1,3 +1,9 @@ +import { Interaction } from "@/Classes"; +import { + unWelcomeButton, + welcomeButton, + welcomeColors, +} from "@/features/welcome"; import { ActionRowBuilder, bold, @@ -9,12 +15,6 @@ import { MessageFlags, TextDisplayBuilder, } from "discord.js"; -import { Interaction } from "../../Classes/index.js"; -import { - unWelcomeButton, - welcomeButton, - welcomeColors, -} from "../../features/welcome.js"; export const welcomed = new Interaction({ customIdPrefix: "welcomed", diff --git a/src/interactions/modals/README.md b/src/interactions/modals/README.md index e83e1eaf..f0b2d2cf 100644 --- a/src/interactions/modals/README.md +++ b/src/interactions/modals/README.md @@ -13,7 +13,7 @@ Interactions start with the bot [sending the modal](https://discordjs.guide/inte ```ts // src/interactions/modals/modal.ts import { ModalSubmitInteraction } from "discord.js"; - import { Interaction } from "../../../Classes/index.js"; + import { Interaction } from "@/Classes"; export default new Interaction({ customIdPrefix: "modal", @@ -27,15 +27,15 @@ Interactions start with the bot [sending the modal](https://discordjs.guide/inte ```ts // src/interactions/modals/index.ts - export { default as string } from "./modal.js"; + export { default as string } from "./modal"; ``` -3. In the root [`index.ts`](../../index.ts), make sure the following is present: +3. In the root [`index.ts`](@/index.ts), make sure the following is present: ```ts // src/index.ts - import { Client } from "./Classes/index.js"; - import * as modals from "./interactions/modals/index.js"; + import { Client } from "./Classes"; + import * as modals from "./interactions/modals"; export const client = new Client({ receiveMessageComponents: true, // enables the usage of message components diff --git a/src/interactions/modals/index.ts b/src/interactions/modals/index.ts index 57d33755..b45c1dbf 100644 --- a/src/interactions/modals/index.ts +++ b/src/interactions/modals/index.ts @@ -1,7 +1,5 @@ -export { warnCreate } from "./warn/create.js"; +export { warnCreate } from "./warn/create"; -export { warnUpdatedById } from "./warn/updateById.js"; +export { messageReport, userReport } from "./report"; -export { messageReport, userReport } from "./report.js"; - -export { statePing } from "./statePing.js"; +export { statePing } from "./statePing"; diff --git a/src/interactions/modals/report.ts b/src/interactions/modals/report.ts index bab5c68d..05db9475 100644 --- a/src/interactions/modals/report.ts +++ b/src/interactions/modals/report.ts @@ -1,3 +1,17 @@ +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { Interaction } from "@/Classes/Interaction"; +import { + SettingsResponse, + zSettingsResponse, +} from "@/contracts/responses/SettingsResponse"; +import { getAuthorOptions } from "@/features/moderation/embeds"; +import { + messageReportColor, + reportModalPrefix, + userReportColor, +} from "@/features/report"; +import { getGuildChannel, getMember } from "@/util"; +import { apiConnService } from "@/util/api/pvapi"; import { ActionRowBuilder, ButtonBuilder, @@ -8,15 +22,6 @@ import { MessageFlags, ModalSubmitInteraction, } from "discord.js"; -import { Interaction } from "../../Classes/Interaction.js"; -import { getAuthorOptions } from "../../features/moderation/embeds.js"; -import { - messageReportColor, - reportModalPrefix, - userReportColor, -} from "../../features/report.js"; -import { GuildSetting } from "../../models/Setting.js"; -import { getGuildChannel, getMember } from "../../util/index.js"; /** * `userReport` is a modal interaction which allows users to report other users. It: @@ -31,7 +36,7 @@ export const userReport = new Interaction({ run: async (interaction) => { if (!interaction.inGuild()) return; - const { guild, guildId, customId, client, member } = interaction; + const { guild, customId, client, member } = interaction; const targetId = customId.split(client.splitCustomIdOn!)[1]; if (!guild) return; @@ -45,15 +50,16 @@ export const userReport = new Interaction({ : await getMember(guild, member.user.id); if (!reporter) return; - const setting = await GuildSetting.findOne({ guildId }); + const res = await apiConnService.get( + Routes.setting("report_log_channel_id"), + zSettingsResponse, + ); + const logChannelId = res.data; const comment = interaction.fields.getTextInputValue("comment"); - if (setting?.report.logChannelId) { - const logChannel = await getGuildChannel( - guild, - setting.report.logChannelId, - ); + if (logChannelId) { + const logChannel = await getGuildChannel(guild, logChannelId); if (!logChannel?.isSendable()) return; logChannel.send({ embeds: [ @@ -90,7 +96,7 @@ export const messageReport = new Interaction({ run: async (interaction) => { if (!interaction.inGuild()) return; - const { guild, guildId, customId, client, member } = interaction; + const { guild, customId, client, member } = interaction; const channelId = customId.split(client.splitCustomIdOn!)[1]; if (!guild) return; @@ -117,15 +123,16 @@ export const messageReport = new Interaction({ if (!reporter) return; - const setting = await GuildSetting.findOne({ guildId }); + const res = await apiConnService.get( + Routes.setting("report_log_channel_id"), + zSettingsResponse, + ); + const logChannelId = res.data; const comment = interaction.fields.getTextInputValue("comment"); - if (setting?.report.logChannelId) { - const logChannel = await getGuildChannel( - guild, - setting.report.logChannelId, - ); + if (logChannelId) { + const logChannel = await getGuildChannel(guild, logChannelId); if (!logChannel?.isSendable()) return; const embed = new EmbedBuilder() .setTitle("Message Report") diff --git a/src/interactions/modals/statePing.ts b/src/interactions/modals/statePing.ts index 0c800ad6..821ec5d4 100644 --- a/src/interactions/modals/statePing.ts +++ b/src/interactions/modals/statePing.ts @@ -1,17 +1,22 @@ +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { Interaction } from "@/Classes/Interaction"; +import { + legacyStateMessageCreate, + stateMessageCreate, + statePingReply, +} from "@/features/state/ping"; +import { apiConnService } from "@/util/api/pvapi"; +import { + IDiscordStateRole, + zDiscordStateRole, +} from "@/util/states/discordStateRole"; +import { isStateAbbreviations } from "@/util/states/types"; import { Guild, MessageCreateOptions, MessageFlags, ModalSubmitInteraction, } from "discord.js"; -import { Interaction } from "../../Classes/Interaction.js"; -import { - legacyStateMessageCreate, - stateMessageCreate, - statePingReply, -} from "../../features/state/ping.js"; -import { States } from "../../models/State.js"; -import { isStateAbbreviations } from "../../util/states/types.js"; /** * `statePing` is a modal interaction that provides state leads an interface @@ -36,40 +41,42 @@ export const statePing = new Interaction({ const stateAbbreviation = args[1]; const legacyOption = args[2] === "true"; - // console.log(stateAbbreviation, legacyOption); if (!isStateAbbreviations(stateAbbreviation)) return; await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - const state = await States.findOne({ - guildId: guild.id, - abbreviation: stateAbbreviation, - }).catch(console.error); - // console.log(state, !(state && state.roleId && state.channelId)); - - if (!(state && state.roleId && state.channelId)) return; + let state: IDiscordStateRole; + try { + state = await apiConnService.get( + Routes.discordStateRole(stateAbbreviation), + zDiscordStateRole, + ); + } catch (err) { + console.error(err); + //@ts-expect-error can't type error args + return interaction.reply(err.message); + } const content = fields.getTextInputValue("message"); const title = fields.getTextInputValue("title"); const stateChannel = - guild.channels.cache.get(state.channelId) ?? - (await guild.channels.fetch(state.channelId).catch(console.error)); - // console.log(stateChannel); + guild.channels.cache.get(state.memberChannelId) ?? + (await guild.channels.fetch(state.memberChannelId).catch(console.error)); if (!(stateChannel && stateChannel.isSendable())) return; let stateMessageCreateOptions: MessageCreateOptions; if (legacyOption) stateMessageCreateOptions = legacyStateMessageCreate( - state.roleId, + state.memberRoleId, user.id, content, title, ); else stateMessageCreateOptions = stateMessageCreate( - state.roleId, + state.memberRoleId, user.id, content, title, diff --git a/src/interactions/modals/warn/create.ts b/src/interactions/modals/warn/create.ts index 96438646..74929451 100644 --- a/src/interactions/modals/warn/create.ts +++ b/src/interactions/modals/warn/create.ts @@ -1,23 +1,20 @@ +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { Interaction } from "@/Classes/Interaction"; +import { SettingsResponse, zSettingsResponse } from "@/contracts/responses"; +import { + warnDMContainer, + warnModContainer, +} from "@/features/moderation/warn-render"; +import { apiConnService, warnSearchManger } from "@/util/api/pvapi"; import { ActionRowBuilder, ButtonBuilder, + ButtonStyle, + DiscordAPIError, + Message, MessageFlags, ModalSubmitInteraction, } from "discord.js"; -import { Interaction } from "../../../Classes/Interaction.js"; -import { - modViewWarningHistory, - userViewWarnHistory, -} from "../../../features/moderation/buttons.js"; -import { - newWarningDmEmbed, - newWarningLogEmbed, - newWarnModEmbed, -} from "../../../features/moderation/embeds.js"; -import { WarnModalPrefixes } from "../../../features/moderation/types.js"; -import { GuildSetting } from "../../../models/Setting.js"; -import { Warn } from "../../../models/Warn.js"; -import { getGuildChannel, isGuildMember } from "../../../util/index.js"; /** * `warnCreate` is a modal interaction which allows mods to send warnings to guild members. It: @@ -29,66 +26,97 @@ import { getGuildChannel, isGuildMember } from "../../../util/index.js"; * */ export const warnCreate = new Interaction({ - customIdPrefix: WarnModalPrefixes.createWarning, + customIdPrefix: "nw", run: async (interaction: ModalSubmitInteraction) => { - const { customId, client, guild, guildId, member, fields } = interaction; - const targetId = customId.split(client.splitCustomIdOn!)[1]; - - const numberRegex: RegExp = /^\d{1,3}$/is; - const target = guild?.members.cache.get(targetId); - const mod = member; - if (!(target && isGuildMember(mod))) return; + if (!interaction.inCachedGuild()) return; + const { member, fields } = interaction; + const targetMember = fields.getSelectedMembers("member")?.first(); + if (!targetMember) return; const reason = fields.getTextInputValue("reason"); - const modalDuration = fields.getTextInputValue("duration"); - let duration: number | undefined; - if (!numberRegex.test(modalDuration)) { - duration = undefined; - } else { - duration = Number(modalDuration); - } - - const record = await Warn.createWarning(target, mod, reason, duration); - const count = await Warn.countDocuments({ - "target.discordId": target.id, - expireAt: { $gte: new Date() }, - }); - const setting = await GuildSetting.findOne({ guildId }); - const userActionRow = new ActionRowBuilder(); + const duration = 30; + const expires = new Date(); + expires.setHours(expires.getHours() + duration * 24); - // TODO: Appeal System to notify head mod - // if (setting?.warn.appealChannelId) { - // userActionRow.addComponents(appealWarn(record)) - // } - - userActionRow.addComponents( - userViewWarnHistory(target.id, guild!).setLabel("View Your History"), - ); - - target.send({ - embeds: [newWarningDmEmbed(record, count, guild!)], - components: [], - }); - - const row = new ActionRowBuilder().addComponents( - modViewWarningHistory(targetId), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_res, newWarn, targetDM, logChannel] = await Promise.all([ + interaction.deferReply({ flags: MessageFlags.Ephemeral }), + warnSearchManger.createWarn({ + moderatorId: member.id, + targetId: targetMember.id, + reason, + expires, + }), + targetMember.createDM().catch((e) => { + if (e instanceof DiscordAPIError) return null; + throw e; + }), + apiConnService + .get( + Routes.setting("warn_log_channel_id"), + zSettingsResponse, + ) + .then(async (u) => { + const channel = await interaction.guild.channels + .fetch(u.data) + .catch((e) => { + if (e instanceof DiscordAPIError) return null; + throw e; + }); + if (channel?.isSendable()) { + return channel; + } + return null; + }), + ]); + let sentToUser: boolean = true; + if (targetDM) { + targetDM + .send({ + flags: MessageFlags.IsComponentsV2, + components: [warnDMContainer(newWarn)], + }) + .catch((e) => { + if (e instanceof DiscordAPIError) { + sentToUser = false; + } else { + throw e; + } + }); + } + const modContainer = await warnModContainer( + newWarn, + interaction.guild, + sentToUser, ); + let message: Message | null = null; + if (logChannel) { + message = await logChannel.send({ + flags: MessageFlags.IsComponentsV2, + components: [modContainer], + allowedMentions: {}, + }); + } - interaction.reply({ - flags: MessageFlags.Ephemeral, - embeds: [newWarnModEmbed(record, mod, target)], - components: [row], - }); - - if (setting?.warn.logChannelId) { - const channel = await getGuildChannel(guild!, setting?.warn.logChannelId); - if (channel?.isSendable()) { - channel.send({ - embeds: [newWarningLogEmbed(record, mod, target)], - components: [row], - }); - } + if (message) { + await interaction.editReply({ + content: "The warn has been successfully been issued. ", + components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("View Warn") + .setStyle(ButtonStyle.Link) + .setURL(message.url), + ), + ], + }); + } else { + await interaction.editReply({ + flags: MessageFlags.IsComponentsV2, + components: [modContainer], + allowedMentions: {}, + }); } }, }); diff --git a/src/interactions/modals/warn/updateById.ts b/src/interactions/modals/warn/updateById.ts deleted file mode 100644 index 1af1060a..00000000 --- a/src/interactions/modals/warn/updateById.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { - ActionRowBuilder, - ButtonBuilder, - GuildMember, - MessageFlags, - ModalSubmitInteraction, -} from "discord.js"; -import { Interaction } from "../../../Classes/Interaction.js"; -import { modViewWarningHistory } from "../../../features/moderation/buttons.js"; -import { warnLogUpdateEmbed } from "../../../features/moderation/embeds.js"; -import { - numberRegex, - WarnModalPrefixes, -} from "../../../features/moderation/types.js"; -import { GuildSetting } from "../../../models/Setting.js"; -import { setDate, Warn } from "../../../models/Warn.js"; -import { getGuildChannel, getMember } from "../../../util/index.js"; - -/** - * `warnUpdatedById` is a modal interaction which allows mods to update a warning by ID. It: - *
    - *
  • Updates the warning in MongoDB
  • - *
  • Notifies the mod that the warning has been updated via an embed
  • - *
  • If there is a channel for warning audit logs, logs the event
  • - *
- */ -export const warnUpdatedById = new Interaction({ - customIdPrefix: WarnModalPrefixes.updateById, - - run: async (interaction: ModalSubmitInteraction) => { - const { customId, client, guild, member } = interaction; - const warnId = customId.split(client.splitCustomIdOn!)[1]; - - const record = await Warn.findById(warnId); - if (!record || !guild) return; - - const [target, mod] = await Promise.all([ - getMember(guild, record.target.discordId), - getMember(guild, record.moderator.discordId), - ]); - - if (!target || !mod) return; - - let updater = member ?? undefined; - if (!updater) return; - else if (!(updater instanceof GuildMember)) - updater = await getMember(guild, updater?.user.id); - if (!updater) return; - const reason = interaction.fields.getTextInputValue("reason"); - - record.reason = reason; - - const modalDuration = interaction.fields.getTextInputValue("duration"); - let duration: number | undefined; - if (numberRegex.test(modalDuration)) { - duration = Number(modalDuration); - record.expireAt = setDate(duration); - } - record.updater = { - discordId: interaction.user.id, - username: interaction.user.username, - }; - record.updatedAt = new Date(); - record.save(); - - const row = new ActionRowBuilder().addComponents( - modViewWarningHistory(target.id), - ); - - interaction.reply({ - flags: MessageFlags.Ephemeral, - embeds: [warnLogUpdateEmbed(record, mod, target, updater)], - components: [row], - }); - - const setting = await GuildSetting.findOne({ guildId: guild?.id }); - - if (setting?.warn.logChannelId) { - const channel = await getGuildChannel(guild, setting.warn.logChannelId); - if (channel?.isSendable()) { - channel.send({ - embeds: [warnLogUpdateEmbed(record, mod, target, updater)], - components: [row], - }); - } - } - }, -}); diff --git a/src/interactions/select_menus/README.md b/src/interactions/select_menus/README.md index 87581c7c..f9d7b952 100644 --- a/src/interactions/select_menus/README.md +++ b/src/interactions/select_menus/README.md @@ -22,7 +22,7 @@ Interactions start with the bot [sending the select menu](https://discordjs.guid ```ts // src/interactions/select_menus/string.ts import { StringSelectMenuInteraction } from "discord.js"; - import { Interaction } from "../../../Classes/index.js"; + import { Interaction } from "@/Classes"; export default new Interaction({ customIdPrefix: "string", @@ -36,15 +36,15 @@ Interactions start with the bot [sending the select menu](https://discordjs.guid ```ts // src/interactions/select_menus/index.ts - export { default as string } from "./string.js"; + export { default as string } from "./string"; ``` -3. In the root [`index.ts`](../../index.ts), make sure the following is present: +3. In the root [`index.ts`](@/index.ts), make sure the following is present: ```ts // src/index.ts - import { Client } from "./Classes/index.js"; - import * as selectMenus from "./interactions/select_menus/index.js"; + import { Client } from "./Classes"; + import * as selectMenus from "./interactions/select_menus"; export const client = new Client({ receiveMessageComponents: true, // enables the usage of message components diff --git a/src/interactions/select_menus/index.ts b/src/interactions/select_menus/index.ts index 6da66eea..ba2d2420 100644 --- a/src/interactions/select_menus/index.ts +++ b/src/interactions/select_menus/index.ts @@ -1 +1 @@ -export { usermove } from "./move.js"; +export { usermove } from "./move"; diff --git a/src/interactions/select_menus/move.ts b/src/interactions/select_menus/move.ts index 61ce0fbc..e2df9044 100644 --- a/src/interactions/select_menus/move.ts +++ b/src/interactions/select_menus/move.ts @@ -1,8 +1,8 @@ +import { Interaction } from "@/Classes"; +import { ns } from "@/commands/chat/move"; +import { localize } from "@/i18n"; import { UserSelectMenuInteraction, VoiceChannel } from "discord.js"; -import { Interaction } from "../../Classes/index.js"; -import { ns } from "../../commands/chat/move.js"; -import { localize } from "../../i18n.js"; -import { client } from "../../index.js"; +import { client } from "../.."; /** * `usermove` is a modal interaction that allows one user to move another from one voice diff --git a/src/models/ScheduledEvent.ts b/src/models/ScheduledEvent.ts deleted file mode 100644 index 0d6274c5..00000000 --- a/src/models/ScheduledEvent.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { GuildScheduledEventStatus, Snowflake } from "discord.js"; -import mongoose, { Document, Model, Schema } from "mongoose"; - -export interface IScheduledEvent extends Document { - recurrence: boolean; - eventUrl: string; - thumbnailUrl: string; - guildId: Snowflake; - eventId: Snowflake; - channelId?: Snowflake; - createdAt: Date; - description: string; - creatorId: Snowflake; - scheduledEnd?: Date; - scheduledStart?: Date; - name: string; - status: GuildScheduledEventStatus; - startedAt: Date; - endedAt: Date; - attendees: [ - { - id: Snowflake; - join: boolean; - timestamp: Date; - }, - ]; - userCount?: number; - logMessageId: Snowflake; -} - -const scheduledEventSchema = new Schema({ - recurrence: { type: Boolean }, - eventUrl: { type: String, required: true, immutable: true }, - thumbnailUrl: { type: String, required: true }, - guildId: { type: String, required: true, immutable: true }, - eventId: { type: String, required: true, immutable: true }, - channelId: { type: String }, - createdAt: { type: Date, required: true, immutable: true }, - description: { type: String }, - creatorId: { type: String, required: true, immutable: true }, - scheduledEnd: { type: Date }, - scheduledStart: { type: Date }, - name: { type: String, required: true }, - status: { type: Number, required: true }, - startedAt: { type: Date }, - endedAt: { type: Date }, - attendees: [ - { - id: String, - join: Boolean, - timestamp: Date, - }, - ], - userCount: { type: Number }, - logMessageId: { type: String }, -}); - -const modelName = "ScheduledEvent"; - -export const ScheduledEvent: Model = - (mongoose.models as Record>).ScheduledEvent || - mongoose.model(modelName, scheduledEventSchema); - -export default ScheduledEvent; diff --git a/src/models/Setting.ts b/src/models/Setting.ts deleted file mode 100644 index c5cf831a..00000000 --- a/src/models/Setting.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Snowflake } from "discord.js"; -import { HydratedDocument, model, Schema } from "mongoose"; - -export interface ISettings { - guildId: Snowflake; - guildName: string; - warn: { - logChannelId?: Snowflake; - appealChannelId?: Snowflake; - }; - report: { - logChannelId?: Snowflake; - }; - welcome: { - channelId?: Snowflake; - roleId?: Snowflake; - }; - logging: { - timeoutChannelId?: Snowflake; - leaveChannelId?: Snowflake; - channelUpdatesChannelId?: Snowflake; - voiceUpdatesChannelId?: Snowflake; - nicknameUpdatesChannelId?: Snowflake; - eventLogChannelId?: Snowflake; - }; -} - -export type SettingRecord = HydratedDocument; - -const settings = new Schema({ - guildId: { - type: String, - required: true, - immutable: true, - }, - guildName: { - type: String, - required: true, - immutable: false, - }, - warn: { - logChannelId: { - type: String, - }, - appealChannelId: { - type: String, - }, - }, - report: { - logChannelId: { - type: String, - }, - }, - welcome: { - channelId: String, - roleId: String, - }, - logging: { - timeoutChannelId: { - type: String, - }, - leaveChannelId: { - type: String, - }, - channelUpdatesChannelId: { - type: String, - }, - voiceUpdatesChannelId: { - type: String, - }, - nicknameUpdatesChannelId: { - type: String, - }, - eventLogChannelId: { - type: String, - }, - }, -}); - -export const GuildSetting = model("setting", settings, "settings"); diff --git a/src/models/State.ts b/src/models/State.ts deleted file mode 100644 index 656dcec1..00000000 --- a/src/models/State.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Snowflake } from "discord.js"; -import { HydratedDocument, model, Schema } from "mongoose"; -import { StateAbbreviation } from "../util/states/types.js"; - -export interface IState { - guildId: Snowflake; - name: string; - abbreviation: StateAbbreviation; - channelId?: Snowflake; - roleId?: Snowflake; - team: { - roleId?: Snowflake; - channelId?: Snowflake; - }; -} - -export type StateRecord = HydratedDocument; - -const state = new Schema({ - guildId: { - type: String, - required: true, - immutable: true, - }, - name: { - type: String, - required: true, - immutable: true, - }, - abbreviation: { - type: String, - required: true, - immutable: true, - }, - channelId: String, - roleId: String, - team: { - roleId: String, - channelId: String, - }, -}); - -export const States = model("state", state, "states"); diff --git a/src/models/Warn.ts b/src/models/Warn.ts deleted file mode 100644 index 77f32801..00000000 --- a/src/models/Warn.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { GuildMember, Snowflake } from "discord.js"; -import { HydratedDocument, Model, Schema, model } from "mongoose"; -import { defaultNumberOfDaysBeforeExpiration } from "../features/moderation/types.js"; -import { IUser, user } from "./index.js"; - -export interface IWarn { - guildId: Snowflake; - guildName: string; - target: IUser; - moderator: IUser; - updater?: IUser; - reason: string; - expireAt: Date; - createdAt: Date; - updatedAt: Date; -} - -export type WarningRecord = HydratedDocument; - -interface WarnModel extends Model { - createWarning( - target: GuildMember, - officer: GuildMember, - reason?: string, - days?: number, - ): Promise; -} - -const noReason = "No Reason Given"; -const warn = new Schema( - { - guildId: { - type: String, - required: true, - immutable: true, - }, - target: user(true, true), - moderator: user(true, true), - updater: user(false, false), - reason: { - type: String, - required: true, - default: noReason, - }, - expireAt: { - type: Date, - required: true, - default: setDate(), - }, - updatedAt: { type: Date, default: new Date() }, - }, - { - timestamps: { - createdAt: true, - updatedAt: true, - }, - statics: { - /** - * Create new warning - * @param target - guild member targeted for warn - * @param officer - moderator issuing the warn - * @param reason - of the warn - * @param days - time the warn will last - * @returns the record of the warn - */ - createWarning( - target: GuildMember, - officer: GuildMember, - reason: string, - days?: number, - ) { - return this.create({ - guildId: target.guild.id, - guildName: target.guild.name, - target: { - discordId: target.id, - username: target.user.username, - }, - moderator: { - discordId: officer.id, - username: officer.user.username, - }, - reason: reason, - expireAt: setDate(days), - }); - }, - }, - }, -); - -export const Warn = model("warn", warn, "warnings"); - -/** - * - * @param days - number of days to set the date - * @returns New Date - */ -export function setDate(days: number = defaultNumberOfDaysBeforeExpiration) { - const d = new Date(); - d.setDate(d.getDate() + days); - return d; -} diff --git a/src/models/WarnSearch.ts b/src/models/WarnSearch.ts deleted file mode 100644 index b1a08594..00000000 --- a/src/models/WarnSearch.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Snowflake } from "discord.js"; -import { HydratedDocument, model, Schema } from "mongoose"; -import { IUser, user } from "./index.js"; - -export interface IWarnSearch { - guildId: Snowflake; - targetDiscordId?: Snowflake; - moderatorDiscordId?: Snowflake; - expireAfter: Date; - searcher: IUser; - pageStart: number; - createdAt: Date; - isModerator: boolean; -} -export type WarnSearch = HydratedDocument; - -const search = new Schema( - { - guildId: { - type: String, - required: true, - immutable: true, - }, - targetDiscordId: String, - moderatorDiscordId: String, - expireAfter: { - type: Date, - required: false, - }, - searcher: user(true, true), - pageStart: { - type: Number, - default: 0, - }, - createdAt: { - type: Date, - default: Date.now(), - expires: 86400, - }, - isModerator: { - type: Boolean, - default: false, - }, - }, - { - timestamps: true, - }, -); - -export const WarningSearch = model( - "search", - search, - "warningSearch", -); diff --git a/src/models/index.ts b/src/models/index.ts deleted file mode 100644 index 1443e3ae..00000000 --- a/src/models/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Snowflake } from "discord.js"; - -export interface IUser { - discordId: Snowflake; - username: string; -} - -/** - * @param required - {@link SchemaTypeOptions.required} - * @param immutable - {@link SchemaTypeOptions.immutable} - * @returns an {@link IUser} for use with MongoDB operations - */ -export function user(required: boolean = false, immutable: boolean = false) { - return { - discordId: { - type: String, - required, - immutable, - }, - username: { - type: String, - required, - immutable, - }, - }; -} diff --git a/src/util/api/pvapi.ts b/src/util/api/pvapi.ts new file mode 100644 index 00000000..f5700d56 --- /dev/null +++ b/src/util/api/pvapi.ts @@ -0,0 +1,12 @@ +import { ApiConnService } from "@/Classes/API/ApiConnService"; +import { WarnSearchManager } from "@/Classes/API/ApiConnService/WarnSearchmanager"; + +const host = process.env.API_HOST_ADDR!; + +const apiConnService = new ApiConnService({ host }); + +apiConnService.auth(process.env.DISCORD_TOKEN!); + +const warnSearchManger = new WarnSearchManager(apiConnService); + +export { apiConnService, warnSearchManger }; diff --git a/src/util/cache/eventLogMessageCache.ts b/src/util/cache/eventLogMessageCache.ts new file mode 100644 index 00000000..cd22581b --- /dev/null +++ b/src/util/cache/eventLogMessageCache.ts @@ -0,0 +1,3 @@ +import { EventLogMessageCache } from "@/Classes/Client/Caches/EventLogMessageCache"; + +export const eventLogMessageCache = new EventLogMessageCache(); diff --git a/src/util/discord/DiscordAPIErrorCodes.ts b/src/util/discord/DiscordAPIErrorCodes.ts deleted file mode 100644 index 751fd0e1..00000000 --- a/src/util/discord/DiscordAPIErrorCodes.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Along with the HTTP error code, our API can also return more detailed error codes through a code key in the JSON error response. The response will also contain a message key containing a more friendly error string. Some of these errors may include additional details in the form of Error Messages provided by an errors object. - * @see https://discord.com/developers/docs/topics/opcodes-and-status-codes#json - */ -export enum DiscordAPIErrorCodes { - UnknownChannel = 10003, - UnknownMember = 10007, -} diff --git a/src/util/discord/index.ts b/src/util/discord/index.ts deleted file mode 100644 index d3f07fd9..00000000 --- a/src/util/discord/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DiscordAPIErrorCodes } from "./DiscordAPIErrorCodes.js"; diff --git a/src/util/events/markAttendance.ts b/src/util/events/markAttendance.ts new file mode 100644 index 00000000..c26645d1 --- /dev/null +++ b/src/util/events/markAttendance.ts @@ -0,0 +1,49 @@ +import { Routes } from "@/Classes/API/ApiConnService/routes"; +import { DiscordEvent, zDiscordEvent } from "@/contracts/data"; +import { CreateDiscordEventAttendeeRequest } from "@/contracts/requests/CreateDiscordEventAttendeeRequest"; +import { GuildMember, GuildScheduledEvent } from "discord.js"; +import { apiConnService } from "../api/pvapi"; + +export async function markAttendance( + event: GuildScheduledEvent, + member: GuildMember, + isJoin: boolean, + preventRedundant: boolean = false, +) { + try { + const data: DiscordEvent = await apiConnService.get( + Routes.latestDiscordEvent(event.id), + zDiscordEvent, + ); + + if (data.status !== 2) + throw Error(`event with id: ${data.id} is not active`); + + if (!data.startedAtUtc) + throw Error( + `Event ${event.id} has no start time but claims to be active`, + ); + + if (Date.now() - data.startedAtUtc.getTime() < 5000) + preventRedundant = true; + + const myAttendee: CreateDiscordEventAttendeeRequest = { + userDiscordId: member.id, + dateAttendedUtc: new Date(), + isJoin, + }; + + const query = new URLSearchParams(); + query.set("preventRedundant", preventRedundant.toString()); + + await apiConnService.post(Routes.discordEventAttendance(data.id), { + headers: { + "Content-Type": "application/json", + }, + query, + body: JSON.stringify(myAttendee), + }); + } catch (e) { + console.error(e); + } +} diff --git a/src/util/index.ts b/src/util/index.ts index 1ed881cf..e088a07e 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -3,14 +3,16 @@ import { APIRole, DiscordAPIError, Guild, + GuildBasedChannel, GuildChannelResolvable, GuildMember, GuildMemberResolvable, + RESTJSONErrorCodes, Role, + Snowflake, + User, } from "discord.js"; -import { Types } from "mongoose"; -import { client } from "../index.js"; -import { DiscordAPIErrorCodes } from "./discord/DiscordAPIErrorCodes.js"; +import { client } from ".."; /** * Check is full GuildMember object is present @@ -36,9 +38,7 @@ export function isRole(data: Role | APIRole | null): data is Role { * @param args - strings * @returns string with arguments separated by client.splitCustomIdOn */ -export function AddSplitCustomId( - ...args: (string | number | boolean | Types.ObjectId)[] -) { +export function AddSplitCustomId(...args: (string | number | boolean)[]) { if (!client.splitCustomIdOn) { throw Error("client.splitCustomIdOn not set in index"); } @@ -64,7 +64,7 @@ export async function getMember(guild: Guild, member: GuildMemberResolvable) { } catch (error) { if ( error instanceof DiscordAPIError && - error.code === DiscordAPIErrorCodes.UnknownMember + error.code === RESTJSONErrorCodes.UnknownMember ) { return undefined; } @@ -81,21 +81,47 @@ export async function getGuildChannel( guild: Guild, channel: GuildChannelResolvable, ) { - let resolvedChannel = guild.channels.resolve(channel) ?? null; + let resolvedChannel: GuildBasedChannel | null | undefined = + guild.channels.resolve(channel); + resolvedChannel ??= undefined; if (resolvedChannel) return resolvedChannel ?? undefined; try { if (typeof channel === "string") { + console.log("channel", channel); resolvedChannel = await guild.channels.fetch(channel); } else { + console.log("channel", channel); resolvedChannel = await channel.fetch(); } + + console.log(resolvedChannel); + return resolvedChannel; } catch (error) { if ( error instanceof DiscordAPIError && - error.code === DiscordAPIErrorCodes.UnknownChannel + error.code === RESTJSONErrorCodes.UnknownChannel ) { - return undefined; + console.error(error); } throw error; } } + +export async function fetchMemberOrUser(id: Snowflake, guild: Guild) { + return guild.members.fetch(id).catch(async (e) => { + if ( + !(e instanceof DiscordAPIError) || + e.code !== RESTJSONErrorCodes.UnknownMember + ) + throw e; + return client.users.fetch(id); + }); +} + +export function getNameToDisplay(user: GuildMember | User): string { + return user instanceof GuildMember + ? (user.nickname ?? user.displayName) + : user instanceof User + ? user.displayName + : "[Deleted User]"; +} diff --git a/src/util/libmongo.ts b/src/util/libmongo.ts deleted file mode 100644 index 6486e687..00000000 --- a/src/util/libmongo.ts +++ /dev/null @@ -1,48 +0,0 @@ -import mongoose from "mongoose"; - -interface MongooseCache { - conn: typeof mongoose | null; - promise: Promise | null; -} - -// dirty hack at module scope -declare global { - // eslint-disable-next-line no-var - var mongoose: MongooseCache; -} - -const MONGODB_URI = process.env.MONGODB_URI; - -if (!MONGODB_URI) { - throw new Error("Please define the MONGODB_URI environment variable"); -} - -let cached = global.mongoose; - -if (!cached) { - cached = global.mongoose = { conn: null, promise: null }; -} - -/** - * Connects to the MongoDB database. - * @returns The MongoDB connection. - */ -async function dbConnect() { - if (cached.conn) { - return cached.conn; - } - - if (!cached.promise) { - const opts = { - bufferCommands: false, - }; - - cached.promise = mongoose.connect(MONGODB_URI!, opts).then((mongoose) => { - return mongoose; - }); - } - cached.conn = await cached.promise; - return cached.conn; -} - -export default dbConnect; diff --git a/src/util/scheduledEventWrapper.ts b/src/util/scheduledEventWrapper.ts index b63443a1..0e0a5e10 100644 --- a/src/util/scheduledEventWrapper.ts +++ b/src/util/scheduledEventWrapper.ts @@ -1,10 +1,10 @@ -import createCsvWriter from "csv-writer"; +import { DiscordEvent } from "@/contracts/data"; +import { createObjectCsvWriter } from "csv-writer"; import { GuildMember, GuildScheduledEventStatus, time } from "discord.js"; -import { client } from "../index.js"; -import { IScheduledEvent } from "../models/ScheduledEvent.js"; +import { client } from ".."; export class ScheduledEventWrapper { - event: IScheduledEvent; + event: DiscordEvent; statusColor = () => { let color: number; @@ -29,14 +29,16 @@ export class ScheduledEventWrapper { }; duration = () => { - if (!this.event.startedAt) { + if (!this.event.startedAtUtc) { return "N/A"; } else { - if (!this.event.endedAt) { + if (!this.event.endedAtUtc) { return "N/A"; } else { + console.log("calculating duration"); return Math.round( - (this.event.endedAt.getTime() - this.event.startedAt.getTime()) / + (this.event.endedAtUtc.getTime() - + this.event.startedAtUtc.getTime()) / 60000, ); } @@ -44,11 +46,15 @@ export class ScheduledEventWrapper { }; guild = async () => { - return await client.guilds.fetch(this.event.guildId); + if (!process.env.PV_GUILD_ID) + throw Error("fill out 'PV_GUILD_ID' in env file"); + return await client.guilds.fetch(process.env.PV_GUILD_ID); }; guildEvent = async () => { - return await (await this.guild()).scheduledEvents.fetch(this.event.id); + return await ( + await this.guild() + ).scheduledEvents.fetch(this.event.discordId); }; channel = async () => { @@ -58,7 +64,7 @@ export class ScheduledEventWrapper { }; createdAt = () => { - return time(this.event.createdAt); + return time(this.event.createdAtUtc); }; description = () => { @@ -66,45 +72,53 @@ export class ScheduledEventWrapper { }; creator = async () => { - return (await this.guild()).members.fetch(this.event.creatorId); + return (await this.guild()).members.fetch(this.event.creatorDiscordId); }; scheduledEnd = () => { - return this.event.scheduledEnd ? time(this.event.scheduledEnd) : "None"; + return this.event.scheduledEndUtc + ? time(this.event.scheduledEndUtc) + : "None"; }; scheduledStart = () => { - return this.event.scheduledStart ? time(this.event.scheduledStart) : "None"; + return this.event.scheduledStartUtc + ? time(this.event.scheduledStartUtc) + : "None"; }; scheduledStartDate = () => { - return this.event.scheduledStart - ? time(this.event.scheduledStart, "D") + return this.event.scheduledStartUtc + ? time(this.event.scheduledStartUtc, "D") : "None"; }; scheduledStartTime = () => { - return this.event.scheduledStart - ? time(this.event.scheduledStart, "t") + return this.event.scheduledStartUtc + ? time(this.event.scheduledStartUtc, "t") : "None"; }; scheduledEndTime = () => { - return this.event.scheduledEnd - ? time(this.event.scheduledEnd, "t") + return this.event.scheduledEndUtc + ? time(this.event.scheduledEndUtc, "t") : "None"; }; startDate = () => { - return this.event.startedAt ? time(this.event.startedAt, "D") : "None"; + return this.event.startedAtUtc + ? time(this.event.startedAtUtc, "D") + : "None"; }; startTime = () => { - return this.event.startedAt ? time(this.event.startedAt, "t") : "None"; + return this.event.startedAtUtc + ? time(this.event.startedAtUtc, "t") + : "None"; }; endTime = () => { - return this.event.endedAt ? time(this.event.endedAt, "t") : "None"; + return this.event.endedAtUtc ? time(this.event.endedAtUtc, "t") : "None"; }; name = () => { @@ -112,22 +126,25 @@ export class ScheduledEventWrapper { }; status = () => { - return GuildScheduledEventStatus[this.event.status]; + return GuildScheduledEventStatus[this.event.status ?? 1]; }; startedAt = () => { - return this.event.startedAt ? time(this.event.startedAt) : "N/A"; + return this.event.startedAtUtc ? time(this.event.startedAtUtc) : "N/A"; }; endedAt = () => { - return this.event.endedAt ? time(this.event.endedAt) : "N/A"; + return this.event.endedAtUtc ? time(this.event.endedAtUtc) : "N/A"; }; attendees = () => { + if (!this.event.attendees) + throw Error("No attendees defined on event: " + this.event.id); const users: string[] = []; this.event.attendees.map((obj) => { + //gonna need some refactoring with joins or some bullshit users.push( - `<@${obj.id}> ${obj.join ? "joined" : "left"} at ${this.getFormattedTime(obj.timestamp)}`, + `<@${obj.userDiscordId}> ${obj.isJoin ? "joined" : "left"} at ${this.getFormattedTime(obj.dateAttendedUtc)}`, ); }); return users; @@ -138,21 +155,25 @@ export class ScheduledEventWrapper { }; recurrence = () => { - return this.event.recurrence ? "Recurring" : "One Time"; + return this.event.recurrent ? "Recurring" : "One Time"; }; thumbnail = () => { return this.event.thumbnailUrl; }; - eventLink = () => { - return this.event.eventUrl; + eventLink = async () => { + const guild = await this.guild(); + const res = await guild.scheduledEvents.fetch(this.event.discordId); + return res.url; }; attendeesNames = async () => { + if (!this.event.attendees) + throw Error("No attendees defined on event: " + this.event.id); const usrIds: string[] = []; this.event.attendees.map((obj) => { - if (!usrIds.includes(obj.id)) usrIds.push(obj.id); + if (!usrIds.includes(obj.userDiscordId)) usrIds.push(obj.userDiscordId); }); const nameMap = await this.getAttendeeNames(usrIds); const entries = await this.attendees(); @@ -160,9 +181,11 @@ export class ScheduledEventWrapper { }; uniqueAttendees = () => { + if (!this.event.attendees) + throw Error("No attendees defined on event: " + this.event.id); const usrIds: string[] = []; this.event.attendees.map((obj) => { - if (!usrIds.includes(obj.id)) usrIds.push(obj.id); + if (!usrIds.includes(obj.userDiscordId)) usrIds.push(obj.userDiscordId); }); return usrIds.length; }; @@ -178,31 +201,34 @@ export class ScheduledEventWrapper { return users; }; - constructor(ev: IScheduledEvent) { + constructor(ev: DiscordEvent) { this.event = ev; } public async writeCsvDump() { + if (!this.event.attendees) + throw Error("No attendees defined on event: " + this.event.id); console.log("writing csv dump"); const names = await this.getAttendeeNames( this.event.attendees.map((entry) => { - return entry.id; + return entry.userDiscordId; }), ); - const writer = createCsvWriter.createObjectCsvWriter({ + const writer = createObjectCsvWriter({ path: "./assets/temp/attendees.csv", - header: ["timestamp", "id", "displayName", "join"], + header: ["timestamp", "id", "discordId", "displayName", "join"], fieldDelimiter: ";", }); - const data = this.event.attendees.map((entry) => { - return { - timestamp: entry.timestamp, - id: entry.id, - displayName: names.get(entry.id) ?? "unknown", - join: entry.join, - }; - }); + const data = this.event.attendees.map((entry) => ({ + timestamp: entry.dateAttendedUtc.toISOString(), + id: entry.id, + discordId: entry.userDiscordId, + displayName: names.get(entry.userDiscordId) ?? "unknown", + join: entry.isJoin ? "join" : "leave", + })); + + console.log(data); await writer.writeRecords(data).catch((err) => console.error(err)); console.log("csv written"); @@ -223,7 +249,7 @@ export class ScheduledEventWrapper { private async getAttendeeNames(ids: string[]) { const buffer = []; - let names: Map = new Map(); + const names: Map = new Map(); for (let i = 0; i < Math.ceil(ids.length / 100); i++) { const slice = ids.slice( i * 100, @@ -249,7 +275,7 @@ export class ScheduledEventWrapper { private calculateAttendanceTime() { interface joinLeavePair { - id: string; + userDiscordId: string; join: Date; leave: Date | null; } @@ -258,42 +284,53 @@ export class ScheduledEventWrapper { const joinLeavePairs: joinLeavePair[] = []; const attendanceTotals: Map = new Map(); + if (!this.event.attendees) + throw Error("No attendees defined on event: " + this.event.id); + console.log(this.event.attendees); this.event.attendees.forEach((entry) => { - if (entry.join) { + if (entry.isJoin) { joinLeavePairs.push({ - id: entry.id, - join: entry.timestamp, + userDiscordId: entry.userDiscordId, + join: entry.dateAttendedUtc, leave: null, }); } else { const existingPair = joinLeavePairs.findLast( - (x) => x.id === entry.id, + (x) => x.userDiscordId === entry.userDiscordId, ); if (!existingPair) throw Error( "Leave entry unaccompanied by join entry in attendance tracking.", ); - existingPair.leave = entry.timestamp; + existingPair.leave = entry.dateAttendedUtc; } }); joinLeavePairs.forEach((pair) => { if (!pair.leave) { const lastIdPair = - joinLeavePairs.findLast((x) => x.id === pair.id)?.join === - pair.join; + joinLeavePairs.findLast( + (x) => x.userDiscordId === pair.userDiscordId, + )?.join === pair.join; if (!lastIdPair) throw Error( "Missing leave timestamp in attendance calculation pairs", ); - pair.leave = this.event.endedAt; + if (!this.event.endedAtUtc) + throw Error( + "Attempting to calculate attendance for unfinished event: " + + this.event.id, + ); + pair.leave = this.event.endedAtUtc; } + console.log("getting pair duration"); + console.log(typeof pair.join); const pairDuration = pair.leave.getTime() - pair.join.getTime(); attendanceTotals.set( - pair.id, - (attendanceTotals.get(pair.id) ?? 0) + pairDuration, + pair.userDiscordId, + (attendanceTotals.get(pair.userDiscordId) ?? 0) + pairDuration, ); }); @@ -305,9 +342,16 @@ export class ScheduledEventWrapper { private calculateAttendancePercentages() { try { + if (!this.event.endedAtUtc || !this.event.startedAtUtc) + throw Error( + "Attempting to calculate attendance percentages without defined start and end times on event: " + + this.event.id, + ); const totals = this.calculateAttendanceTime(); + console.log("duration start"); const eventDuration = - this.event.endedAt.getTime() - this.event.startedAt.getTime(); + this.event.endedAtUtc.getTime() - this.event.startedAtUtc.getTime(); + console.log("duration end"); const percentages: Map = new Map(); if (!totals) throw Error("Failed to calculate attendance totals"); diff --git a/src/util/states/State.ts b/src/util/states/State.ts index 96ea8a37..bc8ab40b 100644 --- a/src/util/states/State.ts +++ b/src/util/states/State.ts @@ -1,5 +1,5 @@ import { ForumChannel, Role, TextChannel } from "discord.js"; -import { StateAbbreviation } from "./types.js"; +import { StateAbbreviation } from "./types"; export class State { readonly name: string; diff --git a/src/util/states/discordStateRole.ts b/src/util/states/discordStateRole.ts new file mode 100644 index 00000000..ffda0b63 --- /dev/null +++ b/src/util/states/discordStateRole.ts @@ -0,0 +1,12 @@ +import z from "zod"; + +export const zDiscordStateRole = z.object({ + stateAbbreviation: z.string().nonempty(), + stateName: z.string().nonempty(), + memberRoleId: z.string().nonempty(), + memberChannelId: z.string().nonempty(), + teamRoleId: z.string().nonempty(), + teamChannelId: z.string().nonempty(), +}); + +export type IDiscordStateRole = z.infer; diff --git a/src/util/states/index.ts b/src/util/states/index.ts index e531cb90..8ef1b786 100644 --- a/src/util/states/index.ts +++ b/src/util/states/index.ts @@ -1,5 +1,5 @@ import { GuildMember, GuildTextBasedChannel, Role } from "discord.js"; -import { statesConfig } from "./types.js"; +import { statesConfig } from "./types"; /** * Get states from a member diff --git a/tsconfig.json b/tsconfig.json index dd851ece..0ac5b84e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,33 +1,29 @@ { - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "rootDir": "./src/", - "target": "ESNEXT", - "typeRoots": [ - "node_modules/@types" - ], - "outDir": "./dist/", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "experimentalDecorators": true, - "forceConsistentCasingInFileNames": true, - "importHelpers": true, - "isolatedModules": true, - "noFallthroughCasesInSwitch": true, - "removeComments": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "strictPropertyInitialization": true, + "compilerOptions": { + "rootDir": ".", + "baseUrl": "src", + "outDir": "./dist", + "paths": { + "@/*": ["./*"] }, - "include": [ - "src/**/*", - "eslint.config.js" - ], - "exclude": [ - "node_modules", - "dist" - ] + + "moduleResolution": "bundler", + "module": "esnext", + "target": "esnext", + + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "importHelpers": true, + "isolatedModules": true, + "noFallthroughCasesInSwitch": true, + "removeComments": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "strictPropertyInitialization": true + }, + "exclude": ["node_modules", "dist"] }