From 27bf3cd0071bb351188b3862aefd0a229195a389 Mon Sep 17 00:00:00 2001 From: Samuel Shuert Date: Fri, 3 Mar 2023 15:51:25 -0500 Subject: [PATCH 1/2] Development (#11) We need this NOW. --------- Co-authored-by: PineaFan Co-authored-by: pineafan Co-authored-by: PineappleFan Co-authored-by: Skyler --- .eslintrc.json | 2 +- .gitignore | 4 +- .prettierignore | 2 +- ClicksMigratingProblems/randomPunishments.js | 2 +- SELF_HOSTING.md | 2 +- TODO | 14 - TODO.json | 27 - events2do | 30 - package-lock.json | 5094 ----------------- package.json | 21 +- src/Unfinished/all.ts | 30 +- ...orisationTest.ts => categorizationTest.ts} | 52 +- src/actions/createModActionTicket.ts | 10 +- src/actions/roleMenu.ts | 61 +- src/actions/tickets/create.ts | 14 +- src/actions/tickets/delete.ts | 35 +- src/api/index.ts | 45 +- src/commands/help.ts | 198 +- src/commands/mod/_meta.ts | 2 +- src/commands/mod/about.ts | 37 +- src/commands/mod/ban.ts | 34 +- src/commands/mod/kick.ts | 40 +- src/commands/mod/mute.ts | 41 +- src/commands/mod/nick.ts | 27 +- src/commands/mod/purge.ts | 110 +- src/commands/mod/slowmode.ts | 15 +- src/commands/mod/softban.ts | 27 +- src/commands/mod/unban.ts | 13 +- src/commands/mod/unmute.ts | 23 +- src/commands/mod/viewas.ts | 14 +- src/commands/mod/warn.ts | 21 +- src/commands/nucleus/_meta.ts | 6 +- src/commands/nucleus/guide.ts | 7 +- src/commands/nucleus/invite.ts | 7 +- src/commands/nucleus/ping.ts | 11 +- src/commands/nucleus/premium.ts | 203 +- src/commands/nucleus/stats.ts | 9 +- src/commands/nucleus/suggest.ts | 7 +- src/commands/privacy.ts | 63 +- src/commands/role/_meta.ts | 8 - src/commands/role/user.ts | 99 - src/commands/rolemenu.ts | 8 +- src/commands/server/about.ts | 7 +- src/commands/server/buttons.ts | 252 + src/commands/settings/automod.ts | 922 +++ src/commands/settings/autopublish.ts | 96 + src/commands/settings/filters.ts | 21 - src/commands/settings/logs/attachment.ts | 248 +- src/commands/settings/logs/channel.ts | 197 - src/commands/settings/logs/events.ts | 186 +- src/commands/settings/logs/staff.ts | 201 - src/commands/settings/logs/warnings.ts | 104 + .../settings/{commands.ts => moderation.ts} | 84 +- src/commands/settings/rolemenu.ts | 473 +- src/commands/settings/stats.ts | 586 +- src/commands/settings/tickets.ts | 578 +- src/commands/settings/tracks.ts | 459 ++ src/commands/settings/verify.ts | 414 +- src/commands/settings/welcome.ts | 506 +- src/commands/tag.ts | 8 +- src/commands/tags/create.ts | 4 +- src/commands/tags/delete.ts | 4 +- src/commands/tags/edit.ts | 10 +- src/commands/tags/list.ts | 10 +- src/commands/ticket/close.ts | 7 +- src/commands/ticket/create.ts | 6 +- src/commands/user/about.ts | 18 +- src/commands/user/avatar.ts | 6 +- src/commands/user/role.ts | 160 + src/commands/user/track.ts | 68 +- src/commands/verify.ts | 7 +- src/config/default.json | 100 - src/config/emojis.json | 41 +- src/config/format.ts | 62 +- src/config/main.d.ts | 26 + src/config/main.json | 20 - src/context/messages/purgeto.ts | 69 +- src/context/users/userinfo.ts | 2 + src/events/channelCreate.ts | 5 +- src/events/channelDelete.ts | 6 +- src/events/channelUpdate.ts | 38 +- src/events/emojiCreate.ts | 5 +- src/events/emojiDelete.ts | 5 +- src/events/emojiUpdate.ts | 5 +- src/events/guildBanAdd.ts | 9 +- src/events/guildBanRemove.ts | 11 +- src/events/guildMemberUpdate.ts | 91 +- src/events/guildUpdate.ts | 7 +- src/events/interactionCreate.ts | 4 +- src/events/inviteCreate.ts | 5 +- src/events/inviteDelete.ts | 7 +- src/events/memberJoin.ts | 5 +- src/events/memberLeave.ts | 40 +- src/events/messageCreate.ts | 41 +- src/events/messageDelete.ts | 110 +- src/events/messageEdit.ts | 10 +- src/events/roleCreate.ts | 3 +- src/events/roleDelete.ts | 5 +- src/events/roleUpdate.ts | 17 +- src/events/stickerCreate.ts | 12 +- src/events/stickerDelete.ts | 5 +- src/events/stickerUpdate.ts | 4 +- src/events/threadCreate.ts | 5 +- src/events/threadDelete.ts | 7 +- src/events/threadUpdate.ts | 7 +- src/events/webhookUpdate.ts | 30 +- src/index.ts | 13 +- src/premium/attachmentLogs.ts | 14 +- src/premium/createTranscript.ts | 160 +- src/reflex/guide.ts | 145 +- src/reflex/scanners.ts | 61 +- src/reflex/statsChannelUpdate.ts | 4 +- src/reflex/verify.ts | 9 +- src/reflex/welcome.ts | 8 +- src/utils/calculate.ts | 8 +- src/utils/client.ts | 31 +- src/utils/commandRegistration/register.ts | 73 +- .../slashCommandBuilder.ts | 24 +- src/utils/confirmationMessage.ts | 26 +- src/utils/convertCurlyBracketString.ts | 2 +- src/utils/createPageIndicator.ts | 31 +- src/utils/createTemporaryStorage.ts | 4 +- src/utils/database.ts | 479 +- src/utils/dualCollector.ts | 55 +- src/utils/ellipsis.ts | 4 + src/utils/eventScheduler.ts | 9 +- src/utils/generateEmojiEmbed.ts | 12 +- src/utils/getCommandDataByName.ts | 28 + src/utils/getCommandMentionByName.ts | 22 - src/utils/getEmojiByName.ts | 8 +- src/utils/listToAndMore.ts | 7 + src/utils/log.ts | 54 +- src/utils/logTranscripts.ts | 64 - src/utils/memory.ts | 4 +- src/utils/performanceTesting/record.ts | 4 +- src/utils/singleNotify.ts | 21 +- src/utils/temp/generateFileName.ts | 2 +- tsconfig.json | 4 +- 138 files changed, 5674 insertions(+), 8652 deletions(-) delete mode 100644 TODO.json delete mode 100644 events2do delete mode 100644 package-lock.json rename src/Unfinished/{categorisationTest.ts => categorizationTest.ts} (71%) delete mode 100644 src/commands/role/_meta.ts delete mode 100644 src/commands/role/user.ts create mode 100644 src/commands/server/buttons.ts create mode 100644 src/commands/settings/automod.ts create mode 100644 src/commands/settings/autopublish.ts delete mode 100644 src/commands/settings/filters.ts delete mode 100644 src/commands/settings/logs/channel.ts delete mode 100644 src/commands/settings/logs/staff.ts create mode 100644 src/commands/settings/logs/warnings.ts rename src/commands/settings/{commands.ts => moderation.ts} (74%) create mode 100644 src/commands/settings/tracks.ts create mode 100644 src/commands/user/role.ts delete mode 100644 src/config/default.json create mode 100644 src/config/main.d.ts delete mode 100644 src/config/main.json create mode 100644 src/utils/ellipsis.ts create mode 100644 src/utils/getCommandDataByName.ts delete mode 100644 src/utils/getCommandMentionByName.ts create mode 100644 src/utils/listToAndMore.ts delete mode 100644 src/utils/logTranscripts.ts diff --git a/.eslintrc.json b/.eslintrc.json index 4b4e25d..165e759 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,7 +4,7 @@ "es2020": true, "node": true }, - "ignorePatterns": ["dist/", "src/Unfinished/"], + "ignorePatterns": ["dist/", "src/Unfinished/", "src/config/main.d.ts", "*.js"], "extends": ["eslint:recommended", "plugin:@typescript-eslint/strict", "prettier"], "parser": "@typescript-eslint/parser", "parserOptions": { diff --git a/.gitignore b/.gitignore index db17304..b8b8506 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,12 @@ dist/ .history/ node_modules/ src/config/* + +!src/config/*.d.ts !src/config/format.ts !src/config/default.json !src/config/emojis.json -src/config/main.json +src/config/main.ts .vscode/ .vim/ yarn-error.log diff --git a/.prettierignore b/.prettierignore index bfedc85..7575e2d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,7 +5,7 @@ src/config/* !src/config/format.ts !src/config/default.json !src/config/emojis.json -src/config/main.json +!src/config/main.ts .vscode/ yarn-error.log yarn.lock diff --git a/ClicksMigratingProblems/randomPunishments.js b/ClicksMigratingProblems/randomPunishments.js index af9c908..432a9f8 100644 --- a/ClicksMigratingProblems/randomPunishments.js +++ b/ClicksMigratingProblems/randomPunishments.js @@ -11,7 +11,7 @@ for (let i = 0; i < 100; i++) { Math.floor(Math.random() * 9) ]; // Select a random date in the last year - let date = new Date(new Date().getTime() - Math.floor(Math.random() * 31536000000)); + let date = new Date(Date.now() - Math.floor(Math.random() * 31536000000)); // Add to database await collection.insertOne({ type: type, diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md index 4f01364..1298cd4 100644 --- a/SELF_HOSTING.md +++ b/SELF_HOSTING.md @@ -23,7 +23,7 @@ However, you **must**: ## How to: -We hide the config file with our important data like the bot token. Below you can find a copy of `src/config/main.json`. +We hide the config file with our important data like the bot token. Below you can find a copy of `src/config/main.ts`. Alternatively, you can run `Installer.js` to generate it for you. ```json diff --git a/TODO b/TODO index d2cd1a2..91d025a 100644 --- a/TODO +++ b/TODO @@ -1,16 +1,2 @@ -Role all Server rules verificationRequired on welcome -// TODO !IMPORTANT! URL + image hash + file hash database - -ROLE MENU SETTINGS - -**Title** -> Description -name: role -name: role -name: role - -[ Select an option to remove ] -[ Reorder roles ] -< Previous page | Add role | Delete page | Add page > diff --git a/TODO.json b/TODO.json deleted file mode 100644 index 6f0b94e..0000000 --- a/TODO.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "filters": { - "images": { - "NSFW": false, - "size": false - }, - "malware": false, - "wordFilter": { - "enabled": true, - "words": { - "strict": [], - "loose": [] - } - }, - "invite": { - "enabled": false, - "channels": [] - }, - "pings": { - "mass": 5, - "everyone": true, - "roles": true - } - }, - "roleMenu": [], - "tracks": [] -} diff --git a/events2do b/events2do deleted file mode 100644 index 15f4cbd..0000000 --- a/events2do +++ /dev/null @@ -1,30 +0,0 @@ --rw-r--r-- 1 pineapplefan pineapplefan 2735 Dec 29 11:03 channelCreate.ts --rw-r--r-- 1 pineapplefan pineapplefan 3661 Dec 29 11:04 channelDelete.ts --rw-r--r-- 1 pineapplefan pineapplefan 7430 Nov 15 20:45 channelUpdate.ts --rw-r--r-- 1 pineapplefan pineapplefan 960 Nov 15 20:39 commandError.ts --rw-r--r-- 1 pineapplefan pineapplefan 1097 Aug 8 21:15 emojiCreate.ts --rw-r--r-- 1 pineapplefan pineapplefan 1184 Aug 8 21:15 emojiDelete.ts --rw-r--r-- 1 pineapplefan pineapplefan 1183 Aug 8 21:15 emojiUpdate.ts --rw-r--r-- 1 pineapplefan pineapplefan 1849 Dec 29 11:04 guildBanAdd.ts --rw-r--r-- 1 pineapplefan pineapplefan 1558 Dec 29 11:04 guildBanRemove.ts --rw-r--r-- 1 pineapplefan pineapplefan 267 Dec 29 11:04 guildCreate.ts --rw-r--r-- 1 pineapplefan pineapplefan 5553 Dec 29 11:04 guildMemberUpdate.ts --rw-r--r-- 1 pineapplefan pineapplefan 3863 Jan 6 17:42 guildUpdate.ts --rw-r--r-- 1 pineapplefan pineapplefan 1710 Jan 6 18:38 interactionCreate.ts --rw-r--r-- 1 pineapplefan pineapplefan 1482 Dec 29 11:04 inviteCreate.ts --rw-r--r-- 1 pineapplefan pineapplefan 1471 Dec 29 11:04 inviteDelete.ts --rw-r--r-- 1 pineapplefan pineapplefan 1387 Dec 29 11:04 memberJoin.ts --rw-r--r-- 1 pineapplefan pineapplefan 3260 Jan 2 21:41 memberLeave.ts --rw-r--r-- 1 pineapplefan pineapplefan 14919 Dec 29 11:04 messageCreate.ts --rw-r--r-- 1 pineapplefan pineapplefan 4907 Dec 29 11:04 ! messageDelete.ts --rw-r--r-- 1 pineapplefan pineapplefan 4907 Dec 29 11:04 ? messageEdit.ts: Check message publishing --rw-r--r-- 1 pineapplefan pineapplefan 1268 Dec 29 11:04 roleCreate.ts --rw-r--r-- 1 pineapplefan pineapplefan 1915 Dec 29 11:04 roleDelete.ts --rw-r--r-- 1 pineapplefan pineapplefan 2562 Dec 29 11:04 roleUpdate.ts --rw-r--r-- 1 pineapplefan pineapplefan 1262 Dec 29 11:04 stickerCreate.ts --rw-r--r-- 1 pineapplefan pineapplefan 1349 Dec 29 11:04 stickerDelete.ts --rw-r--r-- 1 pineapplefan pineapplefan 1272 Dec 29 11:04 stickerUpdate.ts --rw-r--r-- 1 pineapplefan pineapplefan 1967 Dec 29 11:04 threadCreate.ts --rw-r--r-- 1 pineapplefan pineapplefan 2140 Dec 29 11:04 threadDelete.ts --rw-r--r-- 1 pineapplefan pineapplefan 2464 Dec 29 11:04 threadUpdate.ts --rw-r--r-- 1 pineapplefan pineapplefan 6352 Dec 29 11:04 webhookUpdate.ts \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 0d56268..0000000 --- a/package-lock.json +++ /dev/null @@ -1,5094 +0,0 @@ -{ - "name": "nucleus", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nucleus", - "version": "0.0.1", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@discordjs/rest": "^0.2.0-canary.0", - "@hokify/agenda": "^6.2.12", - "@tsconfig/node18-strictest-esm": "^1.0.0", - "@types/node-cron": "^3.0.1", - "@ungap/structured-clone": "^1.0.1", - "agenda": "^4.3.0", - "ansi-styles": "^6.1.0", - "body-parser": "^1.20.0", - "chalk": "^5.0.0", - "deno": "^0.1.1", - "discord.js": "14.7.1", - "eslint": "^8.21.0", - "express": "^4.18.1", - "form-data": "^4.0.0", - "fuse.js": "^6.6.2", - "humanize-duration": "^3.27.1", - "immutable": "^4.1.0", - "mongodb": "^4.7.0", - "node-cron": "^3.0.0", - "node-fetch": "^3.3.0", - "node-tesseract-ocr": "^2.2.1", - "pastebin-api": "^5.1.1", - "structured-clone": "^0.2.2", - "systeminformation": "^5.17.3", - "typescript": "^5.0.0-dev.20230102", - "uuid": "^8.3.2" - }, - "devDependencies": { - "@typescript-eslint/eslint-plugin": "^5.32.0", - "@typescript-eslint/parser": "^5.32.0", - "eslint-config-prettier": "^8.5.0", - "prettier": "^2.7.1", - "prettier-eslint": "^15.0.1", - "tsc-suppress": "^1.0.7" - } - }, - "node_modules/@aws-crypto/ie11-detection": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz", - "integrity": "sha512-5XDMQY98gMAf/WRTic5G++jfmS/VLM0rwpiOpaainKi4L0nqWMSB1SzsrEG5rjFZGYN6ZAefO+/Yta2dFM0kMw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD", - "optional": true - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz", - "integrity": "sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-crypto/ie11-detection": "^2.0.0", - "@aws-crypto/sha256-js": "^2.0.0", - "@aws-crypto/supports-web-crypto": "^2.0.0", - "@aws-crypto/util": "^2.0.0", - "@aws-sdk/types": "^3.1.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@aws-crypto/sha256-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-2.0.2.tgz", - "integrity": "sha512-iXLdKH19qPmIC73fVCrHWCSYjN/sxaAvZ3jNNyw6FclmHyjLKg0f69WlC9KTnyElxCR5MO9SKaG00VwlJwyAkQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-crypto/util": "^2.0.2", - "@aws-sdk/types": "^3.110.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD", - "optional": true - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz", - "integrity": "sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-crypto/util": "^2.0.0", - "@aws-sdk/types": "^3.1.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD", - "optional": true - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.2.tgz", - "integrity": "sha512-6mbSsLHwZ99CTOOswvCRP3C+VCWnzBf+1SnbWxzzJ9lR0mA0JnY2JEAhp8rqmTE0GPFy88rrM27ffgp62oErMQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD", - "optional": true - }, - "node_modules/@aws-crypto/util": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-2.0.2.tgz", - "integrity": "sha512-Lgu5v/0e/BcrZ5m/IWqzPUf3UYFTy/PpeED+uc9SWUR1iZQL8XXbGQg10UfllwwBryO3hFF5dizK+78aoXC1eA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "^3.110.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/util/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD", - "optional": true - }, - "node_modules/@aws-sdk/abort-controller": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.226.0.tgz", - "integrity": "sha512-cJVzr1xxPBd08voknXvR0RLgtZKGKt6WyDpH/BaPCu3rfSqWCDZKzwqe940eqosjmKrxC6pUZNKASIqHOQ8xxQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.252.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.252.0.tgz", - "integrity": "sha512-IHdrzMUGEQcUP7vN/wbVbRCHBXhC0nyaRxnnoHbrJfh5fzPSnkwo7qNf0e8ox+GXq8xgM58dEXefA6/TMYQPFQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-crypto/sha256-browser": "2.0.0", - "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/client-sts": "3.252.0", - "@aws-sdk/config-resolver": "3.234.0", - "@aws-sdk/credential-provider-node": "3.252.0", - "@aws-sdk/fetch-http-handler": "3.226.0", - "@aws-sdk/hash-node": "3.226.0", - "@aws-sdk/invalid-dependency": "3.226.0", - "@aws-sdk/middleware-content-length": "3.226.0", - "@aws-sdk/middleware-endpoint": "3.226.0", - "@aws-sdk/middleware-host-header": "3.226.0", - "@aws-sdk/middleware-logger": "3.226.0", - "@aws-sdk/middleware-recursion-detection": "3.226.0", - "@aws-sdk/middleware-retry": "3.235.0", - "@aws-sdk/middleware-serde": "3.226.0", - "@aws-sdk/middleware-signing": "3.226.0", - "@aws-sdk/middleware-stack": "3.226.0", - "@aws-sdk/middleware-user-agent": "3.226.0", - "@aws-sdk/node-config-provider": "3.226.0", - "@aws-sdk/node-http-handler": "3.226.0", - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/smithy-client": "3.234.0", - "@aws-sdk/types": "3.226.0", - "@aws-sdk/url-parser": "3.226.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.234.0", - "@aws-sdk/util-defaults-mode-node": "3.234.0", - "@aws-sdk/util-endpoints": "3.245.0", - "@aws-sdk/util-retry": "3.229.0", - "@aws-sdk/util-user-agent-browser": "3.226.0", - "@aws-sdk/util-user-agent-node": "3.226.0", - "@aws-sdk/util-utf8-browser": "3.188.0", - "@aws-sdk/util-utf8-node": "3.208.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.252.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.252.0.tgz", - "integrity": "sha512-VgBqJvvCU4y9zAHJwYj5nOeNGcCxKdCO4edUxWQVHcpLsVWu49maOVtWuteq9MOrHYeWfQi8bVWGt8MPvv9+bA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-crypto/sha256-browser": "2.0.0", - "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/config-resolver": "3.234.0", - "@aws-sdk/fetch-http-handler": "3.226.0", - "@aws-sdk/hash-node": "3.226.0", - "@aws-sdk/invalid-dependency": "3.226.0", - "@aws-sdk/middleware-content-length": "3.226.0", - "@aws-sdk/middleware-endpoint": "3.226.0", - "@aws-sdk/middleware-host-header": "3.226.0", - "@aws-sdk/middleware-logger": "3.226.0", - "@aws-sdk/middleware-recursion-detection": "3.226.0", - "@aws-sdk/middleware-retry": "3.235.0", - "@aws-sdk/middleware-serde": "3.226.0", - "@aws-sdk/middleware-stack": "3.226.0", - "@aws-sdk/middleware-user-agent": "3.226.0", - "@aws-sdk/node-config-provider": "3.226.0", - "@aws-sdk/node-http-handler": "3.226.0", - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/smithy-client": "3.234.0", - "@aws-sdk/types": "3.226.0", - "@aws-sdk/url-parser": "3.226.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.234.0", - "@aws-sdk/util-defaults-mode-node": "3.234.0", - "@aws-sdk/util-endpoints": "3.245.0", - "@aws-sdk/util-retry": "3.229.0", - "@aws-sdk/util-user-agent-browser": "3.226.0", - "@aws-sdk/util-user-agent-node": "3.226.0", - "@aws-sdk/util-utf8-browser": "3.188.0", - "@aws-sdk/util-utf8-node": "3.208.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.252.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.252.0.tgz", - "integrity": "sha512-OOwfEXFS+UliGZorEleARsXXUp3ObZSXo9/YY+8XF7/8froAqYjKCEi0tflghgYlh7d6qe7wzD7/6gDL1a/qgA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-crypto/sha256-browser": "2.0.0", - "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/config-resolver": "3.234.0", - "@aws-sdk/fetch-http-handler": "3.226.0", - "@aws-sdk/hash-node": "3.226.0", - "@aws-sdk/invalid-dependency": "3.226.0", - "@aws-sdk/middleware-content-length": "3.226.0", - "@aws-sdk/middleware-endpoint": "3.226.0", - "@aws-sdk/middleware-host-header": "3.226.0", - "@aws-sdk/middleware-logger": "3.226.0", - "@aws-sdk/middleware-recursion-detection": "3.226.0", - "@aws-sdk/middleware-retry": "3.235.0", - "@aws-sdk/middleware-serde": "3.226.0", - "@aws-sdk/middleware-stack": "3.226.0", - "@aws-sdk/middleware-user-agent": "3.226.0", - "@aws-sdk/node-config-provider": "3.226.0", - "@aws-sdk/node-http-handler": "3.226.0", - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/smithy-client": "3.234.0", - "@aws-sdk/types": "3.226.0", - "@aws-sdk/url-parser": "3.226.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.234.0", - "@aws-sdk/util-defaults-mode-node": "3.234.0", - "@aws-sdk/util-endpoints": "3.245.0", - "@aws-sdk/util-retry": "3.229.0", - "@aws-sdk/util-user-agent-browser": "3.226.0", - "@aws-sdk/util-user-agent-node": "3.226.0", - "@aws-sdk/util-utf8-browser": "3.188.0", - "@aws-sdk/util-utf8-node": "3.208.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sts": { - "version": "3.252.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.252.0.tgz", - "integrity": "sha512-wzfsWOlDFLdmeML8R7DUJWGl9wcRKf2uiunfB1aWzpdlgms0Z7FkHWgkDYHjCPyYHL6EBm84ajGl1UkE7AcmqQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-crypto/sha256-browser": "2.0.0", - "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/config-resolver": "3.234.0", - "@aws-sdk/credential-provider-node": "3.252.0", - "@aws-sdk/fetch-http-handler": "3.226.0", - "@aws-sdk/hash-node": "3.226.0", - "@aws-sdk/invalid-dependency": "3.226.0", - "@aws-sdk/middleware-content-length": "3.226.0", - "@aws-sdk/middleware-endpoint": "3.226.0", - "@aws-sdk/middleware-host-header": "3.226.0", - "@aws-sdk/middleware-logger": "3.226.0", - "@aws-sdk/middleware-recursion-detection": "3.226.0", - "@aws-sdk/middleware-retry": "3.235.0", - "@aws-sdk/middleware-sdk-sts": "3.226.0", - "@aws-sdk/middleware-serde": "3.226.0", - "@aws-sdk/middleware-signing": "3.226.0", - "@aws-sdk/middleware-stack": "3.226.0", - "@aws-sdk/middleware-user-agent": "3.226.0", - "@aws-sdk/node-config-provider": "3.226.0", - "@aws-sdk/node-http-handler": "3.226.0", - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/smithy-client": "3.234.0", - "@aws-sdk/types": "3.226.0", - "@aws-sdk/url-parser": "3.226.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.234.0", - "@aws-sdk/util-defaults-mode-node": "3.234.0", - "@aws-sdk/util-endpoints": "3.245.0", - "@aws-sdk/util-retry": "3.229.0", - "@aws-sdk/util-user-agent-browser": "3.226.0", - "@aws-sdk/util-user-agent-node": "3.226.0", - "@aws-sdk/util-utf8-browser": "3.188.0", - "@aws-sdk/util-utf8-node": "3.208.0", - "fast-xml-parser": "4.0.11", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sts/node_modules/fast-xml-parser": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz", - "integrity": "sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA==", - "license": "MIT", - "optional": true, - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - }, - "funding": { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - } - }, - "node_modules/@aws-sdk/config-resolver": { - "version": "3.234.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.234.0.tgz", - "integrity": "sha512-uZxy4wzllfvgCQxVc+Iqhde0NGAnfmV2hWR6ejadJaAFTuYNvQiRg9IqJy3pkyDPqXySiJ8Bom5PoJfgn55J/A==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/signature-v4": "3.226.0", - "@aws-sdk/types": "3.226.0", - "@aws-sdk/util-config-provider": "3.208.0", - "@aws-sdk/util-middleware": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-cognito-identity": { - "version": "3.252.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.252.0.tgz", - "integrity": "sha512-QW3pBYetF06FOQ85FbsFjK6xpon8feF/UOHsL0lMGi4CxUZE6zshV/ectU7tACcc4QV8uMvN7OgcK947CMEEWA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/client-cognito-identity": "3.252.0", - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.226.0.tgz", - "integrity": "sha512-sd8uK1ojbXxaZXlthzw/VXZwCPUtU3PjObOfr3Evj7MPIM2IH8h29foOlggx939MdLQGboJf9gKvLlvKDWtJRA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-imds": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.226.0.tgz", - "integrity": "sha512-//z/COQm2AjYFI1Lb0wKHTQSrvLFTyuKLFQGPJsKS7DPoxGOCKB7hmYerlbl01IDoCxTdyL//TyyPxbZEOQD5Q==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/node-config-provider": "3.226.0", - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/types": "3.226.0", - "@aws-sdk/url-parser": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.252.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.252.0.tgz", - "integrity": "sha512-OfpU8xMYK7+6XQ2dUO4rN0gUhhb/ZLV7iwSL6Ji2pI9gglGhKdOSfmbn6fBfCB50kzWZRNoiQJVaBu/d0Kr0EQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/credential-provider-env": "3.226.0", - "@aws-sdk/credential-provider-imds": "3.226.0", - "@aws-sdk/credential-provider-process": "3.226.0", - "@aws-sdk/credential-provider-sso": "3.252.0", - "@aws-sdk/credential-provider-web-identity": "3.226.0", - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/shared-ini-file-loader": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.252.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.252.0.tgz", - "integrity": "sha512-Jt854JnB7izkJ/gb3S0hBFqAQPUNUP3eL8gXX2uqk9A9bQFQdS57/Ci0FXaEPwOXzJwAAPazD8dTf6HXMhnm3w==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/credential-provider-env": "3.226.0", - "@aws-sdk/credential-provider-imds": "3.226.0", - "@aws-sdk/credential-provider-ini": "3.252.0", - "@aws-sdk/credential-provider-process": "3.226.0", - "@aws-sdk/credential-provider-sso": "3.252.0", - "@aws-sdk/credential-provider-web-identity": "3.226.0", - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/shared-ini-file-loader": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.226.0.tgz", - "integrity": "sha512-iUDMdnrTvbvaCFhWwqyXrhvQ9+ojPqPqXhwZtY1X/Qaz+73S9gXBPJHZaZb2Ke0yKE1Ql3bJbKvmmxC/qLQMng==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/shared-ini-file-loader": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.252.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.252.0.tgz", - "integrity": "sha512-2JGoojMOBjG9/DenctEszjdPechq0uDTpH5nx+z1xxIAugA5+HYG/ncNfpwhmUBCrnOxpRaQViTNqXddEPHlAg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/client-sso": "3.252.0", - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/shared-ini-file-loader": "3.226.0", - "@aws-sdk/token-providers": "3.252.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.226.0.tgz", - "integrity": "sha512-CCpv847rLB0SFOHz2igvUMFAzeT2fD3YnY4C8jltuJoEkn0ITn1Hlgt13nTJ5BUuvyti2mvyXZHmNzhMIMrIlw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-providers": { - "version": "3.252.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.252.0.tgz", - "integrity": "sha512-aA4kwbvSlEcS9QSSlUWoVyoMYEljhkubNxpRhRnObsl4iT9xS06c38lKyhz3m0XIbCVk0lgYTcpue0dlybKS7Q==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/client-cognito-identity": "3.252.0", - "@aws-sdk/client-sso": "3.252.0", - "@aws-sdk/client-sts": "3.252.0", - "@aws-sdk/credential-provider-cognito-identity": "3.252.0", - "@aws-sdk/credential-provider-env": "3.226.0", - "@aws-sdk/credential-provider-imds": "3.226.0", - "@aws-sdk/credential-provider-ini": "3.252.0", - "@aws-sdk/credential-provider-node": "3.252.0", - "@aws-sdk/credential-provider-process": "3.226.0", - "@aws-sdk/credential-provider-sso": "3.252.0", - "@aws-sdk/credential-provider-web-identity": "3.226.0", - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/shared-ini-file-loader": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/fetch-http-handler": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.226.0.tgz", - "integrity": "sha512-JewZPMNEBXfi1xVnRa7pVtK/zgZD8/lQ/YnD8pq79WuMa2cwyhDtr8oqCoqsPW+WJT5ScXoMtuHxN78l8eKWgg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/querystring-builder": "3.226.0", - "@aws-sdk/types": "3.226.0", - "@aws-sdk/util-base64": "3.208.0", - "tslib": "^2.3.1" - } - }, - "node_modules/@aws-sdk/hash-node": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.226.0.tgz", - "integrity": "sha512-MdlJhJ9/Espwd0+gUXdZRsHuostB2WxEVAszWxobP0FTT9PnicqnfK7ExmW+DUAc0ywxtEbR3e0UND65rlSTVw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "@aws-sdk/util-buffer-from": "3.208.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/invalid-dependency": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.226.0.tgz", - "integrity": "sha512-QXOYFmap8g9QzRjumcRCIo2GEZkdCwd7ePQW0OABWPhKHzlJ74vvBxywjU3s39EEBEluWXtZ7Iufg6GxZM4ifw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - } - }, - "node_modules/@aws-sdk/is-array-buffer": { - "version": "3.201.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.201.0.tgz", - "integrity": "sha512-UPez5qLh3dNgt0DYnPD/q0mVJY84rA17QE26hVNOW3fAji8W2wrwrxdacWOxyXvlxWsVRcKmr+lay1MDqpAMfg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-content-length": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.226.0.tgz", - "integrity": "sha512-ksUzlHJN2JMuyavjA46a4sctvnrnITqt2tbGGWWrAuXY1mel2j+VbgnmJUiwHKUO6bTFBBeft5Vd1TSOb4JmiA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-endpoint": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.226.0.tgz", - "integrity": "sha512-EvLFafjtUxTT0AC9p3aBQu1/fjhWdIeK58jIXaNFONfZ3F8QbEYUPuF/SqZvJM6cWfOO9qwYKkRDbCSTYhprIg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/middleware-serde": "3.226.0", - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/signature-v4": "3.226.0", - "@aws-sdk/types": "3.226.0", - "@aws-sdk/url-parser": "3.226.0", - "@aws-sdk/util-config-provider": "3.208.0", - "@aws-sdk/util-middleware": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.226.0.tgz", - "integrity": "sha512-haVkWVh6BUPwKgWwkL6sDvTkcZWvJjv8AgC8jiQuSl8GLZdzHTB8Qhi3IsfFta9HAuoLjxheWBE5Z/L0UrfhLA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.226.0.tgz", - "integrity": "sha512-m9gtLrrYnpN6yckcQ09rV7ExWOLMuq8mMPF/K3DbL/YL0TuILu9i2T1W+JuxSX+K9FMG2HrLAKivE/kMLr55xA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.226.0.tgz", - "integrity": "sha512-mwRbdKEUeuNH5TEkyZ5FWxp6bL2UC1WbY+LDv6YjHxmSMKpAoOueEdtU34PqDOLrpXXxIGHDFmjeGeMfktyEcA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-retry": { - "version": "3.235.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.235.0.tgz", - "integrity": "sha512-50WHbJGpD3SNp9763MAlHqIhXil++JdQbKejNpHg7HsJne/ao3ub+fDOfx//mMBjpzBV25BGd5UlfL6blrClSg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/service-error-classification": "3.229.0", - "@aws-sdk/types": "3.226.0", - "@aws-sdk/util-middleware": "3.226.0", - "@aws-sdk/util-retry": "3.229.0", - "tslib": "^2.3.1", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-sdk-sts": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.226.0.tgz", - "integrity": "sha512-NN9T/qoSD1kZvAT+VLny3NnlqgylYQcsgV3rvi/8lYzw/G/2s8VS6sm/VTWGGZhx08wZRv20MWzYu3bftcyqUg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/middleware-signing": "3.226.0", - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/signature-v4": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-serde": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.226.0.tgz", - "integrity": "sha512-nPuOOAkSfx9TxzdKFx0X2bDlinOxGrqD7iof926K/AEflxGD1DBdcaDdjlYlPDW2CVE8LV/rAgbYuLxh/E/1VA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-signing": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.226.0.tgz", - "integrity": "sha512-E6HmtPcl+IjYDDzi1xI2HpCbBq2avNWcjvCriMZWuTAtRVpnA6XDDGW5GY85IfS3A8G8vuWqEVPr8JcYUcjfew==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/signature-v4": "3.226.0", - "@aws-sdk/types": "3.226.0", - "@aws-sdk/util-middleware": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-stack": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.226.0.tgz", - "integrity": "sha512-85wF29LvPvpoed60fZGDYLwv1Zpd/cM0C22WSSFPw1SSJeqO4gtFYyCg2squfT3KI6kF43IIkOCJ+L7GtryPug==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.226.0.tgz", - "integrity": "sha512-N1WnfzCW1Y5yWhVAphf8OPGTe8Df3vmV7/LdsoQfmpkCZgLZeK2o0xITkUQhRj1mbw7yp8tVFLFV3R2lMurdAQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/node-config-provider": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.226.0.tgz", - "integrity": "sha512-B8lQDqiRk7X5izFEUMXmi8CZLOKCTWQJU9HQf3ako+sF0gexo4nHN3jhoRWyLtcgC5S3on/2jxpAcqtm7kuY3w==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/shared-ini-file-loader": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/node-http-handler": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.226.0.tgz", - "integrity": "sha512-xQCddnZNMiPmjr3W7HYM+f5ir4VfxgJh37eqZwX6EZmyItFpNNeVzKUgA920ka1VPz/ZUYB+2OFGiX3LCLkkaA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/abort-controller": "3.226.0", - "@aws-sdk/protocol-http": "3.226.0", - "@aws-sdk/querystring-builder": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/property-provider": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.226.0.tgz", - "integrity": "sha512-TsljjG+Sg0LmdgfiAlWohluWKnxB/k8xenjeozZfzOr5bHmNHtdbWv6BtNvD/R83hw7SFXxbJHlD5H4u9p2NFg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/protocol-http": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.226.0.tgz", - "integrity": "sha512-zWkVqiTA9RXL6y0hhfZc9bcU4DX2NI6Hw9IhQmSPeM59mdbPjJlY4bLlMr5YxywqO3yQ/ylNoAfrEzrDjlOSRg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/querystring-builder": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.226.0.tgz", - "integrity": "sha512-LVurypuNeotO4lmirKXRC4NYrZRAyMJXuwO0f2a5ZAUJCjauwYrifKue6yCfU7bls7gut7nfcR6B99WBYpHs3g==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "@aws-sdk/util-uri-escape": "3.201.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/querystring-parser": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.226.0.tgz", - "integrity": "sha512-FzB+VrQ47KAFxiPt2YXrKZ8AOLZQqGTLCKHzx4bjxGmwgsjV8yIbtJiJhZLMcUQV4LtGeIY9ixIqQhGvnZHE4A==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/service-error-classification": { - "version": "3.229.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.229.0.tgz", - "integrity": "sha512-dnzWWQ0/NoWMUZ5C0DW3dPm0wC1O76Y/SpKbuJzWPkx1EYy6r8p32Ly4D9vUzrKDbRGf48YHIF2kOkBmu21CLg==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/shared-ini-file-loader": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.226.0.tgz", - "integrity": "sha512-661VQefsARxVyyV2FX9V61V+nNgImk7aN2hYlFKla6BCwZfMng+dEtD0xVGyg1PfRw0qvEv5LQyxMVgHcUSevA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.226.0.tgz", - "integrity": "sha512-/R5q5agdPd7HJB68XMzpxrNPk158EHUvkFkuRu5Qf3kkkHebEzWEBlWoVpUe6ss4rP9Tqcue6xPuaftEmhjpYw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/is-array-buffer": "3.201.0", - "@aws-sdk/types": "3.226.0", - "@aws-sdk/util-hex-encoding": "3.201.0", - "@aws-sdk/util-middleware": "3.226.0", - "@aws-sdk/util-uri-escape": "3.201.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/smithy-client": { - "version": "3.234.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.234.0.tgz", - "integrity": "sha512-8AtR/k4vsFvjXeQbIzq/Wy7Nbk48Ou0wUEeVYPHWHPSU8QamFWORkOwmKtKMfHAyZvmqiAPeQqHFkq+UJhWyyQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/middleware-stack": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.252.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.252.0.tgz", - "integrity": "sha512-xi3pUP31tyKF4lJFCOgtkwSWESE9W1vE23Vybsq53wzXEYfnRql8RP+C9FFkUouAR6ixPHEcEYplB+l92CY49g==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/client-sso-oidc": "3.252.0", - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/shared-ini-file-loader": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.226.0.tgz", - "integrity": "sha512-MmmNHrWeO4man7wpOwrAhXlevqtOV9ZLcH4RhnG5LmRce0RFOApx24HoKENfFCcOyCm5LQBlsXCqi0dZWDWU0A==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/url-parser": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.226.0.tgz", - "integrity": "sha512-p5RLE0QWyP0OcTOLmFcLdVgUcUEzmEfmdrnOxyNzomcYb0p3vUagA5zfa1HVK2azsQJFBv28GfvMnba9bGhObg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/querystring-parser": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - } - }, - "node_modules/@aws-sdk/util-base64": { - "version": "3.208.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64/-/util-base64-3.208.0.tgz", - "integrity": "sha512-PQniZph5A6N7uuEOQi+1hnMz/FSOK/8kMFyFO+4DgA1dZ5pcKcn5wiFwHkcTb/BsgVqQa3Jx0VHNnvhlS8JyTg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/util-buffer-from": "3.208.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-body-length-browser": { - "version": "3.188.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.188.0.tgz", - "integrity": "sha512-8VpnwFWXhnZ/iRSl9mTf+VKOX9wDE8QtN4bj9pBfxwf90H1X7E8T6NkiZD3k+HubYf2J94e7DbeHs7fuCPW5Qg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^2.3.1" - } - }, - "node_modules/@aws-sdk/util-body-length-node": { - "version": "3.208.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.208.0.tgz", - "integrity": "sha512-3zj50e5g7t/MQf53SsuuSf0hEELzMtD8RX8C76f12OSRo2Bca4FLLYHe0TZbxcfQHom8/hOaeZEyTyMogMglqg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-buffer-from": { - "version": "3.208.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.208.0.tgz", - "integrity": "sha512-7L0XUixNEFcLUGPeBF35enCvB9Xl+K6SQsmbrPk1P3mlV9mguWSDQqbOBwY1Ir0OVbD6H/ZOQU7hI/9RtRI0Zw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/is-array-buffer": "3.201.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-config-provider": { - "version": "3.208.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.208.0.tgz", - "integrity": "sha512-DSRqwrERUsT34ug+anlMBIFooBEGwM8GejC7q00Y/9IPrQy50KnG5PW2NiTjuLKNi7pdEOlwTSEocJE15eDZIg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-defaults-mode-browser": { - "version": "3.234.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.234.0.tgz", - "integrity": "sha512-IHMKXjTbOD8XMz5+2oCOsVP94BYb9YyjXdns0aAXr2NAo7k2+RCzXQ2DebJXppGda1F6opFutoKwyVSN0cmbMw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/types": "3.226.0", - "bowser": "^2.11.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@aws-sdk/util-defaults-mode-node": { - "version": "3.234.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.234.0.tgz", - "integrity": "sha512-UGjQ+OjBYYhxFVtUY+jtr0ZZgzZh6OHtYwRhFt8IHewJXFCfZTyfsbX20szBj5y1S4HRIUJ7cwBLIytTqMbI5w==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/config-resolver": "3.234.0", - "@aws-sdk/credential-provider-imds": "3.226.0", - "@aws-sdk/node-config-provider": "3.226.0", - "@aws-sdk/property-provider": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.245.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.245.0.tgz", - "integrity": "sha512-UNOFquB1tKx+8RT8n82Zb5tIwDyZHVPBg/m0LB0RsLETjr6krien5ASpqWezsXKIR1hftN9uaxN4bvf2dZrWHg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-hex-encoding": { - "version": "3.201.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.201.0.tgz", - "integrity": "sha512-7t1vR1pVxKx0motd3X9rI3m/xNp78p3sHtP5yo4NP4ARpxyJ0fokBomY8ScaH2D/B+U5o9ARxldJUdMqyBlJcA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.208.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.208.0.tgz", - "integrity": "sha512-iua1A2+P7JJEDHVgvXrRJSvsnzG7stYSGQnBVphIUlemwl6nN5D+QrgbjECtrbxRz8asYFHSzhdhECqN+tFiBg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-middleware": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.226.0.tgz", - "integrity": "sha512-B96CQnwX4gRvQdaQkdUtqvDPkrptV5+va6FVeJOocU/DbSYMAScLxtR3peMS8cnlOT6nL1Eoa42OI9AfZz1VwQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-retry": { - "version": "3.229.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-retry/-/util-retry-3.229.0.tgz", - "integrity": "sha512-0zKTqi0P1inD0LzIMuXRIYYQ/8c1lWMg/cfiqUcIAF1TpatlpZuN7umU0ierpBFud7S+zDgg0oemh+Nj8xliJw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/service-error-classification": "3.229.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@aws-sdk/util-uri-escape": { - "version": "3.201.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.201.0.tgz", - "integrity": "sha512-TeTWbGx4LU2c5rx0obHeDFeO9HvwYwQtMh1yniBz00pQb6Qt6YVOETVQikRZ+XRQwEyCg/dA375UplIpiy54mA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.226.0.tgz", - "integrity": "sha512-PhBIu2h6sPJPcv2I7ELfFizdl5pNiL4LfxrasMCYXQkJvVnoXztHA1x+CQbXIdtZOIlpjC+6BjDcE0uhnpvfcA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.226.0", - "bowser": "^2.11.0", - "tslib": "^2.3.1" - } - }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.226.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.226.0.tgz", - "integrity": "sha512-othPc5Dz/pkYkxH+nZPhc1Al0HndQT8zHD4e9h+EZ+8lkd8n+IsnLfTS/mSJWrfiC6UlNRVw55cItstmJyMe/A==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/node-config-provider": "3.226.0", - "@aws-sdk/types": "3.226.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/util-utf8-browser": { - "version": "3.188.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.188.0.tgz", - "integrity": "sha512-jt627x0+jE+Ydr9NwkFstg3cUvgWh56qdaqAMDsqgRlKD21md/6G226z/Qxl7lb1VEW2LlmCx43ai/37Qwcj2Q==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "tslib": "^2.3.1" - } - }, - "node_modules/@aws-sdk/util-utf8-node": { - "version": "3.208.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.208.0.tgz", - "integrity": "sha512-jKY87Acv0yWBdFxx6bveagy5FYjz+dtV8IPT7ay1E2WPWH1czoIdMAkc8tSInK31T6CRnHWkLZ1qYwCbgRfERQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/util-buffer-from": "3.208.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@discordjs/builders": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.4.0.tgz", - "integrity": "sha512-nEeTCheTTDw5kO93faM1j8ZJPonAX86qpq/QVoznnSa8WWcCgJpjlu6GylfINTDW6o7zZY0my2SYdxx2mfNwGA==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/util": "^0.1.0", - "@sapphire/shapeshift": "^3.7.1", - "discord-api-types": "^0.37.20", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.2", - "tslib": "^2.4.1" - }, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@discordjs/collection": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.3.0.tgz", - "integrity": "sha512-ylt2NyZ77bJbRij4h9u/wVy7qYw/aDqQLWnadjvDqW/WoWCxrsX6M3CIw9GVP5xcGCDxsrKj5e0r5evuFYwrKg==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@discordjs/rest": { - "version": "0.2.0-canary.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-0.2.0-canary.0.tgz", - "integrity": "sha512-jOxz1aqTEzn9N0qaJcZbHz6FbA0oq+vjpXUKkQzgfMihO6gC+kLlpRnFqG25T/aPYbjaR1UM/lGhrGBB1dutqg==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^0.3.2", - "@sapphire/async-queue": "^1.1.9", - "@sapphire/snowflake": "^3.0.0", - "discord-api-types": "^0.25.2", - "form-data": "^4.0.0", - "node-fetch": "^2.6.5", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.3.2.tgz", - "integrity": "sha512-dMjLl60b2DMqObbH1MQZKePgWhsNe49XkKBZ0W5Acl5uVV43SN414i2QfZwRI7dXAqIn8pEWD2+XXQFn9KWxqg==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@discordjs/rest/node_modules/discord-api-types": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.25.2.tgz", - "integrity": "sha512-O243LXxb5gLLxubu5zgoppYQuolapGVWPw3ll0acN0+O8TnPUE2kFp9Bt3sTRYodw8xFIknOVxjSeyWYBpVcEQ==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/@discordjs/rest/node_modules/node-fetch": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz", - "integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@discordjs/rest/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/@discordjs/rest/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/@discordjs/util": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.1.0.tgz", - "integrity": "sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.4.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@hokify/agenda": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@hokify/agenda/-/agenda-6.3.0.tgz", - "integrity": "sha512-fWrKMDe/8QHJXLOdEsMogb6cb213Z82iNsnU7nFrSIMFifEXSkXNTyCZ99FV3KLf+Du1gS/M9/8uTC6FHyWRZQ==", - "license": "MIT", - "dependencies": { - "cron-parser": "^4", - "date.js": "~0.3.3", - "debug": "~4", - "human-interval": "~2", - "luxon": "^3", - "mongodb": "^4" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@hokify/agenda/node_modules/cron-parser": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.1.tgz", - "integrity": "sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==", - "license": "MIT", - "dependencies": { - "luxon": "^3.2.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "license": "BSD-3-Clause" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@sapphire/async-queue": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", - "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/shapeshift": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.8.1.tgz", - "integrity": "sha512-xG1oXXBhCjPKbxrRTlox9ddaZTvVpOhYLmKmApD/vIWOV1xEYXnpoFs68zHIZBGbqztq6FrUPNPerIrO1Hqeaw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/snowflake": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.4.0.tgz", - "integrity": "sha512-zZxymtVO6zeXVMPds+6d7gv/OfnCc25M1Z+7ZLB0oPmeMTPeRWVPQSS16oDJy5ZsyCOLj7M6mbZml5gWXcVRNw==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, - "node_modules/@tsconfig/node18-strictest-esm": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/node18-strictest-esm/-/node18-strictest-esm-1.0.1.tgz", - "integrity": "sha512-cHzmAqw7CMbyqROWeBgVhard3F2V6zxOSJnQ4E6SJWruXD5ypuP9/QKekwBdfXQ4oUTaizIICKIwb+v3v33t0w==", - "license": "MIT" - }, - "node_modules/@types/eslint": { - "version": "8.4.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", - "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", - "license": "MIT" - }, - "node_modules/@types/node-cron": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.7.tgz", - "integrity": "sha512-9PuLtBboc/+JJ7FshmJWv769gDonTpItN0Ol5TMwclpSQNjVyB2SRxSKBcTtbSysSL5R7Oea06kTTFNciCoYwA==", - "license": "MIT" - }, - "node_modules/@types/prettier": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", - "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==", - "license": "MIT" - }, - "node_modules/@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/webidl-conversions": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", - "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.2.tgz", - "integrity": "sha512-sR0Gja9Ky1teIq4qJOl0nC+Tk64/uYdX+mi+5iB//MH8gwyx8e3SOyhEzeLZEFEEfCaLf8KJq+Bd/6je1t+CAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "5.48.2", - "@typescript-eslint/type-utils": "5.48.2", - "@typescript-eslint/utils": "5.48.2", - "debug": "^4.3.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "regexpp": "^3.2.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "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" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/typescript": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", - "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", - "dev": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.48.2.tgz", - "integrity": "sha512-38zMsKsG2sIuM5Oi/olurGwYJXzmtdsHhn5mI/pQogP+BjYVkK5iRazCQ8RGS0V+YLk282uWElN70zAAUmaYHw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.48.2", - "@typescript-eslint/types": "5.48.2", - "@typescript-eslint/typescript-estree": "5.48.2", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.48.2.tgz", - "integrity": "sha512-zEUFfonQid5KRDKoI3O+uP1GnrFd4tIHlvs+sTJXiWuypUWMuDaottkJuR612wQfOkjYbsaskSIURV9xo4f+Fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.48.2", - "@typescript-eslint/visitor-keys": "5.48.2" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.2.tgz", - "integrity": "sha512-QVWx7J5sPMRiOMJp5dYshPxABRoZV1xbRirqSk8yuIIsu0nvMTZesKErEA3Oix1k+uvsk8Cs8TGJ6kQ0ndAcew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.48.2", - "@typescript-eslint/utils": "5.48.2", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/@typescript-eslint/type-utils/node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "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" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/typescript": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", - "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", - "dev": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.48.2.tgz", - "integrity": "sha512-hE7dA77xxu7ByBc6KCzikgfRyBCTst6dZQpwaTy25iMYOnbNljDT4hjhrGEJJ0QoMjrfqrx+j1l1B9/LtKeuqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.2.tgz", - "integrity": "sha512-bibvD3z6ilnoVxUBFEgkO0k0aFvUc4Cttt0dAreEr+nrAHhWzkO83PEVVuieK3DqcgL6VAK5dkzK8XUVja5Zcg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.48.2", - "@typescript-eslint/visitor-keys": "5.48.2", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "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" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/typescript": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", - "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", - "dev": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.2.tgz", - "integrity": "sha512-2h18c0d7jgkw6tdKTlNaM7wyopbLRBiit8oAxoP89YnuBOzCZ8g8aBCaCqq7h208qUTroL7Whgzam7UY3HVLow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.48.2", - "@typescript-eslint/types": "5.48.2", - "@typescript-eslint/typescript-estree": "5.48.2", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.2.tgz", - "integrity": "sha512-z9njZLSkwmjFWUelGEwEbdf4NwKvfHxvGC0OcGN1Hp/XNDIcJ7D5DpPNPv6x6/mFvc1tQHsaWmpD/a4gOvvCJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.48.2", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.0.1.tgz", - "integrity": "sha512-zKVyTt6rELvPXYwcVPTJcPFtY0AckN5A7xWuc7owBqR0FdtuDYhE9MZZUi6IY1kZUQFSXV1B3UOOIyLkVHYd2w==", - "license": "ISC" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agenda": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/agenda/-/agenda-4.4.0.tgz", - "integrity": "sha512-7fIO4indmmrtkDmj2woOBJnhHIM7jPtkdGR4VOApB46eeBrPGUnO28RFrmjHebc3PMDnKJI0PWFyu9L9VotgJg==", - "license": "MIT", - "dependencies": { - "cron-parser": "^3.0.0", - "date.js": "~0.3.3", - "debug": "~4.3.0", - "human-interval": "~2.0.0", - "moment-timezone": "~0.5.37", - "mongodb": "^4.1.0" - }, - "engines": { - "node": ">=12.9.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "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.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/bowser": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", - "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/bson": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", - "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", - "license": "Apache-2.0", - "dependencies": { - "buffer": "^5.6.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cron-parser": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz", - "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==", - "license": "MIT", - "dependencies": { - "is-nan": "^1.3.2", - "luxon": "^1.26.0" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/cron-parser/node_modules/luxon": { - "version": "1.28.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz", - "integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/dafo": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dafo/-/dafo-0.1.1.tgz", - "integrity": "sha512-18XhtDt5qQ5pAfWzo1t3mRXQPanY8diFQFjCxh+/0mko5QHztCv6oLgPL9kKxB8aeYutp8lWC7BqjGzC8GWX1w==", - "dependencies": { - "noda": "^0.1.2" - } - }, - "node_modules/dafo/node_modules/noda": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/noda/-/noda-0.1.2.tgz", - "integrity": "sha512-oP5xzQsC4dujsuRlshsnb2R+0BWJmPRJx8MXQlTgbrSSqi5Szeu67Lr8Od1/lkc+8evLPRDmHl9L0xeVJYRGaw==", - "license": "ISC" - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/date.js": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/date.js/-/date.js-0.3.3.tgz", - "integrity": "sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw==", - "license": "MIT", - "dependencies": { - "debug": "~3.1.0" - } - }, - "node_modules/date.js/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "license": "MIT" - }, - "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "license": "MIT", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/deno": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/deno/-/deno-0.1.1.tgz", - "integrity": "sha512-vdXCOZOXXgRUFPIV2HbyBBT+u1TjKmDueCEJuk9Q+ADLS7jmVfdfGxp0vI5o/gOjbA1OwOYMVfg249+9vjYa7w==", - "license": "MIT", - "dependencies": { - "dafo": "^0.1.1", - "noda": "^0.6.0", - "qir": "^0.1.0" - }, - "bin": { - "install.js": "try-deno" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/discord-api-types": { - "version": "0.37.28", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.28.tgz", - "integrity": "sha512-K0fw7m7km9th3dCQ2AR90q/FwX3uAj+OLc+Zuo39VY9vCn0Ux/iObM4y1zJYIH3vTc+QlrksVErUvyeONjOKMQ==", - "license": "MIT" - }, - "node_modules/discord.js": { - "version": "14.7.1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.7.1.tgz", - "integrity": "sha512-1FECvqJJjjeYcjSm0IGMnPxLqja/pmG1B0W2l3lUY2Gi4KXiyTeQmU1IxWcbXHn2k+ytP587mMWqva2IA87EbA==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/builders": "^1.4.0", - "@discordjs/collection": "^1.3.0", - "@discordjs/rest": "^1.4.0", - "@discordjs/util": "^0.1.0", - "@sapphire/snowflake": "^3.2.2", - "@types/ws": "^8.5.3", - "discord-api-types": "^0.37.20", - "fast-deep-equal": "^3.1.3", - "lodash.snakecase": "^4.1.1", - "tslib": "^2.4.1", - "undici": "^5.13.0", - "ws": "^8.11.0" - }, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/discord.js/node_modules/@discordjs/rest": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.5.0.tgz", - "integrity": "sha512-lXgNFqHnbmzp5u81W0+frdXN6Etf4EUi8FAPcWpSykKd8hmlWh1xy6BmE0bsJypU1pxohaA8lQCgp70NUI3uzA==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^1.3.0", - "@discordjs/util": "^0.1.0", - "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.2.2", - "discord-api-types": "^0.37.23", - "file-type": "^18.0.0", - "tslib": "^2.4.1", - "undici": "^5.13.0" - }, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.32.0.tgz", - "integrity": "sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ==", - "license": "MIT", - "dependencies": { - "@eslint/eslintrc": "^1.4.1", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "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.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-prettier": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz", - "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.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" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "license": "MIT" - }, - "node_modules/fast-xml-parser": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.13.tgz", - "integrity": "sha512-g+OboAw8ol1FgTHhKLR7ZHcItNudceiY04BBrvqa0JBWoPhi/+e5r4H5AeW+EsQCroJLJwsuOP3dD3c6cc5uOg==", - "license": "MIT", - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - }, - "funding": { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - } - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-type": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.2.0.tgz", - "integrity": "sha512-M3RQMWY3F2ykyWZ+IHwNCjpnUmukYhtdkGGC1ZVEUb0ve5REGF7NNJ4Q9ehCUabtQKtSVFOMbFTXgJlFb0DQIg==", - "license": "MIT", - "dependencies": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "license": "ISC" - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "license": "MIT" - }, - "node_modules/fuse.js": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", - "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.19.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", - "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "license": "MIT" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/human-interval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/human-interval/-/human-interval-2.0.1.tgz", - "integrity": "sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==", - "license": "MIT", - "dependencies": { - "numbered": "^1.1.0" - } - }, - "node_modules/humanize-duration": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.28.0.tgz", - "integrity": "sha512-jMAxraOOmHuPbffLVDKkEKi/NeG8dMqP8lGRd6Tbf7JgAeG33jjgPWDbXXU7ypCI0o+oNKJFgbSB9FKVdWNI2A==", - "license": "Unlicense" - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immutable": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.2.tgz", - "integrity": "sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==", - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", - "license": "MIT" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/js-sdsl": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", - "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "license": "MIT" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", - "license": "MIT" - }, - "node_modules/loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" - } - }, - "node_modules/loglevel-colored-level-prefix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", - "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.1.3", - "loglevel": "^1.4.1" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/luxon": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", - "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT", - "optional": true - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.40", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.40.tgz", - "integrity": "sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg==", - "license": "MIT", - "dependencies": { - "moment": ">= 2.9.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mongodb": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.13.0.tgz", - "integrity": "sha512-+taZ/bV8d1pYuHL4U+gSwkhmDrwkWbH1l4aah4YpmpscMwgFBkufIKxgP/G7m87/NUuQzc2Z75ZTI7ZOyqZLbw==", - "license": "Apache-2.0", - "dependencies": { - "bson": "^4.7.0", - "mongodb-connection-string-url": "^2.5.4", - "socks": "^2.7.1" - }, - "engines": { - "node": ">=12.9.0" - }, - "optionalDependencies": { - "@aws-sdk/credential-providers": "^3.186.0", - "saslprep": "^1.0.3" - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", - "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "license": "MIT" - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/noda": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/noda/-/noda-0.6.0.tgz", - "integrity": "sha512-Zj3eTQ1cL83zGvorxbGmeqNnt/h+2nH3jT/XLI2oXHL9LH6IKoPvFh6feT1e/yFhRRByP3Q+waM+2dcXIdZkqg==", - "license": "ISC", - "engines": { - "node": ">=6" - } - }, - "node_modules/node-cron": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.2.tgz", - "integrity": "sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ==", - "license": "ISC", - "dependencies": { - "uuid": "8.3.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", - "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-tesseract-ocr": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-tesseract-ocr/-/node-tesseract-ocr-2.2.1.tgz", - "integrity": "sha512-Q9cD79JGpPNQBxbi1fV+OAsTxYKLpx22sagsxSyKbu1u+t6UarApf5m32uVc8a5QAP1Wk7fIPN0aJFGGEE9DyQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/numbered": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/numbered/-/numbered-1.1.0.tgz", - "integrity": "sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==", - "license": "MIT" - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pastebin-api": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/pastebin-api/-/pastebin-api-5.1.5.tgz", - "integrity": "sha512-MdRsE8crPA5y4M9IkLcohmU+xeLnUxMj091L5LR+Ywvp3s8bPYu7s7G1BEsvTxaJoLrgC/uPs1MgDliyqxMcNA==", - "license": "MIT", - "dependencies": { - "fast-xml-parser": "^4.0.10", - "undici": "^5.10.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/peek-readable": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", - "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz", - "integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-eslint": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-15.0.1.tgz", - "integrity": "sha512-mGOWVHixSvpZWARqSDXbdtTL54mMBxc5oQYQ6RAqy8jecuNJBgN3t9E5a81G66F8x8fsKNiR1HWaBV66MJDOpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "^8.4.2", - "@types/prettier": "^2.6.0", - "@typescript-eslint/parser": "^5.10.0", - "common-tags": "^1.4.0", - "dlv": "^1.1.0", - "eslint": "^8.7.0", - "indent-string": "^4.0.0", - "lodash.merge": "^4.6.0", - "loglevel-colored-level-prefix": "^1.0.0", - "prettier": "^2.5.1", - "pretty-format": "^23.0.1", - "require-relative": "^0.8.7", - "typescript": "^4.5.4", - "vue-eslint-parser": "^8.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/prettier-eslint/node_modules/typescript": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", - "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.2.0.tgz", - "integrity": "sha512-LN6QV1IJ9ZhxWTNdktaPClrNfp8xdSAYS0Zk2ddX7XsXZAxckMHPCBcHRo0cTcEIgYPRiGEkmji3Idkh2yFtYw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qir": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/qir/-/qir-0.1.0.tgz", - "integrity": "sha512-LJUKDMyPsaog5uDarzqterz+SfdyA3mfvq45kSLV0X5IOSPy/nlueKvLg8nClOSKp57c1E0bU/BFHwHFUoVhcw==", - "license": "ISC", - "dependencies": { - "noda": "^0.6.0" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "license": "MIT", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/require-relative": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", - "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/saslprep": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", - "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", - "license": "MIT", - "optional": true, - "dependencies": { - "sparse-bitfield": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "license": "MIT", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", - "license": "MIT", - "dependencies": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.13.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "memory-pager": "^1.0.2" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", - "license": "MIT" - }, - "node_modules/strtok3": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", - "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/structured-clone": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/structured-clone/-/structured-clone-0.2.2.tgz", - "integrity": "sha512-SucNWVxwmfAjWrzQ9Xsuv4JIDtS/Qpx+MwZD2NEx2CeMpf3hgqvWKssll34trTu6M7ywd7WZDDKO8hhq0SZiAA==", - "license": "MIT" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/systeminformation": { - "version": "5.17.3", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.17.3.tgz", - "integrity": "sha512-IAmnUJdeFUWqY+YneAWJ9rceTdRRIaTiwspvd1B6SG7yhqpxLrSosHgGZKiE8lcaBlBYpLQpY3BRLtus4n8PNQ==", - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32", - "freebsd", - "openbsd", - "netbsd", - "sunos", - "android" - ], - "bin": { - "systeminformation": "lib/cli.js" - }, - "engines": { - "node": ">=8.0.0" - }, - "funding": { - "type": "Buy me a coffee", - "url": "https://www.buymeacoffee.com/systeminfo" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/token-types": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "license": "MIT", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/ts-mixer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.2.tgz", - "integrity": "sha512-zvHx3VM83m2WYCE8XL99uaM7mFwYSkjR2OZti98fabHrwkjsCvgwChda5xctein3xGOyaQhtTeDq/1H/GNvF3A==", - "license": "MIT" - }, - "node_modules/tsc-suppress": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tsc-suppress/-/tsc-suppress-1.0.7.tgz", - "integrity": "sha512-keT8/tFADvf1nc9CGxvMEfkfCdKp5aF2t1v9GaCRtNegljAtk1Kv0C3KLBFrZTaptgB4OXUF6QkE3eMqy6+VNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4", - "minimist": "^1.2.5" - }, - "bin": { - "tsc-suppress": "bin/tsc-suppress.js" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "typescript": ">=3.0.0" - } - }, - "node_modules/tsc-suppress/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/tsc-suppress/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/tsc-suppress/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/tsc-suppress/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.0.0-dev.20230118", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.0-dev.20230118.tgz", - "integrity": "sha512-mLhr9b2PSXo21+f210MSRD3EOdsrOg9NTWghkJNDaY0K7iWVK8E5FsflAsRzi+Rn/CsO7tH3pyl0LeGwVX25Cg==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/undici": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.15.0.tgz", - "integrity": "sha512-wCAZJDyjw9Myv+Ay62LAoB+hZLPW9SmKbQkbHIhMw/acKSlpn7WohdMUc/Vd4j1iSMBO0hWwU8mjB7a5p5bl8g==", - "license": "MIT", - "dependencies": { - "busboy": "^1.6.0" - }, - "engines": { - "node": ">=12.18" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vue-eslint-parser": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz", - "integrity": "sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.2", - "eslint-scope": "^7.0.0", - "eslint-visitor-keys": "^3.1.0", - "espree": "^9.0.0", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "license": "MIT", - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", - "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json index 8110b8c..f28878f 100644 --- a/package.json +++ b/package.json @@ -3,29 +3,24 @@ "@discordjs/rest": "^0.2.0-canary.0", "@hokify/agenda": "^6.2.12", "@tsconfig/node18-strictest-esm": "^1.0.0", - "@types/node-cron": "^3.0.1", "@ungap/structured-clone": "^1.0.1", "agenda": "^4.3.0", - "ansi-styles": "^6.1.0", "body-parser": "^1.20.0", - "chalk": "^5.0.0", - "deno": "^0.1.1", - "discord.js": "14.7.1", + "discord.js": "^14.7.1", "eslint": "^8.21.0", "express": "^4.18.1", - "form-data": "^4.0.0", "fuse.js": "^6.6.2", "humanize-duration": "^3.27.1", "immutable": "^4.1.0", + "lodash": "^4.17.21", "mongodb": "^4.7.0", - "node-cron": "^3.0.0", "node-fetch": "^3.3.0", "node-tesseract-ocr": "^2.2.1", - "pastebin-api": "^5.1.1", "structured-clone": "^0.2.2", - "systeminformation": "^5.17.3", - "typescript": "^5.0.0-dev.20230102", - "uuid": "^8.3.2" + "systeminformation": "^5.17.3" + }, + "resolutions": { + "discord-api-types": "0.37.23" }, "name": "nucleus", "version": "0.0.1", @@ -59,11 +54,13 @@ "private": false, "type": "module", "devDependencies": { + "@types/lodash": "^4.14.191", "@typescript-eslint/eslint-plugin": "^5.32.0", "@typescript-eslint/parser": "^5.32.0", "eslint-config-prettier": "^8.5.0", "prettier": "^2.7.1", "prettier-eslint": "^15.0.1", - "tsc-suppress": "^1.0.7" + "tsc-suppress": "^1.0.7", + "typescript": "^4.9.4" } } diff --git a/src/Unfinished/all.ts b/src/Unfinished/all.ts index 9d9e653..eea33f5 100644 --- a/src/Unfinished/all.ts +++ b/src/Unfinished/all.ts @@ -5,16 +5,18 @@ import Discord, { ActionRowBuilder, ButtonBuilder, SelectMenuBuilder, - ButtonStyle + ButtonStyle, + StringSelectMenuBuilder, + APIMessageComponentEmoji } from "discord.js"; -import { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../utils/generateEmojiEmbed.js"; import getEmojiByName from "../utils/getEmojiByName.js"; import addPlural from "../utils/plurals.js"; import client from "../utils/client.js"; const command = (builder: SlashCommandSubcommandBuilder) => - builder // TODO: DON'T RELEASE THIS + builder .setName("all") .setDescription("Gives or removes a role from everyone"); @@ -171,8 +173,8 @@ const callback = async (interaction: CommandInteraction): Promise => { const all = true; while (true) { let count = 0; - const affected = []; - const members = interaction.guild.members.cache; + const affected: GuildMember[] = []; + const members = interaction.guild!.members.cache; if (all) { members.forEach((member) => { let applies = true; @@ -224,8 +226,8 @@ const callback = async (interaction: CommandInteraction): Promise => { .setStatus("Success") ], components: [ - new ActionRowBuilder().addComponents([ - new SelectMenuBuilder() + new ActionRowBuilder().addComponents([ + new StringSelectMenuBuilder() .setOptions( filters.map((f, index) => ({ label: (f.inverted ? "(Not) " : "") + f.name, @@ -237,18 +239,18 @@ const callback = async (interaction: CommandInteraction): Promise => { .setCustomId("select") .setPlaceholder("Remove a filter") ]), - new ActionRowBuilder().addComponents([ + new ActionRowBuilder().addComponents([ new ButtonBuilder() .setLabel("Apply") .setStyle(ButtonStyle.Primary) .setCustomId("apply") - .setEmoji(client.emojis.cache.get(getEmojiByName("CONTROL.TICK", "id"))) + .setEmoji(client.emojis.cache.get(getEmojiByName("CONTROL.TICK", "id"))! as APIMessageComponentEmoji) .setDisabled(affected.length === 0), new ButtonBuilder() .setLabel("Add filter") .setStyle(ButtonStyle.Primary) .setCustomId("add") - .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.FILTER", "id"))) + .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.FILTER", "id"))! as APIMessageComponentEmoji) .setDisabled(filters.length >= 25) ]) ] @@ -260,12 +262,12 @@ const callback = async (interaction: CommandInteraction): Promise => { const check = async (interaction: CommandInteraction) => { const member = interaction.member as GuildMember; - const me = interaction.guild.me!; - if (!me.permissions.has("MANAGE_ROLES")) throw new Error("I do not have the *Manage Roles* permission"); + const me = interaction.guild!.members.me!; + if (!me.permissions.has("ManageRoles")) return "I do not have the *Manage Roles* permission"; // Allow the owner to role anyone - if (member.id === interaction.guild.ownerId) return true; + if (member.id === interaction.guild!.ownerId) return true; // Check if the user has manage_roles permission - if (!member.permissions.has("MANAGE_ROLES")) throw new Error("You do not have the *Manage Roles* permission"); + if (!member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission"; // Allow role return true; }; diff --git a/src/Unfinished/categorisationTest.ts b/src/Unfinished/categorizationTest.ts similarity index 71% rename from src/Unfinished/categorisationTest.ts rename to src/Unfinished/categorizationTest.ts index dc38dfe..ff2d66b 100644 --- a/src/Unfinished/categorisationTest.ts +++ b/src/Unfinished/categorizationTest.ts @@ -1,28 +1,30 @@ import { LoadingEmbed } from "../utils/defaults.js"; -import { CommandInteraction, GuildChannel, ActionRowBuilder, ButtonBuilder, SelectMenuBuilder, ButtonStyle } from "discord.js"; -import { SlashCommandBuilder } from "@discordjs/builders"; +import { CommandInteraction, GuildChannel, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, StringSelectMenuBuilder, APIMessageComponentEmoji } from "discord.js"; +import { SlashCommandBuilder } from "discord.js"; import EmojiEmbed from "../utils/generateEmojiEmbed.js"; import client from "../utils/client.js"; import addPlural from "../utils/plurals.js"; import getEmojiByName from "../utils/getEmojiByName.js"; -const command = new SlashCommandBuilder().setName("categorise").setDescription("Categorises your servers channels"); +const command = new SlashCommandBuilder().setName("categorize").setDescription("Categorizes your servers channels"); const callback = async (interaction: CommandInteraction): Promise => { - const channels = interaction.guild.channels.cache.filter((c) => c.type !== "GUILD_CATEGORY"); - const categorised = {}; + const channels = interaction.guild!.channels.cache.filter((c) => c.type !== ChannelType.GuildCategory); + const categorized = {}; await interaction.reply({ embeds: LoadingEmbed, ephemeral: true }); const predicted = {}; const types = { - general: ["general", "muted", "main", "topic", "discuss"], + important: ["rule", "announcement", "alert", "info"], + general: ["general", "main", "topic", "discuss"], commands: ["bot", "command", "music"], - images: ["pic", "selfies", "image"], - nsfw: ["porn", "nsfw", "sex"], - links: ["links"], - advertising: ["ads", "advert", "server", "partner"], - staff: ["staff", "mod", "admin"], - spam: ["spam"], - other: ["random"] + images: ["pic", "selfies", "image", "gallery", "meme", "media"], + nsfw: ["porn", "nsfw", "sex", "lewd", "fetish"], + links: ["link"], + advertising: ["ads", "advert", "partner", "bump"], + staff: ["staff", "mod", "admin", "helper", "train"], + spam: ["spam", "count"], + logs: ["log"], + other: ["random", "starboard"], }; for (const c of channels.values()) { for (const type in types) { @@ -38,14 +40,14 @@ const callback = async (interaction: CommandInteraction): Promise => { for (const c of channels) { // convert channel to a channel if its a string let channel: string | GuildChannel; - if (typeof c === "string") channel = interaction.guild.channels.cache.get(channel as string).id; + if (typeof c === "string") channel = interaction.guild!.channels.cache.get(c as string)!.id; else channel = (c[0] as unknown as GuildChannel).id; console.log(channel); if (!predicted[channel]) predicted[channel] = []; m = await interaction.editReply({ embeds: [ new EmojiEmbed() - .setTitle("Categorise") + .setTitle("Categorize") .setDescription( `Select all types that apply to <#${channel}>.\n\n` + `${addPlural(predicted[channel].length, "Suggestion")}: ${predicted[channel].join(", ")}` @@ -54,8 +56,8 @@ const callback = async (interaction: CommandInteraction): Promise => { .setStatus("Success") ], components: [ - new ActionRowBuilder().addComponents([ - new SelectMenuBuilder() + new ActionRowBuilder().addComponents([ + new StringSelectMenuBuilder() .setCustomId("selected") .setMaxValues(Object.keys(types).length) .setMinValues(1) @@ -67,18 +69,18 @@ const callback = async (interaction: CommandInteraction): Promise => { })) ) ]), - new ActionRowBuilder().addComponents([ + new ActionRowBuilder().addComponents([ new ButtonBuilder() .setLabel("Accept Suggestion") .setCustomId("accept") .setStyle(ButtonStyle.Success) .setDisabled(predicted[channel].length === 0) - .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.TICK", "id"))), + .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.TICK", "id")) as APIMessageComponentEmoji), new ButtonBuilder() .setLabel('Use "Other"') .setCustomId("reject") .setStyle(ButtonStyle.Secondary) - .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.CROSS", "id"))) + .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.CROSS", "id")) as APIMessageComponentEmoji) ]) ] }); @@ -86,13 +88,13 @@ const callback = async (interaction: CommandInteraction): Promise => { try { i = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id} }); } catch (e) { return await interaction.editReply({ embeds: [ new EmojiEmbed() - .setTitle("Categorise") + .setTitle("Categorize") .setEmoji("CHANNEL.CATEGORY.DELETE") .setStatus("Danger") .setDescription( @@ -105,7 +107,7 @@ const callback = async (interaction: CommandInteraction): Promise => { ] }); } - i.deferUpdate(); + await i.deferUpdate(); let selected; if (i.customId === "select") { selected = i.values; @@ -116,9 +118,9 @@ const callback = async (interaction: CommandInteraction): Promise => { if (i.customId === "reject") { selected = ["other"]; } - categorised[channel] = selected; + categorized[channel] = selected; } - console.log(categorised); + console.log(categorized); }; const check = () => { diff --git a/src/actions/createModActionTicket.ts b/src/actions/createModActionTicket.ts index d6e9cd9..d86c14a 100644 --- a/src/actions/createModActionTicket.ts +++ b/src/actions/createModActionTicket.ts @@ -1,4 +1,4 @@ -import { getCommandMentionByName } from './../utils/getCommandMentionByName.js'; +import { getCommandMentionByName } from './../utils/getCommandDataByName.js'; import Discord, { ActionRowBuilder, ButtonBuilder, OverwriteType, ChannelType, ButtonStyle } from "discord.js"; import EmojiEmbed from "../utils/generateEmojiEmbed.js"; import getEmojiByName from "../utils/getEmojiByName.js"; @@ -86,7 +86,7 @@ export async function create( `**Support type:** ${customReason ? customReason : "Appeal submission"}\n` + (reason !== null ? `**Reason:**\n> ${reason}\n` : "") + `**Ticket ID:** \`${c.id}\`\n` + - `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.` + `Type ${getCommandMentionByName("ticket/close")} to close this ticket.` ) .setStatus("Success") .setEmoji("GUILD.TICKET.OPEN") @@ -131,7 +131,7 @@ export async function create( `**Support type:** ${customReason ? customReason : "Appeal submission"}\n` + (reason !== null ? `**Reason:**\n> ${reason}\n` : "") + `**Ticket ID:** \`${c.id}\`\n` + - `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.` + `Type ${getCommandMentionByName("ticket/close")} to close this ticket.` ) .setStatus("Success") .setEmoji("GUILD.TICKET.OPEN") @@ -157,12 +157,12 @@ export async function create( calculateType: "ticketUpdate", color: NucleusColors.green, emoji: "GUILD.TICKET.OPEN", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { ticketFor: entry(user.id, renderUser(user)), createdBy: entry(createdBy.id, renderUser(createdBy)), - created: entry((new Date().getTime()).toString(), renderDelta(new Date().getTime())), + created: entry((Date.now()).toString(), renderDelta(Date.now())), ticketChannel: entry(c.id, renderChannel(c)) }, hidden: { diff --git a/src/actions/roleMenu.ts b/src/actions/roleMenu.ts index 7056fe6..be58d99 100644 --- a/src/actions/roleMenu.ts +++ b/src/actions/roleMenu.ts @@ -30,6 +30,36 @@ export interface RoleMenuSchema { interaction: CommandInteraction | ButtonInteraction | ContextMenuCommandInteraction; } +interface ObjectSchema { + name: string; + description: string; + min: number; + max: number; + options: { + name: string; + description: string | null; + role: string; + }[]; +} + +export const configToDropdown = (placeholder: string, currentPageData: ObjectSchema, selectedRoles?: string[]): ActionRowBuilder => { + return new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId("roles") + .setPlaceholder(placeholder) + .setMinValues(currentPageData.min) + .setMaxValues(currentPageData.max) + .addOptions(currentPageData.options.map((option: {name: string; description: string | null; role: string;}) => { + const builder = new StringSelectMenuOptionBuilder() + .setLabel(option.name) + .setValue(option.role) + .setDefault(selectedRoles ? selectedRoles.includes(option.role) : false); + if (option.description) builder.setDescription(option.description); + return builder; + })) + ) +} + export async function callback(interaction: CommandInteraction | ButtonInteraction) { if (!interaction.member) return; if (!interaction.guild) return; @@ -56,7 +86,7 @@ export async function callback(interaction: CommandInteraction | ButtonInteracti ], ephemeral: true }); - const m = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true }); + const m = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true }); if (config.roleMenu.allowWebUI) { // TODO: Make rolemenu web ui const loginMethods: {webUI: boolean} = { webUI: false @@ -75,7 +105,7 @@ export async function callback(interaction: CommandInteraction | ButtonInteracti const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let valid = false; while (!valid) { - itt += 1; + itt ++; code = ""; for (let i = 0; i < length; i++) { code += chars.charAt(Math.floor(Math.random() * chars.length)); @@ -83,7 +113,7 @@ export async function callback(interaction: CommandInteraction | ButtonInteracti if (code in client.roleMenu) continue; if (itt > 1000) { itt = 0; - length += 1; + length ++; continue; } valid = true; @@ -124,9 +154,10 @@ export async function callback(interaction: CommandInteraction | ButtonInteracti try { component = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channelId === interaction.channelId && i.message.id === m.id} }); } catch (e) { + console.log(e); return; } component.deferUpdate(); @@ -151,8 +182,7 @@ export async function callback(interaction: CommandInteraction | ButtonInteracti `**${currentPageData.name}**\n` + `> ${currentPageData.description}\n\n` + (currentPageData.min === currentPageData.max ? `Select ${addPlural(currentPageData.min, "role")}` : - `Select between ${currentPageData.min} and ${currentPageData.max} roles` + ( - currentPageData.min === 0 ? ` or press next` : "")) + "\n\n" + + `Select between ${currentPageData.min} and ${currentPageData.max} roles then press next`) + "\n\n" + createPageIndicator(maxPage, page) ) .setStatus("Success") @@ -175,21 +205,7 @@ export async function callback(interaction: CommandInteraction | ButtonInteracti .setCustomId("done") .setDisabled(!complete) ), - new ActionRowBuilder().addComponents( - new StringSelectMenuBuilder() - .setCustomId("roles") - .setPlaceholder("Select...") - .setMinValues(currentPageData.min) - .setMaxValues(currentPageData.max) - .addOptions(currentPageData.options.map((option) => { - const builder = new StringSelectMenuOptionBuilder() - .setLabel(option.name) - .setValue(option.role) - .setDefault(selectedRoles[page]!.includes(option.role)); - if (option.description) builder.setDescription(option.description); - return builder; - })) - ) + configToDropdown("Select...", currentPageData, selectedRoles[page]) ]; await interaction.editReply({ embeds: [embed], @@ -199,9 +215,10 @@ export async function callback(interaction: CommandInteraction | ButtonInteracti try { component = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id} }); } catch (e) { + console.log(e); return; } component.deferUpdate(); diff --git a/src/actions/tickets/create.ts b/src/actions/tickets/create.ts index 3c2dd2c..237790e 100644 --- a/src/actions/tickets/create.ts +++ b/src/actions/tickets/create.ts @@ -3,7 +3,7 @@ import { tickets, toHexArray } from "../../utils/calculate.js"; import client from "../../utils/client.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; -import { getCommandMentionByName } from "../../utils/getCommandMentionByName.js"; +import { getCommandMentionByName } from "../../utils/getCommandDataByName.js"; function capitalize(s: string) { s = s.replace(/([A-Z])/g, " $1"); @@ -106,7 +106,7 @@ export default async function (interaction: CommandInteraction | ButtonInteracti try { component = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id } }); } catch (e) { return; @@ -225,7 +225,7 @@ export default async function (interaction: CommandInteraction | ButtonInteracti chosenType !== null ? emoji + " " + capitalize(chosenType) : "General" }\n` + `**Ticket ID:** \`${c.id}\`\n${content ?? ""}\n` + - `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.` + `Type ${getCommandMentionByName("ticket/close")} to close this ticket.` ) .setStatus("Success") .setEmoji("GUILD.TICKET.OPEN") @@ -257,7 +257,7 @@ export default async function (interaction: CommandInteraction | ButtonInteracti type: Discord.ChannelType.PrivateThread, reason: "Creating ticket" }) as Discord.PrivateThreadChannel; - c.members.add(interaction.member!.user.id); // TODO: When a thread is used, and a support role is added, automatically set channel permissions + c.members.add(interaction.member!.user.id); try { await c.send({ content: @@ -289,7 +289,7 @@ export default async function (interaction: CommandInteraction | ButtonInteracti chosenType !== null ? emoji + " " + capitalize(chosenType) : "General" }\n` + `**Ticket ID:** \`${c.id}\`\n${content ?? ""}\n` + - `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.` + `Type ${getCommandMentionByName("ticket/close")} to close this ticket.` ) .setStatus("Success") .setEmoji("GUILD.TICKET.OPEN") @@ -323,11 +323,11 @@ export default async function (interaction: CommandInteraction | ButtonInteracti calculateType: "ticketUpdate", color: NucleusColors.green, emoji: "GUILD.TICKET.OPEN", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { ticketFor: entry(interaction.member!.user.id, renderUser(interaction.member!.user! as Discord.User)), - created: entry(new Date().getTime(), renderDelta(new Date().getTime())), + created: entry(Date.now(), renderDelta(Date.now())), ticketChannel: entry(c.id, renderChannel(c)) }, hidden: { diff --git a/src/actions/tickets/delete.ts b/src/actions/tickets/delete.ts index 3263580..990b360 100644 --- a/src/actions/tickets/delete.ts +++ b/src/actions/tickets/delete.ts @@ -1,15 +1,15 @@ -import { getCommandMentionByName } from '../../utils/getCommandMentionByName.js'; +import { getCommandMentionByName } from '../../utils/getCommandDataByName.js'; import Discord, { ActionRowBuilder, ButtonBuilder, ButtonInteraction, PrivateThreadChannel, TextChannel, ButtonStyle, CategoryChannel } from "discord.js"; import client from "../../utils/client.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import { preloadPage } from '../../utils/createTemporaryStorage.js'; +import { LoadingEmbed } from '../../utils/defaults.js'; export default async function (interaction: Discord.CommandInteraction | ButtonInteraction) { if (!interaction.guild) return; const config = await client.database.guilds.read(interaction.guild.id); const { log, NucleusColors, entry, renderUser, renderChannel, renderDelta } = client.logger; - const ticketChannel = config.tickets.category; if (!("parent" in interaction.channel!)) { return await interaction.reply({ @@ -50,7 +50,7 @@ export default async function (interaction: Discord.CommandInteraction | ButtonI calculateType: "ticketUpdate", color: NucleusColors.red, emoji: "GUILD.TICKET.CLOSE", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { ticketFor: entry( @@ -58,7 +58,7 @@ export default async function (interaction: Discord.CommandInteraction | ButtonI renderUser((await interaction.guild.members.fetch(uID!)).user) ), closedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as Discord.User)), - closed: entry(new Date().getTime(), renderDelta(new Date().getTime())), + closed: entry(Date.now(), renderDelta(Date.now())), ticketChannel: entry(channel.id, channel.name) }, hidden: { @@ -69,8 +69,9 @@ export default async function (interaction: Discord.CommandInteraction | ButtonI await channel.delete(); } else if (status === "Active") { - // Close the ticket - + await interaction.reply({embeds: LoadingEmbed, fetchReply: true}); + // Archive the ticket + await interaction.channel.fetch() if (channel.isThread()) { channel.setName(`${channel.name.replace("Active", "Archived")}`); channel.members.remove(channel.name.split(" - ")[1]!); @@ -80,14 +81,14 @@ export default async function (interaction: Discord.CommandInteraction | ButtonI await channel.permissionOverwrites.delete(channel.topic!.split(" ")[0]!); } preloadPage(interaction.channel.id, "privacy", "2") - await interaction.reply({ + const hasPremium = await client.database.premium.hasPremium(interaction.guild.id); + await interaction.editReply({ embeds: [ new EmojiEmbed() .setTitle("Archived Ticket") - .setDescription(`This ticket has been Archived. Type ${await getCommandMentionByName("ticket/close")} to delete it.` + - await client.database.premium.hasPremium(interaction.guild.id) ? - `\n\nFor more info on transcripts, check ${await getCommandMentionByName("privacy")}` : - "") + .setDescription(`This ticket has been Archived. Type ${getCommandMentionByName("ticket/close")} to delete it.\n` + + hasPremium ? ("Creating a transcript will delete all messages in this ticket" + + `\n\nFor more info on transcripts, check ${getCommandMentionByName("privacy")}`): "") .setStatus("Warning") .setEmoji("GUILD.TICKET.ARCHIVED") ], @@ -100,7 +101,7 @@ export default async function (interaction: Discord.CommandInteraction | ButtonI .setCustomId("closeticket") .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) ].concat( - await client.database.premium.hasPremium(interaction.guild.id) + hasPremium ? [ new ButtonBuilder() .setLabel("Create Transcript and Delete") @@ -120,7 +121,7 @@ export default async function (interaction: Discord.CommandInteraction | ButtonI calculateType: "ticketUpdate", color: NucleusColors.yellow, emoji: "GUILD.TICKET.ARCHIVED", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { ticketFor: entry( @@ -128,7 +129,7 @@ export default async function (interaction: Discord.CommandInteraction | ButtonI renderUser((await interaction.guild.members.fetch(uID!)).user) ), archivedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as Discord.User)), - archived: entry(new Date().getTime(), renderDelta(new Date().getTime())), + archived: entry(Date.now(), renderDelta(Date.now())), ticketChannel: entry(channel.id, renderChannel(channel)) }, hidden: { @@ -183,12 +184,12 @@ async function purgeByUser(member: string, guild: string) { calculateType: "ticketUpdate", color: NucleusColors.red, emoji: "GUILD.TICKET.DELETE", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { ticketFor: entry(member, renderUser(member)), deletedBy: entry(null, "Member left server"), - deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())), + deleted: entry(Date.now(), renderDelta(Date.now())), ticketsDeleted: deleted }, hidden: { @@ -198,4 +199,4 @@ async function purgeByUser(member: string, guild: string) { log(data); } -export { purgeByUser }; \ No newline at end of file +export { purgeByUser }; diff --git a/src/api/index.ts b/src/api/index.ts index c24327d..9676194 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -57,7 +57,7 @@ const runServer = (client: NucleusClient) => { calculateType: "guildMemberVerify", color: NucleusColors.green, emoji: "CONTROL.BLOCKTICK", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { member: entry(member.id, renderUser(member.user)), @@ -149,6 +149,49 @@ const runServer = (client: NucleusClient) => { return res.sendStatus(404); }); + app.get("/transcript/:code/human", jsonParser, async function (req: express.Request, res: express.Response) { + const code = req.params.code; + if (code === undefined) return res.status(400).send("No code provided"); + const entry = await client.database.transcripts.read(code); + if (entry === null) return res.status(404).send("Could not find a transcript by that code"); + // Convert to a human readable format + const data = client.database.transcripts.toHumanReadable(entry); + res.attachment(`${code}.txt`); + res.type("txt"); + return res.status(200).send(data); + }); + + app.get("/transcript/:code", jsonParser, async function (req: express.Request, res: express.Response) { + const code = req.params.code; + if (code === undefined) return res.status(400).send("No code provided"); + const entry = await client.database.transcripts.read(code); + if (entry === null) return res.status(404).send("Could not find a transcript by that code"); + // Convert to a human readable format + return res.status(200).send(entry); + }); + + app.get("/channels/:id", jsonParser, async function (req: express.Request, res: express.Response) { + const id = req.params.id; + if (id === undefined) return res.status(400).send("No id provided"); + const channel = await client.channels.fetch(id); + if (channel === null) return res.status(404).send("Could not find a channel by that id"); + if (channel.isDMBased()) return res.status(400).send("Cannot get a DM channel"); + return res.status(200).send(channel.name); + }); + + app.get("/users/:id", jsonParser, async function (req: express.Request, res: express.Response) { + const id = req.params.id; + if (id === undefined) return res.status(400).send("No id provided"); + let user; + try { + user = await client.users.fetch(id); + } catch (e) { + console.log(e) + return res.status(404).send("Could not find a user by that id"); + } + return res.status(200).send(user.username); + }); + app.listen(port); }; diff --git a/src/commands/help.ts b/src/commands/help.ts index 767ca46..90ef133 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -1,23 +1,195 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction } from "discord.js"; -import { SlashCommandBuilder } from "@discordjs/builders"; +import { + ActionRowBuilder, + CommandInteraction, + StringSelectMenuBuilder, + ApplicationCommandOptionType, + ApplicationCommandType, + StringSelectMenuOptionBuilder, + SlashCommandBuilder, + StringSelectMenuInteraction, + ComponentType, + APIMessageComponentEmoji, + ApplicationCommandSubGroup, + PermissionsBitField, + Interaction, + ApplicationCommandOption, + ApplicationCommandSubCommand +} from "discord.js"; +import client from "../utils/client.js"; +import EmojiEmbed from "../utils/generateEmojiEmbed.js"; +import { LoadingEmbed } from "../utils/defaults.js"; +import { capitalize } from "../utils/generateKeyValueList.js"; +import { getCommandByName, getCommandMentionByName } from "../utils/getCommandDataByName.js"; +import getEmojiByName from "../utils/getEmojiByName.js"; const command = new SlashCommandBuilder() .setName("help") .setDescription("Shows help for commands"); +const styles: Record = { + "help": {emoji: "NUCLEUS.LOGO"}, + "mod": {emoji: "PUNISH.BAN.RED"}, + "nucleus": {emoji: "NUCLEUS.LOGO"}, + "privacy": {emoji: "NUCLEUS.LOGO"}, + "role": {emoji: "GUILD.ROLES.DELETE"}, + "rolemenu": {emoji: "GUILD.ROLES.DELETE"}, + "server": {emoji: "GUILD.RED"}, + "settings": {emoji: "GUILD.SETTINGS.RED"}, + "tag": {emoji: "PUNISH.NICKNAME.RED"}, + "tags": {emoji: "PUNISH.NICKNAME.RED"}, + "ticket": {emoji: "GUILD.TICKET.CLOSE"}, + "user": {emoji: "MEMBER.LEAVE"}, + "verify": {emoji: "CONTROL.REDTICK"} +} + const callback = async (interaction: CommandInteraction): Promise => { - interaction.reply({components: [new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setLabel("Create ticket") - .setStyle(ButtonStyle.Primary) - .setCustomId("createticket") - )]}); // TODO: FINISH THIS FOR RELEASE -}; + const m = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true }); + const commands = client.fetchedCommands; + + let closed = false; + let currentPath: [string, string, string] = ["", "", ""] + do { + const commandRow = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("commandRow") + .setPlaceholder("Select a command") + .addOptions( + ...commands.filter(command => command.type === ApplicationCommandType.ChatInput).map((command) => { + const builder = new StringSelectMenuOptionBuilder() + .setLabel(capitalize(command.name)) + .setValue(command.name) + .setDescription(command.description) + .setDefault(currentPath[0] === command.name) + if (styles[command.name]) builder.setEmoji(getEmojiByName(styles[command.name]!.emoji, "id") as APIMessageComponentEmoji) + return builder + }) + ) + ); + const subcommandGroupRow = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("subcommandGroupRow") + ); + const subcommandRow = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("subcommandRow") + ); + const embed = new EmojiEmbed() + .setTitle("Help") + .setStatus("Danger") + .setEmoji("NUCLEUS.LOGO") + + if(currentPath[0] === "" || currentPath[0] === "help") { + embed.setDescription( + `Welcome to Nucleus\n\n` + + `Select a command to get started${ + (interaction.member?.permissions as PermissionsBitField).has("ManageGuild") ? + `, or run ${getCommandMentionByName("nucleus/guide")} for commands to set up your server` : `` + }\n\n\n` + + `Nucleus is fully [open source](https://github.com/clicksminuteper/Nucleus), and all currently free features will remain free forever.\n\n` + + `You can invite Nucleus to your server using ${getCommandMentionByName("nucleus/invite")}` + ) + } else { + const currentData = getCommandByName(currentPath.filter(value => value !== "" && value !== "none").join('/')); + const current = commands.find((command) => command.name === currentPath[0])!; + + let optionString = `` + let options: (ApplicationCommandOption & { + nameLocalized?: string; + descriptionLocalized?: string; + })[] = []; + //options + if(currentPath[1] !== "" && currentPath[1] !== "none" && currentPath[2] !== "" && currentPath[2] !== "none") { + const Op = current.options.find(option => option.name === currentPath[1])! as ApplicationCommandSubGroup + const Op2 = Op.options!.find(option => option.name === currentPath[2])! + options = Op2.options ?? [] + } else if(currentPath[1] !== "" && currentPath[1] !== "none") { + let Op = current.options.find(option => option.name === currentPath[1])! + if(Op.type === ApplicationCommandOptionType.SubcommandGroup) { + options = [] + } else { + Op = Op as ApplicationCommandSubCommand + options = Op.options ?? [] + } + } else { + options = current.options.filter(option => (option.type !== ApplicationCommandOptionType.SubcommandGroup) && (option.type !== ApplicationCommandOptionType.Subcommand)); + } + for(const option of options) { + optionString += `> ${option.name} (${ApplicationCommandOptionType[option.type]})- ${option.description}\n` + } + const APICommand = client.commands["commands/" + currentPath.filter(value => value !== "" && value !== "none").join("/")]![0] + let allowedToRun = true; + if(APICommand?.check) { + allowedToRun = await APICommand.check(interaction as Interaction, true) + } + embed.setDescription( + `${getEmojiByName(styles[currentPath[0]]!.emoji)} **${capitalize(currentData.name)}**\n> ${currentData.mention}\n\n` + + `> ${currentData.description}\n\n` + + (APICommand ? (`${getEmojiByName(allowedToRun ? "CONTROL.TICK" : "CONTROL.CROSS")} You ${allowedToRun ? "" : "don't "}` + + `have permission to use this command\n\n`) : "") + + ((optionString.length > 0) ? "**Options:**\n" + optionString : "") + ) + const subcommands = current.options.filter((option) => option.type === ApplicationCommandOptionType.Subcommand); + const subcommandGroups = current.options.filter((option) => option.type === ApplicationCommandOptionType.SubcommandGroup); -const check = () => { - return true; + if(subcommandGroups.length > 0) { + subcommandGroupRow.components[0]! + .addOptions( + new StringSelectMenuOptionBuilder().setLabel("Select a subcommand").setValue("none").setDefault(currentPath[1] === "none"), + ...subcommandGroups.map((option) => new StringSelectMenuOptionBuilder().setLabel(capitalize(option.name)).setValue(option.name).setDefault(currentPath[1] === option.name)) + ) + if(subcommandGroupRow.components[0]!.options.find((option) => option.data.default && option.data.value !== "none")) { + const subsubcommands = (subcommandGroups.find((option) => option.name === currentPath[1])! as ApplicationCommandSubGroup).options ?? []; + subcommandRow.components[0]! + .addOptions( + new StringSelectMenuOptionBuilder().setLabel("Select a subcommand").setValue("none").setDefault(currentPath[2] === "none"), + ...subsubcommands.map((option) => new StringSelectMenuOptionBuilder().setLabel(capitalize(option.name)).setValue(option.name).setDefault(currentPath[2] === option.name)) + ) + } + } + if(subcommands.length > 0) { + subcommandGroupRow.components[0]! + .addOptions( + ...subcommands.map((option) => new StringSelectMenuOptionBuilder().setLabel(capitalize(option.name)).setValue(option.name).setDefault(currentPath[1] === option.name)) + ) + } + } + + const cmps = [commandRow]; + if(subcommandGroupRow.components[0]!.options.length > 0) cmps.push(subcommandGroupRow); + if(subcommandRow.components[0]!.options.length > 0) cmps.push(subcommandRow); + + await interaction.editReply({ embeds: [embed], components: cmps }); + + let i: StringSelectMenuInteraction; + try { + i = await m.awaitMessageComponent({filter: (newInteraction) => interaction.user.id === newInteraction.user.id,time: 300000}) + } catch (e) { + closed = true; + continue; + } + await i.deferUpdate(); + const value = i.values[0]!; + switch(i.customId) { + case "commandRow": { + currentPath = [value, "", ""]; + break; + } + case "subcommandGroupRow": { + currentPath = [currentPath[0], value , ""]; + break; + } + case "subcommandRow": { + currentPath[2] = value; + break; + } + } + + } while (!closed); }; -export { command }; + +export { command as command }; export { callback }; -export { check }; diff --git a/src/commands/mod/_meta.ts b/src/commands/mod/_meta.ts index af8006c..c5fcca5 100644 --- a/src/commands/mod/_meta.ts +++ b/src/commands/mod/_meta.ts @@ -5,4 +5,4 @@ const description = "Perform moderator actions"; const subcommand = await command(name, description, `mod`); -export { name, description, subcommand as command }; +export { name, description, subcommand as command }; \ No newline at end of file diff --git a/src/commands/mod/about.ts b/src/commands/mod/about.ts index 130cdbc..0a9d962 100644 --- a/src/commands/mod/about.ts +++ b/src/commands/mod/about.ts @@ -3,17 +3,16 @@ import type { HistorySchema } from "../../utils/database.js"; import Discord, { CommandInteraction, GuildMember, - Interaction, Message, ActionRowBuilder, ButtonBuilder, MessageComponentInteraction, ModalSubmitInteraction, ButtonStyle, - StringSelectMenuInteraction, TextInputStyle, + APIMessageComponentEmoji, + SlashCommandSubcommandBuilder } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import client from "../../utils/client.js"; @@ -167,8 +166,7 @@ async function showHistory(member: Discord.GuildMember, interaction: CommandInte .setLabel(value.text) .setValue(key) .setDefault(filteredTypes.includes(key)) - // @ts-expect-error - .setEmoji(getEmojiByName(value.emoji, "id")) // FIXME: This gives a type error but is valid + .setEmoji(getEmojiByName(value.emoji, "id") as APIMessageComponentEmoji) ))) ]); components = components.concat([new ActionRowBuilder().addComponents([ @@ -253,7 +251,7 @@ async function showHistory(member: Discord.GuildMember, interaction: CommandInte try { i = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id } }); } catch (e) { interaction.editReply({ @@ -269,9 +267,9 @@ async function showHistory(member: Discord.GuildMember, interaction: CommandInte timedOut = true; continue; } - i.deferUpdate(); - if (i.customId === "filter") { - filteredTypes = (i as StringSelectMenuInteraction).values; + await i.deferUpdate(); + if (i.customId === "filter" && i.isStringSelectMenu()) { + filteredTypes = i.values; pageIndex = null; refresh = true; } @@ -359,7 +357,7 @@ const callback = async (interaction: CommandInteraction): Promise => { try { i = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id } }); } catch (e) { timedOut = true; @@ -402,17 +400,12 @@ const callback = async (interaction: CommandInteraction): Promise => { }); let out; try { - out = await modalInteractionCollector( - m, - (m: Interaction) => - (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === interaction.channelId, - (m) => m.customId === "modify" - ); + out = await modalInteractionCollector(m, interaction.user); } catch (e) { timedOut = true; continue; } - if (out === null) { + if (out === null || out.isButton()) { continue; } else if (out instanceof ModalSubmitInteraction) { let toAdd = out.fields.getTextInputValue("note") || null; @@ -423,7 +416,7 @@ const callback = async (interaction: CommandInteraction): Promise => { continue; } } else if (i.customId === "history") { - i.deferUpdate(); + await i.deferUpdate(); if (!(await showHistory(member, interaction))) return; } } @@ -436,6 +429,8 @@ const check = (interaction: CommandInteraction) => { return true; }; -export { command }; -export { callback }; -export { check }; +export { command, callback, check }; +export const metadata = { + longDescription: "Shows the moderation history (all previous bans, kicks, warns etc.), and moderator notes for a user.", + premiumOnly: true, +} diff --git a/src/commands/mod/ban.ts b/src/commands/mod/ban.ts index 70e904c..e8309fb 100644 --- a/src/commands/mod/ban.ts +++ b/src/commands/mod/ban.ts @@ -1,11 +1,11 @@ -import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, User, ButtonStyle } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, User, ButtonStyle, SlashCommandSubcommandBuilder } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import keyValueList from "../../utils/generateKeyValueList.js"; import addPlurals from "../../utils/plurals.js"; import client from "../../utils/client.js"; import { LinkWarningFooter } from "../../utils/defaults.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; const command = (builder: SlashCommandSubcommandBuilder) => @@ -26,7 +26,7 @@ const command = (builder: SlashCommandSubcommandBuilder) => const callback = async (interaction: CommandInteraction): Promise => { if (!interaction.guild) return; const { renderUser } = client.logger; - // TODO:[Modals] Replace this with a modal + // TODO:[Modals] Replace the command arguments with a modal let reason = null; let notify = true; let confirmation; @@ -123,17 +123,20 @@ const callback = async (interaction: CommandInteraction): Promise => { calculateType: "guildMemberPunish", color: NucleusColors.red, emoji: "PUNISH.BAN.RED", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(member.user.id, `\`${member.user.id}\``), name: entry(member.user.id, renderUser(member.user)), - banned: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())), + banned: entry(Date.now().toString(), renderDelta(Date.now())), bannedBy: entry(interaction.user.id, renderUser(interaction.user)), reason: entry(reason, reason ? `\n> ${reason}` : "*No reason provided.*"), accountCreated: entry(member.user.createdTimestamp, renderDelta(member.user.createdTimestamp)), serverMemberCount: interaction.guild.memberCount }, + separate: { + end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified` + }, hidden: { guild: interaction.guild.id } @@ -166,9 +169,12 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = async (interaction: CommandInteraction) => { +const check = async (interaction: CommandInteraction, partial: boolean = false) => { if (!interaction.guild) return; const member = interaction.member as GuildMember; + // Check if the user has ban_members permission + if (!member.permissions.has("BanMembers")) return "You do not have the *Ban Members* permission"; + if(partial) return true; const me = interaction.guild.members.me!; let apply = interaction.options.getUser("user") as User | GuildMember; const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0; @@ -181,21 +187,23 @@ const check = async (interaction: CommandInteraction) => { apply = apply as User } // Do not allow banning the owner - if (member.id === interaction.guild.ownerId) throw new Error("You cannot ban the owner of the server"); + if (member.id === interaction.guild.ownerId) return "You cannot ban the owner of the server"; // Check if Nucleus can ban the member - if (!(mePos > applyPos)) throw new Error("I do not have a role higher than that member"); + if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`; // Check if Nucleus has permission to ban - if (!me.permissions.has("BanMembers")) throw new Error("I do not have the *Ban Members* permission"); + if (!me.permissions.has("BanMembers")) return "I do not have the *Ban Members* permission"; // Do not allow banning Nucleus - if (member.id === me.id) throw new Error("I cannot ban myself"); + if (member.id === me.id) return "I cannot ban myself"; // Allow the owner to ban anyone if (member.id === interaction.guild.ownerId) return true; - // Check if the user has ban_members permission - if (!member.permissions.has("BanMembers")) throw new Error("You do not have the *Ban Members* permission"); // Check if the user is below on the role list - if (!(memberPos > applyPos)) throw new Error("You do not have a role higher than that member"); + if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`; // Allow ban return true; }; export { command, callback, check }; +export const metadata = { + longDescription: "Removes a member from the server - this will prevent them from rejoining until they are unbanned, and will delete a specified number of days of messages from them.", + premiumOnly: true, +} diff --git a/src/commands/mod/kick.ts b/src/commands/mod/kick.ts index 380bcc9..059bdb2 100644 --- a/src/commands/mod/kick.ts +++ b/src/commands/mod/kick.ts @@ -1,13 +1,13 @@ import { LinkWarningFooter } from '../../utils/defaults.js'; -import { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle, SlashCommandSubcommandBuilder } from "discord.js"; // @ts-expect-error import humanizeDuration from "humanize-duration"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; import type Discord from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import keyValueList from "../../utils/generateKeyValueList.js"; import client from "../../utils/client.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder @@ -102,8 +102,8 @@ const callback = async (interaction: CommandInteraction): Promise => { await client.database.history.create("kick", interaction.guild.id, member.user, interaction.user, reason); const { log, NucleusColors, entry, renderUser, renderDelta } = client.logger; const timeInServer = member.joinedTimestamp ? entry( - (new Date().getTime() - member.joinedTimestamp).toString(), - humanizeDuration(new Date().getTime() - member.joinedTimestamp, { + (Date.now() - member.joinedTimestamp).toString(), + humanizeDuration(Date.now() - member.joinedTimestamp, { round: true }) ) : entry(null, "*Unknown*") @@ -114,18 +114,21 @@ const callback = async (interaction: CommandInteraction): Promise => { calculateType: "guildMemberPunish", color: NucleusColors.red, emoji: "PUNISH.KICK.RED", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(member.id, `\`${member.id}\``), name: entry(member.id, renderUser(member.user)), joined: undefined as (unknown | typeof entry), - kicked: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())), + kicked: entry(Date.now().toString(), renderDelta(Date.now())), kickedBy: entry(interaction.user.id, renderUser(interaction.user)), reason: entry(reason, reason ? `\n> ${reason}` : "*No reason provided.*"), timeInServer: timeInServer, serverMemberCount: member.guild.memberCount }, + separate: { + end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified` + }, hidden: { guild: member.guild.id } @@ -168,30 +171,37 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, partial: boolean = false) => { if (!interaction.guild) return; + const member = interaction.member as GuildMember; + // Check if the user has kick_members permission + if (!member.permissions.has("KickMembers")) return "You do not have the *Kick Members* permission"; + if (partial) return true; + const me = interaction.guild.members.me!; const apply = interaction.options.getMember("user") as GuildMember; const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0; const mePos = me.roles.cache.size > 1 ? me.roles.highest.position : 0; const applyPos = apply.roles.cache.size > 1 ? apply.roles.highest.position : 0; + // Check if Nucleus has permission to kick + if (!me.permissions.has("KickMembers")) return "I do not have the *Kick Members* permission"; + // Allow the owner to kick anyone + if (member.id === interaction.guild.ownerId) return true; // Do not allow kicking the owner if (member.id === interaction.guild.ownerId) return "You cannot kick the owner of the server"; // Check if Nucleus can kick the member - if (!(mePos > applyPos)) return "I do not have a role higher than that member"; - // Check if Nucleus has permission to kick - if (!me.permissions.has("KickMembers")) return "I do not have the *Kick Members* permission"; + if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`; // Do not allow kicking Nucleus if (member.id === interaction.guild.members.me!.id) return "I cannot kick myself"; - // Allow the owner to kick anyone - if (member.id === interaction.guild.ownerId) return true; - // Check if the user has kick_members permission - if (!member.permissions.has("KickMembers")) return "You do not have the *Kick Members* permission"; // Check if the user is below on the role list - if (!(memberPos > applyPos)) return "You do not have a role higher than that member"; + if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`; // Allow kick return true; }; export { command, callback, check }; +export const metadata = { + longDescription: "Removes a member from the server. They will be able to rejoin if they have an invite link.", + premiumOnly: true, +} diff --git a/src/commands/mod/mute.ts b/src/commands/mod/mute.ts index 86291e5..c795456 100644 --- a/src/commands/mod/mute.ts +++ b/src/commands/mod/mute.ts @@ -1,6 +1,6 @@ import { LinkWarningFooter, LoadingEmbed } from "../../utils/defaults.js"; import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; @@ -103,7 +103,7 @@ const callback = async (interaction: CommandInteraction): Promise => { let component; try { component = await m.awaitMessageComponent({ - filter: (m) => m.user.id === interaction.user.id, + filter: (i) => {return i.user.id === interaction.user.id && i.channelId === interaction.channelId}, time: 300000 }); } catch { @@ -235,8 +235,8 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription( `You have been muted in ${interaction.guild.name}` + (reason ? ` for:\n${reason}` : ".\n*No reason was provided*") + "\n\n" + - `You will be unmuted at: at ` + - ` ( at ` + + ` ()` + "\n\n" + (createAppealTicket ? `You can appeal this in the ticket created in <#${confirmation.components!["appeal"]!.response}>` @@ -267,10 +267,10 @@ const callback = async (interaction: CommandInteraction): Promise => { await member.timeout(muteTime * 1000, reason || "*No reason provided*"); if (config.moderation.mute.role !== null) { await member.roles.add(config.moderation.mute.role); - await client.database.eventScheduler.schedule("naturalUnmute", (new Date().getTime() + muteTime * 1000).toString(), { + await client.database.eventScheduler.schedule("naturalUnmute", (Date.now() + muteTime * 1000).toString(), { guild: interaction.guild.id, user: member.id, - expires: new Date().getTime() + muteTime * 1000 + expires: Date.now() + muteTime * 1000 }); } } else { @@ -282,7 +282,7 @@ const callback = async (interaction: CommandInteraction): Promise => { try { if (config.moderation.mute.role !== null) { await member.roles.add(config.moderation.mute.role); - await client.database.eventScheduler.schedule("unmuteRole", (new Date().getTime() + muteTime * 1000).toString(), { + await client.database.eventScheduler.schedule("unmuteRole", (Date.now() + muteTime * 1000).toString(), { guild: interaction.guild.id, user: member.id, role: config.moderation.mute.role @@ -325,19 +325,22 @@ const callback = async (interaction: CommandInteraction): Promise => { calculateType: "guildMemberPunish", color: NucleusColors.yellow, emoji: "PUNISH.WARN.YELLOW", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(member.user.id, `\`${member.user.id}\``), name: entry(member.user.id, renderUser(member.user)), mutedUntil: entry( - (new Date().getTime() + muteTime * 1000).toString(), - renderDelta(new Date().getTime() + muteTime * 1000) + (Date.now() + muteTime * 1000).toString(), + renderDelta(Date.now() + muteTime * 1000) ), - muted: entry(new Date().getTime.toString(), renderDelta(new Date().getTime() - 1000)), + muted: entry(new Date().getTime.toString(), renderDelta(Date.now() - 1000)), mutedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as Discord.User)), reason: entry(reason, reason ? reason : "*No reason provided*") }, + separate: { + end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified` + }, hidden: { guild: interaction.guild.id } @@ -361,9 +364,12 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = (interaction: CommandInteraction) => { +const check = async (interaction: CommandInteraction, partial: boolean = false) => { if (!interaction.guild) return; const member = interaction.member as GuildMember; + // Check if the user has moderate_members permission + if (!member.permissions.has("ModerateMembers")) return "You do not have the *Moderate Members* permission"; + if (partial) return true; const me = interaction.guild.members.me!; const apply = interaction.options.getMember("user") as GuildMember; const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0; @@ -372,20 +378,21 @@ const check = (interaction: CommandInteraction) => { // Do not allow muting the owner if (member.id === interaction.guild.ownerId) return "You cannot mute the owner of the server"; // Check if Nucleus can mute the member - if (!(mePos > applyPos)) return "I do not have a role higher than that member"; + if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`; // Check if Nucleus has permission to mute if (!me.permissions.has("ModerateMembers")) return "I do not have the *Moderate Members* permission"; // Do not allow muting Nucleus if (member.id === me.id) return "I cannot mute myself"; // Allow the owner to mute anyone if (member.id === interaction.guild.ownerId) return true; - // Check if the user has moderate_members permission - if (!member.permissions.has("ModerateMembers")) - return "You do not have the *Moderate Members* permission"; // Check if the user is below on the role list - if (!(memberPos > applyPos)) return "You do not have a role higher than that member"; + if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`; // Allow mute return true; }; export { command, callback, check }; +export const metadata = { + longDescription: "Stops a member from being able to send messages or join voice channels for a specified amount of time.", + premiumOnly: true, +} diff --git a/src/commands/mod/nick.ts b/src/commands/mod/nick.ts index 9dd9336..5511d19 100644 --- a/src/commands/mod/nick.ts +++ b/src/commands/mod/nick.ts @@ -1,16 +1,16 @@ import { LinkWarningFooter } from './../../utils/defaults.js'; import { ActionRowBuilder, ButtonBuilder, CommandInteraction, GuildMember, ButtonStyle, Message } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import keyValueList from "../../utils/generateKeyValueList.js"; import client from "../../utils/client.js"; import { areTicketsEnabled, create } from "../../actions/createModActionTicket.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("nick") - // .setNameLocalizations({"ru": "name", "zh-CN": "nickname"}) .setDescription("Changes a users nickname") .addUserOption((option) => option.setName("user").setDescription("The user to change").setRequired(true)) .addStringOption((option) => @@ -156,15 +156,18 @@ const callback = async (interaction: CommandInteraction): Promise => { calculateType: "guildMemberUpdate", color: NucleusColors.yellow, emoji: "PUNISH.NICKNAME.YELLOW", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(member.id, `\`${member.id}\``), before: entry(before, before ?? "*No nickname set*"), after: entry(nickname ?? null, nickname ?? "*No nickname set*"), - updated: entry(new Date().getTime(), renderDelta(new Date().getTime())), + updated: entry(Date.now(), renderDelta(Date.now())), updatedBy: entry(interaction.user.id, renderUser(interaction.user)) }, + separate: { + end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified` + }, hidden: { guild: interaction.guild!.id } @@ -189,8 +192,11 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = (interaction: CommandInteraction) => { +const check = async (interaction: CommandInteraction, partial: boolean = false) => { const member = interaction.member as GuildMember; + // Check if the user has manage_nicknames permission + if (!member.permissions.has("ManageNicknames")) return "You do not have the *Manage Nicknames* permission"; + if (partial) return true; const me = interaction.guild!.members.me!; const apply = interaction.options.getMember("user") as GuildMember; const memberPos = member.roles.cache.size ? member.roles.highest.position : 0; @@ -200,20 +206,21 @@ const check = (interaction: CommandInteraction) => { // Do not allow any changing of the owner if (member.id === interaction.guild.ownerId) return "You cannot change the owner's nickname"; // Check if Nucleus can change the nickname - if (!(mePos > applyPos)) return "I do not have a role higher than that member"; + if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`; // Check if Nucleus has permission to change the nickname if (!me.permissions.has("ManageNicknames")) return "I do not have the *Manage Nicknames* permission"; // Allow the owner to change anyone's nickname if (member.id === interaction.guild.ownerId) return true; - // Check if the user has manage_nicknames permission - if (!member.permissions.has("ManageNicknames")) - return "You do not have the *Manage Nicknames* permission"; // Allow changing your own nickname if (member === apply) return true; // Check if the user is below on the role list - if (!(memberPos > applyPos)) return "You do not have a role higher than that member"; + if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`; // Allow change return true; }; export { command, callback, check }; +export const metadata = { + longDescription: "Changes the nickname of a member. This is the name that shows in the member list and on messages.", + premiumOnly: true, +} diff --git a/src/commands/mod/purge.ts b/src/commands/mod/purge.ts index e6b4670..8644e26 100644 --- a/src/commands/mod/purge.ts +++ b/src/commands/mod/purge.ts @@ -1,6 +1,5 @@ -import { JSONTranscriptFromMessageArray, JSONTranscriptToHumanReadable } from '../../utils/logTranscripts.js'; -import Discord, { CommandInteraction, GuildChannel, GuildMember, TextChannel, ButtonStyle, ButtonBuilder } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import Discord, { CommandInteraction, GuildChannel, GuildMember, TextChannel, ButtonStyle, ButtonBuilder, Message } from "discord.js"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import keyValueList from "../../utils/generateKeyValueList.js"; @@ -30,7 +29,7 @@ const callback = async (interaction: CommandInteraction): Promise => { if (!interaction.guild) return; const user = (interaction.options.getMember("user") as GuildMember | null); const channel = interaction.channel as GuildChannel; - if (channel.isTextBased()) { + if (!channel.isTextBased()) { return await interaction.reply({ embeds: [ new EmojiEmbed() @@ -94,7 +93,7 @@ const callback = async (interaction: CommandInteraction): Promise => { let component; try { component = m.awaitMessageComponent({ - filter: (m) => m.user.id === interaction.user.id && m.channel!.id === interaction.channel!.id, + filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id, time: 300000 }); } catch (e) { @@ -148,7 +147,7 @@ const callback = async (interaction: CommandInteraction): Promise => { calculateType: "messageDelete", color: NucleusColors.red, emoji: "CHANNEL.PURGE.RED", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), @@ -161,7 +160,8 @@ const callback = async (interaction: CommandInteraction): Promise => { } }; log(data); - const transcript = JSONTranscriptToHumanReadable(JSONTranscriptFromMessageArray(deleted)!); + const newOut = await client.database.transcripts.createTranscript(deleted, interaction, interaction.member as GuildMember); + const transcript = client.database.transcripts.toHumanReadable(newOut); const attachmentObject = { attachment: Buffer.from(transcript), name: `purge-${channel.id}-${Date.now()}.txt`, @@ -188,7 +188,7 @@ const callback = async (interaction: CommandInteraction): Promise => { let component; try { component = await m.awaitMessageComponent({ - filter: (m) => m.user.id === interaction.user.id && m.channel!.id === interaction.channel!.id, + filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id, time: 300000 }); } catch { @@ -296,7 +296,7 @@ const callback = async (interaction: CommandInteraction): Promise => { calculateType: "messageDelete", color: NucleusColors.red, emoji: "CHANNEL.PURGE.RED", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), @@ -309,35 +309,17 @@ const callback = async (interaction: CommandInteraction): Promise => { } }; log(data); - let out = ""; - messages.reverse().forEach((message) => { - if (!message) { - out += "Unknown message\n\n" - } else { - const author = message.author ?? { username: "Unknown", discriminator: "0000", id: "Unknown" }; - out += `${author.username}#${author.discriminator} (${author.id}) [${new Date( - message.createdTimestamp - ).toISOString()}]\n`; - if (message.content) { - const lines = message.content.split("\n"); - lines.forEach((line) => { - out += `> ${line}\n`; - }); - } - if (message.attachments.size > 0) { - message.attachments.forEach((attachment) => { - out += `Attachment > ${attachment.url}\n`; - }); - } - out += "\n\n"; - } - }); - const attachmentObject = { - attachment: Buffer.from(out), - name: `purge-${channel.id}-${Date.now()}.txt`, - description: "Purge log" - }; - const m = (await interaction.editReply({ + const messageArray: Message[] = messages.filter(message => !( + message!.components.some( + component => component.components.some( + child => child.customId?.includes("transcript") ?? false + ) + ) + )).map(message => message as Message); + const newOut = await client.database.transcripts.createTranscript(messageArray, interaction, interaction.member as GuildMember); + + const code = await client.database.transcripts.create(newOut); + await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.GREEN") @@ -347,62 +329,30 @@ const callback = async (interaction: CommandInteraction): Promise => { ], components: [ new Discord.ActionRowBuilder().addComponents([ - new Discord.ButtonBuilder() - .setCustomId("download") - .setLabel("Download transcript") - .setStyle(ButtonStyle.Success) - .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id")) + new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript?code=${code}`), ]) ] - })) as Discord.Message; - let component; - try { - component = await m.awaitMessageComponent({ - filter: (m) => m.user.id === interaction.user.id && m.channel!.id === interaction.channel!.id, - time: 300000 - }); - } catch { - return; - } - if (component.customId === "download") { - interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("CHANNEL.PURGE.GREEN") - .setTitle("Purge") - .setDescription("Transcript uploaded above") - .setStatus("Success") - ], - components: [], - files: [attachmentObject] - }); - } else { - interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("CHANNEL.PURGE.GREEN") - .setTitle("Purge") - .setDescription("Messages cleared") - .setStatus("Success") - ], - components: [] - }); - } + }); } }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, partial: boolean = false) => { if (!interaction.guild) return false; const member = interaction.member as GuildMember; + // Check if the user has manage_messages permission + if (!member.permissions.has("ManageMessages")) return "You do not have the *Manage Messages* permission"; + if (partial) return true; const me = interaction.guild.members.me!; // Check if nucleus has the manage_messages permission if (!me.permissions.has("ManageMessages")) return "I do not have the *Manage Messages* permission"; // Allow the owner to purge if (member.id === interaction.guild.ownerId) return true; - // Check if the user has manage_messages permission - if (!member.permissions.has("ManageMessages")) return "You do not have the *Manage Messages* permission"; // Allow purge return true; }; export { command, callback, check }; +export const metadata = { + longDescription: "Deletes a specified amount of messages from a channel, optionally from a specific user. Without an amount, you can repeatedly choose a number of messages to delete.", + premiumOnly: true, +} diff --git a/src/commands/mod/slowmode.ts b/src/commands/mod/slowmode.ts index 9792827..f282e82 100644 --- a/src/commands/mod/slowmode.ts +++ b/src/commands/mod/slowmode.ts @@ -1,7 +1,7 @@ // @ts-expect-error import humanizeDuration from "humanize-duration"; import type { CommandInteraction, GuildMember, TextChannel } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import keyValueList from "../../utils/generateKeyValueList.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; @@ -47,7 +47,7 @@ const callback = async (interaction: CommandInteraction): Promise => { }) + "Are you sure you want to set the slowmode in this channel?" ) .setColor("Danger") - .setFailedMessage("No changes were made", "Danger", "CHANNEL.SLOWMODE.ON") + .setFailedMessage("No changes were made", "Success", "CHANNEL.SLOWMODE.ON") .send(); if (confirmation.cancelled || !confirmation.success) return; try { @@ -76,14 +76,19 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, partial: boolean = false) => { const member = interaction.member as GuildMember; - // Check if Nucleus can set the slowmode - if (!interaction.guild!.members.me!.permissions.has("ManageChannels")) return "I do not have the *Manage Channels* permission"; // Check if the user has manage_channel permission if (!member.permissions.has("ManageChannels")) return "You do not have the *Manage Channels* permission"; + if (partial) return true; + // Check if Nucleus can set the slowmode + if (!interaction.guild!.members.me!.permissions.has("ManageChannels")) return "I do not have the *Manage Channels* permission"; // Allow slowmode return true; }; export { command, callback, check }; +export const metadata = { + longDescription: "Stops members from being able to send messages without waiting a certain amount of time between messages.", + premiumOnly: true, +} diff --git a/src/commands/mod/softban.ts b/src/commands/mod/softban.ts index 2787e91..1b404c9 100644 --- a/src/commands/mod/softban.ts +++ b/src/commands/mod/softban.ts @@ -1,11 +1,12 @@ import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, User, ButtonStyle } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import keyValueList from "../../utils/generateKeyValueList.js"; import addPlurals from "../../utils/plurals.js"; import client from "../../utils/client.js"; import { LinkWarningFooter } from "../../utils/defaults.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; const command = (builder: SlashCommandSubcommandBuilder) => @@ -124,17 +125,20 @@ const callback = async (interaction: CommandInteraction): Promise => { calculateType: "guildMemberPunish", color: NucleusColors.yellow, emoji: "PUNISH.BAN.YELLOW", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(member.user.id, `\`${member.user.id}\``), name: entry(member.user.id, renderUser(member.user)), - softbanned: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())), + softbanned: entry(Date.now().toString(), renderDelta(Date.now())), softbannedBy: entry(interaction.user.id, renderUser(interaction.user)), reason: entry(reason, reason ? `\n> ${reason}` : "*No reason provided.*"), accountCreated: entry(member.user.createdTimestamp, renderDelta(member.user.createdTimestamp)), serverMemberCount: interaction.guild.memberCount }, + separate: { + end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified` + }, hidden: { guild: interaction.guild.id } @@ -167,9 +171,12 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = async (interaction: CommandInteraction) => { +const check = async (interaction: CommandInteraction, partial: boolean = false) => { if (!interaction.guild) return; const member = interaction.member as GuildMember; + // Check if the user has ban_members permission + if (!member.permissions.has("BanMembers")) return "You do not have the *Ban Members* permission"; + if (partial) return true; const me = interaction.guild.members.me!; let apply = interaction.options.getUser("user") as User | GuildMember; const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0; @@ -182,19 +189,17 @@ const check = async (interaction: CommandInteraction) => { apply = apply as User } // Do not allow banning the owner - if (member.id === interaction.guild.ownerId) throw new Error("You cannot softban the owner of the server"); + if (member.id === interaction.guild.ownerId) return "You cannot softban the owner of the server"; // Check if Nucleus can ban the member - if (!(mePos > applyPos)) throw new Error("I do not have a role higher than that member"); + if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`; // Check if Nucleus has permission to ban - if (!me.permissions.has("BanMembers")) throw new Error("I do not have the *Ban Members* permission"); + if (!me.permissions.has("BanMembers")) return "I do not have the *Ban Members* permission"; // Do not allow banning Nucleus - if (member.id === me.id) throw new Error("I cannot softban myself"); + if (member.id === me.id) return "I cannot softban myself"; // Allow the owner to ban anyone if (member.id === interaction.guild.ownerId) return true; - // Check if the user has ban_members permission - if (!member.permissions.has("BanMembers")) throw new Error("You do not have the *Ban Members* permission"); // Check if the user is below on the role list - if (!(memberPos > applyPos)) throw new Error("You do not have a role higher than that member"); + if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`; // Allow ban return true; }; diff --git a/src/commands/mod/unban.ts b/src/commands/mod/unban.ts index 37fee99..40f4504 100644 --- a/src/commands/mod/unban.ts +++ b/src/commands/mod/unban.ts @@ -1,5 +1,5 @@ import type { CommandInteraction, GuildMember, User } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import keyValueList from "../../utils/generateKeyValueList.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; @@ -57,12 +57,12 @@ const callback = async (interaction: CommandInteraction): Promise => { calculateType: "guildMemberPunish", color: NucleusColors.green, emoji: "PUNISH.BAN.GREEN", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(member.id, `\`${member.id}\``), name: entry(member.id, renderUser(member)), - unbanned: entry(new Date().getTime(), renderDelta(new Date().getTime())), + unbanned: entry(Date.now(), renderDelta(Date.now())), unbannedBy: entry(interaction.user.id, renderUser(interaction.user)), accountCreated: entry(member.createdTimestamp, renderDelta(member.createdTimestamp)) }, @@ -107,16 +107,17 @@ const callback = async (interaction: CommandInteraction): Promise => { } }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, partial: boolean = false) => { if (!interaction.guild) return; const member = interaction.member as GuildMember; + // Check if the user has ban_members permission + if (!member.permissions.has("BanMembers")) return "You do not have the *Ban Members* permission"; + if (partial) return true; const me = interaction.guild.members.me!; // Check if Nucleus can unban members if (!me.permissions.has("BanMembers")) return "I do not have the *Ban Members* permission"; // Allow the owner to unban anyone if (member.id === interaction.guild.ownerId) return true; - // Check if the user has ban_members permission - if (!member.permissions.has("BanMembers")) return "You do not have the *Ban Members* permission"; // Allow unban return true; }; diff --git a/src/commands/mod/unmute.ts b/src/commands/mod/unmute.ts index e2585e1..8562c4c 100644 --- a/src/commands/mod/unmute.ts +++ b/src/commands/mod/unmute.ts @@ -1,9 +1,10 @@ import type { CommandInteraction, GuildMember } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import keyValueList from "../../utils/generateKeyValueList.js"; import client from "../../utils/client.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder @@ -105,14 +106,17 @@ const callback = async (interaction: CommandInteraction): Promise => { calculateType: "guildMemberPunish", color: NucleusColors.green, emoji: "PUNISH.MUTE.GREEN", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(member.user.id, `\`${member.user.id}\``), name: entry(member.user.id, renderUser(member.user)), - unmuted: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())), + unmuted: entry(Date.now().toString(), renderDelta(Date.now())), unmutedBy: entry(interaction.user.id, renderUser(interaction.user)) }, + separate: { + end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified` + }, hidden: { guild: interaction.guild.id } @@ -131,9 +135,13 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, partial: boolean = false) => { if (!interaction.guild) return; const member = interaction.member as GuildMember; + // Check if the user has moderate_members permission + if (!member.permissions.has("ModerateMembers")) + return "You do not have the *Moderate Members* permission"; + if (partial) return true; const me = interaction.guild.members.me!; const apply = interaction.options.getMember("user") as GuildMember; const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0; @@ -142,16 +150,13 @@ const check = (interaction: CommandInteraction) => { // Do not allow unmuting the owner if (member.id === interaction.guild.ownerId) return "You cannot unmute the owner of the server"; // Check if Nucleus can unmute the member - if (!(mePos > applyPos)) return "I do not have a role higher than that member"; + if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`; // Check if Nucleus has permission to unmute if (!me.permissions.has("ModerateMembers")) return "I do not have the *Moderate Members* permission"; // Allow the owner to unmute anyone if (member.id === interaction.guild.ownerId) return true; - // Check if the user has moderate_members permission - if (!member.permissions.has("ModerateMembers")) - return "You do not have the *Moderate Members* permission"; // Check if the user is below on the role list - if (!(memberPos > applyPos)) return "You do not have a role higher than that member"; + if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`; // Allow unmute return true; }; diff --git a/src/commands/mod/viewas.ts b/src/commands/mod/viewas.ts index b176dd4..ef62816 100644 --- a/src/commands/mod/viewas.ts +++ b/src/commands/mod/viewas.ts @@ -7,9 +7,10 @@ import Discord, { ButtonStyle, NonThreadGuildBasedChannel, StringSelectMenuOptionBuilder, - StringSelectMenuBuilder + StringSelectMenuBuilder, + APIMessageComponentEmoji } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import type { GuildBasedChannel } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; @@ -126,8 +127,7 @@ const callback = async (interaction: CommandInteraction): Promise => { return new StringSelectMenuOptionBuilder() .setLabel(c) .setValue((set * 25 + i).toString()) - // @ts-expect-error - .setEmoji(getEmojiByName("ICONS.CHANNEL.CATEGORY", "id")) // Again, this is valid but TS doesn't think so + .setEmoji(getEmojiByName("ICONS.CHANNEL.CATEGORY", "id") as APIMessageComponentEmoji) // Again, this is valid but TS doesn't think so .setDefault((set * 25 + i) === page) })) )} @@ -157,19 +157,19 @@ const callback = async (interaction: CommandInteraction): Promise => { }); let i; try { - i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id, time: 30000}); + i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id, time: 30000}); } catch (e) { closed = true; continue; } - i.deferUpdate(); + await i.deferUpdate(); if (i.customId === "back") page--; else if (i.customId === "right") page++; else if (i.customId === "category" && i.isStringSelectMenu()) page = parseInt(i.values[0]!); } }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as GuildMember; if (!member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission"; return true; diff --git a/src/commands/mod/warn.ts b/src/commands/mod/warn.ts index 38aa4ad..ea4f084 100644 --- a/src/commands/mod/warn.ts +++ b/src/commands/mod/warn.ts @@ -1,10 +1,11 @@ import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import keyValueList from "../../utils/generateKeyValueList.js"; import { create, areTicketsEnabled } from "../../actions/createModActionTicket.js"; import client from "../../utils/client.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; import { LinkWarningFooter } from "../../utils/defaults.js"; const command = (builder: SlashCommandSubcommandBuilder) => @@ -116,7 +117,7 @@ const callback = async (interaction: CommandInteraction): Promise => { calculateType: "guildMemberPunish", color: NucleusColors.yellow, emoji: "PUNISH.WARN.YELLOW", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { user: entry( @@ -124,7 +125,10 @@ const callback = async (interaction: CommandInteraction): Promise => { renderUser((interaction.options.getMember("user") as GuildMember).user) ), warnedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as Discord.User)), - reason: reason ? `\n> ${reason}` : "*No reason provided*" + reason: reason ? reason : "*No reason provided*" + }, + separate: { + end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified` }, hidden: { guild: interaction.guild.id @@ -186,7 +190,7 @@ const callback = async (interaction: CommandInteraction): Promise => { let component; try { component = await m.awaitMessageComponent({ - filter: (m) => m.user.id === interaction.user.id && m.channel!.id === interaction.channel!.id, + filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id, time: 300000 }); } catch (e) { @@ -275,9 +279,12 @@ const callback = async (interaction: CommandInteraction): Promise => { } }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, partial: boolean = false) => { if (!interaction.guild) return; const member = interaction.member as GuildMember; + if (!member.permissions.has("ModerateMembers")) + return "You do not have the *Moderate Members* permission"; + if(partial) return true; const apply = interaction.options.getMember("user") as GuildMember | null; if (apply === null) return "That member is not in the server"; const memberPos = member.roles.cache.size ? member.roles.highest.position : 0; @@ -287,10 +294,8 @@ const check = (interaction: CommandInteraction) => { // Allow the owner to warn anyone if (member.id === interaction.guild.ownerId) return true; // Check if the user has moderate_members permission - if (!member.permissions.has("ModerateMembers")) - return "You do not have the *Moderate Members* permission"; // Check if the user is below on the role list - if (!(memberPos > applyPos)) return "You do not have a role higher than that member"; + if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`; // Allow warn return true; }; diff --git a/src/commands/nucleus/_meta.ts b/src/commands/nucleus/_meta.ts index 521b338..bd7fd14 100644 --- a/src/commands/nucleus/_meta.ts +++ b/src/commands/nucleus/_meta.ts @@ -3,8 +3,6 @@ import { command } from "../../utils/commandRegistration/slashCommandBuilder.js" const name = "nucleus"; const description = "Commands relating to Nucleus itself"; -const subcommand = await command(name, description, `nucleus`) +const subcommand = await command(name, description, `nucleus`, undefined, undefined, undefined, undefined, true); -const allowedInDMs = true; - -export { name, description, subcommand as command, allowedInDMs }; +export { name, description, subcommand as command }; diff --git a/src/commands/nucleus/guide.ts b/src/commands/nucleus/guide.ts index d3370ba..270ee62 100644 --- a/src/commands/nucleus/guide.ts +++ b/src/commands/nucleus/guide.ts @@ -1,5 +1,5 @@ import type { CommandInteraction } from 'discord.js'; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import guide from "../../reflex/guide.js"; const command = (builder: SlashCommandSubcommandBuilder) => @@ -9,10 +9,5 @@ const callback = async (interaction: CommandInteraction) => { guide(interaction.guild!, interaction); }; -const check = () => { - return true; -}; - export { command }; export { callback }; -export { check }; diff --git a/src/commands/nucleus/invite.ts b/src/commands/nucleus/invite.ts index fd65e51..b89425a 100644 --- a/src/commands/nucleus/invite.ts +++ b/src/commands/nucleus/invite.ts @@ -1,5 +1,5 @@ import { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import client from "../../utils/client.js"; @@ -29,10 +29,5 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = () => { - return true; -}; - export { command }; export { callback }; -export { check }; diff --git a/src/commands/nucleus/ping.ts b/src/commands/nucleus/ping.ts index 12f1c6b..3e02a8f 100644 --- a/src/commands/nucleus/ping.ts +++ b/src/commands/nucleus/ping.ts @@ -1,6 +1,6 @@ import { LoadingEmbed } from "../../utils/defaults.js"; import type { CommandInteraction } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import client from "../../utils/client.js"; @@ -10,9 +10,9 @@ const command = (builder: SlashCommandSubcommandBuilder) => const callback = async (interaction: CommandInteraction): Promise => { // WEBSOCKET | Nucleus -> Discord // EDITING | Nucleus -> discord -> nucleus | edit time / 2 - const initial = new Date().getTime(); + const initial = Date.now(); await interaction.reply({ embeds: LoadingEmbed, ephemeral: true }); - const ping = new Date().getTime() - initial; + const ping = Date.now() - initial; interaction.editReply({ embeds: [ new EmojiEmbed() @@ -28,10 +28,5 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = () => { - return true; -}; - export { command }; export { callback }; -export { check }; diff --git a/src/commands/nucleus/premium.ts b/src/commands/nucleus/premium.ts index 745f167..c431c8e 100644 --- a/src/commands/nucleus/premium.ts +++ b/src/commands/nucleus/premium.ts @@ -1,32 +1,211 @@ -import type { CommandInteraction } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, CommandInteraction, ComponentType, Message, StringSelectMenuBuilder, StringSelectMenuInteraction } from "discord.js"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; +import client from "../../utils/client.js"; +import { LoadingEmbed } from "../../utils/defaults.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder.setName("premium").setDescription("Information about Nucleus Premium"); +//TODO: Allow User to remove Premium + +const dmcallback = async (interaction: CommandInteraction, firstDescription: string, msg: Message): Promise => { + let closed = false; + do { + const dbUser = await client.database.premium.fetchUser(interaction.user.id); + if(!dbUser) { + await interaction.editReply({embeds: [ + new EmojiEmbed() + .setTitle("Premium") + .setDescription(`*You do not have premium! You can't activate premium on any servers.*` + firstDescription) + .setEmoji("NUCLEUS.LOGO") + .setStatus("Danger") + ]}); + return; + } + const premiumGuilds = dbUser.appliesTo.map((guildID) => { + const guild = client.guilds.cache.get(guildID); + if(!guild) return undefined; + return guild.name; + }); + + const options = premiumGuilds.filter((guild) => guild !== undefined) as string[]; + + const removeRow = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("currentPremium") + .setPlaceholder("Select a server to remove premium from") + .setDisabled(premiumGuilds.length === 0) + .addOptions(options.slice(0, Math.min(options.length, 24)).map((guild) => { + return {label: guild, value: guild} + })) + ); + const cancel = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("cancel") + .setLabel("Close") + .setStyle(ButtonStyle.Danger) + ); + + const components: ActionRowBuilder[] = [cancel]; + if(options.length > 0) components.unshift(removeRow); + await interaction.editReply( + { + embeds: [ + new EmojiEmbed() + .setTitle("Premium") + .setDescription( + `*You have premium on the following servers:*\n\n` + + (options.length > 0 ? options.join(', ') : `You have not activated premium in any guilds`) + + firstDescription) + .setEmoji("NUCLEUS.LOGO") + .setStatus("Success") + ], + components: components + }); + + let i: StringSelectMenuInteraction | ButtonInteraction; + try { + const filter = (i: StringSelectMenuInteraction | ButtonInteraction) => i.user.id === interaction.user.id; + i = await msg.awaitMessageComponent({time: 300000, filter}) + } catch (e) { + await interaction.deleteReply(); + closed = true; + break; + } + await i.deferUpdate(); + if(i.isButton()) { + closed = true; + } else { + const response = client.database.premium.removePremium(interaction.user.id, i.values[0]!); + console.log(response) + } + } while (!closed); + await interaction.deleteReply(); +} const callback = async (interaction: CommandInteraction): Promise => { - interaction.reply({ + if (interaction.guild) client.database.premium.hasPremium(interaction.guild.id).finally(() => {}); + const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true}) + const member = await (await interaction.client.guilds.fetch("684492926528651336")).members.fetch(interaction.user.id).catch(() => { + interaction.editReply({ embeds: [ + new EmojiEmbed() + .setTitle("Premium") + .setDescription(`*You are not currently in the Clicks Server. To gain access to premium please join.*` + firstDescription) + .setEmoji("NUCLEUS.LOGO") + .setStatus("Danger") + ], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setStyle(ButtonStyle.Link).setLabel("Join").setURL("https://discord.gg/bPaNnxe"))] }); + }) + if (!member) return; + const firstDescription = "\n\nPremium allows servers of your choice to get access to extra features for a fixed price per month.\nThis includes:\n" + + `${getEmojiByName("MOD.IMAGES.TOOSMALL")} Attachment logs - Stores attachments so they can be viewed after a message is deleted.\n` + + `${getEmojiByName("GUILD.TICKET.ARCHIVED")} Ticket Transcripts - Gives a link to view the history of a ticket after it has been closed.\n` + const dbMember = await client.database.premium.fetchUser(interaction.user.id) + let premium = `You do not have premium! You can't activate premium on any servers.`; + let count = 0; + const {level, appliesTo} = dbMember ?? {level: 0, appliesTo: []} + if (level === 99) { + premium = `You have Infinite Premium! You have been gifted this by the developers as a thank you. You can give premium to any and all servers you are in.`; + count = 200; + } else if (level === 1) { + premium = `You have Premium tier 1! You can give premium to ${1 - appliesTo.length} more server(s).`; + count = 1; + } else if (level === 2) { + premium = `You have Premium tier 2! You can give premium to ${3 - appliesTo.length} more server(s).`; + count = 3; + } else if (level === 3) { + premium = `You have Premium Mod! You can give premium to ${3 - appliesTo.length} more server(s), as well as automatically giving premium to all servers you have a "manage" permission in.` + count = 3; + } + if (dbMember?.expiresAt) { + premium = `**You can't give servers premium anymore because your subscription ended or was cancelled.** To get premium again please subscribe in the Clicks server` + count = 0; + } + if(!interaction.guild) return await dmcallback(interaction, firstDescription, m); + const hasPremium = await client.database.premium.hasPremium(interaction.guild!.id); + let premiumGuild = "" + if (hasPremium) { + premiumGuild = `**This server has premium! It was ${hasPremium[2] === 3 && hasPremium[3] ? `automatically applied by <@${hasPremium[1]}>` : `given by <@${hasPremium[1]}>`}**\n\n` + } + + const components: ActionRowBuilder[] = [] + if (level === 0 || dbMember?.expiresAt) { + components.push( + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setStyle(ButtonStyle.Link) + .setLabel("Join Clicks") + .setURL("https://discord.gg/bPaNnxe") + ) + ) + } else { + components.push( + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setStyle(premiumGuild.length > 0 ? ButtonStyle.Secondary : ButtonStyle.Success) + .setLabel(premiumGuild.length > 0 ? "This server has premium" : "Activate premium here") + .setCustomId("premiumActivate") + .setDisabled(count <= 0 || (hasPremium ? hasPremium[0] : false)) + ) + ) + } + + interaction.editReply({ embeds: [ new EmojiEmbed() .setTitle("Premium") .setDescription( - "*Nucleus Premium is currently not available.*\n\n" + - "Premium allows your server to get access to extra features, for a fixed price per month.\nThis includes:\n" + - "- Attachment logs - Stores attachments so they can be viewed after a message is deleted.\n" + - "- Ticket Transcripts - Gives a link to view the history of a ticket after it has been closed.\n" + premiumGuild + premium + firstDescription ) .setEmoji("NUCLEUS.LOGO") .setStatus("Danger") + .setImage("https://assets.clicks.codes/ads/ads/nucleus-premium.png") ], - ephemeral: true + components: components }); -}; -const check = () => { - return true; + const filter = (i: ButtonInteraction) => i.customId === "premiumActivate" && i.user.id === interaction.user.id; + let i; + try { + i = await interaction.channel!.awaitMessageComponent<2>({ filter, time: 60000 }); + } catch (e) { + return; + } + i.deferUpdate(); + const guild = i.guild!; + if (count - appliesTo.length <= 0) { + interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Premium") + .setDescription( + `You have already activated premium on the maximum amount of servers!` + firstDescription + ) + .setEmoji("NUCLEUS.PREMIUMACTIVATE") + .setStatus("Danger") + ], + components: [] + }); + } else { + await client.database.premium.addPremium(interaction.user.id, guild.id); + interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Premium") + .setDescription( + `You have activated premium on this server!` + firstDescription + ) + .setEmoji("NUCLEUS.LOGO") + .setStatus("Danger") + ], + components: [] + }); + } }; export { command }; export { callback }; -export { check }; diff --git a/src/commands/nucleus/stats.ts b/src/commands/nucleus/stats.ts index d8b2807..19c0949 100644 --- a/src/commands/nucleus/stats.ts +++ b/src/commands/nucleus/stats.ts @@ -1,5 +1,5 @@ import type { CommandInteraction } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import client from "../../utils/client.js"; @@ -13,16 +13,11 @@ const callback = async (interaction: CommandInteraction): Promise => { .setTitle("Stats") .setDescription(`**Servers:** ${client.guilds.cache.size}\n` + `**Ping:** \`${client.ws.ping * 2}ms\``) .setStatus("Success") - .setEmoji("GUILD.GRAPHS") + .setEmoji("SETTINGS.STATS.GREEN") ], ephemeral: true }); }; -const check = () => { - return true; -}; - export { command }; export { callback }; -export { check }; diff --git a/src/commands/nucleus/suggest.ts b/src/commands/nucleus/suggest.ts index de0e69b..6ba3445 100644 --- a/src/commands/nucleus/suggest.ts +++ b/src/commands/nucleus/suggest.ts @@ -1,7 +1,7 @@ import { LoadingEmbed } from '../../utils/defaults.js'; import { ButtonStyle, CommandInteraction } from "discord.js"; import Discord from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import client from "../../utils/client.js"; @@ -66,10 +66,5 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = () => { - return true; -}; - export { command }; export { callback }; -export { check }; diff --git a/src/commands/privacy.ts b/src/commands/privacy.ts index 9100302..46784f5 100644 --- a/src/commands/privacy.ts +++ b/src/commands/privacy.ts @@ -1,6 +1,5 @@ import { LoadingEmbed } from "../utils/defaults.js"; -import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, SelectMenuOptionBuilder, StringSelectMenuBuilder } from "discord.js"; -import { SlashCommandBuilder } from "@discordjs/builders"; +import Discord, { SlashCommandBuilder, CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, SelectMenuOptionBuilder, StringSelectMenuBuilder } from "discord.js"; import EmojiEmbed from "../utils/generateEmojiEmbed.js"; import getEmojiByName from "../utils/getEmojiByName.js"; import createPageIndicator from "../utils/createPageIndicator.js"; @@ -22,8 +21,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription( "Nucleus is a bot that naturally needs to store data about servers.\n" + "We are entirely [open source](https://github.com/ClicksMinutePer/Nucleus), so you can check exactly what we store, and how it works.\n\n" + - "If you are a server administrator, you can view the options page in the dropdown under this message.\n\n" + - "Any questions about Nucleus, how it works and data stored can be asked in [our server](https://discord.gg/bPaNnxe)." + "Any questions about Nucleus, how it works, and what data is stored can be asked in [our server](https://discord.gg/bPaNnxe)." ) .setEmoji("NUCLEUS.LOGO") .setStatus("Danger") @@ -50,39 +48,37 @@ const callback = async (interaction: CommandInteraction): Promise => { new EmojiEmbed() .setTitle("Link scanning and Transcripts") .setDescription( - "**Facebook** - Facebook trackers include data such as your date of birth, and guess your age if not entered, your preferences, who you interact with and more.\n" + - "**AMP** - AMP is a technology that allows websites to be served by Google. This means Google can store and track data, and are pushing this to as many pages as possible.\n\n" + - "Transcripts allow you to store all messages sent in a channel. This could be an issue in some cases, as they are hosted on [Pastebin](https://pastebin.com), so a leaked link could show all messages sent in the channel.\n" // TODO: Not on pastebin + "Transcripts allow you to store all messages sent in a channel. Transcripts are stored in our database along with the rest of the server's settings but is accessible by anyone with the link, so a leaked link could show all messages sent in the channel.\n" ) .setEmoji("NUCLEUS.LOGO") .setStatus("Danger") ) .setTitle("Link scanning and Transcripts") - .setDescription("Regarding Facebook and AMP filter types, and ticket transcripts") + .setDescription("Information about how links and images are scanned, and transcripts are stored") .setPageId(2) ].concat( (interaction.member as Discord.GuildMember).permissions.has("Administrator") ? [ - new Embed() - .setEmbed( - new EmojiEmbed() - .setTitle("Options") - .setDescription("Below are buttons for controlling this servers privacy settings") - .setEmoji("NUCLEUS.LOGO") - .setStatus("Danger") - ) - .setTitle("Options") - .setDescription("Options") - .setPageId(3) - .setComponents([ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel("Clear all data") - .setCustomId("clear-all-data") - .setStyle(ButtonStyle.Danger) - ]) - ]) - ] + new Embed() + .setEmbed( + new EmojiEmbed() + .setTitle("Options") + .setDescription("Below are buttons for controlling this servers privacy settings") + .setEmoji("NUCLEUS.LOGO") + .setStatus("Danger") + ) + .setTitle("Options") + .setDescription("Options") + .setPageId(3) + .setComponents([ + new ActionRowBuilder().addComponents([ + new ButtonBuilder() + .setLabel("Clear all data") + .setCustomId("clear-all-data") + .setStyle(ButtonStyle.Danger) + ]) + ]) + ] : [] ); const m = await interaction.reply({ @@ -150,14 +146,14 @@ const callback = async (interaction: CommandInteraction): Promise => { try { i = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id } }); } catch (e) { timedOut = true; continue; } nextFooter = null; - i.deferUpdate(); + await i.deferUpdate(); if (i.customId === "left") { if (page > 0) page--; selectPaneOpen = false; @@ -180,11 +176,12 @@ const callback = async (interaction: CommandInteraction): Promise => { .setColor("Danger") .send(true); if (confirmation.cancelled) { - break; + continue; } if (confirmation.success) { client.database.guilds.delete(interaction.guild!.id); client.database.history.delete(interaction.guild!.id); + client.database.notes.delete(interaction.guild!.id); nextFooter = "All data cleared"; continue; } else { @@ -208,10 +205,6 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = () => { - return true; -}; export { command }; export { callback }; -export { check }; diff --git a/src/commands/role/_meta.ts b/src/commands/role/_meta.ts deleted file mode 100644 index f546d51..0000000 --- a/src/commands/role/_meta.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { command } from "../../utils/commandRegistration/slashCommandBuilder.js"; - -const name = "role"; -const description = "Change roles for users"; - -const subcommand = await command(name, description, `role`); - -export { name, description, subcommand as command }; diff --git a/src/commands/role/user.ts b/src/commands/role/user.ts deleted file mode 100644 index ad29811..0000000 --- a/src/commands/role/user.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { CommandInteraction, GuildMember, Role, User } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; -import client from "../../utils/client.js"; -import confirmationMessage from "../../utils/confirmationMessage.js"; -import keyValueList from "../../utils/generateKeyValueList.js"; -import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; - -const command = (builder: SlashCommandSubcommandBuilder) => - builder - .setName("user") - .setDescription("Gives or removes a role from someone") - .addUserOption((option) => - option.setName("user").setDescription("The member to give or remove the role from").setRequired(true) - ) - .addRoleOption((option) => - option.setName("role").setDescription("The role to give or remove").setRequired(true) - ) - .addStringOption((option) => - option - .setName("action") - .setDescription("The action to perform") - .setRequired(true) - .addChoices( - {name: "Add", value: "give"}, - {name: "Remove", value: "remove"} - ) - ); - -const callback = async (interaction: CommandInteraction): Promise => { - const { renderUser, renderRole } = client.logger; - const action = interaction.options.get("action")?.value as string; - const role: Role = (await interaction.guild!.roles.fetch(interaction.options.get("role")?.value as string))!; - // TODO:[Modals] Replace this with a modal - const confirmation = await new confirmationMessage(interaction) - .setEmoji("GUILD.ROLES.DELETE") - .setTitle("Role") - .setDescription( - keyValueList({ - user: renderUser(interaction.options.getUser("user")! as User), - role: renderRole(role) - }) + - `\nAre you sure you want to ${ - action === "give" ? "give the role to" : "remove the role from" - } ${interaction.options.getUser("user")}?` - ) - .setColor("Danger") - .setFailedMessage("No changes were made", "Success", "GUILD.ROLES.CREATE") - .send(); - if (confirmation.cancelled || !confirmation.success) return; - try { - const member = interaction.options.getMember("user") as GuildMember; - if ((interaction.options.get("action")?.value as string) === "give") { - member.roles.add(role); - } else { - member.roles.remove(role); - } - } catch (e) { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Role") - .setDescription("Something went wrong and the role could not be added") - .setStatus("Danger") - .setEmoji("CONTROL.BLOCKCROSS") - ], - components: [] - }); - } - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Role") - .setDescription(`The role has been ${action === "give" ? "given" : "removed"} successfully`) - .setStatus("Success") - .setEmoji("GUILD.ROLES.CREATE") - ], - components: [] - }); -}; - -const check = (interaction: CommandInteraction) => { - const member = interaction.member as GuildMember; - if (!interaction.guild) return - const me = interaction.guild.members.me!; - const apply = interaction.options.getMember("user") as GuildMember | null; - if (apply === null) return "That member is not in the server"; - // Check if Nucleus has permission to role - if (!me.permissions.has("ManageRoles")) return "I do not have the *Manage Roles* permission"; - // Allow the owner to role anyone - if (member.id === interaction.guild.ownerId) return true; - // Check if the user has manage_roles permission - if (!member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission"; - // Allow role - return true; -}; - -export { command }; -export { callback }; -export { check }; diff --git a/src/commands/rolemenu.ts b/src/commands/rolemenu.ts index c1ceb2e..2861e05 100644 --- a/src/commands/rolemenu.ts +++ b/src/commands/rolemenu.ts @@ -1,5 +1,4 @@ -import type { CommandInteraction } from "discord.js"; -import { SlashCommandBuilder } from "@discordjs/builders"; +import { CommandInteraction, SlashCommandBuilder } from "discord.js"; import { callback as roleMenu } from "../actions/roleMenu.js"; const command = new SlashCommandBuilder() @@ -10,10 +9,5 @@ const callback = async (interaction: CommandInteraction): Promise => { await roleMenu(interaction); }; -const check = () => { - return true; -}; - export { command }; export { callback }; -export { check }; diff --git a/src/commands/server/about.ts b/src/commands/server/about.ts index 23a53b7..4c88365 100644 --- a/src/commands/server/about.ts +++ b/src/commands/server/about.ts @@ -1,5 +1,5 @@ import { CommandInteraction, GuildMFALevel } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import generateKeyValueList, { toCapitals } from "../../utils/generateKeyValueList.js"; @@ -74,10 +74,5 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = () => { - return true; -}; - export { command }; export { callback }; -export { check }; diff --git a/src/commands/server/buttons.ts b/src/commands/server/buttons.ts new file mode 100644 index 0000000..3297616 --- /dev/null +++ b/src/commands/server/buttons.ts @@ -0,0 +1,252 @@ +import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, ChannelType, CommandInteraction, MessageCreateOptions, ModalBuilder, SlashCommandSubcommandBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; +import type Discord from "discord.js"; +import { LoadingEmbed } from "../../utils/defaults.js"; +import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; +import lodash from "lodash"; +import getEmojiByName from "../../utils/getEmojiByName.js"; +import { modalInteractionCollector } from "../../utils/dualCollector.js"; + +export const command = new SlashCommandSubcommandBuilder() + .setName("buttons") + .setDescription("Create clickable buttons for verifying, role menus etc."); + +interface Data { + buttons: string[], + title: string | null, + description: string | null, + color: number, + channel: string | null +} + +const colors: Record = { + RED: 0xF27878, + ORANGE: 0xE5AB71, + YELLOW: 0xF2D478, + GREEN: 0x65CC76, + BLUE: 0x72AEF5, + PURPLE: 0xA358B2, + PINK: 0xD46899, + GRAY: 0x999999, +} + +const buttonNames: Record = { + verifybutton: "Verify", + rolemenu: "Role Menu", + createticket: "Create Ticket" +} + +export const callback = async (interaction: CommandInteraction): Promise => { + + const m = await interaction.reply({ + embeds: LoadingEmbed, + fetchReply: true, + ephemeral: true + }); + + let closed = false; + const data: Data = { + buttons: [], + title: null, + description: null, + color: colors["RED"]!, + channel: interaction.channelId + } + do { + + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("edit") + .setLabel("Edit Embed") + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId("send") + .setLabel("Send") + .setStyle(ButtonStyle.Primary) + .setDisabled(!data.channel) + ); + + const colorSelect = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("color") + .setPlaceholder("Select a color") + .setMinValues(1) + .addOptions( + Object.keys(colors).map((color: string) => { + return new StringSelectMenuOptionBuilder() + .setLabel(lodash.capitalize(color)) + .setValue(color) + .setEmoji(getEmojiByName("COLORS." + color, "id") as APIMessageComponentEmoji) + .setDefault(data.color === colors[color]) + } + ) + ) + ); + + const buttonSelect = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("button") + .setPlaceholder("Select buttons to add") + .setMinValues(1) + .setMaxValues(3) + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Verify") + .setValue("verifybutton") + .setDescription("Click to get verified in the server") + .setDefault(data.buttons.includes("verifybutton")), + new StringSelectMenuOptionBuilder() + .setLabel("Role Menu") + .setValue("rolemenu") + .setDescription("Click to customize your roles") + .setDefault(data.buttons.includes("rolemenu")), + new StringSelectMenuOptionBuilder() + .setLabel("Ticket") + .setValue("createticket") + .setDescription("Click to create a support ticket") + .setDefault(data.buttons.includes("createticket")) + ) + ) + + const channelMenu = new ActionRowBuilder() + .addComponents( + new ChannelSelectMenuBuilder() + .setCustomId("channel") + .setPlaceholder("Select a channel") + .setChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement, ChannelType.PublicThread, ChannelType.AnnouncementThread) + ) + let channelName = interaction.guild!.channels.cache.get(data.channel!)?.name; + if (data.channel === interaction.channelId) channelName = "this channel"; + const embed = new EmojiEmbed() + .setTitle(data.title ?? "No title set") + .setDescription(data.description ?? "*No description set*") + .setColor(data.color) + .setFooter({text: `Click the button below to edit the embed | The embed will be sent in ${channelName}`}); + + + await interaction.editReply({ + embeds: [embed], + components: [colorSelect, buttonSelect, channelMenu, buttons] + }); + + let i: Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction | Discord.StringSelectMenuInteraction; + try { + i = await interaction.channel!.awaitMessageComponent({ + filter: (i: Discord.Interaction) => i.user.id === interaction.user.id, + time: 300000 + }) as Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction | Discord.StringSelectMenuInteraction; + } catch (e) { + closed = true; + break; + } + if(i.isButton()) { + switch(i.customId) { + case "edit": { + await i.showModal( + new ModalBuilder() + .setCustomId("modal") + .setTitle(`Options for ${i.customId}`) + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("title") + .setLabel("Title") + .setMaxLength(256) + .setRequired(false) + .setStyle(TextInputStyle.Short) + .setValue(data.title ?? "") + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("description") + .setLabel("The text to display below the title") + .setMaxLength(4000) + .setRequired(false) + .setStyle(TextInputStyle.Paragraph) + .setValue(data.description ?? "") + ) + ) + ); + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Button Editor") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + .setEmoji("GUILD.TICKET.OPEN") + ], + components: [ + new ActionRowBuilder().addComponents([ + new ButtonBuilder() + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle(ButtonStyle.Primary) + .setCustomId("back") + ]) + ] + }); + let out: Discord.ModalSubmitInteraction | null; + try { + out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null; + } catch (e) { + closed = true; + continue; + } + if (!out || out.isButton()) continue + data.title = out.fields.getTextInputValue("title").length === 0 ? null : out.fields.getTextInputValue("title"); + data.description = out.fields.getTextInputValue("description").length === 0 ? null : out.fields.getTextInputValue("description"); + break; + } + case "send": { + await i.deferUpdate(); + const channel = interaction.guild!.channels.cache.get(data.channel!) as Discord.TextChannel; + const components = new ActionRowBuilder(); + for(const button of data.buttons) { + components.addComponents( + new ButtonBuilder() + .setCustomId(button) + .setLabel(buttonNames[button]!) + .setStyle(ButtonStyle.Primary) + ); + } + const messageData: MessageCreateOptions = {components: [components]} + if (data.title || data.description) { + const e = new EmojiEmbed() + if(data.title) e.setTitle(data.title); + if(data.description) e.setDescription(data.description); + if(data.color) e.setColor(data.color); + messageData.embeds = [e]; + } + await channel.send(messageData); + break; + } + } + } else if(i.isStringSelectMenu()) { + try {await i.deferUpdate();} catch (err) {console.log(err)} + switch(i.customId) { + case "color": { + data.color = colors[i.values[0]!]!; + break; + } + case "button": { + data.buttons = i.values; + break; + } + } + } else { + await i.deferUpdate(); + data.channel = i.values[0]!; + } + + } while (!closed); + await interaction.deleteReply(); +} + +export const check = (interaction: CommandInteraction, _partial: boolean = false) => { + const member = interaction.member as Discord.GuildMember; + if (!member.permissions.has("ManageMessages")) + return "You must have the *Manage Messages* permission to use this command"; + return true; +}; diff --git a/src/commands/settings/automod.ts b/src/commands/settings/automod.ts new file mode 100644 index 0000000..09b8914 --- /dev/null +++ b/src/commands/settings/automod.ts @@ -0,0 +1,922 @@ +import type Discord from "discord.js"; +import { ActionRowBuilder, + AnySelectMenuInteraction, + APIMessageComponentEmoji, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + ChannelSelectMenuBuilder, + ChannelSelectMenuInteraction, + CommandInteraction, + Message, + ModalBuilder, + RoleSelectMenuBuilder, + RoleSelectMenuInteraction, + StringSelectMenuBuilder, + StringSelectMenuInteraction, + StringSelectMenuOptionBuilder, + TextInputBuilder, + TextInputStyle, + UserSelectMenuBuilder, + UserSelectMenuInteraction +} from "discord.js"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; +import { LoadingEmbed } from "../../utils/defaults.js"; +import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; +import client from "../../utils/client.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; +import { modalInteractionCollector } from "../../utils/dualCollector.js"; +import listToAndMore from "../../utils/listToAndMore.js"; + + +const command = (builder: SlashCommandSubcommandBuilder) => + builder.setName("automod").setDescription("Setting for automatic moderation features"); + + +const emojiFromBoolean = (bool: boolean, id?: string) => bool ? getEmojiByName("CONTROL.TICK", id) : getEmojiByName("CONTROL.CROSS", id); + +const toSelectMenu = async (interaction: StringSelectMenuInteraction, m: Message, ids: string[], type: "member" | "role" | "channel", title: string): Promise => { + + const back = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId("back").setLabel("Back").setStyle(ButtonStyle.Secondary).setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)); + let closed; + do { + let render: string[] = [] + let mapped: string[] = []; + let menu: UserSelectMenuBuilder | RoleSelectMenuBuilder | ChannelSelectMenuBuilder; + switch(type) { + case "member": { + menu = new UserSelectMenuBuilder().setCustomId("user").setPlaceholder("Select users").setMaxValues(25); + mapped = await Promise.all(ids.map(async (id) => { return (await client.users.fetch(id).then(user => user.tag)) || "Unknown User" })); + render = ids.map(id => client.logger.renderUser(id)) + break; + } + case "role": { + menu = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder("Select roles").setMaxValues(25); + mapped = await Promise.all(ids.map(async (id) => { return (await interaction.guild!.roles.fetch(id).then(role => role?.name ?? "Unknown Role"))})); + render = ids.map(id => client.logger.renderRole(id, interaction.guild!)) + break; + } + case "channel": { + menu = new ChannelSelectMenuBuilder().setCustomId("channel").setPlaceholder("Select channels").setMaxValues(25); + mapped = await Promise.all(ids.map(async (id) => { return (await interaction.guild!.channels.fetch(id).then(channel => channel?.name ?? "Unknown Role")) })); + render = ids.map(id => client.logger.renderChannel(id)) + break; + } + } + const removeOptions = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("remove") + .setPlaceholder("Remove") + .addOptions(mapped.map((name, i) => new StringSelectMenuOptionBuilder().setLabel(name).setValue(ids[i]!))) + .setDisabled(ids.length === 0) + ); + + const embed = new EmojiEmbed() + .setTitle(title) + .setEmoji(getEmojiByName("GUILD.SETTINGS.GREEN")) + .setDescription(`Select ${type}s:\n\nCurrent:\n` + (render.length > 0 ? render.join("\n") : "None")) + .setStatus("Success"); + const components: ActionRowBuilder< + StringSelectMenuBuilder | + ButtonBuilder | + ChannelSelectMenuBuilder | + UserSelectMenuBuilder | + RoleSelectMenuBuilder + >[] = [new ActionRowBuilder().addComponents(menu)] + if(ids.length > 0) components.push(removeOptions); + components.push(back); + + await interaction.editReply({embeds: [embed], components: components}) + + let i: AnySelectMenuInteraction | ButtonInteraction; + try { + i = await m.awaitMessageComponent({filter: i => i.user.id === interaction.user.id, time: 300000}); + } catch(e) { + closed = true; + continue; + } + + if(i.isButton()) { + await i.deferUpdate(); + if(i.customId === "back") { + closed = true; + break; + } + } else if(i.isStringSelectMenu()) { + await i.deferUpdate(); + if(i.customId === "remove") { + ids = ids.filter(id => id !== (i as StringSelectMenuInteraction).values[0]); + if(ids.length === 0) { + menu.data.disabled = true; + } + } + } else { + await i.deferUpdate(); + if(i.customId === "user") { + ids = ids.concat((i as UserSelectMenuInteraction).values); + } else if(i.customId === "role") { + ids = ids.concat((i as RoleSelectMenuInteraction).values); + } else if(i.customId === "channel") { + ids = ids.concat((i as ChannelSelectMenuInteraction).values); + } + } + + } while(!closed) + return ids; +} + +const imageMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: { + NSFW: boolean, + size: boolean +}): Promise<{NSFW: boolean, size: boolean}> => { + let closed = false; + do { + const options = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("nsfw") + .setLabel("NSFW") + .setStyle(current.NSFW ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(emojiFromBoolean(current.NSFW, "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("size") + .setLabel("Size") + .setStyle(current.size ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(emojiFromBoolean(current.size, "id") as APIMessageComponentEmoji) + ) + + const embed = new EmojiEmbed() + .setTitle("Image Settings") + .setDescription( + `${emojiFromBoolean(current.NSFW)} **NSFW**\n` + + `${emojiFromBoolean(current.size)} **Size**\n` + ) + + await interaction.editReply({embeds: [embed], components: [options]}); + + let i: ButtonInteraction; + try { + i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction; + } catch (e) { + return current; + } + await i.deferUpdate(); + switch(i.customId) { + case "back": { + closed = true; + break; + } + case "nsfw": { + current.NSFW = !current.NSFW; + break; + } + case "size": { + current.size = !current.size; + break; + } + } + } while(!closed); + return current; +} + +const wordMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: { + enabled: boolean, + words: {strict: string[], loose: string[]}, + allowed: {users: string[], roles: string[], channels: string[]} +}): Promise<{ + enabled: boolean, + words: {strict: string[], loose: string[]}, + allowed: {users: string[], roles: string[], channels: string[]} +}> => { + let closed = false; + do { + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("enabled") + .setLabel("Enabled") + .setStyle(current.enabled ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(emojiFromBoolean(current.enabled, "id") as APIMessageComponentEmoji), + ); + + const selectMenu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("edit") + .setPlaceholder("Edit... ") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Words") + .setDescription("Edit your list of words to filter") + .setValue("words"), + new StringSelectMenuOptionBuilder() + .setLabel("Allowed Users") + .setDescription("Users who will be unaffected by the word filter") + .setValue("allowedUsers"), + new StringSelectMenuOptionBuilder() + .setLabel("Allowed Roles") + .setDescription("Roles that will be unaffected by the word filter") + .setValue("allowedRoles"), + new StringSelectMenuOptionBuilder() + .setLabel("Allowed Channels") + .setDescription("Channels where the word filter will not apply") + .setValue("allowedChannels") + ) + .setDisabled(!current.enabled) + ); + + const embed = new EmojiEmbed() + .setTitle("Word Filters") + .setDescription( + `${emojiFromBoolean(current.enabled)} **Enabled**\n` + + `**Strict Words:** ${listToAndMore(current.words.strict, 5)}\n` + + `**Loose Words:** ${listToAndMore(current.words.loose, 5)}\n\n` + + `**Users:** ` + listToAndMore(current.allowed.users.map(user => `<@${user}>`), 5) + `\n` + + `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `<@&${role}>`), 5) + `\n` + + `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `<#${channel}>`), 5) + ) + .setStatus("Success") + .setEmoji("GUILD.SETTINGS.GREEN") + + await interaction.editReply({embeds: [embed], components: [selectMenu, buttons]}); + + let i: ButtonInteraction | StringSelectMenuInteraction; + try { + i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction; + } catch (e) { + closed = true; + break; + } + + if(i.isButton()) { + await i.deferUpdate(); + switch(i.customId) { + case "back": { + closed = true; + break; + } + case "enabled": { + current.enabled = !current.enabled; + break; + } + } + } else { + switch(i.values[0]) { + case "words": { + await interaction.editReply({embeds: [new EmojiEmbed() + .setTitle("Word Filter") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + .setEmoji("GUILD.SETTINGS.GREEN") + ], components: [new ActionRowBuilder().addComponents(new ButtonBuilder() + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle(ButtonStyle.Primary) + .setCustomId("back") + )]}) + const modal = new ModalBuilder() + .setTitle("Word Filter") + .setCustomId("wordFilter") + .addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId("wordStrict") + .setLabel("Strict Words") + .setPlaceholder("Matches anywhere in the message, including surrounded by other characters") + .setValue(current.words.strict.join(", ")) + .setStyle(TextInputStyle.Paragraph) + .setRequired(false) + ), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId("wordLoose") + .setLabel("Loose Words") + .setPlaceholder("Matches only if the word is by itself, surrounded by spaces or punctuation") + .setValue(current.words.loose.join(", ")) + .setStyle(TextInputStyle.Paragraph) + .setRequired(false) + ) + ) + + await i.showModal(modal); + let out; + try { + out = await modalInteractionCollector(m, interaction.user); + } catch (e) { + break; + } + if (!out) break; + if(out.isButton()) break; + current.words.strict = out.fields.getTextInputValue("wordStrict") + .split(",").map(s => s.trim()).filter(s => s.length > 0); + current.words.loose = out.fields.getTextInputValue("wordLoose") + .split(",").map(s => s.trim()).filter(s => s.length > 0); + break; + } + case "allowedUsers": { + await i.deferUpdate(); + current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Word Filter"); + break; + } + case "allowedRoles": { + await i.deferUpdate(); + current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Word Filter"); + break; + } + case "allowedChannels": { + await i.deferUpdate(); + current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Word Filter"); + break; + } + } + } + } while(!closed); + return current; +} + +const inviteMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: { + enabled: boolean, + allowed: {users: string[], roles: string[], channels: string[]} +}): Promise<{ + enabled: boolean, + allowed: {users: string[], roles: string[], channels: string[]} +}> => { + + let closed = false; + do { + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("enabled") + .setLabel(current.enabled ? "Enabled" : "Disabled") + .setStyle(current.enabled ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(emojiFromBoolean(current.enabled, "id") as APIMessageComponentEmoji) + ); + const menu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("toEdit") + .setPlaceholder("Edit your allow list") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Users") + .setDescription("Users that are allowed to send invites") + .setValue("users"), + new StringSelectMenuOptionBuilder() + .setLabel("Roles") + .setDescription("Roles that are allowed to send invites") + .setValue("roles"), + new StringSelectMenuOptionBuilder() + .setLabel("Channels") + .setDescription("Channels that anyone is allowed to send invites in") + .setValue("channels") + ).setDisabled(!current.enabled) + ) + + const embed = new EmojiEmbed() + .setTitle("Invite Settings") + .setDescription( + "Automatically deletes invites sent by users (outside of staff members and self promotion channels)" + `\n\n` + + `${emojiFromBoolean(current.enabled)} **${current.enabled ? "Enabled" : "Disabled"}**\n\n` + + `**Users:** ` + listToAndMore(current.allowed.users.map(user => `<@${user}>`), 5) + `\n` + + `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `<@&${role}>`), 5) + `\n` + + `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `<#${channel}>`), 5) + ) + .setStatus("Success") + .setEmoji("GUILD.SETTINGS.GREEN") + + + await interaction.editReply({embeds: [embed], components: [menu, buttons]}); + + let i: ButtonInteraction | StringSelectMenuInteraction; + try { + i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction; + } catch (e) { + return current; + } + + if(i.isButton()) { + await i.deferUpdate(); + switch(i.customId) { + case "back": { + closed = true; + break; + } + case "enabled": { + current.enabled = !current.enabled; + break; + } + } + } else { + await i.deferUpdate(); + switch(i.values[0]) { + case "users": { + current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Invite Settings"); + break; + } + case "roles": { + current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Invite Settings"); + break; + } + case "channels": { + current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Invite Settings"); + break; + } + } + } + + } while(!closed); + return current; +} + +const mentionMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: { + mass: number, + everyone: boolean, + roles: boolean, + allowed: { + roles: string[], + rolesToMention: string[], + users: string[], + channels: string[] + } +}): Promise<{ + mass: number, + everyone: boolean, + roles: boolean, + allowed: { + roles: string[], + rolesToMention: string[], + users: string[], + channels: string[] + } +}> => { + let closed = false; + + do { + + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("everyone") + .setLabel(current.everyone ? "Everyone" : "No one") + .setStyle(current.everyone ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(emojiFromBoolean(current.everyone, "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("roles") + .setLabel(current.roles ? "Roles" : "No roles") + .setStyle(current.roles ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(emojiFromBoolean(current.roles, "id") as APIMessageComponentEmoji) + ); + const menu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("toEdit") + .setPlaceholder("Edit mention settings") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Mass Mention Amount") + .setDescription("The amount of mentions before the bot will delete the message") + .setValue("mass"), + new StringSelectMenuOptionBuilder() + .setLabel("Roles") + .setDescription("Roles that are able to be mentioned") + .setValue("roles"), + ) + ) + + const allowedMenu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("allowed") + .setPlaceholder("Edit exceptions") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Users") + .setDescription("Users that are unaffected by the mention filter") + .setValue("users"), + new StringSelectMenuOptionBuilder() + .setLabel("Roles") + .setDescription("Roles that are unaffected by the mention filter") + .setValue("roles"), + new StringSelectMenuOptionBuilder() + .setLabel("Channels") + .setDescription("Channels where anyone is unaffected by the mention filter") + .setValue("channels") + ) + ) + + const embed = new EmojiEmbed() + .setTitle("Mention Settings") + .setDescription( + `Log when members mention:\n` + + `${emojiFromBoolean(true)} **${current.mass}+ members** in one message\n` + + `${emojiFromBoolean(current.everyone)} **Everyone**\n` + + `${emojiFromBoolean(current.roles)} **Roles**\n` + + (current.allowed.rolesToMention.length > 0 ? `> *Except for ${listToAndMore(current.allowed.rolesToMention.map(r => `<@&${r}>`), 3)}*\n` : "") + + "\n" + + `Except if...\n` + + `> ${current.allowed.users.length > 0 ? `Member is: ${listToAndMore(current.allowed.users.map(u => `<@${u}>`), 3)}\n` : ""}` + + `> ${current.allowed.roles.length > 0 ? `Member has role: ${listToAndMore(current.allowed.roles.map(r => `<@&${r}>`), 3)}\n` : ""}` + + `> ${current.allowed.channels.length > 0 ? `In channel: ${listToAndMore(current.allowed.channels.map(c => `<#${c}>`), 3)}\n` : ""}` + ) + .setStatus("Success") + .setEmoji("GUILD.SETTINGS.GREEN") + + await interaction.editReply({embeds: [embed], components: [menu, allowedMenu, buttons]}); + + let i: ButtonInteraction | StringSelectMenuInteraction; + try { + i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction; + } catch (e) { + closed = true; + break; + } + + if(i.isButton()) { + await i.deferUpdate(); + switch (i.customId) { + case "back": { + closed = true; + break; + } + case "everyone": { + current.everyone = !current.everyone; + break; + } + case "roles": { + current.roles = !current.roles; + break; + } + } + } else { + switch (i.customId) { + case "toEdit": { + switch (i.values[0]) { + case "mass": { + await interaction.editReply({embeds: [new EmojiEmbed() + .setTitle("Word Filter") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + .setEmoji("GUILD.SETTINGS.GREEN") + ], components: [new ActionRowBuilder().addComponents(new ButtonBuilder() + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle(ButtonStyle.Primary) + .setCustomId("back") + )]}) + const modal = new ModalBuilder() + .setTitle("Mass Mention Amount") + .setCustomId("mass") + .addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId("mass") + .setPlaceholder("Amount") + .setMinLength(1) + .setMaxLength(3) + .setStyle(TextInputStyle.Short) + ) + ) + await i.showModal(modal); + let out; + try { + out = await modalInteractionCollector(m, interaction.user); + } catch (e) { + break; + } + if (!out) break; + if(out.isButton()) break; + current.mass = parseInt(out.fields.getTextInputValue("mass")); + break; + } + case "roles": { + await i.deferUpdate(); + current.allowed.rolesToMention = await toSelectMenu(interaction, m, current.allowed.rolesToMention, "role", "Mention Settings"); + break; + } + } + break; + } + case "allowed": { + await i.deferUpdate(); + switch (i.values[0]) { + case "users": { + current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Mention Settings"); + break; + } + case "roles": { + current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Mention Settings"); + break; + } + case "channels": { + current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Mention Settings"); + break; + } + } + break; + } + } + } + + } while(!closed); + return current +} + +const cleanMenu = async (interaction: StringSelectMenuInteraction, m: Message, current?: { + channels?: string[], + allowed?: { + roles: string[], + users: string[] + } +}): Promise<{ + channels: string[], + allowed: { + roles: string[], + users: string[] + } +}> => { + let closed = false; + if(!current) current = {channels: [], allowed: {roles: [], users: []}}; + if(!current.channels) current.channels = []; + if(!current.allowed) current.allowed = {roles: [], users: []}; + + const channelMenu = new ActionRowBuilder() + .addComponents( + new ChannelSelectMenuBuilder() + .setCustomId("toAdd") + .setPlaceholder("Select a channel") + ) + + const allowedMenu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("allowed") + .setPlaceholder("Edit exceptions") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Users") + .setDescription("Users that are unaffected by the mention filter") + .setValue("users"), + new StringSelectMenuOptionBuilder() + .setLabel("Roles") + .setDescription("Roles that are unaffected by the mention filter") + .setValue("roles") + ) + ) + + do { + + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + ) + + const embed = new EmojiEmbed() + .setTitle("Clean Settings") + .setEmoji("GUILD.SETTINGS.GREEN") + .setDescription( + `Current clean channels:\n\n` + + `${current.channels.length > 0 ? listToAndMore(current.channels.map(c => `<#${c}>`), 10) : "None"}\n\n` + ) + .setStatus("Success") + + + await interaction.editReply({embeds: [embed], components: [channelMenu, allowedMenu, buttons]}); + + let i: ButtonInteraction | ChannelSelectMenuInteraction; + try { + i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | ChannelSelectMenuInteraction; + } catch (e) { + closed = true; + break; + } + await i.deferUpdate(); + if(i.isButton()) { + switch (i.customId) { + case "back": { + closed = true; + break; + } + } + } else { + switch (i.customId) { + case "toAdd": { + const channelEmbed = new EmojiEmbed() + .setTitle("Clean Settings") + .setDescription(`Editing <#${i.values[0]}>`) + .setEmoji("GUILD.SETTINGS.GREEN") + .setStatus("Success") + const channelButtons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")), + new ButtonBuilder() + .setCustomId("switch") + .setLabel(current.channels.includes(i.values[0]!) ? "Remove" : "Add") + .setStyle(current.channels.includes(i.values[0]!) ? ButtonStyle.Danger : ButtonStyle.Success) + ) + + await i.editReply({embeds: [channelEmbed], components: [channelButtons]}); + let j: ButtonInteraction; + try { + j = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction; + } catch (e) { + closed = true; + break; + } + await j.deferUpdate(); + switch (j.customId) { + case "back": { + break; + } + case "switch": { + if(current.channels.includes(i.values[0]!)) { + current.channels.splice(current.channels.indexOf(i.values[0]!), 1); + } else { + current.channels.push(i.values[0]!); + } + } + } + break; + } + case "allowed": { + switch (i.values[0]) { + case "users": { + current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Mention Settings"); + break; + } + case "roles": { + current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Mention Settings"); + break; + } + } + break; + } + } + } + + } while(!closed); + + return current as { + channels: string[], + allowed: { + roles: string[], + users: string[] + } + }; + +} + +const callback = async (interaction: CommandInteraction): Promise => { + if (!interaction.guild) return; + const m = await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true}); + const config = (await client.database.guilds.read(interaction.guild.id)).filters; + + let closed = false; + + const button = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("save") + .setLabel("Save") + .setStyle(ButtonStyle.Success) + ) + + do { + + const selectMenu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("filter") + .setPlaceholder("Select a filter to edit") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Invites") + .setDescription("Automatically delete messages containing server invites") + .setValue("invites"), + new StringSelectMenuOptionBuilder() + .setLabel("Mentions") + .setDescription("Deletes messages with excessive mentions") + .setValue("mentions"), + new StringSelectMenuOptionBuilder() + .setLabel("Words") + .setDescription("Delete messages containing filtered words") + .setValue("words"), + new StringSelectMenuOptionBuilder() + .setLabel("Malware") + .setDescription("Automatically delete files and links containing malware") + .setValue("malware"), + new StringSelectMenuOptionBuilder() + .setLabel("Images") + .setDescription("Checks performed on images (NSFW, size checking, etc.)") + .setValue("images"), + new StringSelectMenuOptionBuilder() + .setLabel("Clean") + .setDescription("Automatically delete new messages in specific channels") + .setValue("clean") + ) + ); + + const embed = new EmojiEmbed() + .setTitle("Automod Settings") + .setDescription( + `${emojiFromBoolean(config.invite.enabled)} **Invites**\n` + + `${emojiFromBoolean(config.pings.everyone || config.pings.mass > 0 || config.pings.roles)} **Mentions**\n` + + `${emojiFromBoolean(config.wordFilter.enabled)} **Words**\n` + + `${emojiFromBoolean(config.malware)} **Malware**\n` + + `${emojiFromBoolean(config.images.NSFW || config.images.size)} **Images**\n` + + `${emojiFromBoolean(config.clean.channels.length > 0)} **Clean**\n` + ) + .setStatus("Success") + .setEmoji("GUILD.SETTINGS.GREEN") + + + await interaction.editReply({embeds: [embed], components: [selectMenu, button]}); + + let i: StringSelectMenuInteraction | ButtonInteraction; + try { + i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id, time: 300000}) as StringSelectMenuInteraction | ButtonInteraction; + } catch (e) { + closed = true; + continue; + } + await i.deferUpdate(); + if(i.isButton()) { + await client.database.guilds.write(interaction.guild.id, {filters: config}); + } else { + switch(i.values[0]) { + case "invites": { + config.invite = await inviteMenu(i, m, config.invite); + break; + } + case "mentions": { + config.pings = await mentionMenu(i, m, config.pings); + break; + } + case "words": { + config.wordFilter = await wordMenu(i, m, config.wordFilter); + break; + } + case "malware": { + config.malware = !config.malware; + break; + } + case "images": { + const next = await imageMenu(i, m, config.images); + config.images = next; + break; + } + case "clean": { + const next = await cleanMenu(i, m, config.clean); + config.clean = next; + break; + } + } + } + + } while(!closed); + await interaction.deleteReply() + +}; + +const check = (interaction: CommandInteraction, _partial: boolean = false) => { + const member = interaction.member as Discord.GuildMember; + if (!member.permissions.has("ManageMessages")) + return "You must have the *Manage Messages* permission to use this command"; + return true; +}; + +export { command }; +export { callback }; +export { check }; diff --git a/src/commands/settings/autopublish.ts b/src/commands/settings/autopublish.ts new file mode 100644 index 0000000..1dc97e0 --- /dev/null +++ b/src/commands/settings/autopublish.ts @@ -0,0 +1,96 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, CommandInteraction, SlashCommandSubcommandBuilder } from "discord.js"; +import type Discord from "discord.js"; +import client from "../../utils/client.js"; +import { LoadingEmbed } from "../../utils/defaults.js"; +import compare from "lodash" +import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; + +export const command = new SlashCommandSubcommandBuilder() + .setName("autopublish") + .setDescription("Automatically publish messages posted in announcement channels"); + +export const callback = async (interaction: CommandInteraction): Promise => { + await interaction.reply({ + embeds: LoadingEmbed, + ephemeral: true, + fetchReply: true + }); + + let closed = false; + let config = await client.database.guilds.read(interaction.guild!.id); + let data = Object.assign({}, config.autoPublish); + do { + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("switch") + .setLabel(data.enabled ? "Disabled" : "Enabled") + .setStyle(data.enabled ? ButtonStyle.Danger : ButtonStyle.Success) + .setEmoji(data.enabled ? "✅" : "❌"), + new ButtonBuilder() + .setCustomId("save") + .setLabel("Save") + .setStyle(ButtonStyle.Success) + .setEmoji("💾") + .setDisabled(compare.isEqual(data, config.autoPublish)) + ); + + const channelSelect = new ActionRowBuilder() + .addComponents( + new ChannelSelectMenuBuilder() + .setCustomId("channel") + .setPlaceholder("Select a channel") + .setMinValues(1) + ); + + const embed = new EmojiEmbed() + + await interaction.editReply({ + embeds: [embed], + components: [channelSelect, buttons] + }); + + let i: Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction; + try { + i = await interaction.channel!.awaitMessageComponent({ + filter: (i: Discord.Interaction) => i.user.id === interaction.user.id, + time: 300000 + }) as Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction; + } catch (e) { + closed = true; + continue; + } + + if(i.isButton()) { + switch(i.customId) { + case "switch": { + data.enabled = !data.enabled; + break; + } + case "save": { + await client.database.guilds.write(interaction.guild!.id, { "autoPublish": data }); + config = await client.database.guilds.read(interaction.guild!.id); + data = Object.assign({}, config.autoPublish); + break; + } + } + } else { + for(const channel of i.values) { + data.channels.includes(channel) ? data.channels.splice(data.channels.indexOf(channel), 1) : data.channels.push(channel); + } + } + + } while (!closed); + + await interaction.deleteReply(); +} + +export const check = (interaction: CommandInteraction, _partial: boolean = false) => { + const member = interaction.member as Discord.GuildMember; + const me = interaction.guild!.members.me!; + if (!member.permissions.has("ManageMessages")) + return "You must have the *Manage Messages* permission to use this command"; + if (_partial) return true; + if (!me.permissions.has("ManageMessages")) return "I do not have the *Manage Messages* permission"; + return true; +}; diff --git a/src/commands/settings/filters.ts b/src/commands/settings/filters.ts deleted file mode 100644 index 2e6f4c5..0000000 --- a/src/commands/settings/filters.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type Discord from "discord.js"; -import type { CommandInteraction } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; - -const command = (builder: SlashCommandSubcommandBuilder) => - builder.setName("filter").setDescription("Setting for message filters"); - -const callback = async (_interaction: CommandInteraction): Promise => { - console.log("Filters"); -}; - -const check = (interaction: CommandInteraction) => { - const member = interaction.member as Discord.GuildMember; - if (!member.permissions.has("ManageMessages")) - return "You must have the *Manage Messages* permission to use this command"; - return true; -}; - -export { command }; -export { callback }; -export { check }; diff --git a/src/commands/settings/logs/attachment.ts b/src/commands/settings/logs/attachment.ts index 2709bee..238b8b9 100644 --- a/src/commands/settings/logs/attachment.ts +++ b/src/commands/settings/logs/attachment.ts @@ -1,198 +1,110 @@ import { LoadingEmbed } from "../../../utils/defaults.js"; -import { ChannelType } from "discord-api-types/v9"; -import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonInteraction } from "discord.js"; +import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, ChannelType } from "discord.js"; import EmojiEmbed from "../../../utils/generateEmojiEmbed.js"; -import confirmationMessage from "../../../utils/confirmationMessage.js"; import getEmojiByName from "../../../utils/getEmojiByName.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import client from "../../../utils/client.js"; +import { getCommandMentionByName } from "../../../utils/getCommandDataByName.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("attachments") .setDescription("Where attachments should be logged to (Premium only)") - .addChannelOption((option) => - option - .setName("channel") - .setDescription("The channel to log attachments in") - .addChannelTypes(ChannelType.GuildText) - .setRequired(false) - ); const callback = async (interaction: CommandInteraction): Promise => { - const m = (await interaction.reply({ + if (interaction.guild) client.database.premium.hasPremium(interaction.guild.id).finally(() => {}); + await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true - })) as Discord.Message; - if (interaction.options.get("channel")?.channel) { - let channel; - try { - channel = interaction.options.get("channel")?.channel; - } catch { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("CHANNEL.TEXT.DELETE") - .setTitle("Attachment Log Channel") - .setDescription("The channel you provided is not a valid channel") - .setStatus("Danger") - ] - }); - } - channel = channel as Discord.TextChannel; - if (channel.guild.id !== interaction.guild!.id) { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Attachment Log Channel") - .setDescription("You must choose a channel in this server") - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ] - }); - } - const confirmation = await new confirmationMessage(interaction) - .setEmoji("CHANNEL.TEXT.EDIT") - .setTitle("Attachment Log Channel") + }) + + if(!await client.database.premium.hasPremium(interaction.guild!.id)) return interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Premium Required") + .setDescription(`This feature is exclusive to ${getCommandMentionByName("nucleus/premium")} servers.`) + .setStatus("Danger") + .setEmoji("NUCLEUS.PREMIUM") + ] + }); + + let data = await client.database.guilds.read(interaction.guild!.id); + let channel = data.logging.staff.channel; + + let closed = false; + do { + const channelMenu = new ActionRowBuilder() + .addComponents( + new ChannelSelectMenuBuilder() + .setCustomId("channel") + .setPlaceholder("Select a channel") + .setChannelTypes(ChannelType.GuildText) + ); + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("clear") + .setLabel("Clear") + .setStyle(ButtonStyle.Danger) + .setEmoji(getEmojiByName("CONTROL.CROSS", "id") as Discord.APIMessageComponentEmoji) + .setDisabled(!channel), + new ButtonBuilder() + .setCustomId("save") + .setLabel("Save") + .setStyle(ButtonStyle.Success) + .setEmoji(getEmojiByName("ICONS.SAVE", "id") as Discord.APIMessageComponentEmoji) + .setDisabled(channel === data.logging.staff.channel) + ); + + const embed = new EmojiEmbed() + .setTitle("Attachments") .setDescription( - "This will be the channel all attachments will be sent to.\n\n" + - `Are you sure you want to set the attachment log channel to <#${channel.id}>?` + `The channel to send all attachments from the server, allowing you to check them if they are deleted\n` + + `**Channel:** ${channel ? `<#${channel}>` : "*None*"}\n` ) - .setColor("Warning") - .setFailedMessage("Attachment log channel not set", "Warning", "CHANNEL.TEXT.DELETE") - .setInverted(true) - .send(true); - if (confirmation.cancelled) return; - if (confirmation.success) { - try { - await client.database.guilds.write(interaction.guild!.id, { - "logging.attachments.channel": channel.id - }); - const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger; - const data = { - meta: { - type: "attachmentChannelUpdate", - displayName: "Attachment Log Channel Updated", - calculateType: "nucleusSettingsUpdated", - color: NucleusColors.yellow, - emoji: "CHANNEL.TEXT.EDIT", - timestamp: new Date().getTime() - }, - list: { - memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), - changedBy: entry(interaction.user.id, renderUser(interaction.user)), - channel: entry(channel.id, renderChannel(channel)) - }, - hidden: { - guild: interaction.guild!.id - } - }; - log(data); - } catch (e) { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Attachment Log Channel") - .setDescription("Something went wrong and the attachment log channel could not be set") - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ], - components: [] - }); - } - } else { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Attachment Log Channel") - .setDescription("No changes were made") - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], - components: [] - }); - } - } - let clicks = 0; - const data = await client.database.guilds.read(interaction.guild!.id); - let channel = data.logging.staff.channel; + .setStatus("Success") + .setEmoji("CHANNEL.TEXT.CREATE") - let timedOut = false; - while (!timedOut) { await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Attachment Log Channel") - .setDescription( - channel - ? `Your attachment log channel is currently set to <#${channel}>` - : "This server does not have an attachment log channel" + - (await client.database.premium.hasPremium(interaction.guild!.id) - ? "" - : "\n\nThis server does not have premium, so this feature is disabled") - ) - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId("clear") - .setLabel(clicks ? "Click again to confirm" : "Reset channel") - .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id")) - .setStyle(ButtonStyle.Danger) - .setDisabled(!channel) - ]) - ] + embeds: [embed], + components: [channelMenu, buttons] }); - let i; + + let i: Discord.ButtonInteraction | Discord.SelectMenuInteraction; try { - i = await m.awaitMessageComponent({ - time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } - }); + i = (await interaction.channel!.awaitMessageComponent({ + filter: (i: Discord.Interaction) => i.user.id === interaction.user.id, + time: 300000 + })) as Discord.ButtonInteraction | Discord.SelectMenuInteraction; } catch (e) { - timedOut = true; + closed = true; continue; } - i.deferUpdate(); - if ((i.component as unknown as ButtonInteraction).customId === "clear") { - clicks += 1; - if (clicks === 2) { - clicks = 0; - await client.database.guilds.write(interaction.guild!.id, null, ["logging.announcements.channel"]); - channel = null; + await i.deferUpdate(); + if(i.isButton()) { + switch (i.customId) { + case "clear": { + channel = null; + break; + } + case "save": { + await client.database.guilds.write(interaction.guild!.id, { + "logging.attachments.channel": channel + }); + data = await client.database.guilds.read(interaction.guild!.id); + break; + } } + } else { + channel = i.values[0]!; } - } - await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Attachment Log Channel") - .setDescription( - channel - ? `Your attachment log channel is currently set to <#${channel}>` - : "This server does not have an attachment log channel" - ) - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - .setFooter({ text: "Message closed" }) - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId("clear") - .setLabel("Clear") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setStyle(ButtonStyle.Secondary) - .setDisabled(true) - ]) - ] - }); + + } while (!closed); + await interaction.deleteReply() }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as Discord.GuildMember; if (!member.permissions.has("ManageGuild")) return "You must have the *Manage Server* permission to use this command"; diff --git a/src/commands/settings/logs/channel.ts b/src/commands/settings/logs/channel.ts deleted file mode 100644 index 992491a..0000000 --- a/src/commands/settings/logs/channel.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { LoadingEmbed } from "../../../utils/defaults.js"; -import { ChannelType } from "discord-api-types/v9"; -import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonInteraction, ButtonComponent } from "discord.js"; -import EmojiEmbed from "../../../utils/generateEmojiEmbed.js"; -import confirmationMessage from "../../../utils/confirmationMessage.js"; -import getEmojiByName from "../../../utils/getEmojiByName.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; -import client from "../../../utils/client.js"; - -const command = (builder: SlashCommandSubcommandBuilder) => - builder - .setName("channel") - .setDescription("Sets or shows the log channel") - .addChannelOption((option) => - option - .setName("channel") - .setDescription("The channel to set the log channel to") - .addChannelTypes(ChannelType.GuildText) - ); - -const callback = async (interaction: CommandInteraction): Promise => { - const m = (await interaction.reply({ - embeds: LoadingEmbed, - ephemeral: true, - fetchReply: true - })) as Discord.Message; - if (interaction.options.get("channel")?.channel) { - let channel; - try { - channel = interaction.options.get("channel")?.channel; - } catch { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("CHANNEL.TEXT.DELETE") - .setTitle("Log Channel") - .setDescription("The channel you provided is not a valid channel") - .setStatus("Danger") - ] - }); - } - channel = channel as Discord.TextChannel; - if (channel.guild.id !== interaction.guild!.id) { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Log Channel") - .setDescription("You must choose a channel in this server") - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ] - }); - } - const confirmation = await new confirmationMessage(interaction) - .setEmoji("CHANNEL.TEXT.EDIT") - .setTitle("Log Channel") - .setDescription(`Are you sure you want to set the log channel to <#${channel.id}>?`) - .setColor("Warning") - .setFailedMessage("The log channel was not changed", "Danger", "CHANNEL.TEXT.DELETE") - .setInverted(true) - .send(true); - if (confirmation.cancelled) return; - if (confirmation.success) { - try { - await client.database.guilds.write(interaction.guild!.id, { - "logging.logs.channel": channel.id - }); - const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger; - const data = { - meta: { - type: "logChannelUpdate", - displayName: "Log Channel Changed", - calculateType: "nucleusSettingsUpdated", - color: NucleusColors.yellow, - emoji: "CHANNEL.TEXT.EDIT", - timestamp: new Date().getTime() - }, - list: { - memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), - changedBy: entry(interaction.user.id, renderUser(interaction.user)), - channel: entry(channel.id, renderChannel(channel)) - }, - hidden: { - guild: channel.guild.id - } - }; - log(data); - } catch (e) { - console.log(e); - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Log Channel") - .setDescription("Something went wrong and the log channel could not be set") - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ], - components: [] - }); - } - } else { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Log Channel") - .setDescription("No changes were made") - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], - components: [] - }); - } - } - let clicks = 0; - const data = await client.database.guilds.read(interaction.guild!.id); - let channel = data.logging.logs.channel; - let timedOut = false; - while (!timedOut) { - await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Log channel") - .setDescription( - channel - ? `Your log channel is currently set to <#${channel}>` - : "This server does not have a log channel" - ) - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId("clear") - .setLabel(clicks ? "Click again to confirm" : "Reset channel") - .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id")) - .setStyle(ButtonStyle.Danger) - .setDisabled(!channel) - ]) - ] - }); - let i: ButtonInteraction; - try { - i = await m.awaitMessageComponent({ - time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } - }) as ButtonInteraction; - } catch (e) { - timedOut = true; - } - i = i! - i.deferUpdate(); - if ((i.component as ButtonComponent).customId === "clear") { - clicks += 1; - if (clicks === 2) { - clicks = 0; - await client.database.guilds.write(interaction.guild!.id, null, ["logging.logs.channel"]); - channel = null; - } - } - } - await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Log channel") - .setDescription( - channel - ? `Your log channel is currently set to <#${channel}>` - : "This server does not have a log channel" - ) - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - .setFooter({ text: "Message closed" }) - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId("clear") - .setLabel("Clear") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setStyle(ButtonStyle.Secondary) - .setDisabled(true) - ]) - ] - }); -}; - -const check = (interaction: CommandInteraction) => { - const member = interaction.member as Discord.GuildMember; - if (!member.permissions.has("ManageGuild")) - return "You must have the *Manage Server* permission to use this command"; - return true; -}; - -export { command }; -export { callback }; -export { check }; diff --git a/src/commands/settings/logs/events.ts b/src/commands/settings/logs/events.ts index fbe79fa..eeef8fb 100644 --- a/src/commands/settings/logs/events.ts +++ b/src/commands/settings/logs/events.ts @@ -1,9 +1,11 @@ import { LoadingEmbed } from "../../../utils/defaults.js"; -import Discord, { CommandInteraction, Message, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, EmbedBuilder, StringSelectMenuInteraction } from "discord.js"; -import { SlashCommandSubcommandBuilder, StringSelectMenuOptionBuilder } from "@discordjs/builders"; -import EmojiEmbed from "../../../utils/generateEmojiEmbed.js"; +import Discord, { CommandInteraction, ActionRowBuilder, ChannelSelectMenuBuilder, ChannelType, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ButtonInteraction, StringSelectMenuInteraction, ChannelSelectMenuInteraction, APIMessageComponentEmoji } from "discord.js"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import client from "../../../utils/client.js"; +import compare from "lodash"; import { toHexArray, toHexInteger } from "../../../utils/calculate.js"; +import EmojiEmbed from "../../../utils/generateEmojiEmbed.js"; +import getEmojiByName from "../../../utils/getEmojiByName.js"; const logs: Record = { channelUpdate: "Channels created, deleted or modified", @@ -24,88 +26,138 @@ const logs: Record = { webhookUpdate: "Webhooks created or deleted", guildMemberVerify: "Member runs verify", autoModeratorDeleted: "Messages auto deleted by Nucleus", - nucleusSettingsUpdated: "Nucleus' settings updated by a moderator", - ticketUpdate: "Tickets created or deleted" + ticketUpdate: "Tickets created or deleted", + //nucleusSettingsUpdated: "Nucleus' settings updated by a moderator" // TODO }; const command = (builder: SlashCommandSubcommandBuilder) => - builder.setName("events").setDescription("Sets what events should be logged"); + builder + .setName("events") + .setDescription("The general log channel for the server, and setting what events to show") const callback = async (interaction: CommandInteraction): Promise => { - await interaction.reply({ + const m = (await interaction.reply({ embeds: LoadingEmbed, - fetchReply: true, - ephemeral: true - }); - let m: Message; - let timedOut = false; + ephemeral: true, + fetchReply: true + })) as Discord.Message; + + let config = await client.database.guilds.read(interaction.guild!.id); + let data = Object.assign({}, config.logging.logs); + let closed = false; + let show = false; do { - const config = await client.database.guilds.read(interaction.guild!.id); - const converted = toHexArray(config.logging.logs.toLog); - const selectPane = new StringSelectMenuBuilder() - .setPlaceholder("Set events to log") - .setMaxValues(Object.keys(logs).length) - .setCustomId("logs") - .setMinValues(0) - Object.keys(logs).map((e, i) => { - selectPane.addOptions(new StringSelectMenuOptionBuilder() - .setLabel(logs[e]!) - .setValue(i.toString()) - .setDefault(converted.includes(e)) + const channelMenu = new ActionRowBuilder() + .addComponents( + new ChannelSelectMenuBuilder() + .setCustomId("channel") + .setPlaceholder("Select a channel") + .setChannelTypes(ChannelType.GuildText) + ) + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("switch") + .setLabel(data.enabled ? "Enabled" : "Disabled") + .setStyle(data.enabled ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(getEmojiByName((data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS"), "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("remove") + .setLabel("Remove") + .setStyle(ButtonStyle.Danger) + .setDisabled(!data.channel), + new ButtonBuilder() + .setCustomId("show") + .setLabel("Manage Events") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("save") + .setLabel("Save") + .setStyle(ButtonStyle.Success) + .setDisabled(compare.isEqual(data, config.logging.logs)) + ) + + const converted = toHexArray(data.toLog); + const toLogMenu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setPlaceholder("Set events to log") + .setMaxValues(Object.keys(logs).length) + .setCustomId("logs") + .setMinValues(0) + ) + Object.keys(logs).map((e) => { + toLogMenu.components[0]!.addOptions( + new StringSelectMenuOptionBuilder() + .setLabel(logs[e]!) + .setValue(e) + .setDefault(converted.includes(e)) ) }); - m = (await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Logging Events") - .setDescription( - "Below are the events being logged in the server. You can toggle them on and off in the dropdown" - ) - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], - components: [ - new ActionRowBuilder().addComponents(selectPane), - new ActionRowBuilder().addComponents([ - new ButtonBuilder().setLabel("Select all").setStyle(ButtonStyle.Primary).setCustomId("all"), - new ButtonBuilder().setLabel("Select none").setStyle(ButtonStyle.Danger).setCustomId("none") - ]) - ] - })) as Message; - let i; + + const embed = new EmojiEmbed() + .setTitle("General Log Channel") + .setStatus("Success") + .setEmoji("CHANNEL.TEXT.CREATE") + .setDescription( + `This is the channel that all events you set to be logged will be stored\n` + + `**Channel:** ${data.channel ? `<#${data.channel}>` : "None"}\n` + ) + + const components: ActionRowBuilder[] = [channelMenu, buttons]; + if(show) components.push(toLogMenu); + + await interaction.editReply({ + embeds: [embed], + components: components + }); + + let i: ButtonInteraction | StringSelectMenuInteraction | ChannelSelectMenuInteraction; try { i = await m.awaitMessageComponent({ - time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } - }); + filter: (i) => i.user.id === interaction.user.id, + time: 300000 + }) as ButtonInteraction | StringSelectMenuInteraction | ChannelSelectMenuInteraction; } catch (e) { - timedOut = true; + closed = true; continue; } - i.deferUpdate(); - if (i.customId === "logs") { - const selected = (i as StringSelectMenuInteraction).values; - const newLogs = toHexInteger(selected.map((e: string) => Object.keys(logs)[parseInt(e)]!)); - await client.database.guilds.write(interaction.guild!.id, { - "logging.logs.toLog": newLogs - }); - } else if (i.customId === "all") { - const newLogs = toHexInteger(Object.keys(logs).map((e) => e)); - await client.database.guilds.write(interaction.guild!.id, { - "logging.logs.toLog": newLogs - }); - } else if (i.customId === "none") { - await client.database.guilds.write(interaction.guild!.id, { - "logging.logs.toLog": 0 - }); + + await i.deferUpdate(); + + if(i.isButton()) { + switch(i.customId) { + case "show": { + show = !show; + break; + } + case "switch": { + data.enabled = !data.enabled; + break; + } + case "save": { + await client.database.guilds.write(interaction.guild!.id, {"logging.logs": data}); + config = await client.database.guilds.read(interaction.guild!.id); + data = Object.assign({}, config.logging.logs); + break; + } + case "remove": { + data.channel = null; + break; + } + } + } else if(i.isStringSelectMenu()) { + const hex = toHexInteger(i.values); + data.toLog = hex; + } else if(i.isChannelSelectMenu()) { + data.channel = i.values[0]!; } - } while (!timedOut); - await interaction.editReply({ embeds: [new EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message timed out" })] }); - return; + } while (!closed); + await interaction.deleteReply() }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as Discord.GuildMember; if (!member.permissions.has("ManageGuild")) return "You must have the *Manage Server* permission to use this command"; diff --git a/src/commands/settings/logs/staff.ts b/src/commands/settings/logs/staff.ts deleted file mode 100644 index 13125ef..0000000 --- a/src/commands/settings/logs/staff.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { LoadingEmbed } from "../../../utils/defaults.js"; -import { ChannelType } from "discord-api-types/v9"; -import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonComponent } from "discord.js"; -import EmojiEmbed from "../../../utils/generateEmojiEmbed.js"; -import confirmationMessage from "../../../utils/confirmationMessage.js"; -import getEmojiByName from "../../../utils/getEmojiByName.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; -import client from "../../../utils/client.js"; - -const command = (builder: SlashCommandSubcommandBuilder) => - builder - .setName("staff") - .setDescription("Settings for the staff notifications channel") - .addChannelOption((option) => - option - .setName("channel") - .setDescription("The channel to set the staff notifications channel to") - .addChannelTypes(ChannelType.GuildText) - .setRequired(false) - ); - -const callback = async (interaction: CommandInteraction): Promise => { - if (!interaction.guild) return; - const m = (await interaction.reply({ - embeds: LoadingEmbed, - ephemeral: true, - fetchReply: true - })) as Discord.Message; - if (interaction.options.get("channel")?.channel) { - let channel; - try { - channel = interaction.options.get("channel")?.channel; - } catch { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("CHANNEL.TEXT.DELETE") - .setTitle("Staff Notifications Channel") - .setDescription("The channel you provided is not a valid channel") - .setStatus("Danger") - ] - }); - } - channel = channel as Discord.TextChannel; - if (channel.guild.id !== interaction.guild.id) { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Staff Notifications Channel") - .setDescription("You must choose a channel in this server") - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ] - }); - } - const confirmation = await new confirmationMessage(interaction) - .setEmoji("CHANNEL.TEXT.EDIT") - .setTitle("Staff Notifications Channel") - .setDescription( - "This will be the channel all notifications, updates, user reports etc. will be sent to.\n\n" + - `Are you sure you want to set the staff notifications channel to <#${channel.id}>?` - ) - .setColor("Warning") - .setFailedMessage("Staff notifications channel not set", "Warning", "CHANNEL.TEXT.DELETE") - .setInverted(true) - .send(true); - if (confirmation.cancelled) return; - if (confirmation.success) { - try { - await client.database.guilds.write(interaction.guild.id, { - "logging.staff.channel": channel.id - }); - const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger; - const data = { - meta: { - type: "staffChannelUpdate", - displayName: "Staff Notifications Channel Updated", - calculateType: "nucleusSettingsUpdated", - color: NucleusColors.yellow, - emoji: "CHANNEL.TEXT.EDIT", - timestamp: new Date().getTime() - }, - list: { - memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), - changedBy: entry(interaction.user.id, renderUser(interaction.user)), - channel: entry(channel.id, renderChannel(channel)) - }, - hidden: { - guild: interaction.guild.id - } - }; - log(data); - } catch (e) { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Staff Notifications Channel") - .setDescription("Something went wrong and the staff notifications channel could not be set") - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ], - components: [] - }); - } - } else { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Staff Notifications Channel") - .setDescription("No changes were made") - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], - components: [] - }); - } - } - let clicks = 0; - const data = await client.database.guilds.read(interaction.guild.id); - let channel = data.logging.staff.channel; - let timedOut = false; - while (!timedOut) { - await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Staff Notifications channel") - .setDescription( - channel - ? `Your staff notifications channel is currently set to <#${channel}>` - : "This server does not have a staff notifications channel" - ) - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId("clear") - .setLabel(clicks ? "Click again to confirm" : "Reset channel") - .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id")) - .setStyle(ButtonStyle.Danger) - .setDisabled(!channel) - ]) - ] - }); - let i; - try { - i = await m.awaitMessageComponent({ - time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } - }); - } catch (e) { - timedOut = true; - continue; - } - i.deferUpdate(); - if ((i.component as ButtonComponent).customId === "clear") { - clicks += 1; - if (clicks === 2) { - clicks = 0; - await client.database.guilds.write(interaction.guild.id, null, ["logging.staff.channel"]); - channel = null; - } - } - } - await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Staff Notifications channel") - .setDescription( - channel - ? `Your staff notifications channel is currently set to <#${channel}>` - : "This server does not have a staff notifications channel" - ) - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - .setFooter({ text: "Message closed" }) - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId("clear") - .setLabel("Clear") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setStyle(ButtonStyle.Secondary) - .setDisabled(true) - ]) - ] - }); -}; - -const check = (interaction: CommandInteraction) => { - const member = interaction.member as Discord.GuildMember; - if (!member.permissions.has("ManageGuild")) - return "You must have the *Manage Server* permission to use this command"; - return true; -}; - -export { command }; -export { callback }; -export { check }; diff --git a/src/commands/settings/logs/warnings.ts b/src/commands/settings/logs/warnings.ts new file mode 100644 index 0000000..84772e6 --- /dev/null +++ b/src/commands/settings/logs/warnings.ts @@ -0,0 +1,104 @@ +import { LoadingEmbed } from "../../../utils/defaults.js"; +import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, ChannelType } from "discord.js"; +import EmojiEmbed from "../../../utils/generateEmojiEmbed.js"; +import getEmojiByName from "../../../utils/getEmojiByName.js"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; +import client from "../../../utils/client.js"; + +const command = (builder: SlashCommandSubcommandBuilder) => + builder + .setName("warnings") + .setDescription("Settings for the staff notifications channel") + +const callback = async (interaction: CommandInteraction): Promise => { + if (!interaction.guild) return; + await interaction.reply({ + embeds: LoadingEmbed, + ephemeral: true, + fetchReply: true + }) + + let data = await client.database.guilds.read(interaction.guild.id); + let channel = data.logging.staff.channel; + let closed = false; + do { + const channelMenu = new ActionRowBuilder() + .addComponents( + new ChannelSelectMenuBuilder() + .setCustomId("channel") + .setPlaceholder("Select a channel") + .setChannelTypes(ChannelType.GuildText) + ); + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("clear") + .setLabel("Clear") + .setStyle(ButtonStyle.Danger) + .setEmoji(getEmojiByName("CONTROL.CROSS", "id") as Discord.APIMessageComponentEmoji) + .setDisabled(!channel), + new ButtonBuilder() + .setCustomId("save") + .setLabel("Save") + .setStyle(ButtonStyle.Success) + .setEmoji(getEmojiByName("ICONS.SAVE", "id") as Discord.APIMessageComponentEmoji) + .setDisabled(channel === data.logging.staff.channel) + ); + + const embed = new EmojiEmbed() + .setTitle("Staff Notifications Channel") + .setStatus("Success") + .setEmoji("CHANNEL.TEXT.CREATE") + .setDescription( + `Logs which require an action from a moderator or administrator will be sent to this channel.\n` + + `**Channel:** ${channel ? `<#${channel}>` : "*None*"}\n` + ) + + await interaction.editReply({ + embeds: [embed], + components: [channelMenu, buttons] + }); + + let i: Discord.ButtonInteraction | Discord.SelectMenuInteraction; + try { + i = (await interaction.channel!.awaitMessageComponent({ + filter: (i: Discord.Interaction) => i.user.id === interaction.user.id, + time: 300000 + })) as Discord.ButtonInteraction | Discord.SelectMenuInteraction; + } catch (e) { + closed = true; + continue; + } + await i.deferUpdate(); + if(i.isButton()) { + switch (i.customId) { + case "clear": { + channel = null; + break; + } + case "save": { + await client.database.guilds.write(interaction.guild!.id, { + "logging.warnings.channel": channel + }); + data = await client.database.guilds.read(interaction.guild!.id); + break; + } + } + } else { + channel = i.values[0]!; + } + } while (!closed); + + await interaction.deleteReply() +}; + +const check = (interaction: CommandInteraction, _partial: boolean = false) => { + const member = interaction.member as Discord.GuildMember; + if (!member.permissions.has("ManageGuild")) + return "You must have the *Manage Server* permission to use this command"; + return true; +}; + +export { command }; +export { callback }; +export { check }; diff --git a/src/commands/settings/commands.ts b/src/commands/settings/moderation.ts similarity index 74% rename from src/commands/settings/commands.ts rename to src/commands/settings/moderation.ts index 25034b2..336e53a 100644 --- a/src/commands/settings/commands.ts +++ b/src/commands/settings/moderation.ts @@ -1,50 +1,28 @@ import { LoadingEmbed } from "../../utils/defaults.js"; -import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, Role, ButtonStyle, ButtonComponent, TextInputBuilder } from "discord.js"; +import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonComponent, TextInputBuilder, RoleSelectMenuBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import client from "../../utils/client.js"; import { modalInteractionCollector } from "../../utils/dualCollector.js"; -import confirmationMessage from "../../utils/confirmationMessage.js"; -import keyValueList from "../../utils/generateKeyValueList.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder - .setName("commands") + .setName("moderation") .setDescription("Links and text shown to a user after a moderator action is performed") - .addRoleOption((o) => o.setName("role").setDescription("The role given when a member is muted")); -const callback = async (interaction: CommandInteraction): Promise => { - await interaction.reply({ +const callback = async (interaction: CommandInteraction): Promise => { + const m = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true }); - let m; - let clicked = ""; - if (interaction.options.get("role")) { - const confirmation = await new confirmationMessage(interaction) - .setEmoji("GUILD.ROLES.DELETE") - .setTitle("Moderation Commands") - .setDescription( - keyValueList({ - role: `<@&${(interaction.options.get("role") as unknown as Role).id}>` - }) - ) - .setColor("Danger") - .send(true); - if (confirmation.cancelled) return - if (confirmation.success) { - await client.database.guilds.write(interaction.guild!.id, { - ["moderation.mute.role"]: (interaction.options.get("role") as unknown as Role).id - }); - } - } let timedOut = false; while (!timedOut) { const config = await client.database.guilds.read(interaction.guild!.id); const moderation = config.moderation; - m = await interaction.editReply({ + console.log(moderation) + await interaction.editReply({ embeds: [ new EmojiEmbed() .setTitle("Moderation Commands") @@ -52,8 +30,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setStatus("Success") .setDescription( "These links are shown below the message sent in a user's DM when they are punished.\n\n" + - "**Mute Role:** " + - (moderation.mute.role ? `<@&${moderation.mute.role}>` : "*None set*") + "**Mute Role:** " + (moderation.mute.role ? `<@&${moderation.mute.role}>` : "*None set*") ) ], components: [ @@ -92,25 +69,24 @@ const callback = async (interaction: CommandInteraction): Promise => { .setStyle(ButtonStyle.Secondary) ]), new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel(clicked === "clearMuteRole" ? "Click again to confirm" : "Clear mute role") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setCustomId("clearMuteRole") - .setStyle(ButtonStyle.Danger) - .setDisabled(!moderation.mute.role), new ButtonBuilder() .setCustomId("timeout") .setLabel("Mute timeout " + (moderation.mute.timeout ? "Enabled" : "Disabled")) .setStyle(moderation.mute.timeout ? ButtonStyle.Success : ButtonStyle.Danger) .setEmoji(getEmojiByName("CONTROL." + (moderation.mute.timeout ? "TICK" : "CROSS"), "id")) - ]) + ]), + new ActionRowBuilder().addComponents( + new RoleSelectMenuBuilder() + .setCustomId("muteRole") + .setPlaceholder("Select a new mute role") + ) ] }); let i; try { i = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id } }); } catch (e) { timedOut = true; @@ -118,20 +94,13 @@ const callback = async (interaction: CommandInteraction): Promise => { } type modIDs = "mute" | "kick" | "ban" | "softban" | "warn" | "role"; let chosen = moderation[i.customId as modIDs]; - if ((i.component as ButtonComponent).customId === "clearMuteRole") { - i.deferUpdate(); - if (clicked === "clearMuteRole") { - await client.database.guilds.write(interaction.guild!.id, { - "moderation.mute.role": null - }); - } else { - clicked = "clearMuteRole"; - } + if (i.isRoleSelectMenu()) { + await i.deferUpdate(); + await client.database.guilds.write(interaction.guild!.id, { + "moderation.mute.role": i.values[0]! + }); continue; - } else { - clicked = ""; - } - if ((i.component as ButtonComponent).customId === "timeout") { + } else if ((i.component as ButtonComponent).customId === "timeout") { await i.deferUpdate(); await client.database.guilds.write(interaction.guild!.id, { "moderation.mute.timeout": !moderation.mute.timeout @@ -183,15 +152,11 @@ const callback = async (interaction: CommandInteraction): Promise => { }); let out: Discord.ModalSubmitInteraction | null; try { - out = await modalInteractionCollector( - m, - (m) => m.channel!.id === interaction.channel!.id, - (_) => true - ) as Discord.ModalSubmitInteraction | null; + out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null; } catch (e) { continue; } - if (!out) continue + if (!out || out.isButton()) continue const buttonText = out.fields.getTextInputValue("name"); const buttonLink = out.fields.getTextInputValue("url").replace(/{id}/gi, "{id}"); const current = chosen; @@ -206,9 +171,10 @@ const callback = async (interaction: CommandInteraction): Promise => { } } } + await interaction.deleteReply() }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as Discord.GuildMember; if (!member.permissions.has("ManageGuild")) return "You must have the *Manage Server* permission to use this command"; diff --git a/src/commands/settings/rolemenu.ts b/src/commands/settings/rolemenu.ts index b62d962..cccb6f6 100644 --- a/src/commands/settings/rolemenu.ts +++ b/src/commands/settings/rolemenu.ts @@ -1,19 +1,478 @@ import type Discord from "discord.js"; -import type { CommandInteraction } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, CommandInteraction, Message, ModalBuilder, RoleSelectMenuBuilder, RoleSelectMenuInteraction, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; +import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; +import { LoadingEmbed } from "../../utils/defaults.js"; +import client from "../../utils/client.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; +import createPageIndicator from "../../utils/createPageIndicator.js"; +import { configToDropdown } from "../../actions/roleMenu.js"; +import { modalInteractionCollector } from "../../utils/dualCollector.js"; +import ellipsis from "../../utils/ellipsis.js"; +import lodash from 'lodash'; + +const isEqual = lodash.isEqual; const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("rolemenu") - .setDescription("rolemenu") // TODO - .addRoleOption((option) => option.setName("role").setDescription("The role to give after verifying")); // FIXME FOR FUCK SAKE + .setDescription("rolemenu") + +interface ObjectSchema { + name: string; + description: string; + min: number; + max: number; + options: { + name: string; + description: string | null; + role: string; + }[]; +} + +const defaultRolePageConfig = { + name: "Role Menu Page", + description: "A new role menu page", + min: 0, + max: 0, + options: [ + {name: "Role 1", description: null, role: "No role set"} + ] +} + +const reorderRoleMenuPages = async (interaction: CommandInteraction, m: Message, currentObj: ObjectSchema[]) => { + const reorderRow = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("reorder") + .setPlaceholder("Select all pages in the order you want them to appear.") + .setMinValues(currentObj.length) + .setMaxValues(currentObj.length) + .addOptions( + currentObj.map((o, i) => new StringSelectMenuOptionBuilder() + .setLabel(o.name) + .setValue(i.toString()) + ) + ) + ); + const buttonRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) + ) + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Role Menu") + .setDescription("Select pages in the order you want them to appear.") + .setStatus("Success") + ], + components: [reorderRow, buttonRow] + }); + let out: StringSelectMenuInteraction | ButtonInteraction | null; + try { + out = await m.awaitMessageComponent({ + filter: (i) => i.channel!.id === interaction.channel!.id, + time: 300000 + }) as StringSelectMenuInteraction | ButtonInteraction | null; + } catch (e) { + console.error(e); + out = null; + } + if(!out) return; + out.deferUpdate(); + if (out.isButton()) return; + const values = out.values; + + const newOrder: ObjectSchema[] = currentObj.map((_, i) => { + const index = values.findIndex(v => v === i.toString()); + return currentObj[index]; + }) as ObjectSchema[]; + + return newOrder; +} + +const editNameDescription = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data: {name?: string, description?: string}) => { + + let {name, description} = data; + const modal = new ModalBuilder() + .setTitle("Edit Name and Description") + .setCustomId("editNameDescription") + .addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Name") + .setCustomId("name") + .setPlaceholder("The name of the role (e.g. Programmer)") + .setStyle(TextInputStyle.Short) + .setValue(name ?? "") + .setRequired(true) + ), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Description") + .setCustomId("description") + .setPlaceholder("A short description of the role (e.g. A role for people who code)") + .setStyle(TextInputStyle.Short) + .setValue(description ?? "") + ) + ) + const button = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) + ) + + await i.showModal(modal) + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Role Menu") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + ], + components: [button] + }); + + let out: Discord.ModalSubmitInteraction | null; + try { + out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null; + } catch (e) { + console.error(e); + out = null; + } + if(!out) return [name, description]; + if (out.isButton()) return [name, description]; + name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name; + description = out.fields.fields.find((f) => f.customId === "description")?.value ?? description; + return [name, description] + +} + +const editRoleMenuPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: ObjectSchema): Promise => { + if (!data) data = { + name: "Role Menu Page", + description: "A new role menu page", + min: 0, + max: 0, + options: [] + }; + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("edit") + .setLabel("Edit") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("addRole") + .setLabel("Add Role") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji) + ); + + let back = false + if(data.options.length === 0) { + data.options = [ + {name: "Role 1", description: null, role: "No role set"} + ] + } + do { + const previewSelect = configToDropdown("Edit Roles", {name: data.name, description: data.description, min: 1, max: 1, options: data.options}); + const embed = new EmojiEmbed() + .setTitle(`${data.name}`) + .setStatus("Success") + .setDescription( + `**Description:**\n> ${data.description}\n\n` + + `**Min:** ${data.min}` + (data.min === 0 ? " (Members will be given a skip button)" : "") + "\n" + + `**Max:** ${data.max}\n` + ) + + interaction.editReply({embeds: [embed], components: [previewSelect, buttons]}); + let i: StringSelectMenuInteraction | ButtonInteraction; + try { + i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | StringSelectMenuInteraction; + } catch (e) { + back = true; + break; + } + + if (i.isStringSelectMenu()) { + if(i.customId === "roles") { + await i.deferUpdate(); + await createRoleMenuOptionPage(interaction, m, data.options.find((o) => o.role === (i as StringSelectMenuInteraction).values[0])); + } + } else if (i.isButton()) { + switch (i.customId) { + case "back": { + await i.deferUpdate(); + back = true; + break; + } + case "edit": { + const [name, description] = await editNameDescription(i, interaction, m, data); + data.name = name ? name : data.name; + data.description = description ? description : data.description; + break; + } + case "addRole": { + await i.deferUpdate(); + data.options.push(await createRoleMenuOptionPage(interaction, m)); + break; + } + } + } + + } while (!back); + if(isEqual(data, defaultRolePageConfig)) return null; + return data; +} + +const createRoleMenuOptionPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: {name: string; description: string | null; role: string}) => { + const { renderRole} = client.logger; + if (!data) data = { + name: "New role Menu Option", + description: null, + role: "" + }; + let back = false; + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("edit") + .setLabel("Edit Details") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji) + ); + do { + const roleSelect = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder(data.role ? "Set role to" : "Set the role"); + const embed = new EmojiEmbed() + .setTitle(`${data.name}`) + .setStatus("Success") + .setDescription( + `**Description:**\n> ${data.description ?? "No description set"}\n\n` + + `**Role:** ${data.role ? renderRole((await interaction.guild!.roles.fetch(data.role))!) : "No role set"}\n` + ) + + interaction.editReply({embeds: [embed], components: [new ActionRowBuilder().addComponents(roleSelect), buttons]}); + + let i: RoleSelectMenuInteraction | ButtonInteraction; + try { + i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | RoleSelectMenuInteraction; + } catch (e) { + back = true; + break; + } + + if (i.isRoleSelectMenu()) { + if(i.customId === "role") { + await i.deferUpdate(); + data.role = (i as RoleSelectMenuInteraction).values[0]!; + } + } else if (i.isButton()) { + switch (i.customId) { + case "back": { + await i.deferUpdate(); + back = true; + break; + } + case "edit": { + await i.deferUpdate(); + const [name, description] = await editNameDescription(i, interaction, m, data as {name: string; description: string}); + data.name = name ? name : data.name; + data.description = description ? description : data.description; + break; + } + } + } + } while (!back); + return data; +} const callback = async (interaction: CommandInteraction): Promise => { - console.log("we changed the charger again because fuck you"); - await interaction.reply("You're mum"); + if (!interaction.guild) return; + const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true}); + + let page = 0; + let closed = false; + const config = await client.database.guilds.read(interaction.guild.id); + let currentObject: ObjectSchema[] = config.roleMenu.options; + let modified = false; + do { + const embed = new EmojiEmbed() + .setTitle("Role Menu") + .setEmoji("GUILD.GREEN") + .setStatus("Success"); + const noRoleMenus = currentObject.length === 0; + let current: ObjectSchema; + + const pageSelect = new StringSelectMenuBuilder() + .setCustomId("page") + .setPlaceholder("Select a Role Menu page to manage"); + const actionSelect = new StringSelectMenuBuilder() + .setCustomId("action") + .setPlaceholder("Perform an action") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Edit") + .setDescription("Edit this page") + .setValue("edit") + .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), + new StringSelectMenuOptionBuilder() + .setLabel("Delete") + .setDescription("Delete this page") + .setValue("delete") + .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji) + ); + const buttonRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) + .setDisabled(page === 0), + new ButtonBuilder() + .setCustomId("next") + .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Primary) + .setDisabled(page === Object.keys(currentObject).length - 1), + new ButtonBuilder() + .setCustomId("add") + .setLabel("New Page") + .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Secondary) + .setDisabled(Object.keys(currentObject).length >= 24), + new ButtonBuilder() + .setCustomId("reorder") + .setLabel("Reorder Pages") + .setEmoji(getEmojiByName("ICONS.REORDER", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Secondary) + .setDisabled(Object.keys(currentObject).length <= 1), + new ButtonBuilder() + .setCustomId("save") + .setLabel("Save") + .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Success) + .setDisabled(!modified), + ); + if(noRoleMenus) { + embed.setDescription("No role menu pages have been set up yet. Use the button below to add one.\n\n" + + createPageIndicator(1, 1, undefined, true) + ); + pageSelect.setDisabled(true); + actionSelect.setDisabled(true); + pageSelect.addOptions(new StringSelectMenuOptionBuilder() + .setLabel("No role menu pages") + .setValue("none") + ); + } else { + page = Math.min(page, Object.keys(currentObject).length - 1); + current = currentObject[page]!; + embed.setDescription(`**Currently Editing:** ${current.name}\n\n` + + `**Description:**\n> ${current.description}\n` + + `\n\n${createPageIndicator(Object.keys(config.roleMenu.options).length, page)}` + ); + + pageSelect.addOptions( + currentObject.map((key: ObjectSchema, index) => { + return new StringSelectMenuOptionBuilder() + .setLabel(ellipsis(key.name, 50)) + .setDescription(ellipsis(key.description, 50)) + .setValue(index.toString()); + }) + ); + + } + + await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder().addComponents(actionSelect), new ActionRowBuilder().addComponents(pageSelect), buttonRow]}); + let i: StringSelectMenuInteraction | ButtonInteraction; + try { + i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | StringSelectMenuInteraction; + } catch (e) { + closed = true; + continue; + } + + await i.deferUpdate(); + if (i.isButton()) { + switch (i.customId) { + case "back": { + page--; + break; + } + case "next": { + page++; + break; + } + case "add": { + const newPage = await editRoleMenuPage(i, m) + if(!newPage) break; + currentObject.push(); + page = currentObject.length - 1; + break; + } + case "reorder": { + const reordered = await reorderRoleMenuPages(interaction, m, currentObject); + if(!reordered) break; + currentObject = reordered; + break; + } + case "save": { + client.database.guilds.write(interaction.guild.id, {"roleMenu.options": currentObject}); + modified = false; + break; + } + } + } else if (i.isStringSelectMenu()) { + switch (i.customId) { + case "action": { + switch(i.values[0]) { + case "edit": { + const edited = await editRoleMenuPage(i, m, current!); + if(!edited) break; + currentObject[page] = edited; + modified = true; + break; + } + case "delete": { + if(page === 0 && currentObject.keys.length - 1 > 0) page++; + else page--; + currentObject.splice(page, 1); + break; + } + } + break; + } + case "page": { + page = parseInt(i.values[0]!); + break; + } + } + } + + } while (!closed); + await interaction.deleteReply() }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as Discord.GuildMember; if (!member.permissions.has("ManageRoles")) return "You must have the *Manage Roles* permission to use this command"; diff --git a/src/commands/settings/stats.ts b/src/commands/settings/stats.ts index cdd218b..d46b57e 100644 --- a/src/commands/settings/stats.ts +++ b/src/commands/settings/stats.ts @@ -1,249 +1,403 @@ import { LoadingEmbed } from "../../utils/defaults.js"; -import Discord, { CommandInteraction, Message, ActionRowBuilder, GuildMember, StringSelectMenuBuilder, StringSelectMenuInteraction, AutocompleteInteraction } from "discord.js"; +import Discord, { CommandInteraction, Message, ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, APIMessageComponentEmoji, TextInputBuilder, StringSelectMenuInteraction, ButtonInteraction, MessageComponentInteraction, ChannelSelectMenuBuilder, ChannelSelectMenuInteraction, ModalBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; -import confirmationMessage from "../../utils/confirmationMessage.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import client from "../../utils/client.js"; import convertCurlyBracketString from "../../utils/convertCurlyBracketString.js"; -import { callback as statsChannelAddCallback } from "../../reflex/statsChannelUpdate.js"; import singleNotify from "../../utils/singleNotify.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; +import createPageIndicator from "../../utils/createPageIndicator.js"; +import { modalInteractionCollector } from "../../utils/dualCollector.js"; + const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("stats") .setDescription("Controls channels which update when someone joins or leaves the server") - .addChannelOption((option) => option.setName("channel").setDescription("The channel to modify")) - .addStringOption((option) => - option - .setName("name") - .setDescription("The new channel name | Enter any text or use the extra variables like {memberCount}") - .setAutocomplete(true) - ); -const callback = async (interaction: CommandInteraction): Promise => { // TODO: This command feels unintuitive. Clicking a channel in the select menu deletes it - // instead, it should give a submenu to edit the channel, enable/disable or delete it - singleNotify("statsChannelDeleted", interaction.guild!.id, true); - const m = (await interaction.reply({ - embeds: LoadingEmbed, - ephemeral: true, - fetchReply: true - })) as Message; - let config = await client.database.guilds.read(interaction.guild!.id); - if (interaction.options.get("name")?.value as string) { - let channel; - if (Object.keys(config.stats).length >= 25) { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("CHANNEL.TEXT.DELETE") - .setTitle("Stats Channel") - .setDescription("You can only have 25 stats channels in a server") - .setStatus("Danger") - ] - }); - } + +const showModal = async (interaction: MessageComponentInteraction, current: { enabled: boolean; name: string; }) => { + await interaction.showModal( + new ModalBuilder() + .setCustomId("modal") + .setTitle(`Stats channel name`) + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("ex1") + .setLabel("Server Info (1/3)") + .setPlaceholder( + `{serverName} - This server's name\n\n` + + `These placeholders will be replaced with the server's name, etc..` + ) + .setMaxLength(1) + .setRequired(false) + .setStyle(Discord.TextInputStyle.Paragraph) + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("ex2") + .setLabel("Member Counts (2/3) - {MemberCount:...}") + .setPlaceholder( + `{:all} - Total member count\n` + + `{:humans} - Total non-bot users\n` + + `{:bots} - Number of bots\n` + ) + .setMaxLength(1) + .setRequired(false) + .setStyle(Discord.TextInputStyle.Paragraph) + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("ex3") + .setLabel("Latest Member (3/3) - {member:...}") + .setPlaceholder( + `{:name} - The members name\n` + ) + .setMaxLength(1) + .setRequired(false) + .setStyle(Discord.TextInputStyle.Paragraph) + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("text") + .setLabel("Channel name input") + .setMaxLength(1000) + .setRequired(true) + .setStyle(Discord.TextInputStyle.Short) + .setValue(current.name) + ) + ) + ); +} + +type ObjectSchema = Record + + +const addStatsChannel = async (interaction: CommandInteraction, m: Message, currentObject: ObjectSchema): Promise => { + let closed = false; + let cancelled = false; + const originalObject = Object.fromEntries(Object.entries(currentObject).map(([k, v]) => [k, {...v}])); + let newChannel: string | undefined; + let newChannelName: string = "{memberCount:all}-members"; + let newChannelEnabled: boolean = true; + do { + m = await interaction.editReply({ + embeds: [new EmojiEmbed() + .setTitle("Stats Channel") + .setDescription( + `New stats channel` + (newChannel ? ` in <#${newChannel}>` : "") + "\n\n" + + `**Name:** \`${newChannelName}\`\n` + + `**Preview:** ${await convertCurlyBracketString(newChannelName, interaction.user!.id, interaction.user.username, interaction.guild!.name, interaction.guild!.members)}\n` + + `**Enabled:** ${newChannelEnabled ? "Yes" : "No"}\n\n` + ) + .setEmoji("SETTINGS.STATS.GREEN") + .setStatus("Success") + ], components: [ + new ActionRowBuilder().addComponents( + new ChannelSelectMenuBuilder() + .setCustomId("channel") + .setPlaceholder("Select a channel to use") + ), + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Cancel") + .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) + .setStyle(ButtonStyle.Danger) + .setCustomId("back"), + new ButtonBuilder() + .setLabel("Save") + .setEmoji(getEmojiByName("ICONS.SAVE", "id")) + .setStyle(ButtonStyle.Success) + .setCustomId("save"), + new ButtonBuilder() + .setLabel("Edit name") + .setEmoji(getEmojiByName("ICONS.EDIT", "id")) + .setStyle(ButtonStyle.Primary) + .setCustomId("editName"), + new ButtonBuilder() + .setLabel(newChannelEnabled ? "Enabled" : "Disabled") + .setEmoji(getEmojiByName(newChannelEnabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id")) + .setStyle(ButtonStyle.Secondary) + .setCustomId("toggleEnabled") + ) + ] + }); + let i: ButtonInteraction | ChannelSelectMenuInteraction; try { - channel = interaction.options.get("channel")?.channel as Discord.Channel; - } catch { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("CHANNEL.TEXT.DELETE") - .setTitle("Stats Channel") - .setDescription("The channel you provided is not a valid channel") - .setStatus("Danger") - ] - }); - } - channel = channel as Discord.TextChannel; - if (channel.guild.id !== interaction.guild!.id) { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Stats Channel") - .setDescription("You must choose a channel in this server") - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ] - }); - } - let newName = await convertCurlyBracketString( - interaction.options.get("name")?.value as string, - "", - "", - interaction.guild!.name, - interaction.guild!.members - ); - if (interaction.options.get("channel")?.channel!.type === Discord.ChannelType.GuildText) { - newName = newName.toLowerCase().replace(/[\s]/g, "-"); + i = await m.awaitMessageComponent({ time: 300000, filter: (i) => { + return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id; + }}) as ButtonInteraction | ChannelSelectMenuInteraction; + } catch (e) { + closed = true; + cancelled = true; + break; } - const confirmation = await new confirmationMessage(interaction) - .setEmoji("CHANNEL.TEXT.EDIT") - .setTitle("Stats Channel") - .setDescription( - `Are you sure you want to set <#${channel.id}> to a stats channel?\n\n*Preview: ${newName.replace( - /^ +| $/g, - "" - )}*` - ) - .setColor("Warning") - .setInverted(true) - .setFailedMessage(`Could not convert <#${channel.id}> to a stats chanel.`, "Danger", "CHANNEL.TEXT.DELETE") - .send(true); - if (confirmation.cancelled) return; - if (confirmation.success) { - try { - const name = interaction.options.get("name")?.value as string; - const channel = interaction.options.get("channel")?.channel as Discord.TextChannel; - await client.database.guilds.write(interaction.guild!.id, { - [`stats.${channel.id}`]: { name: name, enabled: true } - }); - const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger; - const data = { - meta: { - type: "statsChannelUpdate", - displayName: "Stats Channel Updated", - calculateType: "nucleusSettingsUpdated", - color: NucleusColors.yellow, - emoji: "CHANNEL.TEXT.EDIT", - timestamp: new Date().getTime() - }, - list: { - memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), - changedBy: entry(interaction.user.id, renderUser(interaction.user)), - channel: entry(channel.id, renderChannel(channel)), - name: entry( - interaction.options.get("name")?.value as string, - `\`${interaction.options.get("name")?.value as string}\`` - ) - }, - hidden: { - guild: interaction.guild!.id + if (i.isButton()) { + switch (i.customId) { + case "back": { + await i.deferUpdate(); + closed = true; + break; + } + case "save": { + await i.deferUpdate(); + if (newChannel) { + currentObject[newChannel] = { + name: newChannelName, + enabled: newChannelEnabled + } } - }; - log(data); - } catch (e) { - console.log(e); - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Stats Channel") - .setDescription("Something went wrong and the stats channel could not be set") - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ], - components: [] - }); + closed = true; + break; + } + case "editName": { + await interaction.editReply({ + embeds: [new EmojiEmbed() + .setTitle("Stats Channel") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + .setEmoji("SETTINGS.STATS.GREEN") + ], + components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle(ButtonStyle.Primary) + .setCustomId("back") + ) + ] + }); + showModal(i, {name: newChannelName, enabled: newChannelEnabled}) + + const out: Discord.ModalSubmitInteraction | ButtonInteraction| null = await modalInteractionCollector(m, interaction.user); + if (!out) continue; + if (out.isButton()) continue; + newChannelName = out.fields.getTextInputValue("text"); + break; + } + case "toggleEnabled": { + await i.deferUpdate(); + newChannelEnabled = !newChannelEnabled; + break; + } } } else { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Stats Channel") - .setDescription("No changes were made") - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], - components: [] - }); + await i.deferUpdate(); + if (i.customId === "channel") { + newChannel = i.values[0]; + } } - await statsChannelAddCallback(client, interaction.member as GuildMember); - } - let timedOut = false; - while (!timedOut) { - config = await client.database.guilds.read(interaction.guild!.id); - const stats = config.stats; - const selectMenu = new StringSelectMenuBuilder() - .setCustomId("remove") - .setMinValues(1) - .setMaxValues(Math.max(1, Object.keys(stats).length)); - await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Stats Channel") - .setDescription( - "The following channels update when someone joins or leaves the server. You can select a channel to remove it from the list." - ) - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], - components: [ - new ActionRowBuilder().addComponents( - Object.keys(stats).length - ? [ - selectMenu - .setPlaceholder("Select a stats channel to remove, stopping it updating") - .addOptions( - Object.keys(stats).map((key) => ({ - label: interaction.guild!.channels.cache.get(key)!.name, - value: key, - description: `${stats[key]!.name}` - })) - ) - ] - : [ - selectMenu - .setPlaceholder("The server has no stats channels") - .setDisabled(true) - .setOptions([ - { - label: "*Placeholder*", - value: "placeholder", - description: "No stats channels" - } - ]) - ] - ) - ] - }); - let i: StringSelectMenuInteraction; + } while (!closed) + if (cancelled) return originalObject; + if (!(newChannel && newChannelName && newChannelEnabled)) return originalObject; + return currentObject; +} +const callback = async (interaction: CommandInteraction) => { + if (!interaction.guild) return; + const { renderChannel } = client.logger; + const m: Message = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true }); + let page = 0; + let closed = false; + const config = await client.database.guilds.read(interaction.guild.id); + let currentObject: ObjectSchema = config.stats; + let modified = false; + do { + const embed = new EmojiEmbed() + .setTitle("Stats Settings") + .setEmoji("SETTINGS.STATS.GREEN") + .setStatus("Success"); + const noStatsChannels = Object.keys(currentObject).length === 0; + let current: { enabled: boolean; name: string; }; + + const pageSelect = new StringSelectMenuBuilder() + .setCustomId("page") + .setPlaceholder("Select a stats channel to manage"); + const actionSelect = new StringSelectMenuBuilder() + .setCustomId("action") + .setPlaceholder("Perform an action") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Edit") + .setDescription("Edit the stats channel") + .setValue("edit") + .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), + new StringSelectMenuOptionBuilder() + .setLabel("Delete") + .setDescription("Delete the stats channel") + .setValue("delete") + .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji) + ); + const buttonRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) + .setDisabled(page === 0), + new ButtonBuilder() + .setCustomId("next") + .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Primary) + .setDisabled(page === Object.keys(currentObject).length - 1), + new ButtonBuilder() + .setCustomId("add") + .setLabel("Create new") + .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Secondary) + .setDisabled(Object.keys(currentObject).length >= 24), + new ButtonBuilder() + .setCustomId("save") + .setLabel("Save") + .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Success) + .setDisabled(modified), + ); + if (noStatsChannels) { + embed.setDescription("No stats channels have been set up yet. Use the button below to add one.\n\n" + + createPageIndicator(1, 1, undefined, true) + ); + pageSelect.setDisabled(true); + actionSelect.setDisabled(true); + pageSelect.addOptions(new StringSelectMenuOptionBuilder() + .setLabel("No stats channels") + .setValue("none") + ); + } else { + page = Math.min(page, Object.keys(currentObject).length - 1); + current = currentObject[Object.keys(config.stats)[page]!]! + actionSelect.addOptions(new StringSelectMenuOptionBuilder() + .setLabel(current.enabled ? "Disable" : "Enable") + .setValue("toggleEnabled") + .setDescription(`Currently ${current.enabled ? "Enabled" : "Disabled"}, click to ${current.enabled ? "disable" : "enable"} this channel`) + .setEmoji(getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji) + ); + embed.setDescription(`**Currently Editing:** ${renderChannel(Object.keys(currentObject)[page]!)}\n\n` + + `${getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Currently ${current.enabled ? "Enabled" : "Disabled"}\n` + + `**Name:** \`${current.name}\`\n` + + `**Preview:** ${await convertCurlyBracketString(current.name, interaction.user.id, interaction.user.username, interaction.guild.name, interaction.guild.members)}` + '\n\n' + + createPageIndicator(Object.keys(config.stats).length, page) + ); + for (const [id, { name, enabled }] of Object.entries(currentObject)) { + pageSelect.addOptions(new StringSelectMenuOptionBuilder() + .setLabel(`${name} (${renderChannel(id)})`) + .setEmoji(getEmojiByName(enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji) + .setDescription(`${enabled ? "Enabled" : "Disabled"}`) + .setValue(id) + ); + } + } + + interaction.editReply({embeds: [embed], components: [ + new ActionRowBuilder().addComponents(pageSelect), + new ActionRowBuilder().addComponents(actionSelect), + buttonRow + ]}); + + let i: StringSelectMenuInteraction | ButtonInteraction; try { - i = await m.awaitMessageComponent({ - time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } - }) as StringSelectMenuInteraction; + i = await m.awaitMessageComponent({ filter: (interaction) => interaction.user.id === interaction.user.id, time: 60000 }) as StringSelectMenuInteraction | ButtonInteraction; } catch (e) { - timedOut = true; + closed = true; continue; } - i.deferUpdate(); - if (i.customId === "remove") { - const toRemove = i.values; - await client.database.guilds.write( - interaction.guild!.id, - null, - toRemove.map((k) => `stats.${k}`) - ); + + if(i.isStringSelectMenu()) { + switch(i.customId) { + case "page": { + await i.deferUpdate(); + page = Object.keys(currentObject).indexOf(i.values[0]!); + break; + } + case "action": { + modified = true; + switch(i.values[0]!) { + case "edit": { + showModal(i, current!) + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Stats Channel") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + .setEmoji("SETTINGS.STATS.GREEN") + ], + components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle(ButtonStyle.Primary) + .setCustomId("back") + ) + ] + }); + let out: Discord.ModalSubmitInteraction | null; + try { + out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null; + } catch (e) { + continue; + } + if (!out) continue + if (out.isButton()) continue; + currentObject[Object.keys(currentObject)[page]!]!.name = out.fields.getTextInputValue("text"); + break; + } + case "toggleEnabled": { + await i.deferUpdate(); + currentObject[Object.keys(currentObject)[page]!]!.enabled = !currentObject[Object.keys(currentObject)[page]!]!.enabled; + modified = true; + break; + } + case "delete": { + await i.deferUpdate(); + currentObject = Object.fromEntries(Object.entries(currentObject).filter(([k]) => k !== Object.keys(currentObject)[page]!)); + page = Math.min(page, Object.keys(currentObject).length - 1); + modified = true; + break; + } + } + break; + } + } + } else { + await i.deferUpdate(); + switch(i.customId) { + case "back": { + page--; + break; + } + case "next": { + page++; + break; + } + case "add": { + currentObject = await addStatsChannel(interaction, m, currentObject); + page = Object.keys(currentObject).length - 1; + break; + } + case "save": { + client.database.guilds.write(interaction.guild.id, {stats: currentObject}); + singleNotify("statsChannelDeleted", interaction.guild.id, true); + modified = false; + break; + } + } } - } - await interaction.editReply({ - embeds: [new Discord.EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message timed out" })], - components: [] - }); + + } while (!closed); + await interaction.deleteReply() }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as Discord.GuildMember; if (!member.permissions.has("ManageChannels")) return "You must have the *Manage Channels* permission to use this command"; return true; }; -const generateStatsChannelAutocomplete = (prompt: string): string[] => { - return [prompt]; -}; - -const autocomplete = async (interaction: AutocompleteInteraction): Promise => { - if (!interaction.guild) return []; - const prompt = interaction.options.getString("tag"); - // generateStatsChannelAutocomplete(int.options.getString("name") ?? "") - const results = generateStatsChannelAutocomplete(prompt ?? ""); - return results; -}; - - export { command }; export { callback }; -export { check }; -export { autocomplete }; \ No newline at end of file +export { check }; \ No newline at end of file diff --git a/src/commands/settings/tickets.ts b/src/commands/settings/tickets.ts index 892a420..2e046bf 100644 --- a/src/commands/settings/tickets.ts +++ b/src/commands/settings/tickets.ts @@ -1,68 +1,38 @@ import { LoadingEmbed } from "../../utils/defaults.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; -import confirmationMessage from "../../utils/confirmationMessage.js"; import Discord, { CommandInteraction, - GuildChannel, Message, ActionRowBuilder, ButtonBuilder, - MessageComponentInteraction, StringSelectMenuBuilder, - Role, - StringSelectMenuInteraction, ButtonStyle, TextInputBuilder, ButtonComponent, - StringSelectMenuComponent, ModalSubmitInteraction, - APIMessageComponentEmoji + APIMessageComponentEmoji, + RoleSelectMenuBuilder, + ChannelSelectMenuBuilder, + RoleSelectMenuInteraction, + ButtonInteraction, + ChannelSelectMenuInteraction, + TextInputStyle, + ModalBuilder, + ChannelType } from "discord.js"; -import { SlashCommandSubcommandBuilder, StringSelectMenuOptionBuilder } from "@discordjs/builders"; -import { ChannelType } from "discord-api-types/v9"; +import { SlashCommandSubcommandBuilder, StringSelectMenuOptionBuilder } from "discord.js"; import client from "../../utils/client.js"; import { toHexInteger, toHexArray, tickets as ticketTypes } from "../../utils/calculate.js"; import { capitalize } from "../../utils/generateKeyValueList.js"; import { modalInteractionCollector } from "../../utils/dualCollector.js"; import type { GuildConfig } from "../../utils/database.js"; +import { LinkWarningFooter } from "../../utils/defaults.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("tickets") - .setDescription("Shows settings for tickets | Use no arguments to manage custom types") - .addStringOption((option) => - option - .setName("enabled") - .setDescription("If users should be able to create tickets") - .setRequired(false) - .addChoices( - {name: "Yes", value: "yes"}, - {name: "No",value: "no"} - ) - ) - .addChannelOption((option) => - option - .setName("category") - .setDescription("The category where tickets are created") - .addChannelTypes(ChannelType.GuildCategory) - .setRequired(false) - ) - .addNumberOption((option) => - option - .setName("maxticketsperuser") - .setDescription("The maximum amount of tickets a user can create | Default: 5") - .setRequired(false) - .setMinValue(1) - ) - .addRoleOption((option) => - option - .setName("supportrole") - .setDescription( - "This role will have view access to all tickets and will be pinged when a ticket is created" - ) - .setRequired(false) - ); + .setDescription("Shows settings for tickets") const callback = async (interaction: CommandInteraction): Promise => { if (!interaction.guild) return; @@ -71,392 +41,130 @@ const callback = async (interaction: CommandInteraction): Promise => { ephemeral: true, fetchReply: true })) as Message; - const options = { - enabled: (interaction.options.get("enabled")?.value as string).startsWith("yes") as boolean | null, - category: interaction.options.get("category")?.channel as Discord.CategoryChannel | null, - maxtickets: interaction.options.get("maxticketsperuser")?.value as number | null, - supportping: interaction.options.get("supportrole")?.role as Role | null - }; - if (options.enabled !== null || options.category || options.maxtickets || options.supportping) { - if (options.category) { - let channel: GuildChannel | null; - try { - channel = await interaction.guild.channels.fetch(options.category.id) as GuildChannel; - } catch { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("CHANNEL.TEXT.DELETE") - .setTitle("Tickets > Category") - .setDescription("The channel you provided is not a valid category") - .setStatus("Danger") - ] - }); - } - channel = channel as Discord.CategoryChannel; - if (channel.guild.id !== interaction.guild.id) - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Tickets > Category") - .setDescription("You must choose a category in this server") - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ] - }); - } - if (options.maxtickets) { - if (options.maxtickets < 1) - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Tickets > Max Tickets") - .setDescription("You must choose a number greater than 0") - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ] - }); - } - let role: Role | null; - if (options.supportping) { - try { - role = await interaction.guild.roles.fetch(options.supportping.id); - } catch { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("GUILD.ROLE.DELETE") - .setTitle("Tickets > Support Ping") - .setDescription("The role you provided is not a valid role") - .setStatus("Danger") - ] - }); - } - if (!role) return; - role = role as Discord.Role; - if (role.guild.id !== interaction.guild.id) - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Tickets > Support Ping") - .setDescription("You must choose a role in this server") - .setStatus("Danger") - .setEmoji("GUILD.ROLE.DELETE") - ] - }); - } - - const confirmation = await new confirmationMessage(interaction) - .setEmoji("GUILD.TICKET.ARCHIVED") - .setTitle("Tickets") - .setDescription( - (options.category ? `**Category:** ${options.category.name}\n` : "") + - (options.maxtickets ? `**Max Tickets:** ${options.maxtickets}\n` : "") + - (options.supportping ? `**Support Ping:** ${options.supportping.name}\n` : "") + - (options.enabled !== null - ? `**Enabled:** ${ - options.enabled - ? `${getEmojiByName("CONTROL.TICK")} Yes` - : `${getEmojiByName("CONTROL.CROSS")} No` - }\n` - : "") + - "\nAre you sure you want to apply these settings?" - ) - .setColor("Warning") - .setFailedMessage("Cancelled", "Warning", "GUILD.TICKET.CLOSE") // TODO: Set Actual Message - .setInverted(true) - .send(true); - if (confirmation.cancelled) return; - if (confirmation.success) { - const toUpdate: Record = {}; - if (options.enabled !== null) toUpdate["tickets.enabled"] = options.enabled; - if (options.category) toUpdate["tickets.category"] = options.category.id; - if (options.maxtickets) toUpdate["tickets.maxTickets"] = options.maxtickets; - if (options.supportping) toUpdate["tickets.supportRole"] = options.supportping.id; - try { - await client.database.guilds.write(interaction.guild.id, toUpdate); - } catch (e) { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Tickets") - .setDescription("Something went wrong and the staff notifications channel could not be set") - .setStatus("Danger") - .setEmoji("GUILD.TICKET.DELETE") - ], - components: [] - }); - } - } else { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Tickets") - .setDescription("No changes were made") - .setStatus("Success") - .setEmoji("GUILD.TICKET.OPEN") - ], - components: [] - }); - } - } const data = await client.database.guilds.read(interaction.guild.id); data.tickets.customTypes = (data.tickets.customTypes ?? []).filter( (value: string, index: number, array: string[]) => array.indexOf(value) === index ); - let lastClicked = ""; - const embed: EmojiEmbed = new EmojiEmbed(); - const compiledData = { - enabled: data.tickets.enabled, - category: data.tickets.category, - maxTickets: data.tickets.maxTickets, - supportRole: data.tickets.supportRole, - useCustom: data.tickets.useCustom, - types: data.tickets.types, - customTypes: data.tickets.customTypes as string[] | null - }; + let ticketData = (await client.database.guilds.read(interaction.guild.id)).tickets + let changesMade = false; let timedOut = false; + let errorMessage = ""; while (!timedOut) { - embed + const embed: EmojiEmbed = new EmojiEmbed() .setTitle("Tickets") .setDescription( - `${compiledData.enabled ? "" : getEmojiByName("TICKETS.REPORT")} **Enabled:** ${ - compiledData.enabled ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No` + `${ticketData.enabled ? "" : getEmojiByName("TICKETS.REPORT")} **Enabled:** ${ + ticketData.enabled ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No` }\n` + - `${compiledData.category ? "" : getEmojiByName("TICKETS.REPORT")} **Category:** ${ - compiledData.category ? `<#${compiledData.category}>` : "*None set*" - }\n` + - `**Max Tickets:** ${compiledData.maxTickets ? compiledData.maxTickets : "*No limit*"}\n` + - `**Support Ping:** ${compiledData.supportRole ? `<@&${compiledData.supportRole}>` : "*None set*"}\n\n` + - (compiledData.useCustom && compiledData.customTypes === null ? `${getEmojiByName("TICKETS.REPORT")} ` : "") + - `${compiledData.useCustom ? "Custom" : "Default"} types in use` + + `${ticketData.category ? "" : getEmojiByName("TICKETS.REPORT")}` + + ((await interaction.guild.channels.fetch(ticketData.category!))!.type === ChannelType.GuildCategory ? + `**Category:** ` : `**Channel:** `) + // TODO: Notify if permissions are wrong + `${ticketData.category ? `<#${ticketData.category}>` : "*None set*"}\n` + + `**Max Tickets:** ${ticketData.maxTickets ? ticketData.maxTickets : "*No limit*"}\n` + + `**Support Ping:** ${ticketData.supportRole ? `<@&${ticketData.supportRole}>` : "*None set*"}\n\n` + + (ticketData.useCustom && ticketData.customTypes === null ? `${getEmojiByName("TICKETS.REPORT")} ` : "") + + `${ticketData.useCustom ? "Custom" : "Default"} types in use` + "\n\n" + `${getEmojiByName("TICKETS.REPORT")} *Indicates a setting stopping tickets from being used*` ) .setStatus("Success") .setEmoji("GUILD.TICKET.OPEN"); + if (errorMessage) embed.setFooter({text: errorMessage, iconURL: LinkWarningFooter.iconURL}); m = (await interaction.editReply({ embeds: [embed], components: [ - new ActionRowBuilder().addComponents([ + new ActionRowBuilder().addComponents( new ButtonBuilder() - .setLabel("Tickets " + (compiledData.enabled ? "enabled" : "disabled")) - .setEmoji(getEmojiByName("CONTROL." + (compiledData.enabled ? "TICK" : "CROSS"), "id")) - .setStyle(compiledData.enabled ? ButtonStyle.Success : ButtonStyle.Danger) + .setLabel("Tickets " + (ticketData.enabled ? "enabled" : "disabled")) + .setEmoji(getEmojiByName("CONTROL." + (ticketData.enabled ? "TICK" : "CROSS"), "id")) + .setStyle(ticketData.enabled ? ButtonStyle.Success : ButtonStyle.Danger) .setCustomId("enabled"), new ButtonBuilder() - .setLabel(lastClicked === "cat" ? "Click again to confirm" : "Clear category") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setStyle(ButtonStyle.Danger) - .setCustomId("clearCategory") - .setDisabled(compiledData.category === null), - new ButtonBuilder() - .setLabel(lastClicked === "max" ? "Click again to confirm" : "Reset max tickets") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setStyle(ButtonStyle.Danger) - .setCustomId("clearMaxTickets") - .setDisabled(compiledData.maxTickets === 5), - new ButtonBuilder() - .setLabel(lastClicked === "sup" ? "Click again to confirm" : "Clear support ping") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setStyle(ButtonStyle.Danger) - .setCustomId("clearSupportPing") - .setDisabled(compiledData.supportRole === null) - ]), - new ActionRowBuilder().addComponents([ + .setLabel("Set max tickets") + .setEmoji(getEmojiByName("CONTROL.TICKET", "id")) + .setStyle(ButtonStyle.Primary) + .setCustomId("setMaxTickets") + .setDisabled(!ticketData.enabled), new ButtonBuilder() .setLabel("Manage types") .setEmoji(getEmojiByName("TICKETS.OTHER", "id")) .setStyle(ButtonStyle.Secondary) - .setCustomId("manageTypes"), + .setCustomId("manageTypes") + .setDisabled(!ticketData.enabled), new ButtonBuilder() - .setLabel("Add create ticket button") - .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id")) - .setStyle(ButtonStyle.Primary) - .setCustomId("send") - ]) + .setLabel("Save") + .setEmoji(getEmojiByName("ICONS.SAVE", "id")) + .setStyle(ButtonStyle.Success) + .setCustomId("save") + .setDisabled(!changesMade) + ), + new ActionRowBuilder().addComponents( + new RoleSelectMenuBuilder() + .setCustomId("supportRole") + .setPlaceholder("Select a support role") + .setDisabled(!ticketData.enabled) + ), + new ActionRowBuilder().addComponents( + new ChannelSelectMenuBuilder() + .setCustomId("category") + .setPlaceholder("Select a category or channel") + .setDisabled(!ticketData.enabled) + ) ] - })) as Message; - let i: MessageComponentInteraction; + })); + let i: RoleSelectMenuInteraction | ButtonInteraction | ChannelSelectMenuInteraction; try { - i = await m.awaitMessageComponent({ + i = await m.awaitMessageComponent<2 | 6 | 8>({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id } }); } catch (e) { timedOut = true; continue; } - i.deferUpdate(); - if ((i.component as ButtonComponent).customId === "clearCategory") { - if (lastClicked === "cat") { - lastClicked = ""; - await client.database.guilds.write(interaction.guild.id, null, ["tickets.category"]); - compiledData.category = null; - } else lastClicked = "cat"; - } else if ((i.component as ButtonComponent).customId === "clearMaxTickets") { - if (lastClicked === "max") { - lastClicked = ""; - await client.database.guilds.write(interaction.guild.id, null, ["tickets.maxTickets"]); - compiledData.maxTickets = 5; - } else lastClicked = "max"; - } else if ((i.component as ButtonComponent).customId === "clearSupportPing") { - if (lastClicked === "sup") { - lastClicked = ""; - await client.database.guilds.write(interaction.guild.id, null, ["tickets.supportRole"]); - compiledData.supportRole = null; - } else lastClicked = "sup"; - } else if ((i.component as ButtonComponent).customId === "send") { - const ticketMessages = [ - { - label: "Create ticket", - description: "Click the button below to create a ticket" - }, - { - label: "Issues, questions or feedback?", - description: "Click below to open a ticket and get help from our staff team" - }, - { - label: "Contact Us", - description: "Click the button below to speak to us privately" + changesMade = true; + if (i.isRoleSelectMenu()) { + await i.deferUpdate(); + ticketData.supportRole = i.values[0] ?? null; + } else if (i.isChannelSelectMenu()) { + await i.deferUpdate(); + ticketData.category = i.values[0] ?? null; + } else { + switch(i.customId) { + case "save": { + await i.deferUpdate(); + await client.database.guilds.write(interaction.guild.id, { tickets: ticketData }); + changesMade = false; + break; } - ]; - let innerTimedOut = false; - let templateSelected = false; - while (!innerTimedOut && !templateSelected) { - const enabled = compiledData.enabled && compiledData.category !== null; - await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Ticket Button") - .setDescription("Select a message template to send in this channel") - .setFooter({ - text: enabled - ? "" - : "Tickets are not set up correctly so the button may not work for users. Check the main menu to find which options must be set." - }) - .setStatus(enabled ? "Success" : "Warning") - .setEmoji("GUILD.ROLES.CREATE") - ], - components: [ - new ActionRowBuilder().addComponents([ - new StringSelectMenuBuilder() - .setOptions( - ticketMessages.map( - ( - t: { - label: string; - description: string; - value?: string; - }, - index - ) => { - t.value = index.toString(); - return t as { - value: string; - label: string; - description: string; - }; - } - ) - ) - .setCustomId("template") - .setMaxValues(1) - .setMinValues(1) - .setPlaceholder("Select a message template") - ]), - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId("back") - .setLabel("Back") - .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) - .setStyle(ButtonStyle.Danger), - new ButtonBuilder().setCustomId("blank").setLabel("Empty").setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId("custom") - .setLabel("Custom") - .setEmoji(getEmojiByName("TICKETS.OTHER", "id")) - .setStyle(ButtonStyle.Primary) - ]) - ] - }); - let i: MessageComponentInteraction; - try { - i = await m.awaitMessageComponent({ - time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } - }); - } catch (e) { - innerTimedOut = true; - continue; + case "enabled": { + await i.deferUpdate(); + ticketData.enabled = !ticketData.enabled; + break; } - if ((i.component as StringSelectMenuComponent).customId === "template") { - i.deferUpdate(); - await interaction.channel!.send({ - embeds: [ - new EmojiEmbed() - .setTitle(ticketMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.label) - .setDescription( - ticketMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.description - ) - .setStatus("Success") - .setEmoji("GUILD.TICKET.OPEN") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel("Create Ticket") - .setEmoji(getEmojiByName("CONTROL.TICK", "id")) - .setStyle(ButtonStyle.Success) - .setCustomId("createticket") - ]) - ] - }); - templateSelected = true; - continue; - } else if ((i.component as ButtonComponent).customId === "blank") { - i.deferUpdate(); - await interaction.channel!.send({ - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel("Create Ticket") - .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id")) - .setStyle(ButtonStyle.Success) - .setCustomId("createticket") - ]) - ] - }); - templateSelected = true; - continue; - } else if ((i.component as ButtonComponent).customId === "custom") { + case "setMaxTickets": { await i.showModal( - new Discord.ModalBuilder() - .setCustomId("modal") - .setTitle("Enter embed details") + new ModalBuilder() + .setCustomId("maxTickets") + .setTitle("Set max tickets") .addComponents( - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId("title") - .setLabel("Title") - .setMaxLength(256) - .setRequired(true) - .setStyle(Discord.TextInputStyle.Short) - ), - new ActionRowBuilder().addComponents( + new ActionRowBuilder().setComponents( new TextInputBuilder() - .setCustomId("description") - .setLabel("Description") - .setMaxLength(4000) - .setRequired(true) - .setStyle(Discord.TextInputStyle.Paragraph) + .setLabel("Max tickets - Leave blank for no limit") + .setCustomId("maxTickets") + .setPlaceholder("Enter a number") + .setRequired(false) + .setValue(ticketData.maxTickets.toString()) + .setMinLength(1) + .setMaxLength(3) + .setStyle(TextInputStyle.Short) ) ) - ); - await interaction.editReply({ + ) + await i.editReply({ embeds: [ new EmojiEmbed() - .setTitle("Ticket Button") + .setTitle("Tickets") .setDescription("Modal opened. If you can't see it, click back and try again.") .setStatus("Success") .setEmoji("GUILD.TICKET.OPEN") @@ -473,54 +181,33 @@ const callback = async (interaction: CommandInteraction): Promise => { }); let out; try { - out = await modalInteractionCollector( - m, - (m) => m.channel!.id === interaction.channel!.id, - (m) => m.customId === "modify" - ); + out = await modalInteractionCollector(m, interaction.user); } catch (e) { - innerTimedOut = true; continue; } + if (!out || out.isButton()) continue; out = out as ModalSubmitInteraction; - const title = out.fields.getTextInputValue("title"); - const description = out.fields.getTextInputValue("description"); - await interaction.channel!.send({ - embeds: [ - new EmojiEmbed() - .setTitle(title) - .setDescription(description) - .setStatus("Success") - .setEmoji("GUILD.TICKET.OPEN") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel("Create Ticket") - .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id")) - .setStyle(ButtonStyle.Success) - .setCustomId("createticket") - ]) - ] - }); - templateSelected = true; + const toAdd = out.fields.getTextInputValue("maxTickets"); + if(isNaN(parseInt(toAdd))) { + errorMessage = "You entered an invalid number - No changes were made"; + break; + } + ticketData.maxTickets = toAdd === "" ? 0 : parseInt(toAdd); + break; + } + case "manageTypes": { + await i.deferUpdate(); + ticketData = await manageTypes(interaction, data.tickets, m); + break; } } - } else if ((i.component as ButtonComponent).customId === "enabled") { - await client.database.guilds.write(interaction.guild.id, { - "tickets.enabled": !compiledData.enabled - }); - compiledData.enabled = !compiledData.enabled; - } else if ((i.component as ButtonComponent).customId === "manageTypes") { - data.tickets = await manageTypes(interaction, data.tickets, m as Message); } } - await interaction.editReply({ - embeds: [ embed.setFooter({ text: "Message timed out" })], - components: [] - }); + await interaction.deleteReply() }; + + async function manageTypes(interaction: CommandInteraction, data: GuildConfig["tickets"], m: Message) { let timedOut = false; let backPressed = false; @@ -545,7 +232,7 @@ async function manageTypes(interaction: CommandInteraction, data: GuildConfig["t .setStatus("Success") .setEmoji("GUILD.TICKET.OPEN") ], - components: (customTypes + components: (customTypes && customTypes.length > 0 ? [ new ActionRowBuilder().addComponents([ new Discord.StringSelectMenuBuilder() @@ -637,29 +324,23 @@ async function manageTypes(interaction: CommandInteraction, data: GuildConfig["t try { i = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id } }); } catch (e) { timedOut = true; continue; } - if ((i.component as StringSelectMenuComponent).customId === "types") { - i.deferUpdate(); - const types = toHexInteger((i as StringSelectMenuInteraction).values, ticketTypes); - await client.database.guilds.write(interaction.guild!.id, { - "tickets.types": types - }); + if (i.isStringSelectMenu() && i.customId === "types") { + await i.deferUpdate(); + const types = toHexInteger(i.values, ticketTypes); data.types = types; - } else if ((i.component as StringSelectMenuComponent).customId === "removeTypes") { - i.deferUpdate(); - const types = (i as StringSelectMenuInteraction).values; + } else if (i.isStringSelectMenu() && i.customId === "removeTypes") { + await i.deferUpdate(); + const types = i.values; let customTypes = data.customTypes; if (customTypes) { customTypes = customTypes.filter((t) => !types.includes(t)); customTypes = customTypes.length > 0 ? customTypes : null; - await client.database.guilds.write(interaction.guild!.id, { - "tickets.customTypes": customTypes - }); data.customTypes = customTypes; } } else if ((i.component as ButtonComponent).customId === "addType") { @@ -680,7 +361,7 @@ async function manageTypes(interaction: CommandInteraction, data: GuildConfig["t ) ) ); - await interaction.editReply({ + await i.editReply({ embeds: [ new EmojiEmbed() .setTitle("Tickets > Types") @@ -700,14 +381,11 @@ async function manageTypes(interaction: CommandInteraction, data: GuildConfig["t }); let out; try { - out = await modalInteractionCollector( - m, - (m) => m.channel!.id === interaction.channel!.id, - (m) => m.customId === "addType" - ); + out = await modalInteractionCollector(m, interaction.user); } catch (e) { continue; } + if (!out || out.isButton()) continue; out = out as ModalSubmitInteraction; let toAdd = out.fields.getTextInputValue("type"); if (!toAdd) { @@ -715,31 +393,31 @@ async function manageTypes(interaction: CommandInteraction, data: GuildConfig["t } toAdd = toAdd.substring(0, 80); try { - await client.database.guilds.append(interaction.guild!.id, "tickets.customTypes", toAdd); + if(!data.customTypes) data.customTypes = []; + data.customTypes.push(toAdd); } catch { continue; } - data.customTypes = data.customTypes ?? []; if (!data.customTypes.includes(toAdd)) { data.customTypes.push(toAdd); } } else if ((i.component as ButtonComponent).customId === "switchToDefault") { - i.deferUpdate(); + await i.deferUpdate(); await client.database.guilds.write(interaction.guild!.id, { "tickets.useCustom": false }, []); data.useCustom = false; } else if ((i.component as ButtonComponent).customId === "switchToCustom") { - i.deferUpdate(); + await i.deferUpdate(); await client.database.guilds.write(interaction.guild!.id, { "tickets.useCustom": true }, []); data.useCustom = true; } else { - i.deferUpdate(); + await i.deferUpdate(); backPressed = true; } } return data; } -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as Discord.GuildMember; if (!member.permissions.has("ManageGuild")) return "You must have the *Manage Server* permission to use this command"; diff --git a/src/commands/settings/tracks.ts b/src/commands/settings/tracks.ts new file mode 100644 index 0000000..d9d485d --- /dev/null +++ b/src/commands/settings/tracks.ts @@ -0,0 +1,459 @@ +import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, Collection, CommandInteraction, GuildMember, Message, ModalBuilder, ModalSubmitInteraction, PermissionsBitField, Role, RoleSelectMenuBuilder, RoleSelectMenuInteraction, SlashCommandSubcommandBuilder, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; +import client from "../../utils/client.js"; +import createPageIndicator, { createVerticalTrack } from "../../utils/createPageIndicator.js"; +import { LoadingEmbed } from "../../utils/defaults.js"; +import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; +import ellipsis from "../../utils/ellipsis.js"; +import { modalInteractionCollector } from "../../utils/dualCollector.js"; + +const { renderRole } = client.logger + +const command = (builder: SlashCommandSubcommandBuilder) => + builder + .setName("tracks") + .setDescription("Manage the tracks for the server") + +interface ObjectSchema { + name: string; + retainPrevious: boolean; + nullable: boolean; + track: string[]; + manageableBy: string[]; +} + + +const editName = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, current?: string) => { + + let name = current ?? ""; + const modal = new ModalBuilder() + .setTitle("Edit Name and Description") + .setCustomId("editNameDescription") + .addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Name") + .setCustomId("name") + .setPlaceholder("The name of the track (e.g. Moderators)") + .setStyle(TextInputStyle.Short) + .setValue(name) + .setRequired(true) + ) + ) + const button = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) + ) + + await i.showModal(modal) + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Tracks") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + ], + components: [button] + }); + + let out: ModalSubmitInteraction | null; + try { + out = await modalInteractionCollector(m, interaction.user) as ModalSubmitInteraction | null; + } catch (e) { + console.error(e); + out = null; + } + if(!out) return name; + if (out.isButton()) return name; + name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name; + return name + +} + +const reorderTracks = async (interaction: ButtonInteraction, m: Message, roles: Collection, currentObj: string[]) => { + const reorderRow = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("reorder") + .setPlaceholder("Select all roles in the order you want users to gain them (Lowest to highest rank).") + .setMinValues(currentObj.length) + .setMaxValues(currentObj.length) + .addOptions( + currentObj.map((o, i) => new StringSelectMenuOptionBuilder() + .setLabel(roles.get(o)!.name) + .setValue(i.toString()) + ) + ) + ); + const buttonRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) + ) + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Tracks") + .setDescription("Select all roles in the order you want users to gain them (Lowest to highest rank).") + .setStatus("Success") + ], + components: [reorderRow, buttonRow] + }); + let out: StringSelectMenuInteraction | ButtonInteraction | null; + try { + out = await m.awaitMessageComponent({ + filter: (i) => i.channel!.id === interaction.channel!.id, + time: 300000 + }) as StringSelectMenuInteraction | ButtonInteraction | null; + } catch (e) { + console.error(e); + out = null; + } + if(!out) return; + out.deferUpdate(); + if (out.isButton()) return; + const values = out.values; + + const newOrder: string[] = currentObj.map((_, i) => { + const index = values.findIndex(v => v === i.toString()); + return currentObj[index]; + }) as string[]; + + return newOrder; +} + +const editTrack = async (interaction: ButtonInteraction | StringSelectMenuInteraction, message: Message, roles: Collection, current?: ObjectSchema) => { + const isAdmin = (interaction.member!.permissions as PermissionsBitField).has("Administrator"); + if(!current) { + current = { + name: "", + retainPrevious: false, + nullable: false, + track: [], + manageableBy: [] + } + } + + const roleSelect = new ActionRowBuilder() + .addComponents( + new RoleSelectMenuBuilder() + .setCustomId("addRole") + .setPlaceholder("Select a role to add") + .setDisabled(!isAdmin) + ); + let closed = false; + do { + const editableRoles: string[] = current.track.map((r) => { + if(!(roles.get(r)!.position >= (interaction.member as GuildMember).roles.highest.position) || interaction.user.id === interaction.guild?.ownerId) return roles.get(r)!.name; + }).filter(v => v !== undefined) as string[]; + const selectMenu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("removeRole") + .setPlaceholder("Select a role to remove") + .setDisabled(!isAdmin) + .addOptions( + editableRoles.map((r, i) => { + return new StringSelectMenuOptionBuilder() + .setLabel(r) + .setValue(i.toString())} + ) + ) + ); + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("edit") + .setLabel("Edit Name") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("reorder") + .setLabel("Reorder") + .setDisabled(!isAdmin) + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("ICONS.REORDER", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("retainPrevious") + .setLabel("Retain Previous") + .setStyle(current.retainPrevious ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"), "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("nullable") + .setLabel(`Role ${current.nullable ? "Not " : ""}Required`) + .setStyle(current.nullable ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(getEmojiByName("CONTROL." + (current.nullable ? "TICK" : "CROSS"), "id") as APIMessageComponentEmoji) + ); + + const allowed: boolean[] = []; + for (const role of current.track) { + const disabled: boolean = + roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position; + allowed.push(disabled) + } + const mapped = current.track.map(role => roles.find(aRole => aRole.id === role)!); + + const embed = new EmojiEmbed() + .setTitle("Tracks") + .setDescription( + `**Currently Editing:** ${current.name}\n\n` + + `${getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"))} Members ${current.nullable ? "don't " : ""}need a role in this track\n` + + `${getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"))} Members ${current.retainPrevious ? "" : "don't "}keep all roles below their current highest\n\n` + + createVerticalTrack( + mapped.map(role => renderRole(role)), new Array(current.track.length).fill(false), allowed) + ) + .setStatus("Success") + + const comps: ActionRowBuilder[] = [roleSelect, buttons]; + if(current.track.length >= 1) comps.splice(1, 0, selectMenu); + + interaction.editReply({embeds: [embed], components: comps}); + + let out: ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null; + + try { + out = await message.awaitMessageComponent({ + filter: (i) => i.channel!.id === interaction.channel!.id, + time: 300000 + }) as ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null; + } catch (e) { + console.error(e); + out = null; + } + + if(!out) return; + if (out.isButton()) { + switch(out.customId) { + case "back": { + out.deferUpdate(); + closed = true; + break; + } + case "edit": { + current.name = (await editName(out, interaction, message, current.name))!; + break; + } + case "reorder": { + out.deferUpdate(); + current.track = (await reorderTracks(out, message, roles, current.track))!; + break; + } + case "retainPrevious": { + out.deferUpdate(); + current.retainPrevious = !current.retainPrevious; + break; + } + case "nullable": { + out.deferUpdate(); + current.nullable = !current.nullable; + break; + } + } + } else if (out.isStringSelectMenu()) { + out.deferUpdate(); + switch(out.customId) { + case "removeRole": { + const index = current.track.findIndex(v => v === editableRoles[parseInt((out! as StringSelectMenuInteraction).values![0]!)]); + current.track.splice(index, 1); + break; + } + } + } else { + switch(out.customId) { + case "addRole": { + const role = out.values![0]!; + if(!current.track.includes(role)) { + current.track.push(role); + } else { + out.reply({content: "That role is already on this track", ephemeral: true}) + } + break; + } + } + } + + } while(!closed); + return current; +} + +const callback = async (interaction: CommandInteraction) => { + + const m = await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true}) + const config = await client.database.guilds.read(interaction.guild!.id); + const tracks: ObjectSchema[] = config.tracks; + const roles = await interaction.guild!.roles.fetch(); + + let page = 0; + let closed = false; + let modified = false; + + do { + const embed = new EmojiEmbed() + .setTitle("Track Settings") + .setEmoji("TRACKS.ICON") + .setStatus("Success"); + const noTracks = config.tracks.length === 0; + let current: ObjectSchema; + + const pageSelect = new StringSelectMenuBuilder() + .setCustomId("page") + .setPlaceholder("Select a track to manage"); + const actionSelect = new StringSelectMenuBuilder() + .setCustomId("action") + .setPlaceholder("Perform an action") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Edit") + .setDescription("Edit this track") + .setValue("edit") + .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), + new StringSelectMenuOptionBuilder() + .setLabel("Delete") + .setDescription("Delete this track") + .setValue("delete") + .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji) + ); + const buttonRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) + .setDisabled(page === 0), + new ButtonBuilder() + .setCustomId("next") + .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Primary) + .setDisabled(page === tracks.length - 1), + new ButtonBuilder() + .setCustomId("add") + .setLabel("New Track") + .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Secondary) + .setDisabled(Object.keys(tracks).length >= 24), + new ButtonBuilder() + .setCustomId("save") + .setLabel("Save") + .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Success) + .setDisabled(!modified), + ); + if(noTracks) { + embed.setDescription("No tracks have been set up yet. Use the button below to add one.\n\n" + + createPageIndicator(1, 1, undefined, true) + ); + pageSelect.setDisabled(true); + actionSelect.setDisabled(true); + pageSelect.addOptions(new StringSelectMenuOptionBuilder() + .setLabel("No tracks") + .setValue("none") + ); + } else { + page = Math.min(page, Object.keys(tracks).length - 1); + current = tracks[page]!; + const mapped = current.track.map(role => roles.find(aRole => aRole.id === role)!); + embed.setDescription(`**Currently Editing:** ${current.name}\n\n` + + `${getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"))} Members ${current.nullable ? "don't " : ""}need a role in this track\n` + + `${getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"))} Members ${current.retainPrevious ? "" : "don't "}keep all roles below their current highest\n\n` + + createVerticalTrack(mapped.map(role => renderRole(role)), new Array(current.track.length).fill(false)) + + `\n${createPageIndicator(config.tracks.length, page)}` + ); + + pageSelect.addOptions( + tracks.map((key: ObjectSchema, index) => { + return new StringSelectMenuOptionBuilder() + .setLabel(ellipsis(key.name, 50)) + .setDescription(ellipsis(roles.get(key.track[0]!)?.name!, 50)) + .setValue(index.toString()); + }) + ); + + } + + await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder().addComponents(actionSelect), new ActionRowBuilder().addComponents(pageSelect), buttonRow]}); + let i: StringSelectMenuInteraction | ButtonInteraction; + try { + i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | StringSelectMenuInteraction; + } catch (e) { + closed = true; + continue; + } + + await i.deferUpdate(); + if (i.isButton()) { + switch (i.customId) { + case "back": { + page--; + break; + } + case "next": { + page++; + break; + } + case "add": { + const newPage = await editTrack(i, m, roles) + if(!newPage) break; + tracks.push(); + page = tracks.length - 1; + break; + } + case "save": { + client.database.guilds.write(interaction.guild!.id, {tracks: tracks}); + modified = false; + break; + } + } + } else if (i.isStringSelectMenu()) { + switch (i.customId) { + case "action": { + switch(i.values[0]) { + case "edit": { + const edited = await editTrack(i, m, roles, current!); + if(!edited) break; + tracks[page] = edited; + modified = true; + break; + } + case "delete": { + if(page === 0 && tracks.keys.length - 1 > 0) page++; + else page--; + tracks.splice(page, 1); + break; + } + } + break; + } + case "page": { + page = parseInt(i.values[0]!); + break; + } + } + } + + } while (!closed); + await interaction.deleteReply() +} + +const check = (interaction: CommandInteraction, _partial: boolean = false) => { + const member = interaction.member as GuildMember; + if (!member.permissions.has("ManageRoles")) + return "You must have the *Manage Server* permission to use this command"; + return true; +}; + +export { command }; +export { callback }; +export { check }; diff --git a/src/commands/settings/verify.ts b/src/commands/settings/verify.ts index 0f9f4a0..c440b75 100644 --- a/src/commands/settings/verify.ts +++ b/src/commands/settings/verify.ts @@ -1,35 +1,25 @@ import { LoadingEmbed } from "../../utils/defaults.js"; import Discord, { CommandInteraction, - Interaction, Message, ActionRowBuilder, ButtonBuilder, - MessageComponentInteraction, - ModalSubmitInteraction, - Role, ButtonStyle, - StringSelectMenuBuilder, - StringSelectMenuComponent, - TextInputBuilder, - EmbedBuilder, - StringSelectMenuInteraction, - ButtonComponent + RoleSelectMenuBuilder, + APIMessageComponentEmoji } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; -import confirmationMessage from "../../utils/confirmationMessage.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import client from "../../utils/client.js"; -import { modalInteractionCollector } from "../../utils/dualCollector.js"; +import { getCommandMentionByName } from "../../utils/getCommandDataByName.js"; +import lodash from "lodash"; const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("verify") - .setDescription("Manage the role given after typing /verify") - .addRoleOption((option) => - option.setName("role").setDescription("The role to give after verifying").setRequired(false) - ); + .setDescription("Manage the role given after a user runs /verify") + const callback = async (interaction: CommandInteraction): Promise => { if (!interaction.guild) return; @@ -38,356 +28,82 @@ const callback = async (interaction: CommandInteraction): Promise => { ephemeral: true, fetchReply: true })) as Message; - if (interaction.options.get("role")?.role) { - let role: Role; - try { - role = interaction.options.get("role")?.role as Role; - } catch { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("GUILD.ROLES.DELETE") - .setTitle("Verify Role") - .setDescription("The role you provided is not a valid role") - .setStatus("Danger") - ] - }); - } - role = role as Discord.Role; - if (role.guild.id !== interaction.guild.id) { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Verify Role") - .setDescription("You must choose a role in this server") - .setStatus("Danger") - .setEmoji("GUILD.ROLES.DELETE") - ] - }); - } - const confirmation = await new confirmationMessage(interaction) - .setEmoji("GUILD.ROLES.EDIT") + + let closed = false; + let config = await client.database.guilds.read(interaction.guild.id); + let data = Object.assign({}, config.verify); + do { + const selectMenu = new ActionRowBuilder() + .addComponents( + new RoleSelectMenuBuilder() + .setCustomId("role") + .setPlaceholder("Select a role") + ); + + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("switch") + .setLabel(data.enabled ? "Enabled" : "Disabled") + .setStyle(data.enabled ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(getEmojiByName(data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("save") + .setLabel("Save") + .setStyle(ButtonStyle.Success) + .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji) + .setDisabled(lodash.isEqual(config.verify, data)) + ); + + const embed = new EmojiEmbed() .setTitle("Verify Role") - .setDescription(`Are you sure you want to set the verify role to <@&${role.id}>?`) - .setColor("Warning") - .setFailedMessage("No changes were made", "Warning", "GUILD.ROLES.DELETE") - .setInverted(true) - .send(true); - if (confirmation.cancelled) return; - if (confirmation.success) { - try { - await client.database.guilds.write(interaction.guild.id, { - "verify.role": role.id, - "verify.enabled": true - }); - const { log, NucleusColors, entry, renderUser, renderRole } = client.logger; - const data = { - meta: { - type: "verifyRoleChanged", - displayName: "Verify Role Changed", - calculateType: "nucleusSettingsUpdated", - color: NucleusColors.green, - emoji: "CONTROL.BLOCKTICK", - timestamp: new Date().getTime() - }, - list: { - memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), - changedBy: entry(interaction.user.id, renderUser(interaction.user)), - role: entry(role.id, renderRole(role)) - }, - hidden: { - guild: interaction.guild.id - } - }; - log(data); - } catch (e) { - console.log(e); - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Verify Role") - .setDescription("Something went wrong while setting the verify role") - .setStatus("Danger") - .setEmoji("GUILD.ROLES.DELETE") - ], - components: [] - }); - } - } else { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Verify Role") - .setDescription("No changes were made") - .setStatus("Success") - .setEmoji("GUILD.ROLES.CREATE") - ], - components: [] - }); - } - } - let clicks = 0; - const data = await client.database.guilds.read(interaction.guild.id); - let role = data.verify.role; + .setDescription( + `Select a role to be given to users after they run ${getCommandMentionByName("verify")}` + + `\n\nCurrent role: ${config.verify.role ? `<@&${config.verify.role}>` : "None"}` + ) + .setStatus("Success") + .setEmoji("CHANNEL.TEXT.CREATE"); - let timedOut = false; - while (!timedOut) { await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Verify Role") - .setDescription( - role ? `Your verify role is currently set to <@&${role}>` : "You have not set a verify role" - ) - .setStatus("Success") - .setEmoji("GUILD.ROLES.CREATE") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId("clear") - .setLabel(clicks ? "Click again to confirm" : "Reset role") - .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id")) - .setStyle(ButtonStyle.Danger) - .setDisabled(!role), - new ButtonBuilder() - .setCustomId("send") - .setLabel("Add verify button") - .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id")) - .setStyle(ButtonStyle.Primary) - ]) - ] + embeds: [embed], + components: [selectMenu, buttons] }); - let i: MessageComponentInteraction; + + let i; try { i = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id } }); } catch (e) { - timedOut = true; + closed = true; continue; } - i.deferUpdate(); - if ((i.component as ButtonComponent).customId === "clear") { - clicks += 1; - if (clicks === 2) { - clicks = 0; - await client.database.guilds.write(interaction.guild.id, null, ["verify.role", "verify.enabled"]); - role = null; - } - } else if ((i.component as ButtonComponent).customId === "send") { - const verifyMessages = [ - { - label: "Verify", - description: "Click the button below to get verified" - }, - { - label: "Get verified", - description: "To get access to the rest of the server, click the button below" - }, - { - label: "Ready to verify?", - description: "Click the button below to verify yourself" - } - ]; - let innerTimedOut = false; - let templateSelected = false; - while (!innerTimedOut && !templateSelected) { - await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Verify Button") - .setDescription("Select a message template to send in this channel") - .setFooter({ - text: role ? "" : "You do no have a verify role set so the button will not work." - }) - .setStatus(role ? "Success" : "Warning") - .setEmoji("GUILD.ROLES.CREATE") - ], - components: [ - new ActionRowBuilder().addComponents([ - new StringSelectMenuBuilder() - .setOptions( - verifyMessages.map( - ( - t: { - label: string; - description: string; - value?: string; - }, - index - ) => { - t.value = index.toString(); - return t as { - value: string; - label: string; - description: string; - }; - } - ) - ) - .setCustomId("template") - .setMaxValues(1) - .setMinValues(1) - .setPlaceholder("Select a message template") - ]), - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId("back") - .setLabel("Back") - .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) - .setStyle(ButtonStyle.Danger), - new ButtonBuilder().setCustomId("blank").setLabel("Empty").setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId("custom") - .setLabel("Custom") - .setEmoji(getEmojiByName("TICKETS.OTHER", "id")) - .setStyle(ButtonStyle.Primary) - ]) - ] - }); - let i: MessageComponentInteraction; - try { - i = await m.awaitMessageComponent({ - time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } - }); - } catch (e) { - innerTimedOut = true; - continue; + + await i.deferUpdate(); + + if(i.isButton()) { + switch (i.customId) { + case "save": { + client.database.guilds.write(interaction.guild.id, {"verify": data} ) + config = await client.database.guilds.read(interaction.guild.id); + data = Object.assign({}, config.verify); + break } - if ((i.component as StringSelectMenuComponent).customId === "template") { - i.deferUpdate(); - await interaction.channel!.send({ - embeds: [ - new EmojiEmbed() - .setTitle(verifyMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.label) - .setDescription( - verifyMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.description - ) - .setStatus("Success") - .setEmoji("CONTROL.BLOCKTICK") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel("Verify") - .setEmoji(getEmojiByName("CONTROL.TICK", "id")) - .setStyle(ButtonStyle.Success) - .setCustomId("verifybutton") - ]) - ] - }); - templateSelected = true; - continue; - } else if ((i.component as ButtonComponent).customId === "blank") { - i.deferUpdate(); - await interaction.channel!.send({ - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel("Verify") - .setEmoji(getEmojiByName("CONTROL.TICK", "id")) - .setStyle(ButtonStyle.Success) - .setCustomId("verifybutton") - ]) - ] - }); - templateSelected = true; - continue; - } else if ((i.component as ButtonComponent).customId === "custom") { - await i.showModal( - new Discord.ModalBuilder() - .setCustomId("modal") - .setTitle("Enter embed details") - .addComponents( - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId("title") - .setLabel("Title") - .setMaxLength(256) - .setRequired(true) - .setStyle(Discord.TextInputStyle.Short) - ), - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId("description") - .setLabel("Description") - .setMaxLength(4000) - .setRequired(true) - .setStyle(Discord.TextInputStyle.Paragraph) - ) - ) - ); - await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Verify Button") - .setDescription("Modal opened. If you can't see it, click back and try again.") - .setStatus("Success") - .setEmoji("GUILD.TICKET.OPEN") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel("Back") - .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) - .setStyle(ButtonStyle.Primary) - .setCustomId("back") - ]) - ] - }); - let out; - try { - out = await modalInteractionCollector( - m, - (m: Interaction) => - (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === - interaction.channelId, - (m) => m.customId === "modify" - ); - } catch (e) { - innerTimedOut = true; - continue; - } - if (out !== null && out instanceof ModalSubmitInteraction) { - const title = out.fields.getTextInputValue("title"); - const description = out.fields.getTextInputValue("description"); - await interaction.channel!.send({ - embeds: [ - new EmojiEmbed() - .setTitle(title) - .setDescription(description) - .setStatus("Success") - .setEmoji("CONTROL.BLOCKTICK") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel("Verify") - .setEmoji(getEmojiByName("CONTROL.TICK", "id")) - .setStyle(ButtonStyle.Success) - .setCustomId("verifybutton") - ]) - ] - }); - templateSelected = true; - } + case "switch": { + data.enabled = !data.enabled; + break } } } else { - i.deferUpdate(); - break; + data.role = i.values[0]!; } - } - await interaction.editReply({ - embeds: [new EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message closed" })], - components: [] - }); + + } while (!closed); + await interaction.deleteReply() }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as Discord.GuildMember; if (!member.permissions.has("ManageGuild")) return "You must have the *Manage Server* permission to use this command"; diff --git a/src/commands/settings/welcome.ts b/src/commands/settings/welcome.ts index e7143fb..7584624 100644 --- a/src/commands/settings/welcome.ts +++ b/src/commands/settings/welcome.ts @@ -1,307 +1,263 @@ import { LoadingEmbed } from "../../utils/defaults.js"; import Discord, { - Channel, CommandInteraction, - Message, + AutocompleteInteraction, ActionRowBuilder, ButtonBuilder, - MessageComponentInteraction, - Role, ButtonStyle, - AutocompleteInteraction, - GuildChannel, - EmbedBuilder + APIMessageComponentEmoji, + ChannelSelectMenuBuilder, + RoleSelectMenuBuilder, + RoleSelectMenuInteraction, + ChannelSelectMenuInteraction, + ButtonInteraction, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + ModalSubmitInteraction, } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import client from "../../utils/client.js"; -import confirmationMessage from "../../utils/confirmationMessage.js"; -import generateKeyValueList from "../../utils/generateKeyValueList.js"; -import { ChannelType } from "discord-api-types/v9"; import getEmojiByName from "../../utils/getEmojiByName.js"; +import convertCurlyBracketString from "../../utils/convertCurlyBracketString.js"; +import { modalInteractionCollector } from "../../utils/dualCollector.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("welcome") .setDescription("Messages and roles sent or given when someone joins the server") - .addStringOption((option) => - option - .setName("message") - .setDescription("The message to send when someone joins the server") - .setAutocomplete(true) - ) - .addRoleOption((option) => - option.setName("role").setDescription("The role given when someone joins the server") - ) - .addRoleOption((option) => - option.setName("ping").setDescription("The role pinged when someone joins the server") - ) - .addChannelOption((option) => - option - .setName("channel") - .setDescription("The channel the welcome message should be sent to") - .addChannelTypes(ChannelType.GuildText) - ); -const callback = async (interaction: CommandInteraction): Promise => { - const { renderRole, renderChannel, log, NucleusColors, entry, renderUser } = client.logger; - await interaction.reply({ +const callback = async (interaction: CommandInteraction): Promise => { + const { renderChannel } = client.logger; + const m = await interaction.reply({ embeds: LoadingEmbed, fetchReply: true, ephemeral: true }); - let m: Message; - if ( - interaction.options.get("role")?.role || - interaction.options.get("channel")?.channel || - interaction.options.get("message")?.value as string - ) { - let role: Role | null; - let ping: Role | null; - let channel: Channel | null; - const message: string | null = interaction.options.get("message")?.value as string | null; - try { - role = interaction.options.get("role")?.role as Role | null; - ping = interaction.options.get("ping")?.role as Role | null; - } catch { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("GUILD.ROLES.DELETE") - .setTitle("Welcome Events") - .setDescription("The role you provided is not a valid role") - .setStatus("Danger") - ] - }); - } - try { - channel = interaction.options.get("channel")?.channel as Channel | null; - } catch { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("GUILD.ROLES.DELETE") - .setTitle("Welcome Events") - .setDescription("The channel you provided is not a valid channel") - .setStatus("Danger") - ] - }); - } - const options: { - role?: string; - ping?: string; - channel?: string; - message?: string; - } = {}; - - if (role) options.role = renderRole(role); - if (ping) options.ping = renderRole(ping); - if (channel) options.channel = renderChannel(channel as GuildChannel); - if (message) options.message = "\n> " + message; - const confirmation = await new confirmationMessage(interaction) - .setEmoji("GUILD.ROLES.EDIT") - .setTitle("Welcome Events") - .setDescription(generateKeyValueList(options)) - .setColor("Warning") - .setFailedMessage("Cancelled", "Warning", "GUILD.ROLES.DELETE") //TODO: Actual Message Needed - .setInverted(true) - .send(true); - if (confirmation.cancelled) return; - if (confirmation.success) { - try { - const toChange: { - "welcome.role"?: string; - "welcome.ping"?: string; - "welcome.channel"?: string; - "welcome.message"?: string; - } = {}; - if (role) toChange["welcome.role"] = role.id; - if (ping) toChange["welcome.ping"] = ping.id; - if (channel) toChange["welcome.channel"] = channel.id; - if (message) toChange["welcome.message"] = message; - await client.database.guilds.write(interaction.guild!.id, toChange); - const list: { - memberId: ReturnType; - changedBy: ReturnType; - role?: ReturnType; - ping?: ReturnType; - channel?: ReturnType; - message?: ReturnType; - } = { - memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), - changedBy: entry(interaction.user.id, renderUser(interaction.user)) - }; - if (role) list.role = entry(role.id, renderRole(role)); - if (ping) list.ping = entry(ping.id, renderRole(ping)); - if (channel) list.channel = entry(channel.id, renderChannel(channel as GuildChannel)); - if (message) list.message = entry(message, `\`${message}\``); - const data = { - meta: { - type: "welcomeSettingsUpdated", - displayName: "Welcome Settings Changed", - calculateType: "nucleusSettingsUpdated", - color: NucleusColors.green, - emoji: "CONTROL.BLOCKTICK", - timestamp: new Date().getTime() - }, - list: list, - hidden: { - guild: interaction.guild!.id - } - }; - log(data); - } catch (e) { - console.log(e); - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Welcome Events") - .setDescription("Something went wrong while updating welcome settings") - .setStatus("Danger") - .setEmoji("GUILD.ROLES.DELETE") - ], - components: [] - }); - } - } else { - return interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Welcome Events") - .setDescription("No changes were made") - .setStatus("Success") - .setEmoji("GUILD.ROLES.CREATE") - ], - components: [] - }); - } - } - let lastClicked = null; - let timedOut = false; + let closed = false; + let config = await client.database.guilds.read(interaction.guild!.id); + let data = Object.assign({}, config.welcome); do { - const config = await client.database.guilds.read(interaction.guild!.id); - m = (await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Welcome Events") - .setDescription( - `**Message:** ${config.welcome.message ? `\n> ${config.welcome.message}` : "*None set*"}\n` + - `**Role:** ${ - config.welcome.role - ? renderRole((await interaction.guild!.roles.fetch(config.welcome.role))!) - : "*None set*" - }\n` + - `**Ping:** ${ - config.welcome.ping - ? renderRole((await interaction.guild!.roles.fetch(config.welcome.ping))!) - : "*None set*" - }\n` + - `**Channel:** ${ - config.welcome.channel - ? config.welcome.channel == "dm" - ? "DM" - : renderChannel((await interaction.guild!.channels.fetch(config.welcome.channel))!) - : "*None set*" - }` + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("switch") + .setLabel(data.enabled ? "Enabled" : "Disabled") + .setStyle(data.enabled ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(getEmojiByName(data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("message") + .setLabel((data.message ? "Change" : "Set") + "Message") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("channelDM") + .setLabel("Send in DMs") + .setStyle(ButtonStyle.Primary) + .setDisabled(data.channel === "dm"), + new ButtonBuilder() + .setCustomId("role") + .setLabel("Clear Role") + .setStyle(ButtonStyle.Danger) + .setEmoji(getEmojiByName("CONTROL.CROSS", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("save") + .setLabel("Save") + .setStyle(ButtonStyle.Success) + .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji) + .setDisabled( + data.enabled === config.welcome.enabled && + data.message === config.welcome.message && + data.role === config.welcome.role && + data.ping === config.welcome.ping && + data.channel === config.welcome.channel ) - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel(lastClicked == "clear-message" ? "Click again to confirm" : "Clear Message") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setCustomId("clear-message") - .setDisabled(!config.welcome.message) - .setStyle(ButtonStyle.Danger), - new ButtonBuilder() - .setLabel(lastClicked == "clear-role" ? "Click again to confirm" : "Clear Role") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setCustomId("clear-role") - .setDisabled(!config.welcome.role) - .setStyle(ButtonStyle.Danger), - new ButtonBuilder() - .setLabel(lastClicked == "clear-ping" ? "Click again to confirm" : "Clear Ping") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setCustomId("clear-ping") - .setDisabled(!config.welcome.ping) - .setStyle(ButtonStyle.Danger), - new ButtonBuilder() - .setLabel(lastClicked == "clear-channel" ? "Click again to confirm" : "Clear Channel") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setCustomId("clear-channel") - .setDisabled(!config.welcome.channel) - .setStyle(ButtonStyle.Danger), - new ButtonBuilder() - .setLabel("Set Channel to DM") - .setCustomId("set-channel-dm") - .setDisabled(config.welcome.channel == "dm") - .setStyle(ButtonStyle.Secondary) - ]) - ] - })) as Message; - let i: MessageComponentInteraction; + ); + + const channelMenu = new ActionRowBuilder() + .addComponents( + new ChannelSelectMenuBuilder() + .setCustomId("channel") + .setPlaceholder("Select a channel to send welcome messages to") + ); + const roleMenu = new ActionRowBuilder() + .addComponents( + new RoleSelectMenuBuilder() + .setCustomId("roleToGive") + .setPlaceholder("Select a role to give to the member when they join the server") + ); + const pingMenu = new ActionRowBuilder() + .addComponents( + new RoleSelectMenuBuilder() + .setCustomId("roleToPing") + .setPlaceholder("Select a role to ping when a member joins the server") + ); + + const embed = new EmojiEmbed() + .setTitle("Welcome Settings") + .setStatus("Success") + .setDescription( + `${getEmojiByName(data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Welcome messages and roles are ${data.enabled ? "enabled" : "disabled"}\n` + + `**Welcome message:** ${data.message ? + `\n> ` + + await convertCurlyBracketString( + data.message, + interaction.user.id, + interaction.user.username, + interaction.guild!.name, + interaction.guild!.members + ) + : "*None*"}\n` + + `**Send message in:** ` + (data.channel ? (data.channel == "dm" ? "DMs" : renderChannel(data.channel)) : `*None set*`) + `\n` + + `**Role to ping:** ` + (data.ping ? `<@&${data.ping}>` : `*None set*`) + `\n` + + `**Role given on join:** ` + (data.role ? `<@&${data.role}>` : `*None set*`) + ) + + await interaction.editReply({ + embeds: [embed], + components: [buttons, channelMenu, roleMenu, pingMenu] + }); + + let i: RoleSelectMenuInteraction | ChannelSelectMenuInteraction | ButtonInteraction; try { i = await m.awaitMessageComponent({ - time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } - }); + filter: (interaction) => interaction.user.id === interaction.user.id, + time: 300000 + }) as RoleSelectMenuInteraction | ChannelSelectMenuInteraction | ButtonInteraction; } catch (e) { - timedOut = true; + closed = true; continue; } - i.deferUpdate(); - if (i.customId == "clear-message") { - if (lastClicked == "clear-message") { - await client.database.guilds.write(interaction.guild!.id, { - "welcome.message": null - }); - lastClicked = null; - } else { - lastClicked = "clear-message"; - } - } else if (i.customId == "clear-role") { - if (lastClicked == "clear-role") { - await client.database.guilds.write(interaction.guild!.id, { - "welcome.role": null - }); - lastClicked = null; - } else { - lastClicked = "clear-role"; - } - } else if (i.customId == "clear-ping") { - if (lastClicked == "clear-ping") { - await client.database.guilds.write(interaction.guild!.id, { - "welcome.ping": null - }); - lastClicked = null; - } else { - lastClicked = "clear-ping"; + + if(i.isButton()) { + switch(i.customId) { + case "switch": { + await i.deferUpdate(); + data.enabled = !data.enabled; + break; + } + case "message": { + const modal = new ModalBuilder() + .setCustomId("modal") + .setTitle("Welcome Message") + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("ex1") + .setLabel("Server Info (1/3)") + .setPlaceholder( + `{serverName} - This server's name\n\n` + + `These placeholders will be replaced with the server's name, etc..` + ) + .setMaxLength(1) + .setRequired(false) + .setStyle(TextInputStyle.Paragraph) + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("ex2") + .setLabel("Member Counts (2/3) - {MemberCount:...}") + .setPlaceholder( + `{:all} - Total member count\n` + + `{:humans} - Total non-bot users\n` + + `{:bots} - Number of bots\n` + ) + .setMaxLength(1) + .setRequired(false) + .setStyle(TextInputStyle.Paragraph) + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("ex3") + .setLabel("Member who joined (3/3) - {member:...}") + .setPlaceholder( + `{:name} - The members name\n` + ) + .setMaxLength(1) + .setRequired(false) + .setStyle(TextInputStyle.Paragraph) + ), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId("message") + .setPlaceholder("Enter a message to send when someone joins the server") + .setValue(data.message ?? "") + .setLabel("Message") + .setStyle(TextInputStyle.Paragraph) + ) + ) + const button = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) + ) + await i.showModal(modal) + await i.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Welcome Settings") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + ], + components: [button] + }); + + let out: ModalSubmitInteraction | null; + try { + out = await modalInteractionCollector(m, interaction.user) as ModalSubmitInteraction | null; + } catch (e) { + console.error(e); + out = null; + } + if(!out) break; + data.message = out.fields.getTextInputValue("message"); + break; + } + case "save": { + await i.deferUpdate(); + await client.database.guilds.write(interaction.guild!.id, {"welcome": data}); + config = await client.database.guilds.read(interaction.guild!.id); + data = Object.assign({}, config.welcome); + break; + } + case "channelDM": { + await i.deferUpdate(); + data.channel = "dm"; + break; + } + case "role": { + await i.deferUpdate(); + data.role = null; + break; + } } - } else if (i.customId == "clear-channel") { - if (lastClicked == "clear-channel") { - await client.database.guilds.write(interaction.guild!.id, { - "welcome.channel": null - }); - lastClicked = null; - } else { - lastClicked = "clear-channel"; + } else if (i.isRoleSelectMenu()) { + await i.deferUpdate(); + switch(i.customId) { + case "roleToGive": { + data.role = i.values[0]!; + break + } + case "roleToPing": { + data.ping = i.values[0]!; + break + } } - } else if (i.customId == "set-channel-dm") { - await client.database.guilds.write(interaction.guild!.id, { - "welcome.channel": "dm" - }); - lastClicked = null; + } else { + await i.deferUpdate(); + data.channel = i.values[0]!; } - } while (!timedOut); - await interaction.editReply({ - embeds: [new EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message timed out" })], - components: [] - }); + + } while (!closed); + await interaction.deleteReply() }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as Discord.GuildMember; if (!member.permissions.has("ManageGuild")) return "You must have the *Manage Server* permission to use this command"; @@ -309,7 +265,7 @@ const check = (interaction: CommandInteraction) => { }; const autocomplete = async (interaction: AutocompleteInteraction): Promise => { - const validReplacements = ["serverName", "memberCount", "memberCount:bots", "memberCount:humans"] + const validReplacements = ["serverName", "memberCount:all", "memberCount:bots", "memberCount:humans"] if (!interaction.guild) return []; const prompt = interaction.options.getString("message"); const autocompletions = []; @@ -324,7 +280,7 @@ const autocomplete = async (interaction: AutocompleteInteraction): Promise => { return; }; -const check = () => { - return true; -}; - const autocomplete = async (interaction: AutocompleteInteraction): Promise => { if (!interaction.guild) return []; const prompt = interaction.options.getString("tag"); @@ -65,5 +60,4 @@ const autocomplete = async (interaction: AutocompleteInteraction): Promise => { }); }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as Discord.GuildMember; if (!member.permissions.has("ManageMessages")) return "You must have the *Manage Messages* permission to use this command"; diff --git a/src/commands/tags/delete.ts b/src/commands/tags/delete.ts index 18143d3..4fdb10f 100644 --- a/src/commands/tags/delete.ts +++ b/src/commands/tags/delete.ts @@ -1,6 +1,6 @@ import type Discord from "discord.js"; import type { CommandInteraction } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import keyValueList from "../../utils/generateKeyValueList.js"; @@ -68,7 +68,7 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as Discord.GuildMember; if (!member.permissions.has("ManageMessages")) return "You must have the *Manage Messages* permission to use this command"; diff --git a/src/commands/tags/edit.ts b/src/commands/tags/edit.ts index e15f9ac..7e297c8 100644 --- a/src/commands/tags/edit.ts +++ b/src/commands/tags/edit.ts @@ -1,5 +1,5 @@ import type { CommandInteraction, GuildMember } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import keyValueList from "../../utils/generateKeyValueList.js"; @@ -19,9 +19,9 @@ const command = (builder: SlashCommandSubcommandBuilder) => const callback = async (interaction: CommandInteraction): Promise => { if (!interaction.guild) return; - const name = interaction.options.get("name")?.value as string; - const value = interaction.options.get("value")?.value as string; - const newname = interaction.options.get("newname")?.value as string; + const name = (interaction.options.get("name")?.value ?? "") as string; + const value = (interaction.options.get("value")?.value ?? "") as string; + const newname = (interaction.options.get("newname")?.value ?? "") as string; if (!newname && !value) return await interaction.reply({ embeds: [ @@ -126,7 +126,7 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = (interaction: CommandInteraction) => { +const check = (interaction: CommandInteraction, _partial: boolean = false) => { const member = interaction.member as GuildMember; if (!member.permissions.has("ManageMessages")) return "You must have the *Manage Messages* permission to use this command"; diff --git a/src/commands/tags/list.ts b/src/commands/tags/list.ts index f0563c7..dbb1200 100644 --- a/src/commands/tags/list.ts +++ b/src/commands/tags/list.ts @@ -10,7 +10,7 @@ import Discord, { ButtonComponent, StringSelectMenuBuilder } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import client from "../../utils/client.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; @@ -139,13 +139,13 @@ const callback = async (interaction: CommandInteraction): Promise => { try { i = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id } }); } catch (e) { timedOut = true; continue; } - i.deferUpdate(); + await i.deferUpdate(); if ((i.component as ButtonComponent).customId === "left") { if (page > 0) page--; selectPaneOpen = false; @@ -173,10 +173,6 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = () => { - return true; -}; export { command }; export { callback }; -export { check }; diff --git a/src/commands/ticket/close.ts b/src/commands/ticket/close.ts index d2ffaf9..ff9da8b 100644 --- a/src/commands/ticket/close.ts +++ b/src/commands/ticket/close.ts @@ -1,5 +1,5 @@ import type { CommandInteraction } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import close from "../../actions/tickets/delete.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder.setName("close").setDescription("Closes a ticket"); @@ -8,10 +8,5 @@ const callback = async (interaction: CommandInteraction): Promise => { await close(interaction); }; -const check = () => { - return true; -}; - export { command }; export { callback }; -export { check }; diff --git a/src/commands/ticket/create.ts b/src/commands/ticket/create.ts index 91442b5..2f3ddc6 100644 --- a/src/commands/ticket/create.ts +++ b/src/commands/ticket/create.ts @@ -1,5 +1,5 @@ import type { CommandInteraction } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import create from "../../actions/tickets/create.js"; const command = (builder: SlashCommandSubcommandBuilder) => @@ -14,10 +14,6 @@ const callback = async (interaction: CommandInteraction): Promise => { await create(interaction); }; -const check = () => { - return true; -}; export { command }; export { callback }; -export { check }; diff --git a/src/commands/user/about.ts b/src/commands/user/about.ts index e43ecb7..0eb8580 100644 --- a/src/commands/user/about.ts +++ b/src/commands/user/about.ts @@ -10,7 +10,7 @@ import Discord, { APISelectMenuOption, StringSelectMenuBuilder } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import generateKeyValueList from "../../utils/generateKeyValueList.js"; @@ -173,11 +173,8 @@ async function userAbout(guild: Discord.Guild, member: Discord.GuildMember, inte generateKeyValueList({ member: renderUser(member.user), id: `\`${member.id}\``, - roles: `${member.roles.cache.size - 1}` // FIXME - }) + - "\n" + - (s.length > 0 ? s : "*None*") + - "\n" + roles: `${member.roles.cache.size - 1}` + }) + "\n" + (s.length > 0 ? s : "*None*") + "\n" ) ) .setTitle("Roles") @@ -258,13 +255,13 @@ async function userAbout(guild: Discord.Guild, member: Discord.GuildMember, inte try { i = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id } }); } catch { timedOut = true; continue; } - i.deferUpdate(); + await i.deferUpdate(); if (i.customId === "left") { if (page > 0) page--; selectPaneOpen = false; @@ -286,11 +283,6 @@ async function userAbout(guild: Discord.Guild, member: Discord.GuildMember, inte }); }; -const check = () => { - return true; -}; - export { command }; export { callback }; -export { check }; export { userAbout }; \ No newline at end of file diff --git a/src/commands/user/avatar.ts b/src/commands/user/avatar.ts index 88b3270..da33f51 100644 --- a/src/commands/user/avatar.ts +++ b/src/commands/user/avatar.ts @@ -1,6 +1,6 @@ import type { CommandInteraction } from "discord.js"; import type Discord from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import generateKeyValueList from "../../utils/generateKeyValueList.js"; import client from "../../utils/client.js"; @@ -35,10 +35,6 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = () => { - return true; -}; export { command }; export { callback }; -export { check }; diff --git a/src/commands/user/role.ts b/src/commands/user/role.ts new file mode 100644 index 0000000..41820ac --- /dev/null +++ b/src/commands/user/role.ts @@ -0,0 +1,160 @@ +import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, CommandInteraction, GuildMember, Role, RoleSelectMenuBuilder, RoleSelectMenuInteraction, UserSelectMenuBuilder, UserSelectMenuInteraction } from "discord.js"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; +import client from "../../utils/client.js"; +import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; +import { LoadingEmbed } from "../../utils/defaults.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; +import listToAndMore from "../../utils/listToAndMore.js" + +const { renderUser } = client.logger; + +const canEdit = (role: Role, member: GuildMember, me: GuildMember): [string, boolean] => { + if(role.position >= me.roles.highest.position || + role.position >= member.roles.highest.position + ) return [`~~<@&${role.id}>~~`, false]; + return [`<@&${role.id}>`, true]; +}; + +const command = (builder: SlashCommandSubcommandBuilder) => + builder + .setName("role") + .setDescription("Gives or removes a role from someone") + .addUserOption((option) => option.setName("user").setDescription("The user to give or remove the role from")) + +const callback = async (interaction: CommandInteraction): Promise => { + const m = await interaction.reply({ embeds: LoadingEmbed, fetchReply: true, ephemeral: true }); + + let member = interaction.options.getMember("user") as GuildMember | null; + + if(!member) { + const memberEmbed = new EmojiEmbed() + .setTitle("Role") + .setDescription(`Please choose a member to edit the roles of.`) + .setEmoji("GUILD.ROLES.CREATE") + .setStatus("Success"); + const memberChooser = new ActionRowBuilder().addComponents( + new UserSelectMenuBuilder() + .setCustomId("memberChooser") + .setPlaceholder("Select a member") + ); + await interaction.editReply({embeds: [memberEmbed], components: [memberChooser]}); + + const filter = (i: UserSelectMenuInteraction) => i.customId === "memberChooser" && i.user.id === interaction.user.id; + + let i: UserSelectMenuInteraction | null; + try { + i = await m.awaitMessageComponent<5>({ filter, time: 300000}); + } catch (e) { + return; + } + + memberEmbed.setDescription(`Editing roles for ${renderUser(i.values[0]!)}`); + await i.deferUpdate(); + await interaction.editReply({ embeds: LoadingEmbed, components: [] }) + member = await interaction.guild?.members.fetch(i.values[0]!)!; + + } + + let closed = false; + let rolesToChange: string[] = []; + const roleAdd = new ActionRowBuilder() + .addComponents( + new RoleSelectMenuBuilder() + .setCustomId("roleAdd") + .setPlaceholder("Select a role to add") + .setMaxValues(25) + ); + + do { + + const removing = rolesToChange.filter((r) => member!.roles.cache.has(r)).map((r) => canEdit(interaction.guild?.roles.cache.get(r)!, interaction.member as GuildMember, interaction.guild?.members.me!)[0]) + const adding = rolesToChange.filter((r) => !member!.roles.cache.has(r)).map((r) => canEdit(interaction.guild?.roles.cache.get(r)!, interaction.member as GuildMember, interaction.guild?.members.me!)[0]) + const embed = new EmojiEmbed() + .setTitle("Role") + .setDescription( + `${getEmojiByName("ICONS.EDIT")} Editing roles for <@${member.id}>\n\n` + + `Adding:\n` + + `${listToAndMore(adding.length > 0 ? adding : ["None"], 5)}\n` + + `Removing:\n` + + `${listToAndMore(removing.length > 0 ? removing : ["None"], 5)}\n` + ) + .setEmoji("GUILD.ROLES.CREATE") + .setStatus("Success"); + + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("roleSave") + .setLabel("Apply") + .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId("roleDiscard") + .setLabel("Reset") + .setEmoji(getEmojiByName("CONTROL.CROSS", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Danger) + ); + + await interaction.editReply({ embeds: [embed], components: [roleAdd, buttons] }); + + let i: RoleSelectMenuInteraction | ButtonInteraction | null; + try { + i = await m.awaitMessageComponent({ filter: (i) => i.user.id === interaction.user.id, time: 300000 }) as RoleSelectMenuInteraction | ButtonInteraction; + } catch (e) { + closed = true; + continue; + } + + i.deferUpdate(); + if(i.isButton()) { + switch(i.customId) { + case "roleSave": { + const roles = rolesToChange.map((r) => interaction.guild?.roles.cache.get(r)!); + await interaction.editReply({ embeds: LoadingEmbed, components: [] }); + const rolesToAdd: Role[] = []; + const rolesToRemove: Role[] = []; + for(const role of roles) { + if(!canEdit(role, interaction.member as GuildMember, interaction.guild?.members.me!)[1]) continue; + if(member.roles.cache.has(role.id)) { + rolesToRemove.push(role); + } else { + rolesToAdd.push(role); + } + } + await member.roles.add(rolesToAdd); + await member.roles.remove(rolesToRemove); + rolesToChange = []; + break; + } + case "roleDiscard": { + rolesToChange = []; + await interaction.editReply({ embeds: LoadingEmbed, components: [] }); + break; + } + } + } else { + rolesToChange = i.values; + } + + } while (!closed); + +}; + +const check = (interaction: CommandInteraction, partial: boolean = false) => { + const member = interaction.member as GuildMember; + // Check if the user has manage_roles permission + if (!member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission"; + if (partial) return true; + if (!interaction.guild) return + const me = interaction.guild.members.me!; + // Check if Nucleus has permission to role + if (!me.permissions.has("ManageRoles")) return "I do not have the *Manage Roles* permission"; + // Allow the owner to role anyone + if (member.id === interaction.guild.ownerId) return true; + // Allow role + return true; +}; + +export { command }; +export { callback }; +export { check }; diff --git a/src/commands/user/track.ts b/src/commands/user/track.ts index 0814cfa..c7f441f 100644 --- a/src/commands/user/track.ts +++ b/src/commands/user/track.ts @@ -1,10 +1,11 @@ import { LoadingEmbed } from "../../utils/defaults.js"; -import Discord, { CommandInteraction, GuildMember, Message, ActionRowBuilder, ButtonBuilder, ButtonStyle, SelectMenuOptionBuilder, APIMessageComponentEmoji, StringSelectMenuBuilder, MessageComponentInteraction, StringSelectMenuInteraction } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import Discord, { CommandInteraction, GuildMember, Message, ActionRowBuilder, ButtonBuilder, ButtonStyle, APIMessageComponentEmoji, StringSelectMenuBuilder, MessageComponentInteraction, StringSelectMenuInteraction, StringSelectMenuOptionBuilder } from "discord.js"; +import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import addPlural from "../../utils/plurals.js"; import client from "../../utils/client.js"; +import { createVerticalTrack } from "../../utils/createPageIndicator.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder @@ -12,17 +13,8 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setDescription("Moves a user along a role track") .addUserOption((option) => option.setName("user").setDescription("The user to manage").setRequired(true)); -const generateFromTrack = (position: number, active: string | boolean, size: number, disabled: string | boolean) => { - active = active ? "ACTIVE" : "INACTIVE"; - disabled = disabled ? "GREY." : ""; - if (position === 0 && size === 1) return "TRACKS.SINGLE." + disabled + active; - if (position === size - 1) return "TRACKS.VERTICAL.BOTTOM." + disabled + active; - if (position === 0) return "TRACKS.VERTICAL.TOP." + disabled + active; - return "TRACKS.VERTICAL.MIDDLE." + disabled + active; -}; - const callback = async (interaction: CommandInteraction): Promise => { - const { renderUser } = client.logger; + const { renderUser, renderRole} = client.logger; const member = interaction.options.getMember("user") as GuildMember; const guild = interaction.guild; if (!guild) return; @@ -44,10 +36,10 @@ const callback = async (interaction: CommandInteraction): Promise => { const dropdown = new Discord.StringSelectMenuBuilder() .addOptions( config.tracks.map((option, index) => { - const hasRoleInTrack = option.track.some((element: string) => { + const hasRoleInTrack: boolean = option.track.some((element: string) => { return memberRoles.cache.has(element); }); - return new SelectMenuOptionBuilder({ + return new StringSelectMenuOptionBuilder({ default: index === track, label: option.name, value: index.toString(), @@ -68,33 +60,23 @@ const callback = async (interaction: CommandInteraction): Promise => { (data.retainPrevious ? "When promoted, the user keeps previous roles" : "Members will lose their current role when promoted") + "\n"; - generated += - "\n" + - data.track - .map((role, index) => { - const allow: boolean = - roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position && - !managed; - allowed.push(!allow); - return ( - getEmojiByName( - generateFromTrack(index, memberRoles.cache.has(role), data.track.length, allow) - ) + - " " + - roles.get(role)!.name + - " [<@&" + - roles.get(role)!.id + - ">]" - ); - }) - .join("\n"); + for (const role of data.track) { + const disabled: boolean = + roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position && !managed; + allowed.push(!disabled) + } + generated += "\n" + createVerticalTrack( + data.track.map((role) => renderRole(roles.get(role)!)), + data.track.map((role) => memberRoles.cache.has(role)), + allowed.map((allow) => !allow) + ); const selected = []; for (const position of data.track) { if (memberRoles.cache.has(position)) selected.push(position); } const conflict = data.retainPrevious ? false : selected.length > 1; let conflictDropdown: StringSelectMenuBuilder[] = []; - const conflictDropdownOptions: SelectMenuOptionBuilder[] = []; + const conflictDropdownOptions: StringSelectMenuOptionBuilder[] = []; let currentRoleIndex: number = -1; if (conflict) { generated += `\n\n${getEmojiByName(`PUNISH.WARN.${managed ? "YELLOW" : "RED"}`)} This user has ${ @@ -106,10 +88,9 @@ const callback = async (interaction: CommandInteraction): Promise => { "In order to promote or demote this user, you must select which role the member should keep."; selected.forEach((role) => { conflictDropdownOptions.push( - new SelectMenuOptionBuilder({ - label: roles.get(role)!.name, - value: roles.get(role)!.id - }) + new StringSelectMenuOptionBuilder() + .setLabel(roles.get(role)!.name) + .setValue(roles.get(role)!.id) ); }); conflictDropdown = [ @@ -169,7 +150,7 @@ const callback = async (interaction: CommandInteraction): Promise => { try { component = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id } }); } catch (e) { timedOut = true; @@ -207,9 +188,9 @@ const callback = async (interaction: CommandInteraction): Promise => { } }; -const check = async (interaction: CommandInteraction) => { +const check = async (interaction: CommandInteraction, _partial: boolean = false) => { const tracks = (await client.database.guilds.read(interaction.guild!.id)).tracks; - if (tracks.length === 0) throw new Error("This server does not have any tracks"); + if (tracks.length === 0) return "This server does not have any tracks"; const member = interaction.member as GuildMember; // Allow the owner to promote anyone if (member.id === interaction.guild!.ownerId) return true; @@ -223,8 +204,7 @@ const check = async (interaction: CommandInteraction) => { break; } // Check if the user has manage_roles permission - if (!managed && !member.permissions.has("ManageRoles")) - throw new Error("You do not have the *Manage Roles* permission"); + if (!managed && !member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission"; // Allow track return true; }; diff --git a/src/commands/verify.ts b/src/commands/verify.ts index 4fafe69..0dd8b24 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -1,5 +1,5 @@ import type { CommandInteraction } from "discord.js"; -import { SlashCommandBuilder } from "@discordjs/builders"; +import { SlashCommandBuilder } from "discord.js"; import verify from "../reflex/verify.js"; const command = new SlashCommandBuilder().setName("verify").setDescription("Get verified in the server"); @@ -8,10 +8,5 @@ const callback = async (interaction: CommandInteraction): Promise => { verify(interaction); }; -const check = () => { - return true; -}; - export { command }; export { callback }; -export { check }; diff --git a/src/config/default.json b/src/config/default.json deleted file mode 100644 index 8e4197c..0000000 --- a/src/config/default.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "id": "default", - "version": 1, - "singleEventNotifications": { - "statsChannelDeleted": false - }, - "filters": { - "images": { - "NSFW": false, - "size": false - }, - "malware": false, - "wordFilter": { - "enabled": false, - "words": { - "strict": [], - "loose": [] - } - }, - "invite": { - "enabled": false, - "allowed": { - "users": [], - "roles": [], - "channels": [] - } - }, - "pings": { - "mass": 5, - "everyone": true, - "roles": true - } - }, - "welcome": { - "enabled": false, - "role": null, - "ping": null, - "channel": null, - "message": null - }, - "stats": {}, - "logging": { - "logs": { - "enabled": true, - "channel": null, - "toLog": "3fffff" - }, - "staff": { - "channel": null - }, - "attachments": { - "channel": null, - "saved": {} - } - }, - "verify": { - "enabled": false, - "role": null - }, - "tickets": { - "enabled": false, - "category": null, - "types": "3f", - "customTypes": null, - "useCustom": false, - "supportRole": null, - "maxTickets": 5 - }, - "moderation": { - "mute": { - "timeout": true, - "role": null, - "text": null, - "link": null - }, - "kick": { - "text": null, - "link": null - }, - "ban": { - "text": null, - "link": null - }, - "softban": { - "text": null, - "link": null - }, - "warn": { - "text": null, - "link": null - }, - "nickname": { - "text": null, - "link": null - } - }, - "tracks": [], - "roleMenu": [], - "tags": {} -} diff --git a/src/config/emojis.json b/src/config/emojis.json index 26d4c98..ecf1858 100644 --- a/src/config/emojis.json +++ b/src/config/emojis.json @@ -1,6 +1,8 @@ { "NUCLEUS": { "LOGO": "953040840945721385", + "PREMIUMACTIVATE": "a1067536222764925068", + "PREMIUM": "1067928702027042876", "LOADING": "a946346549271732234", "INFO": { "HELP": "751751467014029322", @@ -22,9 +24,11 @@ "FILTER": "990242059451514902", "ATTACHMENT": "997570687193587812", "LOGGING": "999613304446144562", + "SAVE": "1065722246322200586", + "REORDER": "1069323453909454890", "NOTIFY": { "ON": "1000726394579464232", - "OFF": "1000726363495477368" + "OFF": "1078058136092541008" }, "OPP": { "ADD": "837355918831124500", @@ -94,9 +98,8 @@ "TITLEUPDATE": "729763053620691044", "TOPICUPDATE": "729763053477953536", "SLOWMODE": { - "ON": "777138171301068831", - "OFF": "777138171447869480", - "// TODO": "Make these green and red respectively" + "ON": "973616021304913950", + "OFF": "777138171447869480" }, "NSFW": { "ON": "729064531208175736", @@ -210,6 +213,12 @@ "STOP": "853519660116738078" } }, + "SETTINGS": { + "STATS": { + "GREEN": "752214059159650396", + "RED": "1065677252630675556" + } + }, "GUILD": { "RED": "959779988264079361", "YELLOW": "729763053352124529", @@ -219,8 +228,10 @@ "EDIT": "729066518549233795", "DELETE": "953035210121953320" }, - "GRAPHS": "752214059159650396", - "SETTINGS": "752570111063228507", + "SETTINGS": { + "GREEN": "752570111063228507", + "RED": "1068607393728049253" + }, "ICONCHANGE": "729763053612302356", "TICKET": { "OPEN": "853245836331188264", @@ -338,7 +349,7 @@ "TOP": { "ACTIVE": "963122664648630293", "INACTIVE": "963122659862917140", - "GREY": { + "GRAY": { "ACTIVE": "963123505052934144", "INACTIVE": "963123495221469194" } @@ -346,7 +357,7 @@ "MIDDLE": { "ACTIVE": "963122679332880384", "INACTIVE": "963122673246937199", - "GREY": { + "GRAY": { "ACTIVE": "963123517702955018", "INACTIVE": "963123511927390329" } @@ -354,7 +365,7 @@ "BOTTOM": { "ACTIVE": "963122691752218624", "INACTIVE": "963122685691453552", - "GREY": { + "GRAY": { "ACTIVE": "963123529988059187", "INACTIVE": "963123523742748742" } @@ -363,10 +374,20 @@ "SINGLE": { "ACTIVE": "963361162215424060", "INACTIVE": "963361431758176316", - "GREY": { + "GRAY": { "ACTIVE": "963361204695334943", "INACTIVE": "963361200828198952" } } + }, + "COLORS": { + "RED": "875822912802803754", + "ORANGE": "875822913104785418", + "YELLOW": "875822913079611402", + "GREEN": "875822913213841418", + "BLUE": "875822912777637889", + "PURPLE": "875822913213841419", + "PINK": "875822913088020541", + "GRAY": "875822913117368340" } } diff --git a/src/config/format.ts b/src/config/format.ts index 713a233..e32bef6 100644 --- a/src/config/format.ts +++ b/src/config/format.ts @@ -1,8 +1,8 @@ + import fs from "fs"; -// @ts-expect-error import * as readLine from "node:readline/promises"; -const defaultDict: Record = { +const defaultDict: Record> = { developmentToken: "Your development bot token (Used for testing in one server, rather than production)", developmentGuildID: "Your development guild ID", enableDevelopment: true, @@ -15,7 +15,17 @@ const defaultDict: Record = { userContextFolder: "Your built user context folder (usually dist/context/users)", verifySecret: "If using verify, enter a code here which matches the secret sent back by your website. You can use a random code if you do not have one already. (Optional)", - mongoUrl: "Your Mongo connection string, e.g. mongodb://127.0.0.1:27017", + mongoUsername: "Your Mongo username (optional)", + mongoPassword: "Your Mongo password (optional)", + mongoDatabase: "Your Mongo database name (optional, e.g. Nucleus)", + mongoHost: "Your Mongo host (optional, e.g. localhost:27017)", + mongoOptions: { + username: "", + password: "", + database: "", + host: "", + authSource: "", + }, baseUrl: "Your website where buttons such as Verify and Role menu will link to, e.g. https://example.com/", pastebinApiKey: "An API key for pastebin (optional)", pastebinUsername: "Your pastebin username (optional)", @@ -51,18 +61,27 @@ export default async function (walkthrough = false) { // } } - let json; + let json: typeof defaultDict; let out = true; try { - json = JSON.parse(fs.readFileSync("./src/config/main.json", "utf8")); + json = await import("./main.js") as unknown as typeof defaultDict; } catch (e) { - console.log("\x1b[31m⚠ No main.json found, creating one."); - console.log(" \x1b[2mYou can edit src/config/main.json directly using template written to the file.\x1b[0m\n"); + console.log("\x1b[31m⚠ No main.ts found, creating one."); + console.log(" \x1b[2mYou can edit src/config/main.ts directly using template written to the file.\x1b[0m\n"); out = false; - json = {}; + json = {} as typeof defaultDict; + } + + if (Object.keys(json).length) { + if (json["token"] === defaultDict["token"] || json["developmentToken"] === defaultDict["developmentToken"]) { + console.log("\x1b[31m⚠ No main.ts found, creating one."); + console.log(" \x1b[2mYou can edit src/config/main.ts directly using template written to the file.\x1b[0m\n"); + json = {}; + } } + for (const key in defaultDict) { - if (!json[key]) { + if (Object.keys(json).includes(key)) { if (walkthrough) { switch (key) { case "enableDevelopment": { @@ -88,18 +107,20 @@ export default async function (walkthrough = false) { json[key] = toWrite; break; } + case "mongoOptions": { + break; + } default: { json[key] = await getInput(`\x1b[36m${key} \x1b[0m(\x1b[35m${defaultDict[key]}\x1b[0m) > `); } } } else { - json[key] = defaultDict[key]; + json[key] = defaultDict[key]!; } } } - if (walkthrough && !json.mongoUrl) json.mongoUrl = "mongodb://127.0.0.1:27017"; - if (!json.mongoUrl.endsWith("/")) json.mongoUrl += "/"; - if (!json.baseUrl.endsWith("/")) json.baseUrl += "/"; + if (walkthrough && !(json["mongoUrl"] ?? false)) json["mongoUrl"] = "mongodb://127.0.0.1:27017"; + if (!((json["baseUrl"] as string | undefined) ?? "").endsWith("/")) (json["baseUrl"] as string) += "/"; let hosts; try { hosts = fs.readFileSync("/etc/hosts", "utf8").toString().split("\n"); @@ -108,16 +129,23 @@ export default async function (walkthrough = false) { "\x1b[31m⚠ No /etc/hosts found. Please ensure the file exists and is readable. (Windows is not supported, Mac and Linux users should not experience this error)" ); } - let localhost = hosts.find((line) => line.split(" ")[1] === "localhost"); + let localhost: string | undefined = hosts.find((line) => line.split(" ")[1] === "localhost"); if (localhost) { localhost = localhost.split(" ")[0]; } else { localhost = "127.0.0.1"; } - json.mongoUrl = json.mongoUrl.replace("localhost", localhost); - json.baseUrl = json.baseUrl.replace("localhost", localhost); + json["mongoUrl"] = (json["mongoUrl"]! as string).replace("localhost", localhost!); + json["baseUrl"] = (json["baseUrl"]! as string).replace("localhost", localhost!); + json["mongoOptions"] = { + username: json["username"] as string, + password: json["password"] as string, + database: json["database"] as string, + host: json["host"] as string, + authSource: json["authSource"] as string, + }; - fs.writeFileSync("./src/config/main.json", JSON.stringify(json, null, 4)); + fs.writeFileSync("./src/config/main.ts", "export default " + JSON.stringify(json, null, 4) + ";"); if (walkthrough) { console.log("\x1b[32m✓ All properties added.\x1b[0m"); diff --git a/src/config/main.d.ts b/src/config/main.d.ts new file mode 100644 index 0000000..6549234 --- /dev/null +++ b/src/config/main.d.ts @@ -0,0 +1,26 @@ +declare const config: { + developmentToken: string, + developmentGuildID: string, + enableDevelopment: boolean, + token: string, + managementGuildID: string, + owners: string[], + commandsFolder: string, + eventsFolder: string, + messageContextFolder: string, + userContextFolder: string, + verifySecret: string, + mongoOptions: { + username: string, + password: string, + database: string, + host: string, + }, + baseUrl: string, + pastebinApiKey: string, + pastebinUsername: string, + pastebinPassword: string, + rapidApiKey: string +}; + +export default config; \ No newline at end of file diff --git a/src/config/main.json b/src/config/main.json deleted file mode 100644 index 64abe93..0000000 --- a/src/config/main.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - developmentToken: "Your development bot token (Used for testing in one server, rather than production)", - developmentGuildID: "Your development guild ID", - enableDevelopment: true, - token: "Your bot token", - managementGuildID: "Your management guild ID (Used for running management commands on the bot)", - owners: [], - commandsFolder: "Your built commands folder (usually dist/commands)", - eventsFolder: "Your built events folder (usually dist/events)", - messageContextFolder: "Your built message context folder (usually dist/context/messages)", - userContextFolder: "Your built user context folder (usually dist/context/users)", - verifySecret: - "If using verify, enter a code here which matches the secret sent back by your website. You can use a random code if you do not have one already. (Optional)", - mongoUrl: "Your Mongo connection string, e.g. mongodb://127.0.0.1:27017", - baseUrl: "Your website where buttons such as Verify and Role menu will link to, e.g. https://example.com/", - pastebinApiKey: "An API key for pastebin (optional)", - pastebinUsername: "Your pastebin username (optional)", - pastebinPassword: "Your pastebin password (optional)", - rapidApiKey: "Your RapidAPI key (optional), used for Unscan" -} diff --git a/src/context/messages/purgeto.ts b/src/context/messages/purgeto.ts index df52e0b..aef159b 100644 --- a/src/context/messages/purgeto.ts +++ b/src/context/messages/purgeto.ts @@ -1,10 +1,9 @@ import confirmationMessage from '../../utils/confirmationMessage.js'; import EmojiEmbed from '../../utils/generateEmojiEmbed.js'; import { LoadingEmbed } from '../../utils/defaults.js'; -import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContextMenuCommandBuilder, GuildTextBasedChannel, MessageContextMenuCommandInteraction } from "discord.js"; +import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContextMenuCommandBuilder, GuildMember, GuildTextBasedChannel, Message, MessageContextMenuCommandInteraction } from "discord.js"; import client from "../../utils/client.js"; -import getEmojiByName from '../../utils/getEmojiByName.js'; -import { JSONTranscriptFromMessageArray, JSONTranscriptToHumanReadable } from "../../utils/logTranscripts.js"; +import { messageException } from '../../utils/createTemporaryStorage.js'; const command = new ContextMenuCommandBuilder() .setName("Purge up to here") @@ -13,7 +12,7 @@ const command = new ContextMenuCommandBuilder() async function waitForButton(m: Discord.Message, member: Discord.GuildMember): Promise { let component; try { - component = m.awaitMessageComponent({ time: 200000, filter: (i) => i.user.id === member.id && i.channel!.id === m.channel.id }); + component = m.awaitMessageComponent({ time: 200000, filter: (i) => i.user.id === member.id && i.channel!.id === m.channel.id && i.message.id === m.id }); } catch (e) { return false; } @@ -171,7 +170,7 @@ const callback = async (interaction: MessageContextMenuCommandInteraction) => { calculateType: "messageDelete", color: NucleusColors.red, emoji: "PUNISH.BAN.RED", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), @@ -184,13 +183,18 @@ const callback = async (interaction: MessageContextMenuCommandInteraction) => { } }; log(data); - const transcript = JSONTranscriptToHumanReadable(JSONTranscriptFromMessageArray(deleted.map((m) => m as Discord.Message))!); - const attachmentObject = { - attachment: Buffer.from(transcript), - name: `purge-${channel.id}-${Date.now()}.txt`, - description: "Purge log" - }; - const m = (await interaction.editReply({ + const messages: Message[] = deleted.map(m => m).filter(m => m instanceof Message).map(m => m as Message); + if (messages.length === 1) messageException(interaction.guild!.id, interaction.channel.id, messages[0]!.id) + const messageArray: Message[] = messages.filter(message => !( + message!.components.some( + component => component.components.some( + child => child.customId?.includes("transcript") ?? false + ) + ) + )).map(message => message as Message); + const transcript = await client.database.transcripts.createTranscript(messageArray, interaction, interaction.member as GuildMember); + const code = await client.database.transcripts.create(transcript); + await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.GREEN") @@ -200,47 +204,10 @@ const callback = async (interaction: MessageContextMenuCommandInteraction) => { ], components: [ new Discord.ActionRowBuilder().addComponents([ - new Discord.ButtonBuilder() - .setCustomId("download") - .setLabel("Download transcript") - .setStyle(ButtonStyle.Success) - .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id")) + new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript?code=${code}`), ]) ] - })) as Discord.Message; - let component; - try { - component = await m.awaitMessageComponent({ - filter: (m) => m.user.id === interaction.user.id && m.channel!.id === interaction.channel!.id, - time: 300000 - }); - } catch { - return; - } - if (component.customId === "download") { - interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("CHANNEL.PURGE.GREEN") - .setTitle("Purge") - .setDescription("Transcript uploaded above") - .setStatus("Success") - ], - components: [], - files: [attachmentObject] - }); - } else { - interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("CHANNEL.PURGE.GREEN") - .setTitle("Purge") - .setDescription("Messages cleared") - .setStatus("Success") - ], - components: [] - }); - } + }); } const check = async (_interaction: MessageContextMenuCommandInteraction) => { diff --git a/src/context/users/userinfo.ts b/src/context/users/userinfo.ts index 3b1a6bd..496f84e 100644 --- a/src/context/users/userinfo.ts +++ b/src/context/users/userinfo.ts @@ -5,6 +5,7 @@ const command = new ContextMenuCommandBuilder() .setName("User info") const callback = async (interaction: UserContextMenuCommandInteraction) => { + console.log("callback") const guild = interaction.guild! let member = interaction.targetMember if (!member) member = await guild.members.fetch(interaction.targetId) @@ -12,6 +13,7 @@ const callback = async (interaction: UserContextMenuCommandInteraction) => { } const check = async (_interaction: UserContextMenuCommandInteraction) => { + console.log("check") return true; } diff --git a/src/events/channelCreate.ts b/src/events/channelCreate.ts index dda37af..b42ded7 100644 --- a/src/events/channelCreate.ts +++ b/src/events/channelCreate.ts @@ -4,7 +4,8 @@ import type { NucleusClient } from "../utils/client.js"; export const event = "channelCreate"; export async function callback(client: NucleusClient, channel: GuildBasedChannel) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + const { getAuditLog, log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + if (!await isLogging(channel.guild.id, "channelUpdate")) return; const auditLog = (await getAuditLog(channel.guild, AuditLogEvent.ChannelCreate)) .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildBasedChannel)!.id === channel.id)[0]; if (!auditLog) return; @@ -62,7 +63,7 @@ export async function callback(client: NucleusClient, channel: GuildBasedChannel calculateType: "channelUpdate", color: NucleusColors.green, emoji: emoji, - timestamp: channel.createdTimestamp + timestamp: channel.createdTimestamp ?? Date.now() }, list: { channelId: entry(channel.id, `\`${channel.id}\``), diff --git a/src/events/channelDelete.ts b/src/events/channelDelete.ts index 4780700..890a15f 100644 --- a/src/events/channelDelete.ts +++ b/src/events/channelDelete.ts @@ -14,8 +14,8 @@ import getEmojiByName from "../utils/getEmojiByName.js"; export const event = "channelDelete"; export async function callback(client: NucleusClient, channel: GuildBasedChannel) { - const { getAuditLog, log, NucleusColors, entry, renderDelta, renderUser } = client.logger; - // const audit = auditLog.entries.filter((entry: GuildAuditLogsEntry) => entry.target!.id === channel.id).first(); + const { getAuditLog, log, isLogging, NucleusColors, entry, renderDelta, renderUser } = client.logger; + if (!await isLogging(channel.guild.id, "channelUpdate")) return; const auditLog = (await getAuditLog(channel.guild, AuditLogEvent.ChannelDelete)) .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildBasedChannel)!.id === channel.id)[0]; if (!auditLog) return; @@ -82,7 +82,7 @@ export async function callback(client: NucleusClient, channel: GuildBasedChannel ), nsfw: null, created: entry(channel.createdTimestamp, renderDelta(channel.createdTimestamp!)), - deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())), + deleted: entry(Date.now(), renderDelta(Date.now())), deletedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)) }; if ((channel instanceof BaseGuildTextChannel || channel instanceof StageChannel) && channel.topic !== null) diff --git a/src/events/channelUpdate.ts b/src/events/channelUpdate.ts index 7c80e12..052acc1 100644 --- a/src/events/channelUpdate.ts +++ b/src/events/channelUpdate.ts @@ -32,11 +32,11 @@ interface channelChanges { topic?: ReturnType; bitrate?: ReturnType; userLimit?: ReturnType; - rateLimitPerUser?: ReturnType; parent?: ReturnType; permissionOverwrites?: ReturnType; region?: ReturnType; maxUsers?: ReturnType; + autoArchiveDuration?: ReturnType; } @@ -44,8 +44,9 @@ interface channelChanges { export const event = "channelUpdate"; export async function callback(client: NucleusClient, oldChannel: GuildChannel, newChannel: GuildChannel) { + const { getAuditLog, log, isLogging, NucleusColors, renderDelta, renderUser, renderChannel } = client.logger; + if (!await isLogging(newChannel.guild.id, "channelUpdate")) return; const config = await client.memory.readGuildInfo(newChannel.guild.id); - const { getAuditLog, log, NucleusColors, renderDelta, renderUser, renderChannel } = client.logger; entry = client.logger.entry; if (newChannel.parent && newChannel.parent.id === config.tickets.category) return; @@ -60,7 +61,7 @@ export async function callback(client: NucleusClient, oldChannel: GuildChannel, const changes: channelChanges = { channelId: entry(newChannel.id, `\`${newChannel.id}\``), channel: entry(newChannel.id, renderChannel(newChannel)), - edited: entry(new Date().getTime(), renderDelta(new Date().getTime())), + edited: entry(Date.now(), renderDelta(Date.now())), editedBy: entry(auditLog.executor!.id, renderUser((await newChannel.guild.members.fetch(auditLog.executor!.id)).user)), }; if (oldChannel.name !== newChannel.name) changes.name = entry([oldChannel.name, newChannel.name], `${oldChannel.name} -> ${newChannel.name}`); @@ -68,12 +69,16 @@ export async function callback(client: NucleusClient, oldChannel: GuildChannel, changes.position = entry([oldChannel.position.toString(), newChannel.position.toString()], `${oldChannel.position} -> ${newChannel.position}`); switch (newChannel.type) { + case ChannelType.PrivateThread: + case ChannelType.PublicThread: { + return; + } case ChannelType.GuildText: { emoji = "CHANNEL.TEXT.EDIT"; readableType = "Text"; displayName = "Text Channel"; - let oldTopic = (oldChannel as TextChannel).topic, - newTopic = (newChannel as TextChannel).topic; + let oldTopic = (oldChannel as TextChannel).topic ?? "*None*", + newTopic = (oldChannel as TextChannel).topic ?? "*None*"; if (oldTopic) { if (oldTopic.length > 256) oldTopic = `\`\`\`\n${oldTopic.replace("`", "'").substring(0, 253) + "..."}\n\`\`\``; @@ -91,14 +96,20 @@ export async function callback(client: NucleusClient, oldChannel: GuildChannel, const nsfw = ["", ""]; nsfw[0] = (oldChannel as TextChannel).nsfw ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`; nsfw[1] = (newChannel as TextChannel).nsfw ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`; - if ((oldChannel as TextChannel).topic !== (newChannel as TextChannel).topic) + if (oldTopic !== newTopic) changes.description = entry([(oldChannel as TextChannel).topic ?? "", (newChannel as TextChannel).topic ?? ""], `\nBefore: ${oldTopic}\nAfter: ${newTopic}`); if ((oldChannel as TextChannel).nsfw !== (newChannel as TextChannel).nsfw) changes.nsfw = entry([(oldChannel as TextChannel).nsfw ? "On" : "Off", (newChannel as TextChannel).nsfw ? "On" : "Off"], `${nsfw[0]} -> ${nsfw[1]}`); - if ((oldChannel as TextChannel).rateLimitPerUser !== (newChannel as TextChannel).rateLimitPerUser && (oldChannel as TextChannel).rateLimitPerUser !== 0) - changes.rateLimitPerUser = entry( + if ((oldChannel as TextChannel).rateLimitPerUser !== (newChannel as TextChannel).rateLimitPerUser) + changes.slowmode = entry( [((oldChannel as TextChannel).rateLimitPerUser).toString(), ((newChannel as TextChannel).rateLimitPerUser).toString()], `${humanizeDuration((oldChannel as TextChannel).rateLimitPerUser * 1000)} -> ${humanizeDuration((newChannel as TextChannel).rateLimitPerUser * 1000)}` ); + if((oldChannel as TextChannel).defaultAutoArchiveDuration !== (newChannel as TextChannel).defaultAutoArchiveDuration) { + changes.autoArchiveDuration = entry( + [((oldChannel as TextChannel).defaultAutoArchiveDuration ?? 4320).toString(), ((newChannel as TextChannel).defaultAutoArchiveDuration ?? 4320).toString()], + `${humanizeDuration(((oldChannel as TextChannel).defaultAutoArchiveDuration ?? 4320) * 60 * 1000)} -> ${humanizeDuration(((newChannel as TextChannel).defaultAutoArchiveDuration ?? 4320) * 60 * 1000)}` + ); + } break; } @@ -122,8 +133,15 @@ export async function callback(client: NucleusClient, oldChannel: GuildChannel, } else { newTopic = "None"; } - if ((oldChannel as TextChannel).nsfw !== (newChannel as TextChannel).nsfw) + if ((oldChannel as TextChannel).nsfw !== (newChannel as TextChannel).nsfw) { changes.nsfw = entry([(oldChannel as TextChannel).nsfw ? "On" : "Off", (newChannel as TextChannel).nsfw ? "On" : "Off"], `${(oldChannel as TextChannel).nsfw ? "On" : "Off"} -> ${(newChannel as TextChannel).nsfw ? "On" : "Off"}`); + } + if((oldChannel as TextChannel).defaultAutoArchiveDuration !== (newChannel as TextChannel).defaultAutoArchiveDuration) { + changes.autoArchiveDuration = entry( + [((oldChannel as TextChannel).defaultAutoArchiveDuration ?? 4320).toString(), ((newChannel as TextChannel).defaultAutoArchiveDuration ?? 4320).toString()], + `${humanizeDuration(((oldChannel as TextChannel).defaultAutoArchiveDuration ?? 4320) * 60 * 1000)} -> ${humanizeDuration(((newChannel as TextChannel).defaultAutoArchiveDuration ?? 4320) * 60 * 1000)}` + ); + } break; } case ChannelType.GuildVoice: { @@ -174,7 +192,7 @@ export async function callback(client: NucleusClient, oldChannel: GuildChannel, if ((oldChannel as StageChannel).rtcRegion !== (newChannel as StageChannel).rtcRegion) changes.region = entry( [(oldChannel as StageChannel).rtcRegion ?? "Automatic", (newChannel as StageChannel).rtcRegion ?? "Automatic"], - `${(oldChannel as StageChannel).rtcRegion?.toUpperCase() ?? "Automatic"} -> ${(newChannel as StageChannel).rtcRegion?.toUpperCase() ?? "Automatic"}` + `${capitalize((oldChannel as StageChannel).rtcRegion?.toLowerCase() ?? "automatic")} -> ${capitalize((newChannel as StageChannel).rtcRegion?.toLowerCase() ?? "automatic")}` ); break; } diff --git a/src/events/emojiCreate.ts b/src/events/emojiCreate.ts index 8023abc..2630295 100644 --- a/src/events/emojiCreate.ts +++ b/src/events/emojiCreate.ts @@ -4,9 +4,10 @@ import type { GuildEmoji, GuildAuditLogsEntry } from 'discord.js' export const event = "emojiCreate"; export async function callback(client: NucleusClient, emoji: GuildEmoji) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger; + const { getAuditLog, log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger; + if (!await isLogging(emoji.guild.id, "emojiUpdate")) return; const auditLog = (await getAuditLog(emoji.guild, AuditLogEvent.EmojiCreate)) - .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildEmoji)!.id === emoji.id)[0]; + .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildEmoji)!.id === emoji.id)[0]; if (!auditLog) return; if (auditLog.executor!.id === client.user!.id) return; const data = { diff --git a/src/events/emojiDelete.ts b/src/events/emojiDelete.ts index f607cf4..c4b488e 100644 --- a/src/events/emojiDelete.ts +++ b/src/events/emojiDelete.ts @@ -4,8 +4,9 @@ import type { GuildEmoji, GuildAuditLogsEntry } from 'discord.js' export const event = "emojiDelete"; export async function callback(client: NucleusClient, emoji: GuildEmoji) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger; - const auditLog = (await getAuditLog(emoji.guild, AuditLogEvent.EmojiCreate)) + const { getAuditLog, log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger; + if (!await isLogging(emoji.guild.id, "emojiUpdate")) return; + const auditLog = (await getAuditLog(emoji.guild, AuditLogEvent.EmojiDelete)) .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildEmoji)!.id === emoji.id)[0]; if (!auditLog) return; if (auditLog.executor!.id === client.user!.id) return; diff --git a/src/events/emojiUpdate.ts b/src/events/emojiUpdate.ts index 201dd42..98ff558 100644 --- a/src/events/emojiUpdate.ts +++ b/src/events/emojiUpdate.ts @@ -4,9 +4,10 @@ import type { GuildEmoji, GuildAuditLogsEntry } from 'discord.js' export const event = "emojiUpdate"; export async function callback(client: NucleusClient, oldEmoji: GuildEmoji, newEmoji: GuildEmoji) { - const { getAuditLog, log, NucleusColors, entry, renderDelta, renderUser, renderEmoji } = client.logger; + const { getAuditLog, log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger; + if (!(await isLogging(newEmoji.guild.id, "emojiUpdate"))) return; - const auditLog = (await getAuditLog(newEmoji.guild, AuditLogEvent.EmojiCreate)) + const auditLog = (await getAuditLog(newEmoji.guild, AuditLogEvent.EmojiUpdate)) .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildEmoji)!.id === newEmoji.id)[0]; if (!auditLog) return; if (auditLog.executor!.id === client.user!.id) return; diff --git a/src/events/guildBanAdd.ts b/src/events/guildBanAdd.ts index 3d96245..cac4b41 100644 --- a/src/events/guildBanAdd.ts +++ b/src/events/guildBanAdd.ts @@ -7,10 +7,11 @@ import type { NucleusClient } from "../utils/client.js"; export const event = "guildBanAdd"; export async function callback(client: NucleusClient, ban: GuildBan) { + const { log, isLogging, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger; await statsChannelRemove(client, undefined, ban.guild, ban.user); purgeByUser(ban.user.id, ban.guild.id); - const { log, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger; - const auditLog: GuildAuditLogsEntry | undefined = (await getAuditLog(ban.guild, AuditLogEvent.EmojiCreate)) + if (!(await isLogging(ban.guild.id, "guildMemberPunish"))) return; + const auditLog: GuildAuditLogsEntry | undefined = (await getAuditLog(ban.guild, AuditLogEvent.MemberBanAdd)) .filter((entry: GuildAuditLogsEntry) => ((entry.target! as User).id === ban.user.id))[0]; if (!auditLog) return; if (auditLog.executor!.id === client.user!.id) return; @@ -22,12 +23,12 @@ export async function callback(client: NucleusClient, ban: GuildBan) { calculateType: "guildMemberPunish", color: NucleusColors.red, emoji: "PUNISH.BAN.RED", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(ban.user.id, `\`${ban.user.id}\``), name: entry(ban.user.id, renderUser(ban.user)), - banned: entry(new Date().getTime(), renderDelta(new Date().getTime())), + banned: entry(Date.now(), renderDelta(Date.now())), bannedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)), reason: entry(auditLog.reason, auditLog.reason ? `\n> ${auditLog.reason}` : "*No reason provided.*"), accountCreated: entry(ban.user.createdTimestamp, renderDelta(ban.user.createdTimestamp)), diff --git a/src/events/guildBanRemove.ts b/src/events/guildBanRemove.ts index bcb70d5..3be4560 100644 --- a/src/events/guildBanRemove.ts +++ b/src/events/guildBanRemove.ts @@ -1,14 +1,13 @@ import type { GuildAuditLogsEntry, GuildBan, User } from "discord.js"; import { AuditLogEvent } from "discord.js"; -import { purgeByUser } from "../actions/tickets/delete.js"; import type { NucleusClient } from "../utils/client.js"; export const event = "guildBanRemove"; export async function callback(client: NucleusClient, ban: GuildBan) { - purgeByUser(ban.user.id, ban.guild.id); - const { log, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger; - const auditLog = (await getAuditLog(ban.guild, AuditLogEvent.EmojiCreate)) + const { log, isLogging, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger; + if (!await isLogging(ban.guild.id, "guildMemberPunish")) return; + const auditLog = (await getAuditLog(ban.guild, AuditLogEvent.MemberBanRemove)) .filter((entry: GuildAuditLogsEntry) => ((entry.target! as User).id === ban.user.id))[0]; if (!auditLog) return; if (auditLog.executor!.id === client.user!.id) return; @@ -20,12 +19,12 @@ export async function callback(client: NucleusClient, ban: GuildBan) { calculateType: "guildMemberPunish", color: NucleusColors.green, emoji: "PUNISH.BAN.GREEN", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(ban.user.id, `\`${ban.user.id}\``), name: entry(ban.user.id, renderUser(ban.user)), - unbanned: entry(new Date().getTime(), renderDelta(new Date().getTime())), + unbanned: entry(Date.now(), renderDelta(Date.now())), unbannedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)), accountCreated: entry(ban.user.createdTimestamp, renderDelta(ban.user.createdTimestamp)) }, diff --git a/src/events/guildMemberUpdate.ts b/src/events/guildMemberUpdate.ts index 8889f57..721978f 100644 --- a/src/events/guildMemberUpdate.ts +++ b/src/events/guildMemberUpdate.ts @@ -1,11 +1,76 @@ import { AuditLogEvent, GuildAuditLogsEntry, GuildMember } from "discord.js"; import type { NucleusClient } from "../utils/client.js"; +import type { LoggerOptions } from "../utils/log.js"; +import { generalException } from "../utils/createTemporaryStorage.js"; export const event = "guildMemberUpdate"; export async function callback(client: NucleusClient, before: GuildMember, after: GuildMember) { const { log, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger; - const auditLog = (await getAuditLog(after.guild, AuditLogEvent.EmojiCreate)) + if(before.guild.id === "684492926528651336") { + await client.database.premium.checkAllPremium(after) + } + + if(!before.roles.cache.equals(after.roles.cache)) { + const auditLog = (await getAuditLog(after.guild, AuditLogEvent.MemberRoleUpdate)) + .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildMember)!.id === after.id)[0]; + if (!auditLog) return; + if (client.noLog.includes(`${after.guild.id}${after.id}${auditLog.id}`)) return; + generalException(`${after.guild.id}${after.id}${auditLog.id}`); + if (auditLog.executor!.id !== client.user!.id) { + const rolesAdded = after.roles.cache.filter(role => !before.roles.cache.has(role.id)); + const rolesRemoved = before.roles.cache.filter(role => !after.roles.cache.has(role.id)); + let displayName = "Roles Removed"; + let color = NucleusColors.red; + let emoji = "GUILD.ROLES.DELETE"; + if(rolesAdded.size > 0 && rolesRemoved.size > 0) {displayName = "Roles Changed"; color = NucleusColors.yellow; emoji = "GUILD.ROLES.EDIT";} + else if(rolesAdded.size > 0) {displayName = "Roles Added"; color = NucleusColors.green; emoji = "GUILD.ROLES.CREATE";} + const removedEntry = rolesRemoved.map(role => role.id); + const addedEntry = rolesAdded.map(role => role.id); + + let list = { + memberId: entry(after.id, `\`${after.id}\``), + name: entry(after.user.id, renderUser(after.user)), + }; + + if (rolesAdded.size > 0) { + list = Object.assign(list, {rolesAdded: entry(addedEntry, addedEntry.map(id => `<@&${id}>`).join(", "))}); + } + if (rolesRemoved.size > 0) { + list = Object.assign(list, {rolesRemoved: entry(removedEntry, removedEntry.map(id => `<@&${id}>`).join(", "))}); + } + + list = Object.assign(list, { + changed: entry(Date.now(), renderDelta(Date.now())), + changedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)) + }); + + let data: LoggerOptions = { + meta: { + type: "memberUpdate", + displayName: displayName, + calculateType: "guildMemberUpdate", + color: color, + emoji: emoji, + timestamp: Date.now() + }, + list: {}, + hidden: { + guild: after.guild.id + } + }; + + if(rolesAdded.size > 0) { + list = Object.assign(list, {rolesAdded: entry(addedEntry, addedEntry.map(id => `<@&${id}>`).join(", "))}); + } + if(rolesRemoved.size > 0) { + list = Object.assign(list, {rolesRemoved: entry(removedEntry, removedEntry.map(id => `<@&${id}>`).join(", "))}); + } + data = Object.assign(data, {list: list}); + log(data); + } + } + const auditLog = (await getAuditLog(after.guild, AuditLogEvent.MemberUpdate)) .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildMember)!.id === after.id)[0]; if (!auditLog) return; if (auditLog.executor!.id === client.user!.id) return; @@ -26,14 +91,14 @@ export async function callback(client: NucleusClient, before: GuildMember, after calculateType: "guildMemberUpdate", color: NucleusColors.yellow, emoji: "PUNISH.NICKNAME.YELLOW", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(after.id, `\`${after.id}\``), name: entry(after.user.id, renderUser(after.user)), before: entry(before.nickname, before.nickname ? before.nickname : "*None*"), after: entry(after.nickname, after.nickname ? after.nickname : "*None*"), - changed: entry(new Date().getTime(), renderDelta(new Date().getTime())), + changed: entry(Date.now(), renderDelta(Date.now())), changedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)) }, hidden: { @@ -41,9 +106,10 @@ export async function callback(client: NucleusClient, before: GuildMember, after } }; log(data); - } else if ( - (before.communicationDisabledUntilTimestamp ?? 0) < new Date().getTime() && - (after.communicationDisabledUntil ?? 0) > new Date().getTime() // TODO: test this + } + if ( + (before.communicationDisabledUntilTimestamp ?? 0) < Date.now() && + new Date(after.communicationDisabledUntil ?? 0).getTime() > Date.now() ) { await client.database.history.create( "mute", @@ -62,7 +128,7 @@ export async function callback(client: NucleusClient, before: GuildMember, after calculateType: "guildMemberPunish", color: NucleusColors.yellow, emoji: "PUNISH.MUTE.YELLOW", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(after.id, `\`${after.id}\``), @@ -71,7 +137,7 @@ export async function callback(client: NucleusClient, before: GuildMember, after after.communicationDisabledUntilTimestamp, renderDelta(after.communicationDisabledUntilTimestamp!) ), - muted: entry(new Date().getTime(), renderDelta(new Date().getTime())), + muted: entry(Date.now(), renderDelta(Date.now())), mutedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)), reason: entry(auditLog.reason, auditLog.reason ? auditLog.reason : "\n> *No reason provided*") }, @@ -85,10 +151,11 @@ export async function callback(client: NucleusClient, before: GuildMember, after user: after.id, expires: after.communicationDisabledUntilTimestamp }); - } else if ( + } + if ( after.communicationDisabledUntil === null && before.communicationDisabledUntilTimestamp !== null && - new Date().getTime() >= auditLog.createdTimestamp + Date.now() >= auditLog.createdTimestamp ) { await client.database.history.create( "unmute", @@ -107,12 +174,12 @@ export async function callback(client: NucleusClient, before: GuildMember, after calculateType: "guildMemberPunish", color: NucleusColors.green, emoji: "PUNISH.MUTE.GREEN", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(after.id, `\`${after.id}\``), name: entry(after.user.id, renderUser(after.user)), - unmuted: entry(new Date().getTime(), renderDelta(new Date().getTime())), + unmuted: entry(Date.now(), renderDelta(Date.now())), unmutedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)) }, hidden: { diff --git a/src/events/guildUpdate.ts b/src/events/guildUpdate.ts index 8690af2..6b25e48 100644 --- a/src/events/guildUpdate.ts +++ b/src/events/guildUpdate.ts @@ -6,7 +6,8 @@ export const event = "guildUpdate"; export async function callback(client: NucleusClient, before: Guild, after: Guild) { await statsChannelUpdate(client, after.members.me!); - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta } = client.logger; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta } = client.logger; + if (!await isLogging(after.id, "guildUpdate")) return; const auditLog = (await getAuditLog(after, AuditLogEvent.GuildUpdate)) .filter((entry: GuildAuditLogsEntry) => (entry.target as Guild)!.id === after.id)[0]!; if (auditLog.executor!.id === client.user!.id) return; @@ -74,7 +75,7 @@ export async function callback(client: NucleusClient, before: Guild, after: Guil ); if (!Object.keys(list).length) return; - list["updated"] = entry(new Date().getTime(), renderDelta(new Date().getTime())); + list["updated"] = entry(Date.now(), renderDelta(Date.now())); list["updatedBy"] = entry(auditLog.executor!.id, renderUser(auditLog.executor!)); const data = { meta: { @@ -83,7 +84,7 @@ export async function callback(client: NucleusClient, before: Guild, after: Guil calculateType: "guildUpdate", color: NucleusColors.yellow, emoji: "GUILD.YELLOW", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: list, hidden: { diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index a22045b..80c2c1b 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -31,14 +31,14 @@ async function modifySuggestion(interaction: Discord.MessageComponentInteraction await message.fetch(); if (message.embeds.length === 0) return; const embed = message.embeds[0]; - const newColour = accept ? "Success" : "Danger"; + const newcolor = accept ? "Success" : "Danger"; const footer = {text: `Suggestion ${accept ? "accepted" : "denied"} by ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL()}; const newEmbed = new EmojiEmbed() .setTitle(embed!.title!) .setDescription(embed!.description!) .setFooter(footer) - .setStatus(newColour); + .setStatus(newcolor); await interaction.update({embeds: [newEmbed], components: []}); } diff --git a/src/events/inviteCreate.ts b/src/events/inviteCreate.ts index a267f09..34f66dc 100644 --- a/src/events/inviteCreate.ts +++ b/src/events/inviteCreate.ts @@ -7,7 +7,8 @@ export const event = "inviteCreate"; export async function callback(client: NucleusClient, invite: Invite) { if(!invite.guild) return; // This is a DM invite (not a guild invite - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + if (!await isLogging(invite.guild.id, "guildUpdate")) return; const auditLog = (await getAuditLog(invite.guild as Guild, AuditLogEvent.InviteCreate)) .filter((entry: GuildAuditLogsEntry) => (entry.target as Invite)!.code === invite.code)[0]!; if (auditLog.executor!.id === client.user!.id) return; @@ -18,7 +19,7 @@ export async function callback(client: NucleusClient, invite: Invite) { calculateType: "guildUpdate", color: NucleusColors.green, emoji: "INVITE.CREATE", - timestamp: invite.createdTimestamp + timestamp: invite.createdTimestamp ?? Date.now() }, list: { channel: entry(invite.channel!.id, renderChannel(invite.channel as GuildChannel)), diff --git a/src/events/inviteDelete.ts b/src/events/inviteDelete.ts index 1ded432..456af90 100644 --- a/src/events/inviteDelete.ts +++ b/src/events/inviteDelete.ts @@ -7,7 +7,8 @@ export const event = "inviteDelete"; export async function callback(client: NucleusClient, invite: Invite) { if(!invite.guild) return; // This is a DM invite (not a guild invite - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + if (!await isLogging(invite.guild.id, "guildUpdate")) return; const auditLog = (await getAuditLog(invite.guild as Guild, AuditLogEvent.InviteDelete)) .filter((entry: GuildAuditLogsEntry) => (entry.target as Invite)!.code === invite.code)[0]!; if (auditLog.executor!.id === client.user!.id) return; @@ -18,14 +19,14 @@ export async function callback(client: NucleusClient, invite: Invite) { calculateType: "guildUpdate", color: NucleusColors.red, emoji: "INVITE.DELETE", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { channel: entry(invite.channel!.id, renderChannel(invite.channel as GuildChannel)), link: entry(invite.url, invite.url), expires: entry(invite.maxAge, invite.maxAge ? humanizeDuration(invite.maxAge * 1000) : "Never"), deletedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)), - deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())) + deleted: entry(Date.now(), renderDelta(Date.now())) }, hidden: { guild: invite.guild!.id diff --git a/src/events/memberJoin.ts b/src/events/memberJoin.ts index daf195a..77b111f 100644 --- a/src/events/memberJoin.ts +++ b/src/events/memberJoin.ts @@ -8,7 +8,8 @@ export const event = "guildMemberAdd"; export async function callback(client: NucleusClient, member: GuildMember) { welcome(client, member); statsChannelAdd(client, member); - const { log, NucleusColors, entry, renderUser, renderDelta } = client.logger; + const { log, isLogging, NucleusColors, entry, renderUser, renderDelta } = client.logger; + if (!await isLogging(member.guild.id, "guildMemberUpdate")) return; await client.database.history.create("join", member.guild.id, member.user, null, null); const data = { meta: { @@ -17,7 +18,7 @@ export async function callback(client: NucleusClient, member: GuildMember) { calculateType: "guildMemberUpdate", color: NucleusColors.green, emoji: "MEMBER" + (member.user.bot ? ".BOT" : "") + ".JOIN", - timestamp: member.joinedTimestamp + timestamp: member.joinedTimestamp ?? Date.now() }, list: { memberId: entry(member.id, `\`${member.id}\``), diff --git a/src/events/memberLeave.ts b/src/events/memberLeave.ts index e70fb3c..8b3d1b1 100644 --- a/src/events/memberLeave.ts +++ b/src/events/memberLeave.ts @@ -7,22 +7,36 @@ import { callback as statsChannelRemove } from "../reflex/statsChannelUpdate.js" export const event = "guildMemberRemove"; export async function callback(client: NucleusClient, member: GuildMember) { + const startTime = Date.now() - 10 * 1000; purgeByUser(member.id, member.guild.id); await statsChannelRemove(client, member); - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta } = client.logger; - const auditLog = (await getAuditLog(member.guild as Guild, AuditLogEvent.MemberKick)) + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta } = client.logger; + if (!await isLogging(member.guild.id, "guildMemberUpdate")) return; + const kickAuditLog = (await getAuditLog(member.guild as Guild, AuditLogEvent.MemberKick)) + .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildMember)!.id === member.id)[0]; + const banAuditLog = (await getAuditLog(member.guild as Guild, AuditLogEvent.MemberBanAdd)) .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildMember)!.id === member.id)[0]; let type = "leave"; - if (auditLog) { - if (auditLog.executor!.id === client.user!.id) return; - if (auditLog.createdAt.valueOf() - 100 >= new Date().getTime()) { + if (kickAuditLog) { + if (kickAuditLog.executor!.id === client.user!.id) return; + if (kickAuditLog.createdAt.getTime() >= startTime) { type = "kick"; } } + if (banAuditLog) { + if (banAuditLog.executor!.id === client.user!.id) return; + if (banAuditLog.createdAt.getTime() >= startTime) { + if (!kickAuditLog) { + return + } else if (kickAuditLog.createdAt.valueOf() < banAuditLog.createdAt.valueOf()) { + return + } + } + } let data; if (type === "kick") { - if (!auditLog) return; - await client.database.history.create("kick", member.guild.id, member.user, auditLog.executor, auditLog.reason); + if (!kickAuditLog) return; + await client.database.history.create("kick", member.guild.id, member.user, kickAuditLog.executor, kickAuditLog.reason); data = { meta: { type: "memberKick", @@ -30,15 +44,15 @@ export async function callback(client: NucleusClient, member: GuildMember) { calculateType: "guildMemberPunish", color: NucleusColors.red, emoji: "PUNISH.KICK.RED", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(member.id, `\`${member.id}\``), name: entry(member.id, renderUser(member.user)), joined: entry(member.joinedTimestamp, renderDelta(member.joinedTimestamp?.valueOf()!)), - kicked: entry(new Date().getTime(), renderDelta(new Date().getTime())), - kickedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)), - reason: entry(auditLog.reason, auditLog.reason ? `\n> ${auditLog.reason}` : "*No reason provided.*"), + kicked: entry(Date.now(), renderDelta(Date.now())), + kickedBy: entry(kickAuditLog.executor!.id, renderUser(kickAuditLog.executor!)), + reason: entry(kickAuditLog.reason, kickAuditLog.reason ? `\n> ${kickAuditLog.reason}` : "*No reason provided.*"), accountCreated: entry(member.user.createdTimestamp, renderDelta(member.user.createdTimestamp)), serverMemberCount: member.guild.memberCount }, @@ -55,13 +69,13 @@ export async function callback(client: NucleusClient, member: GuildMember) { calculateType: "guildMemberUpdate", color: NucleusColors.red, emoji: "MEMBER." + (member.user.bot ? "BOT." : "") + "LEAVE", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(member.id, `\`${member.id}\``), name: entry(member.id, renderUser(member.user)), joined: entry(member.joinedTimestamp, renderDelta(member.joinedTimestamp?.valueOf()!)), - left: entry(new Date().getTime(), renderDelta(new Date().getTime())), + left: entry(Date.now(), renderDelta(Date.now())), accountCreated: entry(member.user.createdTimestamp, renderDelta(member.user.createdTimestamp)), serverMemberCount: member.guild.memberCount }, diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 69bc542..4f525fc 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -19,12 +19,23 @@ export async function callback(_client: NucleusClient, message: Message) { console.log(e); } - const { log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + const { log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; const fileNames = await logAttachment(message); const content = message.content.toLowerCase() || ""; const config = await client.memory.readGuildInfo(message.guild.id); + if(config.filters.clean.channels.includes(message.channel.id)) { + const memberRoles = message.member!.roles.cache.map(role => role.id); + const roleAllow = config.filters.clean.allowed.roles.some(role => memberRoles.includes(role)); + const userAllow = config.filters.clean.allowed.users.includes(message.author.id); + if(!roleAllow && !userAllow) return await message.delete(); + } + + if (config.autoPublish.enabled && config.autoPublish.channels.includes(message.channel.id)) { + await message.crosspost(); + } + const filter = getEmojiByName("ICONS.FILTER"); let attachmentJump = ""; if (config.logging.attachments.saved[message.channel.id + message.id]) { @@ -34,7 +45,7 @@ export async function callback(_client: NucleusClient, message: Message) { messageId: entry(message.id, `\`${message.id}\``), sentBy: entry(message.author.id, renderUser(message.author)), sentIn: entry(message.channel.id, renderChannel(message.channel)), - deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())), + deleted: entry(Date.now(), renderDelta(Date.now())), mentions: message.mentions.users.size, attachments: entry(message.attachments.size, message.attachments.size + attachmentJump), repliedTo: entry( @@ -57,7 +68,7 @@ export async function callback(_client: NucleusClient, message: Message) { calculateType: "autoModeratorDeleted", color: NucleusColors.red, emoji: "MESSAGE.DELETE", - timestamp: new Date().getTime() + timestamp: Date.now() }, separate: { start: @@ -78,7 +89,8 @@ export async function callback(_client: NucleusClient, message: Message) { if (fileNames.files.length > 0) { for (const element of fileNames.files) { const url = element.url ? element.url : element.local; - if (/\.(jpg|jpeg|png|gif|gifv|webm|webp|mp4|wav|mp3|ogg)$/.test(url)) { + if (/\.(j(pe?g|fif)|a?png|gifv?|w(eb[mp]|av)|mp([34]|eg-\d)|ogg|avi|h\.26(4|5)|cda)$/.test(url.toLowerCase())) { + // jpg|jpeg|png|apng|gif|gifv|webm|webp|mp4|wav|mp3|ogg|jfif|MPEG-#|avi|h.264|h.265 if ( config.filters.images.NSFW && !(message.channel instanceof ThreadChannel ? message.channel.parent?.nsfw : message.channel.nsfw) @@ -93,7 +105,7 @@ export async function callback(_client: NucleusClient, message: Message) { calculateType: "autoModeratorDeleted", color: NucleusColors.red, emoji: "MESSAGE.DELETE", - timestamp: new Date().getTime() + timestamp: Date.now() }, separate: { start: @@ -128,7 +140,7 @@ export async function callback(_client: NucleusClient, message: Message) { calculateType: "autoModeratorDeleted", color: NucleusColors.red, emoji: "MESSAGE.DELETE", - timestamp: new Date().getTime() + timestamp: Date.now() }, separate: { start: @@ -158,7 +170,7 @@ export async function callback(_client: NucleusClient, message: Message) { calculateType: "autoModeratorDeleted", color: NucleusColors.red, emoji: "MESSAGE.DELETE", - timestamp: new Date().getTime() + timestamp: Date.now() }, separate: { start: @@ -189,7 +201,7 @@ export async function callback(_client: NucleusClient, message: Message) { calculateType: "autoModeratorDeleted", color: NucleusColors.red, emoji: "MESSAGE.DELETE", - timestamp: new Date().getTime() + timestamp: Date.now() }, separate: { start: @@ -221,7 +233,7 @@ export async function callback(_client: NucleusClient, message: Message) { calculateType: "autoModeratorDeleted", color: NucleusColors.red, emoji: "MESSAGE.DELETE", - timestamp: new Date().getTime() + timestamp: Date.now() }, separate: { start: @@ -253,7 +265,7 @@ export async function callback(_client: NucleusClient, message: Message) { calculateType: "autoModeratorDeleted", color: NucleusColors.red, emoji: "MESSAGE.DELETE", - timestamp: new Date().getTime() + timestamp: Date.now() }, separate: { start: @@ -271,6 +283,7 @@ export async function callback(_client: NucleusClient, message: Message) { } if (config.filters.pings.everyone && message.mentions.everyone) { + if(!await isLogging(message.guild.id, "messageMassPing")) return; const data = { meta: { type: "everyonePing", @@ -278,7 +291,7 @@ export async function callback(_client: NucleusClient, message: Message) { calculateType: "messageMassPing", color: NucleusColors.yellow, emoji: "MESSAGE.PING.EVERYONE", - timestamp: new Date().getTime() + timestamp: Date.now() }, separate: { start: content ? `**Message:**\n\`\`\`${content}\`\`\`` : "**Message:** *Message had no content*" @@ -295,6 +308,7 @@ export async function callback(_client: NucleusClient, message: Message) { if (!config.filters.pings.allowed.roles.includes(roleId)) { messageException(message.guild.id, message.channel.id, message.id); await message.delete(); + if(!await isLogging(message.guild.id, "messageMassPing")) return; const data = { meta: { type: "rolePing", @@ -302,7 +316,7 @@ export async function callback(_client: NucleusClient, message: Message) { calculateType: "messageMassPing", color: NucleusColors.yellow, emoji: "MESSAGE.PING.ROLE", - timestamp: new Date().getTime() + timestamp: Date.now() }, separate: { start: content @@ -321,6 +335,7 @@ export async function callback(_client: NucleusClient, message: Message) { if (message.mentions.users.size >= config.filters.pings.mass && config.filters.pings.mass) { messageException(message.guild.id, message.channel.id, message.id); await message.delete(); + if(!await isLogging(message.guild.id, "messageMassPing")) return; const data = { meta: { type: "massPing", @@ -328,7 +343,7 @@ export async function callback(_client: NucleusClient, message: Message) { calculateType: "messageMassPing", color: NucleusColors.yellow, emoji: "MESSAGE.PING.MASS", - timestamp: new Date().getTime() + timestamp: Date.now() }, separate: { start: content ? `**Message:**\n\`\`\`${content}\`\`\`` : "**Message:** *Message had no content*" diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index f8433fc..aac83f4 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -4,61 +4,59 @@ import Discord, { AuditLogEvent, GuildAuditLogsEntry, Message, User } from "disc export const event = "messageDelete"; export async function callback(client: NucleusClient, message: Message) { - try { - if (message.author.id === client.user!.id) return; - if (client.noLog.includes(`${message.id}/${message.channel.id}/${message.id}`)) return; - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; - const auditLog = (await getAuditLog(message.guild!, AuditLogEvent.MemberBanAdd)) - .filter((entry: GuildAuditLogsEntry) => (entry.target! as User).id === message.author.id)[0]; - if (auditLog) { - if (auditLog.createdTimestamp - 1000 < new Date().getTime()) return; - } - const replyTo = message.reference; - let content = message.cleanContent; - content.replace("`", "\\`"); - if (content.length > 256) content = content.substring(0, 253) + "..."; - const attachments = - message.attachments.size + ( - message.content.match( - /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/gi - ) ?? [] - ).length; - let attachmentJump = ""; - const config = (await client.database.guilds.read(message.guild!.id)).logging.attachments.saved[ - message.channel.id + message.id - ]; - if (config) { attachmentJump = ` [[View attachments]](${config})`; } - const data = { - meta: { - type: "messageDelete", - displayName: "Message Deleted", - calculateType: "messageDelete", - color: NucleusColors.red, - emoji: "MESSAGE.DELETE", - timestamp: new Date().getTime() - }, - separate: { - start: content ? `**Message:**\n\`\`\`${content}\`\`\`` : "**Message:** *Message had no content*" - }, - list: { - messageId: entry(message.id, `\`${message.id}\``), - sentBy: entry(message.author.id, renderUser(message.author)), - sentIn: entry(message.channel.id, renderChannel(message.channel as Discord.GuildChannel | Discord.ThreadChannel)), - deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())), - mentions: message.mentions.users.size, - attachments: entry(attachments, attachments + attachmentJump), - repliedTo: entry( - replyTo ? replyTo.messageId! : null, - replyTo ? `[[Jump to message]](https://discord.com/channels/${message.guild!.id}/${message.channel.id}/${replyTo.messageId})` - : "None" - ) - }, - hidden: { - guild: message.guild!.id - } - }; - log(data); - } catch (e) { - console.log(e); + if (message.author.id === client.user!.id) return; + if (message.author.bot) return; + if (client.noLog.includes(`${message.guild!.id}/${message.channel.id}/${message.id}`)) return; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + if (!await isLogging(message.guild!.id, "messageDelete")) return; + const auditLog = (await getAuditLog(message.guild!, AuditLogEvent.MemberBanAdd)) + .filter((entry: GuildAuditLogsEntry) => (entry.target! as User).id === message.author.id)[0]; + if (auditLog) { + if (auditLog.createdTimestamp - 1000 < Date.now()) return; } + const replyTo = message.reference; + let content = message.cleanContent; + content.replace("`", "\\`"); + if (content.length > 256) content = content.substring(0, 253) + "..."; + const attachments = + message.attachments.size + ( + message.content.match( + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/gi + ) ?? [] + ).length; + let attachmentJump = ""; + const config = (await client.database.guilds.read(message.guild!.id)).logging.attachments.saved[ + message.channel.id + message.id + ]; + if (config) { attachmentJump = ` [[View attachments]](${config})`; } + const data = { + meta: { + type: "messageDelete", + displayName: "Message Deleted", + calculateType: "messageDelete", + color: NucleusColors.red, + emoji: "MESSAGE.DELETE", + timestamp: Date.now() + }, + separate: { + start: content ? `**Message:**\n\`\`\`${content}\`\`\`` : "**Message:** *Message had no content*" + }, + list: { + messageId: entry(message.id, `\`${message.id}\``), + sentBy: entry(message.author.id, renderUser(message.author)), + sentIn: entry(message.channel.id, renderChannel(message.channel as Discord.GuildChannel | Discord.ThreadChannel)), + deleted: entry(Date.now(), renderDelta(Date.now())), + mentions: message.mentions.users.size, + attachments: entry(attachments, attachments + attachmentJump), + repliedTo: entry( + replyTo ? replyTo.messageId! : null, + replyTo ? `[[Jump to message]](https://discord.com/channels/${message.guild!.id}/${message.channel.id}/${replyTo.messageId})` + : "None" + ) + }, + hidden: { + guild: message.guild!.id + } + }; + log(data); } diff --git a/src/events/messageEdit.ts b/src/events/messageEdit.ts index 20624fe..f5a28a4 100644 --- a/src/events/messageEdit.ts +++ b/src/events/messageEdit.ts @@ -6,8 +6,10 @@ export const event = "messageUpdate"; export async function callback(client: NucleusClient, oldMessage: Message, newMessage: Message) { if (newMessage.author.id === client.user!.id) return; + if (newMessage.author.bot) return; if (!newMessage.guild) return; - const { log, NucleusColors, entry, renderUser, renderDelta, renderNumberDelta, renderChannel } = client.logger; + const { log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderNumberDelta, renderChannel } = client.logger; + const replyTo: MessageReference | null = newMessage.reference; let newContent = newMessage.cleanContent.replaceAll("`", "‘"); let oldContent = oldMessage.cleanContent.replaceAll("`", "‘"); @@ -18,7 +20,8 @@ export async function callback(client: NucleusClient, oldMessage: Message, newMe if (config) { attachmentJump = ` [[View attachments]](${config})`; } - if (newContent === oldContent && newMessage.attachments.size === oldMessage.attachments.size) { + if (newMessage.crosspostable !== oldMessage.crosspostable) { + if(!await isLogging(newMessage.guild.id, "messageAnnounce")) return; if (!replyTo) { const data = { meta: { @@ -27,7 +30,7 @@ export async function callback(client: NucleusClient, oldMessage: Message, newMe calculateType: "messageAnnounce", color: NucleusColors.yellow, emoji: "MESSAGE.CREATE", - timestamp: newMessage.editedTimestamp + timestamp: newMessage.editedTimestamp ?? Date.now() }, separate: { end: `[[Jump to message]](${newMessage.url})` @@ -57,6 +60,7 @@ export async function callback(client: NucleusClient, oldMessage: Message, newMe return log(data); } } + if (!await isLogging(newMessage.guild.id, "messageUpdate")) return; if (!newMessage.editedTimestamp) { return; } diff --git a/src/events/roleCreate.ts b/src/events/roleCreate.ts index d253ce7..be385f0 100644 --- a/src/events/roleCreate.ts +++ b/src/events/roleCreate.ts @@ -4,7 +4,8 @@ import { AuditLogEvent, Guild, GuildAuditLogsEntry, Role } from "discord.js"; export const event = "roleCreate"; export async function callback(client: NucleusClient, role: Role) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderRole } = client.logger; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderRole } = client.logger; + if (!await isLogging(role.guild.id, "guildRoleUpdate")) return; if (role.managed) return; const auditLog = (await getAuditLog(role.guild as Guild, AuditLogEvent.RoleCreate)) .filter((entry: GuildAuditLogsEntry) => (entry.target as Role)!.id === role.id)[0]!; diff --git a/src/events/roleDelete.ts b/src/events/roleDelete.ts index f41241b..b207f4f 100644 --- a/src/events/roleDelete.ts +++ b/src/events/roleDelete.ts @@ -5,7 +5,8 @@ import { AuditLogEvent, Guild, GuildAuditLogsEntry, Role } from "discord.js"; export const event = "roleDelete"; export async function callback(client: NucleusClient, role: Role) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta } = client.logger; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta } = client.logger; + if (!await isLogging(role.guild.id, "guildRoleUpdate")) return; if (role.managed) return; const auditLog = (await getAuditLog(role.guild as Guild, AuditLogEvent.RoleDelete)) .filter((entry: GuildAuditLogsEntry) => (entry.target as Role)!.id === role.id)[0]!; @@ -34,7 +35,7 @@ export async function callback(client: NucleusClient, role: Role) { members: entry(role.members.size, `${role.members.size}`), deletedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)), created: entry(role.createdTimestamp, renderDelta(role.createdTimestamp)), - deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())) + deleted: entry(Date.now(), renderDelta(Date.now())) }, hidden: { guild: role.guild.id diff --git a/src/events/roleUpdate.ts b/src/events/roleUpdate.ts index 924ec3e..8d9ef10 100644 --- a/src/events/roleUpdate.ts +++ b/src/events/roleUpdate.ts @@ -5,16 +5,17 @@ import getEmojiByName from "../utils/getEmojiByName.js"; export const event = "roleUpdate"; export async function callback(client: NucleusClient, oldRole: Role, newRole: Role) { - const { getAuditLog, log, NucleusColors, entry, renderDelta, renderUser, renderRole } = client.logger; - + const { getAuditLog, isLogging, log, NucleusColors, entry, renderDelta, renderUser, renderRole } = client.logger; + if (!await isLogging(newRole.guild.id, "guildRoleUpdate")) return; const auditLog = (await getAuditLog(newRole.guild as Guild, AuditLogEvent.RoleUpdate)) - .filter((entry: GuildAuditLogsEntry) => (entry.target as Role)!.id === newRole.id)[0]!; + .filter((entry: GuildAuditLogsEntry) => (entry.target as Role)!.id === newRole.id)[0]; + if (!auditLog) return; if (auditLog.executor!.id === client.user!.id) return; const changes: Record> = { roleId: entry(newRole.id, `\`${newRole.id}\``), role: entry(newRole.id, renderRole(newRole)), - edited: entry(new Date().getTime(), renderDelta(new Date().getTime())), + edited: entry(Date.now(), renderDelta(Date.now())), editedBy: entry(auditLog.executor!.id, renderUser((await newRole.guild.members.fetch(auditLog.executor!.id)).user)) }; const mentionable = ["", ""]; @@ -31,6 +32,12 @@ export async function callback(client: NucleusClient, oldRole: Role, newRole: Ro changes["mentionable"] = entry([oldRole.mentionable, newRole.mentionable], `${mentionable[0]} -> ${mentionable[1]}`); if (oldRole.hexColor !== newRole.hexColor) changes["color"] = entry([oldRole.hexColor, newRole.hexColor], `\`${oldRole.hexColor}\` -> \`${newRole.hexColor}\``); + if (oldRole.permissions.bitfield !== newRole.permissions.bitfield) { + changes["permissions"] = entry( + [oldRole.permissions.bitfield.toString(), newRole.permissions.bitfield.toString()], + `[[Old]](https://discordapi.com/permissions.html#${oldRole.permissions.bitfield.toString()}) -> [[New]](https://discordapi.com/permissions.html#${newRole.permissions.bitfield.toString()})` + ); + } if (Object.keys(changes).length === 4) return; @@ -47,6 +54,6 @@ export async function callback(client: NucleusClient, oldRole: Role, newRole: Ro hidden: { guild: newRole.guild.id } - }; // TODO: show perms changed (webpage) + }; // TODO: make our own page for this log(data); } diff --git a/src/events/stickerCreate.ts b/src/events/stickerCreate.ts index b341ae9..5d2e443 100644 --- a/src/events/stickerCreate.ts +++ b/src/events/stickerCreate.ts @@ -1,13 +1,17 @@ import type { NucleusClient } from "../utils/client.js"; import { AuditLogEvent, GuildAuditLogsEntry, Sticker } from "discord.js"; +import { generalException } from "../utils/createTemporaryStorage.js"; -export const event = "stickerDelete"; +export const event = "stickerCreate"; export async function callback(client: NucleusClient, sticker: Sticker) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta } = client.logger; - const auditLog = (await getAuditLog(sticker.guild!, AuditLogEvent.EmojiCreate)) - .filter((entry: GuildAuditLogsEntry) => (entry.target as Sticker)!.id === sticker.id)[0]!; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta } = client.logger; + if (!await isLogging(sticker.guild!.id, "stickerUpdate")) return; + const auditLog = (await getAuditLog(sticker.guild!, AuditLogEvent.StickerCreate)) + .filter((entry: GuildAuditLogsEntry) => (entry.target as Sticker)!.id === sticker.id)[0]!; if (auditLog.executor!.id === client.user!.id) return; + if (client.noLog.includes(`${sticker.guild!.id}${auditLog.id}`)) return; + generalException(`${sticker.guild!.id}${auditLog.id}`); const data = { meta: { type: "stickerCreate", diff --git a/src/events/stickerDelete.ts b/src/events/stickerDelete.ts index ce26a85..d123f44 100644 --- a/src/events/stickerDelete.ts +++ b/src/events/stickerDelete.ts @@ -4,7 +4,8 @@ import { AuditLogEvent, GuildAuditLogsEntry, Sticker } from "discord.js"; export const event = "stickerDelete"; export async function callback(client: NucleusClient, sticker: Sticker) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta } = client.logger; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta } = client.logger; + if (!await isLogging(sticker.guild!.id, "stickerUpdate")) return; const auditLog = (await getAuditLog(sticker.guild!, AuditLogEvent.StickerDelete)) .filter((entry: GuildAuditLogsEntry) => (entry.target as Sticker)!.id === sticker.id)[0]!; if (auditLog.executor!.id === client.user!.id) return; @@ -14,7 +15,7 @@ export async function callback(client: NucleusClient, sticker: Sticker) { displayName: "Sticker Deleted", calculateType: "stickerUpdate", color: NucleusColors.red, - sticker: "GUILD.sticker.DELETE", + emoji: "GUILD.EMOJI.DELETE", timestamp: auditLog.createdTimestamp }, list: { diff --git a/src/events/stickerUpdate.ts b/src/events/stickerUpdate.ts index ed01b71..aef28a4 100644 --- a/src/events/stickerUpdate.ts +++ b/src/events/stickerUpdate.ts @@ -4,8 +4,8 @@ import { AuditLogEvent, GuildAuditLogsEntry, Sticker } from "discord.js"; export const event = "stickerUpdate"; export async function callback(client: NucleusClient, oldSticker: Sticker, newSticker: Sticker) { - const { getAuditLog, log, NucleusColors, entry, renderDelta, renderUser } = client.logger; - + const { getAuditLog, isLogging, log, NucleusColors, entry, renderDelta, renderUser } = client.logger; + if (!await isLogging(newSticker.guild!.id, "stickerUpdate")) return; if (oldSticker.name === newSticker.name) return; const auditLog = (await getAuditLog(newSticker.guild!, AuditLogEvent.StickerUpdate)) .filter((entry: GuildAuditLogsEntry) => (entry.target as Sticker)!.id === newSticker.id)[0]!; diff --git a/src/events/threadCreate.ts b/src/events/threadCreate.ts index 6d3225c..f56e1bb 100644 --- a/src/events/threadCreate.ts +++ b/src/events/threadCreate.ts @@ -5,7 +5,8 @@ import type { NucleusClient } from "../utils/client.js"; export const event = "threadCreate"; export async function callback(client: NucleusClient, thread: ThreadChannel) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + if (!await isLogging(thread.guild.id, "channelUpdate")) return; const auditLog = (await getAuditLog(thread.guild, AuditLogEvent.ThreadCreate)) .filter((entry: GuildAuditLogsEntry) => (entry.target as ThreadChannel)!.id === thread.id)[0]!; if (auditLog.executor!.id === client.user!.id) return; @@ -22,7 +23,7 @@ export async function callback(client: NucleusClient, thread: ThreadChannel) { calculateType: "channelUpdate", color: NucleusColors.green, emoji: "CHANNEL.TEXT.CREATE", - timestamp: thread.createdTimestamp + timestamp: thread.createdTimestamp ?? Date.now() }, list: { threadId: entry(thread.id, `\`${thread.id}\``), diff --git a/src/events/threadDelete.ts b/src/events/threadDelete.ts index 429f63a..bfac75e 100644 --- a/src/events/threadDelete.ts +++ b/src/events/threadDelete.ts @@ -5,7 +5,8 @@ import type { NucleusClient } from "../utils/client.js"; export const event = "threadDelete"; export async function callback(client: NucleusClient, thread: ThreadChannel) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + if (!await isLogging(thread.guild.id, "channelUpdate")) return; const auditLog = (await getAuditLog(thread.guild, AuditLogEvent.ThreadDelete)) .filter((entry: GuildAuditLogsEntry) => (entry.target as ThreadChannel)!.id === thread.id)[0]!; if (auditLog.executor!.id === client.user!.id) return; @@ -22,7 +23,7 @@ export async function callback(client: NucleusClient, thread: ThreadChannel) { calculateType: "channelUpdate", color: NucleusColors.red, emoji: "CHANNEL.TEXT.DELETE", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { threadId: entry(thread.id, `\`${thread.id}\``), @@ -38,7 +39,7 @@ export async function callback(client: NucleusClient, thread: ThreadChannel) { membersInThread: entry(thread.memberCount, thread.memberCount!.toString()), deletedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)), created: entry(thread.createdTimestamp, renderDelta(thread.createdTimestamp!)), - deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())) + deleted: entry(Date.now(), renderDelta(Date.now())) }, hidden: { guild: thread.guild.id diff --git a/src/events/threadUpdate.ts b/src/events/threadUpdate.ts index 555b17f..af792bc 100644 --- a/src/events/threadUpdate.ts +++ b/src/events/threadUpdate.ts @@ -6,7 +6,8 @@ import type { NucleusClient } from "../utils/client.js"; export const event = "threadUpdate"; export async function callback(client: NucleusClient, oldThread: ThreadChannel, newThread: ThreadChannel) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; + if (!await isLogging(newThread.guild.id, "channelUpdate")) return; const auditLog = (await getAuditLog(newThread.guild, AuditLogEvent.ThreadUpdate)) .filter((entry: GuildAuditLogsEntry) => (entry.target as ThreadChannel)!.id === newThread.id)[0]!; if (auditLog.executor!.id === client.user!.id) return; @@ -37,7 +38,7 @@ export async function callback(client: NucleusClient, oldThread: ThreadChannel, ); } if (!(Object.keys(list).length - 3)) return; - list["updated"] = entry(new Date().getTime(), renderDelta(new Date().getTime())); + list["updated"] = entry(Date.now(), renderDelta(Date.now())); list["updatedBy"] = entry(auditLog.executor!.id, renderUser(auditLog.executor!)); const data = { meta: { @@ -46,7 +47,7 @@ export async function callback(client: NucleusClient, oldThread: ThreadChannel, calculateType: "channelUpdate", color: NucleusColors.yellow, emoji: "CHANNEL.TEXT.EDIT", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: list, hidden: { diff --git a/src/events/webhookUpdate.ts b/src/events/webhookUpdate.ts index e5f07dd..004f754 100644 --- a/src/events/webhookUpdate.ts +++ b/src/events/webhookUpdate.ts @@ -10,18 +10,18 @@ interface accType { export async function callback(client: NucleusClient, channel: Discord.GuildChannel) { try { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderChannel, renderDelta } = client.logger; + const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderChannel, renderDelta } = client.logger; + if (!await isLogging(channel.guild.id, "webhookUpdate")) return; const auditCreate = (await getAuditLog(channel.guild, AuditLogEvent.WebhookCreate)) - .filter((entry: GuildAuditLogsEntry) => (entry.target as Webhook)!.id === channel.id)[0]!; - const auditDelete = (await getAuditLog(channel.guild, AuditLogEvent.WebhookDelete)) - .filter((entry: GuildAuditLogsEntry) => (entry.target as Webhook)!.id === channel.id)[0]; - const auditUpdate = (await getAuditLog(channel.guild, AuditLogEvent.WebhookUpdate)) - .filter((entry: GuildAuditLogsEntry) => (entry.target as Webhook)!.id === channel.id)[0]; - - if (!auditUpdate && !auditDelete) return; + .filter((entry: GuildAuditLogsEntry | null) => (entry?.target) ? (entry.target as Webhook)!.channelId === channel.id : false)[0]; + const auditDelete = (await getAuditLog(channel.guild, AuditLogEvent.WebhookDelete, 0)) + .filter((entry: GuildAuditLogsEntry | null) => (entry?.target) ? (entry.target as Webhook)!.channelId === channel.id : false)[0]; + const auditUpdate = (await getAuditLog(channel.guild, AuditLogEvent.WebhookUpdate, 0)) + .filter((entry: GuildAuditLogsEntry | null) => (entry?.target) ? (entry.target as Webhook)!.channelId === channel.id : false)[0]; + if (!auditCreate && !auditUpdate && !auditDelete) return; let action: "Create" | "Update" | "Delete" = "Create"; let list: Record | string> = {}; - const createTimestamp = auditCreate.createdTimestamp; + const createTimestamp = auditCreate ? auditCreate.createdTimestamp : 0; const deleteTimestamp = auditDelete ? auditDelete.createdTimestamp : 0; const updateTimestamp = auditUpdate ? auditUpdate.createdTimestamp : 0; if (updateTimestamp > createTimestamp && updateTimestamp > deleteTimestamp && auditUpdate) { @@ -46,7 +46,7 @@ export async function callback(client: NucleusClient, channel: Discord.GuildChan (auditUpdate.target! as Extract).createdTimestamp, renderDelta((auditUpdate.target! as Extract).createdTimestamp) ); - list["edited"] = entry(after["editedTimestamp"]!, renderDelta(new Date().getTime())); + list["edited"] = entry(after["editedTimestamp"]!, renderDelta(Date.now())); list["editedBy"] = entry(auditUpdate.executor!.id, renderUser(auditUpdate.executor!)); action = "Update"; } else if (deleteTimestamp > createTimestamp && deleteTimestamp > updateTimestamp && auditDelete) { @@ -61,7 +61,7 @@ export async function callback(client: NucleusClient, channel: Discord.GuildChan name: entry(before["name"]!, `${before["name"]}`), channel: entry(before["channel_id"]!, renderChannel((await client.channels.fetch(before["channel_id"]!)) as GuildChannel)), created: entry((auditDelete.target! as Extract).createdTimestamp, renderDelta((auditDelete.target! as Extract).createdTimestamp)), - deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())), + deleted: entry(Date.now(), renderDelta(Date.now())), deletedBy: entry( auditDelete.executor!.id, renderUser((await channel.guild.members.fetch(auditDelete.executor!.id)).user) @@ -80,10 +80,10 @@ export async function callback(client: NucleusClient, channel: Discord.GuildChan name: entry(before["name"]!, `${before["name"]}`), channel: entry(before["channel_id"]!, renderChannel(await client.channels.fetch(before["channel_id"]!) as GuildChannel)), createdBy: entry( - auditCreate.executor!.id, - renderUser((await channel.guild.members.fetch(auditCreate.executor!.id)).user) + auditCreate!.executor!.id, + renderUser((await channel.guild.members.fetch(auditCreate!.executor!.id)).user) ), - created: entry(new Date().getTime(), renderDelta(new Date().getTime())) + created: entry(Date.now(), renderDelta(Date.now())) }; } const cols = { @@ -98,7 +98,7 @@ export async function callback(client: NucleusClient, channel: Discord.GuildChan calculateType: "webhookUpdate", color: NucleusColors[cols[action] as keyof typeof NucleusColors], emoji: "WEBHOOK." + action.toUpperCase(), - timestamp: new Date().getTime() + timestamp: Date.now() }, list: list, hidden: { diff --git a/src/index.ts b/src/index.ts index 362b805..12f6659 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,24 @@ import runServer from "./api/index.js"; import client from "./utils/client.js"; -import config from "./config/main.json" assert { type: "json" }; +import config from "./config/main.js"; import register from "./utils/commandRegistration/register.js"; import { record as recordPerformance } from "./utils/performanceTesting/record.js"; -client.on("ready", () => { +client.on("ready", async () => { console.log(`Logged in as ${client.user!.tag}!`); register(); runServer(client); + if (config.enableDevelopment) { + client.fetchedCommands = await client.guilds.cache.get(config.developmentGuildID)?.commands.fetch()!; + } else { + client.fetchedCommands = await client.application?.commands.fetch()!; + } + await client.database.premium.checkAllPremium(); }); + process.on("unhandledRejection", (err) => { console.error(err) }); process.on("uncaughtException", (err) => { console.error(err) }); await client.login(config.enableDevelopment ? config.developmentToken : config.token) -await recordPerformance(); \ No newline at end of file +await recordPerformance(); diff --git a/src/premium/attachmentLogs.ts b/src/premium/attachmentLogs.ts index 156503a..b2c8391 100644 --- a/src/premium/attachmentLogs.ts +++ b/src/premium/attachmentLogs.ts @@ -1,4 +1,4 @@ -import { getCommandMentionByName } from './../utils/getCommandMentionByName.js'; +import { getCommandMentionByName } from './../utils/getCommandDataByName.js'; import client from "../utils/client.js"; import keyValueList from "../utils/generateKeyValueList.js"; import singleNotify from "../utils/singleNotify.js"; @@ -8,12 +8,13 @@ import addPlural from "../utils/plurals.js"; import type { GuildTextBasedChannel, Message } from "discord.js"; export default async function logAttachment(message: Message): Promise { + if (message.guild) client.database.premium.hasPremium(message.guild.id).finally(() => {}); if (!message.guild) throw new Error("Tried to log an attachment in a non-guild message"); const { renderUser, renderChannel, renderDelta } = client.logger; const attachments = []; for (const attachment of message.attachments.values()) { attachments.push({ - local: await saveAttachment(attachment.url), + local: (await saveAttachment(attachment.url))[0], url: attachment.url, height: attachment.height, width: attachment.width, @@ -24,7 +25,7 @@ export default async function logAttachment(message: Message): Promise file.local) }); - // await client.database.guilds.write(interaction.guild.id, {[`tags.${name}`]: value}); client.database.guilds.write(message.guild.id, { [`logging.attachments.saved.${message.channel.id}${message.id}`]: m.url }); diff --git a/src/premium/createTranscript.ts b/src/premium/createTranscript.ts index 04fdc08..67aed04 100644 --- a/src/premium/createTranscript.ts +++ b/src/premium/createTranscript.ts @@ -7,18 +7,23 @@ import { MessageComponentInteraction, TextChannel, ButtonStyle, - User + User, + ThreadChannel } from "discord.js"; import EmojiEmbed from "../utils/generateEmojiEmbed.js"; import getEmojiByName from "../utils/getEmojiByName.js"; -import { PasteClient, Publicity, ExpireDate } from "pastebin-api"; import client from "../utils/client.js"; +import { messageException } from '../utils/createTemporaryStorage.js'; -const pbClient = new PasteClient(client.config.pastebinApiKey); +const noTopic = new EmojiEmbed() + .setTitle("User not found") + .setDescription("There is no user associated with this ticket.") + .setStatus("Danger") + .setEmoji("CONTROL.BLOCKCROSS") export default async function (interaction: CommandInteraction | MessageComponentInteraction) { if (interaction.channel === null) return; - if (!(interaction.channel instanceof TextChannel)) return; + if (!(interaction.channel instanceof TextChannel || interaction.channel instanceof ThreadChannel)) return; const { log, NucleusColors, entry, renderUser, renderDelta } = client.logger; let messages: Message[] = []; @@ -29,95 +34,75 @@ export default async function (interaction: CommandInteraction | MessageComponen const deleted = await (interaction.channel as TextChannel).bulkDelete(fetched, true); deletedCount = deleted.size; messages = messages.concat(Array.from(deleted.values() as Iterable)); + if (messages.length === 1) messageException(interaction.guild!.id, interaction.channel.id, messages[0]!.id) } while (deletedCount === 100); + messages = messages.filter(message => !( + message.components.some( + component => component.components.some( + child => child.customId?.includes("transcript") ?? false + ) + ) + )); - let out = ""; - messages.reverse().forEach((message) => { - if (!message.author.bot) { - const sentDate = new Date(message.createdTimestamp); - out += `${message.author.username}#${message.author.discriminator} (${ - message.author.id - }) [${sentDate.toUTCString()}]\n`; - const lines = message.content.split("\n"); - lines.forEach((line) => { - out += `> ${line}\n`; - }); - out += "\n\n"; - } - }); - const topic = interaction.channel.topic; - let member: GuildMember | null = null; - if (topic !== null) { - const part = topic.split(" ")[0] ?? null; - if (part !== null) member = interaction.guild!.members.cache.get(part) ?? null; - } - let m: Message; - if (out !== "") { - const url = await pbClient.createPaste({ - code: out, - expireDate: ExpireDate.Never, - name: - `Ticket Transcript ${ - member ? "for " + member.user.username + "#" + member.user.discriminator + " " : "" - }` + `(Created at ${new Date(interaction.channel.createdTimestamp).toDateString()})`, - publicity: Publicity.Unlisted - }); - const guildConfig = await client.database.guilds.read(interaction.guild!.id); - m = (await interaction.reply({ - embeds: [ - new EmojiEmbed() - .setTitle("Transcript") - .setDescription( - "You can view the transcript using the link below. You can save the link for later" + - (guildConfig.logging.logs.channel - ? ` or find it in <#${guildConfig.logging.logs.channel}> once you press delete below. After this the channel will be deleted.` - : ".") - ) - .setStatus("Success") - .setEmoji("CONTROL.DOWNLOAD") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(url), - new ButtonBuilder() - .setLabel("Delete") - .setStyle(ButtonStyle.Danger) - .setCustomId("close") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - ]) - ], - fetchReply: true - })) as Message; + let topic + let member: GuildMember = interaction.guild?.members.me!; + if (interaction.channel instanceof TextChannel) { + topic = interaction.channel.topic; + if (topic === null) return await interaction.reply({ embeds: [noTopic] }); + const mem = interaction.guild!.members.cache.get(topic.split(" ")[1]!); + if (mem) member = mem; } else { - m = (await interaction.reply({ - embeds: [ - new EmojiEmbed() - .setTitle("Transcript") - .setDescription( - "The transcript was empty, so no changes were made. To delete this ticket, press the delete button below." - ) - .setStatus("Success") - .setEmoji("CONTROL.DOWNLOAD") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel("Delete") - .setStyle(ButtonStyle.Danger) - .setCustomId("close") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - ]) - ], - fetchReply: true - })) as Message; + topic = interaction.channel.name; + const split = topic.split("-").map(p => p.trim()) as [string, string, string]; + const mem = interaction.guild!.members.cache.get(split[1]) + if (mem) member = mem; } + + const newOut = await client.database.transcripts.createTranscript(messages, interaction, member); + + const code = await client.database.transcripts.create(newOut); + if(!code) return await interaction.reply({ + embeds: [ + new EmojiEmbed() + .setTitle("Error") + .setDescription("An error occurred while creating the transcript.") + .setStatus("Danger") + .setEmoji("CONTROL.BLOCKCROSS") + ] + }) + const guildConfig = await client.database.guilds.read(interaction.guild!.id); + const m: Message = (await interaction.reply({ + embeds: [ + new EmojiEmbed() + .setTitle("Transcript") + .setDescription( + "You can view the transcript using the link below. You can save the link for later" + + (guildConfig.logging.logs.channel + ? ` or find it in <#${guildConfig.logging.logs.channel}> once you press delete below. After this the channel will be deleted.` + : ".") + ) + .setStatus("Success") + .setEmoji("CONTROL.DOWNLOAD") + ], + components: [ + new ActionRowBuilder().addComponents([ + new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript/${code}`), + new ButtonBuilder() + .setLabel("Delete") + .setStyle(ButtonStyle.Danger) + .setCustomId("close") + .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) + ]) + ], + fetchReply: true + })) as Message; let i; try { i = await m.awaitMessageComponent({ time: 300000, - filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id } + filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id } }); - i.deferUpdate(); + await i.deferUpdate(); } catch { return; } @@ -128,12 +113,13 @@ export default async function (interaction: CommandInteraction | MessageComponen calculateType: "ticketUpdate", color: NucleusColors.red, emoji: "GUILD.TICKET.CLOSE", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { - ticketFor: member ? entry(member.id, renderUser(member.user)) : entry(null, "*Unknown*"), + ticketFor: entry(member.id, renderUser(member.user)), deletedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as User)), - deleted: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())) + deleted: entry(Date.now().toString(), renderDelta(Date.now())), + transcript: entry(code, `https://clicks.codes/nucleus/transcript/${code}`) }, hidden: { guild: interaction.guild!.id diff --git a/src/reflex/guide.ts b/src/reflex/guide.ts index 6829ef2..a3027e4 100644 --- a/src/reflex/guide.ts +++ b/src/reflex/guide.ts @@ -1,9 +1,9 @@ +import { getCommandMentionByName } from './../utils/getCommandDataByName.js'; import { LoadingEmbed } from "../utils/defaults.js"; import Discord, { ActionRowBuilder, ButtonBuilder, MessageComponentInteraction, - StringSelectMenuInteraction, Guild, CommandInteraction, GuildTextBasedChannel, @@ -19,32 +19,43 @@ import { Embed } from "../utils/defaults.js"; export default async (guild: Guild, interaction?: CommandInteraction) => { let c: GuildTextBasedChannel | null = guild.publicUpdatesChannel ? guild.publicUpdatesChannel : guild.systemChannel; c = c - ? c - : (guild.channels.cache.find( - (ch) => - [ - ChannelType.GuildText, - ChannelType.GuildAnnouncement, - ChannelType.PublicThread, - ChannelType.PrivateThread, - ChannelType.AnnouncementThread - ].includes(ch.type) && - ch.permissionsFor(guild.roles.everyone).has("SendMessages") && - ch.permissionsFor(guild.members.me!).has("EmbedLinks") - ) as GuildTextBasedChannel | undefined) ?? null; + ? c + : (guild.channels.cache.find( + (ch) => + [ + ChannelType.GuildText, + ChannelType.GuildAnnouncement, + ChannelType.PublicThread, + ChannelType.PrivateThread, + ChannelType.AnnouncementThread + ].includes(ch.type) && + ch.permissionsFor(guild.roles.everyone).has("SendMessages") && + ch.permissionsFor(guild.members.me!).has("EmbedLinks") + ) as GuildTextBasedChannel | undefined) ?? null; if (interaction) c = interaction.channel as GuildTextBasedChannel; if (!c) { return; } + let m: Message; + if (interaction) { + m = (await interaction.reply({ + embeds: LoadingEmbed, + fetchReply: true, + ephemeral: true + })) as Message; + } else { + m = await c.send({ embeds: LoadingEmbed }); + } + let page = 0; const pages = [ new Embed() .setEmbed( new EmojiEmbed() .setTitle("Welcome to Nucleus") .setDescription( - "Thanks for adding Nucleus to your server\n\n" + - "On the next few pages you can find instructions on getting started, and commands you may want to set up\n\n" + - "If you need support, have questions or want features, you can let us know in [Clicks](https://discord.gg/bPaNnxe)" + "Thanks for adding Nucleus to your server!\n\n" + + "The next few pages will show what features Nucleus has to offer, and how to enable them.\n\n" + + "If you need support, have questions or want features, you can let us know in [Clicks](https://discord.gg/bPaNnxe)!" ) .setEmoji("NUCLEUS.LOGO") .setStatus("Danger") @@ -55,15 +66,17 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { new Embed() .setEmbed( new EmojiEmbed() - .setTitle("Logging") + .setTitle("Logs") .setDescription( "Nucleus can log server events and keep you informed with what content is being posted to your server.\n" + "We have 2 different types of logs, which each can be configured to send to a channel of your choice:\n" + - "**General Logs:** These are events like kicks and channel changes etc.\n" + - "**Warning Logs:** Warnings like NSFW avatars and spam etc. that may require action by a server staff member. " + - "These go to to a separate staff notifications channel.\n\n" + - "A general log channel can be set with `/settings log`\n" + - "A warning log channel can be set with `/settings warnings channel`" + "**General:** These are events like kicks and channel changes etc.\n" + + `> These are standard logs and can be set with ${getCommandMentionByName("settings/logs/general")}\n` + + "**Warnings:** Warnings like NSFW avatars and spam etc. that may require action by a server staff member.\n" + + `> These may require special action by a moderator. You can set the channel with ${getCommandMentionByName("settings/logs/warnings")}\n` + + "**Attachments:** All images sent in the server - Used to keep a record of deleted images\n" + + `> Sent to a separate log channel to avoid spam. This can be set with ${getCommandMentionByName("settings/logs/attachments")}\n` + + `> ${getEmojiByName("NUCLEUS.PREMIUM")} Please note this feature is only available with ${getCommandMentionByName("nucleus/premium")}` ) .setEmoji("ICONS.LOGGING") .setStatus("Danger") @@ -77,27 +90,15 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { .setTitle("Moderation") .setDescription( "Nucleus has a number of commands that can be used to moderate your server.\n" + - "These commands are all found under `/mod`, and they include:\n" + - `**${getEmojiByName( - "PUNISH.WARN.YELLOW" - )} Warn:** The user is warned (via DM) that they violated server rules.\n` + - `**${getEmojiByName( - "PUNISH.CLEARHISTORY" - )} Clear:** Some messages from a user are deleted in a channel.\n` + - `**${getEmojiByName( - "PUNISH.MUTE.YELLOW" - )} Mute:** The user is unable to send messages or join voice chats.\n` + - `**${getEmojiByName( - "PUNISH.MUTE.GREEN" - )} Unmute:** The user is able to send messages in the server.\n` + - `**${getEmojiByName("PUNISH.KICK.RED")} Kick:** The user is removed from the server.\n` + - `**${getEmojiByName( - "PUNISH.SOFTBAN" - )} Softban:** Kicks the user, deleting their messages from every channel.\n` + - `**${getEmojiByName( - "PUNISH.BAN.RED" - )} Ban:** The user is removed from the server, and they are unable to rejoin.\n` + - `**${getEmojiByName("PUNISH.BAN.GREEN")} Unban:** The user is able to rejoin the server.` + `These commands are all found under ${getCommandMentionByName(("mod"))}, and they include:\n` + + `${getEmojiByName("PUNISH.WARN.YELLOW")} ${getCommandMentionByName("mod/warn")}: The user is warned (via DM) that they violated server rules. More options given if DMs are disabled.\n` + + `${getEmojiByName("PUNISH.CLEARHISTORY")} ${getCommandMentionByName("mod/purge")}: Deletes messages in a channel, giving options to only delete messages by a certain user.\n` + + `${getEmojiByName("PUNISH.MUTE.YELLOW")} ${getCommandMentionByName("mod/mute")}: Stops users sending messages or joining voice chats.\n` + + `${getEmojiByName("PUNISH.MUTE.GREEN")} ${getCommandMentionByName("mod/unmute")}: Allows user to send messages and join voice chats.\n` + + `${getEmojiByName("PUNISH.KICK.RED")} ${getCommandMentionByName("mod/kick")}: Removes a member from the server. They will be able to rejoin.\n` + + `${getEmojiByName("PUNISH.SOFTBAN")} ${getCommandMentionByName("mod/softban")}: Kicks the user, deleting their messages from every channel in a given time frame.\n` + + `${getEmojiByName("PUNISH.BAN.RED")} ${getCommandMentionByName("mod/ban")}: Removes the user from the server, deleting messages from every channel and stops them from rejoining.\n` + + `${getEmojiByName("PUNISH.BAN.GREEN")} ${getCommandMentionByName("mod/unban")}: Allows a member to rejoin the server after being banned.` ) .setEmoji("PUNISH.BAN.RED") .setStatus("Danger") @@ -111,9 +112,9 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { .setTitle("Verify") .setDescription( "Nucleus has a verification system that allows users to prove they aren't bots.\n" + - "This is done by running `/verify` which sends a message only the user can see, giving them a link to a CAPTCHA to verify.\n" + - "After the user complete's the CAPTCHA, they are given a role and can use the permissions accordingly.\n" + - "You can set the role given with `/settings verify`" + `This is done by running ${getCommandMentionByName("verify")} which sends a message only the user can see, giving them a link to a website to verify.\n` + + "After the user complete's the check, they are given a role, which can be set to unlock specific channels.\n" + + `You can set the role given with ${getCommandMentionByName("settings/verify")}` ) .setEmoji("CONTROL.REDTICK") .setStatus("Danger") @@ -127,8 +128,8 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { .setTitle("Content Scanning") .setDescription( "Nucleus has a content scanning system that automatically scans links and images sent by users.\n" + - "Nucleus can detect, delete, and punish users for sending NSFW content, or links to scam or adult sites.\n" + - "You can set the threshold for this in `/settings automation`" // TODO + "The staff team can be notified when an NSFW image is detected, or malicious links are sent.\n" + + `You can check and manage what to moderate in ${getCommandMentionByName("settings/automod")}` ) .setEmoji("MOD.IMAGES.TOOSMALL") .setStatus("Danger") @@ -141,10 +142,12 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { new EmojiEmbed() .setTitle("Tickets") .setDescription( - "Nucleus has a ticket system that allows users to create tickets and have a support team respond to them.\n" + - "Tickets can be created with `/ticket create` and a channel is created, pinging the user and support role.\n" + - "When the ticket is resolved, anyone can run `/ticket close` (or click the button) to close it.\n" + - "Running `/ticket close` again will delete the ticket." + "Nucleus has a ticket system which allows users to create tickets and talk to the server staff or support team.\n" + + `Tickets can be created by users with ${getCommandMentionByName("ticket/create")}, or by clicking a button created by moderators.\n` + + `After being created, a new channel or thread is created, and the user and support team are pinged. \n` + + `The category or channel to create threads in can be set with ${getCommandMentionByName("settings/tickets")}\n` + + `When the ticket is resolved, anyone can run ${getCommandMentionByName("ticket/close")} (or click the button) to close it.\n` + + `Running ${getCommandMentionByName("ticket/close")} again will delete the ticket.` ) .setEmoji("GUILD.TICKET.CLOSE") .setStatus("Danger") @@ -157,11 +160,10 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { new EmojiEmbed() .setTitle("Tags") .setDescription( - "Add a tag system to your server with the `/tag` and `/tags` commands.\n" + - "To create a tag, type `/tags create `.\n" + - "Tag names and content can be edited with `/tags edit`.\n" + - "To delete a tag, type `/tags delete `.\n" + - "To view all tags, type `/tags list`.\n" + "Nucleus allows you to create tags, which allow a message to be sent when a specific tag is typed.\n" + + `Tags can be created with ${getCommandMentionByName("tags/create")}, and can be edited with ${getCommandMentionByName("tags/edit")}\n` + + `Tags can be deleted with ${getCommandMentionByName("tags/delete")}, and can be listed with ${getCommandMentionByName("tags/list")}\n` + + `To use a tag, you can type ${getCommandMentionByName("tag")}, followed by the tag to send` ) .setEmoji("PUNISH.NICKNAME.RED") .setStatus("Danger") @@ -174,29 +176,18 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { new EmojiEmbed() .setTitle("Premium") .setDescription( - "In the near future, we will be releasing extra premium only features.\n" + - "These features will include:\n\n" + - "**Attachment logs**\n> When a message with attachments is edited or deleted, the logs will also include the images sent.\n" + - "\nPremium is not yet available. Check `/nucleus premium` for updates on features and pricing" + "Nucleus Premium allows you to use extra features in your server, which are useful but not essential.\n" + + "**No currently free commands will become premium features.**\n" + + "Premium features include creating ticket transcripts and attachment logs.\n\n" + + "Premium can be purchased in [our server](https://discord.gg/bPaNnxe) in the subscriptions page" // TODO: add a table graphic ) - .setEmoji("NUCLEUS.COMMANDS.LOCK") + .setEmoji("NUCLEUS.PREMIUM") .setStatus("Danger") ) .setTitle("Premium") .setDescription("Premium features") .setPageId(7) ]; - let m: Message; - if (interaction) { - m = (await interaction.reply({ - embeds: LoadingEmbed, - fetchReply: true, - ephemeral: true - })) as Message; - } else { - m = await c.send({ embeds: LoadingEmbed }); - } - let page = 0; const publicFilter = async (component: MessageComponentInteraction) => { return (component.member as Discord.GuildMember).permissions.has("ManageGuild"); @@ -274,7 +265,7 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { timedOut = true; continue; } - i.deferUpdate(); + await i.deferUpdate(); if (!("customId" in i.component)) { continue; } else if (i.component.customId === "left") { @@ -285,8 +276,8 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { selectPaneOpen = false; } else if (i.component.customId === "select") { selectPaneOpen = !selectPaneOpen; - } else if (i.component.customId === "page") { - page = parseInt((i as StringSelectMenuInteraction).values[0]!); + } else if (i.isStringSelectMenu() && i.component.customId === "page") { + page = parseInt(i.values[0]!); selectPaneOpen = false; } else { cancelled = true; diff --git a/src/reflex/scanners.ts b/src/reflex/scanners.ts index 9761e4b..cf713e6 100644 --- a/src/reflex/scanners.ts +++ b/src/reflex/scanners.ts @@ -1,23 +1,27 @@ import fetch from "node-fetch"; -import FormData from "form-data"; -import { writeFileSync, createReadStream } from "fs"; +import fs, { writeFileSync, createReadStream } from "fs"; import generateFileName from "../utils/temp/generateFileName.js"; import Tesseract from "node-tesseract-ocr"; import type Discord from "discord.js"; import client from "../utils/client.js"; +import { createHash } from "crypto"; interface NSFWSchema { nsfw: boolean; + errored?: boolean; } interface MalwareSchema { safe: boolean; + errored?: boolean; } export async function testNSFW(link: string): Promise { - const p = await saveAttachment(link); - const data = new FormData(); - console.log(link); - data.append("file", createReadStream(p)); + const [p, hash] = await saveAttachment(link); + const alreadyHaveCheck = await client.database.scanCache.read(hash) + if(alreadyHaveCheck) return { nsfw: alreadyHaveCheck.data }; + const data = new URLSearchParams(); + const r = createReadStream(p) + data.append("file", r.read(fs.statSync(p).size)); const result = await fetch("https://unscan.p.rapidapi.com/", { method: "POST", headers: { @@ -26,20 +30,24 @@ export async function testNSFW(link: string): Promise { }, body: data }) - .then((response) => response.json() as Promise) + .then((response) => response.status === 200 ? response.json() as Promise : { nsfw: false, errored: true }) .catch((err) => { console.error(err); - return { nsfw: false }; + return { nsfw: false, errored: true }; }); - console.log(result); + if(!result.errored) { + client.database.scanCache.write(hash, result.nsfw); + } return { nsfw: result.nsfw }; } export async function testMalware(link: string): Promise { - const p = await saveAttachment(link); - const data = new FormData(); - data.append("file", createReadStream(p)); - console.log(link); + const [p, hash] = await saveAttachment(link); + const alreadyHaveCheck = await client.database.scanCache.read(hash) + if(alreadyHaveCheck) return { safe: alreadyHaveCheck.data }; + const data = new URLSearchParams(); + const f = createReadStream(p); + data.append("file", f.read(fs.statSync(p).size)); const result = await fetch("https://unscan.p.rapidapi.com/malware", { method: "POST", headers: { @@ -48,18 +56,21 @@ export async function testMalware(link: string): Promise { }, body: data }) - .then((response) => response.json() as Promise) + .then((response) => response.status === 200 ? response.json() as Promise : { safe: true, errored: true }) .catch((err) => { console.error(err); - return { safe: true }; + return { safe: true, errored: true }; }); - console.log(result); + if (!result.errored) { + client.database.scanCache.write(hash, result.safe); + } return { safe: result.safe }; } export async function testLink(link: string): Promise<{ safe: boolean; tags: string[] }> { - console.log(link); - const scanned: { safe?: boolean; tags?: string[] } = await fetch("https://unscan.p.rapidapi.com/malware", { + const alreadyHaveCheck = await client.database.scanCache.read(link) + if(alreadyHaveCheck) return { safe: alreadyHaveCheck.data, tags: [] }; + const scanned: { safe?: boolean; tags?: string[] } = await fetch("https://unscan.p.rapidapi.com/link", { method: "POST", headers: { "X-RapidAPI-Key": client.config.rapidApiKey, @@ -72,18 +83,19 @@ export async function testLink(link: string): Promise<{ safe: boolean; tags: str console.error(err); return { safe: true, tags: [] }; }); - console.log(scanned); + client.database.scanCache.write(link, scanned.safe ?? true, []); return { safe: scanned.safe ?? true, tags: scanned.tags ?? [] }; } -export async function saveAttachment(link: string): Promise { - const image = (await (await fetch(link)).buffer()).toString("base64"); +export async function saveAttachment(link: string): Promise<[string, string]> { + const image = await (await fetch(link)).arrayBuffer() const fileName = generateFileName(link.split("/").pop()!.split(".").pop()!); - writeFileSync(fileName, image, "base64"); - return fileName; + const enc = new TextDecoder("utf-8"); + writeFileSync(fileName, new DataView(image), "base64"); + return [fileName, createHash('sha512').update(enc.decode(image), 'base64').digest('base64')]; } const linkTypes = { @@ -139,8 +151,7 @@ export async function LinkCheck(message: Discord.Message): Promise { export async function NSFWCheck(element: string): Promise { try { - const test = await testNSFW(element); - return test.nsfw; + return (await testNSFW(element)).nsfw; } catch { return false; } diff --git a/src/reflex/statsChannelUpdate.ts b/src/reflex/statsChannelUpdate.ts index db705d9..daa82fd 100644 --- a/src/reflex/statsChannelUpdate.ts +++ b/src/reflex/statsChannelUpdate.ts @@ -1,4 +1,4 @@ -import { getCommandMentionByName } from '../utils/getCommandMentionByName.js'; +import { getCommandMentionByName } from '../utils/getCommandDataByName.js'; import type { Guild, User } from "discord.js"; import type { NucleusClient } from "../utils/client.js"; import type { GuildMember } from "discord.js"; @@ -32,7 +32,7 @@ export async function callback(client: NucleusClient, member?: GuildMember, guil return singleNotify( "statsChannelDeleted", guild!.id, - `One or more of your stats channels have been deleted. You can use ${await getCommandMentionByName("settings/stats")}.\n` + + `One or more of your stats channels have been deleted. You can use ${getCommandMentionByName("settings/stats")}.\n` + `The channels name was: ${deleted!.name}`, "Critical" ); diff --git a/src/reflex/verify.ts b/src/reflex/verify.ts index 573da5e..290e372 100644 --- a/src/reflex/verify.ts +++ b/src/reflex/verify.ts @@ -13,6 +13,8 @@ import fetch from "node-fetch"; import { TestString, NSFWCheck } from "./scanners.js"; import createPageIndicator from "../utils/createPageIndicator.js"; import client from "../utils/client.js"; +import singleNotify from "../utils/singleNotify.js"; +import { getCommandMentionByName } from "../utils/getCommandDataByName.js"; export interface VerifySchema { uID: string; @@ -182,14 +184,14 @@ export default async function (interaction: CommandInteraction | ButtonInteracti let itt = 0; const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; do { - itt += 1; + itt ++; code = ""; for (let i = 0; i < length; i++) { code += chars.charAt(Math.floor(Math.random() * chars.length)); } if (itt > 1000) { itt = 0; - length += 1; + length ++; } } while (code in verify); const role: Role | null = await interaction.guild!.roles.fetch(config.verify.role); @@ -206,7 +208,8 @@ export default async function (interaction: CommandInteraction | ButtonInteracti .setEmoji("CONTROL.BLOCKCROSS") ] }); - return; // TODO: SEN + singleNotify("verifyRoleDeleted", interaction.guild!.id, `The role given when a member is verified has been deleted. Use ${getCommandMentionByName("settings/verify")} to set a new one`, "Critical") + return; } verify[code] = { uID: interaction.member!.user.id, diff --git a/src/reflex/welcome.ts b/src/reflex/welcome.ts index 87bb81a..c2eede3 100644 --- a/src/reflex/welcome.ts +++ b/src/reflex/welcome.ts @@ -1,4 +1,4 @@ -import { getCommandMentionByName } from './../utils/getCommandMentionByName.js'; +import { getCommandMentionByName } from './../utils/getCommandDataByName.js'; import type { NucleusClient } from "../utils/client.js"; import convertCurlyBracketString from "../utils/convertCurlyBracketString.js"; import client from "../utils/client.js"; @@ -27,19 +27,19 @@ export async function callback(_client: NucleusClient, member: GuildMember) { }); } else { const channel: GuildChannel | null = await member.guild.channels.fetch(config.welcome.channel) as GuildChannel | null; - if (!channel) return await singleNotify("welcomeChannelDeleted", member.guild.id, `The welcome channel has been deleted or is no longer accessible. Use ${await getCommandMentionByName("settings/welcome")} to set a new one`, "Warning") + if (!channel) return await singleNotify("welcomeChannelDeleted", member.guild.id, `The welcome channel has been deleted or is no longer accessible. Use ${getCommandMentionByName("settings/welcome")} to set a new one`, "Warning") if (!(channel instanceof BaseGuildTextChannel)) return; if (channel.guild.id !== member.guild.id) return; try { await channel.send({ embeds: [new EmojiEmbed().setDescription(string).setStatus("Success")], - content: (config.welcome.ping ? `<@${config.welcome.ping}>` : "") + `<@${member.id}>` + content: (config.welcome.ping ? `<@&${config.welcome.ping}>` : "") + `<@${member.id}>` }); } catch (err) { singleNotify( "welcomeChannelDeleted", member.guild.id, - `The welcome channel has been deleted or is no longer accessible. Use ${await getCommandMentionByName("settings/welcome")} to set a new one`, + `The welcome channel has been deleted or is no longer accessible. Use ${getCommandMentionByName("settings/welcome")} to set a new one`, "Warning" ) } diff --git a/src/utils/calculate.ts b/src/utils/calculate.ts index 0bd5a9f..fde1340 100644 --- a/src/utils/calculate.ts +++ b/src/utils/calculate.ts @@ -17,16 +17,14 @@ const logs = [ "webhookUpdate", "guildMemberVerify", "autoModeratorDeleted", - "nucleusSettingsUpdated", - "ticketUpdate" + "ticketUpdate", + // "nucleusSettingsUpdated" ]; const tickets = ["support", "report", "question", "issue", "suggestion", "other"]; const toHexInteger = (permissions: string[], array?: string[]): string => { - if (!array) { - array = logs; - } + if (!array) { array = logs; } let int = 0n; for (const perm of permissions) { diff --git a/src/utils/client.ts b/src/utils/client.ts index 46d9f92..b1fa31f 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -1,11 +1,11 @@ -import Discord, { Client, Interaction, AutocompleteInteraction, GatewayIntentBits } from 'discord.js'; +import Discord, { Client, Interaction, AutocompleteInteraction, Collection } from 'discord.js'; import { Logger } from "../utils/log.js"; import Memory from "../utils/memory.js"; import type { VerifySchema } from "../reflex/verify.js"; -import { Guilds, History, ModNotes, Premium, PerformanceTest } from "../utils/database.js"; +import { Guilds, History, ModNotes, Premium, PerformanceTest, ScanCache, Transcript, } from "../utils/database.js"; import EventScheduler from "../utils/eventScheduler.js"; import type { RoleMenuSchema } from "../actions/roleMenu.js"; -import config from "../config/main.json" assert { type: "json" }; +import config from "../config/main.js"; class NucleusClient extends Client { @@ -22,36 +22,33 @@ class NucleusClient extends Client { premium: Premium; eventScheduler: EventScheduler; performanceTest: PerformanceTest; + scanCache: ScanCache; + transcripts: Transcript }; preloadPage: Record = {}; // e.g. { channelID: { command: privacy, page: 3}} - commands: Record Discord.SlashCommandBuilder) | Discord.SlashCommandSubcommandBuilder | ((builder: Discord.SlashCommandSubcommandBuilder) => Discord.SlashCommandSubcommandBuilder) | Discord.SlashCommandSubcommandGroupBuilder | ((builder: Discord.SlashCommandSubcommandGroupBuilder) => Discord.SlashCommandSubcommandGroupBuilder), callback: (interaction: Interaction) => Promise, - check: (interaction: Interaction) => Promise | boolean, + check: (interaction: Interaction, partial: boolean) => Promise | boolean, autocomplete: (interaction: AutocompleteInteraction) => Promise - }> = {}; - + } | undefined, {name: string, description: string}]> = {}; + fetchedCommands = new Collection(); constructor(database: typeof NucleusClient.prototype.database) { - super({ intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - GatewayIntentBits.GuildPresences, - GatewayIntentBits.GuildMembers - ]}); + super({ intents: 0b1100011011011111111111}); this.database = database; } } - const client = new NucleusClient({ - guilds: await new Guilds().setup(), + guilds: await new Guilds(), history: new History(), notes: new ModNotes(), premium: new Premium(), eventScheduler: new EventScheduler(), - performanceTest: new PerformanceTest() + performanceTest: new PerformanceTest(), + scanCache: new ScanCache(), + transcripts: new Transcript() }); export default client; diff --git a/src/utils/commandRegistration/register.ts b/src/utils/commandRegistration/register.ts index 281be18..33c88b0 100644 --- a/src/utils/commandRegistration/register.ts +++ b/src/utils/commandRegistration/register.ts @@ -1,12 +1,12 @@ import type { CommandInteraction } from 'discord.js'; import Discord, { Interaction, SlashCommandBuilder, ApplicationCommandType } from 'discord.js'; -import config from "../../config/main.json" assert { type: "json" }; +import config from "../../config/main.js"; import client from "../client.js"; import fs from "fs"; import EmojiEmbed from '../generateEmojiEmbed.js'; import getEmojiByName from '../getEmojiByName.js'; -const colours = { +const colors = { red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", @@ -26,23 +26,26 @@ async function registerCommands() { for (const file of files) { const last = i === files.length - 1 ? "└" : "├"; if (file.isDirectory()) { - console.log(`${last}─ ${colours.yellow}Loading subcommands of ${file.name}${colours.none}`) - const fetched = (await import(`../../../${config.commandsFolder}/${file.name}/_meta.js`)).command; - commands.push(fetched); + console.log(`${last}─ ${colors.yellow}Loading subcommands of ${file.name}${colors.none}`) + const fetched = (await import(`../../../${config.commandsFolder}/${file.name}/_meta.js`)); + commands.push(fetched.command); } else if (file.name.endsWith(".js")) { - console.log(`${last}─ ${colours.yellow}Loading command ${file.name}${colours.none}`) + console.log(`${last}─ ${colors.yellow}Loading command ${file.name}${colors.none}`) const fetched = (await import(`../../../${config.commandsFolder}/${file.name}`)); fetched.command.setDMPermission(fetched.allowedInDMs ?? false) fetched.command.setNameLocalizations(fetched.nameLocalizations ?? {}) fetched.command.setDescriptionLocalizations(fetched.descriptionLocalizations ?? {}) - if (fetched.nameLocalizations || fetched.descriptionLocalizations) console.log("AAAAA") + // if (fetched.nameLocalizations || fetched.descriptionLocalizations) commands.push(fetched.command); - client.commands["commands/" + fetched.command.name] = fetched; + client.commands["commands/" + fetched.command.name] = [ + fetched, + {name: fetched.name ?? fetched.command.name, description: fetched.description ?? fetched.command.description} + ]; } i++; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${files.length}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${files.length}]${colors.none}`) } - console.log(`${colours.yellow}Loaded ${commands.length} commands, processing...`) + console.log(`${colors.yellow}Loaded ${commands.length} commands, processing...`) const processed = [] for (const subcommand of commands) { @@ -53,7 +56,7 @@ async function registerCommands() { } } - console.log(`${colours.green}Processed ${processed.length} commands`) + console.log(`${colors.green}Processed ${processed.length} commands${colors.none}`) return processed; }; @@ -70,15 +73,15 @@ async function registerEvents() { const last = i === files.length - 1 ? "└" : "├"; i++; try { - console.log(`${last}─ ${colours.yellow}Loading event ${file.name}${colours.none}`) + console.log(`${last}─ ${colors.yellow}Loading event ${file.name}${colors.none}`) const event = (await import(`../../../${config.eventsFolder}/${file.name}`)); client.on(event.event, event.callback.bind(null, client)); - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${files.length}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${files.length}]${colors.none}`) } catch (e) { errors++; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.red}Failed to load ${file.name} [${i} / ${files.length}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.red}Failed to load ${file.name} [${i} / ${files.length}]${colors.none}`) } } console.log(`Loaded ${files.length - errors} events (${errors} failed)`) @@ -101,36 +104,36 @@ async function registerContextMenus() { const last = i === totalFiles - 1 ? "└" : "├"; i++; try { - console.log(`${last}─ ${colours.yellow}Loading message context menu ${file.name}${colours.none}`) + console.log(`${last}─ ${colors.yellow}Loading message context menu ${file.name}${colors.none}`) const context = (await import(`../../../${config.messageContextFolder}/${file.name}`)); context.command.setType(ApplicationCommandType.Message); context.command.setDMPermission(context.allowedInDMs ?? false) context.command.setNameLocalizations(context.nameLocalizations ?? {}) commands.push(context.command); - client.commands["contextCommands/message/" + context.command.name] = context; + client.commands["contextCommands/message/" + context.command.name] = [context, {name: context.name ?? context.command.name, description: context.description ?? context.command.description}]; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${totalFiles}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${totalFiles}]${colors.none}`) } catch (e) { errors++; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.red}Failed to load ${file.name} [${i} / ${totalFiles}] | ${e}${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.red}Failed to load ${file.name} [${i} / ${totalFiles}] | ${e}${colors.none}`) } } for (const file of userFiles) { const last = i === totalFiles - 1 ? "└" : "├"; i++; try { - console.log(`${last}─ ${colours.yellow}Loading user context menu ${file.name}${colours.none}`) + console.log(`${last}─ ${colors.yellow}Loading user context menu ${file.name}${colors.none}`) const context = (await import(`../../../${config.userContextFolder}/${file.name}`)); context.command.setType(ApplicationCommandType.User); commands.push(context.command); client.commands["contextCommands/user/" + context.command.name] = context; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${totalFiles}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${totalFiles}]${colors.none}`) } catch (e) { errors++; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.red}Failed to load ${file.name} [${i} / ${totalFiles}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.red}Failed to load ${file.name} [${i} / ${totalFiles}]${colors.none}`) } } @@ -142,11 +145,11 @@ async function registerCommandHandler() { client.on("interactionCreate", async (interaction: Interaction) => { if (interaction.isUserContextMenuCommand()) {; const commandName = "contextCommands/user/" + interaction.commandName; - execute(client.commands[commandName]?.check, client.commands[commandName]?.callback, interaction) + execute(client.commands[commandName]![0]?.check, client.commands[commandName]![0]?.callback, interaction) return; } else if (interaction.isMessageContextMenuCommand()) { const commandName = "contextCommands/message/" + interaction.commandName; - execute(client.commands[commandName]?.check, client.commands[commandName]?.callback, interaction) + execute(client.commands[commandName]![0]?.check, client.commands[commandName]![0]?.callback, interaction) return; } else if (interaction.isAutocomplete()) { const commandName = interaction.commandName; @@ -155,7 +158,7 @@ async function registerCommandHandler() { const fullCommandName = "commands/" + commandName + (subcommandGroupName ? `/${subcommandGroupName}` : "") + (subcommandName ? `/${subcommandName}` : ""); - const choices = await client.commands[fullCommandName]?.autocomplete(interaction); + const choices = await client.commands[fullCommandName]![0]?.autocomplete(interaction); const formatted = (choices ?? []).map(choice => { return { name: choice, value: choice } @@ -168,7 +171,8 @@ async function registerCommandHandler() { const fullCommandName = "commands/" + commandName + (subcommandGroupName ? `/${subcommandGroupName}` : "") + (subcommandName ? `/${subcommandName}` : ""); - const command = client.commands[fullCommandName]; + // console.log(fullCommandName, client.commands[fullCommandName]) + const command = client.commands[fullCommandName]![0]; const callback = command?.callback; const check = command?.check; execute(check, callback, interaction); @@ -177,6 +181,7 @@ async function registerCommandHandler() { } async function execute(check: Function | undefined, callback: Function | undefined, data: CommandInteraction) { + // console.log(client.commands["contextCommands/user/User info"]) if (!callback) return; if (check) { let result; @@ -189,12 +194,11 @@ async function execute(check: Function | undefined, callback: Function | undefin if (typeof result === "string") { const { NucleusColors } = client.logger return data.reply({embeds: [new EmojiEmbed() - .setTitle("") .setDescription(result) .setColor(NucleusColors.red) .setEmoji(getEmojiByName("CONTROL.BLOCKCROSS")) - ]}); - }; + ], ephemeral: true}); + } } callback(data); } @@ -207,17 +211,20 @@ export default async function register() { if (process.argv.includes("--update-commands")) { if (config.enableDevelopment) { const guild = await client.guilds.fetch(config.developmentGuildID); - console.log(`${colours.purple}Registering commands in ${guild!.name}${colours.none}`) + console.log(`${colors.purple}Registering commands in ${guild!.name}${colors.none}`) await guild.commands.set(commandList); } else { - console.log(`${colours.blue}Registering commands in production mode${colours.none}`) + console.log(`${colors.blue}Registering commands in production mode${colors.none}`) + const guild = await client.guilds.fetch(config.developmentGuildID); + await guild.commands.set([]); await client.application?.commands.set(commandList); } } await registerCommandHandler(); await registerEvents(); - console.log(`${colours.green}Registered commands, events and context menus${colours.none}`) + console.log(`${colors.green}Registered commands, events and context menus${colors.none}`) console.log( - (config.enableDevelopment ? `${colours.purple}Bot started in Development mode` : - `${colours.blue}Bot started in Production mode`) + colours.none) + (config.enableDevelopment ? `${colors.purple}Bot started in Development mode` : + `${colors.blue}Bot started in Production mode`) + colors.none + ) }; diff --git a/src/utils/commandRegistration/slashCommandBuilder.ts b/src/utils/commandRegistration/slashCommandBuilder.ts index b2927d6..66291b3 100644 --- a/src/utils/commandRegistration/slashCommandBuilder.ts +++ b/src/utils/commandRegistration/slashCommandBuilder.ts @@ -1,12 +1,12 @@ -import type { SlashCommandSubcommandGroupBuilder } from "@discordjs/builders"; +import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from "discord.js"; import type { SlashCommandBuilder } from "discord.js"; -import config from "../../config/main.json" assert { type: "json" }; +import config from "../../config/main.js"; import getSubcommandsInFolder from "./getFilesInFolder.js"; import client from "../client.js"; import Discord from "discord.js"; -const colours = { +const colors = { red: "\x1b[31m", green: "\x1b[32m", none: "\x1b[0m" @@ -23,7 +23,7 @@ export async function group( // If the name of the command does not match the path (e.g. attachment.ts has /attachments), use commandString console.log(`│ ├─ Loading group ${name}`) const fetched = await getSubcommandsInFolder(config.commandsFolder + "/" + path, "│ ") - console.log(`│ │ └─ ${fetched.errors ? colours.red : colours.green}Loaded ${fetched.subcommands.length} subcommands for ${name} (${fetched.errors} failed)${colours.none}`) + console.log(`│ │ └─ ${fetched.errors ? colors.red : colors.green}Loaded ${fetched.subcommands.length} subcommands for ${name} (${fetched.errors} failed)${colors.none}`) return (subcommandGroup: SlashCommandSubcommandGroupBuilder) => { subcommandGroup .setName(name) @@ -32,7 +32,9 @@ export async function group( if (descriptionLocalizations) { subcommandGroup.setDescriptionLocalizations(descriptionLocalizations) } for (const subcommand of fetched.subcommands) { - subcommandGroup.addSubcommand(subcommand.command); + const processedCommand = subcommand.command(new SlashCommandSubcommandBuilder()); + client.commands["commands/" + path + "/" + processedCommand.name] = [subcommand, { name: processedCommand.name, description: processedCommand.description }] + subcommandGroup.addSubcommand(processedCommand); }; return subcommandGroup; @@ -52,7 +54,9 @@ export async function command( // If the name of the command does not match the path (e.g. attachment.ts has /attachments), use commandString commandString = "commands/" + (commandString ?? path); const fetched = await getSubcommandsInFolder(config.commandsFolder + "/" + path); - console.log(`│ ├─ ${fetched.errors ? colours.red : colours.green}Loaded ${fetched.subcommands.length} subcommands and ${fetched.subcommandGroups.length} subcommand groups for ${name} (${fetched.errors} failed)${colours.none}`) + console.log(`│ ├─ ${fetched.errors ? colors.red : colors.green}Loaded ${fetched.subcommands.length} subcommands and ${fetched.subcommandGroups.length} subcommand groups for ${name} (${fetched.errors} failed)${colors.none}`) + // console.log({name: name, description: description}) + client.commands[commandString!] = [undefined, { name: name, description: description }] return (command: SlashCommandBuilder) => { command.setName(name) command.setDescription(description) @@ -68,15 +72,17 @@ export async function command( for (const subcommand of fetched.subcommands) { let fetchedCommand; if (subcommand.command instanceof Function) { - fetchedCommand = subcommand.command(new Discord.SlashCommandSubcommandBuilder()); + fetchedCommand = subcommand.command(new SlashCommandSubcommandBuilder()); } else { fetchedCommand = subcommand.command; } - client.commands[commandString! + "/" + fetchedCommand.name] = subcommand + client.commands[commandString! + "/" + fetchedCommand.name] = [subcommand, { name: fetchedCommand.name, description: fetchedCommand.description }] command.addSubcommand(fetchedCommand); } for (const group of fetched.subcommandGroups) { - command.addSubcommandGroup(group.command); + const processedCommand = group.command(new SlashCommandSubcommandGroupBuilder()); + client.commands[commandString! + "/" + processedCommand.name] = [undefined, { name: processedCommand.name, description: processedCommand.description }] + command.addSubcommandGroup(processedCommand); }; return command; }; diff --git a/src/utils/confirmationMessage.ts b/src/utils/confirmationMessage.ts index 4d90676..f7cccaf 100644 --- a/src/utils/confirmationMessage.ts +++ b/src/utils/confirmationMessage.ts @@ -1,11 +1,9 @@ -import { TextInputBuilder } from "@discordjs/builders"; +import { TextInputBuilder } from "discord.js"; import Discord, { CommandInteraction, - Interaction, Message, ActionRowBuilder, ButtonBuilder, - MessageComponentInteraction, ModalSubmitInteraction, ButtonStyle, TextInputStyle @@ -183,13 +181,12 @@ class confirmationMessage { let component; try { component = await m.awaitMessageComponent({ - filter: (m) => m.user.id === this.interaction.user.id && m.channel!.id === this.interaction.channel!.id, + filter: (i) => i.user.id === this.interaction.user.id && i.channel!.id === this.interaction.channel!.id, time: 300000 }); } catch (e) { success = false; - returnComponents = true; - continue; + break; } if (component.customId === "yes") { component.deferUpdate(); @@ -247,17 +244,12 @@ class confirmationMessage { }); let out; try { - out = await modalInteractionCollector( - m, - (m: Interaction) => - (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === this.interaction.channelId, - (m) => m.customId === "reason" - ); + out = await modalInteractionCollector(m, this.interaction.user) as Discord.ModalSubmitInteraction | null; } catch (e) { cancelled = true; continue; } - if (out === null) { + if (out === null || out.isButton()) { cancelled = true; continue; } @@ -277,23 +269,23 @@ class confirmationMessage { } const returnValue: Awaited> = {}; - if (returnComponents || success !== undefined) returnValue.components = this.customButtons; - if (success !== undefined) returnValue.success = success; if (cancelled) { await this.timeoutError() returnValue.cancelled = true; } - if (success == false) { + if (success === false) { await this.interaction.editReply({ embeds: [new EmojiEmbed() .setTitle(this.title) - .setDescription(this.failedMessage ?? "") + .setDescription(this.failedMessage ?? "*Message timed out*") .setStatus(this.failedStatus ?? "Danger") .setEmoji(this.failedEmoji ?? this.redEmoji ?? this.emoji) ], components: [] }); return {success: false} } + if (returnComponents || success !== undefined) returnValue.components = this.customButtons; + if (success !== undefined) returnValue.success = success; if (newReason) returnValue.newReason = newReason; const typedReturnValue = returnValue as {cancelled: true} | diff --git a/src/utils/convertCurlyBracketString.ts b/src/utils/convertCurlyBracketString.ts index 5d2c23d..058ba16 100644 --- a/src/utils/convertCurlyBracketString.ts +++ b/src/utils/convertCurlyBracketString.ts @@ -13,7 +13,7 @@ async function convertCurlyBracketString( .replace("{member:mention}", memberID ? `<@${memberID}>` : "{member:mention}") .replace("{member:name}", memberName ? `${memberName}` : "{member:name}") .replace("{serverName}", serverName ? `${serverName}` : "{serverName}") - .replace("{memberCount}", memberCount ? `${memberCount}` : "{memberCount}") + .replace("{memberCount:all}", memberCount ? `${memberCount}` : "{memberCount}") .replace("{memberCount:bots}", bots ? `${bots}` : "{memberCount:bots}") .replace("{memberCount:humans}", memberCount && bots ? `${memberCount - bots}` : "{memberCount:humans}"); diff --git a/src/utils/createPageIndicator.ts b/src/utils/createPageIndicator.ts index ee123d6..6bc86a4 100644 --- a/src/utils/createPageIndicator.ts +++ b/src/utils/createPageIndicator.ts @@ -1,17 +1,17 @@ import getEmojiByName from "./getEmojiByName.js"; -function pageIndicator(amount: number, selected: number, showDetails?: boolean | true) { +function pageIndicator(amount: number, selected: number, showDetails?: boolean, disabled?: boolean | string) { let out = ""; - + disabled = disabled ? "GRAY." : "" if (amount === 1) { - out += getEmojiByName("TRACKS.SINGLE." + (selected === 0 ? "ACTIVE" : "INACTIVE")); + out += getEmojiByName("TRACKS.SINGLE." + (disabled) + (selected === 0 ? "ACTIVE" : "INACTIVE")); } else { for (let i = 0; i < amount; i++) { out += getEmojiByName( "TRACKS.HORIZONTAL." + - (i === 0 ? "LEFT" : i === amount - 1 ? "RIGHT" : "MIDDLE") + - "." + - (i === selected ? "ACTIVE" : "INACTIVE") + (i === 0 ? "LEFT" : i === amount - 1 ? "RIGHT" : "MIDDLE") + + "." + (disabled) + + (i === selected ? "ACTIVE" : "INACTIVE") ); } } @@ -21,4 +21,23 @@ function pageIndicator(amount: number, selected: number, showDetails?: boolean | return out; } +export const verticalTrackIndicator = (position: number, active: string | boolean, size: number, disabled: string | boolean) => { + active = active ? "ACTIVE" : "INACTIVE"; + disabled = disabled ? "GRAY." : ""; + if (position === 0 && size === 1) return "TRACKS.SINGLE." + disabled + active; + if (position === size - 1) return "TRACKS.VERTICAL.BOTTOM." + disabled + active; + if (position === 0) return "TRACKS.VERTICAL.TOP." + disabled + active; + return "TRACKS.VERTICAL.MIDDLE." + disabled + active; +}; + +export const createVerticalTrack = (items: string[], active: boolean[], disabled?: boolean[]) => { + let out = ""; + if (!disabled) disabled = new Array(items.length).fill(false); + for (let i = 0; i < items.length; i++) { + out += getEmojiByName(verticalTrackIndicator(i, active[i] ?? false, items.length, disabled[i] ?? false)); + out += items[i] + "\n"; + } + return out; +} + export default pageIndicator; diff --git a/src/utils/createTemporaryStorage.ts b/src/utils/createTemporaryStorage.ts index a684d9d..e8a8073 100644 --- a/src/utils/createTemporaryStorage.ts +++ b/src/utils/createTemporaryStorage.ts @@ -1,6 +1,6 @@ import client from "./client.js"; -function generalException(location: string) { +export function generalException(location: string) { client.noLog.push(location); setTimeout(() => { client.noLog = client.noLog.filter((i: string) => { @@ -29,4 +29,4 @@ export function preloadPage(target: string, command: string, message: string) { }) client.preloadPage = Object.fromEntries(object); }, 60 * 5 * 1000); -} \ No newline at end of file +} diff --git a/src/utils/database.ts b/src/utils/database.ts index 1e8e990..2e64320 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -1,32 +1,45 @@ +import { ButtonStyle, CommandInteraction, ComponentType, GuildMember, Message, MessageComponentInteraction } from "discord.js"; import type Discord from "discord.js"; import { Collection, MongoClient } from "mongodb"; -import config from "../config/main.json" assert { type: "json" }; - -const mongoClient = new MongoClient(config.mongoUrl); +import config from "../config/main.js"; +import client from "../utils/client.js"; +import * as crypto from "crypto"; +import _ from "lodash"; +import defaultData from '../config/default.js'; +// config.mongoOptions.host, { +// auth: { +// username: config.mongoOptions.username, +// password: config.mongoOptions.password +// }, +// authSource: config.mongoOptions.authSource +// } +// mongodb://emails:SweetLife2023!!@127.0.0.1:28180/saveEmail?retryWrites=true&w=majority +const username = encodeURIComponent(config.mongoOptions.username); +const password = encodeURIComponent(config.mongoOptions.password); +const mongoClient = new MongoClient(username ? `mongodb://${username}:${password}@${config.mongoOptions.host}` : `mongodb://${config.mongoOptions.host}`, {authSource: "admin"}); await mongoClient.connect(); -const database = mongoClient.db("Nucleus"); +const database = mongoClient.db(); + +const collectionOptions = { authdb: "admin" }; export class Guilds { guilds: Collection; - defaultData: GuildConfig | null; + defaultData: GuildConfig; constructor() { this.guilds = database.collection("guilds"); - this.defaultData = null; - } - - async setup(): Promise { - this.defaultData = (await import("../config/default.json", { assert: { type: "json" } })) - .default as unknown as GuildConfig; - return this; + this.defaultData = defaultData; } async read(guild: string): Promise { + // console.log("Guild read") const entry = await this.guilds.findOne({ id: guild }); - return Object.assign({}, this.defaultData, entry); + const data = _.clone(this.defaultData!); + return _.merge(data, entry ?? {}); } async write(guild: string, set: object | null, unset: string[] | string = []) { + // console.log("Guild write") // eslint-disable-next-line @typescript-eslint/no-explicit-any const uo: Record = {}; if (!Array.isArray(unset)) unset = [unset]; @@ -41,6 +54,7 @@ export class Guilds { // eslint-disable-next-line @typescript-eslint/no-explicit-any async append(guild: string, key: string, value: any) { + // console.log("Guild append") if (Array.isArray(value)) { await this.guilds.updateOne( { id: guild }, @@ -67,7 +81,7 @@ export class Guilds { value: any, innerKey?: string | null ) { - console.log(Array.isArray(value)); + // console.log("Guild remove") if (innerKey) { await this.guilds.updateOne( { id: guild }, @@ -96,10 +110,255 @@ export class Guilds { } async delete(guild: string) { + // console.log("Guild delete") await this.guilds.deleteOne({ id: guild }); } } +interface TranscriptEmbed { + title?: string; + description?: string; + fields?: { + name: string; + value: string; + inline: boolean; + }[]; + footer?: { + text: string; + iconURL?: string; + }; + color?: number; + timestamp?: string; + author?: { + name: string; + iconURL?: string; + url?: string; + }; +} + +interface TranscriptComponent { + type: number; + style?: ButtonStyle; + label?: string; + description?: string; + placeholder?: string; + emojiURL?: string; +} + +interface TranscriptAuthor { + username: string; + discriminator: number; + nickname?: string; + id: string; + iconURL?: string; + topRole: { + color: number; + badgeURL?: string; + }; + bot: boolean; +} + +interface TranscriptAttachment { + url: string; + filename: string; + size: number; + log?: string; +} + +interface TranscriptMessage { + id: string; + author: TranscriptAuthor; + content?: string; + embeds?: TranscriptEmbed[]; + components?: TranscriptComponent[][]; + editedTimestamp?: number; + createdTimestamp: number; + flags?: string[]; + attachments?: TranscriptAttachment[]; + stickerURLs?: string[]; + referencedMessage?: string | [string, string, string]; // the message id, the channel id, the guild id +} + +interface TranscriptSchema { + code: string; + for: TranscriptAuthor; + type: "ticket" | "purge" + guild: string; + channel: string; + messages: TranscriptMessage[]; + createdTimestamp: number; + createdBy: TranscriptAuthor; +} + +export class Transcript { + transcripts: Collection; + + constructor() { + this.transcripts = database.collection("transcripts"); + } + + async create(transcript: Omit) { + // console.log("Transcript create") + let code; + do { + code = crypto.randomBytes(64).toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-"); + } while (await this.transcripts.findOne({ code: code })); + + const doc = await this.transcripts.insertOne(Object.assign(transcript, { code: code }), collectionOptions); + if(doc.acknowledged) return code; + else return null; + } + + async read(code: string) { + // console.log("Transcript read") + return await this.transcripts.findOne({ code: code }); + } + + async createTranscript(messages: Message[], interaction: MessageComponentInteraction | CommandInteraction, member: GuildMember) { + const interactionMember = await interaction.guild?.members.fetch(interaction.user.id) + const newOut: Omit = { + type: "ticket", + for: { + username: member!.user.username, + discriminator: parseInt(member!.user.discriminator), + id: member!.user.id, + topRole: { + color: member!.roles.highest.color + }, + iconURL: member!.user.displayAvatarURL({ forceStatic: true}), + bot: member!.user.bot + }, + guild: interaction.guild!.id, + channel: interaction.channel!.id, + messages: [], + createdTimestamp: Date.now(), + createdBy: { + username: interaction.user.username, + discriminator: parseInt(interaction.user.discriminator), + id: interaction.user.id, + topRole: { + color: interactionMember?.roles.highest.color ?? 0x000000 + }, + iconURL: interaction.user.displayAvatarURL({ forceStatic: true}), + bot: interaction.user.bot + } + } + if(member.nickname) newOut.for.nickname = member.nickname; + if(interactionMember?.roles.icon) newOut.createdBy.topRole.badgeURL = interactionMember.roles.icon.iconURL()!; + messages.reverse().forEach((message) => { + const msg: TranscriptMessage = { + id: message.id, + author: { + username: message.author.username, + discriminator: parseInt(message.author.discriminator), + id: message.author.id, + topRole: { + color: message.member!.roles.highest.color + }, + iconURL: message.member!.user.displayAvatarURL({ forceStatic: true}), + bot: message.author.bot + }, + createdTimestamp: message.createdTimestamp + }; + if(message.member?.nickname) msg.author.nickname = message.member.nickname; + if (message.member!.roles.icon) msg.author.topRole.badgeURL = message.member!.roles.icon.iconURL()!; + if (message.content) msg.content = message.content; + if (message.embeds.length > 0) msg.embeds = message.embeds.map(embed => { + const obj: TranscriptEmbed = {}; + if (embed.title) obj.title = embed.title; + if (embed.description) obj.description = embed.description; + if (embed.fields.length > 0) obj.fields = embed.fields.map(field => { + return { + name: field.name, + value: field.value, + inline: field.inline ?? false + } + }); + if (embed.color) obj.color = embed.color; + if (embed.timestamp) obj.timestamp = embed.timestamp + if (embed.footer) obj.footer = { + text: embed.footer.text, + }; + if (embed.footer?.iconURL) obj.footer!.iconURL = embed.footer.iconURL; + if (embed.author) obj.author = { + name: embed.author.name + }; + if (embed.author?.iconURL) obj.author!.iconURL = embed.author.iconURL; + if (embed.author?.url) obj.author!.url = embed.author.url; + return obj; + }); + if (message.components.length > 0) msg.components = message.components.map(component => component.components.map(child => { + const obj: TranscriptComponent = { + type: child.type + } + if (child.type === ComponentType.Button) { + obj.style = child.style; + obj.label = child.label ?? ""; + } else if (child.type > 2) { + obj.placeholder = child.placeholder ?? ""; + } + return obj + })); + if (message.editedTimestamp) msg.editedTimestamp = message.editedTimestamp; + msg.flags = message.flags.toArray(); + + if (message.stickers.size > 0) msg.stickerURLs = message.stickers.map(sticker => sticker.url); + if (message.reference) msg.referencedMessage = [message.reference.guildId ?? "", message.reference.channelId, message.reference.messageId ?? ""]; + newOut.messages.push(msg); + }); + return newOut; + } + + toHumanReadable(transcript: Omit): string { + let out = ""; + for (const message of transcript.messages) { + if (message.referencedMessage) { + if (Array.isArray(message.referencedMessage)) { + out += `> [Crosspost From] ${message.referencedMessage[0]} in ${message.referencedMessage[1]} in ${message.referencedMessage[2]}\n`; + } + else out += `> [Reply To] ${message.referencedMessage}\n`; + } + out += `${message.author.nickname ?? message.author.username}#${message.author.discriminator} (${message.author.id}) (${message.id})`; + out += ` [${new Date(message.createdTimestamp).toISOString()}]`; + if (message.editedTimestamp) out += ` [Edited: ${new Date(message.editedTimestamp).toISOString()}]`; + out += "\n"; + if (message.content) out += `[Content]\n${message.content}\n\n`; + if (message.embeds) { + for (const embed of message.embeds) { + out += `[Embed]\n`; + if (embed.title) out += `| Title: ${embed.title}\n`; + if (embed.description) out += `| Description: ${embed.description}\n`; + if (embed.fields) { + for (const field of embed.fields) { + out += `| Field: ${field.name} - ${field.value}\n`; + } + } + if (embed.footer) { + out += `|Footer: ${embed.footer.text}\n`; + } + out += "\n"; + } + } + if (message.components) { + for (const component of message.components) { + out += `[Component]\n`; + for (const button of component) { + out += `| Button: ${button.label ?? button.description}\n`; + } + out += "\n"; + } + } + if (message.attachments) { + for (const attachment of message.attachments) { + out += `[Attachment] ${attachment.filename} (${attachment.size} bytes) ${attachment.url}\n`; + } + } + out += "\n\n" + } + return out + } +} + export class History { histories: Collection; @@ -117,6 +376,7 @@ export class History { after?: string | null, amount?: string | null ) { + // console.log("History create"); await this.histories.insertOne({ type: type, guild: guild, @@ -127,10 +387,11 @@ export class History { before: before ?? null, after: after ?? null, amount: amount ?? null - }); + }, collectionOptions); } async read(guild: string, user: string, year: number) { + // console.log("History read"); const entry = (await this.histories .find({ guild: guild, @@ -145,10 +406,41 @@ export class History { } async delete(guild: string) { + // console.log("History delete"); await this.histories.deleteMany({ guild: guild }); } } +interface ScanCacheSchema { + addedAt: Date; + hash: string; + data: boolean; + tags: string[]; +} + +export class ScanCache { + scanCache: Collection; + + constructor() { + this.scanCache = database.collection("scanCache"); + } + + async read(hash: string) { + // console.log("ScanCache read"); + return await this.scanCache.findOne({ hash: hash }); + } + + async write(hash: string, data: boolean, tags?: string[]) { + // console.log("ScanCache write"); + await this.scanCache.insertOne({ hash: hash, data: data, tags: tags ?? [], addedAt: new Date() }, collectionOptions); + } + + async cleanup() { + // console.log("ScanCache cleanup"); + await this.scanCache.deleteMany({ addedAt: { $lt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 31)) }, hash: { $not$text: "http"} }); + } +} + export class PerformanceTest { performanceData: Collection; @@ -157,10 +449,12 @@ export class PerformanceTest { } async record(data: PerformanceDataSchema) { + // console.log("PerformanceTest record"); data.timestamp = new Date(); - await this.performanceData.insertOne(data); + await this.performanceData.insertOne(data, collectionOptions); } async read() { + // console.log("PerformanceTest read"); return await this.performanceData.find({}).toArray(); } } @@ -184,27 +478,161 @@ export class ModNotes { } async create(guild: string, user: string, note: string | null) { + // console.log("ModNotes create"); await this.modNotes.updateOne({ guild: guild, user: user }, { $set: { note: note } }, { upsert: true }); } async read(guild: string, user: string) { + // console.log("ModNotes read"); const entry = await this.modNotes.findOne({ guild: guild, user: user }); return entry?.note ?? null; } + + async delete(guild: string) { + // console.log("ModNotes delete"); + await this.modNotes.deleteMany({ guild: guild }); + } } export class Premium { premium: Collection; + cache: Map; // Date indicates the time one hour after it was created + cacheTimeout = 1000 * 60 * 60; // 1 hour constructor() { this.premium = database.collection("premium"); + this.cache = new Map(); + } + + async updateUser(user: string, level: number) { + // console.log("Premium updateUser"); + if(!(await this.userExists(user))) await this.createUser(user, level); + await this.premium.updateOne({ user: user }, { $set: { level: level } }, { upsert: true }); } - async hasPremium(guild: string) { + async userExists(user: string): Promise { + // console.log("Premium userExists"); + const entry = await this.premium.findOne({ user: user }); + return entry ? true : false; + } + + async createUser(user: string, level: number) { + // console.log("Premium createUser"); + await this.premium.insertOne({ user: user, appliesTo: [], level: level }, collectionOptions); + } + + async hasPremium(guild: string): Promise<[boolean, string, number, boolean] | null> { + // console.log("Premium hasPremium"); + // [Has premium, user giving premium, level, is mod: if given automatically] + const cached = this.cache.get(guild); + if (cached && cached[4].getTime() < Date.now()) return [cached[0], cached[1], cached[2], cached[3]]; + const entries = await this.premium.find({}).toArray(); + const members = (await client.guilds.fetch(guild)).members.cache + for(const {user} of entries) { + const member = members.get(user); + if(member) { //TODO: Notify user if they've given premium to a server that has since gotten premium via a mod. + const modPerms = //TODO: Create list in config for perms + member.permissions.has("Administrator") || + member.permissions.has("ManageChannels") || + member.permissions.has("ManageRoles") || + member.permissions.has("ManageEmojisAndStickers") || + member.permissions.has("ManageWebhooks") || + member.permissions.has("ManageGuild") || + member.permissions.has("KickMembers") || + member.permissions.has("BanMembers") || + member.permissions.has("ManageEvents") || + member.permissions.has("ManageMessages") || + member.permissions.has("ManageThreads") + const entry = entries.find(e => e.user === member.id); + if(entry && (entry.level === 3) && modPerms) { + this.cache.set(guild, [true, member.id, entry.level, true, new Date(Date.now() + this.cacheTimeout)]); + return [true, member.id, entry.level, true]; + } + } + } const entry = await this.premium.findOne({ - appliesTo: { $in: [guild] } + appliesTo: { + $elemMatch: { + $eq: guild + } + } }); - return entry !== null; + this.cache.set(guild, [entry ? true : false, entry?.user ?? "", entry?.level ?? 0, false, new Date(Date.now() + this.cacheTimeout)]); + return entry ? [true, entry.user, entry.level, false] : null; + } + + async fetchUser(user: string): Promise { + // console.log("Premium fetchUser"); + const entry = await this.premium.findOne({ user: user }); + if (!entry) return null; + return entry; + } + + async checkAllPremium(member?: GuildMember) { + // console.log("Premium checkAllPremium"); + const entries = await this.premium.find({}).toArray(); + if(member) { + const entry = entries.find(e => e.user === member.id); + if(entry) { + const expiresAt = entry.expiresAt; + if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: member.id}) : null; + } + const roles = member.roles; + let level = 0; + if (roles.cache.has("1066468879309750313")) { + level = 99; + } else if (roles.cache.has("1066465491713003520")) { + level = 1; + } else if (roles.cache.has("1066439526496604194")) { + level = 2; + } else if (roles.cache.has("1066464134322978912")) { + level = 3; + } + await this.updateUser(member.id, level); + if (level > 0) { + await this.premium.updateOne({ user: member.id }, {$unset: { expiresAt: ""}}) + } else { + await this.premium.updateOne({ user: member.id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }}) + } + } else { + const members = await (await client.guilds.fetch('684492926528651336')).members.fetch(); + for(const {roles, id} of members.values()) { + const entry = entries.find(e => e.user === id); + if(entry) { + const expiresAt = entry.expiresAt; + if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: id}) : null; + } + let level: number = 0; + if (roles.cache.has("1066468879309750313")) { + level = 99; + } else if (roles.cache.has("1066465491713003520")) { + level = 1; + } else if (roles.cache.has("1066439526496604194")) { + level = 2; + } else if (roles.cache.has("1066464134322978912")) { + level = 3; + } + await this.updateUser(id, level); + if (level > 0) { + await this.premium.updateOne({ user: id }, {$unset: { expiresAt: ""}}) + } else { + await this.premium.updateOne({ user: id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }}) + } + } + } + } + + async addPremium(user: string, guild: string) { + // console.log("Premium addPremium"); + const { level } = (await this.fetchUser(user))!; + this.cache.set(guild, [true, user, level, false, new Date(Date.now() + this.cacheTimeout)]); + return this.premium.updateOne({ user: user }, { $addToSet: { appliesTo: guild } }, { upsert: true }); + } + + removePremium(user: string, guild: string) { + // console.log("Premium removePremium"); + this.cache.set(guild, [false, "", 0, false, new Date(Date.now() + this.cacheTimeout)]); + return this.premium.updateOne({ user: user }, { $pull: { appliesTo: guild } }); } } @@ -249,7 +677,18 @@ export interface GuildConfig { channels: string[]; }; }; + clean: { + channels: string[]; + allowed: { + users: string[]; + roles: string[]; + } + } }; + autoPublish: { + enabled: boolean; + channels: string[]; + } welcome: { enabled: boolean; role: string | null; @@ -364,6 +803,6 @@ export interface ModNoteSchema { export interface PremiumSchema { user: string; level: number; - expires: Date; appliesTo: string[]; + expiresAt?: number; } diff --git a/src/utils/dualCollector.ts b/src/utils/dualCollector.ts index 714a2d9..0b05779 100644 --- a/src/utils/dualCollector.ts +++ b/src/utils/dualCollector.ts @@ -1,4 +1,4 @@ -import Discord, { Client, Interaction, Message, MessageComponentInteraction } from "discord.js"; +import { ButtonInteraction, Client, User, Interaction, InteractionCollector, Message, MessageComponentInteraction, ModalSubmitInteraction } from "discord.js"; import client from "./client.js"; export default async function ( @@ -10,15 +10,14 @@ export default async function ( try { out = await new Promise((resolve, _reject) => { const mes = m - .createMessageComponentCollector({ - filter: (m) => interactionFilter(m), - time: 300000 - }) - .on("collect", (m) => { + .createMessageComponentCollector({ + filter: (m) => interactionFilter(m), + time: 300000 + }) + .on("collect", (m) => { resolve(m); }); - const int = m.channel - .createMessageCollector({ + const int = m.channel.createMessageCollector({ filter: (m) => messageFilter(m), time: 300000 }) @@ -45,35 +44,41 @@ export default async function ( return out; } +function defaultInteractionFilter(i: MessageComponentInteraction, user: User, m: Message) { + return i.channel!.id === m.channel!.id && i.user.id === user.id +} +function defaultModalFilter(i: ModalSubmitInteraction, user: User, m: Message) { + return i.channel!.id === m.channel!.id && i.user.id === user.id +} + + export async function modalInteractionCollector( - m: Message, - modalFilter: (i: Interaction) => boolean | Promise, - interactionFilter: (i: MessageComponentInteraction) => boolean | Promise -): Promise { - let out: Interaction; + m: Message, user: User, + modalFilter?: (i: Interaction) => boolean | Promise, + interactionFilter?: (i: MessageComponentInteraction) => boolean | Promise +): Promise { + let out: ButtonInteraction | ModalSubmitInteraction; try { out = await new Promise((resolve, _reject) => { const int = m .createMessageComponentCollector({ - filter: (i: MessageComponentInteraction) => interactionFilter(i), + filter: (i: MessageComponentInteraction) => (interactionFilter ? interactionFilter(i) : true) && defaultInteractionFilter(i, user, m), time: 300000 }) - .on("collect", (i: Interaction) => { + .on("collect", async (i: ButtonInteraction) => { + mod.stop(); + int.stop(); + await i.deferUpdate(); resolve(i); }); - const mod = new Discord.InteractionCollector(client as Client, { - filter: (i: Interaction) => modalFilter(i), + const mod = new InteractionCollector(client as Client, { + filter: (i: Interaction) => (modalFilter ? modalFilter(i) : true) && i.isModalSubmit() && defaultModalFilter(i, user, m), time: 300000 - }).on("collect", async (i: Interaction) => { + }).on("collect", async (i: ModalSubmitInteraction) => { int.stop(); - (i as Discord.ModalSubmitInteraction).deferUpdate(); - resolve(i as Discord.ModalSubmitInteraction); - }); - int.on("end", () => { mod.stop(); - }); - mod.on("end", () => { - int.stop(); + await i.deferUpdate(); + resolve(i); }); }); } catch (e) { diff --git a/src/utils/ellipsis.ts b/src/utils/ellipsis.ts new file mode 100644 index 0000000..6ec5888 --- /dev/null +++ b/src/utils/ellipsis.ts @@ -0,0 +1,4 @@ +export default (str: string, max: number): string => { + if (str.length <= max) return str; + return str.slice(0, max - 3) + "..."; +} \ No newline at end of file diff --git a/src/utils/eventScheduler.ts b/src/utils/eventScheduler.ts index 3c9d6ca..a79a260 100644 --- a/src/utils/eventScheduler.ts +++ b/src/utils/eventScheduler.ts @@ -2,7 +2,7 @@ import { Agenda } from "@hokify/agenda"; import client from "./client.js"; import * as fs from "fs"; import * as path from "path"; -import config from "../config/main.json" assert { type: "json" }; +import config from "../config/main.js"; class EventScheduler { private agenda: Agenda; @@ -10,11 +10,10 @@ class EventScheduler { constructor() { this.agenda = new Agenda({ db: { - address: config.mongoUrl + "Nucleus", + address: config.mongoOptions.host, collection: "eventScheduler" } }); - this.agenda.define("unmuteRole", async (job) => { const guild = await client.guilds.fetch(job.attrs.data.guild); const user = await guild.members.fetch(job.attrs.data.user); @@ -43,12 +42,12 @@ class EventScheduler { calculateType: "guildMemberPunish", color: NucleusColors.green, emoji: "PUNISH.MUTE.GREEN", - timestamp: new Date().getTime() + timestamp: Date.now() }, list: { memberId: entry(user.user.id, `\`${user.user.id}\``), name: entry(user.user.id, renderUser(user.user)), - unmuted: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())), + unmuted: entry(Date.now().toString(), renderDelta(Date.now())), unmutedBy: entry(null, "*Time out ended*") }, hidden: { diff --git a/src/utils/generateEmojiEmbed.ts b/src/utils/generateEmojiEmbed.ts index c0f17ae..a326fc5 100644 --- a/src/utils/generateEmojiEmbed.ts +++ b/src/utils/generateEmojiEmbed.ts @@ -1,4 +1,4 @@ -import { EmbedBuilder } from "@discordjs/builders"; +import { EmbedBuilder } from "discord.js"; import getEmojiByName from "./getEmojiByName.js"; const colors = { @@ -13,13 +13,16 @@ class EmojiEmbed extends EmbedBuilder { description = ""; _generateTitle() { + if (this._emoji && !this._title) return getEmojiByName(this._emoji) if (this._emoji) { return `${getEmojiByName(this._emoji)} ${this._title}`; } - return this._title; + if (this._title) { return this._title }; + return ""; } override setTitle(title: string) { this._title = title; - super.setTitle(this._generateTitle()); + const proposedTitle = this._generateTitle(); + if (proposedTitle) super.setTitle(proposedTitle); return this; } override setDescription(description: string) { @@ -29,7 +32,8 @@ class EmojiEmbed extends EmbedBuilder { } setEmoji(emoji: string) { this._emoji = emoji; - super.setTitle(this._generateTitle()); + const proposedTitle = this._generateTitle(); + if (proposedTitle) super.setTitle(proposedTitle); return this; } setStatus(color: "Danger" | "Warning" | "Success") { diff --git a/src/utils/getCommandDataByName.ts b/src/utils/getCommandDataByName.ts new file mode 100644 index 0000000..da3e54b --- /dev/null +++ b/src/utils/getCommandDataByName.ts @@ -0,0 +1,28 @@ +import type Discord from "discord.js"; +import client from "./client.js"; + + +export const getCommandMentionByName = (name: string): string => { + const split = name.replaceAll("/", " ").split(" ") + const commandName: string = split[0]!; + + const filterCommand = (command: Discord.ApplicationCommand) => command.name === commandName; + + const command = client.fetchedCommands.filter(c => filterCommand(c)) + if (command.size === 0) return `\`/${name.replaceAll("/", " ")}\``; + const commandID = command.first()!.id; + return ``; +} + +export const getCommandByName = (name: string): {name: string, description: string, mention: string} => { + + const split = name.replaceAll(" ", "/") + const command = client.commands["commands/" + split]!; + // console.log(command) + const mention = getCommandMentionByName(name); + return { + name: command[1].name, + description: command[1].description, + mention: mention + } +} \ No newline at end of file diff --git a/src/utils/getCommandMentionByName.ts b/src/utils/getCommandMentionByName.ts deleted file mode 100644 index b2b9937..0000000 --- a/src/utils/getCommandMentionByName.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type Discord from "discord.js"; -import client from "./client.js"; -import config from "../config/main.json" assert { type: "json"}; - - -export const getCommandMentionByName = async (name: string): Promise => { - const split = name.replaceAll("/", " ").split(" ") - const commandName: string = split[0]!; - let commandID: string; - - const filterCommand = (command: Discord.ApplicationCommand) => command.name === commandName; - - if (config.enableDevelopment) { - const developmentGuild = client.guilds.cache.get(config.developmentGuildID)!; - await developmentGuild.commands.fetch(); - commandID = developmentGuild.commands.cache.filter(c => filterCommand(c)).first()!.id; - } else { - await client.application?.commands.fetch(); - commandID = client.application?.commands.cache.filter(c => filterCommand(c)).first()!.id!; - } - return ``; -} \ No newline at end of file diff --git a/src/utils/getEmojiByName.ts b/src/utils/getEmojiByName.ts index 3fa2b53..9df17a4 100644 --- a/src/utils/getEmojiByName.ts +++ b/src/utils/getEmojiByName.ts @@ -1,5 +1,7 @@ import emojis from "../config/emojis.json" assert { type: "json" }; +import lodash from 'lodash'; +const isArray = lodash.isArray; interface EmojisIndex { [key: string]: string | EmojisIndex | EmojisIndex[]; } @@ -12,7 +14,7 @@ function getEmojiByName(name: string | null, format?: string): string { if (typeof id === "string" || id === undefined) { throw new Error(`Emoji ${name} not found`); } - if (Array.isArray(id)) { + if (isArray(id)) { id = id[parseInt(part)]; } else { id = id[part]; @@ -21,6 +23,10 @@ function getEmojiByName(name: string | null, format?: string): string { if (typeof id !== "string" && id !== undefined) { throw new Error(`Emoji ${name} not found`); } + return getEmojiFromId(id, format); +} + +function getEmojiFromId(id: string | undefined, format?: string): string { if (format === "id") { if (id === undefined) return "0"; return id.toString(); diff --git a/src/utils/listToAndMore.ts b/src/utils/listToAndMore.ts new file mode 100644 index 0000000..791ce40 --- /dev/null +++ b/src/utils/listToAndMore.ts @@ -0,0 +1,7 @@ +export default (list: string[], max: number) => { + // PineappleFan, Coded, Mini (and 10 more) + if(list.length > max) { + return list.slice(0, max).join(", ") + ` (and ${list.length - max} more)`; + } + return list.join(", "); +} \ No newline at end of file diff --git a/src/utils/log.ts b/src/utils/log.ts index 54f656a..c6416a1 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -7,10 +7,37 @@ import client from "./client.js"; const wait = promisify(setTimeout); +export interface LoggerOptions { + meta: { + type: string; + displayName: string; + calculateType: string; + color: number; + emoji: string; + timestamp: number; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + list: any; + hidden: { + guild: string; + }, + separate?: { + start?: string; + end?: string; + } +} + +async function isLogging(guild: string, type: string): Promise { + const config = await client.database.guilds.read(guild); + if (!config.logging.logs.enabled) return false; + if (!config.logging.logs.channel) return false; + if (!toHexArray(config.logging.logs.toLog).includes(type)) { return false; } + return true; +} export const Logger = { renderUser(user: Discord.User | string) { - if (typeof user === "string") return `${user} [<@${user}>]`; + if (typeof user === "string") user = client.users.cache.get(user)!; return `${user.username} [<@${user.id}>]`; }, renderTime(t: number) { @@ -29,10 +56,12 @@ export const Logger = { if (typeof value === "number") value = value.toString(); return { value: value, displayValue: displayValue }; }, - renderChannel(channel: Discord.GuildChannel | Discord.ThreadChannel) { + renderChannel(channel: Discord.GuildChannel | Discord.ThreadChannel | string) { + if (typeof channel === "string") channel = client.channels.cache.get(channel) as Discord.GuildChannel | Discord.ThreadChannel; return `${channel.name} [<#${channel.id}>]`; }, - renderRole(role: Discord.Role) { + renderRole(role: Discord.Role | string, guild?: Discord.Guild | string) { + if (typeof role === "string") role = (typeof guild === "string" ? client.guilds.cache.get(guild) : guild)!.roles.cache.get(role)!; return `${role.name} [<@&${role.id}>]`; }, renderEmoji(emoji: Discord.GuildEmoji) { @@ -43,19 +72,14 @@ export const Logger = { yellow: 0xf2d478, green: 0x68d49e }, - async getAuditLog(guild: Discord.Guild, event: Discord.GuildAuditLogsResolvable): Promise { - await wait(250); + async getAuditLog(guild: Discord.Guild, event: Discord.GuildAuditLogsResolvable, delay?: number): Promise { + await wait(delay ?? 250); const auditLog = (await guild.fetchAuditLogs({ type: event })).entries.map(m => m) return auditLog as Discord.GuildAuditLogsEntry[]; }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async log(log: any): Promise { + async log(log: LoggerOptions): Promise { + if (!await isLogging(log.hidden.guild, log.meta.calculateType)) return; const config = await client.database.guilds.read(log.hidden.guild); - if (!config.logging.logs.enabled) return; - if (!toHexArray(config.logging.logs.toLog).includes(log.meta.calculateType)) { - console.log("Not logging this type of event"); - return; - } if (config.logging.logs.channel) { const channel = (await client.channels.fetch(config.logging.logs.channel)) as Discord.TextChannel | null; const description: Record = {}; @@ -70,7 +94,7 @@ export const Logger = { } }); if (channel) { - log.separate = log.separate || {}; + log.separate = log.separate ?? {}; const embed = new Discord.EmbedBuilder() .setTitle(`${getEmojiByName(log.meta.emoji)} ${log.meta.displayName}`) .setDescription( @@ -83,8 +107,8 @@ export const Logger = { channel.send({ embeds: [embed] }); } } - } + }, + isLogging }; - export default {}; diff --git a/src/utils/logTranscripts.ts b/src/utils/logTranscripts.ts deleted file mode 100644 index 0950664..0000000 --- a/src/utils/logTranscripts.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type Discord from 'discord.js'; - -export interface JSONTranscriptSchema { - messages: { - content: string | null; - attachments: { - url: string; - name: string; - size: number; - }[]; - authorID: string; - authorUsername: string; - authorUsernameColor: string; - timestamp: string; - id: string; - edited: boolean; - }[]; - channel: string; - guild: string; - timestamp: string; -} - - -export const JSONTranscriptFromMessageArray = (messages: Discord.Message[]): JSONTranscriptSchema | null => { - if (messages.length === 0) return null; - return { - guild: messages[0]!.guild!.id, - channel: messages[0]!.channel.id, - timestamp: Date.now().toString(), - messages: messages.map((message: Discord.Message) => { - return { - content: message.content, - attachments: message.attachments.map((attachment: Discord.Attachment) => { - return { - url: attachment.url, - name: attachment.name!, - size: attachment.size, - }; - }), - authorID: message.author.id, - authorUsername: message.author.username + "#" + message.author.discriminator, - authorUsernameColor: message.member!.displayHexColor.toString(), - timestamp: message.createdTimestamp.toString(), - id: message.id, - edited: message.editedTimestamp ? true : false, - }; - }) - }; -} - -export const JSONTranscriptToHumanReadable = (data: JSONTranscriptSchema): string => { - let out = ""; - - for (const message of data.messages) { - const date = new Date(parseInt(message.timestamp)); - out += `${message.authorUsername} (${message.authorID}) [${date}]`; - if (message.edited) out += " (edited)"; - if (message.content) out += "\nContent:\n" + message.content.split("\n").map((line: string) => `\n> ${line}`).join(""); - if (message.attachments.length > 0) out += "\nAttachments:\n" + message.attachments.map((attachment: { url: string; name: string; size: number; }) => `\n> [${attachment.name}](${attachment.url}) (${attachment.size} bytes)`).join("\n"); - - out += "\n\n"; - } - return out; -} \ No newline at end of file diff --git a/src/utils/memory.ts b/src/utils/memory.ts index 870ffaf..60a6535 100644 --- a/src/utils/memory.ts +++ b/src/utils/memory.ts @@ -7,6 +7,7 @@ interface GuildData { logging: GuildConfig["logging"]; tickets: GuildConfig["tickets"]; tags: GuildConfig["tags"]; + autoPublish: GuildConfig["autoPublish"]; } class Memory { @@ -31,7 +32,8 @@ class Memory { filters: guildData.filters, logging: guildData.logging, tickets: guildData.tickets, - tags: guildData.tags + tags: guildData.tags, + autoPublish: guildData.autoPublish }); } return this.memory.get(guild)!; diff --git a/src/utils/performanceTesting/record.ts b/src/utils/performanceTesting/record.ts index 95761e9..71883c5 100644 --- a/src/utils/performanceTesting/record.ts +++ b/src/utils/performanceTesting/record.ts @@ -2,7 +2,7 @@ import client from "../client.js"; import * as CP from 'child_process'; import * as process from 'process'; import systeminformation from "systeminformation"; -import config from "../../config/main.json" assert { type: "json" }; +import config from "../../config/main.js"; import singleNotify from "../singleNotify.js"; @@ -39,7 +39,7 @@ const record = async () => { singleNotify( "performanceTest", config.developmentGuildID, - `Discord ping time: \`${results.discord}ms\`\nDatabase read time: \`${results.databaseRead}ms\`\nCPU usage: \`${results.resources.cpu}%\`\nMemory usage: \`${results.resources.memory}MB\`\nCPU temperature: \`${results.resources.temperature}°C\``, + `Discord ping time: \`${results.discord}ms\`\nDatabase read time: \`${results.databaseRead}ms\`\nCPU usage: \`${results.resources.cpu}%\`\nMemory usage: \`${Math.round(results.resources.memory)}MB\`\nCPU temperature: \`${results.resources.temperature}°C\``, "Critical", config.owners ) diff --git a/src/utils/singleNotify.ts b/src/utils/singleNotify.ts index 8e3aa60..6bf63e1 100644 --- a/src/utils/singleNotify.ts +++ b/src/utils/singleNotify.ts @@ -1,6 +1,7 @@ import client from "./client.js"; import EmojiEmbed from "./generateEmojiEmbed.js"; import { Record as ImmutableRecord } from "immutable"; +import type { TextChannel, ThreadChannel, NewsChannel } from "discord.js"; const severitiesType = ImmutableRecord({ Critical: "Danger", @@ -31,20 +32,20 @@ export default async function ( const channel = await client.channels.fetch(data.logging.staff.channel); if (!channel) return; if (!channel.isTextBased()) return; + const textChannel = channel as TextChannel | ThreadChannel | NewsChannel; + let messageData = {embeds: [ + new EmojiEmbed() + .setTitle(`${severity} notification`) + .setDescription(message) + .setStatus(severities.get(severity)) + .setEmoji("CONTROL.BLOCKCROSS") + ]} if (pings) { - await channel.send({ + messageData = Object.assign(messageData, { content: pings.map((ping) => `<@${ping}>`).join(" ") }); } - await channel.send({ - embeds: [ - new EmojiEmbed() - .setTitle(`${severity} notification`) - .setDescription(message) - .setStatus(severities.get(severity)) - .setEmoji("CONTROL.BLOCKCROSS") - ] - }); + await textChannel.send(messageData); } catch (err) { console.error(err); } diff --git a/src/utils/temp/generateFileName.ts b/src/utils/temp/generateFileName.ts index 3aab64c..109478d 100644 --- a/src/utils/temp/generateFileName.ts +++ b/src/utils/temp/generateFileName.ts @@ -12,7 +12,7 @@ export default function generateFileName(ending: string): string { if (fs.existsSync(`./${fileName}`)) { fileName = generateFileName(ending); } - client.database.eventScheduler.schedule("deleteFile", (new Date().getTime() + 60 * 1000).toString(), { + client.database.eventScheduler.schedule("deleteFile", (Date.now() + 60 * 1000).toString(), { fileName: `${fileName}.${ending}` }); return path.join(__dirname, fileName + "." + ending); diff --git a/tsconfig.json b/tsconfig.json index a39c584..537e3dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,8 @@ "resolveJsonModule": true, "moduleResolution": "NodeNext", "skipLibCheck": true, - "noImplicitReturns": false + "noImplicitReturns": false, }, - "include": ["src/**/*"], + "include": ["src/**/*", "src/*", "src/config/main.d.ts", "src/config/main.ts"], "exclude": ["src/Unfinished/**/*"] } From cc63dee71e78c947462b026f98107d3a4f3c5d8a Mon Sep 17 00:00:00 2001 From: Samuel Shuert Date: Fri, 3 Mar 2023 18:54:29 -0500 Subject: [PATCH 2/2] Development (#12) Co-authored-by: PineaFan Co-authored-by: pineafan Co-authored-by: PineappleFan Co-authored-by: Skyler --- .eslintignore | 2 +- src/api/index.ts | 4 ++-- src/commands/mod/purge.ts | 7 +++++-- src/commands/privacy.ts | 8 +++++--- src/context/messages/purgeto.ts | 8 ++++++-- src/events/guildDelete.ts | 10 ++++++++++ src/premium/createTranscript.ts | 8 ++++++-- src/utils/database.ts | 35 +++++++++++++++++++++++++++++---- 8 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 src/events/guildDelete.ts diff --git a/.eslintignore b/.eslintignore index 45ad95d..d7b2f7f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1 @@ -ClicksMigratingProblems/**/* +ClicksMigratingProblems/**/* \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts index 9676194..c8b7b14 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -152,7 +152,7 @@ const runServer = (client: NucleusClient) => { app.get("/transcript/:code/human", jsonParser, async function (req: express.Request, res: express.Response) { const code = req.params.code; if (code === undefined) return res.status(400).send("No code provided"); - const entry = await client.database.transcripts.read(code); + const entry = await client.database.transcripts.read(code, req.query.key as string, req.query.iv as string); if (entry === null) return res.status(404).send("Could not find a transcript by that code"); // Convert to a human readable format const data = client.database.transcripts.toHumanReadable(entry); @@ -164,7 +164,7 @@ const runServer = (client: NucleusClient) => { app.get("/transcript/:code", jsonParser, async function (req: express.Request, res: express.Response) { const code = req.params.code; if (code === undefined) return res.status(400).send("No code provided"); - const entry = await client.database.transcripts.read(code); + const entry = await client.database.transcripts.read(code, req.query.key as string, req.query.iv as string); if (entry === null) return res.status(404).send("Could not find a transcript by that code"); // Convert to a human readable format return res.status(200).send(entry); diff --git a/src/commands/mod/purge.ts b/src/commands/mod/purge.ts index 8644e26..004f5ff 100644 --- a/src/commands/mod/purge.ts +++ b/src/commands/mod/purge.ts @@ -318,7 +318,8 @@ const callback = async (interaction: CommandInteraction): Promise => { )).map(message => message as Message); const newOut = await client.database.transcripts.createTranscript(messageArray, interaction, interaction.member as GuildMember); - const code = await client.database.transcripts.create(newOut); + const [code, key, iv] = await client.database.transcripts.create(newOut); + await interaction.editReply({ embeds: [ new EmojiEmbed() @@ -329,7 +330,9 @@ const callback = async (interaction: CommandInteraction): Promise => { ], components: [ new Discord.ActionRowBuilder().addComponents([ - new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript?code=${code}`), + + new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript/${code}?key=${key}&iv=${iv}`).setDisabled(!code), + ]) ] }); diff --git a/src/commands/privacy.ts b/src/commands/privacy.ts index 46784f5..dcdebb1 100644 --- a/src/commands/privacy.ts +++ b/src/commands/privacy.ts @@ -179,9 +179,11 @@ const callback = async (interaction: CommandInteraction): Promise => { continue; } if (confirmation.success) { - client.database.guilds.delete(interaction.guild!.id); - client.database.history.delete(interaction.guild!.id); - client.database.notes.delete(interaction.guild!.id); + await client.database.guilds.delete(interaction.guild!.id); + await client.database.history.delete(interaction.guild!.id); + await client.database.notes.delete(interaction.guild!.id); + await client.database.transcripts.deleteAll(interaction.guild!.id); + nextFooter = "All data cleared"; continue; } else { diff --git a/src/context/messages/purgeto.ts b/src/context/messages/purgeto.ts index aef159b..616b085 100644 --- a/src/context/messages/purgeto.ts +++ b/src/context/messages/purgeto.ts @@ -193,7 +193,9 @@ const callback = async (interaction: MessageContextMenuCommandInteraction) => { ) )).map(message => message as Message); const transcript = await client.database.transcripts.createTranscript(messageArray, interaction, interaction.member as GuildMember); - const code = await client.database.transcripts.create(transcript); + + const [code, key, iv] = await client.database.transcripts.create(transcript); + await interaction.editReply({ embeds: [ new EmojiEmbed() @@ -204,7 +206,9 @@ const callback = async (interaction: MessageContextMenuCommandInteraction) => { ], components: [ new Discord.ActionRowBuilder().addComponents([ - new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript?code=${code}`), + + new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript/${code}?key=${key}&iv=${iv}`).setDisabled(!code), + ]) ] }); diff --git a/src/events/guildDelete.ts b/src/events/guildDelete.ts new file mode 100644 index 0000000..92af401 --- /dev/null +++ b/src/events/guildDelete.ts @@ -0,0 +1,10 @@ +import client, { NucleusClient } from '../utils/client.js' +import type { Guild } from 'discord.js' + +export const event = 'guildDelete' +export const callback = async (_client: NucleusClient, guild: Guild) => { + await client.database.guilds.delete(guild.id); + await client.database.history.delete(guild.id); + await client.database.notes.delete(guild.id); + await client.database.transcripts.deleteAll(guild.id); +} \ No newline at end of file diff --git a/src/premium/createTranscript.ts b/src/premium/createTranscript.ts index 67aed04..dd01a98 100644 --- a/src/premium/createTranscript.ts +++ b/src/premium/createTranscript.ts @@ -60,7 +60,9 @@ export default async function (interaction: CommandInteraction | MessageComponen const newOut = await client.database.transcripts.createTranscript(messages, interaction, member); - const code = await client.database.transcripts.create(newOut); + + const [code, key, iv] = await client.database.transcripts.create(newOut); + if(!code) return await interaction.reply({ embeds: [ new EmojiEmbed() @@ -86,7 +88,9 @@ export default async function (interaction: CommandInteraction | MessageComponen ], components: [ new ActionRowBuilder().addComponents([ - new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript/${code}`), + + new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://testing.coded.codes/nucleus/transcript/${code}?key=${key}&iv=${iv}`), + new ButtonBuilder() .setLabel("Delete") .setStyle(ButtonStyle.Danger) diff --git a/src/utils/database.ts b/src/utils/database.ts index 2e64320..06c41f0 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -22,6 +22,9 @@ const database = mongoClient.db(); const collectionOptions = { authdb: "admin" }; +const getIV = () => crypto.randomBytes(16); + + export class Guilds { guilds: Collection; defaultData: GuildConfig; @@ -203,15 +206,39 @@ export class Transcript { do { code = crypto.randomBytes(64).toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-"); } while (await this.transcripts.findOne({ code: code })); + const key = crypto.randomBytes(32**2).toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-").substring(0, 32); + const iv = getIV().toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-"); + for(const message of transcript.messages) { + if(message.content) { + const encCipher = crypto.createCipheriv("AES-256-CBC", key, iv); + message.content = encCipher.update(message.content, "utf8", "base64") + encCipher.final("base64"); + } + } const doc = await this.transcripts.insertOne(Object.assign(transcript, { code: code }), collectionOptions); - if(doc.acknowledged) return code; - else return null; + if(doc.acknowledged) return [code, key, iv]; + else return [null, null, null]; } - async read(code: string) { + async read(code: string, key: string, iv: string) { // console.log("Transcript read") - return await this.transcripts.findOne({ code: code }); + const doc = await this.transcripts.findOne({ code: code }); + if(!doc) return null; + for(const message of doc.messages) { + if(message.content) { + const decCipher = crypto.createDecipheriv("AES-256-CBC", key, iv); + message.content = decCipher.update(message.content, "base64", "utf8") + decCipher.final("utf8"); + } + } + return doc; + } + + async deleteAll(guild: string) { + // console.log("Transcript delete") + const filteredDocs = await this.transcripts.find({ guild: guild }).toArray(); + for (const doc of filteredDocs) { + await this.transcripts.deleteOne({ code: doc.code }); + } } async createTranscript(messages: Message[], interaction: MessageComponentInteraction | CommandInteraction, member: GuildMember) {