From 7437deab7137204662e3f6a73bfa14fae5692e50 Mon Sep 17 00:00:00 2001 From: Naomi Date: Sun, 12 May 2024 01:52:39 -0700 Subject: [PATCH] feat: migrate from github --- .eslintrc.json | 45 + .gitattributes | 8 + .gitignore | 3 + .knip.jsonc | 15 + .npmrc | 1 + .prettierrc.json | 1 + CODE_OF_CONDUCT.md | 3 + CONTRIBUTING.md | 3 + LICENSE.md | 5 + PRIVACY.md | 3 + README.md | 1 + SECURITY.md | 3 + TERMS.md | 3 + package.json | 47 + pnpm-lock.yaml | 4117 +++++++++++++++++ prisma/schema.prisma | 81 + sample.env | 11 + src/commands/ban.ts | 99 + src/commands/cases.ts | 90 + src/commands/config.ts | 136 + src/commands/help.ts | 88 + src/commands/history.ts | 198 + src/commands/kick.ts | 145 + src/commands/leaderboard.ts | 156 + src/commands/levelRoles.ts | 106 + src/commands/lockdown.ts | 98 + src/commands/massBan.ts | 49 + src/commands/mute.ts | 122 + src/commands/note.ts | 67 + src/commands/ping.ts | 50 + src/commands/profile.ts | 129 + src/commands/prune.ts | 71 + src/commands/rank.ts | 52 + src/commands/role.ts | 52 + src/commands/softBan.ts | 102 + src/commands/unban.ts | 87 + src/commands/unlock.ts | 96 + src/commands/unmute.ts | 106 + src/commands/warn.ts | 99 + src/config/DefaultConfig.ts | 10 + src/config/EmbedColours.ts | 12 + src/config/Github.ts | 17 + src/config/IntentOptions.ts | 10 + src/config/LevelScale.ts | 14 + src/config/LogChannelChoices.ts | 15 + src/config/ServerUploadLimits.ts | 8 + src/contexts/report.ts | 126 + src/database/connectDatabase.ts | 20 + src/events/_handleEvents.ts | 129 + src/events/client/onDisconnect.ts | 16 + src/events/client/onReady.ts | 13 + src/events/guild/onAuditLogEntry.ts | 78 + src/events/guild/onGuildCreate.ts | 18 + src/events/guild/onGuildDelete.ts | 16 + src/events/interaction/onInteraction.ts | 71 + src/events/member/onMemberAdd.ts | 38 + src/events/member/onMemberRemove.ts | 45 + src/events/member/onMemberUpdate.ts | 80 + src/events/message/onMessage.ts | 141 + src/events/message/onMessageDelete.ts | 73 + src/events/message/onMessageEdit.ts | 64 + src/events/thread/onThreadCreate.ts | 42 + src/events/thread/onThreadDelete.ts | 38 + src/events/thread/onThreadUpdate.ts | 54 + src/events/voice/onVoiceUpdate.ts | 82 + src/index.ts | 24 + src/interfaces/Action.ts | 30 + src/interfaces/ActionPayload.ts | 11 + src/interfaces/Command.ts | 19 + src/interfaces/CommandHandler.ts | 8 + src/interfaces/Context.ts | 13 + src/interfaces/ExtendedClient.ts | 18 + src/interfaces/GitHubPayloads.ts | 190 + src/interfaces/Interactions.ts | 15 + src/modules/buttons/handleCopyIdButton.ts | 28 + .../buttons/handleReportAcknowledgeButton.ts | 43 + src/modules/commands/calculateMuteDuration.ts | 24 + src/modules/commands/generateProfileImage.ts | 213 + src/modules/commands/profileValidation.ts | 32 + src/modules/data/getConfig.ts | 39 + src/modules/data/setConfig.ts | 40 + src/modules/events/checkSpamDomain.ts | 46 + .../events/getModActionFromAuditLog.ts | 35 + .../interactions/handleChatInputCommand.ts | 55 + .../interactions/handleContextMenuCommand.ts | 42 + src/modules/modals/handleMassBanModal.ts | 127 + .../modals/handleMessageReportModal.ts | 82 + .../subcommands/config/handleAppealLink.ts | 34 + .../subcommands/config/handleInviteLink.ts | 34 + src/modules/subcommands/config/handleList.ts | 141 + .../subcommands/config/handleLogging.ts | 40 + src/modules/subcommands/config/handleRole.ts | 47 + src/modules/validateEnv.ts | 27 + src/modules/validateGuildCommands.ts | 33 + src/server/github/generateCommentEmbed.ts | 5 + src/server/github/generateForkEmbed.ts | 5 + src/server/github/generateIssueEmbed.ts | 25 + src/server/github/generatePingEmbed.ts | 5 + src/server/github/generatePullEmbed.ts | 17 + src/server/github/generateStarEmbed.ts | 8 + src/server/serve.ts | 306 ++ src/utils/addCase.ts | 51 + src/utils/checkEntitledGuild.ts | 21 + src/utils/customSubstring.ts | 11 + src/utils/errorHandler.ts | 41 + src/utils/generateTimestamp.ts | 10 + src/utils/getArrayIndex.ts | 19 + src/utils/isModerator.ts | 20 + src/utils/loadCommands.ts | 36 + src/utils/loadContexts.ts | 36 + src/utils/logHandler.ts | 24 + src/utils/processModAction.ts | 120 + src/utils/registerCommands.ts | 44 + src/utils/sendDebugMessage.ts | 22 + src/utils/sendLogMessage.ts | 90 + src/utils/sendModDm.ts | 57 + src/utils/triggerModRequest.ts | 126 + tsconfig.json | 8 + 118 files changed, 10375 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .knip.jsonc create mode 100644 .npmrc create mode 100644 .prettierrc.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.md create mode 100644 PRIVACY.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 TERMS.md create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 prisma/schema.prisma create mode 100644 sample.env create mode 100644 src/commands/ban.ts create mode 100644 src/commands/cases.ts create mode 100644 src/commands/config.ts create mode 100644 src/commands/help.ts create mode 100644 src/commands/history.ts create mode 100644 src/commands/kick.ts create mode 100644 src/commands/leaderboard.ts create mode 100644 src/commands/levelRoles.ts create mode 100644 src/commands/lockdown.ts create mode 100644 src/commands/massBan.ts create mode 100644 src/commands/mute.ts create mode 100644 src/commands/note.ts create mode 100644 src/commands/ping.ts create mode 100644 src/commands/profile.ts create mode 100644 src/commands/prune.ts create mode 100644 src/commands/rank.ts create mode 100644 src/commands/role.ts create mode 100644 src/commands/softBan.ts create mode 100644 src/commands/unban.ts create mode 100644 src/commands/unlock.ts create mode 100644 src/commands/unmute.ts create mode 100644 src/commands/warn.ts create mode 100644 src/config/DefaultConfig.ts create mode 100644 src/config/EmbedColours.ts create mode 100644 src/config/Github.ts create mode 100644 src/config/IntentOptions.ts create mode 100644 src/config/LevelScale.ts create mode 100644 src/config/LogChannelChoices.ts create mode 100644 src/config/ServerUploadLimits.ts create mode 100644 src/contexts/report.ts create mode 100644 src/database/connectDatabase.ts create mode 100644 src/events/_handleEvents.ts create mode 100644 src/events/client/onDisconnect.ts create mode 100644 src/events/client/onReady.ts create mode 100644 src/events/guild/onAuditLogEntry.ts create mode 100644 src/events/guild/onGuildCreate.ts create mode 100644 src/events/guild/onGuildDelete.ts create mode 100644 src/events/interaction/onInteraction.ts create mode 100644 src/events/member/onMemberAdd.ts create mode 100644 src/events/member/onMemberRemove.ts create mode 100644 src/events/member/onMemberUpdate.ts create mode 100644 src/events/message/onMessage.ts create mode 100644 src/events/message/onMessageDelete.ts create mode 100644 src/events/message/onMessageEdit.ts create mode 100644 src/events/thread/onThreadCreate.ts create mode 100644 src/events/thread/onThreadDelete.ts create mode 100644 src/events/thread/onThreadUpdate.ts create mode 100644 src/events/voice/onVoiceUpdate.ts create mode 100644 src/index.ts create mode 100644 src/interfaces/Action.ts create mode 100644 src/interfaces/ActionPayload.ts create mode 100644 src/interfaces/Command.ts create mode 100644 src/interfaces/CommandHandler.ts create mode 100644 src/interfaces/Context.ts create mode 100644 src/interfaces/ExtendedClient.ts create mode 100644 src/interfaces/GitHubPayloads.ts create mode 100644 src/interfaces/Interactions.ts create mode 100644 src/modules/buttons/handleCopyIdButton.ts create mode 100644 src/modules/buttons/handleReportAcknowledgeButton.ts create mode 100644 src/modules/commands/calculateMuteDuration.ts create mode 100644 src/modules/commands/generateProfileImage.ts create mode 100644 src/modules/commands/profileValidation.ts create mode 100644 src/modules/data/getConfig.ts create mode 100644 src/modules/data/setConfig.ts create mode 100644 src/modules/events/checkSpamDomain.ts create mode 100644 src/modules/events/getModActionFromAuditLog.ts create mode 100644 src/modules/interactions/handleChatInputCommand.ts create mode 100644 src/modules/interactions/handleContextMenuCommand.ts create mode 100644 src/modules/modals/handleMassBanModal.ts create mode 100644 src/modules/modals/handleMessageReportModal.ts create mode 100644 src/modules/subcommands/config/handleAppealLink.ts create mode 100644 src/modules/subcommands/config/handleInviteLink.ts create mode 100644 src/modules/subcommands/config/handleList.ts create mode 100644 src/modules/subcommands/config/handleLogging.ts create mode 100644 src/modules/subcommands/config/handleRole.ts create mode 100644 src/modules/validateEnv.ts create mode 100644 src/modules/validateGuildCommands.ts create mode 100644 src/server/github/generateCommentEmbed.ts create mode 100644 src/server/github/generateForkEmbed.ts create mode 100644 src/server/github/generateIssueEmbed.ts create mode 100644 src/server/github/generatePingEmbed.ts create mode 100644 src/server/github/generatePullEmbed.ts create mode 100644 src/server/github/generateStarEmbed.ts create mode 100644 src/server/serve.ts create mode 100644 src/utils/addCase.ts create mode 100644 src/utils/checkEntitledGuild.ts create mode 100644 src/utils/customSubstring.ts create mode 100644 src/utils/errorHandler.ts create mode 100644 src/utils/generateTimestamp.ts create mode 100644 src/utils/getArrayIndex.ts create mode 100644 src/utils/isModerator.ts create mode 100644 src/utils/loadCommands.ts create mode 100644 src/utils/loadContexts.ts create mode 100644 src/utils/logHandler.ts create mode 100644 src/utils/processModAction.ts create mode 100644 src/utils/registerCommands.ts create mode 100644 src/utils/sendDebugMessage.ts create mode 100644 src/utils/sendLogMessage.ts create mode 100644 src/utils/sendModDm.ts create mode 100644 src/utils/triggerModRequest.ts create mode 100644 tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..49192e8 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,45 @@ +{ + "extends": "@nhcarrigan", + "rules": { + "camelcase": [ + "error", + { + "allow": [ + "serverId_userId", + "userId_serverId", + "pull_request", + "issue_number", + "issue_comment", + "serverId_level_roleId", + "serverId_roleId" + ] + } + ] + }, + "overrides": [ + { + "files": ["src/server/github/*.ts"], + "rules": { + "camelcase": [ + "error", + { + "allow": ["icon_url"] + } + ], + "jsdoc/require-jsdoc": "off" + } + }, + { + "files": ["src/modules/subcommands/config/*.ts"], + "rules": { + "jsdoc/require-param": "off" + } + }, + { + "files": ["src/modules/data/*.ts"], + "rules": { + "require-atomic-updates": "off" + } + } + ] +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2bffe04 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Auto detect text files and perform LF normalization +* text eol=LF +*.ts text +*.spec.ts text + +# Ignore binary files >:( +*.png binary +*.jpg binary \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1168ce1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/prod/ +.env diff --git a/.knip.jsonc b/.knip.jsonc new file mode 100644 index 0000000..65847c9 --- /dev/null +++ b/.knip.jsonc @@ -0,0 +1,15 @@ +{ + "$schema": "https://unpkg.com/knip@2/schema.json", + "entry": [ + "src/index.ts", + /** + * Because the commands are dynamically generated, knip can't follow them. + * Treat them as entry files to allow for dependency and import/export validation. + */ + "src/commands/*.ts", + "src/contexts/*.ts" + ], + "project": ["src/**/*.ts"], + "ignore": [], + "ignoreDependencies": [] +} diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b7425b9 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +enable-pre-post-scripts=true \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..74cfb8c --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +"@nhcarrigan/prettier-config" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a3bbfe2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +Our Code of Conduct can be found here: https://docs.nhcarrigan.com/#/coc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f67101e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing + +Our contributing guidelines can be found here: https://docs.nhcarrigan.com/#/contributing diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5424732 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,5 @@ +# License + +This software is licensed under our [global software license](https://docs.nhcarrigan.com/#/license). + +Copyright held by Naomi Carrigan. diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..96791d1 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,3 @@ +# Privacy Policy + +Our privacy policy can be found here: https://docs.nhcarrigan.com/#/privacy diff --git a/README.md b/README.md new file mode 100644 index 0000000..4508766 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Mod bot is private for reasons \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..bb3fc2d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +Our security policy can be found here: https://docs.nhcarrigan.com/#/security diff --git a/TERMS.md b/TERMS.md new file mode 100644 index 0000000..f556156 --- /dev/null +++ b/TERMS.md @@ -0,0 +1,3 @@ +# Terms of Service + +Our Terms of Service can be found here: https://docs.nhcarrigan.com/#/terms diff --git a/package.json b/package.json new file mode 100644 index 0000000..74ea5ea --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "naomis-moderation-bot", + "version": "1.2.0", + "description": "A public paid moderation bot for Discord.", + "main": "prod/index.js", + "scripts": { + "audit": "knip", + "prebuild": "rm -rf prod && prisma generate", + "build": "tsc", + "lint": "eslint src --max-warnings 0 && prettier src --check", + "start": "node -r dotenv/config prod/index.js", + "test": "echo 'no tests yet'" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nhcarrigan/mod-bot.git" + }, + "author": "Naomi Carrigan", + "bugs": { + "url": "https://github.com/nhcarrigan/mod-bot/issues" + }, + "engines": { + "node": "20", + "pnpm": "8" + }, + "homepage": "https://github.com/nhcarrigan/mod-bot#readme", + "devDependencies": { + "@nhcarrigan/eslint-config": "3.2.0", + "@nhcarrigan/prettier-config": "3.2.0", + "@nhcarrigan/typescript-config": "3.0.0", + "@types/express": "4.17.21", + "eslint": "8.57.0", + "knip": "5.15.0", + "prettier": "3.2.5", + "prisma": "5.13.0", + "typescript": "5.4.5" + }, + "dependencies": { + "@octokit/rest": "20.1.1", + "@prisma/client": "5.13.0", + "discord.js": "14.15.2", + "dotenv": "16.4.5", + "express": "4.19.2", + "node-html-to-image": "4.0.0", + "winston": "3.13.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..a40355b --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4117 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@octokit/rest': + specifier: 20.1.1 + version: 20.1.1 + '@prisma/client': + specifier: 5.13.0 + version: 5.13.0(prisma@5.13.0) + discord.js: + specifier: 14.15.2 + version: 14.15.2 + dotenv: + specifier: 16.4.5 + version: 16.4.5 + express: + specifier: 4.19.2 + version: 4.19.2 + node-html-to-image: + specifier: 4.0.0 + version: 4.0.0 + winston: + specifier: 3.13.0 + version: 3.13.0 + +devDependencies: + '@nhcarrigan/eslint-config': + specifier: 3.2.0 + version: 3.2.0(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5) + '@nhcarrigan/prettier-config': + specifier: 3.2.0 + version: 3.2.0(prettier@3.2.5) + '@nhcarrigan/typescript-config': + specifier: 3.0.0 + version: 3.0.0(typescript@5.4.5) + '@types/express': + specifier: 4.17.21 + version: 4.17.21 + eslint: + specifier: 8.57.0 + version: 8.57.0 + knip: + specifier: 5.15.0 + version: 5.15.0(@types/node@20.3.1)(typescript@5.4.5) + prettier: + specifier: 3.2.5 + version: 3.2.5 + prisma: + specifier: 5.13.0 + version: 5.13.0 + typescript: + specifier: 5.4.5 + version: 5.4.5 + +packages: + + /@babel/code-frame@7.24.2: + resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.24.5 + picocolors: 1.0.0 + dev: false + + /@babel/helper-validator-identifier@7.24.5: + resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/highlight@7.24.5: + resolution: {integrity: sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.24.5 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.0 + dev: false + + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + dev: false + + /@colors/colors@1.6.0: + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + dev: false + + /@dabh/diagnostics@2.0.3: + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + dev: false + + /@discordjs/builders@1.8.1: + resolution: {integrity: sha512-GkF+HM01FHy+NSoTaUPR8z44otfQgJ1AIsRxclYGUZDyUbdZEFyD/5QVv2Y1Flx6M+B0bQLzg2M9CJv5lGTqpA==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/formatters': 0.4.0 + '@discordjs/util': 1.1.0 + '@sapphire/shapeshift': 3.9.7 + discord-api-types: 0.37.83 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.4 + tslib: 2.6.2 + dev: false + + /@discordjs/collection@1.5.3: + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + dev: false + + /@discordjs/collection@2.1.0: + resolution: {integrity: sha512-mLcTACtXUuVgutoznkh6hS3UFqYirDYAg5Dc1m8xn6OvPjetnUlf/xjtqnnc47OwWdaoCQnHmHh9KofhD6uRqw==} + engines: {node: '>=18'} + dev: false + + /@discordjs/formatters@0.4.0: + resolution: {integrity: sha512-fJ06TLC1NiruF35470q3Nr1bi95BdvKFAF+T5bNfZJ4bNdqZ3VZ+Ttg6SThqTxm6qumSG3choxLBHMC69WXNXQ==} + engines: {node: '>=16.11.0'} + dependencies: + discord-api-types: 0.37.83 + dev: false + + /@discordjs/rest@2.3.0: + resolution: {integrity: sha512-C1kAJK8aSYRv3ZwMG8cvrrW4GN0g5eMdP8AuN8ODH5DyOCbHgJspze1my3xHOAgwLJdKUbWNVyAeJ9cEdduqIg==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/collection': 2.1.0 + '@discordjs/util': 1.1.0 + '@sapphire/async-queue': 1.5.2 + '@sapphire/snowflake': 3.5.3 + '@vladfrangu/async_event_emitter': 2.2.4 + discord-api-types: 0.37.83 + magic-bytes.js: 1.10.0 + tslib: 2.6.2 + undici: 6.13.0 + dev: false + + /@discordjs/util@1.1.0: + resolution: {integrity: sha512-IndcI5hzlNZ7GS96RV3Xw1R2kaDuXEp7tRIy/KlhidpN/BQ1qh1NZt3377dMLTa44xDUNKT7hnXkA/oUAzD/lg==} + engines: {node: '>=16.11.0'} + dev: false + + /@discordjs/ws@1.1.0: + resolution: {integrity: sha512-O97DIeSvfNTn5wz5vaER6ciyUsr7nOqSEtsLoMhhIgeFkhnxLRqSr00/Fpq2/ppLgjDGLbQCDzIK7ilGoB/M7A==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/collection': 2.1.0 + '@discordjs/rest': 2.3.0 + '@discordjs/util': 1.1.0 + '@sapphire/async-queue': 1.5.2 + '@types/ws': 8.5.10 + '@vladfrangu/async_event_emitter': 2.2.4 + discord-api-types: 0.37.83 + tslib: 2.6.2 + ws: 8.17.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /@ericcornelissen/bash-parser@0.5.2: + resolution: {integrity: sha512-4pIMTa1nEFfMXitv7oaNEWOdM+zpOZavesa5GaiWTgda6Zk32CFGxjUp/iIaN0PwgUW1yTq/fztSjbpE8SLGZQ==} + engines: {node: '>=4'} + dependencies: + array-last: 1.3.0 + babylon: 6.18.0 + compose-function: 3.0.3 + deep-freeze: 0.0.1 + filter-iterator: 0.0.1 + filter-obj: 1.1.0 + has-own-property: 0.1.0 + identity-function: 1.0.0 + is-iterable: 1.1.1 + iterable-lookahead: 1.0.0 + lodash.curry: 4.1.1 + magic-string: 0.16.0 + map-obj: 2.0.0 + object-pairs: 0.1.0 + object-values: 1.0.0 + reverse-arguments: 1.0.0 + shell-quote-word: 1.0.1 + to-pascal-case: 1.0.0 + unescape-js: 1.1.4 + dev: true + + /@es-joy/jsdoccomment@0.37.1: + resolution: {integrity: sha512-5vxWJ1gEkEF0yRd0O+uK6dHJf7adrxwQSX8PuRiPfFSAbNLnY0ZJfXaZucoz14Jj2N11xn2DnlEPwWRpYpvRjg==} + engines: {node: ^14 || ^16 || ^17 || ^18 || ^19 || ^20} + dependencies: + comment-parser: 1.3.1 + esquery: 1.5.0 + jsdoc-type-pratt-parser: 4.0.0 + dev: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.1 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint-community/regexpp@4.5.1: + resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.57.0: + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@humanwhocodes/config-array@0.11.14: + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.3: + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + dev: true + + /@nhcarrigan/eslint-config@3.2.0(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5): + resolution: {integrity: sha512-DlB0T5o0BcRlqJ9ktejs+jtufrxwyWnzWXsCM8GrWMph+19dCBOf8w3aDQpuIu9hM16d79mDTvSwwDx5ekOVvw==} + engines: {node: '20', pnpm: '8'} + peerDependencies: + eslint: '>=8' + dependencies: + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + eslint: 8.57.0 + eslint-config-prettier: 9.0.0(eslint@8.57.0) + eslint-plugin-import: 2.28.1(@typescript-eslint/parser@5.62.0)(eslint@8.57.0) + eslint-plugin-jsdoc: 41.1.2(eslint@8.57.0) + eslint-plugin-no-only-tests: 3.1.0 + eslint-plugin-prettier: 5.0.0(eslint-config-prettier@9.0.0)(eslint@8.57.0)(prettier@3.2.5) + transitivePeerDependencies: + - '@types/eslint' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - prettier + - supports-color + - typescript + dev: true + + /@nhcarrigan/prettier-config@3.2.0(prettier@3.2.5): + resolution: {integrity: sha512-AZOzwDTZfRiEinjUmqRj4gqZLYpLANhN1iMIsESxeuln+/BjGI06pINQsePIZ/I2HoPd+HGjNqu0S+Os9nHzuw==} + engines: {node: '20', pnpm: '8'} + peerDependencies: + prettier: '>=3' + dependencies: + prettier: 3.2.5 + dev: true + + /@nhcarrigan/typescript-config@3.0.0(typescript@5.4.5): + resolution: {integrity: sha512-uLi6gIMBW9Frqfg+XwKKEt1JW/s2oJiCoQ2MA6DJROeSLcAd+kvDcMTRGb74DT86PSIEVGby9/BqJSl7okSgZg==} + engines: {node: '20', pnpm: '8'} + peerDependencies: + typescript: ^5.0.0 + dependencies: + typescript: 5.4.5 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.scandir@3.0.0: + resolution: {integrity: sha512-ktI9+PxfHYtKjF3cLTUAh2N+b8MijCRPNwKJNqTVdL0gB0QxLU2rIRaZ1t71oEa3YBDE6bukH1sR0+CDnpp/Mg==} + engines: {node: '>=16.14.0'} + dependencies: + '@nodelib/fs.stat': 3.0.0 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.stat@3.0.0: + resolution: {integrity: sha512-2tQOI38s19P9i7X/Drt0v8iMA+KMsgdhB/dyPER+e+2Y8L1Z7QvnuRdW/uLuf5YRFUYmnj4bMA6qCuZHFI1GDQ==} + engines: {node: '>=16.14.0'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@nodelib/fs.walk@2.0.0: + resolution: {integrity: sha512-54voNDBobGdMl3BUXSu7UaDh1P85PGHWlJ5e0XhPugo1JulOyCtp2I+5ri4wplGDJ8QGwPEQW7/x3yTLU7yF1A==} + engines: {node: '>=16.14.0'} + dependencies: + '@nodelib/fs.scandir': 3.0.0 + fastq: 1.15.0 + dev: true + + /@octokit/auth-token@4.0.0: + resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} + engines: {node: '>= 18'} + dev: false + + /@octokit/core@5.2.0: + resolution: {integrity: sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-token': 4.0.0 + '@octokit/graphql': 7.1.0 + '@octokit/request': 8.4.0 + '@octokit/request-error': 5.1.0 + '@octokit/types': 13.5.0 + before-after-hook: 2.2.3 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/endpoint@9.0.5: + resolution: {integrity: sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 13.5.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/graphql@7.1.0: + resolution: {integrity: sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request': 8.4.0 + '@octokit/types': 13.5.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/openapi-types@22.2.0: + resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} + dev: false + + /@octokit/plugin-paginate-rest@11.3.1(@octokit/core@5.2.0): + resolution: {integrity: sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '5' + dependencies: + '@octokit/core': 5.2.0 + '@octokit/types': 13.5.0 + dev: false + + /@octokit/plugin-request-log@4.0.1(@octokit/core@5.2.0): + resolution: {integrity: sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '5' + dependencies: + '@octokit/core': 5.2.0 + dev: false + + /@octokit/plugin-rest-endpoint-methods@13.2.2(@octokit/core@5.2.0): + resolution: {integrity: sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': ^5 + dependencies: + '@octokit/core': 5.2.0 + '@octokit/types': 13.5.0 + dev: false + + /@octokit/request-error@5.1.0: + resolution: {integrity: sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 13.5.0 + deprecation: 2.3.1 + once: 1.4.0 + dev: false + + /@octokit/request@8.4.0: + resolution: {integrity: sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/endpoint': 9.0.5 + '@octokit/request-error': 5.1.0 + '@octokit/types': 13.5.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/rest@20.1.1: + resolution: {integrity: sha512-MB4AYDsM5jhIHro/dq4ix1iWTLGToIGk6cWF5L6vanFaMble5jTX/UBQyiv05HsWnwUtY8JrfHy2LWfKwihqMw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/core': 5.2.0 + '@octokit/plugin-paginate-rest': 11.3.1(@octokit/core@5.2.0) + '@octokit/plugin-request-log': 4.0.1(@octokit/core@5.2.0) + '@octokit/plugin-rest-endpoint-methods': 13.2.2(@octokit/core@5.2.0) + dev: false + + /@octokit/types@13.5.0: + resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==} + dependencies: + '@octokit/openapi-types': 22.2.0 + dev: false + + /@pkgr/core@0.1.1: + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dev: true + + /@prisma/client@5.13.0(prisma@5.13.0): + resolution: {integrity: sha512-uYdfpPncbZ/syJyiYBwGZS8Gt1PTNoErNYMuqHDa2r30rNSFtgTA/LXsSk55R7pdRTMi5pHkeP9B14K6nHmwkg==} + engines: {node: '>=16.13'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + prisma: 5.13.0 + dev: false + + /@prisma/debug@5.13.0: + resolution: {integrity: sha512-699iqlEvzyCj9ETrXhs8o8wQc/eVW+FigSsHpiskSFydhjVuwTJEfj/nIYqTaWFYuxiWQRfm3r01meuW97SZaQ==} + + /@prisma/engines-version@5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b: + resolution: {integrity: sha512-AyUuhahTINGn8auyqYdmxsN+qn0mw3eg+uhkp8zwknXYIqoT3bChG4RqNY/nfDkPvzWAPBa9mrDyBeOnWSgO6A==} + + /@prisma/engines@5.13.0: + resolution: {integrity: sha512-hIFLm4H1boj6CBZx55P4xKby9jgDTeDG0Jj3iXtwaaHmlD5JmiDkZhh8+DYWkTGchu+rRF36AVROLnk0oaqhHw==} + requiresBuild: true + dependencies: + '@prisma/debug': 5.13.0 + '@prisma/engines-version': 5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b + '@prisma/fetch-engine': 5.13.0 + '@prisma/get-platform': 5.13.0 + + /@prisma/fetch-engine@5.13.0: + resolution: {integrity: sha512-Yh4W+t6YKyqgcSEB3odBXt7QyVSm0OQlBSldQF2SNXtmOgMX8D7PF/fvH6E6qBCpjB/yeJLy/FfwfFijoHI6sA==} + dependencies: + '@prisma/debug': 5.13.0 + '@prisma/engines-version': 5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b + '@prisma/get-platform': 5.13.0 + + /@prisma/get-platform@5.13.0: + resolution: {integrity: sha512-B/WrQwYTzwr7qCLifQzYOmQhZcFmIFhR81xC45gweInSUn2hTEbfKUPd2keAog+y5WI5xLAFNJ3wkXplvSVkSw==} + dependencies: + '@prisma/debug': 5.13.0 + + /@puppeteer/browsers@1.5.0: + resolution: {integrity: sha512-za318PweGINh5LnHSph7C4xhs0tmRjCD8EPpzcKlw4nzSPhnULj+LTG3+TGefZvW1ti5gjw2JkdQvQsivBeZlg==} + engines: {node: '>=16.3.0'} + hasBin: true + dependencies: + debug: 4.3.4 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.3.0 + tar-fs: 3.0.4 + unbzip2-stream: 1.4.3 + yargs: 17.7.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@sapphire/async-queue@1.5.2: + resolution: {integrity: sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dev: false + + /@sapphire/shapeshift@3.9.7: + resolution: {integrity: sha512-4It2mxPSr4OGn4HSQWGmhFMsNFGfFVhWeRPCRwbH972Ek2pzfGRZtb0pJ4Ze6oIzcyh2jw7nUDa6qGlWofgd9g==} + engines: {node: '>=v16'} + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.17.21 + dev: false + + /@sapphire/snowflake@3.5.3: + resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dev: false + + /@snyk/github-codeowners@1.1.0: + resolution: {integrity: sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw==} + engines: {node: '>=8.10'} + hasBin: true + dependencies: + commander: 4.1.1 + ignore: 5.2.4 + p-map: 4.0.0 + dev: true + + /@tootallnate/quickjs-emscripten@0.23.0: + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + dev: false + + /@types/body-parser@1.19.2: + resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} + dependencies: + '@types/connect': 3.4.35 + '@types/node': 20.3.1 + dev: true + + /@types/connect@3.4.35: + resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} + dependencies: + '@types/node': 20.3.1 + dev: true + + /@types/express-serve-static-core@4.17.35: + resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} + dependencies: + '@types/node': 20.3.1 + '@types/qs': 6.9.7 + '@types/range-parser': 1.2.4 + '@types/send': 0.17.1 + dev: true + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.2 + '@types/express-serve-static-core': 4.17.35 + '@types/qs': 6.9.7 + '@types/serve-static': 1.15.1 + dev: true + + /@types/json-schema@7.0.12: + resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} + dev: true + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + + /@types/mime@1.3.2: + resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} + dev: true + + /@types/mime@3.0.1: + resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} + dev: true + + /@types/node@20.3.1: + resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==} + + /@types/qs@6.9.7: + resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} + dev: true + + /@types/range-parser@1.2.4: + resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} + dev: true + + /@types/semver@7.5.0: + resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} + dev: true + + /@types/send@0.17.1: + resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} + dependencies: + '@types/mime': 1.3.2 + '@types/node': 20.3.1 + dev: true + + /@types/serve-static@1.15.1: + resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} + dependencies: + '@types/mime': 3.0.1 + '@types/node': 20.3.1 + dev: true + + /@types/triple-beam@1.3.2: + resolution: {integrity: sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==} + dev: false + + /@types/ws@8.5.10: + resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + dependencies: + '@types/node': 20.3.1 + dev: false + + /@types/yauzl@2.10.3: + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + requiresBuild: true + dependencies: + '@types/node': 20.3.1 + dev: false + optional: true + + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.5.1 + '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + debug: 4.3.4 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + semver: 7.5.2 + tsutils: 3.21.0(typescript@5.4.5) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) + debug: 4.3.4 + eslint: 8.57.0 + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.62.0: + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + dev: true + + /@typescript-eslint/type-utils@5.62.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + debug: 4.3.4 + eslint: 8.57.0 + tsutils: 3.21.0(typescript@5.4.5) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.62.0: + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.5): + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.2 + tsutils: 3.21.0(typescript@5.4.5) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.0 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) + eslint: 8.57.0 + eslint-scope: 5.1.1 + semver: 7.5.2 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@5.62.0: + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.1 + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@vladfrangu/async_event_emitter@2.2.4: + resolution: {integrity: sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dev: false + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /acorn-jsx@5.3.2(acorn@8.11.3): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.3 + dev: true + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: false + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + /arity-n@1.0.4: + resolution: {integrity: sha512-fExL2kFDC1Q2DUOx3whE/9KoN66IzkY4b4zUHUBFM1ojEYjZZYDcUW3bek/ufGionX9giIKDC5redH2IlGqcQQ==} + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: true + + /array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + dev: true + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /array-includes@3.1.6: + resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + dev: true + + /array-last@1.3.0: + resolution: {integrity: sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==} + engines: {node: '>=0.10.0'} + dependencies: + is-number: 4.0.0 + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.flat@1.3.1: + resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap@1.3.1: + resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + es-shim-unscopables: 1.0.0 + dev: true + + /arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + dev: true + + /ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + dependencies: + tslib: 2.6.2 + dev: false + + /async@3.2.4: + resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + dev: false + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: true + + /b4a@1.6.6: + resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} + dev: false + + /babylon@6.18.0: + resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} + hasBin: true + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /bare-events@2.2.2: + resolution: {integrity: sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==} + requiresBuild: true + dev: false + optional: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + + /basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + dev: false + + /before-after-hook@2.2.3: + resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + dev: false + + /body-parser@1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: false + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: false + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chromium-bidi@0.4.20(devtools-protocol@0.0.1147663): + resolution: {integrity: sha512-ruHgVZFEv00mAQMz1tQjfjdG63jiPWrQPF6HLlX2ucqLqVTJoWngeBEKHaJ6n1swV/HSvgnBNbtTRIlcVyW3Fw==} + peerDependencies: + devtools-protocol: '*' + dependencies: + devtools-protocol: 0.0.1147663 + mitt: 3.0.1 + dev: false + + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: false + + /clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + requiresBuild: true + dev: true + optional: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: false + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: false + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: false + + /color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + dev: false + + /colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + dev: false + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /comment-parser@1.3.1: + resolution: {integrity: sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==} + engines: {node: '>= 12.0.0'} + dev: true + + /compose-function@3.0.3: + resolution: {integrity: sha512-xzhzTJ5eC+gmIzvZq+C3kCJHsp9os6tJkrigDRZclyGtOKINbZtE8n1Tzmeh32jW+BUDPbvZpibwvJHBLGMVwg==} + dependencies: + arity-n: 1.0.4 + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + dev: false + + /cosmiconfig@8.2.0: + resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} + engines: {node: '>=14'} + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + dev: false + + /cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + dev: false + + /data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + + /deep-freeze@0.0.1: + resolution: {integrity: sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg==} + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + requiresBuild: true + dependencies: + clone: 1.0.4 + dev: true + optional: true + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: true + + /define-properties@1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + + /degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /deprecation@2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /devtools-protocol@0.0.1147663: + resolution: {integrity: sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==} + dev: false + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /discord-api-types@0.37.83: + resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} + dev: false + + /discord.js@14.15.2: + resolution: {integrity: sha512-wGD37YCaTUNprtpqMIRuNiswwsvSWXrHykBSm2SAosoTYut0VUDj9yo9t4iLtMKvuhI49zYkvKc2TNdzdvpJhg==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/builders': 1.8.1 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.4.0 + '@discordjs/rest': 2.3.0 + '@discordjs/util': 1.1.0 + '@discordjs/ws': 1.1.0 + '@sapphire/snowflake': 3.5.3 + discord-api-types: 0.37.83 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + tslib: 2.6.2 + undici: 6.13.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + dev: false + + /easy-table@1.2.0: + resolution: {integrity: sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==} + dependencies: + ansi-regex: 5.0.1 + optionalDependencies: + wcwidth: 1.0.1 + dev: true + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + + /enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: false + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: false + + /es-abstract@1.21.2: + resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.10 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.0 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.7 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.9 + dev: true + + /es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + dev: true + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: true + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: true + + /es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: true + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: true + + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + + /es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + dependencies: + hasown: 2.0.2 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + dev: false + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: false + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: false + + /eslint-config-prettier@9.0.0(eslint@8.57.0): + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.57.0 + dev: true + + /eslint-import-resolver-node@0.3.7: + resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} + dependencies: + debug: 3.2.7 + is-core-module: 2.13.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint@8.57.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + debug: 3.2.7 + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.7 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-import@2.28.1(@typescript-eslint/parser@5.62.0)(eslint@8.57.0): + resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + array-includes: 3.1.6 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.1 + array.prototype.flatmap: 1.3.1 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.7 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint@8.57.0) + has: 1.0.3 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.1.6 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-jsdoc@41.1.2(eslint@8.57.0): + resolution: {integrity: sha512-MePJXdGiPW7AG06CU5GbKzYtKpoHwTq1lKijjq+RwL/cQkZtBZ59Zbv5Ep0RVxSMnq6242249/n+w4XrTZ1Afg==} + engines: {node: ^14 || ^16 || ^17 || ^18 || ^19} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@es-joy/jsdoccomment': 0.37.1 + are-docs-informative: 0.0.2 + comment-parser: 1.3.1 + debug: 4.3.4 + escape-string-regexp: 4.0.0 + eslint: 8.57.0 + esquery: 1.5.0 + semver: 7.5.2 + spdx-expression-parse: 3.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-no-only-tests@3.1.0: + resolution: {integrity: sha512-Lf4YW/bL6Un1R6A76pRZyE1dl1vr31G/ev8UzIc/geCgFWyrKil8hVjYqWVKGB/UIGmb6Slzs9T0wNezdSVegw==} + engines: {node: '>=5.0.0'} + dev: true + + /eslint-plugin-prettier@5.0.0(eslint-config-prettier@9.0.0)(eslint@8.57.0)(prettier@3.2.5): + resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.57.0 + eslint-config-prettier: 9.0.0(eslint@8.57.0) + prettier: 3.2.5 + prettier-linter-helpers: 1.0.0 + synckit: 0.8.8 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.1: + resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + eslint-visitor-keys: 3.4.3 + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /express@4.19.2: + resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.2 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.6.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + dependencies: + debug: 4.3.4 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + + /fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: false + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + 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.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + dependencies: + pend: 1.2.0 + dev: false + + /fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + dev: false + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + dev: true + + /file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + dependencies: + flat-cache: 4.0.1 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /filter-iterator@0.0.1: + resolution: {integrity: sha512-v4lhL7Qa8XpbW3LN46CEnmhGk3eHZwxfNl5at20aEkreesht4YKb/Ba3BUIbnPhAC/r3dmu7ABaGk6MAvh2alA==} + dev: true + + /filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + dev: true + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + dev: true + + /flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + dev: true + + /fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + dev: false + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /function.prototype.name@1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + functions-have-names: 1.2.3 + dev: true + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: false + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.2 + dev: true + + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: false + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: true + + /get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + dev: true + + /get-uri@6.0.3: + resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} + engines: {node: '>= 14'} + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.3.4 + fs-extra: 11.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.0 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: false + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.17.4 + dev: false + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: false + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-own-property@0.1.0: + resolution: {integrity: sha512-14qdBKoonU99XDhWcFKZTShK+QV47qU97u8zzoVo9cL5TZ3BmBHXogItSt9qJjR0KUMFRhcCW8uGIGl8nkl7Aw==} + dev: true + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.1 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /https-proxy-agent@7.0.4: + resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.1 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /identity-function@1.0.0: + resolution: {integrity: sha512-kNrgUK0qI+9qLTBidsH85HjDLpZfrrS0ElquKKe/fJFdB3D7VeKdXXEvOPDUHSHOzdZKCAAaQIWWyp0l2yq6pw==} + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.4 + dev: true + + /ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + dev: false + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.10 + dev: true + + /is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: false + + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: false + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.2 + dev: true + + /is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + dependencies: + is-typed-array: 1.1.13 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-iterable@1.1.1: + resolution: {integrity: sha512-EdOZCr0NsGE00Pot+x1ZFx9MJK3C6wy91geZpXwvwexDLJvA4nzYyZf7r+EIwSeVsOLDdBz7ATg9NqKTzuNYuQ==} + engines: {node: '>= 4'} + dev: true + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-number@4.0.0: + resolution: {integrity: sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: false + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.10: + resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.15 + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /iterable-lookahead@1.0.0: + resolution: {integrity: sha512-hJnEP2Xk4+44DDwJqUQGdXal5VbyeWLaPyDl2AQc242Zr7iqz4DgpQOrEzglWVMGHMDCkguLHEKxd1+rOsmgSQ==} + engines: {node: '>=4'} + dev: true + + /jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + + /jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + dev: false + + /jsdoc-type-pratt-parser@4.0.0: + resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==} + engines: {node: '>=12.0.0'} + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: false + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /knip@5.15.0(@types/node@20.3.1)(typescript@5.4.5): + resolution: {integrity: sha512-RB1Unv8NWrIAyNTNGA1z+uDSnHEp78BtGhkJxyg/Ie2lPHx6k+6kZOx+AZeE6J9/ZDvQ8nEg+BkRmB++7NIrDA==} + engines: {node: '>=18.6.0'} + hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4' + dependencies: + '@ericcornelissen/bash-parser': 0.5.2 + '@nodelib/fs.walk': 2.0.0 + '@snyk/github-codeowners': 1.1.0 + '@types/node': 20.3.1 + easy-table: 1.2.0 + fast-glob: 3.3.2 + file-entry-cache: 8.0.0 + jiti: 1.21.0 + js-yaml: 4.1.0 + minimist: 1.2.8 + picocolors: 1.0.0 + picomatch: 4.0.2 + pretty-ms: 9.0.0 + resolve: 1.22.8 + smol-toml: 1.1.4 + strip-json-comments: 5.0.1 + summary: 2.1.0 + typescript: 5.4.5 + zod: 3.23.8 + zod-validation-error: 3.2.0(zod@3.23.8) + dev: true + + /kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + dev: false + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: false + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.curry@4.1.1: + resolution: {integrity: sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /logform@2.5.1: + resolution: {integrity: sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==} + dependencies: + '@colors/colors': 1.5.0 + '@types/triple-beam': 1.3.2 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.4.3 + triple-beam: 1.3.0 + dev: false + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: false + + /magic-bytes.js@1.10.0: + resolution: {integrity: sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==} + dev: false + + /magic-string@0.16.0: + resolution: {integrity: sha512-c4BEos3y6G2qO0B9X7K0FVLOPT9uGrjYwYRLFmDqyl5YMboUviyecnXWp94fJTSMwPw2/sf+CEYt5AGpmklkkQ==} + dependencies: + vlq: 0.2.3 + dev: true + + /map-obj@2.0.0: + resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==} + engines: {node: '>=4'} + dev: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + /mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + dev: false + + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + /natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: false + + /netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + dev: false + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /node-html-to-image@4.0.0: + resolution: {integrity: sha512-lB8fkRleAKG4afJ2Wr7qJzIA5+//ue9OEoz+BMxQsowriGKR8sf4j4lK/pIXKakYwf/3aZHoDUNgOXuJ4HOzYA==} + dependencies: + handlebars: 4.7.8 + puppeteer: 21.0.1 + puppeteer-cluster: 0.23.0(puppeteer@21.0.1) + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object-pairs@0.1.0: + resolution: {integrity: sha512-3ECr6K831I4xX/Mduxr9UC+HPOz/d6WKKYj9p4cmC8Lg8p7g8gitzsxNX5IWlSIgFWN/a4JgrJaoAMKn20oKwA==} + dev: true + + /object-values@1.0.0: + resolution: {integrity: sha512-+8hwcz/JnQ9EpLIXzN0Rs7DLsBpJNT/xYehtB/jU93tHYr5BFEO8E+JGQNOSqE7opVzz5cGksKFHt7uUJVLSjQ==} + engines: {node: '>=0.10.0'} + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + + /object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + dev: true + + /object.values@1.1.6: + resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + dependencies: + fn.name: 1.1.0 + dev: false + + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + dependencies: + aggregate-error: 3.1.0 + dev: true + + /pac-proxy-agent@7.0.1: + resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==} + engines: {node: '>= 14'} + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.1 + debug: 4.3.4 + get-uri: 6.0.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.3 + transitivePeerDependencies: + - supports-color + dev: false + + /pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + dev: false + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.24.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: false + + /parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + dev: true + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + /pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + dev: false + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + dev: true + + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + dependencies: + fast-diff: 1.3.0 + dev: true + + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /pretty-ms@9.0.0: + resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==} + engines: {node: '>=18'} + dependencies: + parse-ms: 4.0.0 + dev: true + + /prisma@5.13.0: + resolution: {integrity: sha512-kGtcJaElNRAdAGsCNykFSZ7dBKpL14Cbs+VaQ8cECxQlRPDjBlMHNFYeYt0SKovAVy2Y65JXQwB3A5+zIQwnTg==} + engines: {node: '>=16.13'} + hasBin: true + requiresBuild: true + dependencies: + '@prisma/engines': 5.13.0 + + /progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /proxy-agent@6.3.0: + resolution: {integrity: sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.1 + debug: 4.3.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + lru-cache: 7.18.3 + pac-proxy-agent: 7.0.1 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.3 + transitivePeerDependencies: + - supports-color + dev: false + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: false + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: true + + /puppeteer-cluster@0.23.0(puppeteer@21.0.1): + resolution: {integrity: sha512-108terIWDzPrQopmoYSPd5yDoy3FGJ2dNnoGMkGYPs6xtkdhgaECwpfZkzaRToMQPZibUOz0/dSSGgPEdXEhkQ==} + peerDependencies: + puppeteer: '>=1.5.0' + dependencies: + debug: 4.3.4 + puppeteer: 21.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /puppeteer-core@21.0.1: + resolution: {integrity: sha512-E8eWLGhaZZpa7dYe/58qGX7SLb4mTg42NP5M7B+ibPrncgNjTOQa9x1sFIlTn1chF/BmoZqOcMIvwuxcb/9XzQ==} + engines: {node: '>=16.3.0'} + dependencies: + '@puppeteer/browsers': 1.5.0 + chromium-bidi: 0.4.20(devtools-protocol@0.0.1147663) + cross-fetch: 4.0.0 + debug: 4.3.4 + devtools-protocol: 0.0.1147663 + ws: 8.13.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + + /puppeteer@21.0.1: + resolution: {integrity: sha512-KTjmSdPZ6bMkq3EbAzAUhcB3gMDXvdwd6912rxG9hNtjwRJzHSA568vh6vIbO2WQeNmozRdt1LtiUMLSWfeMrg==} + engines: {node: '>=16.3.0'} + deprecated: < 21.9.0 is no longer supported + requiresBuild: true + dependencies: + '@puppeteer/browsers': 1.5.0 + cosmiconfig: 8.2.0 + puppeteer-core: 21.0.1 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /regexp.prototype.flags@1.5.0: + resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + functions-have-names: 1.2.3 + dev: true + + /regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: false + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /reverse-arguments@1.0.0: + resolution: {integrity: sha512-/x8uIPdTafBqakK0TmPNJzgkLP+3H+yxpUJhCQHsLBg1rYEVNR2D8BRYNWQhVBjyOd7oo1dZRVzIkwMY2oqfYQ==} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: true + + /safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + dev: true + + /safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.5.2: + resolution: {integrity: sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: true + + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + dev: true + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /shell-quote-word@1.0.1: + resolution: {integrity: sha512-lT297f1WLAdq0A4O+AknIFRP6kkiI3s8C913eJ0XqBxJbZPGWUNkRQk2u8zk4bEAjUJ5i+fSLwB6z1HzeT+DEg==} + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: false + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + dev: false + + /smol-toml@1.1.4: + resolution: {integrity: sha512-Y0OT8HezWsTNeEOSVxDnKOW/AyNXHQ4BwJNbAXlLTF5wWsBvrcHhIkE5Rf8kQMLmgf7nDX3PVOlgC6/Aiggu3Q==} + engines: {node: '>= 18', pnpm: '>= 8'} + dev: true + + /socks-proxy-agent@8.0.3: + resolution: {integrity: sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.1 + debug: 4.3.4 + socks: 2.8.3 + transitivePeerDependencies: + - supports-color + dev: false + + /socks@2.8.3: + resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + dev: false + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: false + + /spdx-exceptions@2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.13 + dev: true + + /spdx-license-ids@3.0.13: + resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==} + dev: true + + /sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + dev: false + + /stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + dev: false + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /streamx@2.16.1: + resolution: {integrity: sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==} + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + optionalDependencies: + bare-events: 2.2.2 + dev: false + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + + /string.fromcodepoint@0.2.1: + resolution: {integrity: sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg==} + dev: true + + /string.prototype.trim@1.2.7: + resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimend@1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimstart@1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /strip-json-comments@5.0.1: + resolution: {integrity: sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==} + engines: {node: '>=14.16'} + dev: true + + /summary@2.1.0: + resolution: {integrity: sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==} + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: false + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /synckit@0.8.8: + resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.6.2 + dev: true + + /tar-fs@3.0.4: + resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} + dependencies: + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 3.1.7 + dev: false + + /tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + dependencies: + b4a: 1.6.6 + fast-fifo: 1.3.2 + streamx: 2.16.1 + dev: false + + /text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + dev: false + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + dev: false + + /to-no-case@1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + dev: true + + /to-pascal-case@1.0.0: + resolution: {integrity: sha512-QGMWHqM6xPrcQW57S23c5/3BbYb0Tbe9p+ur98ckRnGDwD4wbbtDiYI38CfmMKNB5Iv0REjs5SNDntTwvDxzZA==} + dependencies: + to-space-case: 1.0.0 + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /to-space-case@1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + dependencies: + to-no-case: 1.0.2 + dev: true + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + + /triple-beam@1.3.0: + resolution: {integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==} + dev: false + + /ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + dev: false + + /tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + /tsutils@3.21.0(typescript@5.4.5): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 5.4.5 + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.10 + dev: true + + /typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + dev: true + + /typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: false + optional: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + dependencies: + buffer: 5.7.1 + through: 2.3.8 + dev: false + + /undici@6.13.0: + resolution: {integrity: sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==} + engines: {node: '>=18.0'} + dev: false + + /unescape-js@1.1.4: + resolution: {integrity: sha512-42SD8NOQEhdYntEiUQdYq/1V/YHwr1HLwlHuTJB5InVVdOSbgI6xu8jK5q65yIzuFCfczzyDF/7hbGzVbyCw0g==} + dependencies: + string.fromcodepoint: 0.2.1 + dev: true + + /universal-user-agent@6.0.1: + resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + dev: false + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: false + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /vlq@0.2.3: + resolution: {integrity: sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==} + dev: true + + /wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + requiresBuild: true + dependencies: + defaults: 1.0.4 + dev: true + optional: true + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: true + + /which-typed-array@1.1.9: + resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + is-typed-array: 1.1.10 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /winston-transport@4.7.0: + resolution: {integrity: sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==} + engines: {node: '>= 12.0.0'} + dependencies: + logform: 2.5.1 + readable-stream: 3.6.2 + triple-beam: 1.3.0 + dev: false + + /winston@3.13.0: + resolution: {integrity: sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.4 + is-stream: 2.0.1 + logform: 2.5.1 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.4.3 + stack-trace: 0.0.10 + triple-beam: 1.3.0 + winston-transport: 4.7.0 + dev: false + + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: false + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /ws@8.17.0: + resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: false + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: false + + /yargs@17.7.1: + resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: false + + /yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + dev: false + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /zod-validation-error@3.2.0(zod@3.23.8): + resolution: {integrity: sha512-cYlPR6zuyrgmu2wRTdumEAJGuwI7eHVHGT+VyneAQxmRAKtGRL1/7pjz4wfLhz4J05f5qoSZc3rGacswgyTjjw==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.18.0 + dependencies: + zod: 3.23.8 + dev: true + + /zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + dev: true diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..62a52ef --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,81 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("MONGO_URI") +} + +model cases { + id String @id @default(auto()) @map("_id") @db.ObjectId + serverId String + userId String + number Int + action String + reason String + evidence String[] + timestamp String + moderator String + + @@unique([serverId, number]) + @@index([serverId]) +} + +model levelRoles { + id String @id @default(auto()) @map("_id") @db.ObjectId + serverId String + level Int + roleId String + + @@unique([serverId, level, roleId]) + @@index([serverId], map: "serverId") +} + +model levels { + id String @id @default(auto()) @map("_id") @db.ObjectId + serverId String + userId String + points Int @default(0) + level Int @default(0) + username String + avatar String + backgroundColour String @default("000000") + colour String @default("ffffff") + backgroundImage String @default("https://cdn.nhcarrigan.com/banner.png") + cooldown DateTime @default(now()) + + @@unique([serverId, userId], map: "serverId_userId") +} + +model configs { + id String @id @default(auto()) @map("_id") @db.ObjectId + serverId String + inviteLink String @default("") + banAppealLink String @default("") + modLogChannel String @default("") + eventLogChannel String @default("") + messageReportChannel String @default("") + + @@unique([serverId], map: "serverId") +} + +model entitlements { + id String @id @default(auto()) @map("_id") @db.ObjectId + serverId String + purchaserId String + notes String @default("") + + @@unique([serverId]) +} + +model roles { + id String @id @default(auto()) @map("_id") @db.ObjectId + serverId String + roleId String + + @@unique([serverId, roleId], map: "serverId_roleId") +} diff --git a/sample.env b/sample.env new file mode 100644 index 0000000..2b2ce3b --- /dev/null +++ b/sample.env @@ -0,0 +1,11 @@ +## Global Values +BOT_TOKEN=-"" +MONGO_URI="" +DEBUG_HOOK="" +NODE_ENV="development" + +## Server Shit +GITHUB_WEBHOOK_SECRET="" +PATREON_WEBHOOK_SECRET="" +KOFI_WEBHOOK_SECRET="" +GITHUB_TOKEN="" diff --git a/src/commands/ban.ts b/src/commands/ban.ts new file mode 100644 index 0000000..913ec0e --- /dev/null +++ b/src/commands/ban.ts @@ -0,0 +1,99 @@ +import { + GuildMember, + PermissionFlagsBits, + SlashCommandBuilder +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; +import { isModerator } from "../utils/isModerator"; +import { processModAction } from "../utils/processModAction"; + +export const ban: Command = { + data: new SlashCommandBuilder() + .setName("ban") + .setDescription("Ban a user from the server.") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to ban.") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("The reason for banning.") + .setRequired(true) + .setMinLength(1) + .setMaxLength(400) + ) + .addIntegerOption((option) => + option + .setName("prune") + .setDescription("Number of days to prune messages.") + .setMinValue(0) + .setMaxValue(7) + ) + .addStringOption((option) => + option + .setName("evidence") + .setDescription( + "A link to the evidence for the ban. For multiple links, separate with a space." + ) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + + if (!member || !guild) { + await interaction.editReply({ + content: "There was an error loading the guild and member data." + }); + return; + } + + if ( + !(member as GuildMember).permissions.has(PermissionFlagsBits.BanMembers) + ) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const reason = interaction.options.getString("reason", true); + const evidence = + interaction.options.getString("evidence")?.split(/\s+/) || []; + const prune = interaction.options.getInteger("prune") || 0; + const user = interaction.options.getUser("user", true); + const target = + guild.members.cache.get(user.id) || + (await guild.members.fetch(user.id).catch(() => null)); + + if (target && isModerator(target)) { + await interaction.editReply({ + content: "You cannot ban a moderator." + }); + return; + } + await processModAction( + bot, + interaction, + guild, + user, + "ban", + reason, + evidence, + undefined, + undefined, + prune + ); + } catch (err) { + const id = await errorHandler(bot, "ban command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/cases.ts b/src/commands/cases.ts new file mode 100644 index 0000000..2f0aeb7 --- /dev/null +++ b/src/commands/cases.ts @@ -0,0 +1,90 @@ +import { GuildMember, EmbedBuilder, SlashCommandBuilder } from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { customSubstring } from "../utils/customSubstring"; +import { errorHandler } from "../utils/errorHandler"; +import { isModerator } from "../utils/isModerator"; + +export const cases: Command = { + data: new SlashCommandBuilder() + .setName("case") + .setDescription("View a specific moderation case.") + .addIntegerOption((option) => + option + .setName("number") + .setDescription("The case number to view.") + .setRequired(true) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + if (!member || !guild) { + await interaction.editReply({ + content: "There was an error loading the guild and member data." + }); + return; + } + + if (!isModerator(member as GuildMember)) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const target = interaction.options.getUser("user", true); + const number = interaction.options.getInteger("number", true); + + const requestedCase = await bot.db.cases.findFirst({ + where: { + userId: target.id, + serverId: guild.id, + number + } + }); + + if (!requestedCase) { + await interaction.editReply({ + content: "That user doesn't seem to have a moderation history yet." + }); + return; + } + + const viewEmbed = new EmbedBuilder(); + viewEmbed.setTitle( + `Case ${requestedCase.number} - ${requestedCase.action}` + ); + viewEmbed.setAuthor({ + name: target.tag, + iconURL: target.displayAvatarURL() + }); + viewEmbed.setDescription(customSubstring(requestedCase.reason, 4000)); + viewEmbed.addFields( + { + name: "Evidence", + value: + customSubstring(requestedCase.evidence.join("\n"), 2000) || + "No evidence provided" + }, + { + name: "Date", + value: requestedCase.timestamp + }, + { + name: "Moderator", + value: requestedCase.moderator + } + ); + + await interaction.editReply({ + embeds: [viewEmbed] + }); + } catch (err) { + const id = await errorHandler(bot, "cases command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/config.ts b/src/commands/config.ts new file mode 100644 index 0000000..71c14cc --- /dev/null +++ b/src/commands/config.ts @@ -0,0 +1,136 @@ +import { + SlashCommandBuilder, + SlashCommandSubcommandBuilder, + Guild, + GuildMember, + PermissionFlagsBits +} from "discord.js"; + +import { logChannelChoices } from "../config/LogChannelChoices"; +import { Command } from "../interfaces/Command"; +import { getConfig } from "../modules/data/getConfig"; +import { handleAppealLink } from "../modules/subcommands/config/handleAppealLink"; +import { handleInviteLink } from "../modules/subcommands/config/handleInviteLink"; +import { handleList } from "../modules/subcommands/config/handleList"; +import { handleLogging } from "../modules/subcommands/config/handleLogging"; +import { handleRole } from "../modules/subcommands/config/handleRole"; +import { errorHandler } from "../utils/errorHandler"; + +export const config: Command = { + data: new SlashCommandBuilder() + .setName("config") + .setDescription("Modify the config settings.") + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("list") + .setDescription("List your server's current config settings") + ) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("invite-link") + .setDescription( + "Set the link to be sent to someone to rejoin the server after they are kicked." + ) + .addStringOption((option) => + option + .setRequired(true) + .setName("link") + .setDescription("The invite link to send.") + ) + ) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("appeal-link") + .setDescription( + "Set the link to be sent to someone when they are banned to appeal the decision." + ) + .addStringOption((option) => + option + .setRequired(true) + .setName("link") + .setDescription("The appeal link to send.") + ) + ) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("logging") + .setDescription("Configure a logging channel.") + .addStringOption((option) => + option + .setName("log-type") + .setDescription("The type of log to configure.") + .addChoices(...logChannelChoices) + .setRequired(true) + ) + .addChannelOption((option) => + option + .setName("channel") + .setDescription("The channel to log to.") + .setRequired(true) + ) + ) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("roles") + .setDescription("Toggle roles to be self-assignable by users.") + .addRoleOption((o) => + o + .setName("role") + .setDescription("The role to toggle.") + .setRequired(true) + ) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + + const member = interaction.member as GuildMember; + const guild = interaction.guild as Guild; + + if (!member || !guild) { + await interaction.editReply({ + content: "You must be in a server to use this command." + }); + return; + } + + const config = await getConfig(bot, guild.id); + + if (!member.permissions.has(PermissionFlagsBits.ManageGuild)) { + await interaction.editReply({ + content: "You do not have permission to use this command." + }); + return; + } + + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand) { + case "list": + await handleList(bot, interaction, config); + break; + case "logging": + await handleLogging(bot, interaction, config); + break; + case "invite-link": + await handleInviteLink(bot, interaction, config); + break; + case "appeal-link": + await handleAppealLink(bot, interaction, config); + break; + case "roles": + await handleRole(bot, interaction, config); + break; + default: + await interaction.editReply({ + content: "This is an invalid subcommand. Please contact Naomi." + }); + } + } catch (err) { + const id = await errorHandler(bot, "config command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/help.ts b/src/commands/help.ts new file mode 100644 index 0000000..6abd0fc --- /dev/null +++ b/src/commands/help.ts @@ -0,0 +1,88 @@ +import { execSync } from "child_process"; + +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + SlashCommandBuilder +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { checkEntitledGuild } from "../utils/checkEntitledGuild"; +import { errorHandler } from "../utils/errorHandler"; + +export const help: Command = { + data: new SlashCommandBuilder() + .setName("help") + .setDMPermission(false) + .setDescription("Get help with the bot."), + run: async (bot, interaction) => { + try { + await interaction.deferReply(); + + const version = process.env.npm_package_version; + const commit = execSync("git rev-parse HEAD").toString().trim(); + const subscribed = await checkEntitledGuild(bot, interaction.guild); + + const servers = bot.guilds.cache.size; + const members = bot.guilds.cache.reduce( + (sum, guild) => sum + guild.memberCount, + 0 + ); + + const embed = new EmbedBuilder(); + embed.setTitle("Naomi's Moderation Bot"); + embed.setDescription( + "This is a highly focused moderation bot designed to deliver the best experience when it comes to keeping your community safe and welcoming. To ensure we are able to deliver the features our users require, this bot is only available through a $5/month subscription." + ); + embed.addFields( + { + name: "Version", + value: version ? `v${version}` : "unable to parse version", + inline: true + }, + { + name: "Current Commit", + value: `[${commit.slice( + 0, + 7 + )}](https://github.com/nhcarrigan/mod-bot/commit/${commit})`, + inline: true + }, + { + name: "Is this server subscribed?", + value: subscribed ? "Yes!" : "No :c", + inline: true + }, + { + name: "Details", + value: `Currently protecting ${servers} servers and watching over ${members} users.` + } + ); + + const supportButton = new ButtonBuilder() + .setStyle(ButtonStyle.Link) + .setURL("https://chat.naomi.lgbt") + .setLabel("Join our Support Server"); + const subscribeButton = new ButtonBuilder() + .setStyle(ButtonStyle.Link) + .setURL("https://docs.nhcarrigan.com/#/donate") + .setLabel("Subscribe for Access"); + const row = new ActionRowBuilder().addComponents( + supportButton, + subscribeButton + ); + + await interaction.editReply({ + embeds: [embed], + components: [row] + }); + } catch (err) { + const id = await errorHandler(bot, "help command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/history.ts b/src/commands/history.ts new file mode 100644 index 0000000..e51afe6 --- /dev/null +++ b/src/commands/history.ts @@ -0,0 +1,198 @@ +import { + GuildMember, + EmbedBuilder, + SlashCommandBuilder, + ButtonBuilder, + ActionRowBuilder, + ComponentType, + ButtonStyle +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; +import { getNextIndex, getPreviousIndex } from "../utils/getArrayIndex"; +import { isModerator } from "../utils/isModerator"; + +export const history: Command = { + data: new SlashCommandBuilder() + .setName("history") + .setDescription("View a user's history.") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to view the history for.") + .setRequired(true) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + if (!member || !guild) { + await interaction.editReply({ + content: "There was an error loading the guild and member data." + }); + return; + } + + if (!isModerator(member as GuildMember)) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const target = interaction.options.getUser("user", true); + + const cases = await bot.db.cases.findMany({ + where: { + userId: target.id, + serverId: guild.id + } + }); + + if (!cases.length) { + await interaction.editReply({ + content: "That user is squeaky clean!" + }); + return; + } + + const caseNumbers = cases + .filter((c) => c.action !== "note") + .map((c) => `**#${c.number} - ${c.action}**`); + const noteNumbers = cases + .filter((c) => c.action === "note") + .map((c) => `**#${c.number} - ${c.action}**`); + ``; + + const historyEmbed = new EmbedBuilder(); + historyEmbed.setTitle(`${target.tag}'s history`); + historyEmbed.addFields( + { + name: "Bans", + value: String(cases.filter((c) => c.action === "ban").length) || "0", + inline: true + }, + { + name: "Unbans", + value: + String(cases.filter((c) => c.action === "unban").length) || "0", + inline: true + }, + { + name: "Softbans", + value: + String(cases.filter((c) => c.action === "softban").length) || "0", + inline: true + }, + { + name: "Kicks", + value: String(cases.filter((c) => c.action === "kick").length) || "0", + inline: true + }, + { + name: "Mutes", + value: String(cases.filter((c) => c.action === "mute").length) || "0", + inline: true + }, + { + name: "Unmutes", + value: + String(cases.filter((c) => c.action === "unmute").length) || "0", + inline: true + }, + { + name: "Warns", + value: String(cases.filter((c) => c.action === "warn").length) || "0", + inline: true + }, + { + name: "Notes", + value: String(cases.filter((c) => c.action === "note").length) || "0", + inline: true + } + ); + + const embeds = [historyEmbed]; + + if (caseNumbers.length) { + const manualEmbed = new EmbedBuilder() + .setTitle("Manual Cases") + .setDescription(caseNumbers.join(", ")); + embeds.push(manualEmbed); + } + + if (noteNumbers.length) { + const noteEmbed = new EmbedBuilder() + .setTitle("Notes") + .setDescription(noteNumbers.join(", ")); + embeds.push(noteEmbed); + } + + let index = 0; + const nextButton = new ButtonBuilder() + .setCustomId("next") + .setStyle(ButtonStyle.Primary) + .setLabel( + embeds[getNextIndex(embeds, index)]?.data.title || "Unknown embed." + ) + .setEmoji("▶️"); + const prevButton = new ButtonBuilder() + .setCustomId("prev") + .setStyle(ButtonStyle.Primary) + .setLabel( + embeds[getPreviousIndex(embeds, index)]?.data.title || + "Unknown embed." + ) + .setEmoji("◀️"); + const initialRow = new ActionRowBuilder().addComponents( + prevButton, + nextButton + ); + + const response = await interaction.editReply({ + embeds: [embeds[index] as EmbedBuilder], + components: [initialRow] + }); + + const collector = + response.createMessageComponentCollector({ + time: 1000 * 60 * 5 + }); + + collector.on("collect", async (i) => { + await i.deferUpdate(); + index = + i.customId === "next" + ? getNextIndex(embeds, index) + : getPreviousIndex(embeds, index); + prevButton.setLabel( + embeds[getPreviousIndex(embeds, index)]?.data.title || + "Unknown embed." + ); + nextButton.setLabel( + embeds[getNextIndex(embeds, index)]?.data.title || "Unknown embed." + ); + const newRow = new ActionRowBuilder().addComponents( + prevButton, + nextButton + ); + await i.editReply({ + embeds: [embeds[index] as EmbedBuilder], + components: [newRow] + }); + }); + + collector.on("end", async () => { + await interaction.editReply({ + components: [] + }); + }); + } catch (err) { + const id = await errorHandler(bot, "history command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/kick.ts b/src/commands/kick.ts new file mode 100644 index 0000000..848832e --- /dev/null +++ b/src/commands/kick.ts @@ -0,0 +1,145 @@ +import { + GuildMember, + Message, + ActionRowBuilder, + ButtonBuilder, + PermissionFlagsBits, + ButtonStyle, + ComponentType, + SlashCommandBuilder +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; +import { isModerator } from "../utils/isModerator"; +import { processModAction } from "../utils/processModAction"; + +export const kick: Command = { + data: new SlashCommandBuilder() + .setName("kick") + .setDescription("Kick a user from the server.") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to kick.") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("The reason for kicking.") + .setRequired(true) + .setMinLength(1) + .setMaxLength(400) + ) + .addStringOption((option) => + option + .setName("evidence") + .setDescription( + "A link to the evidence for the kick. For multiple links, separate with a space." + ) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + + if (!member || !guild) { + await interaction.editReply({ + content: "There was an error loading the guild and member data." + }); + return; + } + + if ( + !(member as GuildMember).permissions.has( + PermissionFlagsBits.KickMembers + ) + ) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const reason = interaction.options.getString("reason", true); + const evidence = + interaction.options.getString("evidence")?.split(/\s+/) || []; + const user = interaction.options.getUser("user", true); + const target = + guild.members.cache.get(user.id) || + (await guild.members.fetch(user.id).catch(() => null)); + + if (!target) { + await interaction.editReply({ + content: `${user.tag} is not in this server and thus cannot be kicked.` + }); + return; + } + + if (isModerator(target)) { + await interaction.editReply({ + content: "You cannot kick a moderator." + }); + return; + } + + const yes = new ButtonBuilder() + .setCustomId("confirm") + .setLabel("Confirm") + .setStyle(ButtonStyle.Success); + const no = new ButtonBuilder() + .setCustomId("cancel") + .setLabel("Cancel") + .setStyle(ButtonStyle.Danger); + const row = new ActionRowBuilder().addComponents(yes, no); + const response = (await interaction.editReply({ + content: `Are you sure you want to kick <@!${user.id}>?`, + components: [row] + })) as Message; + + const collector = + response.createMessageComponentCollector({ + filter: (click) => click.user.id === interaction.user.id, + time: 10000, + max: 1 + }); + + collector.on("end", async (clicks) => { + const choice = clicks.first()?.customId; + if (!clicks || clicks.size <= 0 || !choice) { + await interaction.editReply({ + content: "This command has timed out.", + components: [] + }); + return; + } + + if (choice === "confirm") { + await processModAction( + bot, + interaction, + guild, + user, + "kick", + reason, + evidence + ); + return; + } + + if (choice === "cancel") { + interaction.editReply({ + content: "Kick cancelled.", + components: [] + }); + } + }); + } catch (err) { + const id = await errorHandler(bot, "kick command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/leaderboard.ts b/src/commands/leaderboard.ts new file mode 100644 index 0000000..75e1048 --- /dev/null +++ b/src/commands/leaderboard.ts @@ -0,0 +1,156 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, + SlashCommandBuilder +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { generateLeaderboardImage } from "../modules/commands/generateProfileImage"; +import { errorHandler } from "../utils/errorHandler"; + +export const leaderboard: Command = { + data: new SlashCommandBuilder() + .setName("leaderboard") + .setDescription("See the levels for this community.") + .setDMPermission(false), + run: async (bot, interaction) => { + try { + await interaction.deferReply(); + + const levels = await bot.db.levels.findMany({ + where: { + serverId: interaction.guild.id + }, + orderBy: { + points: "desc" + } + }); + + const mapped = levels.map((user, index) => ({ + ...user, + index: index + 1 + })); + + let page = 1; + const lastPage = Math.ceil(mapped.length / 10); + + const pageBack = new ButtonBuilder() + .setCustomId("prev") + .setDisabled(true) + .setLabel("◀") + .setStyle(ButtonStyle.Primary); + const pageForward = new ButtonBuilder() + .setCustomId("next") + .setLabel("▶") + .setStyle(ButtonStyle.Primary); + + if (page <= 1) { + pageBack.setDisabled(true); + } else { + pageBack.setDisabled(false); + } + + if (page >= lastPage) { + pageForward.setDisabled(true); + } else { + pageForward.setDisabled(false); + } + + const attachment = await generateLeaderboardImage( + bot, + mapped.slice(page * 10 - 10, page * 10) + ); + + if (!attachment) { + await interaction.editReply({ + content: "Failed to load leaderboard image.", + files: [], + components: [] + }); + return; + } + + const sent = await interaction.editReply({ + files: [attachment], + components: [ + new ActionRowBuilder().addComponents( + pageBack, + pageForward + ) + ] + }); + + const clickyClick = + sent.createMessageComponentCollector({ + time: 300000, + filter: (click) => click.user.id === interaction.user.id + }); + + clickyClick.on("collect", async (click) => { + await click.deferUpdate(); + if (click.customId === "prev") { + page--; + } + if (click.customId === "next") { + page++; + } + + if (page <= 1) { + pageBack.setDisabled(true); + } else { + pageBack.setDisabled(false); + } + + if (page >= lastPage) { + pageForward.setDisabled(true); + } else { + pageForward.setDisabled(false); + } + + const attachment = await generateLeaderboardImage( + bot, + mapped.slice(page * 10 - 10, page * 10) + ); + + if (!attachment) { + await interaction.editReply({ + content: "Failed to load leaderboard image.", + files: [], + components: [] + }); + return; + } + + await interaction.editReply({ + files: [attachment], + components: [ + new ActionRowBuilder().addComponents( + pageBack, + pageForward + ) + ] + }); + }); + + clickyClick.on("end", async () => { + pageBack.setDisabled(true); + pageForward.setDisabled(true); + await interaction.editReply({ + components: [ + new ActionRowBuilder().addComponents( + pageBack, + pageForward + ) + ] + }); + }); + } catch (err) { + const id = await errorHandler(bot, "leaderboard subcommand", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/levelRoles.ts b/src/commands/levelRoles.ts new file mode 100644 index 0000000..f1b627f --- /dev/null +++ b/src/commands/levelRoles.ts @@ -0,0 +1,106 @@ +import { + PermissionFlagsBits, + SlashCommandBuilder, + SlashCommandSubcommandBuilder +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; + +export const levelRoles: Command = { + data: new SlashCommandBuilder() + .setName("level-role") + .setDescription("Manage level roles.") + .setDMPermission(false) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("create") + .setDescription("Create a new level role.") + .addRoleOption((o) => + o + .setName("role") + .setDescription("The role to assign") + .setRequired(true) + ) + .addIntegerOption((o) => + o + .setName("level") + .setDescription("The level at which to assign the role.") + .setRequired(true) + .setMinValue(1) + .setMaxValue(1000) + ) + ) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("delete") + .setDescription("Delete a level role.") + .addRoleOption((o) => + o + .setName("role") + .setDescription("The role to remove") + .setRequired(true) + ) + .addIntegerOption((o) => + o + .setName("level") + .setDescription("The level at which the role was being assigned") + .setRequired(true) + .setMinValue(1) + .setMaxValue(1000) + ) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member } = interaction; + + if (!member.permissions.has(PermissionFlagsBits.ManageRoles)) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + const role = interaction.options.getRole("role", true); + const level = interaction.options.getInteger("level", true); + const action = interaction.options.getSubcommand(true); + + let success = false; + if (action === "create") { + success = !!(await bot.db.levelRoles + .create({ + data: { + serverId: interaction.guild.id, + roleId: role.id, + level + } + }) + .catch(() => null)); + } + if (action === "delete") { + success = !!(await bot.db.levelRoles + .delete({ + where: { + serverId_level_roleId: { + serverId: interaction.guild.id, + roleId: role.id, + level + } + } + }) + .catch(() => null)); + } + + await interaction.editReply({ + content: success + ? `Successfully ${action}ed your level ${level} ${role} assignment.` + : `Failed to ${action} your level ${level} ${role} assignment.` + }); + } catch (err) { + const id = await errorHandler(bot, "level roles command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/lockdown.ts b/src/commands/lockdown.ts new file mode 100644 index 0000000..3f2bdb1 --- /dev/null +++ b/src/commands/lockdown.ts @@ -0,0 +1,98 @@ +import { + ChannelType, + EmbedBuilder, + GuildMember, + PermissionFlagsBits, + SlashCommandBuilder, + TextChannel +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { getConfig } from "../modules/data/getConfig"; +import { errorHandler } from "../utils/errorHandler"; + +export const lockdown: Command = { + data: new SlashCommandBuilder() + .setName("lockdown") + .setDescription("Lock down a channel.") + .addChannelOption((option) => + option + .setName("channel") + .setDescription("The channel to lock down.") + .setRequired(true) + .addChannelTypes(ChannelType.GuildText) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + + if (!member || !guild) { + await interaction.editReply({ + content: "You must be in a guild to use this command." + }); + return; + } + + if ( + !(member as GuildMember).permissions.has( + PermissionFlagsBits.ManageChannels + ) + ) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const channel = interaction.options.getChannel( + "channel", + true + ) as TextChannel; + + if (!("send" in channel)) { + await interaction.editReply({ + content: "You must use this command to target a text based channel." + }); + return; + } + + await channel.permissionOverwrites.edit( + guild.id, + { + SendMessages: false + }, + { reason: `Lockdown Performed by ${interaction.user.tag}` } + ); + + const config = await getConfig(bot, guild.id); + if (!config.modLogChannel) { + return; + } + const logChannel = + guild.channels.cache.get(config.modLogChannel) || + (await guild.channels.fetch(config.modLogChannel)); + + if (!logChannel || !("send" in logChannel)) { + return; + } + const embed = new EmbedBuilder(); + embed.setTitle("Channel Locked Down"); + embed.setDescription( + `The <#${channel.id}> channel has been locked down.` + ); + embed.setAuthor({ + name: interaction.user.tag, + iconURL: interaction.user.displayAvatarURL() + }); + await logChannel.send({ embeds: [embed] }); + + await interaction.editReply({ content: "Channel has been locked down!" }); + } catch (err) { + const id = await errorHandler(bot, "lockdown", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/massBan.ts b/src/commands/massBan.ts new file mode 100644 index 0000000..60bd6e4 --- /dev/null +++ b/src/commands/massBan.ts @@ -0,0 +1,49 @@ +import { + ActionRowBuilder, + ModalBuilder, + SlashCommandBuilder, + TextInputBuilder, + TextInputStyle +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; + +export const massBan: Command = { + data: new SlashCommandBuilder() + .setName("massban") + .setDescription("Ban a list of user IDs at once."), + run: async (bot, interaction) => { + try { + const textInput = new TextInputBuilder() + .setCustomId("mass-ban-ids") + .setLabel("Input your list of IDs separated by new lines") + .setMaxLength(4000) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true); + const reasonInput = new TextInputBuilder() + .setCustomId("reason") + .setLabel("Reason for the mass ban?") + .setStyle(TextInputStyle.Short) + .setRequired(true); + const inputRow = new ActionRowBuilder().addComponents( + textInput + ); + const reasonRow = new ActionRowBuilder().addComponents( + reasonInput + ); + const modal = new ModalBuilder() + .setCustomId("mass-ban-modal") + .setTitle("Mass Ban") + .addComponents(inputRow, reasonRow); + + await interaction.showModal(modal); + } catch (err) { + const id = await errorHandler(bot, "mass ban", err); + await interaction.reply({ + ephemeral: true, + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/mute.ts b/src/commands/mute.ts new file mode 100644 index 0000000..8285bf2 --- /dev/null +++ b/src/commands/mute.ts @@ -0,0 +1,122 @@ +import { + GuildMember, + PermissionFlagsBits, + SlashCommandBuilder +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; +import { isModerator } from "../utils/isModerator"; +import { processModAction } from "../utils/processModAction"; + +export const mute: Command = { + data: new SlashCommandBuilder() + .setName("mute") + .setDescription("Mute a member") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to mute.") + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName("duration") + .setDescription("The duration of the mute.") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("duration-unit") + .setDescription("The unit for the duration value") + .setRequired(true) + .addChoices( + { name: "Minutes", value: "minutes" }, + { name: "Hours", value: "hours" }, + { name: "Days", value: "days" }, + { name: "Weeks", value: "weeks" } + ) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("The reason for the mute.") + .setRequired(true) + .setMinLength(1) + .setMaxLength(400) + ) + .addStringOption((option) => + option + .setName("evidence") + .setDescription( + "A link to the evidence for the mute. For multiple links, separate with a space." + ) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + + if (!member || !guild) { + await interaction.editReply({ + content: "There was an error loading the guild and member data." + }); + return; + } + + if ( + !(member as GuildMember).permissions.has( + PermissionFlagsBits.ModerateMembers + ) + ) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const user = interaction.options.getUser("user", true); + const target = + guild.members.cache.get(user.id) || + (await guild.members.fetch(user.id).catch(() => null)); + + if (!target) { + await interaction.editReply({ + content: "That member appears to have left the server." + }); + return; + } + + if (isModerator(target)) { + await interaction.editReply({ + content: "You cannot mute a moderator." + }); + return; + } + + const duration = interaction.options.getInteger("duration", true); + const durationUnit = interaction.options.getString("duration-unit", true); + + const reason = interaction.options.getString("reason", true); + const evidence = + interaction.options.getString("evidence")?.split(/\s+/) || []; + + await processModAction( + bot, + interaction, + guild, + user, + "mute", + reason, + evidence, + duration, + durationUnit + ); + } catch (err) { + const id = await errorHandler(bot, "mute command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/note.ts b/src/commands/note.ts new file mode 100644 index 0000000..f614a1e --- /dev/null +++ b/src/commands/note.ts @@ -0,0 +1,67 @@ +import { GuildMember, SlashCommandBuilder } from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; +import { isModerator } from "../utils/isModerator"; +import { processModAction } from "../utils/processModAction"; + +export const note: Command = { + data: new SlashCommandBuilder() + .setName("note") + .setDescription("Add a note to a member's record.") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to add a note to.") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("The reason for adding this note.") + .setRequired(true) + .setMinLength(1) + .setMaxLength(400) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + + if (!member || !guild) { + await interaction.editReply({ + content: "There was an error loading the guild and member data." + }); + return; + } + + if (!isModerator(member as GuildMember)) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const user = interaction.options.getUser("user", true); + const reason = interaction.options.getString("reason", true); + + const target = + guild.members.cache.get(user.id) || + (await guild.members.fetch(user.id).catch(() => null)); + + if (target && isModerator(target)) { + await interaction.editReply({ + content: "You cannot add a note to a moderator." + }); + return; + } + + await processModAction(bot, interaction, guild, user, "note", reason, []); + } catch (err) { + const id = await errorHandler(bot, "note command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/ping.ts b/src/commands/ping.ts new file mode 100644 index 0000000..f4a320f --- /dev/null +++ b/src/commands/ping.ts @@ -0,0 +1,50 @@ +import { EmbedBuilder, SlashCommandBuilder } from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; + +export const ping: Command = { + data: new SlashCommandBuilder() + .setName("ping") + .setDescription("Check the response time of the bot."), + run: async (bot, interaction) => { + try { + await interaction.deferReply(); + const receivedInteraction = Date.now(); + const { createdTimestamp } = interaction; + + const discordLatency = receivedInteraction - createdTimestamp; + const websocketLatency = bot.ws.ping; + + await bot.db.$runCommandRaw({ ping: 1 }); + const databaseLatency = Date.now() - receivedInteraction; + + const pingEmbed = new EmbedBuilder(); + pingEmbed.setTitle("Pong!"); + pingEmbed.setDescription("Here is my latency information!"); + pingEmbed.addFields( + { + name: "Interaction Latency", + value: `${discordLatency}ms`, + inline: true + }, + { + name: "Websocket Latency", + value: `${websocketLatency}ms`, + inline: true + }, + { + name: "Database Latency", + value: `${databaseLatency}ms`, + inline: true + } + ); + await interaction.editReply({ embeds: [pingEmbed] }); + } catch (err) { + const id = await errorHandler(bot, "ping command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/profile.ts b/src/commands/profile.ts new file mode 100644 index 0000000..c96e9ab --- /dev/null +++ b/src/commands/profile.ts @@ -0,0 +1,129 @@ +import { SlashCommandBuilder } from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { + validateColour, + validateImage +} from "../modules/commands/profileValidation"; +import { errorHandler } from "../utils/errorHandler"; + +export const profile: Command = { + data: new SlashCommandBuilder() + .setName("profile") + .setDescription("Edit your profile that appears in the leaderboard") + .setDMPermission(false) + .addStringOption((option) => + option + .setName("avatar") + .setDescription( + "The avatar to appear on your profile card must be a URL that points to an image." + ) + ) + .addStringOption((option) => + option + .setName("background-colour") + .setDescription( + "The semi-transparent background color for your profile card must be a 6-digit hex value." + ) + ) + .addStringOption((option) => + option + .setName("background-image") + .setDescription( + "The background image for your profile card must be a URL that points to an image." + ) + ) + .addStringOption((option) => + option + .setName("colour") + .setDescription( + "The color for the text on your profile card must be a 6-digit hex value." + ) + ), + run: async (CamperChan, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + + const responses = ["Your profile settings have been updated!"]; + const opts = { + avatar: interaction.options.getString("avatar"), + backgroundColour: interaction.options.getString("background-colour"), + backgroundImage: interaction.options.getString("background-image"), + colour: interaction.options.getString("colour") + }; + if (opts.avatar) { + const isValid = await validateImage(opts.avatar); + if (!isValid) { + responses.push(`${opts.avatar} is not a valid image URL.`); + opts.avatar = ""; + } + } + if (opts.backgroundImage) { + const isValid = await validateImage(opts.backgroundImage); + if (!isValid) { + responses.push(`${opts.backgroundImage} is not a valid image URL.`); + opts.backgroundImage = ""; + } + } + if (opts.colour) { + if (opts.colour.startsWith("#")) { + opts.colour = opts.colour.slice(1); + } + if (!validateColour(opts.colour)) { + opts.colour = null; + responses.push( + `${interaction.options.getString("colour")} is not a valid hex code. Please try again with a 6 character hex code (# is optional).` + ); + } + } + if (opts.backgroundColour) { + if (opts.backgroundColour.startsWith("#")) { + opts.backgroundColour = opts.backgroundColour.slice(1); + } + if (!validateColour(opts.backgroundColour)) { + opts.backgroundColour = null; + responses.push( + `${interaction.options.getString("background-colour")} is not a valid hex code. Please try again with a 6 character hex code (# is optional).` + ); + } + } + + const query = ( + Object.entries(opts) as [keyof typeof opts, string][] + ).reduce( + (acc, [key, val]) => { + if (val) { + acc[key] = val; + } + return acc; + }, + {} as Record + ); + + await CamperChan.db.levels.upsert({ + where: { + serverId_userId: { + serverId: interaction.guild.id, + userId: interaction.user.id + } + }, + update: { + ...query + }, + create: { + userId: interaction.user.id, + serverId: interaction.guild.id, + username: interaction.user.username, + ...query + } + }); + + await interaction.editReply({ content: responses.join("\n") }); + } catch (err) { + const id = await errorHandler(CamperChan, "user settings command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/prune.ts b/src/commands/prune.ts new file mode 100644 index 0000000..2cd96ee --- /dev/null +++ b/src/commands/prune.ts @@ -0,0 +1,71 @@ +import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { getConfig } from "../modules/data/getConfig"; +import { errorHandler } from "../utils/errorHandler"; + +export const prune: Command = { + data: new SlashCommandBuilder() + .setName("prune") + .setDescription("Prune messages from THIS channel.") + .addNumberOption((option) => + option + .setName("amount") + .setDescription("The amount of messages to remove") + .setMinValue(1) + .setMaxValue(100) + .setRequired(true) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + if (!member || !guild) { + await interaction.editReply({ + content: "Could not find the member or guild." + }); + return; + } + + if (!member.permissions.has(PermissionFlagsBits.ManageMessages)) { + await interaction.editReply({ + content: "You do not have permission to prune messages." + }); + return; + } + + const channel = interaction.channel; + const amount = interaction.options.getNumber("amount", true); + + if (!channel) { + await interaction.editReply({ + content: "Please provide a text channel or thread." + }); + return; + } + const messages = await channel.messages.fetch({ limit: amount }); + for (const message of messages.values()) { + await message.delete().catch(() => null); + } + + await interaction.editReply({ content: "Complete." }); + const config = await getConfig(bot, interaction.guild.id); + if (config.modLogChannel) { + const logChannel = + interaction.guild.channels.cache.get(config.modLogChannel) || + (await interaction.guild.channels.fetch(config.modLogChannel)); + + if (logChannel && "send" in logChannel) { + await logChannel.send({ + content: `Prune run by <@!${interaction.user.id}>. Deleted Messages: ${amount}` + }); + } + } + } catch (err) { + const id = await errorHandler(bot, "prune interaction", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/rank.ts b/src/commands/rank.ts new file mode 100644 index 0000000..8e58271 --- /dev/null +++ b/src/commands/rank.ts @@ -0,0 +1,52 @@ +import { SlashCommandBuilder } from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { generateProfileImage } from "../modules/commands/generateProfileImage"; +import { errorHandler } from "../utils/errorHandler"; + +export const rank: Command = { + data: new SlashCommandBuilder() + .setDMPermission(false) + .setName("rank") + .setDescription("See your level rank in the community."), + run: async (bot, interaction) => { + try { + await interaction.deferReply(); + const { user } = interaction; + + const target = user.id; + + const record = await bot.db.levels.findUnique({ + where: { + serverId_userId: { + userId: target, + serverId: interaction.guild.id + } + } + }); + + if (!record) { + await interaction.editReply({ + content: "Error loading your database record." + }); + return; + } + + const file = await generateProfileImage(bot, record); + + if (!file) { + await interaction.editReply({ + content: "There was an error generating your profile. :c" + }); + return; + } + + await interaction.editReply({ files: [file] }); + } catch (err) { + const id = await errorHandler(bot, "rank command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/role.ts b/src/commands/role.ts new file mode 100644 index 0000000..41ddbbc --- /dev/null +++ b/src/commands/role.ts @@ -0,0 +1,52 @@ +import { SlashCommandBuilder } from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; + +export const role: Command = { + data: new SlashCommandBuilder() + .setDMPermission(false) + .setName("role") + .setDescription("Give yourself a permitted role, or remove it.") + .addRoleOption((o) => + o.setName("role").setDescription("The role to toggle.").setRequired(true) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const role = interaction.options.getRole("role", true); + const isPermitted = await bot.db.roles + .findUnique({ + where: { + serverId_roleId: { + serverId: interaction.guild.id, + roleId: role.id + } + } + }) + .catch(() => null); + if (!isPermitted) { + await interaction.editReply({ + content: `The <@&${role.id}> role is not self-assignable.` + }); + return; + } + if (interaction.member.roles.cache.has(role.id)) { + await interaction.member.roles.remove(role.id); + await interaction.editReply({ + content: `The <@&${role.id}> role has been removed.` + }); + return; + } + await interaction.member.roles.add(role.id); + await interaction.editReply({ + content: `The <@&${role.id}> role has been added.` + }); + } catch (err) { + const id = await errorHandler(bot, "role command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/softBan.ts b/src/commands/softBan.ts new file mode 100644 index 0000000..68a2d32 --- /dev/null +++ b/src/commands/softBan.ts @@ -0,0 +1,102 @@ +import { + GuildMember, + PermissionFlagsBits, + SlashCommandBuilder +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; +import { isModerator } from "../utils/isModerator"; +import { processModAction } from "../utils/processModAction"; + +export const softBan: Command = { + data: new SlashCommandBuilder() + .setName("softban") + .setDescription( + "Bans a user from the server, cleans up their messages, and removes the ban." + ) + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to softban.") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("The reason for softbanning.") + .setRequired(true) + .setMinLength(1) + .setMaxLength(400) + ) + .addIntegerOption((option) => + option + .setName("prune") + .setDescription("Number of days to prune messages.") + .setMinValue(1) + .setMaxValue(7) + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("evidence") + .setDescription( + "A link to the evidence for the ban. For multiple links, separate with a space." + ) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + + if (!member || !guild) { + await interaction.editReply({ + content: "There was an error loading the guild and member data." + }); + return; + } + + if ( + !(member as GuildMember).permissions.has(PermissionFlagsBits.BanMembers) + ) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const reason = interaction.options.getString("reason", true); + const evidence = + interaction.options.getString("evidence")?.split(/\s+/) || []; + const prune = interaction.options.getInteger("prune", true); + const user = interaction.options.getUser("user", true); + const target = + guild.members.cache.get(user.id) || + (await guild.members.fetch(user.id).catch(() => null)); + + if (target && isModerator(target)) { + await interaction.editReply({ + content: "You cannot ban a moderator." + }); + return; + } + await processModAction( + bot, + interaction, + guild, + user, + "softban", + reason, + evidence, + undefined, + undefined, + prune + ); + } catch (err) { + const id = await errorHandler(bot, "softban command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/unban.ts b/src/commands/unban.ts new file mode 100644 index 0000000..f0dee46 --- /dev/null +++ b/src/commands/unban.ts @@ -0,0 +1,87 @@ +import { + GuildMember, + PermissionFlagsBits, + SlashCommandBuilder +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; +import { processModAction } from "../utils/processModAction"; + +export const unban: Command = { + data: new SlashCommandBuilder() + .setName("unban") + .setDescription("Unban a user from the server.") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to unban.") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("The reason for unbanning.") + .setRequired(true) + .setMinLength(1) + .setMaxLength(400) + ) + .addStringOption((option) => + option + .setName("evidence") + .setDescription( + "A link to the evidence for the unban. For multiple links, separate with a space." + ) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + + if (!member || !guild) { + await interaction.editReply({ + content: "There was an error loading the guild and member data." + }); + return; + } + + if ( + !(member as GuildMember).permissions.has(PermissionFlagsBits.BanMembers) + ) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const reason = interaction.options.getString("reason", true); + const evidence = + interaction.options.getString("evidence")?.split(/\s+/) || []; + const user = interaction.options.getUser("user", true); + + const isBanned = await guild.bans.fetch(user.id).catch(() => false); + + if (!isBanned) { + await interaction.editReply({ + content: `ID ${user.id} is not banned.` + }); + return; + } + + await processModAction( + bot, + interaction, + guild, + user, + "unban", + reason, + evidence + ); + } catch (err) { + const id = await errorHandler(bot, "unban command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/unlock.ts b/src/commands/unlock.ts new file mode 100644 index 0000000..389bca0 --- /dev/null +++ b/src/commands/unlock.ts @@ -0,0 +1,96 @@ +import { + ChannelType, + EmbedBuilder, + GuildMember, + PermissionFlagsBits, + SlashCommandBuilder, + TextChannel +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { getConfig } from "../modules/data/getConfig"; +import { errorHandler } from "../utils/errorHandler"; + +export const unlock: Command = { + data: new SlashCommandBuilder() + .setName("unlock") + .setDescription("Remove lock down from a channel.") + .addChannelOption((option) => + option + .setName("channel") + .setDescription("The channel to unlock.") + .setRequired(true) + .addChannelTypes(ChannelType.GuildText) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + + if (!member || !guild) { + await interaction.editReply({ + content: "You must be in a guild to use this command." + }); + return; + } + + if ( + !(member as GuildMember).permissions.has( + PermissionFlagsBits.ManageChannels + ) + ) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const channel = interaction.options.getChannel( + "channel", + true + ) as TextChannel; + + if (!("send" in channel)) { + await interaction.editReply({ + content: "You must use this command to target a text based channel." + }); + return; + } + + await channel.permissionOverwrites.edit( + guild.id, + { + SendMessages: null + }, + { reason: `Lockdown Removed by ${interaction.user.tag}` } + ); + + const config = await getConfig(bot, guild.id); + if (!config.modLogChannel) { + return; + } + const logChannel = + guild.channels.cache.get(config.modLogChannel) || + (await guild.channels.fetch(config.modLogChannel)); + + if (!logChannel || !("send" in logChannel)) { + return; + } + const embed = new EmbedBuilder(); + embed.setTitle("Channel Unlocked"); + embed.setDescription(`The <#${channel.id}> channel has been unlocked.`); + embed.setAuthor({ + name: interaction.user.tag, + iconURL: interaction.user.displayAvatarURL() + }); + await logChannel.send({ embeds: [embed] }); + + await interaction.editReply({ content: "Channel has been unlocked!" }); + } catch (err) { + const id = await errorHandler(bot, "unlock command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/unmute.ts b/src/commands/unmute.ts new file mode 100644 index 0000000..b26877a --- /dev/null +++ b/src/commands/unmute.ts @@ -0,0 +1,106 @@ +import { + GuildMember, + PermissionFlagsBits, + SlashCommandBuilder +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; +import { isModerator } from "../utils/isModerator"; +import { processModAction } from "../utils/processModAction"; + +export const unmute: Command = { + data: new SlashCommandBuilder() + .setName("unmute") + .setDescription("Unmute a member") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to unmute.") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("The reason for the unmute.") + .setRequired(true) + .setMinLength(1) + .setMaxLength(400) + ) + .addStringOption((option) => + option + .setName("evidence") + .setDescription( + "A link to the evidence for the unmute. For multiple links, separate with a space." + ) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + + if (!member || !guild) { + await interaction.editReply({ + content: "There was an error loading the guild and member data." + }); + return; + } + + if ( + !(member as GuildMember).permissions.has( + PermissionFlagsBits.ModerateMembers + ) + ) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const user = interaction.options.getUser("user", true); + const target = + guild.members.cache.get(user.id) || + (await guild.members.fetch(user.id).catch(() => null)); + + if (!target) { + await interaction.editReply({ + content: "That member appears to have left the server." + }); + return; + } + + if (!target.isCommunicationDisabled()) { + await interaction.editReply({ + content: "That member is not muted." + }); + return; + } + + if (isModerator(target)) { + await interaction.editReply({ + content: + "A moderator should never be muted. How on earth did you achieve this???" + }); + } + + const reason = interaction.options.getString("reason", true); + const evidence = + interaction.options.getString("evidence")?.split(/\s+/) || []; + + await processModAction( + bot, + interaction, + guild, + user, + "unmute", + reason, + evidence + ); + } catch (err) { + const id = await errorHandler(bot, "unmute command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/commands/warn.ts b/src/commands/warn.ts new file mode 100644 index 0000000..906fc1d --- /dev/null +++ b/src/commands/warn.ts @@ -0,0 +1,99 @@ +import { + GuildMember, + PermissionFlagsBits, + SlashCommandBuilder +} from "discord.js"; + +import { Command } from "../interfaces/Command"; +import { errorHandler } from "../utils/errorHandler"; +import { isModerator } from "../utils/isModerator"; +import { processModAction } from "../utils/processModAction"; + +export const warn: Command = { + data: new SlashCommandBuilder() + .setName("warn") + .setDescription("Warn a member") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to warn.") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("The reason for the warning.") + .setRequired(true) + .setMinLength(1) + .setMaxLength(400) + ) + .addStringOption((option) => + option + .setName("evidence") + .setDescription( + "A link to the evidence for the warning. For multiple links, separate with a space." + ) + ), + run: async (bot, interaction) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { member, guild } = interaction; + + if (!member || !guild) { + await interaction.editReply({ + content: "There was an error loading the guild and member data." + }); + return; + } + + if ( + !(member as GuildMember).permissions.has( + PermissionFlagsBits.KickMembers + ) + ) { + await interaction.editReply({ + content: "You do not have permission to run this command." + }); + return; + } + + const user = interaction.options.getUser("user", true); + const target = + guild.members.cache.get(user.id) || + (await guild.members.fetch(user.id).catch(() => null)); + + if (!target) { + await interaction.editReply({ + content: "That member appears to have left the server." + }); + return; + } + + if (isModerator(target)) { + await interaction.editReply({ + content: "You cannot warn a moderator." + }); + return; + } + + const reason = interaction.options.getString("reason", true); + const evidence = + interaction.options.getString("evidence")?.split(/\s+/) || []; + + await processModAction( + bot, + interaction, + guild, + user, + "warn", + reason, + evidence + ); + } catch (err) { + const id = await errorHandler(bot, "warn command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/config/DefaultConfig.ts b/src/config/DefaultConfig.ts new file mode 100644 index 0000000..0644b19 --- /dev/null +++ b/src/config/DefaultConfig.ts @@ -0,0 +1,10 @@ +import { configs } from "@prisma/client"; + +export const defaultConfig: Omit = { + serverId: "", + inviteLink: "", + banAppealLink: "", + modLogChannel: "", + eventLogChannel: "", + messageReportChannel: "" +}; diff --git a/src/config/EmbedColours.ts b/src/config/EmbedColours.ts new file mode 100644 index 0000000..821f0a3 --- /dev/null +++ b/src/config/EmbedColours.ts @@ -0,0 +1,12 @@ +import { Action } from "../interfaces/Action"; + +export const EmbedColours: { [K in Action]: number } = { + ban: 0xfa000c, + softban: 0xfa9900, + kick: 0xffee00, + warn: 0x2600ff, + mute: 0xd900ff, + unmute: 0x00ff22, + unban: 0x00ff22, + note: 0x000001 +}; diff --git a/src/config/Github.ts b/src/config/Github.ts new file mode 100644 index 0000000..f9eaa6d --- /dev/null +++ b/src/config/Github.ts @@ -0,0 +1,17 @@ +export const IgnoredActors = [ + "renovate[bot]", + "codeclimate[bot]", + "dependabot[bot]", + "lgtm-com[bot]", + "deepsource-autofix[bot]", + "sonarcloud[bot]", + "melody-iuvo" +]; + +export const ThankYou = `## Thank You + + Thank you for your contribution to our project. We have reviewed your pull request and are happy to accept these changes. + + Please continue to watch for issues labelled \`help wanted\`, as these will be additional opportunities to contribute. + + You can also see all open issues [through our contributor tool](https://contribute.naomi.lgbt). Also, feel free to join our [Discord server](https://chat.naomi.lgbt) to chat with us and get notified when new issues are available!`; diff --git a/src/config/IntentOptions.ts b/src/config/IntentOptions.ts new file mode 100644 index 0000000..db0aee5 --- /dev/null +++ b/src/config/IntentOptions.ts @@ -0,0 +1,10 @@ +import { GatewayIntentBits } from "discord.js"; + +export const IntentOptions = [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildModeration, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMessageReactions +]; diff --git a/src/config/LevelScale.ts b/src/config/LevelScale.ts new file mode 100644 index 0000000..63fd16b --- /dev/null +++ b/src/config/LevelScale.ts @@ -0,0 +1,14 @@ +/** + * This config is an automatically-generated scale for mapping experience + * point values to level values. + */ +const levelScale: number[] = []; + +let j = 0; + +for (let i = 0; i <= 1000; i++) { + j += i * 1000; + levelScale[i] = j; +} + +export default levelScale; diff --git a/src/config/LogChannelChoices.ts b/src/config/LogChannelChoices.ts new file mode 100644 index 0000000..390b100 --- /dev/null +++ b/src/config/LogChannelChoices.ts @@ -0,0 +1,15 @@ +import { configs } from "@prisma/client"; + +export const logChannelChoices: { name: string; value: keyof configs }[] = [ + { name: "Moderation Action Log Channel", value: "modLogChannel" }, + { name: "Private Event Log Channel", value: "eventLogChannel" }, + { name: "Message Reporting Channel", value: "messageReportChannel" } +]; + +export const logChannelChoicesMap: { + [key: string]: string; +} = { + modLogChannel: "moderation actions", + eventLogChannel: "gateway events", + messageReportChannel: "message reports" +}; diff --git a/src/config/ServerUploadLimits.ts b/src/config/ServerUploadLimits.ts new file mode 100644 index 0000000..34841d2 --- /dev/null +++ b/src/config/ServerUploadLimits.ts @@ -0,0 +1,8 @@ +import { GuildPremiumTier } from "discord.js"; + +export const ServerUploadLimits: { [tier in GuildPremiumTier]: number } = { + 0: 8000000, + 1: 8000000, + 2: 50000000, + 3: 100000000 +}; diff --git a/src/contexts/report.ts b/src/contexts/report.ts new file mode 100644 index 0000000..504cc31 --- /dev/null +++ b/src/contexts/report.ts @@ -0,0 +1,126 @@ +import { + Message, + ActionRowBuilder, + ButtonBuilder, + EmbedBuilder, + ButtonStyle, + TextInputBuilder, + TextInputStyle, + ModalBuilder +} from "discord.js"; + +import { EmbedColours } from "../config/EmbedColours"; +import { Context } from "../interfaces/Context"; +import { getConfig } from "../modules/data/getConfig"; +import { errorHandler } from "../utils/errorHandler"; + +export const report: Context = { + data: { + name: "report", + type: 3 + }, + run: async (bot, interaction) => { + try { + if (!interaction.isMessageContextMenuCommand()) { + await interaction.reply({ + content: + "This command is improperly configured. Please contact Naomi.", + ephemeral: true + }); + return; + } + const guild = interaction.guild; + const message = interaction.options.getMessage( + "message", + true + ) as Message; + + if (!guild || !message) { + await interaction.reply({ + content: "Could not find the guild record...", + ephemeral: true + }); + return; + } + + const config = await getConfig(bot, guild.id); + + if (!config.messageReportChannel) { + await interaction.reply({ + content: "Reporting has not been set up for this server.", + ephemeral: true + }); + return; + } + + const channel = + guild.channels.cache.get(config.messageReportChannel) || + (await guild.channels.fetch(config.messageReportChannel)); + + if (!channel || !("send" in channel)) { + await interaction.reply({ + content: "Reporting channel not found.", + ephemeral: true + }); + return; + } + + const embed = new EmbedBuilder() + .setTitle("Message Reported") + .setDescription(message.content) + .setColor(EmbedColours.ban) + .addFields( + { + name: "Author", + value: `<@${message.author.id}>`, + inline: true + }, + { + name: "Reporter", + value: `<@${interaction.user.id}>`, + inline: true + } + ); + + const link = new ButtonBuilder() + .setLabel("Message Link") + .setStyle(ButtonStyle.Link) + .setURL(message.url); + const button = new ButtonBuilder() + .setLabel("Acknowledge") + .setStyle(ButtonStyle.Primary) + .setCustomId(`ack-${message.id}`); + + const row = new ActionRowBuilder().addComponents([ + link, + button + ]); + + const reportLog = await channel.send({ + embeds: [embed], + components: [row] + }); + + const reportModal = new ModalBuilder() + .setCustomId(`rep-${reportLog.id}`) + .setTitle("Report Message"); + const reasonInput = new TextInputBuilder() + .setCustomId("reason") + .setLabel("Why are you reporting this message?") + .setStyle(TextInputStyle.Paragraph) + .setRequired(true); + const actionRow = new ActionRowBuilder().addComponents( + reasonInput + ); + reportModal.addComponents(actionRow); + + await interaction.showModal(reportModal); + } catch (err) { + const id = await errorHandler(bot, "report context command", err); + await interaction.reply({ + ephemeral: true, + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/database/connectDatabase.ts b/src/database/connectDatabase.ts new file mode 100644 index 0000000..2cbaa02 --- /dev/null +++ b/src/database/connectDatabase.ts @@ -0,0 +1,20 @@ +import { PrismaClient } from "@prisma/client"; + +import { ExtendedClient } from "../interfaces/ExtendedClient"; +import { errorHandler } from "../utils/errorHandler"; +import { sendDebugMessage } from "../utils/sendDebugMessage"; + +/** + * Connects to the database. + * + * @param {ExtendedClient} bot The bot's Discord instance. + */ +export const connectDatabase = async (bot: ExtendedClient) => { + try { + bot.db = new PrismaClient(); + await bot.db.$connect(); + await sendDebugMessage(bot, "Connected to database."); + } catch (err) { + await errorHandler(bot, "connect database", err); + } +}; diff --git a/src/events/_handleEvents.ts b/src/events/_handleEvents.ts new file mode 100644 index 0000000..b4d6bba --- /dev/null +++ b/src/events/_handleEvents.ts @@ -0,0 +1,129 @@ +import { ExtendedClient } from "../interfaces/ExtendedClient"; +import { checkEntitledGuild } from "../utils/checkEntitledGuild"; + +import { onDisconnect } from "./client/onDisconnect"; +import { onReady } from "./client/onReady"; +import { onAuditLogEntry } from "./guild/onAuditLogEntry"; +import { onGuildCreate } from "./guild/onGuildCreate"; +import { onGuildDelete } from "./guild/onGuildDelete"; +import { onInteraction } from "./interaction/onInteraction"; +import { onMemberAdd } from "./member/onMemberAdd"; +import { onMemberRemove } from "./member/onMemberRemove"; +import { onMemberUpdate } from "./member/onMemberUpdate"; +import { onMessage } from "./message/onMessage"; +import { onMessageDelete } from "./message/onMessageDelete"; +import { onMessageEdit } from "./message/onMessageEdit"; +import { onThreadCreate } from "./thread/onThreadCreate"; +import { onThreadDelete } from "./thread/onThreadDelete"; +import { onThreadUpdate } from "./thread/onThreadUpdate"; +import { onVoiceUpdate } from "./voice/onVoiceUpdate"; + +/** + * Module to mount the Discord event listeners. + * + * @param {ExtendedClient} bot The bot's Discord instance. + */ +export const handleEvents = (bot: ExtendedClient) => { + /* Client Events */ + bot.on("ready", async () => await onReady(bot)); + bot.on("disconnect", () => onDisconnect(bot)); + + /* Message Events */ + bot.on("messageCreate", async (message) => { + if (!message.guild || !(await checkEntitledGuild(bot, message.guild))) { + return; + } + await onMessage(bot, message); + }); + bot.on("messageDelete", async (message) => { + if (!message.guild || !(await checkEntitledGuild(bot, message.guild))) { + return; + } + await onMessageDelete(bot, message); + }); + bot.on("messageUpdate", async (oldMessage, newMessage) => { + if ( + !newMessage.guild || + !(await checkEntitledGuild(bot, newMessage.guild)) + ) { + return; + } + await onMessageEdit(bot, oldMessage, newMessage); + }); + + /* Interaction Events */ + bot.on( + "interactionCreate", + async (interaction) => await onInteraction(bot, interaction) + ); + + /* Thread Events */ + bot.on("threadCreate", async (thread) => { + if (!thread.guild || !(await checkEntitledGuild(bot, thread.guild))) { + return; + } + await onThreadCreate(bot, thread); + }); + bot.on("threadDelete", async (thread) => { + if (!thread.guild || !(await checkEntitledGuild(bot, thread.guild))) { + return; + } + await onThreadDelete(bot, thread); + }); + bot.on("threadUpdate", async (oldThread, newThread) => { + if (!newThread.guild || !(await checkEntitledGuild(bot, newThread.guild))) { + return; + } + await onThreadUpdate(bot, oldThread, newThread); + }); + + /* Voice Events */ + bot.on("voiceStateUpdate", async (oldVoice, newVoice) => { + if (!newVoice.guild || !(await checkEntitledGuild(bot, newVoice.guild))) { + return; + } + await onVoiceUpdate(bot, oldVoice, newVoice); + }); + + /* Member Events */ + bot.on("guildMemberAdd", async (member) => { + if (!member.guild || !(await checkEntitledGuild(bot, member.guild))) { + return; + } + await onMemberAdd(bot, member); + }); + bot.on("guildMemberRemove", async (member) => { + if (!member.guild || !(await checkEntitledGuild(bot, member.guild))) { + return; + } + await onMemberRemove(bot, member); + }); + bot.on("guildMemberUpdate", async (oldMember, newMember) => { + if (!newMember.guild || !(await checkEntitledGuild(bot, newMember.guild))) { + return; + } + await onMemberUpdate(bot, oldMember, newMember); + }); + + /* Guild Events */ + bot.on("guildAuditLogEntryCreate", async (log, guild) => { + if (!guild || !(await checkEntitledGuild(bot, guild))) { + return; + } + await onAuditLogEntry(bot, log, guild); + }); + + bot.on("guildCreate", async (guild) => { + if (!guild || !(await checkEntitledGuild(bot, guild))) { + return; + } + await onGuildCreate(bot, guild); + }); + + bot.on("guildDelete", async (guild) => { + if (!guild) { + return; + } + await onGuildDelete(bot, guild); + }); +}; diff --git a/src/events/client/onDisconnect.ts b/src/events/client/onDisconnect.ts new file mode 100644 index 0000000..b174a8a --- /dev/null +++ b/src/events/client/onDisconnect.ts @@ -0,0 +1,16 @@ +import { EmbedBuilder } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; + +/** + * Sends a message to the debug hook when the bot disconnects. + * + * @param {ExtendedClient} bot The bot's Discord instance. + */ +export const onDisconnect = async (bot: ExtendedClient) => { + const disconnectEmbed = new EmbedBuilder(); + disconnectEmbed.setTitle("Disconnected"); + disconnectEmbed.setDescription("I have been disconnected from Discord."); + disconnectEmbed.setTimestamp(); + await bot.env.debugHook.send({ embeds: [disconnectEmbed] }); +}; diff --git a/src/events/client/onReady.ts b/src/events/client/onReady.ts new file mode 100644 index 0000000..a92e418 --- /dev/null +++ b/src/events/client/onReady.ts @@ -0,0 +1,13 @@ +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { registerCommands } from "../../utils/registerCommands"; +import { sendDebugMessage } from "../../utils/sendDebugMessage"; + +/** + * Handles the `ready` from Discord. + * + * @param {ExtendedClient} bot The bot's Discord instance. + */ +export const onReady = async (bot: ExtendedClient) => { + await sendDebugMessage(bot, `Logged in as ${bot.user?.tag}`); + await registerCommands(bot); +}; diff --git a/src/events/guild/onAuditLogEntry.ts b/src/events/guild/onAuditLogEntry.ts new file mode 100644 index 0000000..d219490 --- /dev/null +++ b/src/events/guild/onAuditLogEntry.ts @@ -0,0 +1,78 @@ +import { AuditLogEvent, Guild, GuildAuditLogsEntry, User } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { getModActionFromAuditLog } from "../../modules/events/getModActionFromAuditLog"; +import { addCase } from "../../utils/addCase"; +import { errorHandler } from "../../utils/errorHandler"; +import { sendLogMessage } from "../../utils/sendLogMessage"; + +/** + * Handles properly logging a manual mod action based on audit logs. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {GuildAuditLogsEntry} log The audit log payload from Discord. + * @param {Guild} guild The guild payload from Discord. + */ +export const onAuditLogEntry = async ( + bot: ExtendedClient, + log: GuildAuditLogsEntry, + guild: Guild +) => { + try { + const { action, changes, executorId, targetId, target, reason } = log; + if (executorId === bot.user?.id) { + return; + } + // if not a mod action we don't care. + if ( + ![ + AuditLogEvent.MemberBanAdd, + AuditLogEvent.MemberBanRemove, + AuditLogEvent.MemberKick, + AuditLogEvent.MemberUpdate + ].includes(action) || + (action === AuditLogEvent.MemberUpdate && + !changes.find( + (change) => change.key === "communication_disabled_until" + )) || + !targetId || + !(target instanceof User) || + !executorId + ) { + return; + } + + const modAction = getModActionFromAuditLog(log); + + if (!modAction) { + return; + } + + const reasonString = `This was a manual action pulled from the audit log. Please use the bot for accurate reporting.\n\nReason: ${ + reason || "Unable to parse reason." + }`; + + const caseNum = await addCase( + bot, + guild.id, + target.id, + reasonString, + modAction, + executorId, + [] + ); + await sendLogMessage( + bot, + guild, + target, + modAction, + reasonString, + executorId, + [], + false, + caseNum + ); + } catch (err) { + await errorHandler(bot, "on audit log entry", err); + } +}; diff --git a/src/events/guild/onGuildCreate.ts b/src/events/guild/onGuildCreate.ts new file mode 100644 index 0000000..6db0724 --- /dev/null +++ b/src/events/guild/onGuildCreate.ts @@ -0,0 +1,18 @@ +import { Guild } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +/** + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {Guild} guild The newly joined Discord guild. + */ +export const onGuildCreate = async function ( + bot: ExtendedClient, + guild: Guild +) { + const owner = await guild.fetchOwner(); + + await bot.env.debugHook.send({ + content: `JOINED GUILD: ${guild.name} (${guild.id}) - owned by ${owner?.displayName} (${owner.id})` + }); +}; diff --git a/src/events/guild/onGuildDelete.ts b/src/events/guild/onGuildDelete.ts new file mode 100644 index 0000000..191c319 --- /dev/null +++ b/src/events/guild/onGuildDelete.ts @@ -0,0 +1,16 @@ +import { Guild } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +/** + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {Guild} guild The newly left Discord guild. + */ +export const onGuildDelete = async function ( + bot: ExtendedClient, + guild: Guild +) { + await bot.env.debugHook.send({ + content: `LEFT GUILD: ${guild.name} (${guild.id}) ` + }); +}; diff --git a/src/events/interaction/onInteraction.ts b/src/events/interaction/onInteraction.ts new file mode 100644 index 0000000..d5c1b1d --- /dev/null +++ b/src/events/interaction/onInteraction.ts @@ -0,0 +1,71 @@ +import { Interaction } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { handleCopyIdButton } from "../../modules/buttons/handleCopyIdButton"; +import { handleReportAcknowledgeButton } from "../../modules/buttons/handleReportAcknowledgeButton"; +import { handleChatInputCommand } from "../../modules/interactions/handleChatInputCommand"; +import { handleContextMenuCommand } from "../../modules/interactions/handleContextMenuCommand"; +import { handleMassBanModal } from "../../modules/modals/handleMassBanModal"; +import { handleMessageReportModal } from "../../modules/modals/handleMessageReportModal"; +import { checkEntitledGuild } from "../../utils/checkEntitledGuild"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Handles interactions. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {Interaction} interaction The interaction payload from Discord. + */ +export const onInteraction = async ( + bot: ExtendedClient, + interaction: Interaction +) => { + try { + if (interaction.isAutocomplete()) { + return; + } + if ( + !interaction.guild || + (!(await checkEntitledGuild(bot, interaction.guild)) && + (!interaction.isChatInputCommand() || + interaction.commandName !== "help")) + ) { + await interaction.reply( + "Your guild does not appear to be subscribed to use our bot. Please reach out to us in our [support server]() if you would like to sign up." + ); + return; + } + if (interaction.isChatInputCommand()) { + handleChatInputCommand(bot, interaction); + } + + if (interaction.isContextMenuCommand()) { + handleContextMenuCommand(bot, interaction); + } + + if (interaction.isButton()) { + if (interaction.customId.startsWith("ack")) { + await handleReportAcknowledgeButton(bot, interaction); + } + if (interaction.customId.startsWith("copyid")) { + await handleCopyIdButton(bot, interaction); + } + } + + if (interaction.isModalSubmit()) { + if (interaction.customId === "mass-ban-modal") { + await handleMassBanModal(bot, interaction); + } + if (interaction.customId.startsWith("rep")) { + await handleMessageReportModal(bot, interaction); + } + } + } catch (err) { + const id = await errorHandler(bot, "on interaction", err); + if (!interaction.isAutocomplete()) { + await interaction.reply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } + } +}; diff --git a/src/events/member/onMemberAdd.ts b/src/events/member/onMemberAdd.ts new file mode 100644 index 0000000..e94fc54 --- /dev/null +++ b/src/events/member/onMemberAdd.ts @@ -0,0 +1,38 @@ +import { GuildMember } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { getConfig } from "../../modules/data/getConfig"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Sends a log message to the configured log channel when a member + * joins the server. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {GuildMember} member The user's Discord instance. + */ +export const onMemberAdd = async (bot: ExtendedClient, member: GuildMember) => { + try { + const { user, guild } = member; + + const config = await getConfig(bot, guild.id); + + if (!config.eventLogChannel) { + return; + } + + const channel = + guild.channels.cache.get(config.eventLogChannel) || + (await guild.channels.fetch(config.eventLogChannel)); + + if (!channel || !("send" in channel)) { + return; + } + + await channel.send({ + content: `${user.tag} (${user.id}) has joined the server. Total Members: ${guild.memberCount}` + }); + } catch (err) { + await errorHandler(bot, "on member add", err); + } +}; diff --git a/src/events/member/onMemberRemove.ts b/src/events/member/onMemberRemove.ts new file mode 100644 index 0000000..5799536 --- /dev/null +++ b/src/events/member/onMemberRemove.ts @@ -0,0 +1,45 @@ +import { GuildMember, PartialGuildMember } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { getConfig } from "../../modules/data/getConfig"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Sends a log message to the configured log channel when a member + * leaves the server. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {GuildMember} member The user's Discord instance. + */ +export const onMemberRemove = async ( + bot: ExtendedClient, + member: GuildMember | PartialGuildMember +) => { + try { + const { user, guild } = member; + + const config = await getConfig(bot, guild.id); + + if (!config.eventLogChannel) { + return; + } + + const channel = + guild.channels.cache.get(config.eventLogChannel) || + (await guild.channels.fetch(config.eventLogChannel)); + + if (!channel || !("send" in channel)) { + return; + } + + const joinStamp = member.joinedTimestamp + ? `` + : "unknown"; + + await channel.send({ + content: `${user.tag} (${user.id}) has left the server (joined at ${joinStamp}). Total Members: ${guild.memberCount}` + }); + } catch (err) { + await errorHandler(bot, "on member remove", err); + } +}; diff --git a/src/events/member/onMemberUpdate.ts b/src/events/member/onMemberUpdate.ts new file mode 100644 index 0000000..8c87c22 --- /dev/null +++ b/src/events/member/onMemberUpdate.ts @@ -0,0 +1,80 @@ +import { GuildMember, PartialGuildMember } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { getConfig } from "../../modules/data/getConfig"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Sends a log message to the configured log channel when a member's + * data is updated. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {GuildMember} oldMember The user's old Discord instance. + * @param {GuildMember} newMember The user's new Discord instance. + */ +export const onMemberUpdate = async ( + bot: ExtendedClient, + oldMember: GuildMember | PartialGuildMember, + newMember: GuildMember +) => { + try { + const { user, guild } = newMember; + + const config = await getConfig(bot, guild.id); + + if (!config.eventLogChannel) { + return; + } + + const channel = + guild.channels.cache.get(config.eventLogChannel) || + (await guild.channels.fetch(config.eventLogChannel)); + + if (!channel || !("send" in channel)) { + return; + } + + if (oldMember.user.tag !== newMember.user.tag) { + await channel.send({ + content: `${user.tag} (${user.id}) has changed their name from ${oldMember.user.tag} to ${newMember.user.tag}` + }); + } + + if (oldMember.nickname !== newMember.nickname) { + await channel.send({ + content: `${user.tag} (${user.id}) has changed their nickname from ${ + oldMember.nickname || "**none**" + } to ${newMember.nickname || "**none**"}` + }); + } + + const removedRoles = oldMember.roles.cache.filter( + (role) => !newMember.roles.cache.has(role.id) + ); + const addedRoles = newMember.roles.cache.filter( + (role) => !oldMember.roles.cache.has(role.id) + ); + + if (removedRoles.size > 0) { + await channel.send({ + content: `${user.tag} (${ + user.id + }) has removed the following roles: ${removedRoles.map( + (role) => role.name + )}` + }); + } + + if (addedRoles.size > 0) { + await channel.send({ + content: `${user.tag} (${ + user.id + }) has added the following roles: ${addedRoles.map( + (role) => role.name + )}` + }); + } + } catch (err) { + await errorHandler(bot, "on member update", err); + } +}; diff --git a/src/events/message/onMessage.ts b/src/events/message/onMessage.ts new file mode 100644 index 0000000..34c2c69 --- /dev/null +++ b/src/events/message/onMessage.ts @@ -0,0 +1,141 @@ +import { Message } from "discord.js"; + +import levelScale from "../../config/LevelScale"; +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { calculateMuteDuration } from "../../modules/commands/calculateMuteDuration"; +import { checkSpamDomain } from "../../modules/events/checkSpamDomain"; +import { addCase } from "../../utils/addCase"; +import { errorHandler } from "../../utils/errorHandler"; +import { sendLogMessage } from "../../utils/sendLogMessage"; +import { sendModDm } from "../../utils/sendModDm"; +import { triggerModRequest } from "../../utils/triggerModRequest"; + +const linkRegex = /https?:\/\/([a-zA-Z0-9_.-]{2,256}\.\w{2,24}\b)/g; + +/** + * Module to handle the messageCreate event from Discord. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {Message} message The message payload from Discord. + */ +export const onMessage = async (bot: ExtendedClient, message: Message) => { + try { + const { guild, member, author, system } = message; + if (!guild || !member || system || author.bot) { + return; + } + + const links = message.content.match(linkRegex); + + if (links) { + for (const link of links) { + if (await checkSpamDomain(bot, link.replace(/https?:\/\//, ""))) { + await message.delete().catch(() => null); + const notified = await sendModDm( + bot, + "mute", + author, + guild, + "Your account appears to be compromised." + ); + const caseNum = await addCase( + bot, + guild.id, + author.id, + "Your account appears to be compromised", + "mute", + "Automoderator", + [link] + ); + await sendLogMessage( + bot, + guild, + author, + "mute", + "Your account appears to be compromised", + "Automoderation", + [link], + notified, + caseNum + ); + await triggerModRequest(bot, { + userId: author.id, + serverId: guild.id, + action: "mute", + reason: "Your account appears to be compromised", + moderator: "Automoderator", + duration: calculateMuteDuration(24, "hours"), + pruneDays: 0 + }); + return; + } + } + } + + const bonus = Math.floor(message.content.length / 10); + const pointsEarned = Math.floor(Math.random() * (20 + bonus)) + 5; + const user = await bot.db.levels.upsert({ + where: { + serverId_userId: { + serverId: guild.id, + userId: author.id + } + }, + update: {}, + create: { + serverId: guild.id, + userId: author.id, + username: author.username, + avatar: author.displayAvatarURL(), + points: 0, + level: 0 + } + }); + + if (Date.now() - user.cooldown.getTime() < 60000 || user.level >= 1000) { + return; + } + user.points += pointsEarned; + user.cooldown = new Date(); + let levelUp = false; + + while (user.points > (levelScale[user.level + 1] ?? Infinity)) { + user.level++; + levelUp = true; + } + + await bot.db.levels.update({ + where: { + serverId_userId: { + serverId: guild.id, + userId: author.id + } + }, + data: { + points: user.points, + level: user.level, + username: author.username, + avatar: author.displayAvatarURL(), + cooldown: user.cooldown + } + }); + + if (levelUp) { + await message.reply(`Congrats! You're now level ${user.level}!!`); + } + + const levelRoles = await bot.db.levelRoles.findMany({ + where: { + serverId: guild.id, + level: { + lte: user.level + } + } + }); + for (const record of levelRoles) { + await member.roles.add(record.roleId).catch(() => null); + } + } catch (err) { + await errorHandler(bot, "on message", err); + } +}; diff --git a/src/events/message/onMessageDelete.ts b/src/events/message/onMessageDelete.ts new file mode 100644 index 0000000..c1277e6 --- /dev/null +++ b/src/events/message/onMessageDelete.ts @@ -0,0 +1,73 @@ +import { Message, PartialMessage } from "discord.js"; + +import { ServerUploadLimits } from "../../config/ServerUploadLimits"; +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { getConfig } from "../../modules/data/getConfig"; +import { customSubstring } from "../../utils/customSubstring"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Handles a message delete event. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {Message} message The message that was deleted. + */ +export const onMessageDelete = async ( + bot: ExtendedClient, + message: Message | PartialMessage +) => { + try { + const { author, channel, content, guild, embeds, attachments, stickers } = + message; + + if (!guild || author?.bot) { + return; + } + + const config = await getConfig(bot, guild.id); + + if (!config.eventLogChannel) { + return; + } + + const logChannel = + guild.channels.cache.get(config.eventLogChannel) || + (await guild.channels.fetch(config.eventLogChannel)); + + if (!logChannel || !("send" in logChannel)) { + return; + } + + const deletedContent = content || "**No message content found.**"; + const mappedAttachements = attachments + .map((el) => el) + .filter((el) => el.size <= ServerUploadLimits[guild.premiumTier]); + const mappedStickers = stickers + .map((el) => el) + .filter((el) => el.available); + + let logContent = `${author?.tag} (${author?.id}) had a message (${message.id}) deleted in <#${channel.id}>:\n\n\`${deletedContent}\``; + + if (message.reference && message.reference.messageId) { + logContent += `\n\n**This message was in reply to: https://discord.com/channels/${guild.id}/${message.reference.channelId}/${message.reference.messageId}**`; + } + + if (attachments.size && mappedAttachements.length < attachments.size) { + logContent += `\n\n**${ + attachments.size - mappedAttachements.length + } attachment(s) were too large to log.**`; + } + + await logChannel.send({ + content: customSubstring(logContent, 2000), + files: mappedAttachements, + embeds, + stickers: mappedStickers, + allowedMentions: { + parse: [] + } + }); + } catch (err) { + await errorHandler(bot, "on message delete", err); + } +}; diff --git a/src/events/message/onMessageEdit.ts b/src/events/message/onMessageEdit.ts new file mode 100644 index 0000000..9872126 --- /dev/null +++ b/src/events/message/onMessageEdit.ts @@ -0,0 +1,64 @@ +import { Message, PartialMessage } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { getConfig } from "../../modules/data/getConfig"; +import { customSubstring } from "../../utils/customSubstring"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Handles a message edit event. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {Message} oldMessage The old message payload. + * @param {Message} newMessage The new message payload. + */ +export const onMessageEdit = async ( + bot: ExtendedClient, + oldMessage: Message | PartialMessage, + newMessage: Message | PartialMessage +) => { + try { + const { author, channel, guild } = newMessage; + + if (!guild || author?.bot) { + return; + } + + if ( + !oldMessage.content || + !newMessage.content || + oldMessage.content === newMessage.content + ) { + return; + } + + const config = await getConfig(bot, guild.id); + + if (!config.eventLogChannel) { + return; + } + + const logChannel = + guild.channels.cache.get(config.eventLogChannel) || + (await guild.channels.fetch(config.eventLogChannel)); + + if (!logChannel || !("send" in logChannel)) { + return; + } + + await logChannel.send({ + content: `${author?.tag} (${author?.id}) edited their message in in <#${ + channel.id + }>:\n\n**Old Content:**\n\`${customSubstring( + oldMessage.content, + 1750 + )}\`\n\n**New content:**\n\`${customSubstring( + newMessage.content, + 1750 + )}\`\n\n${newMessage.url}`, + allowedMentions: { parse: [] } + }); + } catch (err) { + await errorHandler(bot, "on message edit", err); + } +}; diff --git a/src/events/thread/onThreadCreate.ts b/src/events/thread/onThreadCreate.ts new file mode 100644 index 0000000..5676955 --- /dev/null +++ b/src/events/thread/onThreadCreate.ts @@ -0,0 +1,42 @@ +import { ThreadChannel } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { getConfig } from "../../modules/data/getConfig"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Handles the creation of a new thread. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {ThreadChannel} thread The thread payload from Discord. + */ +export const onThreadCreate = async ( + bot: ExtendedClient, + thread: ThreadChannel +) => { + try { + if (thread.joinable) { + await thread.join(); + } + + const config = await getConfig(bot, thread.guild.id); + + if (!config.eventLogChannel) { + return; + } + + const channel = + thread.guild.channels.cache.get(config.eventLogChannel) || + (await thread.guild.channels.fetch(config.eventLogChannel)); + + if (!channel || !("send" in channel)) { + return; + } + + await channel.send({ + content: `${thread.name} has been created in <#${thread.parentId}>` + }); + } catch (err) { + await errorHandler(bot, "on thread create", err); + } +}; diff --git a/src/events/thread/onThreadDelete.ts b/src/events/thread/onThreadDelete.ts new file mode 100644 index 0000000..9735ba5 --- /dev/null +++ b/src/events/thread/onThreadDelete.ts @@ -0,0 +1,38 @@ +import { ThreadChannel } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { getConfig } from "../../modules/data/getConfig"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Handles the deletion of a thread. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {ThreadChannel} thread The thread payload from Discord. + */ +export const onThreadDelete = async ( + bot: ExtendedClient, + thread: ThreadChannel +) => { + try { + const config = await getConfig(bot, thread.guild.id); + + if (!config.eventLogChannel) { + return; + } + + const channel = + thread.guild.channels.cache.get(config.eventLogChannel) || + (await thread.guild.channels.fetch(config.eventLogChannel)); + + if (!channel || !("send" in channel)) { + return; + } + + await channel.send({ + content: `${thread.name} has been deleted from <#${thread.parentId}>` + }); + } catch (err) { + await errorHandler(bot, "on thread create", err); + } +}; diff --git a/src/events/thread/onThreadUpdate.ts b/src/events/thread/onThreadUpdate.ts new file mode 100644 index 0000000..56099fe --- /dev/null +++ b/src/events/thread/onThreadUpdate.ts @@ -0,0 +1,54 @@ +import { ThreadChannel } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { getConfig } from "../../modules/data/getConfig"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Handles a thread update. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {ThreadChannel} oldThread The old thread payload. + * @param {ThreadChannel} newThread The new thread payload. + */ +export const onThreadUpdate = async ( + bot: ExtendedClient, + oldThread: ThreadChannel, + newThread: ThreadChannel +) => { + try { + const config = await getConfig(bot, newThread.guild.id); + + if (!config.eventLogChannel) { + return; + } + + const channel = + oldThread.guild.channels.cache.get(config.eventLogChannel) || + (await oldThread.guild.channels.fetch(config.eventLogChannel)); + + if (!channel || !("send" in channel)) { + return; + } + + if (!oldThread.archived && newThread.archived) { + await channel.send({ + content: `${newThread.name} has been archived <#${newThread.parentId}>` + }); + } + + if (oldThread.archived && !newThread.archived) { + await channel.send({ + content: `${newThread.name} has been unarchived <#${newThread.parentId}>` + }); + } + + if (oldThread.name !== newThread.name) { + await channel.send({ + content: `${oldThread.name} has been renamed to ${newThread.name} in <#${newThread.parentId}>` + }); + } + } catch (err) { + await errorHandler(bot, "on thread update", err); + } +}; diff --git a/src/events/voice/onVoiceUpdate.ts b/src/events/voice/onVoiceUpdate.ts new file mode 100644 index 0000000..b1ec67d --- /dev/null +++ b/src/events/voice/onVoiceUpdate.ts @@ -0,0 +1,82 @@ +import { VoiceState } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { getConfig } from "../../modules/data/getConfig"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Handles voice state updates. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {VoiceState} oldVoice The old voice state. + * @param {VoiceState} newVoice The new voice state. + */ +export const onVoiceUpdate = async ( + bot: ExtendedClient, + oldVoice: VoiceState, + newVoice: VoiceState +) => { + try { + const config = await getConfig(bot, newVoice.guild.id); + + if (!config.eventLogChannel || !newVoice.member) { + return; + } + + const channel = + newVoice.guild.channels.cache.get(config.eventLogChannel) || + (await newVoice.guild.channels.fetch(config.eventLogChannel)); + + if (!channel || !("send" in channel)) { + return; + } + + if ( + oldVoice.channelId && + newVoice.channelId && + oldVoice.channelId !== newVoice.channelId + ) { + await channel.send({ + content: `${newVoice.member.user.tag} (${newVoice.member.id}) has moved from <#!${oldVoice.channelId}> to <#!${newVoice.channelId}>.` + }); + } + + if (oldVoice.channelId && !newVoice.channelId) { + await channel.send({ + content: `${newVoice.member.user.tag} (${newVoice.member.id}) has disconnected from <#!${oldVoice.channelId}>.` + }); + } + + if (!oldVoice.channelId && newVoice.channelId) { + await channel.send({ + content: `${newVoice.member.user.tag} (${newVoice.member.id}) has connected to <#!${newVoice.channelId}>.` + }); + } + + if (oldVoice.mute && !newVoice.mute) { + await channel.send({ + content: `${newVoice.member.user.tag} (${newVoice.member.id}) has been unmuted.` + }); + } + + if (!oldVoice.mute && newVoice.mute) { + await channel.send({ + content: `${newVoice.member.user.tag} (${newVoice.member.id}) has been muted.` + }); + } + + if (oldVoice.deaf && !newVoice.deaf) { + await channel.send({ + content: `${newVoice.member.user.tag} (${newVoice.member.id}) has been undeafened.` + }); + } + + if (!oldVoice.deaf && newVoice.deaf) { + await channel.send({ + content: `${newVoice.member.user.tag} (${newVoice.member.id}) has been deafened.` + }); + } + } catch (err) { + await errorHandler(bot, "on voice update", err); + } +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8d0f765 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,24 @@ +import { Client } from "discord.js"; + +import { IntentOptions } from "./config/IntentOptions"; +import { connectDatabase } from "./database/connectDatabase"; +import { handleEvents } from "./events/_handleEvents"; +import { ExtendedClient } from "./interfaces/ExtendedClient"; +import { validateEnv } from "./modules/validateEnv"; +import { serve } from "./server/serve"; +import { loadCommands } from "./utils/loadCommands"; +import { loadContexts } from "./utils/loadContexts"; + +(async () => { + const bot = new Client({ intents: IntentOptions }) as ExtendedClient; + bot.env = validateEnv(); + bot.configs = {}; + bot.commands = await loadCommands(bot); + bot.contexts = await loadContexts(bot); + + await connectDatabase(bot); + handleEvents(bot); + serve(bot); + + await bot.login(bot.env.token); +})(); diff --git a/src/interfaces/Action.ts b/src/interfaces/Action.ts new file mode 100644 index 0000000..596762d --- /dev/null +++ b/src/interfaces/Action.ts @@ -0,0 +1,30 @@ +type PastAction = + | "warned" + | "kicked" + | "banned" + | "muted" + | "unmuted" + | "unbanned" + | "noted" + | "softbanned"; + +export type Action = + | "warn" + | "kick" + | "ban" + | "mute" + | "unmute" + | "unban" + | "note" + | "softban"; + +export const ActionToPastTense: { [key in Action]: PastAction } = { + warn: "warned", + kick: "kicked", + ban: "banned", + mute: "muted", + unmute: "unmuted", + unban: "unbanned", + note: "noted", + softban: "softbanned" +}; diff --git a/src/interfaces/ActionPayload.ts b/src/interfaces/ActionPayload.ts new file mode 100644 index 0000000..ee4fe47 --- /dev/null +++ b/src/interfaces/ActionPayload.ts @@ -0,0 +1,11 @@ +import { Action } from "./Action"; + +export interface ActionPayload { + userId: string; + serverId: string; + action: Action; + reason: string; + moderator: string; + duration?: number; + pruneDays?: number | undefined; +} diff --git a/src/interfaces/Command.ts b/src/interfaces/Command.ts new file mode 100644 index 0000000..a20a5d0 --- /dev/null +++ b/src/interfaces/Command.ts @@ -0,0 +1,19 @@ +import { + SlashCommandBuilder, + SlashCommandOptionsOnlyBuilder, + SlashCommandSubcommandsOnlyBuilder +} from "discord.js"; + +import { ExtendedClient } from "./ExtendedClient"; +import { GuildCommandInteraction } from "./Interactions"; + +export interface Command { + data: + | SlashCommandOptionsOnlyBuilder + | Omit + | SlashCommandSubcommandsOnlyBuilder; + run: ( + bot: ExtendedClient, + interaction: GuildCommandInteraction + ) => Promise; +} diff --git a/src/interfaces/CommandHandler.ts b/src/interfaces/CommandHandler.ts new file mode 100644 index 0000000..0a13689 --- /dev/null +++ b/src/interfaces/CommandHandler.ts @@ -0,0 +1,8 @@ +import { ExtendedClient } from "./ExtendedClient"; +import { GuildCommandInteraction } from "./Interactions"; + +export type CommandHandler = ( + bot: ExtendedClient, + interaction: GuildCommandInteraction, + config: ExtendedClient["configs"][""] +) => Promise; diff --git a/src/interfaces/Context.ts b/src/interfaces/Context.ts new file mode 100644 index 0000000..656175d --- /dev/null +++ b/src/interfaces/Context.ts @@ -0,0 +1,13 @@ +import { ExtendedClient } from "./ExtendedClient"; +import { GuildContextInteraction } from "./Interactions"; + +export interface Context { + data: { + name: string; + type: 2 | 3; + }; + run: ( + bot: ExtendedClient, + interaction: GuildContextInteraction + ) => Promise; +} diff --git a/src/interfaces/ExtendedClient.ts b/src/interfaces/ExtendedClient.ts new file mode 100644 index 0000000..d232b12 --- /dev/null +++ b/src/interfaces/ExtendedClient.ts @@ -0,0 +1,18 @@ +import { PrismaClient, configs } from "@prisma/client"; +import { Client, WebhookClient } from "discord.js"; + +import { Command } from "./Command"; +import { Context } from "./Context"; + +export interface ExtendedClient extends Client { + env: { + token: string; + debugHook: WebhookClient; + mongoUri: string; + devMode: boolean; + }; + db: PrismaClient; + commands: Command[]; + contexts: Context[]; + configs: { [serverId: string]: Omit }; +} diff --git a/src/interfaces/GitHubPayloads.ts b/src/interfaces/GitHubPayloads.ts new file mode 100644 index 0000000..180aa41 --- /dev/null +++ b/src/interfaces/GitHubPayloads.ts @@ -0,0 +1,190 @@ +/** + * The structure of the NESTED issue data from the GitHub Webhook. + */ +interface GithubIssuePayload { + id: number; + node_id: string; + url: string; + repository_url: string; + html_url: string; + number: number; + state: string; + state_reason: string | null; + title: string; + body: string; + user: GithubUserPayload; + created_at: string; + updated_at: string; + closed_by: GithubUserPayload; +} + +interface GithubPullRequestPayload { + html_url: string; + body: string; + number: number; + merged: boolean; + title: string; + user: GithubUserPayload; +} + +/** + * Structure of the repo data, sent on pretty much + * every GitHub Webhook payload. + */ +interface GithubRepoPayload { + id: number; + node_id: string; + name: string; + full_name: string; + owner: GithubUserPayload; + private: boolean; + html_url: string; + description: string; + fork: boolean; + url: string; + archive_url: string; + assignees_url: string; + blobs_url: string; + branches_url: string; + collaborators_url: string; + comments_url: string; + commits_url: string; + compare_url: string; + contents_url: string; + contributors_url: string; + deployments_url: string; + downloads_url: string; + events_url: string; + forks_url: string; + git_commits_url: string; + git_refs_url: string; + git_tags_url: string; + git_url: string; + issue_comment_url: string; + issue_events_url: string; + issues_url: string; + keys_url: string; + labels_url: string; + languages_url: string; + merges_url: string; + milestones_url: string; + notifications_url: string; + pulls_url: string; + releases_url: string; + ssh_url: string; + stargazers_url: string; + statuses_url: string; + subscribers_url: string; + subscription_url: string; + tags_url: string; + teams_url: string; + trees_url: string; + clone_url: string; + mirror_url: string; + hooks_url: string; + svn_url: string; + homepage: string; + language: string | null; + forks: number; + forks_count: number; + stargazers_count: number; + watchers_count: number; + watchers: number; + size: number; + default_branch: string; + open_issues_count: number; + open_issues: number; + created_at: string; +} + +interface GithubUserPayload { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + name: string; + company: string; + blog: string; + location: string; + email: string; + hireable: boolean; +} + +/** + * The structure of the comment data from the Github Webhook. + */ +export interface GithubCommentPayload { + action: string; + issue: GithubIssuePayload; + comment: { + html_url: string; + body: string; + user: GithubUserPayload; + }; + repository: GithubRepoPayload; + sender: GithubUserPayload; +} + +export interface GithubForkPayload { + forkee: GithubRepoPayload; + repository: GithubRepoPayload; + sender: GithubUserPayload; +} + +/** + * The structure of the top level issue data from the GitHub webhook. + */ +export interface GithubIssuesPayload { + action: string; + issue: GithubIssuePayload; + repository: GithubRepoPayload; + sender: GithubUserPayload; +} + +/** + * The structure of the ping payload when a new GitHub webhook + * is initialised. + */ +export interface GithubPingPayload { + zen: string; + hook_id: string; + hook: Record; + repository: GithubRepoPayload; + organization: Record; + sender: GithubUserPayload; +} + +/** + * Structure of the pull request data from the GitHub Webhook. + */ +export interface GithubPullPayload { + action: string; + number: number; + pull_request: GithubPullRequestPayload; + repository: GithubRepoPayload; + sender: GithubUserPayload; +} + +/** + * Structure of the star data sent from the GitHub Webhook. + */ +export interface GithubStarPayload { + action: "created" | "deleted"; + starred_at: string; + repository: GithubRepoPayload; + sender: GithubUserPayload; +} diff --git a/src/interfaces/Interactions.ts b/src/interfaces/Interactions.ts new file mode 100644 index 0000000..e061101 --- /dev/null +++ b/src/interfaces/Interactions.ts @@ -0,0 +1,15 @@ +import { + ChatInputCommandInteraction, + ContextMenuCommandInteraction, + Guild, + GuildMember +} from "discord.js"; + +export interface GuildCommandInteraction extends ChatInputCommandInteraction { + guild: Guild; + member: GuildMember; +} + +export interface GuildContextInteraction extends ContextMenuCommandInteraction { + guild: Guild; +} diff --git a/src/modules/buttons/handleCopyIdButton.ts b/src/modules/buttons/handleCopyIdButton.ts new file mode 100644 index 0000000..a51063d --- /dev/null +++ b/src/modules/buttons/handleCopyIdButton.ts @@ -0,0 +1,28 @@ +import { ButtonInteraction } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Handles the logic for the acknowledge button on message reports. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {ButtonInteraction} interaction The interaction payload from Discord. + */ +export const handleCopyIdButton = async ( + bot: ExtendedClient, + interaction: ButtonInteraction +) => { + try { + await interaction.deferReply({ ephemeral: true }); + const id = interaction.customId.split("-")[1]; + await interaction.editReply({ + content: id || "Unable to parse ID." + }); + } catch (err) { + const id = await errorHandler(bot, "handle copy id button", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } +}; diff --git a/src/modules/buttons/handleReportAcknowledgeButton.ts b/src/modules/buttons/handleReportAcknowledgeButton.ts new file mode 100644 index 0000000..66de4a0 --- /dev/null +++ b/src/modules/buttons/handleReportAcknowledgeButton.ts @@ -0,0 +1,43 @@ +import { ButtonInteraction } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Handles the logic for the acknowledge button on message reports. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {ButtonInteraction} interaction The interaction payload from Discord. + */ +export const handleReportAcknowledgeButton = async ( + bot: ExtendedClient, + interaction: ButtonInteraction +) => { + try { + await interaction.deferUpdate(); + const message = interaction.message; + const embed = message.embeds[0]; + await interaction.editReply({ + embeds: [ + { + title: embed?.title || "wtf", + description: embed?.description || "wtf", + fields: [ + ...(embed?.fields ?? []), + { + name: "Acknowledged by", + value: `<@${interaction.user.id}>` + } + ], + color: 0x00ff00 + } + ], + components: [] + }); + } catch (err) { + const id = await errorHandler(bot, "handle report acknowledge button", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } +}; diff --git a/src/modules/commands/calculateMuteDuration.ts b/src/modules/commands/calculateMuteDuration.ts new file mode 100644 index 0000000..dbd38e9 --- /dev/null +++ b/src/modules/commands/calculateMuteDuration.ts @@ -0,0 +1,24 @@ +/** + * Parses a value/unit pair into a number of milliseconds. For example, + * (1, "seconds") would return one second in milliseconds. + * + * @param {number} value The number of "unit" to convert to milliseconds. + * @param {string} unit The unit of time to convert to milliseconds. + * @returns {number} The number of milliseconds. + */ +export const calculateMuteDuration = (value: number, unit: string) => { + switch (unit) { + case "seconds": + return value * 1000; + case "minutes": + return value * 60000; + case "hours": + return value * 3600000; + case "days": + return value * 86400000; + case "weeks": + return value * 604800000; + default: + return 0; + } +}; diff --git a/src/modules/commands/generateProfileImage.ts b/src/modules/commands/generateProfileImage.ts new file mode 100644 index 0000000..b951d0e --- /dev/null +++ b/src/modules/commands/generateProfileImage.ts @@ -0,0 +1,213 @@ +import { levels } from "@prisma/client"; +import { AttachmentBuilder } from "discord.js"; +import nodeHtmlToImage from "node-html-to-image"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Creates an image from the user's profile settings, converts it into a Discord + * attachment, and returns it. + * + * @param {ExtendedClient} CamperChan The CamperChan's Discord instance. + * @param {levels} record The user's record from the database. + * @returns {AttachmentBuilder} The attachment, or null on error. + */ +export const generateProfileImage = async ( + CamperChan: ExtendedClient, + record: levels +): Promise => { + try { + const { + avatar, + backgroundColour, + backgroundImage, + colour, + username, + level, + points + } = record; + + const html = ` + + +
+
+ +
+

${username}

+

Level ${level} (${points.toLocaleString("en-GB")}xp)

+
+
+
+ + `; + const alt = `${username} is at level ${level} with ${points.toLocaleString("en-GB")} experience points.`; + + const image = await nodeHtmlToImage({ + html, + selector: "body", + transparent: true + }); + + if (!(image instanceof Buffer)) { + return null; + } + + const attachment = new AttachmentBuilder(image, { + name: `${username}.png`, + description: alt + }); + + return attachment; + } catch (err) { + await errorHandler(CamperChan, "generate profile image module", err); + return null; + } +}; + +/** + * Generates the image for the leaderboard. + * + * @param {ExtendedClient} CamperChan The CamperChan's Discord instance. + * @param {levels} levels The user's record from the database. + * @returns {AttachmentBuilder} The attachment, or null on error. + */ +export const generateLeaderboardImage = async ( + CamperChan: ExtendedClient, + levels: (levels & { index: number })[] +): Promise => { + try { + const html = ` + + + ${levels.map( + (l) => + `
+ +
+

#${l.index}. ${l.username}

+

Level ${l.level} (${l.points}xp)

+
+
` + )} + + `; + const alt = levels + .map( + (l) => + `${l.username} is rank ${l.index} at ${l.level} with ${l.points.toLocaleString("en-GB")} experience points.` + ) + .join(", "); + + const image = await nodeHtmlToImage({ + html, + selector: "body", + transparent: true + }); + + if (!(image instanceof Buffer)) { + return null; + } + + const attachment = new AttachmentBuilder(image, { + name: `leaderboard-${levels[0]?.index}.png`, + description: alt + }); + + return attachment; + } catch (err) { + await errorHandler(CamperChan, "generate leaderboard image module", err); + return null; + } +}; diff --git a/src/modules/commands/profileValidation.ts b/src/modules/commands/profileValidation.ts new file mode 100644 index 0000000..d491449 --- /dev/null +++ b/src/modules/commands/profileValidation.ts @@ -0,0 +1,32 @@ +/** + * Checks if a string matches a 6 character hex code. + * + * @param {string} colour The colour code to validate. + * @returns {boolean} If the string is in the correct format. + */ +export const validateColour = (colour: string): boolean => { + return /[\da-f]{6}/gi.test(colour); +}; + +/** + * Checks if a url points to a valid image. + * + * @param {string} url The URL to validate. + * @returns {boolean} If the URL provides a 2XX response, and if the response content type + * is an image. + */ +export const validateImage = async (url: string): Promise => { + const validImage = await fetch(url, { + method: "HEAD" + }).catch(() => null); + + if (!validImage) { + return false; + } + + if (!validImage.headers.get("content-type")?.startsWith("image/")) { + return false; + } + + return true; +}; diff --git a/src/modules/data/getConfig.ts b/src/modules/data/getConfig.ts new file mode 100644 index 0000000..53ec710 --- /dev/null +++ b/src/modules/data/getConfig.ts @@ -0,0 +1,39 @@ +import { configs } from "@prisma/client"; + +import { defaultConfig } from "../../config/DefaultConfig"; +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Module to get the server config from the cache, database, or default. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {string} serverId The ID of the server to get the config for. + * @returns {ExtendedClient["config"]} The server config. + */ +export const getConfig = async ( + bot: ExtendedClient, + serverId: string +): Promise> => { + try { + const exists = bot.configs[serverId]; + if (exists) { + return exists; + } + const record = await bot.db.configs.upsert({ + where: { + serverId + }, + create: { + ...defaultConfig, + serverId + }, + update: {} + }); + bot.configs[serverId] = record; + return record; + } catch (err) { + await errorHandler(bot, "get config", err); + return defaultConfig; + } +}; diff --git a/src/modules/data/setConfig.ts b/src/modules/data/setConfig.ts new file mode 100644 index 0000000..614aadd --- /dev/null +++ b/src/modules/data/setConfig.ts @@ -0,0 +1,40 @@ +import { defaultConfig } from "../../config/DefaultConfig"; +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Module to update a config. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {string} serverId The ID of the server to update. + * @param {keyof ExtendedClient["config"]} setting The setting to update. + * @param {number | string} value The value to update the setting to. + * @returns {boolean} True on successful update. + */ +export const setConfig = async ( + bot: ExtendedClient, + serverId: string, + setting: keyof ExtendedClient["configs"][""], + value: number | string +): Promise => { + try { + const configData = await bot.db.configs.upsert({ + where: { + serverId + }, + create: { + ...defaultConfig, + serverId, + [setting]: value + }, + update: { + [setting]: value + } + }); + bot.configs[serverId] = configData; + return true; + } catch (err) { + await errorHandler(bot, "set config", err); + return false; + } +}; diff --git a/src/modules/events/checkSpamDomain.ts b/src/modules/events/checkSpamDomain.ts new file mode 100644 index 0000000..0faa365 --- /dev/null +++ b/src/modules/events/checkSpamDomain.ts @@ -0,0 +1,46 @@ +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { errorHandler } from "../../utils/errorHandler"; + +/** + * Checks if a domain is a known source of Discord scams. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {string} domain The domain to validate. DO NOT include the protocol or path. + * @returns {boolean} True if the domain is known as a scam. + */ +export const checkSpamDomain = async ( + bot: ExtendedClient, + domain: string +): Promise => { + try { + const walshyReq = await fetch("https://bad-domains.walshy.dev/check", { + method: "POST", + headers: { + accept: "application/json", + "X-Identity": "Naomi's mod bot - built by naomi_lgbt" + }, + body: JSON.stringify({ domain }) + }); + const walshyRes = (await walshyReq.json()) as { badDomain: boolean }; + if (walshyRes.badDomain) { + return true; + } + const yachtsReq = await fetch( + `https://phish.sinking.yachts/v2/check/${encodeURI(domain)}`, + { + headers: { + accept: "application/json", + "X-Identity": "Naomi's mod bot - built by naomi_lgbt" + } + } + ); + const yachtsRes = (await yachtsReq.json()) as boolean; + if (yachtsRes) { + return true; + } + return false; + } catch (err) { + await errorHandler(bot, "load spam domains", err); + return false; + } +}; diff --git a/src/modules/events/getModActionFromAuditLog.ts b/src/modules/events/getModActionFromAuditLog.ts new file mode 100644 index 0000000..accc8ae --- /dev/null +++ b/src/modules/events/getModActionFromAuditLog.ts @@ -0,0 +1,35 @@ +import { AuditLogEvent, GuildAuditLogsEntry } from "discord.js"; + +import { Action } from "../../interfaces/Action"; + +/** + * Module to parse the audit log entry and return the moderation action. + * + * @param {GuildAuditLogsEntry} log The audit log entry payload. + * @returns {Action | null} The mod action string, or null if not found. + */ +export const getModActionFromAuditLog = ( + log: GuildAuditLogsEntry +): Action | null => { + const muteChange = log.changes.find( + (change) => change.key === "communication_disabled_until" + ); + switch (log.action) { + case AuditLogEvent.MemberBanAdd: + return "ban"; + case AuditLogEvent.MemberBanRemove: + return "unban"; + case AuditLogEvent.MemberKick: + return "kick"; + case AuditLogEvent.MemberUpdate: + if (!muteChange) { + return null; + } + if (muteChange.new) { + return "mute"; + } + return "unmute"; + default: + return null; + } +}; diff --git a/src/modules/interactions/handleChatInputCommand.ts b/src/modules/interactions/handleChatInputCommand.ts new file mode 100644 index 0000000..d90e4e0 --- /dev/null +++ b/src/modules/interactions/handleChatInputCommand.ts @@ -0,0 +1,55 @@ +import { ChatInputCommandInteraction } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { errorHandler } from "../../utils/errorHandler"; +import { isModerator } from "../../utils/isModerator"; +import { isGuildCommandInteraction } from "../validateGuildCommands"; + +/** + * Handles the logic for running slash commands. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {ChatInputCommandInteraction} interaction The interaction payload from Discord. + */ +export const handleChatInputCommand = async ( + bot: ExtendedClient, + interaction: ChatInputCommandInteraction +) => { + try { + if (!isGuildCommandInteraction(interaction)) { + await interaction.reply({ + content: "You can only use commands in a server.", + ephemeral: true + }); + return; + } + const command = bot.commands.find( + (c) => c.data.name === interaction.commandName + ); + if (!command) { + await interaction.reply({ + content: "That's not a valid command. Please contact Naomi.", + ephemeral: true + }); + return; + } + if ( + (!interaction.member || !isModerator(interaction.member)) && + !["leaderboard", "rank", "profile", "role", "help", "ping"].includes( + interaction.commandName + ) + ) { + await interaction.reply({ + content: "You must be a moderator to use bot commands.", + ephemeral: true + }); + return; + } + await command.run(bot, interaction); + } catch (err) { + const id = await errorHandler(bot, "handle chat input command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } +}; diff --git a/src/modules/interactions/handleContextMenuCommand.ts b/src/modules/interactions/handleContextMenuCommand.ts new file mode 100644 index 0000000..0e41610 --- /dev/null +++ b/src/modules/interactions/handleContextMenuCommand.ts @@ -0,0 +1,42 @@ +import { ContextMenuCommandInteraction } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { errorHandler } from "../../utils/errorHandler"; +import { isGuildContextInteraction } from "../validateGuildCommands"; + +/** + * Handles the logic for running context commands. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {ContextMenuCommandInteraction} interaction The interaction payload from Discord. + */ +export const handleContextMenuCommand = async ( + bot: ExtendedClient, + interaction: ContextMenuCommandInteraction +) => { + try { + if (!isGuildContextInteraction(interaction)) { + await interaction.reply({ + content: "You can only use this in a server.", + ephemeral: true + }); + return; + } + const context = bot.contexts.find( + (c) => c.data.name === interaction.commandName + ); + if (!context) { + await interaction.reply({ + content: "That's not a valid context. Please contact Naomi.", + ephemeral: true + }); + return; + } + await context.run(bot, interaction); + } catch (err) { + const id = await errorHandler(bot, "handle context menu command", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } +}; diff --git a/src/modules/modals/handleMassBanModal.ts b/src/modules/modals/handleMassBanModal.ts new file mode 100644 index 0000000..5687a6f --- /dev/null +++ b/src/modules/modals/handleMassBanModal.ts @@ -0,0 +1,127 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, + EmbedBuilder, + Message, + ModalSubmitInteraction +} from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { errorHandler } from "../../utils/errorHandler"; +import { getConfig } from "../data/getConfig"; + +/** + * Handles the submission of the mass ban form. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {ModalSubmitInteraction} interaction The interaction payload from Discord. + */ +export const handleMassBanModal = async ( + bot: ExtendedClient, + interaction: ModalSubmitInteraction +) => { + try { + await interaction.deferReply({ ephemeral: true }); + const { guild } = interaction; + if (!guild) { + await interaction.editReply({ + content: "This command can only be used in a guild." + }); + return; + } + + const rawBanList = interaction.fields.getTextInputValue("mass-ban-ids"); + const banList = rawBanList.trim().split(/\b/g); + + const reason = interaction.fields.getTextInputValue("reason"); + + const embed = new EmbedBuilder(); + embed.setTitle("Confirm Mass Ban of Following IDs:"); + embed.setDescription(banList.join("\n")); + embed.addFields({ + name: "Reason", + value: reason + }); + + const yes = new ButtonBuilder() + .setCustomId("confirm") + .setLabel("Confirm") + .setStyle(ButtonStyle.Success); + const no = new ButtonBuilder() + .setCustomId("cancel") + .setLabel("Cancel") + .setStyle(ButtonStyle.Danger); + const row = new ActionRowBuilder().addComponents(yes, no); + const response = (await interaction.editReply({ + embeds: [embed], + components: [row] + })) as Message; + + const collector = + response.createMessageComponentCollector({ + filter: (click) => click.user.id === interaction.user.id, + time: 10000, + max: 1 + }); + + collector.on("end", async (clicks) => { + const choice = clicks.first()?.customId; + if (!clicks || clicks.size <= 0 || !choice) { + await interaction.editReply({ + content: "This command has timed out.", + embeds: [], + components: [] + }); + return; + } + + if (choice === "confirm") { + for (const id of banList) { + await guild.bans.create(id, { + reason: `Massban by ${interaction.user.tag} for: ${reason}`, + deleteMessageSeconds: 86400 + }); + } + + const config = await getConfig(bot, guild.id); + if (!config.modLogChannel) { + return; + } + const channel = + guild.channels.cache.get(config.modLogChannel) || + (await guild.channels.fetch(config.modLogChannel)); + + if (!channel || !("send" in channel)) { + return; + } + + embed.setTitle("Mass Ban:"); + embed.setAuthor({ + name: interaction.user.tag, + iconURL: interaction.user.displayAvatarURL() + }); + await channel.send({ embeds: [embed] }); + await interaction.editReply({ + content: "Mass ban complete.", + embeds: [], + components: [] + }); + } + + if (choice === "cancel") { + interaction.editReply({ + content: "Mass ban cancelled.", + embeds: [], + components: [] + }); + } + }); + } catch (err) { + const id = await errorHandler(bot, "handle mass ban modal", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } +}; diff --git a/src/modules/modals/handleMessageReportModal.ts b/src/modules/modals/handleMessageReportModal.ts new file mode 100644 index 0000000..7a5672d --- /dev/null +++ b/src/modules/modals/handleMessageReportModal.ts @@ -0,0 +1,82 @@ +import { ModalSubmitInteraction } from "discord.js"; + +import { ExtendedClient } from "../../interfaces/ExtendedClient"; +import { errorHandler } from "../../utils/errorHandler"; +import { getConfig } from "../data/getConfig"; + +/** + * Handles the submission of the message report form. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {ModalSubmitInteraction} interaction The interaction payload from Discord. + */ +export const handleMessageReportModal = async ( + bot: ExtendedClient, + interaction: ModalSubmitInteraction +) => { + try { + await interaction.deferReply({ ephemeral: true }); + if (!interaction.guild) { + await interaction.editReply({ + content: "This command can only be used in a guild." + }); + return; + } + const reportLogId = interaction.customId.split("-")[1] ?? "oops"; + const config = await getConfig(bot, interaction.guild.id); + + if (!config.messageReportChannel) { + await interaction.editReply({ + content: "Reporting has not been set up for this server." + }); + return; + } + + const channel = + interaction.guild.channels.cache.get(config.messageReportChannel) || + (await interaction.guild.channels.fetch(config.messageReportChannel)); + + if (!channel || !("send" in channel)) { + await interaction.editReply({ + content: "Reporting channel not found." + }); + return; + } + + const reportLog = await channel.messages + .fetch(reportLogId) + .catch(() => null); + if (!reportLog) { + await interaction.editReply({ + content: "Could not find the report log." + }); + return; + } + + const embed = reportLog.embeds[0]; + await reportLog.edit({ + embeds: [ + { + title: embed?.title || "wtf", + description: embed?.description || "wtf", + fields: [ + ...(embed?.fields ?? []), + { + name: "Reason", + value: interaction.fields.getTextInputValue("reason") + } + ] + } + ] + }); + + await interaction.editReply({ + content: "Your report has been submitted." + }); + } catch (err) { + const id = await errorHandler(bot, "handle message report modal", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } +}; diff --git a/src/modules/subcommands/config/handleAppealLink.ts b/src/modules/subcommands/config/handleAppealLink.ts new file mode 100644 index 0000000..81ca63e --- /dev/null +++ b/src/modules/subcommands/config/handleAppealLink.ts @@ -0,0 +1,34 @@ +import { CommandHandler } from "../../../interfaces/CommandHandler"; +import { errorHandler } from "../../../utils/errorHandler"; +import { setConfig } from "../../data/setConfig"; + +/** + * Sets the ban appeal link for the server. + */ +export const handleAppealLink: CommandHandler = async (bot, interaction) => { + try { + const link = interaction.options.getString("link", true); + + const success = await setConfig( + bot, + interaction.guild.id, + "banAppealLink", + link + ); + + if (success) { + await interaction.editReply({ + content: `Members who are banned can appeal at <${link}>.` + }); + return; + } + await interaction.editReply({ + content: "Failed to set the settings." + }); + } catch (err) { + const id = await errorHandler(bot, "automod logging subcommand", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } +}; diff --git a/src/modules/subcommands/config/handleInviteLink.ts b/src/modules/subcommands/config/handleInviteLink.ts new file mode 100644 index 0000000..123e822 --- /dev/null +++ b/src/modules/subcommands/config/handleInviteLink.ts @@ -0,0 +1,34 @@ +import { CommandHandler } from "../../../interfaces/CommandHandler"; +import { errorHandler } from "../../../utils/errorHandler"; +import { setConfig } from "../../data/setConfig"; + +/** + * Sets the invite link for the server. + */ +export const handleInviteLink: CommandHandler = async (bot, interaction) => { + try { + const link = interaction.options.getString("link", true); + + const success = await setConfig( + bot, + interaction.guild.id, + "inviteLink", + link + ); + + if (success) { + await interaction.editReply({ + content: `Members who are kicked will be invited back with <${link}>.` + }); + return; + } + await interaction.editReply({ + content: "Failed to set the settings." + }); + } catch (err) { + const id = await errorHandler(bot, "automod logging subcommand", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } +}; diff --git a/src/modules/subcommands/config/handleList.ts b/src/modules/subcommands/config/handleList.ts new file mode 100644 index 0000000..af89a93 --- /dev/null +++ b/src/modules/subcommands/config/handleList.ts @@ -0,0 +1,141 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, + EmbedBuilder +} from "discord.js"; + +import { CommandHandler } from "../../../interfaces/CommandHandler"; +import { errorHandler } from "../../../utils/errorHandler"; +import { getNextIndex, getPreviousIndex } from "../../../utils/getArrayIndex"; + +/** + * Fetches the automod settings for the given guild. + */ +export const handleList: CommandHandler = async (bot, interaction, config) => { + try { + const embed = new EmbedBuilder(); + + embed.setTitle("Automod Settings"); + embed.addFields([ + { + name: "Moderation Log Channel", + value: config.modLogChannel ? `<#${config.modLogChannel}>` : "Not set.", + inline: true + }, + { + name: "Event Log Channel", + value: config.eventLogChannel + ? `<#${config.eventLogChannel}>` + : "Not set.", + inline: true + }, + { + name: "Message Report Channel", + value: config.messageReportChannel + ? `<#${config.messageReportChannel}>` + : "Not set.", + inline: true + }, + { + name: "Invite Link", + value: config.inviteLink || "None", + inline: true + }, + { + name: "Ban Appeal Link", + value: config.banAppealLink || "None", + inline: true + } + ]); + + const roles = await bot.db.levelRoles.findMany({ + where: { serverId: interaction.guild.id } + }); + + const levelRoles = new EmbedBuilder(); + levelRoles.setTitle("Level Roles"); + levelRoles.setDescription( + roles + .map((r) => `- <@&${r.roleId}> is assigned at level ${r.level}`) + .join("\n") || "No roles are currently set." + ); + + const assignRoles = await bot.db.roles.findMany({ + where: { + serverId: interaction.guild.id + } + }); + const assignRolesEmbed = new EmbedBuilder(); + assignRolesEmbed.setTitle("Self-Assignable Roles"); + assignRolesEmbed.setDescription( + assignRoles.map((r) => `<@&${r.roleId}>`).join(" ") || + "No roles are currently set." + ); + const embeds = [embed, levelRoles, assignRolesEmbed]; + + let index = 0; + const nextButton = new ButtonBuilder() + .setCustomId("next") + .setStyle(ButtonStyle.Primary) + .setLabel( + embeds[getNextIndex(embeds, index)]?.data.title || "Unknown embed." + ) + .setEmoji("▶️"); + const prevButton = new ButtonBuilder() + .setCustomId("prev") + .setStyle(ButtonStyle.Primary) + .setLabel( + embeds[getPreviousIndex(embeds, index)]?.data.title || "Unknown embed." + ) + .setEmoji("◀️"); + const initialRow = new ActionRowBuilder().addComponents( + prevButton, + nextButton + ); + + const response = await interaction.editReply({ + embeds: [embeds[index] as EmbedBuilder], + components: [initialRow] + }); + + const collector = + response.createMessageComponentCollector({ + time: 1000 * 60 * 5 + }); + + collector.on("collect", async (i) => { + await i.deferUpdate(); + index = + i.customId === "next" + ? getNextIndex(embeds, index) + : getPreviousIndex(embeds, index); + prevButton.setLabel( + embeds[getPreviousIndex(embeds, index)]?.data.title || "Unknown embed." + ); + nextButton.setLabel( + embeds[getNextIndex(embeds, index)]?.data.title || "Unknown embed." + ); + const newRow = new ActionRowBuilder().addComponents( + prevButton, + nextButton + ); + await i.editReply({ + embeds: [embeds[index] as EmbedBuilder], + components: [newRow] + }); + }); + + collector.on("end", async () => { + await interaction.editReply({ + components: [] + }); + }); + } catch (err) { + const id = await errorHandler(bot, "automod list subcommand", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } +}; diff --git a/src/modules/subcommands/config/handleLogging.ts b/src/modules/subcommands/config/handleLogging.ts new file mode 100644 index 0000000..083c060 --- /dev/null +++ b/src/modules/subcommands/config/handleLogging.ts @@ -0,0 +1,40 @@ +import { logChannelChoicesMap } from "../../../config/LogChannelChoices"; +import { CommandHandler } from "../../../interfaces/CommandHandler"; +import { ExtendedClient } from "../../../interfaces/ExtendedClient"; +import { errorHandler } from "../../../utils/errorHandler"; +import { setConfig } from "../../data/setConfig"; + +/** + * Sets the logging channel for the server. + */ +export const handleLogging: CommandHandler = async (bot, interaction) => { + try { + const logType = interaction.options.getString( + "log-type", + true + ) as keyof ExtendedClient["configs"][""]; + const channel = interaction.options.getChannel("channel", true); + + const success = await setConfig( + bot, + interaction.guild.id, + logType, + channel.id + ); + + if (success) { + await interaction.editReply({ + content: `Your server will log ${logChannelChoicesMap[logType]} in <#${channel.id}>.` + }); + return; + } + await interaction.editReply({ + content: "Failed to set the settings." + }); + } catch (err) { + const id = await errorHandler(bot, "automod logging subcommand", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } +}; diff --git a/src/modules/subcommands/config/handleRole.ts b/src/modules/subcommands/config/handleRole.ts new file mode 100644 index 0000000..dac41b4 --- /dev/null +++ b/src/modules/subcommands/config/handleRole.ts @@ -0,0 +1,47 @@ +import { CommandHandler } from "../../../interfaces/CommandHandler"; +import { errorHandler } from "../../../utils/errorHandler"; + +/** + * Toggles a role to be self-assignable or not. + */ +export const handleRole: CommandHandler = async (bot, interaction) => { + try { + const role = interaction.options.getRole("role", true); + const exists = await bot.db.roles.findUnique({ + where: { + serverId_roleId: { + serverId: interaction.guild.id, + roleId: role.id + } + } + }); + if (exists) { + await bot.db.roles.delete({ + where: { + serverId_roleId: { + serverId: interaction.guild.id, + roleId: role.id + } + } + }); + await interaction.editReply({ + content: `Your <@&${role.id}> role is no longer self-assignable.` + }); + return; + } + await bot.db.roles.create({ + data: { + serverId: interaction.guild.id, + roleId: role.id + } + }); + await interaction.editReply({ + content: `Your <@&${role.id}> role is now self-assignable.` + }); + } catch (err) { + const id = await errorHandler(bot, "automod logging subcommand", err); + await interaction.editReply({ + content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\`` + }); + } +}; diff --git a/src/modules/validateEnv.ts b/src/modules/validateEnv.ts new file mode 100644 index 0000000..6e6b0a0 --- /dev/null +++ b/src/modules/validateEnv.ts @@ -0,0 +1,27 @@ +import { WebhookClient } from "discord.js"; + +import { ExtendedClient } from "../interfaces/ExtendedClient"; +import { logHandler } from "../utils/logHandler"; + +/** + * Validates the environment variables and constructs the object. + * + * @returns {ExtendedClient["env"]} The environment variable object to attach to the bot. + */ +export const validateEnv = (): ExtendedClient["env"] => { + if ( + !process.env.BOT_TOKEN || + !process.env.DEBUG_HOOK || + !process.env.MONGO_URI + ) { + logHandler.log("error", "MIssing environment variables!"); + process.exit(1); + } + + return { + token: process.env.BOT_TOKEN, + debugHook: new WebhookClient({ url: process.env.DEBUG_HOOK }), + mongoUri: process.env.MONGO_URI, + devMode: process.env.NODE_ENV !== "production" + }; +}; diff --git a/src/modules/validateGuildCommands.ts b/src/modules/validateGuildCommands.ts new file mode 100644 index 0000000..f341656 --- /dev/null +++ b/src/modules/validateGuildCommands.ts @@ -0,0 +1,33 @@ +import { + ChatInputCommandInteraction, + ContextMenuCommandInteraction, + Guild, + GuildMember +} from "discord.js"; + +import { + GuildCommandInteraction, + GuildContextInteraction +} from "../interfaces/Interactions"; + +/** + * Validates that a slash command payload has the guild and member. + * + * @param {ChatInputCommandInteraction} interaction The interaction payload from Discord. + * @returns {boolean} Whether the expected properties are present. + */ +export const isGuildCommandInteraction = ( + interaction: ChatInputCommandInteraction +): interaction is GuildCommandInteraction => + interaction.guild instanceof Guild && + interaction.member instanceof GuildMember; + +/** + * Validates that a slash command payload has the guild and member. + * + * @param {ContextMenuCommandInteraction} interaction The interaction payload from Discord. + * @returns {boolean} Whether the expected properties are present. + */ +export const isGuildContextInteraction = ( + interaction: ContextMenuCommandInteraction +): interaction is GuildContextInteraction => interaction.guild instanceof Guild; diff --git a/src/server/github/generateCommentEmbed.ts b/src/server/github/generateCommentEmbed.ts new file mode 100644 index 0000000..495b202 --- /dev/null +++ b/src/server/github/generateCommentEmbed.ts @@ -0,0 +1,5 @@ +import { GithubCommentPayload } from "../../interfaces/GitHubPayloads"; + +export const generateCommentEmbed = (data: GithubCommentPayload): string => { + return `[New comment detected on ${data.repository.name}#${data.issue.number}.](<${data.comment.html_url}>)`; +}; diff --git a/src/server/github/generateForkEmbed.ts b/src/server/github/generateForkEmbed.ts new file mode 100644 index 0000000..c56cad4 --- /dev/null +++ b/src/server/github/generateForkEmbed.ts @@ -0,0 +1,5 @@ +import { GithubForkPayload } from "../../interfaces/GitHubPayloads"; + +export const generateForkEmbed = (data: GithubForkPayload): string => { + return `[New fork detected - ${data.repository.name}](<${data.forkee.html_url}>)`; +}; diff --git a/src/server/github/generateIssueEmbed.ts b/src/server/github/generateIssueEmbed.ts new file mode 100644 index 0000000..b3851b9 --- /dev/null +++ b/src/server/github/generateIssueEmbed.ts @@ -0,0 +1,25 @@ +import { GithubIssuesPayload } from "../../interfaces/GitHubPayloads"; + +export const generateIssuesEmbed = ( + data: GithubIssuesPayload +): string | null => { + if (!["opened", "edited", "closed"].includes(data.action)) { + return null; + } + if (data.action === "closed") { + if (data.issue.state_reason === "completed") { + return `[Issue closed as complete - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; + } + if (data.issue.state_reason === "not_planned") { + return `[Issue closed as not planned - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; + } + return `[Issue closed - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; + } + if (data.issue.state_reason === "reopened") { + return `[Issue reopened - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; + } + if (data.action === "edited") { + return `[Issue updated - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; + } + return `[New issue created - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; +}; diff --git a/src/server/github/generatePingEmbed.ts b/src/server/github/generatePingEmbed.ts new file mode 100644 index 0000000..c4c2e5a --- /dev/null +++ b/src/server/github/generatePingEmbed.ts @@ -0,0 +1,5 @@ +import { GithubPingPayload } from "../../interfaces/GitHubPayloads"; + +export const generatePingEmbed = (data: GithubPingPayload): string => { + return `[Now watching ${data.repository.name}](<${data.repository.url}>)`; +}; diff --git a/src/server/github/generatePullEmbed.ts b/src/server/github/generatePullEmbed.ts new file mode 100644 index 0000000..9cd4f63 --- /dev/null +++ b/src/server/github/generatePullEmbed.ts @@ -0,0 +1,17 @@ +import { GithubPullPayload } from "../../interfaces/GitHubPayloads"; + +export const generatePullEmbed = (data: GithubPullPayload): string | null => { + if (!["opened", "edited", "closed"].includes(data.action)) { + return null; + } + if (data.pull_request.merged) { + return `[Pull request merged - ${data.repository.name}#${data.pull_request.number}](<${data.pull_request.html_url}>)`; + } + if (data.action === "edited") { + return `[Pull request updated - ${data.repository.name}#${data.pull_request.number}](<${data.pull_request.html_url}>)`; + } + if (data.action === "closed") { + return `[Pull request closed - ${data.repository.name}#${data.pull_request.number}](<${data.pull_request.html_url}>)`; + } + return `[New pull request - ${data.repository.name}#${data.pull_request.number}](<${data.pull_request.html_url}>)`; +}; diff --git a/src/server/github/generateStarEmbed.ts b/src/server/github/generateStarEmbed.ts new file mode 100644 index 0000000..b9ac61c --- /dev/null +++ b/src/server/github/generateStarEmbed.ts @@ -0,0 +1,8 @@ +import { GithubStarPayload } from "../../interfaces/GitHubPayloads"; + +export const generateStarEmbed = (data: GithubStarPayload): string | null => { + if (data.action !== "created") { + return null; + } + return `[New stargazer! ${data.repository.name}](<${data.repository.html_url}>)`; +}; diff --git a/src/server/serve.ts b/src/server/serve.ts new file mode 100644 index 0000000..daf3382 --- /dev/null +++ b/src/server/serve.ts @@ -0,0 +1,306 @@ +import { createHmac, timingSafeEqual } from "crypto"; +import { readFile } from "fs/promises"; +import http from "http"; +import https from "https"; + +import { Octokit } from "@octokit/rest"; +import { GuildTextBasedChannel } from "discord.js"; +import express from "express"; + +import { IgnoredActors, ThankYou } from "../config/Github"; +import { ExtendedClient } from "../interfaces/ExtendedClient"; +import { errorHandler } from "../utils/errorHandler"; + +import { generateCommentEmbed } from "./github/generateCommentEmbed"; +import { generateForkEmbed } from "./github/generateForkEmbed"; +import { generateIssuesEmbed } from "./github/generateIssueEmbed"; +import { generatePingEmbed } from "./github/generatePingEmbed"; +import { generatePullEmbed } from "./github/generatePullEmbed"; +import { generateStarEmbed } from "./github/generateStarEmbed"; + +/** + * Instantiates the web server for GitHub webhooks. + * + * @param {ExtendedClient} bot The bot's Discord instance. + */ +export const serve = async (bot: ExtendedClient) => { + const githubSecret = process.env.GITHUB_WEBHOOK_SECRET; + const patreonSecret = process.env.PATREON_WEBHOOK_SECRET; + const kofiSecret = process.env.KOFI_WEBHOOK_SECRET; + const token = process.env.GITHUB_TOKEN; + if (!githubSecret || !token || !kofiSecret || !patreonSecret) { + await bot.env.debugHook.send({ + content: "Missing necessary secrets. Web server will not be started.", + username: bot.user?.username ?? "bot", + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" + }); + return; + } + const app = express(); + app.post("/patreon", express.text({ type: "*/*" })); + app.use(express.urlencoded({ extended: true })); + app.use(express.json()); + + app.get("/", (_req, res) => { + res.send("bot online!"); + }); + + app.post("/kofi", async (req, res) => { + const payload = JSON.parse(req.body.data); + const { + verification_token: verifyToken, + from_name: fromName, + is_subscription_payment: isSub, + is_first_subscription_payment: isFirstSub + } = payload; + if (!verifyToken) { + await bot.env.debugHook.send({ + content: + "Received request with no signature.\n\n" + + JSON.stringify(req.body).slice(0, 1500), + username: bot.user?.username ?? "bot", + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" + }); + res.status(400).send("Invalid payload."); + return; + } + if (verifyToken !== kofiSecret) { + await bot.env.debugHook.send({ + content: + "Received request with bad signature.\n\n" + + JSON.stringify(req.body).slice(0, 1500), + username: bot.user?.username ?? "bot", + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" + }); + res.status(403).send("Invalid signature."); + return; + } + res.status(200).send("Valid signature found!"); + + // ignore recurring subscriptions + if (isSub && !isFirstSub) { + return; + } + + const channel = (await bot.channels.fetch( + "1235114666322034790" + )) as GuildTextBasedChannel; + + await channel.send({ + content: `## Big thanks to ${fromName} for sponsoring us on KoFi!\n\nTo claim your sponsor role, please DM Naomi with your KoFi receipt.` + }); + }); + + app.post("/patreon", async (req, res) => { + // validate headers + const header = req.headers["x-patreon-signature"]; + if (!header) { + await bot.env.debugHook.send({ + content: + "Received request with no signature.\n\n" + + String(req.body).slice(0, 1500), + username: bot.user?.username ?? "bot", + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" + }); + res.status(403).send("No valid signature present."); + return; + } + const hash = createHmac("MD5", patreonSecret) + .update(req.body) + .digest("hex"); + if (hash !== header) { + await bot.env.debugHook.send({ + content: + "Received request with bad signature.\n\n" + + String(req.body).slice(0, 1500), + username: bot.user?.username ?? "bot", + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" + }); + res.status(403).send("Signature is not correct."); + return; + } + res.status(200).send("Signature is correct."); + + const event = req.headers["x-patreon-event"]; + + if (event !== "pledges:create") { + return; + } + + const obj = JSON.parse(req.body); + + const user = obj.included.find( + (obj: Record) => obj.type === "user" + ); + + const channel = (await bot.channels.fetch( + "1235114666322034790" + )) as GuildTextBasedChannel; + + await channel?.send({ + content: `## Big thanks to ${user.attributes.full_name} for sponsoring us on Patreon!\n\nTo claim your sponsor role, please DM Naomi with your patreon receipt.` + }); + }); + + app.post("/github", async (req, res) => { + try { + const header = req.headers["x-hub-signature-256"]; + if (!header || Array.isArray(header)) { + await bot.env.debugHook.send({ + content: + "Received request with no signature.\n\n" + + JSON.stringify(req.body).slice(0, 1500), + username: bot.user?.username ?? "bot", + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" + }); + res.status(403).send("No valid signature present."); + return; + } + const signature = createHmac("sha256", githubSecret) + .update(JSON.stringify(req.body)) + .digest("hex"); + const trusted = Buffer.from(`sha256=${signature}`, "ascii"); + const sent = Buffer.from(header, "ascii"); + const safe = timingSafeEqual(trusted, sent); + if (!safe) { + await bot.env.debugHook.send({ + content: + "Received request with bad signature.\n\n" + + JSON.stringify(req.body).slice(0, 1500), + username: bot.user?.username ?? "bot", + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" + }); + res.status(403).send("Signature is not correct."); + return; + } + res.status(200).send("Signature is correct."); + + const event = req.headers["x-github-event"] as string; + if (event === "sponsorship" && req.body.action === "created") { + const channel = (await bot.channels.fetch( + "1235114666322034790" + )) as GuildTextBasedChannel; + await channel?.send({ + content: `## Big thanks to ${req.body.sponsorship.sponsor.login} for sponsoring us on GitHub!\n\nTo claim your sponsor role, please make sure your GitHub account is connected to your Discord account, then ping Mama Naomi for your role!` + }); + } + + if ( + IgnoredActors.includes( + req.body.pull_request?.user.login || req.body.sender?.login + ) + ) { + return; + } + + const embedGenerator = { + ping: generatePingEmbed, + star: generateStarEmbed, + issues: generateIssuesEmbed, + pull_request: generatePullEmbed, + issue_comment: generateCommentEmbed, + fork: generateForkEmbed + }; + + const isValidKey = ( + event: string + ): event is keyof typeof embedGenerator => { + return event in embedGenerator; + }; + + const content = isValidKey(event) + ? embedGenerator[event](req.body) + : null; + + if (content) { + const channel = (await bot.channels.fetch( + "1231028190403891212" + )) as GuildTextBasedChannel; + + await channel?.send({ + content + }); + } + + if (event === "pull_request") { + const owner = req.body.repository.owner.login; + const repo = req.body.repository.name; + const number = req.body.number; + const isMerged = + req.body.action === "closed" && req.body.pull_request.merged; + const github = new Octokit({ + auth: process.env.GITHUB_TOKEN + }); + if (isMerged && req.body.pull_request?.user.login !== "naomi-lgbt") { + await github.issues.createComment({ + owner, + repo, + issue_number: number, + body: ThankYou + }); + } + } + } catch (err) { + await errorHandler(bot, "github webhook", err); + } + }); + + const httpServer = http.createServer(app); + + httpServer.listen(9080, async () => { + await bot.env.debugHook.send({ + content: "http server listening on port 9080", + username: bot.user?.username ?? "bot", + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" + }); + }); + + if (process.env.NODE_ENV === "production") { + const privateKey = await readFile( + "/etc/letsencrypt/live/hooks.nhcarrigan.com/privkey.pem", + "utf8" + ); + const certificate = await readFile( + "/etc/letsencrypt/live/hooks.nhcarrigan.com/cert.pem", + "utf8" + ); + const ca = await readFile( + "/etc/letsencrypt/live/hooks.nhcarrigan.com/chain.pem", + "utf8" + ); + + const credentials = { + key: privateKey, + cert: certificate, + ca: ca + }; + + const httpsServer = https.createServer(credentials, app); + + httpsServer.listen(9443, async () => { + await bot.env.debugHook.send({ + content: "https server listening on port 9443", + username: bot.user?.username ?? "bot", + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" + }); + }); + } +}; diff --git a/src/utils/addCase.ts b/src/utils/addCase.ts new file mode 100644 index 0000000..1990479 --- /dev/null +++ b/src/utils/addCase.ts @@ -0,0 +1,51 @@ +import { Action } from "../interfaces/Action"; +import { ExtendedClient } from "../interfaces/ExtendedClient"; + +import { errorHandler } from "./errorHandler"; +import { generateTimestamp } from "./generateTimestamp"; + +/** + * Adds a case to the user record. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {string} serverId The server id. + * @param {string} userId The user's id. + * @param {string} reason The reason for the case. + * @param {string} action The action taken. + * @param {string} moderator The ID of the moderator that took the action. + * @param {string} evidence A link to the evidence. + * @returns {number} The case number. + */ +export const addCase = async ( + bot: ExtendedClient, + serverId: string, + userId: string, + reason: string, + action: Action, + moderator: string, + evidence: string[] +): Promise => { + try { + const existingCases = await bot.db.cases.count({ + where: { + serverId + } + }); + await bot.db.cases.create({ + data: { + serverId, + userId, + number: existingCases + 1, + reason, + action, + moderator, + evidence, + timestamp: generateTimestamp(Date.now()) + } + }); + return existingCases + 1; + } catch (err) { + await errorHandler(bot, "add case", err); + return 0; + } +}; diff --git a/src/utils/checkEntitledGuild.ts b/src/utils/checkEntitledGuild.ts new file mode 100644 index 0000000..489606a --- /dev/null +++ b/src/utils/checkEntitledGuild.ts @@ -0,0 +1,21 @@ +import { Guild } from "discord.js"; + +import { ExtendedClient } from "../interfaces/ExtendedClient"; + +/** + * Checks if a donation record exists for a specific server. Can be expanded + * after verification to check for Discord payments directly. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {Guild} guild The guild record from Discord. + * @returns {boolean} Whether an entitlement record exists for the guild ID in the database. + */ +export const checkEntitledGuild = async ( + bot: ExtendedClient, + guild: Guild +): Promise => + Boolean( + await bot.db.entitlements + .findFirst({ where: { serverId: guild.id } }) + .catch(() => null) + ); diff --git a/src/utils/customSubstring.ts b/src/utils/customSubstring.ts new file mode 100644 index 0000000..fb97da7 --- /dev/null +++ b/src/utils/customSubstring.ts @@ -0,0 +1,11 @@ +/** + * Determines if a string is longer than the given length, and if so + * substrings it and appends an ellipsis. + * + * @param {string} str The string to shorten. + * @param {number} len The maximum allowed length for the string. + * @returns {string} The potentially shortened string. + */ +export const customSubstring = (str: string, len: number): string => { + return str.length > len ? str.substring(0, len - 3) + "..." : str; +}; diff --git a/src/utils/errorHandler.ts b/src/utils/errorHandler.ts new file mode 100644 index 0000000..e49b6ba --- /dev/null +++ b/src/utils/errorHandler.ts @@ -0,0 +1,41 @@ +import { SnowflakeUtil } from "discord.js"; + +import { ExtendedClient } from "../interfaces/ExtendedClient"; + +import { logHandler } from "./logHandler"; + +/** + * Handles logging the error to the terminal and sending it to the debug webhook. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {string} context A brief description of where the error occurred. + * @param {Error} err The error object. + * @returns {string} A unique ID to use in logs. + */ +export const errorHandler = async ( + bot: ExtendedClient, + context: string, + err: unknown +) => { + const id = SnowflakeUtil.generate(); + const error = err as Error; + logHandler.log("error", `${context}: ${error.message}`); + logHandler.log("error", JSON.stringify(error.stack, null, 2)); + if (bot.env.debugHook) { + await bot.env.debugHook.send({ + content: `**${id}\n${context}: ${error.message}`, + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png", + username: bot.user?.username ?? "Mod bot" + }); + await bot.env.debugHook.send({ + content: "```\n" + JSON.stringify(error.stack, null, 2) + "\n```", + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png", + username: bot.user?.username ?? "Mod bot" + }); + } + return id; +}; diff --git a/src/utils/generateTimestamp.ts b/src/utils/generateTimestamp.ts new file mode 100644 index 0000000..8de299b --- /dev/null +++ b/src/utils/generateTimestamp.ts @@ -0,0 +1,10 @@ +/** + * Generates a Discord timestamp based on a millisecond input. + * + * @param {number} time The timestamp, in milliseconds. + * @returns {string} A discord formatted timestamp string. + */ +export const generateTimestamp = (time: number) => { + const seconds = Math.floor(time / 1000); + return ``; +}; diff --git a/src/utils/getArrayIndex.ts b/src/utils/getArrayIndex.ts new file mode 100644 index 0000000..ac56317 --- /dev/null +++ b/src/utils/getArrayIndex.ts @@ -0,0 +1,19 @@ +/** + * Gets the previous index in the array, looping as needed. + * + * @param {unknown[]} array The array to query. + * @param {number} index The current index. + * @returns {number} The previous index. + */ +export const getPreviousIndex = (array: unknown[], index: number) => + index === 0 ? array.length - 1 : index - 1; + +/** + * Gets the next index in the array, looping as needed. + * + * @param {unknown[]} array The array to query. + * @param {number} index The current index. + * @returns {number} The next index. + */ +export const getNextIndex = (array: unknown[], index: number) => + index === array.length - 1 ? 0 : index + 1; diff --git a/src/utils/isModerator.ts b/src/utils/isModerator.ts new file mode 100644 index 0000000..1509082 --- /dev/null +++ b/src/utils/isModerator.ts @@ -0,0 +1,20 @@ +import { GuildMember, PermissionFlagsBits } from "discord.js"; + +/** + * Takes a list of moderator permissions and checks if the member has + * any of those permissions. + * + * @param {GuildMember} member The member to check. + * @returns {boolean} True if the member has at least one of the configured permissions. + */ +export const isModerator = (member: GuildMember) => { + const modPermissions = [ + PermissionFlagsBits.BanMembers, + PermissionFlagsBits.KickMembers, + PermissionFlagsBits.ManageMessages, + PermissionFlagsBits.ManageGuild, + PermissionFlagsBits.ModerateMembers, + PermissionFlagsBits.Administrator + ]; + return modPermissions.some((perm) => member.permissions.has(perm)); +}; diff --git a/src/utils/loadCommands.ts b/src/utils/loadCommands.ts new file mode 100644 index 0000000..d9e64a9 --- /dev/null +++ b/src/utils/loadCommands.ts @@ -0,0 +1,36 @@ +import { readdir } from "fs/promises"; +import { join } from "path"; + +import { Command } from "../interfaces/Command"; +import { ExtendedClient } from "../interfaces/ExtendedClient"; + +import { errorHandler } from "./errorHandler"; + +/** + * Reads the `/commands` directory and dynamically imports the files, + * then pushes the imported data to an array. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @returns {Command[]} Array of Command objects representing the imported commands. + */ +export const loadCommands = async (bot: ExtendedClient): Promise => { + try { + const result: Command[] = []; + const files = await readdir( + join(process.cwd(), "prod", "commands"), + "utf-8" + ); + for (const file of files) { + const name = file.split(".")[0]; + if (!name) { + continue; + } + const mod = await import(join(process.cwd(), "prod", "commands", file)); + result.push(mod[name] as Command); + } + return result; + } catch (err) { + await errorHandler(bot, "slash command loader", err); + return []; + } +}; diff --git a/src/utils/loadContexts.ts b/src/utils/loadContexts.ts new file mode 100644 index 0000000..09b7f95 --- /dev/null +++ b/src/utils/loadContexts.ts @@ -0,0 +1,36 @@ +import { readdir } from "fs/promises"; +import { join } from "path"; + +import { Context } from "../interfaces/Context"; +import { ExtendedClient } from "../interfaces/ExtendedClient"; + +import { errorHandler } from "./errorHandler"; + +/** + * Reads the `/contexts` directory and dynamically imports the files, + * then pushes the imported data to an array. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @returns {Context[]} Array of Context objects representing the imported context commands. + */ +export const loadContexts = async (bot: ExtendedClient): Promise => { + try { + const result: Context[] = []; + const files = await readdir( + join(process.cwd(), "prod", "contexts"), + "utf-8" + ); + for (const file of files) { + const name = file.split(".")[0]; + if (!name) { + continue; + } + const mod = await import(join(process.cwd(), "prod", "contexts", file)); + result.push(mod[name] as Context); + } + return result; + } catch (err) { + await errorHandler(bot, "slash command loader", err); + return []; + } +}; diff --git a/src/utils/logHandler.ts b/src/utils/logHandler.ts new file mode 100644 index 0000000..e724c65 --- /dev/null +++ b/src/utils/logHandler.ts @@ -0,0 +1,24 @@ +import { createLogger, format, transports, config } from "winston"; + +const { combine, timestamp, colorize, printf } = format; + +/** + * Standard log handler, using winston to wrap and format + * messages. Call with `logHandler.log(level, message)`. + * + * @param {string} level - The log level to use. + * @param {string} message - The message to log. + */ +export const logHandler = createLogger({ + levels: config.npm.levels, + level: "silly", + transports: [new transports.Console()], + format: combine( + timestamp({ + format: "YYYY-MM-DD HH:mm:ss" + }), + colorize(), + printf((info) => `${info.level}: ${[info.timestamp]}: ${info.message}`) + ), + exitOnError: false +}); diff --git a/src/utils/processModAction.ts b/src/utils/processModAction.ts new file mode 100644 index 0000000..44e278f --- /dev/null +++ b/src/utils/processModAction.ts @@ -0,0 +1,120 @@ +import { CommandInteraction, Guild, User } from "discord.js"; + +import { Action, ActionToPastTense } from "../interfaces/Action"; +import { ExtendedClient } from "../interfaces/ExtendedClient"; +import { calculateMuteDuration } from "../modules/commands/calculateMuteDuration"; + +import { addCase } from "./addCase"; +import { errorHandler } from "./errorHandler"; +import { generateTimestamp } from "./generateTimestamp"; +import { sendLogMessage } from "./sendLogMessage"; +import { sendModDm } from "./sendModDm"; +import { triggerModRequest } from "./triggerModRequest"; + +/** + * Module to process a moderation action. Handles DMing, logging, and adding a case. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {CommandInteraction} interaction The interaction payload from Discord. + * @param {Guild} guild The server the action is taken in. + * @param {User} user The user being actioned. + * @param {action} action The action being taken. + * @param {string} rawReason The reason for the action. + * @param {string[]} evidence The evidence for the action. + * @param {number} duration If this is a mute, the duration for the mute. + * @param {string} durationUnit If this is a mute, the unit for the duration. + * @param {number} pruneDays If this is a ban, the number of days to delete messages. + */ +export const processModAction = async ( + bot: ExtendedClient, + interaction: CommandInteraction, + guild: Guild, + user: User, + action: Action, + rawReason: string, + evidence: string[], + duration?: number, + durationUnit?: string, + pruneDays?: number +) => { + try { + const calculatedDuration = calculateMuteDuration( + duration || 0, + durationUnit || "seconds" + ); + // cap at 28 days + const finalDuration = + calculatedDuration <= 2419200000 ? calculatedDuration : 2419200000; + const reason = + action === "mute" + ? `${rawReason}\n\nThis mute expires at ${generateTimestamp( + Math.round(finalDuration + Date.now()) + )}` + : rawReason; + const notified = + action === "note" || action === "unban" + ? false + : await sendModDm(bot, action, user, guild, reason); + const caseNum = await addCase( + bot, + guild.id, + user.id, + reason, + action, + interaction.user.tag, + evidence + ); + await sendLogMessage( + bot, + guild, + user, + action, + reason, + interaction.user.id, + evidence, + notified, + caseNum + ); + + if (!caseNum) { + await interaction.editReply({ + content: + "There was a failure in generating a case. Is it possible someone else was actioning this user too? Please try again.", + components: [] + }); + } + + const success = + action === "note" || action === "warn" + ? true + : await triggerModRequest(bot, { + userId: user.id, + serverId: guild.id, + action, + reason, + moderator: interaction.user.id, + duration: finalDuration, + pruneDays + }); + + if (!success) { + await interaction.editReply({ + content: `Failed to ${action} ${user.tag} - please have Naomi check the logs!!!`, + components: [] + }); + return; + } + + const confirmation = + action === "warn" && !notified + ? `${ActionToPastTense[action]} ${user.tag} for ${reason} - but was not able to DM them.` + : `${ActionToPastTense[action]} ${user.tag} for ${reason}`; + + await interaction.editReply({ + content: confirmation, + components: [] + }); + } catch (err) { + await errorHandler(bot, "process mod action", err); + } +}; diff --git a/src/utils/registerCommands.ts b/src/utils/registerCommands.ts new file mode 100644 index 0000000..fc54566 --- /dev/null +++ b/src/utils/registerCommands.ts @@ -0,0 +1,44 @@ +import { REST, Routes } from "discord.js"; + +import { ExtendedClient } from "../interfaces/ExtendedClient"; + +import { errorHandler } from "./errorHandler"; +import { sendDebugMessage } from "./sendDebugMessage"; + +/** + * Registers the commands for the bot. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @returns {boolean} True on successful registration. + */ +export const registerCommands = async ( + bot: ExtendedClient +): Promise => { + try { + if (!bot.user?.id) { + await sendDebugMessage( + bot, + "Cannot register commands as bot has not authenticated to Discord." + ); + return false; + } + const rest = new REST({ version: "10" }).setToken(bot.env.token); + const commandData = bot.commands.map((command) => command.data.toJSON()); + const contextData = bot.contexts.map((context) => context.data); + const combinedData = [...commandData, ...contextData]; + + if (!commandData.length) { + await sendDebugMessage(bot, "No commands found to register."); + return false; + } + + await sendDebugMessage(bot, "Received a request to register commands."); + await rest.put(Routes.applicationCommands(bot.user.id), { + body: combinedData + }); + return true; + } catch (err) { + await errorHandler(bot, "register commands", err); + return false; + } +}; diff --git a/src/utils/sendDebugMessage.ts b/src/utils/sendDebugMessage.ts new file mode 100644 index 0000000..d4bd8f7 --- /dev/null +++ b/src/utils/sendDebugMessage.ts @@ -0,0 +1,22 @@ +import { ExtendedClient } from "../interfaces/ExtendedClient"; + +/** + * Sends a log message to the worker log hook. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {string} message The message to send. + */ +export const sendDebugMessage = async ( + bot: ExtendedClient, + message: string +) => { + if (bot.env.debugHook) { + await bot.env.debugHook.send({ + content: message, + avatarURL: + bot.user?.displayAvatarURL() ?? + "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png", + username: bot.user?.username ?? "Mod bot" + }); + } +}; diff --git a/src/utils/sendLogMessage.ts b/src/utils/sendLogMessage.ts new file mode 100644 index 0000000..9d4cde9 --- /dev/null +++ b/src/utils/sendLogMessage.ts @@ -0,0 +1,90 @@ +import { Guild, EmbedBuilder, User } from "discord.js"; + +import { EmbedColours } from "../config/EmbedColours"; +import { Action, ActionToPastTense } from "../interfaces/Action"; +import { ExtendedClient } from "../interfaces/ExtendedClient"; +import { getConfig } from "../modules/data/getConfig"; + +import { customSubstring } from "./customSubstring"; +import { errorHandler } from "./errorHandler"; + +/** + * Sends a manual moderation log. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {Guild} guild The guild the action was taken in. + * @param {User} user The user that was actioned. + * @param {Action} action The action that was taken. + * @param {string} reason The reason for the action. + * @param {string} moderatorId The moderator that took the action. + * @param {string[]} evidence The evidence for the action. + * @param {boolean} notified Whether the user was notified of the action. + * @param {number} caseNum The number assigned to the case. + * @returns {string | null} The URL of the log message, or null if not sent. + */ +export const sendLogMessage = async ( + bot: ExtendedClient, + guild: Guild, + user: User, + action: Action, + reason: string, + moderatorId: string, + evidence: string[], + notified: boolean, + caseNum: number +): Promise<{ url: string; embed: EmbedBuilder } | null> => { + try { + const config = await getConfig(bot, guild.id); + if (!config.modLogChannel) { + return null; + } + + const channel = + guild.channels.cache.get(config.modLogChannel) || + (await guild.channels.fetch(config.modLogChannel)); + + if (!channel || !("send" in channel)) { + return null; + } + + const embed = new EmbedBuilder(); + embed.setTitle(`Case ${caseNum}: User ${ActionToPastTense[action]}!`); + embed.setDescription(customSubstring(reason, 4000)); + embed.addFields( + { + name: "Moderator", + value: `<@!${moderatorId}>`, + inline: true + }, + { + name: "User", + value: `<@${user.id}>`, + inline: true + }, + { + name: "Evidence", + value: evidence.length > 0 ? evidence.join(", ") : "None", + inline: false + }, + { + name: "DM sent?", + value: notified ? "Yes" : "No", + inline: true + } + ); + embed.setColor(EmbedColours[action]); + embed.setAuthor({ + name: user.tag, + iconURL: user.displayAvatarURL() + }); + embed.setFooter({ + text: `User ID: ${user.id}` + }); + + const caseLog = await channel.send({ embeds: [embed] }); + return { url: caseLog.url, embed }; + } catch (err) { + await errorHandler(bot, "send log message", err); + return null; + } +}; diff --git a/src/utils/sendModDm.ts b/src/utils/sendModDm.ts new file mode 100644 index 0000000..69b9082 --- /dev/null +++ b/src/utils/sendModDm.ts @@ -0,0 +1,57 @@ +import { Guild, User } from "discord.js"; + +import { Action, ActionToPastTense } from "../interfaces/Action"; +import { ExtendedClient } from "../interfaces/ExtendedClient"; +import { getConfig } from "../modules/data/getConfig"; + +import { customSubstring } from "./customSubstring"; +import { errorHandler } from "./errorHandler"; + +/** + * Sends a moderation notice to a user. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {string} action The action taken on the user. + * @param {User} user The user to DM. + * @param {Guild} guild The server the infraction occurred in. + * @param {string} reason The reason for the action. + * @returns {boolean} True if the DM was successful, false otherwise. + */ +export const sendModDm = async ( + bot: ExtendedClient, + action: Action, + user: User, + guild: Guild, + reason: string +): Promise => { + try { + const config = await getConfig(bot, guild.id); + let content = `Hello ${user.username}!\n\nYou have been ${ + ActionToPastTense[action] + } ${["kick", "ban", "softban"].includes(action) ? "from" : "in"} ${ + guild.name + } for: ${customSubstring(reason, 3000)}`; + if (action === "ban") { + content += `\n\nYou can appeal the ban by following this link:\n${config.banAppealLink}`; + } else if ( + (action === "kick" || action === "softban") && + config.inviteLink + ) { + content += `\n\nIf you'd like to rejoin our community, you can find an invite link here: ${config.inviteLink}\n\nPlease keep in mind that breaking the rules after rejoining can escalate to a ban.`; + } else { + content += + "\n\nIf you think this was a mistake or wish to discuss this, please DM our ModMail bot!"; + } + + const success = await user + .send({ + content + }) + .then(() => true) + .catch(() => false); + return success; + } catch (err) { + await errorHandler(bot, "send moderation dm", err); + return false; + } +}; diff --git a/src/utils/triggerModRequest.ts b/src/utils/triggerModRequest.ts new file mode 100644 index 0000000..6bf4842 --- /dev/null +++ b/src/utils/triggerModRequest.ts @@ -0,0 +1,126 @@ +import { ActionPayload } from "../interfaces/ActionPayload"; +import { ExtendedClient } from "../interfaces/ExtendedClient"; + +import { errorHandler } from "./errorHandler"; +import { sendDebugMessage } from "./sendDebugMessage"; + +/** + * Processes a moderation request. + * + * @param {ExtendedClient} bot The bot's Discord instance. + * @param {ActionPayload} actionPayload The moderation action data. + * @returns {boolean} True if the worker reports success, false otherwise. + */ +export const triggerModRequest = async ( + bot: ExtendedClient, + actionPayload: ActionPayload +): Promise => { + try { + if (actionPayload.action === "note") { + return true; + } + const guild = await bot.guilds.fetch(actionPayload.serverId); + if (!guild) { + await sendDebugMessage( + bot, + `Tried to ${actionPayload.action} user ${actionPayload.userId} in guild ${actionPayload.serverId} but:\n\nCould not find guild ${actionPayload.serverId}` + ); + return false; + } + + if (actionPayload.action === "unban") { + if (!(await guild.bans.fetch(actionPayload.userId).catch(() => null))) { + await sendDebugMessage( + bot, + `Could not find ban for user ${actionPayload.userId}` + ); + return false; + } + await guild.bans.remove( + actionPayload.userId, + `By ${actionPayload.moderator} : ${actionPayload.reason}` + ); + return true; + } + + const member = await guild.members + .fetch(actionPayload.userId) + .catch(() => null); + + if (!member && actionPayload.action !== "ban") { + return false; + } + + const reason = `By ${actionPayload.moderator} : ${actionPayload.reason}`; + + switch (actionPayload.action) { + case "softban": + if (!member) { + await guild.bans.create(actionPayload.userId, { + reason, + deleteMessageSeconds: (actionPayload.pruneDays ?? 1) * 86400 + }); + await guild.bans.remove(actionPayload.userId, "Soft ban."); + return true; + } + await member.ban({ + reason, + deleteMessageSeconds: (actionPayload.pruneDays ?? 1) * 86400 + }); + await guild.bans.remove(actionPayload.userId, "Soft ban."); + return true; + case "ban": + if (!member) { + await guild.bans.create(actionPayload.userId, { + reason, + deleteMessageSeconds: (actionPayload.pruneDays ?? 1) * 86400 + }); + return true; + } + await member.ban({ + reason, + deleteMessageSeconds: (actionPayload.pruneDays || 0) * 86400 + }); + return true; + case "kick": + if (!member) { + await sendDebugMessage( + bot, + `Tried to ${actionPayload.action} user ${actionPayload.userId} in guild ${actionPayload.serverId} but:\n\nCould not find member ${actionPayload.userId}` + ); + return false; + } + await member.kick(reason); + return true; + case "mute": + if (!member) { + await sendDebugMessage( + bot, + `Could not find member ${actionPayload.userId}` + ); + return false; + } + await member.timeout(actionPayload.duration || null, reason); + return true; + case "unmute": + if (!member) { + await sendDebugMessage( + bot, + `Tried to ${actionPayload.action} user ${actionPayload.userId} in guild ${actionPayload.serverId} but:\n\nCould not find member ${actionPayload.userId}` + ); + return false; + } + await member.timeout(null, reason); + return true; + default: + await sendDebugMessage( + bot, + `I received a mod action of ${actionPayload.action} but don't know what to do with it!!` + ); + return false; + } + } catch (err) { + await errorHandler(bot, "handle mod action", err); + return false; + } +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..679dcfb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@nhcarrigan/typescript-config", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./prod" + }, + "exclude": ["./test"] +}