From 74f78b310b9cf915dc85f5fbc95485552807bfe2 Mon Sep 17 00:00:00 2001 From: Razor1121 Date: Sat, 21 Feb 2026 20:34:36 -0500 Subject: [PATCH] initial commit --- .github/CONTRIBUTING.md | 13 + .github/FUNDING.yml | 2 + .github/SUPPORT.md | 7 + .gitignore | 6 + CHANGELOG.md | 162 + CODE_OF_CONDUCT.md | 128 + LICENSE | 61 + README.md | 361 + SECURITY.md | 4 + add-config-element-object.js | 19 + anti-phish.code-workspace | 8 + config-generator/config.json | 179 + config-generator/strings.json | 255 + generate-config.js | 33 + locales/de.json | 986 + locales/en.json | 1021 + main.js | 520 + modules/admin-tools/commands/admin.js | 111 + modules/admin-tools/commands/stealemote.js | 32 + modules/admin-tools/config.json | 15 + modules/admin-tools/module.json | 23 + modules/afk-system/commands/afk.js | 86 + modules/afk-system/config.json | 129 + modules/afk-system/events/messageCreate.js | 43 + modules/afk-system/models/User.js | 27 + modules/afk-system/module.json | 25 + modules/anti-ghostping/config.json | 83 + .../anti-ghostping/events/messageCreate.js | 13 + .../anti-ghostping/events/messageDelete.js | 36 + modules/anti-ghostping/module.json | 23 + modules/auto-delete/channels.json | 78 + modules/auto-delete/events/botReady.js | 70 + modules/auto-delete/events/messageCreate.js | 23 + .../auto-delete/events/voiceStateUpdate.js | 29 + modules/auto-delete/module.json | 25 + modules/auto-delete/voice-channels.json | 62 + modules/auto-messager/cronjob.json | 76 + modules/auto-messager/daily.json | 94 + modules/auto-messager/events/botReady.js | 48 + modules/auto-messager/hourly.json | 77 + modules/auto-messager/module.json | 26 + modules/auto-publisher/config.json | 75 + .../auto-publisher/events/messageCreate.js | 21 + modules/auto-publisher/module.json | 24 + modules/auto-react/configs/config.json | 101 + modules/auto-react/configs/replies.json | 42 + modules/auto-react/events/messageCreate.js | 94 + modules/auto-react/module.json | 25 + modules/auto-thread/config.json | 64 + modules/auto-thread/events/messageCreate.js | 13 + modules/auto-thread/module.json | 24 + modules/betterstatus/config.json | 223 + modules/betterstatus/events/botReady.js | 49 + modules/betterstatus/events/guildMemberAdd.js | 22 + modules/betterstatus/module.json | 23 + modules/birthday/birthday.js | 197 + modules/birthday/commands/birthday.js | 127 + modules/birthday/config.json | 241 + modules/birthday/events/botReady.js | 24 + modules/birthday/events/guildMemberRemove.js | 5 + modules/birthday/models/User.js | 32 + modules/birthday/module.json | 26 + modules/channel-stats/channels.json | 188 + modules/channel-stats/events/botReady.js | 73 + modules/channel-stats/module.json | 24 + modules/color-me/commands/color-me.js | 258 + modules/color-me/configs/config.json | 88 + modules/color-me/configs/strings.json | 158 + modules/color-me/events/guildMemberUpdate.js | 74 + modules/color-me/models/Role.js | 27 + modules/color-me/module.json | 26 + modules/connect-four/commands/connect-four.js | 289 + modules/connect-four/module.json | 23 + modules/counter/config.json | 230 + modules/counter/events/botReady.js | 19 + modules/counter/events/messageCreate.js | 98 + modules/counter/milestones.json | 70 + modules/counter/models/CountChannel.js | 27 + modules/counter/module.json | 26 + modules/duel/commands/duel.js | 194 + modules/duel/module.json | 23 + modules/economy-system/cli.js | 61 + .../economy-system/commands/economy-system.js | 540 + modules/economy-system/commands/shop.js | 124 + modules/economy-system/configs/config.json | 364 + modules/economy-system/configs/strings.json | 686 + modules/economy-system/economy-system.js | 500 + modules/economy-system/events/botReady.js | 49 + .../events/interactionCreate.js | 9 + .../economy-system/events/messageCreate.js | 39 + modules/economy-system/models/cooldowns.js | 20 + modules/economy-system/models/dropMsg.js | 21 + modules/economy-system/models/shop.js | 24 + modules/economy-system/models/user.js | 23 + modules/economy-system/module.json | 28 + modules/fun/commands/hug.js | 26 + modules/fun/commands/kiss.js | 26 + modules/fun/commands/pat.js | 26 + modules/fun/commands/random.js | 86 + modules/fun/commands/slap.js | 26 + modules/fun/config.json | 428 + modules/fun/module.json | 24 + modules/giveaways/commands/giveaway.js | 196 + modules/giveaways/commands/gmessages.js | 34 + modules/giveaways/configs/config.json | 157 + modules/giveaways/configs/strings.json | 529 + modules/giveaways/events/botReady.js | 30 + modules/giveaways/events/interactionCreate.js | 154 + modules/giveaways/events/messageCreate.js | 31 + modules/giveaways/giveaways.js | 276 + modules/giveaways/models/Giveaway.js | 48 + modules/giveaways/module.json | 27 + modules/guess-the-number/commands/manage.js | 133 + modules/guess-the-number/config.json | 152 + .../events/interactionCreate.js | 7 + .../guess-the-number/events/messageCreate.js | 41 + modules/guess-the-number/models/Channel.js | 33 + modules/guess-the-number/module.json | 26 + .../commands/hunt-the-code-admin.js | 121 + .../hunt-the-code/commands/hunt-the-code.js | 114 + modules/hunt-the-code/models/Code.js | 27 + modules/hunt-the-code/models/User.js | 29 + modules/hunt-the-code/module.json | 25 + modules/hunt-the-code/strings.json | 207 + modules/info-commands/commands/info.js | 259 + modules/info-commands/module.json | 24 + modules/info-commands/strings.json | 293 + .../invite-tracking/commands/trace-invites.js | 75 + modules/invite-tracking/config.json | 28 + .../invite-tracking/events/guildMemberJoin.js | 79 + .../events/guildMemberRemove.js | 60 + .../events/interactionCreate.js | 48 + modules/invite-tracking/models/UserInvite.js | 30 + modules/invite-tracking/module.json | 26 + modules/invite-tracking/onLoad.js | 18 + modules/levels/commands/leaderboard.js | 126 + modules/levels/commands/manage-levels.js | 352 + modules/levels/commands/profile.js | 55 + modules/levels/configs/config.json | 282 + .../configs/random-levelup-messages.json | 94 + .../configs/special-levelup-messages.json | 89 + modules/levels/configs/strings.json | 301 + modules/levels/events/botReady.js | 10 + modules/levels/events/guildMemberRemove.js | 13 + modules/levels/events/interactionCreate.js | 24 + modules/levels/events/messageCreate.js | 108 + modules/levels/leaderboardChannel.js | 77 + modules/levels/models/User.js | 31 + modules/levels/module.json | 28 + modules/massrole/commands/massrole.js | 309 + modules/massrole/configs/config.json | 40 + modules/massrole/configs/strings.json | 52 + modules/massrole/module.json | 24 + .../__tests__/phishingService.test.js | 54 + modules/moderation/commands/moderate.js | 937 + modules/moderation/commands/report.js | 83 + modules/moderation/configs/antiGrief.json | 112 + modules/moderation/configs/antiJoinRaid.json | 116 + modules/moderation/configs/antiSpam.json | 217 + modules/moderation/configs/config.json | 493 + modules/moderation/configs/joinGate.json | 147 + modules/moderation/configs/strings.json | 519 + modules/moderation/configs/verification.json | 239 + modules/moderation/events/botReady.js | 104 + modules/moderation/events/guildMemberAdd.js | 299 + .../moderation/events/interactionCreate.js | 37 + modules/moderation/events/messageCreate.js | 139 + modules/moderation/events/messageUpdate.js | 11 + modules/moderation/models/ModerationAction.js | 28 + modules/moderation/models/UserNotes.js | 22 + modules/moderation/moderationActions.js | 353 + modules/moderation/module.json | 32 + modules/moderation/phishingConfig.js | 43 + modules/moderation/phishingHeuristics.js | 101 + modules/moderation/phishingService.js | 302 + modules/nicknames/configs/config.json | 28 + modules/nicknames/configs/strings.json | 59 + modules/nicknames/events/botReady.js | 8 + modules/nicknames/events/guildMemberUpdate.js | 11 + modules/nicknames/models/User.js | 22 + modules/nicknames/module.json | 26 + modules/nicknames/renameMember.js | 73 + modules/partner-list/commands/partner.js | 231 + modules/partner-list/config.json | 218 + modules/partner-list/events/botReady.js | 5 + modules/partner-list/models/Partner.js | 27 + modules/partner-list/module.json | 26 + modules/partner-list/partnerlist.js | 57 + modules/ping-on-vc-join/actual-config.json | 46 + modules/ping-on-vc-join/config.json | 127 + .../events/voiceStateUpdate.js | 52 + modules/ping-on-vc-join/module.json | 25 + modules/polls/commands/poll.js | 153 + modules/polls/configs/config.json | 59 + modules/polls/configs/strings.json | 53 + modules/polls/events/botReady.js | 12 + modules/polls/events/interactionCreate.js | 99 + modules/polls/models/Poll.js | 26 + modules/polls/module.json | 26 + modules/polls/polls.js | 137 + modules/quiz/commands/quiz.js | 276 + modules/quiz/configs/config.json | 146 + modules/quiz/configs/quizList.json | 76 + modules/quiz/configs/strings.json | 59 + modules/quiz/events/botReady.js | 28 + modules/quiz/events/interactionCreate.js | 99 + modules/quiz/models/Quiz.js | 29 + modules/quiz/models/QuizUser.js | 37 + modules/quiz/module.json | 28 + modules/quiz/quizUtil.js | 250 + modules/reminders/commands/reminder.js | 49 + modules/reminders/config.json | 71 + modules/reminders/events/botReady.js | 12 + modules/reminders/models/Reminder.js | 28 + modules/reminders/module.json | 26 + modules/reminders/reminders.js | 22 + .../commands/rock-paper-scissors.js | 308 + modules/rock-paper-scissors/module.json | 23 + modules/serverinfo/configs/config.json | 55 + modules/serverinfo/configs/fields.json | 166 + modules/serverinfo/events/botReady.js | 94 + modules/serverinfo/module.json | 25 + modules/starboard/configs/config.json | 227 + modules/starboard/events/botReady.js | 15 + .../starboard/events/messageReactionAdd.js | 6 + .../starboard/events/messageReactionRemove.js | 6 + modules/starboard/handleStarboard.js | 103 + modules/starboard/models/StarMsg.js | 19 + modules/starboard/models/StarUser.js | 19 + modules/starboard/module.json | 24 + modules/status-roles/configs/config.json | 62 + modules/status-roles/events/presenceUpdate.js | 37 + modules/status-roles/module.json | 24 + .../configs/sticky-messages.json | 60 + modules/sticky-messages/events/botReady.js | 16 + .../sticky-messages/events/messageCreate.js | 73 + modules/sticky-messages/module.json | 24 + .../suggestions/commands/manage-suggestion.js | 129 + modules/suggestions/commands/suggestion.js | 20 + modules/suggestions/config.json | 452 + modules/suggestions/events/messageCreate.js | 8 + modules/suggestions/models/Suggestion.js | 27 + modules/suggestions/module.json | 26 + modules/suggestions/suggestion.js | 77 + modules/team-list/config.json | 130 + modules/team-list/events/botReady.js | 68 + modules/team-list/module.json | 24 + modules/temp-channels/channel-settings.js | 318 + .../temp-channels/commands/temp-channel.js | 140 + modules/temp-channels/config.json | 393 + modules/temp-channels/events/botReady.js | 67 + modules/temp-channels/events/channelDelete.js | 22 + .../temp-channels/events/interactionCreate.js | 191 + .../temp-channels/events/voiceStateUpdate.js | 113 + modules/temp-channels/locales.json | 29 + modules/temp-channels/models/TempChannel.js | 25 + modules/temp-channels/models/TempChannelV1.js | 23 + modules/temp-channels/module.json | 26 + modules/tic-tak-toe/commands/tic-tac-toe.js | 247 + modules/tic-tak-toe/module.json | 28 + modules/tickets/config.json | 301 + modules/tickets/events/botReady.js | 75 + modules/tickets/events/interactionCreate.js | 152 + modules/tickets/events/messageCreate.js | 15 + modules/tickets/models/Message.js | 25 + modules/tickets/models/Ticket.js | 38 + modules/tickets/models/TicketV1.js | 37 + modules/tickets/module.json | 24 + .../twitch-notifications/configs/config.json | 47 + .../configs/streamers.json | 151 + .../twitch-notifications/events/botReady.js | 133 + .../twitch-notifications/models/Streamer.js | 22 + modules/twitch-notifications/module.json | 26 + modules/uno/commands/uno.js | 455 + modules/uno/module.json | 22 + modules/welcomer/configs/channels.json | 311 + modules/welcomer/configs/config.json | 227 + modules/welcomer/configs/random-messages.json | 168 + modules/welcomer/events/guildMemberAdd.js | 97 + modules/welcomer/events/guildMemberRemove.js | 85 + modules/welcomer/events/guildMemberUpdate.js | 70 + modules/welcomer/events/interactionCreate.js | 33 + modules/welcomer/models/User.js | 26 + modules/welcomer/module.json | 27 + package-lock.json | 19082 ++++++++++++++++ package.json | 60 + quick-test.js | 14 + simulate-phish.js | 228 + src/cli.js | 54 + src/commands/help.js | 142 + src/commands/reload.js | 31 + src/events/botReady.js | 4 + src/events/interactionCreate.js | 109 + src/functions/configuration.js | 362 + src/functions/helpers.js | 562 + src/functions/localize.js | 43 + src/gen-doc/Client.js | 97 + src/models/ChannelLock.js | 22 + src/models/DatabaseSchemeVersion.js | 21 + test-phishing.js | 21 + 300 files changed, 51486 insertions(+) create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/SUPPORT.md create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 add-config-element-object.js create mode 100644 anti-phish.code-workspace create mode 100644 config-generator/config.json create mode 100644 config-generator/strings.json create mode 100644 generate-config.js create mode 100644 locales/de.json create mode 100644 locales/en.json create mode 100644 main.js create mode 100644 modules/admin-tools/commands/admin.js create mode 100644 modules/admin-tools/commands/stealemote.js create mode 100644 modules/admin-tools/config.json create mode 100644 modules/admin-tools/module.json create mode 100644 modules/afk-system/commands/afk.js create mode 100644 modules/afk-system/config.json create mode 100644 modules/afk-system/events/messageCreate.js create mode 100644 modules/afk-system/models/User.js create mode 100644 modules/afk-system/module.json create mode 100644 modules/anti-ghostping/config.json create mode 100644 modules/anti-ghostping/events/messageCreate.js create mode 100644 modules/anti-ghostping/events/messageDelete.js create mode 100644 modules/anti-ghostping/module.json create mode 100644 modules/auto-delete/channels.json create mode 100644 modules/auto-delete/events/botReady.js create mode 100644 modules/auto-delete/events/messageCreate.js create mode 100644 modules/auto-delete/events/voiceStateUpdate.js create mode 100644 modules/auto-delete/module.json create mode 100644 modules/auto-delete/voice-channels.json create mode 100644 modules/auto-messager/cronjob.json create mode 100644 modules/auto-messager/daily.json create mode 100644 modules/auto-messager/events/botReady.js create mode 100644 modules/auto-messager/hourly.json create mode 100644 modules/auto-messager/module.json create mode 100644 modules/auto-publisher/config.json create mode 100644 modules/auto-publisher/events/messageCreate.js create mode 100644 modules/auto-publisher/module.json create mode 100644 modules/auto-react/configs/config.json create mode 100644 modules/auto-react/configs/replies.json create mode 100644 modules/auto-react/events/messageCreate.js create mode 100644 modules/auto-react/module.json create mode 100644 modules/auto-thread/config.json create mode 100644 modules/auto-thread/events/messageCreate.js create mode 100644 modules/auto-thread/module.json create mode 100644 modules/betterstatus/config.json create mode 100644 modules/betterstatus/events/botReady.js create mode 100644 modules/betterstatus/events/guildMemberAdd.js create mode 100644 modules/betterstatus/module.json create mode 100644 modules/birthday/birthday.js create mode 100644 modules/birthday/commands/birthday.js create mode 100644 modules/birthday/config.json create mode 100644 modules/birthday/events/botReady.js create mode 100644 modules/birthday/events/guildMemberRemove.js create mode 100644 modules/birthday/models/User.js create mode 100644 modules/birthday/module.json create mode 100644 modules/channel-stats/channels.json create mode 100644 modules/channel-stats/events/botReady.js create mode 100644 modules/channel-stats/module.json create mode 100644 modules/color-me/commands/color-me.js create mode 100644 modules/color-me/configs/config.json create mode 100644 modules/color-me/configs/strings.json create mode 100644 modules/color-me/events/guildMemberUpdate.js create mode 100644 modules/color-me/models/Role.js create mode 100644 modules/color-me/module.json create mode 100644 modules/connect-four/commands/connect-four.js create mode 100644 modules/connect-four/module.json create mode 100644 modules/counter/config.json create mode 100644 modules/counter/events/botReady.js create mode 100644 modules/counter/events/messageCreate.js create mode 100644 modules/counter/milestones.json create mode 100644 modules/counter/models/CountChannel.js create mode 100644 modules/counter/module.json create mode 100644 modules/duel/commands/duel.js create mode 100644 modules/duel/module.json create mode 100644 modules/economy-system/cli.js create mode 100644 modules/economy-system/commands/economy-system.js create mode 100644 modules/economy-system/commands/shop.js create mode 100644 modules/economy-system/configs/config.json create mode 100644 modules/economy-system/configs/strings.json create mode 100644 modules/economy-system/economy-system.js create mode 100644 modules/economy-system/events/botReady.js create mode 100644 modules/economy-system/events/interactionCreate.js create mode 100644 modules/economy-system/events/messageCreate.js create mode 100644 modules/economy-system/models/cooldowns.js create mode 100644 modules/economy-system/models/dropMsg.js create mode 100644 modules/economy-system/models/shop.js create mode 100644 modules/economy-system/models/user.js create mode 100644 modules/economy-system/module.json create mode 100644 modules/fun/commands/hug.js create mode 100644 modules/fun/commands/kiss.js create mode 100644 modules/fun/commands/pat.js create mode 100644 modules/fun/commands/random.js create mode 100644 modules/fun/commands/slap.js create mode 100644 modules/fun/config.json create mode 100644 modules/fun/module.json create mode 100644 modules/giveaways/commands/giveaway.js create mode 100644 modules/giveaways/commands/gmessages.js create mode 100644 modules/giveaways/configs/config.json create mode 100644 modules/giveaways/configs/strings.json create mode 100644 modules/giveaways/events/botReady.js create mode 100644 modules/giveaways/events/interactionCreate.js create mode 100644 modules/giveaways/events/messageCreate.js create mode 100644 modules/giveaways/giveaways.js create mode 100644 modules/giveaways/models/Giveaway.js create mode 100644 modules/giveaways/module.json create mode 100644 modules/guess-the-number/commands/manage.js create mode 100644 modules/guess-the-number/config.json create mode 100644 modules/guess-the-number/events/interactionCreate.js create mode 100644 modules/guess-the-number/events/messageCreate.js create mode 100644 modules/guess-the-number/models/Channel.js create mode 100644 modules/guess-the-number/module.json create mode 100644 modules/hunt-the-code/commands/hunt-the-code-admin.js create mode 100644 modules/hunt-the-code/commands/hunt-the-code.js create mode 100644 modules/hunt-the-code/models/Code.js create mode 100644 modules/hunt-the-code/models/User.js create mode 100644 modules/hunt-the-code/module.json create mode 100644 modules/hunt-the-code/strings.json create mode 100644 modules/info-commands/commands/info.js create mode 100644 modules/info-commands/module.json create mode 100644 modules/info-commands/strings.json create mode 100644 modules/invite-tracking/commands/trace-invites.js create mode 100644 modules/invite-tracking/config.json create mode 100644 modules/invite-tracking/events/guildMemberJoin.js create mode 100644 modules/invite-tracking/events/guildMemberRemove.js create mode 100644 modules/invite-tracking/events/interactionCreate.js create mode 100644 modules/invite-tracking/models/UserInvite.js create mode 100644 modules/invite-tracking/module.json create mode 100644 modules/invite-tracking/onLoad.js create mode 100644 modules/levels/commands/leaderboard.js create mode 100644 modules/levels/commands/manage-levels.js create mode 100644 modules/levels/commands/profile.js create mode 100644 modules/levels/configs/config.json create mode 100644 modules/levels/configs/random-levelup-messages.json create mode 100644 modules/levels/configs/special-levelup-messages.json create mode 100644 modules/levels/configs/strings.json create mode 100644 modules/levels/events/botReady.js create mode 100644 modules/levels/events/guildMemberRemove.js create mode 100644 modules/levels/events/interactionCreate.js create mode 100644 modules/levels/events/messageCreate.js create mode 100644 modules/levels/leaderboardChannel.js create mode 100644 modules/levels/models/User.js create mode 100644 modules/levels/module.json create mode 100644 modules/massrole/commands/massrole.js create mode 100644 modules/massrole/configs/config.json create mode 100644 modules/massrole/configs/strings.json create mode 100644 modules/massrole/module.json create mode 100644 modules/moderation/__tests__/phishingService.test.js create mode 100644 modules/moderation/commands/moderate.js create mode 100644 modules/moderation/commands/report.js create mode 100644 modules/moderation/configs/antiGrief.json create mode 100644 modules/moderation/configs/antiJoinRaid.json create mode 100644 modules/moderation/configs/antiSpam.json create mode 100644 modules/moderation/configs/config.json create mode 100644 modules/moderation/configs/joinGate.json create mode 100644 modules/moderation/configs/strings.json create mode 100644 modules/moderation/configs/verification.json create mode 100644 modules/moderation/events/botReady.js create mode 100644 modules/moderation/events/guildMemberAdd.js create mode 100644 modules/moderation/events/interactionCreate.js create mode 100644 modules/moderation/events/messageCreate.js create mode 100644 modules/moderation/events/messageUpdate.js create mode 100644 modules/moderation/models/ModerationAction.js create mode 100644 modules/moderation/models/UserNotes.js create mode 100644 modules/moderation/moderationActions.js create mode 100644 modules/moderation/module.json create mode 100644 modules/moderation/phishingConfig.js create mode 100644 modules/moderation/phishingHeuristics.js create mode 100644 modules/moderation/phishingService.js create mode 100644 modules/nicknames/configs/config.json create mode 100644 modules/nicknames/configs/strings.json create mode 100644 modules/nicknames/events/botReady.js create mode 100644 modules/nicknames/events/guildMemberUpdate.js create mode 100644 modules/nicknames/models/User.js create mode 100644 modules/nicknames/module.json create mode 100644 modules/nicknames/renameMember.js create mode 100644 modules/partner-list/commands/partner.js create mode 100644 modules/partner-list/config.json create mode 100644 modules/partner-list/events/botReady.js create mode 100644 modules/partner-list/models/Partner.js create mode 100644 modules/partner-list/module.json create mode 100644 modules/partner-list/partnerlist.js create mode 100644 modules/ping-on-vc-join/actual-config.json create mode 100644 modules/ping-on-vc-join/config.json create mode 100644 modules/ping-on-vc-join/events/voiceStateUpdate.js create mode 100644 modules/ping-on-vc-join/module.json create mode 100644 modules/polls/commands/poll.js create mode 100644 modules/polls/configs/config.json create mode 100644 modules/polls/configs/strings.json create mode 100644 modules/polls/events/botReady.js create mode 100644 modules/polls/events/interactionCreate.js create mode 100644 modules/polls/models/Poll.js create mode 100644 modules/polls/module.json create mode 100644 modules/polls/polls.js create mode 100644 modules/quiz/commands/quiz.js create mode 100644 modules/quiz/configs/config.json create mode 100644 modules/quiz/configs/quizList.json create mode 100644 modules/quiz/configs/strings.json create mode 100644 modules/quiz/events/botReady.js create mode 100644 modules/quiz/events/interactionCreate.js create mode 100644 modules/quiz/models/Quiz.js create mode 100644 modules/quiz/models/QuizUser.js create mode 100644 modules/quiz/module.json create mode 100644 modules/quiz/quizUtil.js create mode 100644 modules/reminders/commands/reminder.js create mode 100644 modules/reminders/config.json create mode 100644 modules/reminders/events/botReady.js create mode 100644 modules/reminders/models/Reminder.js create mode 100644 modules/reminders/module.json create mode 100644 modules/reminders/reminders.js create mode 100644 modules/rock-paper-scissors/commands/rock-paper-scissors.js create mode 100644 modules/rock-paper-scissors/module.json create mode 100644 modules/serverinfo/configs/config.json create mode 100644 modules/serverinfo/configs/fields.json create mode 100644 modules/serverinfo/events/botReady.js create mode 100644 modules/serverinfo/module.json create mode 100644 modules/starboard/configs/config.json create mode 100644 modules/starboard/events/botReady.js create mode 100644 modules/starboard/events/messageReactionAdd.js create mode 100644 modules/starboard/events/messageReactionRemove.js create mode 100644 modules/starboard/handleStarboard.js create mode 100644 modules/starboard/models/StarMsg.js create mode 100644 modules/starboard/models/StarUser.js create mode 100644 modules/starboard/module.json create mode 100644 modules/status-roles/configs/config.json create mode 100644 modules/status-roles/events/presenceUpdate.js create mode 100644 modules/status-roles/module.json create mode 100644 modules/sticky-messages/configs/sticky-messages.json create mode 100644 modules/sticky-messages/events/botReady.js create mode 100644 modules/sticky-messages/events/messageCreate.js create mode 100644 modules/sticky-messages/module.json create mode 100644 modules/suggestions/commands/manage-suggestion.js create mode 100644 modules/suggestions/commands/suggestion.js create mode 100644 modules/suggestions/config.json create mode 100644 modules/suggestions/events/messageCreate.js create mode 100644 modules/suggestions/models/Suggestion.js create mode 100644 modules/suggestions/module.json create mode 100644 modules/suggestions/suggestion.js create mode 100644 modules/team-list/config.json create mode 100644 modules/team-list/events/botReady.js create mode 100644 modules/team-list/module.json create mode 100644 modules/temp-channels/channel-settings.js create mode 100644 modules/temp-channels/commands/temp-channel.js create mode 100644 modules/temp-channels/config.json create mode 100644 modules/temp-channels/events/botReady.js create mode 100644 modules/temp-channels/events/channelDelete.js create mode 100644 modules/temp-channels/events/interactionCreate.js create mode 100644 modules/temp-channels/events/voiceStateUpdate.js create mode 100644 modules/temp-channels/locales.json create mode 100644 modules/temp-channels/models/TempChannel.js create mode 100644 modules/temp-channels/models/TempChannelV1.js create mode 100644 modules/temp-channels/module.json create mode 100644 modules/tic-tak-toe/commands/tic-tac-toe.js create mode 100644 modules/tic-tak-toe/module.json create mode 100644 modules/tickets/config.json create mode 100644 modules/tickets/events/botReady.js create mode 100644 modules/tickets/events/interactionCreate.js create mode 100644 modules/tickets/events/messageCreate.js create mode 100644 modules/tickets/models/Message.js create mode 100644 modules/tickets/models/Ticket.js create mode 100644 modules/tickets/models/TicketV1.js create mode 100644 modules/tickets/module.json create mode 100644 modules/twitch-notifications/configs/config.json create mode 100644 modules/twitch-notifications/configs/streamers.json create mode 100644 modules/twitch-notifications/events/botReady.js create mode 100644 modules/twitch-notifications/models/Streamer.js create mode 100644 modules/twitch-notifications/module.json create mode 100644 modules/uno/commands/uno.js create mode 100644 modules/uno/module.json create mode 100644 modules/welcomer/configs/channels.json create mode 100644 modules/welcomer/configs/config.json create mode 100644 modules/welcomer/configs/random-messages.json create mode 100644 modules/welcomer/events/guildMemberAdd.js create mode 100644 modules/welcomer/events/guildMemberRemove.js create mode 100644 modules/welcomer/events/guildMemberUpdate.js create mode 100644 modules/welcomer/events/interactionCreate.js create mode 100644 modules/welcomer/models/User.js create mode 100644 modules/welcomer/module.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 quick-test.js create mode 100644 simulate-phish.js create mode 100644 src/cli.js create mode 100644 src/commands/help.js create mode 100644 src/commands/reload.js create mode 100644 src/events/botReady.js create mode 100644 src/events/interactionCreate.js create mode 100644 src/functions/configuration.js create mode 100644 src/functions/helpers.js create mode 100644 src/functions/localize.js create mode 100644 src/gen-doc/Client.js create mode 100644 src/models/ChannelLock.js create mode 100644 src/models/DatabaseSchemeVersion.js create mode 100644 test-phishing.js diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..91a04be7 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing to this repository +## Getting started +Before you begin: +* This bot is powered by Node.js. Please make sure you have node.js 16 or newer installed. +* Please review our [Code of Conduct](CODE_OF_CONDUCT.md) and our [Terms of service](https://sc-net.work/tos) +* We highly suggest joining our [discord](https://sc-net.work/dc) to discuss up-coming changes with the rest of our community. You can also apply for the open-source-developer-role [here](https://sc-net.work/open-source-dev-application). + +## Setting up +1. Fork and clone this repository and make sure you are on the current **main** branch as that's were development happens. If you are developing a new module, please use the **stable** branch. +2. Run `npm ci` +3. You can code now ^^ +4. Run `npm test` to run ESLint and to ensure any JSDoc changes are valid +5. [Submit a pullrequest](https://github.com/SCNetwork/CustomDcBot/compare). \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..025f9520 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [ "ScootKit", "scderox" ] +custom: ["https://membership.sc-network.net", "https://www.buymeacoffee.com/scderox"] \ No newline at end of file diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 00000000..71274549 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,7 @@ +# Seeking support? + +We only use this issue tracker for bug reports and feature request. We are not able to provide general support or answer questions in the form of GitHub issues. + +For general questions or problems with this bot, please use [discussions](https://github.com/SCNetwork/CustomDCBot/discussions). + +Any issues that don't directly involve a bug or a feature request will likely be closed and redirected. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..66c4c6c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/data/ +/node_modules/ +/config/ +src/functions/scnx-integration.js +/.vscode/ +/.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..85553b81 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,162 @@ +# Changelog + +This changelog contains mostly API-Changes and changes for developers. + +## v3.8.0 +⚠️ Version numbers are not in sync with SCNX versions - v3.8.0 is equivalent to SCNX v3.17.1. + +### New API changes: +* Support `defaultMemberPermissions` instead of `defaultPermission` to simply setup and usage ([docs](README.md#interaction-command)) +* Support for the new `emoji` field type. Values of this field are treated like a string, but can be expected to be an emoji string (= [`Emoji.toString()`](https://old.discordjs.dev/#/docs/discord.js/main/class/Emoji?scrollTo=toString)) + +### New policy changes for modules: + +Modules must +* protect sensitive slash-commands with the proper [`defaultMemberPermissions`](#interaction-command) settings +* must comply with our [end-user documentation requirements](https://docs.scnx.xyz/oss/create-module-docs) + +View full [rules of modules](README.md#rules-for-modules) + +### Additional changes: +* Bunch of Bug-Fixes & Improvements (sync with closed-sourced version) +* Added button-based settings for temp channels in [#96](https://github.com/ScootKit/CustomDCBot/pull/96) +* switched locales because weird stuff in [#97](https://github.com/ScootKit/CustomDCBot/pull/97) +* Add rock paper scissors module in [#104](https://github.com/ScootKit/CustomDCBot/pull/104) +* Add Connect Four module in [#107](https://github.com/ScootKit/CustomDCBot/pull/107) +* Add uno module in [#110](https://github.com/ScootKit/CustomDCBot/pull/110) +* Add quiz module in [#113](https://github.com/ScootKit/CustomDCBot/pull/113) +* Uno fixes in [#119](https://github.com/ScootKit/CustomDCBot/pull/119) +* Updated to close sourced version in [#122](https://github.com/ScootKit/CustomDCBot/pull/122) +* Added security measure for the massrole command in [#121](https://github.com/ScootKit/CustomDCBot/pull/121) +* Add sticky messages in [#125](https://github.com/ScootKit/CustomDCBot/pull/125) +* Add starboard feature in [#127](https://github.com/ScootKit/CustomDCBot/pull/127) +* Fixed a 2 things in the economy-system in [#128](https://github.com/ScootKit/CustomDCBot/pull/128) +* Sticky messages setting to allow responses to bots in [#130](https://github.com/ScootKit/CustomDCBot/pull/130) + +## v3.7.0 +* Rework of configuration localization and loading. All files got migrated. +* Synced open source-version with close-source-SCNX-version. Please read the detailed changelog on our Discord to learn about new module functionality. +* feat(economy-system): Added ability to make command-replies public ([#79](https://github.com/SCNetwork/CustomDCBot/pull/79)) +* feat(Twitch-Notifications): Added live-Roles ([#81](https://github.com/SCNetwork/CustomDCBot/pull/81)) + +## v3.6.0 +* Support for configuration-example-file `content.elementToggle` toggle to improve UX in the SCNX Dashboard ([#76](https://github.com/SCNetwork/CustomDCBot/pull/76)) +* Support for configuration-example-file `content.dependsOn` toggle to improve UX in the SCNX Dashboard ([#76](https://github.com/SCNetwork/CustomDCBot/pull/76)) +* Support for new field-types: `userID`, `imgURL` ([#76](https://github.com/SCNetwork/CustomDCBot/pull/76)) +* Moderation-Modul: Support for Channel-Mutes ([#77](https://github.com/SCNetwork/CustomDCBot/pull/77)) +* Channel-Stats-Modul: Support for userWithRole parameters ([#78](https://github.com/SCNetwork/CustomDCBot/pull/78)) + +## v3.5.0 +* Like ten new previously closed-sourced-modules got added +* Locales-Loading now takes place in splitted files, instead of a big `default-locales.json` +* Added documentation about localizable configuration +* Added information about our Developer-Support-Programs (e.g. Bounties, OSS-Developer-Pool) +* Added new branch-protection settings and improved certain aspects of the repository + +## v3.4.0 +* "Welcomer" can now automatically delete join-messages of users who left the server after joining within 7 days ([#64](https://github.com/SCNetwork/CustomDCBot/pull/64)) +* "auto-react" can now reply to mentions of configured users ([#65](https://github.com/SCNetwork/CustomDCBot/pull/65), [#66](https://github.com/SCNetwork/CustomDCBot/pull/66)) +* Twitch-Notifications now supports Config-Elements ([#67](https://github.com/SCNetwork/CustomDCBot/pull/67)) +* Twitch-Notifications now supports more arguments for messages ([#68](https://github.com/SCNetwork/CustomDCBot/pull/68)) +* Fixed the /shop buy command of economy-Module ([#71](https://github.com/SCNetwork/CustomDCBot/pull/71)) +* Support for timezone type and skipContent file parameter +* Commands can now optionally be synced globally +* Made auto-delete-module public +* Made tickets-module public +* Several fixes of small bugs in welcomer, levels, birthdays, twitch-notifications and tickets module +* Support for `content` option on config-fields with type `channelID` to allow editing of allowed types + +## v3.3.0 +* Bumped and fixed dependencies +* Added code-hunt-module ([#60](https://github.com/SCNetwork/CustomDCBot/pull/60)) +* Remove-Feature for status-roles module ([#61](https://github.com/SCNetwork/CustomDCBot/pull/61)) +* Added color-me module ([#62](https://github.com/SCNetwork/CustomDCBot/pull/62)) +* Added auto react for message authors ([#63](https://github.com/SCNetwork/CustomDCBot/pull/63)) +* Added auto react for category-reactions +* Few new improvements, support for `commandsWarnings` config parameter + +Contributors: [hfgd123](https://github.com/hfgd123), [scderox](https://github.com/scderox) + + +## v3.2.0 +* Added support for timezone-config-parameter +* Bumped dependencies +* New modules: status-role, massrole +* Optimizations for the economy module + +Contributors: [jateute](https://github.com/jateute/), [hfgd123](https://github.com/hfgd123), [scderox](https://github.com/scderox) + +## v3.1.1 +* Discord released their new way of editing slash-command-permissions ([read their blog](https://discord.com/blog/slash-commands-permissions-discord-apps-bots)), which made a lot of features basiclly usless: + * Commands can now only set a `defaultPermission` value + * Commands can not set a `permission` field anymore, as it can't be synced with Discord's API + * Removed the `arrayToApplicationCommandPermissions` helper function as it's not needed anymore + * Removed the auto-generated documentation, as it was never really useful and didn't work + * Bumped dependencies + +## v3.1.0 + +* Made the bot actually work +* Code-Improvements, Bug-Fixes and clarification +* Added support for new module.json fields + * `author.scnxOrgID`: Support for SCNX-Organisation-IDs (allows developers to accept donations and will show up to + users in the dashboard) + * `openSourceURL`: URL to the Source-Code of a module (licenced under an Open-Source-Licence; will show + donation-banners in the SCNX Dashboard (if orgID is set) and qualifies (qualified) developers for financial + support from the Open-Source-Pool of SCNX) +* No Developer-API for modules (apart from mentioned above) should have been changed + +## v3.0.0 + +* Dropped support for message commands +* Module-Database-Models now always get loaded, even if module is not enabled (this allows to enable/disable modules on + the fly) +* Database-Models can not be nested (because no one did that) +* CLI-Commands, Application-Commands, Events and other relevant data will now always get loaded, even if the module is + not enabled (this allows to enable/disable modules on the fly) +* Every time an event or CLI-Command gets executed, the bot will check if that module is enabled and will return if not +* Every time application commands need to get synced, the bot will check if the corresponding module is enabled. + [To ensure the safe performance of all authorized activities](https://soundcloud.com/gamequotes/glados-to-ensure-the-safe) + , this check will also get executed when a command gets executed. +* Errors in module configuration will only disable the module, not stop the bot. + * 💡 Errors in the built-in-configuration will still shut down the bot +* Module-Configuration will now only be generated on startup, not if configuration gets reloaded +* Added `disableModule` to helpers.js +* Improved `embedType` function +* `asyncForEach` is now deprecated, will be removed in v3.1.0 +* Performance: To reduce the number of event listeners on `command`, every event used by every module will only once + register an + event listener. When an event gets invoked, the bot will run every registered module-event. To ensure fast + reaction-times, this will get done synchronously. + +## v2.1.0 + +* Added new concept of localization +* Updated modules to the newest version, including new features, localization and bug-fixes +* Introduced new helper-functions and database-schemes (including channelLock, DatabaseSchemeVerison) +* Introduced autocomplete + +## v2.0.0 + +* Added new configuration-option `logLevel` +* Added logger (`client.logger`) to allow for more detailed logs (instance + of [log4js.Logger](https://github.com/log4js-node/log4js-node)) +* Added `--pm2-setup`-command-argument to indicate an [pm2](https://pm2.keymetrics.io)-setup +* Switched to discord.js version 1.13 which includes breaking changes +* Reworked some configuration-loading and switched to `jsonfile` as a dependencies +* Configuration of every module is now stored in a global client object: `client.configuration[moduleName]`. Please use + this instead of using `require` as this method allows users to reload configuration. +* Commands are now slash commands. Old commands are not recommended being used, but can be by changing `commands-dir` + to `message-commands-dir`. Remember, that in future we may remove this feature. +* It's no longer required to add `module` as a config-parameter for every command +* Added `sendMultipleSiteButtonMessage` to `/src/functions/helpers.js` to create fancy multiple-site-embed-messages +* `embedType()` now returns [MessageOptions](https://discord.js.org/#/docs/main/stable/typedef/MessageOptions) +* `footer` can now be set for each embed individually +* `.eslintrc.js` added - please use this configuration if you create a pullrequest +* Added `client.logChannel` ([TextChannel](https://discord.js.org/#/docs/main/stable/class/TextChannel)) which should be + used as a default for log-channels and in which some relevant information gets sent. ⚠️️ In some cases this value + is `null` so always catch or check the value before any calls on this property. +* Forgot the prefix of your bot? You can now use @-mentions instead of your prefix +* `mesageCommand.config.args` now only (!) accepts an integer which represents how many arguments are at least needed +* Slash-Commands are now available and should be used as much as possible +* Errors in the executing of a command will nun result in a message to the user \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..d21f9dcf --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +opensource@sc-network.net. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8ceb9732 --- /dev/null +++ b/LICENSE @@ -0,0 +1,61 @@ +License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. +“Business Source License” is a trademark of MariaDB Corporation Ab. + +Parameters + +Licensor: Simon Csaba ("ScootKit") +Licensed Work: ScootKit/CustomDCBot. The Licensed Work is (c) 2023 Simon Csaba +Additional Use Grant: You may make production use of the Licensed Work, + provided such use does not include offering the Licensed Work + to third parties on a hosted or embedded basis which is + competitive with Simon Csaba ("ScootKit")'s products. +Change Date: Six years from the date the Licensed Work is published. +Change License: MIT License + +For information about alternative licensing arrangements for the Licensed Work, +please contact oss@scootkit.net. + +Notice + +Business Source License 1.1 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..a82bdc7b --- /dev/null +++ b/README.md @@ -0,0 +1,361 @@ +# Custom-Bot v3 + +Create your own discord bot - Fully customizable and with a lot of features. This bot is for advanced JS-Users, you +should only use it if you have some experience with Javascript, discord.js and JSON files. + +--- + +## Get your own Custom-Bot completely free and with a modern webinterface and a lot more features! + +Go check it out on our [website](https://scnx.xyz) or get started in the [dashboard](https://scnx.app). +In addition to the here +available features we offer: + +* Free hosting +* Custom-Commands +* Easy-to-use Embed-Editor +* Self-Roles +* Send and edit messages in specific channels +* Easy-to-use Configuration-Editor +* Human-Readable Issue Reporting - never look at logs again +* and a modern dashboard +* and *a lot* more - for free + +[Get started now](https://scnx.xyz) - it's free - forever! + +## Applicable [license](LICENSE) terms if you use this bot + +We really love open-source. It does not make sense financially to publish this Source-Code publicly (as our business +model is to host these bots on [SCNX](https://scnx.xyz), but we still do it. +While this project does not fit the [definition of Open Source](https://opensource.org/osd-annotated) +set forward by the Open Source Initiative, +we are committed to allowing you as much freedom as possible. +Please read the [license](LICENSE) and follow it. + +Here's a summary: + +* You may use the bot on your server and change the source code (as long as you follow the license). +* You have to retain a link to the [LICENSE](LICENSE) and this repository in your bot, most likely in your `/help` + command. +* All changes you make to this codebase are subject to these license terms, you cannot remove the link to the license, + even if you change large parts of the bot. +* You may not create a competitor to [SCNX](https://scnx.xyz) or other ScootKit products using this source code. +* You may not use the "ScootKit" brand name or any other trademarks outside of the LICENSE notice. + +Please read the full [license](LICENSE), as the terms laid out there apply. This is not legal advice. + +Failure to abide by these terms might result in deactivation of your bot from Discord or legal action being taken +(but we'll act in good faith and usually try to solve the issue before doing anything drastic). + +## Support development + +As mentioned above, our business model is to host these bots for servers - it does not really make sense to publish our +product here - but we do it anyway - but we need your support! Feel free to [contribute](.github/CONTRIBUTING.md) or +becoming a [GitHub Sponsor](https://github.com/sponsors/ScootKit/). Thank you so much <3 + +## Need help? + +Are you stuck? Please do not ask on our Discord (unless you are using our hosted version), instead ask in +the [discussions-tab](https://github.com/ScootKit/CustomDCBot/discussions). + +## Need something even more custom? + +We are happy to give you a quote for individual requirements. Please email `sales@sc-network.net` with your +requirements. + +### Table of contents + +[Installation](#installation)\ +[Features](#features)\ +[Configuration](#configuration)\ +[Modules](#modules)\ +[Add your own module (or API)](#add-your-own-modules) + +### Installation + +1. Clone this repo +2. Run `npm ci` +3. Run `npm run generate-config` +4. Replace your token in the `config/config.json` file. +5. Start the bot with `npm start` +6. The bot is now generating a `modules.json` and a `strings.json` file inside your `config` directory. You + can [change](#configuration) them. + +When reading thought the code, you may encounter code "tracking" / "issue reporting" parts of the bot. +This part is only enabled in the SCNX-Version and only used to allow users to see (configuration) issues of their bot +and to allow our team to detect bugs more easily (users can opt-out of that if they want to; we use the sentry-sdk for +that, but don't actually send any data to them, instead to our glitchtip instance - the open-source-version does neither +of that). +This open-source-version won't contact SCNX, SC Network and won't share any information with us, don't worry. You +can verify this by looking at the source code, which you should do before executing any code from the internet. + +### Features + +* Everything is split in different [modules](#modules) - you can enable, configure and disable it how you want +* Highly configurable - The goal with this bot is that you can change *everything* +* Built-in phishing and scam URL detection with customizable patterns & ML heuristics (moderation module) including optional log channel for detections + +* A small suite of unit tests for the moderation/phishing logic – run with `npm test` +* Add your own modules +* Easy configuration - Every config field has a description in an example file + +### Configuration + +You can find all the configuration-files inside your `config` folder. Every **enabled module** will have their own +folder with config-files inside them. **These files are generated automatically**. Every module has slightly different +configuration options. Every module has example files. Inside these files are more information about every configuration +option. +Some config values also support [embeds](https://discordjs.guide/popular-topics/embeds.html). This is the case +if `allowEmbed` is true.\ +You either input a string (normal Discord message), or an embed object with the following values: + +* `title`: Title of the embed +* `message`: Message outside the embed (optional) +* `description`: Description of the embed (optional) +* `color`: Color of the embed, must be + a [ColorResolvable](https://old.discordjs.dev/#/docs/discord.js/13.16.0/typedef/ColorResolvable) (optional) +* `url`: URL of the embed (optional) +* `image`: Image of the embed, should be an url (optional) +* `thumbnail`: Thumbnail-Image of the embed, should be an url (optional) +* `author` (optional): + * `name`: Name of the author + * `img`: Image of the author, should be an url +* `fields`: Fields of the embed, must be an array + of [EmbedFieldData](https://old.discordjs.dev/#/docs/discord.js/13.16.0/typedef/EmbedFieldData) (optional) +* `footer`: Footer value (optional, default: global footer value) +* `footerImgUrl`: URL to image of the footer (optional, default: global footer value) + +The footer of the embed is global and is defined in your global `strings.json` file. The timestamp is set automatically +to the current time. + +### Modules + +The bot is split in modules. Each module can register their own commands, events and even database models, so they can +do basically anything. Every module can register "example-config-files" witch are files with information about the +config file, so the bot can automatically check configs and do all the boring stuff for you. + +### Add your own modules + +As per the [License](LICENSE) you *have* to make *every* of your modules publicly available under the same license. +Please read the license for more information. + +**Before you make a module**: +Please create an issue with your suggestion and claim that you are working on it so nobody is working on the same +thing (;\ +Also please read the [Rules for modules](#rules-for-modules).\ +**Submit a module**: Simply create a pull request, and we will check your module and merge it then (; + +#### Rules for modules + +Every module should + +* Use Slash-Commands wherever possible +* Should provide a file with exported functions which other modules can use to manipulate data or perform actions in + your module (eg: an economy module should provide a file with exported functions like `User.addToBalance()`) +* Answer with ephemeral messages wherever it makes sense +* Create as few commands as possible (we have a limit to 100 commands in total), so please try to + use [Sub-Commands](https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups) + wherever possible (eg: instead of having /ban, /kick, /mute etc, have a /moderate command with sub-commands) +* Use the newest features of the discord api and discord.js (buttons, selects, etc) if possible +* Process and Store only needed user information and data +* Support localization (you don't need to translate everything, you only need to support translations, read + more [here](#Localization) +* protect sensitive slash-commands with the proper [`defaultMemberPermissions`](#interaction-command) settings +* must comply with our [end-user documentation requirements](https://docs.scnx.xyz/oss/create-module-docs) +* follow our [terms of service](https://sc-net.work/tos), [Discord's Terms of Service](https://discord.com/tos) and + the [Discord Developer Terms of Service](https://discord.com/developers/docs/legal). A module should not allow users + to bypass or break the mentioned documents. This includes but is not limited to Nitro-Only-Features. + +#### Localization + +We'd like to offer SCNX and this bot in as many languages as possible. Because of this, we highly encourage you to use +translationable systems in your module. + +* Localizations of not-user-editable strings: Use `localize(key, string, replace = {})` from `src/functions/localize.js` + to localize strings. Translations of these strings happen + on [Weblate](https://localize.sc-network.net/projects/custombot/locales/) + * `key`: Key of the string (usually your module name, check out any files in `locales` to get an idea how this + works) + * `string`: Name of the string + * `replace` (optional, object): Will replace `%` in the source string by `` +* Localizations of configuration-files and user-editable strings: All localizable configuration fields are an object + with values keyed based on language codes. + Example: `{"description": {"de": "Beschreibung des Feldes", "en": "Description of the field"}`. Each field needs to + have at least an English value, as every other language will default back to English. + +#### module.json + +Every module has to contain a `module.json` file with the following content: + +* `name` of the module. Should be the same as the name of your dictionary. +* `humanReadableName`: [Localized](#localization) name of the module, shown to users +* `author` + * `name`: Name of the author + * `link`: Link to the author + * `scnxOrgID`: [SCNX](https://scnx.xyz)-Organisation-ID of the developer (allows you to accept donations in the + dashboard and will show up to users in the dashboard) +* `openSourceURL`: URL to the Source-Code of the module licensed under an Open-Source-License (will show + donation-banners in the SCNX Dashboard (if orgID is set) and qualifies (qualified) developers for financial support + from the Open-Source-Pool of SCNX) +* `description`: [Localized](#localization) short description of the module +* `cli` (optional): [CLI-File](#cli-files) of your module +* `commands-dir` (optional): Directory inside your module folder where all + the [interaction-command-files](#interaction-command) are in +* `on-load-event` (optional): File with exported `onLoad` function in it. Gets executed when your commands got loaded + successfully; at this point the Client is not logged in yet, so you can't communicate with Discord (yet). +* `events-dir` (optional): Directory inside your module folder where all the [event-files](#events) are in +* `models-dir` (optional): Directory inside your module folder where all the models-files are in +* `config-example-files` (optional, seriously leave this out when you don't have config files): Array + of [config-files](#example-config-file) inside your module directory. +* `tags` (optional): Array of tags. +* `fa-icon`: Used for matching of icons in our dashboard. We will fill this out for you, please do not set this field. + +#### Interaction-Command + +Note: Interaction-Commands get loaded after the configuration got checked.\ +An interaction-command ("slash command") file has to export the following things: + +* `run` (function; provided arguments: `interaction`): + * Without subcommands: Function that gets triggered if the interactions is being used + * With subcommands: Optional function that gets triggered after the subcommand functions (if specified) got executed +* `beforeSubcommand` (optional, only if subcommands exit): Function which gets executed before the function in + subcommands gets executed +* `autoComplete` (only required if any of your options use `autocomplete`): Object of functions, sorted by + subcommandgroup, subcommand and option name +* `subcommands` (only required if subcommands exist): Object of functions, sorted by subcommandgroup and subcommand +* `help` +* `config` (both for !help and slash-commands) + * `name`: Name of the command (should be the same name as the file name) + * `description`: Description of the command + * `restricted`: Can this command only be run one of the bot operators (e.g. config reloading, change status or ..., + boolean) + * `defaultMemberPermissions`: This will determine which users can use your commands by default - leave `null` (or `undefined`) to allow usage by @everyone, otherwise, use [PermissionsResolvable](https://old.discordjs.dev/#/docs/discord.js/main/typedef/PermissionResolvable). + * `options`: + * [ApplicationCommandOptionData](https://old.discordjs.dev/#/docs/discord.js/13.16.0/typedef/ApplicationCommandData) + OR + * Async function + returning [ApplicationCommandOptionData](https://old.discordjs.dev/#/docs/discord.js/13.16.0/typedef/ApplicationCommandData) ( + gets called with `client` as argument) + +#### Message-Command + +Starting V3, message-commands are no longer supported. Please use [Interaction-Commands](#interaction-command) +instead. Read more in [CHANGELOG.md](CHANGELOG.md). + +#### Events + +An event file should export the following things: + +* `run`: Function that gets triggered if the event gets executed (provided arguments: `client` (discord.js Client) and + all the arguments that gets past by discord.js for this event) +* `allowPartial` (optional, default: `false`): Boolean determining whether the `run` function should be called if the event + has [partial structures](https://discordjs.guide/popular-topics/partials.html#enabling-partials). When enabling, + please make sure you handle partial data correctly. + +#### CLI-Files + +A CLI-File should export the following things: + +* `commands`: Array of the following objects: + * `command`: Command which should be entered in the CLI + * `description`: Description of the command + * `run`: Function which should be executed when the command gets executed. The function gets executed with an object + of following structure as argument: + * `input`: The whole input + * `args`: Array of arguments (split by spaces) + * `client`: [Client](https://old.discordjs.dev/#/docs/discord.js/13.16.0/class/Client) + * `cliCommands`: Array of all CLICommands + +Note: We might allow users to execute CLI-Commands via the Dashboard in the future. This is not supported right now. + +#### Config-Elements + +Certain configuration may contain an array of multiple objects with different values - these are called " +Config-Elements". + +To add a new Config-Element to your configuration +use `node add-config-element-object.js `. + +#### Example config-file + +An example config file should include the following things: + +* `filename`: Name of the generated config file +* `humanname`: [Localized](#localization) name of the file, shown to users +* `description`: [Localized](#localization) description of the file, shown to users +* `configElements` (boolean, default: `false`): If enabled the configuration-file will be an array of an object of the + content-fields +* `elementLimits` (optional, if configElements = `true`): Configuration to limit the amount of configuration elements + that guilds with a specific plan +* `commandsWarnings`: This field is used to indicate, that users need to manually set up the permissions for commands in + their discord-server-settings + * `normal`: Array of commands which that can be configured without any limitation in the discord-server-settings + * `special`: Array of commands that need special configuration in addition to editing the permissions in the + server-settings + * `name`: Name of the command + * `info`: Key by language; Information about the command; used to explain users what exactly they should do +* `content`: Array of content fields: + * `field_name`: Name of the config field + * `default`: [Localized](#localization) default value of this field + * `type`: Can be `channelID`, `userID`, `imgURL`, `select`, `timezone` (treated as string, please check validity + before using), `roleID` + , `boolean`, `integer`, `array`, `emoji`, `keyed` (codename for an JS-Object) + or `string` + * `description`: [Localized](#localization) description of this field + * `humanname`: [Localized](#localization) name of this field show to users + * `allowEmbed` (if type === `array, keyed or string`): Allow the usage of an [embed](#configuration) (Note: Please + use the build-in function in `src/functions/helpers.js`) + * `content` (if type === `array`): Type (see `type` above) of every value + * `content` (if type === `channelID`): Array of + supported [ChannelType](https://old.discordjs.dev/#/docs/discord.js/13.16.0/typedef/ChannelType)s ( + default: `['GUILD_TEXT', 'GUILD_VOICE', 'GUILD_CATEGORY', 'GUILD_NEWS', 'GUILD_STAGE_VOICE']`). To improve user + experience, we recommend adding information about supported types into `description`. The bot will verify that the + channel is inside the bot's guild. + * `content` (if type === `select`): Array of the possible options + * `content` (if type === `keyed`): + * `key`: Type (see `type` above) of the index of every value + * `value`: Type as string (see `type` above) of the value of every value + * `params`: (if type === `string`, array, optional) Possible parameters + * `name`: Name of the parameter (e.g. `%mention%`) + * `description`: [Localized](#localization) Description of the parameter (e.g. `Mention of the user`) + * `isImage`: If true, users will be able to set this parameter as Image, Author-Icon, Footer-Icon or Thumbnail + of an embed (only if `allowEmbed` is enabled) + * `allowNull` (default: `false`, optional): If the value of this field can be empty + * `disableKeyEdits` (if type === `keyed`): If enabled the user can not edit the keys of the object + * `elementToggle` (if type === `boolean`): If this option gets turned off, other fields of the config-element / file + will not be rendered in the dashboard + * `dependsOn` (a name of any (other) boolean-field): If the referenced boolean field (the value of this option + should be equal to the `field.field_name` of a boolean field) is turned off, the field will be not be rendered in + the dashboard + * `links` (optional): Array of links displayed below the field description in the SCNX Dashboard + * `label`: [Localized](#localization) label of the link displayed to the user + * `url`: URL the user will be redirected to on click + +#### `botReady`-Event and Config-Reload + +If you plan to use the [ready](https://old.discordjs.dev/#/docs/discord.js/13.16.0/class/Client?scrollTo=e-ready) event of +discord.js to run some action when the client is ready, and you need to load some configuration-files you should use +the `botReady`-event instead. Please remember that this event gets re-emitted on configuration reloading. If you set +callbacks that get executed later or similar please remember to remove them on `configReload`. If you set intervals, +please push the return value to `client.intervals` to get them removed on `configReload` or do it manually. + +#### Helper-Functions + +The bot includes a lot of functions to make your live easier. Check out the file `src/functions/helpers.js`. + +### Support for developers + +As we earn some money with hosting your modules for users, we have decided to give you some (remember, we need to pay +for hosting) of this money. Here are the main ways to earn some pocket-cash with developing for SCNX: + +* [Open-Source-Developer-Pool](https://faq.scnx.app/open-source-developer-pool/): We give you a monthly amount for each + paying server using your module +* [Bounties](https://faq.scnx.app/open-source-developer-pool/#bounties): We give you a small amount of money for merged + pull-requests and contributions + We support a lot of payout-methods, learn more [here](https://faq.scnx.app/scnx-referrals-faq/#payout-methods). + +© Simon Csaba, 2020-2023 + +ScootKit is a trademark, registered in Germany. + +We ♥ you - yes you. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..bf8118e2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,4 @@ +# Security Policy + +The newest version of the code inside this repository is eligible for +ScootKit's [Vulnerability Disclosure Policy](https://scootkit.net/security). \ No newline at end of file diff --git a/add-config-element-object.js b/add-config-element-object.js new file mode 100644 index 00000000..2af55b58 --- /dev/null +++ b/add-config-element-object.js @@ -0,0 +1,19 @@ +const args = process.argv.slice(2); +if (!args[0] || !args[1]) { + console.error('Wrong usage. node add-embedtype-object.js '); + process.exit(1); +} + +const jsonfile = require('jsonfile'); +const exampleFile = jsonfile.readFileSync(args[0]); +let configFile = jsonfile.readFileSync(args[1]); +if (!Array.isArray(configFile)) configFile = []; + +const newObject = {}; +for (const field of exampleFile.content) { + newObject[field.field_name] = field['default-en'] || field.default; +} + +configFile.push(newObject); +jsonfile.writeFileSync(args[1], configFile, {spaces: 2}); +console.log('Successfully added default object to configuration-file.'); \ No newline at end of file diff --git a/anti-phish.code-workspace b/anti-phish.code-workspace new file mode 100644 index 00000000..876a1499 --- /dev/null +++ b/anti-phish.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/config-generator/config.json b/config-generator/config.json new file mode 100644 index 00000000..fef94470 --- /dev/null +++ b/config-generator/config.json @@ -0,0 +1,179 @@ +{ + "description": { + "en": "Configure the basic features of the bot here", + "de": "Generelle Konfiguration deines Bots" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "token", + "humanName": {}, + "default": { + "en": "yourtokengoeshere" + }, + "description": { + "en": "Replace this with your token" + }, + "hidden": true, + "type": "string" + }, + { + "name": "prefix", + "humanName": { + "en": "Prefix of your bot", + "de": "Prefix deines Botes" + }, + "default": { + "en": "!" + }, + "description": { + "en": "Set the prefix of your bot here", + "de": "Dein eigener Prefix - Wir empfehlen ihn einfach bei ! zu belassen." + }, + "type": "string" + }, + { + "name": "botOperators", + "humanName": {}, + "default": { + "en": [] + }, + "description": { + "en": "Bot operators can reload the configuration and perform system relevant actions with this bot. Please only add users you really trust (and yourself of course)" + }, + "hidden": true, + "type": "array", + "content": "string" + }, + { + "name": "guildID", + "humanName": {}, + "default": { + "en": "489786377261678592" + }, + "description": { + "en": "Replace this the id of the guild the bot should work in." + }, + "hidden": true, + "type": "guildID" + }, + { + "name": "disableStatus", + "humanName": { + "en": "Disable Bot-Status", + "de": "Bot-Status deaktivieren" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, the bot won't have a status in discord", + "de": "Wenn aktiviert wird der Bot keinen Status in Discord haben" + }, + "type": "boolean" + }, + { + "name": "user_presence", + "humanName": { + "en": "Bot-Status" + }, + "default": { + "en": "Change this in your Bot-Configuration on scnx.app", + "de": "Ändere das in deiner Bot-Konfiguration auf scnx.app" + }, + "description": { + "en": "This will show up in Discord as \"Playing \"", + "de": "Das wird in Discord als \"Spielt \" angezeigt" + }, + "type": "string" + }, + { + "name": "logLevel", + "humanName": { + "en": "Logging-Level" + }, + "default": { + "en": "debug" + }, + "description": { + "en": "Log-Level of the bot. Leave it as it is, if you don't know what this means", + "de": "Log-Level des Bots. Belasse es wie es ist, wenn du nicht weißt, was das bedeutet." + }, + "type": "select", + "content": [ + "debug", + "info", + "warn", + "error", + "fatal", + "off" + ] + }, + { + "name": "logChannelID", + "humanName": { + "en": "Log-Channel", + "de": "Log-Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "Default log-channel for most modules and used to log relevant information", + "de": "Standard Log-Kanal für viele Module. Wird außerdem genutzt, um wesentliche Bot-Informationen zu senden" + }, + "type": "channelID", + "allowNull": true + }, + { + "name": "timezone", + "humanName": { + "en": "Timezone", + "de": "Zeitzone" + }, + "default": { + "en": "Europe/Berlin" + }, + "description": { + "en": "Timezone the bot runs in", + "de": "Zeitzone in der der Bot laufen soll" + }, + "type": "timezone" + }, + { + "name": "disableEveryoneProtection", + "humanName": { + "en": "Allow @everyone / @here pings", + "de": "@everyone und @here Pings erlauben" + }, + "default": { + "en": false + }, + "description": { + "en": "Allows @everyone and @here pings for messages configurable in the dashboard", + "de": "Erlaubt @everyone und @here pings in im Dashboard anpassbaren Nachrichten" + }, + "type": "boolean", + "pro": true + }, + { + "name": "syncCommandGlobally", + "humanName": { + "en": "Sync module commands as global commands", + "de": "Speichere Modul-Befehle als Globale-Befehle" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, module-commands will be synced to discord as global commands. They will show up on other servers, but won't work. Syncing can take up to 2 hours, so changes may not be reflected immediately.", + "de": "Wenn aktiviert, werden Befehle von Modulen als Globale-Befehle zu Discord gesendet. Sie werden auf anderen Servern angezeigt, werden aber nicht funktionieren. Synchronisierung kann bis zu 2 Stunden dauern." + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/config-generator/strings.json b/config-generator/strings.json new file mode 100644 index 00000000..371fa39b --- /dev/null +++ b/config-generator/strings.json @@ -0,0 +1,255 @@ +{ + "description": { + "en": "Configure strings & messages of your bot here", + "de": "Passe die Nachrichten deines Botes an" + }, + "humanName": { + "en": "Messages", + "de": "Nachrichten" + }, + "filename": "strings.json", + "content": [ + { + "name": "addAtToUsernames", + "humanName": { + "en": "Add @ to usernames", + "de": "@ zu Nutzernamen hinzufügen" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, every username will be prefixed by an \"@\". Example: \"scderox\" -> \"@scderox\"", + "de": "Wenn aktiviert, wird vor jedem Nutzername ein \"@\" eingefügt. Beispiel: \"scderox\" -> \"@scderox\"" + }, + "type": "boolean" + }, + { + "name": "footer", + "humanName": { + "en": "Embed-Footer" + }, + "default": { + "en": "Powered by scnx.xyz ⚡" + }, + "description": { + "en": "Footer of every embed", + "de": "Footer jedes Embeds" + }, + "type": "string", + "pro": true + }, + { + "name": "footerImgUrl", + "humanName": { + "en": "Embed-Footer-Image-URL", + "de": "Embed-Footer-Bild-URL" + }, + "default": { + "en": "https://scnx.xyz/favicon.png" + }, + "description": { + "en": "Footer-Image of every embed", + "de": "Footer-Bild von jedem Embed" + }, + "type": "string", + "pro": true + }, + { + "name": "need_args", + "humanName": { + "en": "More arguments are needed", + "de": "Mehr Argumente werden benötigt" + }, + "default": { + "en": "This command needs more arguments - you passed %count%, but you need to provide at least %neededCount%.", + "de": "This command needs more arguments - you passed %count%, but you need to provide at least %neededCount%." + }, + "description": { + "en": "This message gets send if there are not enough arguments specified", + "de": "Diese Nachricht wird versendet, wenn eine oder mehrere Argumente für einen Befehl fehlen" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "count", + "description": { + "en": "Count of arguments provided", + "de": "Anzahl von angegebenen Parameter" + } + }, + { + "name": "neededCount", + "description": { + "en": "Count of arguments needed", + "de": "Anzahl von benötigten Argumenten" + } + } + ] + }, + { + "name": "updated_roles", + "humanName": { + "en": "Roles updated", + "de": "Rollen erfolgreich geupdated" + }, + "default": { + "en": "✅ Updated roles according to your settings", + "de": "✅ Rollen-Änderungen übernommen" + }, + "description": { + "en": "This message gets send after a user selects self-roles on a self-role-element.", + "de": "Diese Nachricht wird gesendet, wenn ein Nutzer eine Self-Rolle auswählt." + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "added_role", + "humanName": { + "en": "Role added", + "de": "Rolle erfolgreich hinzugefügt" + }, + "default": { + "en": "✅ Role %role% successfully added", + "de": "✅ Rolle %role% erfolgreich hinzugefügt" + }, + "description": { + "en": "This message gets send when a user adds a role to themselves.", + "de": "Diese Nachricht wird gesendet, wenn ein Nutzer sich selbst eine Self-Rolle hinzufügt." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "role", + "description": { + "en": "Name of the role", + "de": "Name der Rolle" + } + } + ] + }, + { + "name": "removed_role", + "humanName": { + "en": "Role removed", + "de": "Rolle erfolgreich entfernt" + }, + "default": { + "en": "✅ Role %role% successfully removed", + "de": "✅ Rolle %role% erfolgreich entfernt" + }, + "description": { + "en": "This message gets send when a user removes a role from themselves.", + "de": "Diese Nachricht wird gesendet, wenn ein Nutzer sich selbst eine Self-Rolle entfernt." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "role", + "description": { + "en": "Name of the role", + "de": "Name der Rolle" + } + } + ] + }, + { + "name": "not_enough_permissions", + "humanName": { + "en": "Not enough permissions", + "de": "Nicht genügend Rechte" + }, + "default": { + "en": "Seems like you don't have enough permissions.", + "de": "Scheint als hättest du nicht genügend Rechte." + }, + "description": { + "en": "This message gets send if an user don't hase enough permissions", + "de": "Diese Nachricht wird versendet, wenn der Nutzer nicht genügen Rechte hat" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "helpembed", + "humanName": { + "en": "Help-Message", + "de": "Hilfe-Nachricht" + }, + "default": { + "en": { + "title": "Help", + "description": "You can find every command here", + "module_translation": "%name% by %author%: %description%", + "build_in": "Build-In-Commands" + }, + "de": { + "title": "Help", + "description": "Alle Commands findest du hier", + "module_translation": "%name% by %author%: %description%", + "build_in": "Build-In-Commands" + } + }, + "description": { + "en": "Strings for help command" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + }, + { + "name": "disableHelpEmbedStats", + "humanName": { + "en": "Disable Stats in Help-Embed", + "de": "Deaktiviere Stats im Hilfe-Embed" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, the stats-field in the Help-Embed will get hidden", + "de": "Wenn aktiviert, wird der Stats-Bereich im Help-Embed verborgen" + }, + "type": "boolean", + "pro": true + }, + { + "name": "disableFooterTimestamp", + "humanName": { + "en": "Disable default Timestamp in footer", + "de": "Standard-Timestamp im Footer deaktivieren" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, the current time will not be displayed in the embed footer", + "de": "Wenn aktiviert, wird die Aktuelle Uhrzeit nicht im Footer angezeigt" + }, + "type": "boolean" + }, + { + "name": "putBotInfoOnLastSite", + "humanName": { + "en": "Put Bot-Info on the last site of the Help-Embed", + "de": "Verschiebt die Bot-Info auf die letzte Seite im Hilfe-Embed" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, the Bot-Info-Section of the Help-Embed will be moved to the last site", + "de": "Wenn aktiviert, wird der Bot-Info-Bereich im Help-Embed auf die letzte Seite verschoben" + }, + "type": "boolean", + "pro": true + } + ] +} \ No newline at end of file diff --git a/generate-config.js b/generate-config.js new file mode 100644 index 00000000..656141ca --- /dev/null +++ b/generate-config.js @@ -0,0 +1,33 @@ +const exampleFile = require('./config-generator/config.json'); +const config = {}; +const jsonfile = require('jsonfile'); +const fs = require('fs'); +let confDir = `${__dirname}/config`; +const args = process.argv.slice(2); +if (args[0] === '--help' || args[0] === '-h') { + console.log('node generate-config.js '); + process.exit(); +} +if (args[0]) { + confDir = args[0]; +} +try { + require(`${confDir}/config.json`); + console.error('Seems like you already have an config file! You can start the bot now with "npm start"!'); + process.exit(1); +} catch (e) { + console.log('[INFO] Starting generation...'); + exampleFile.content.forEach(async field => { + if (!field.name) return; + config[field.name] = field.default.en; + }); + + if (!fs.existsSync(`${confDir}`)) { + fs.mkdirSync(`${confDir}`); + } + + jsonfile.writeFile(`${confDir}/config.json`, config, {spaces: 2}, (err => { + if (err) console.error(`[ERROR] An error occurred while saving: ${err}`); + else console.log('[DONE]: Config was saved successfully successfully. Please edit the config.json file inside your config dictionary and start the bot then with "npm start". Have a great day <3'); + })); +} \ No newline at end of file diff --git a/locales/de.json b/locales/de.json new file mode 100644 index 00000000..320d9b3e --- /dev/null +++ b/locales/de.json @@ -0,0 +1,986 @@ +{ + "main": { + "startup-info": "SCNX-CustomBot v2 - Log-Level: %l", + "missing-moduleconf": "Es fehlt die Modul-Konfiguration. Alle Module werden deaktiviert und die Datei wird generiert", + "sync-db": "Datenbank erfolgreich synchronisiert", + "login-error": "Der Bot konnte sich nicht anmelden. Fehler: %e", + "not-invited": "Der Bot ist nicht auf deinem Server, bitte lade ihn ein: %inv", + "logged-in": "Bot ist nun als %tag eingeloggt und ist online.", + "logchannel-wrong-type": "Es wurde kein Log-Kanal gesetzt oder der Log-Kanal hat den falschen Type (nur Text-Kanäle werden unterstützt).", + "config-check-failed": "Konfiguration-Überprüfung ist fehlgeschlagen. Du findest weitere Information in deinem Log. Der Bot stoppt sich nun automatisch.", + "bot-ready": "Der Bot wurde erfolgreich gestartet und ist nun bereit, Commands zu empfangen", + "no-command-permissions": "Die Befehle des Bots konnten nicht übernommen werden, bitte hier klicken: %inv", + "perm-sync": "Rechte für /%c wurden synchronisiert", + "perm-sync-failed": "Rechte für /%c konnten nicht synchronisiert werden: %e", + "loading-module": "Modul %m wird geladen", + "module-disabled": "Module %m ist deaktiviert", + "command-loaded": "Befehl %d/%f wurde geladen", + "command-dir": "Befehle in %d/%f werden geladen", + "command-sync": "Befehle wurden synchronisiert", + "command-no-sync-required": "Befehle sind aktuell, es ist keine Synchronisation notwendig", + "event-loaded": "Event %d/%f wurde geladen", + "event-dir": "Befehle in %d/%f werden geladen", + "model-loaded": "Datenbank-Model %d/%f wurde geladen", + "model-dir": "Datenbank-Modele in %d/%f werden geladen", + "loaded-cli": "API-Aktion %c in %p wurde geladen", + "channel-lock": "Kanal gesperrt", + "channel-unlock": "Kanal entsperrt", + "channel-unlock-data-not-found": "Kanal mit ID %c konnte nicht entsperrt werden, weil dieser nie gesperrt wurde (was merkwürdig ist).", + "login-error-token": "Der Bot konnte sich nicht anmelden, da der angegebene Token ungültig ist. Bitte ändere deinen Token.", + "login-error-intents": "Der Bot konnte sich nicht anmelden, da die Intents nicht korrekt aktiviert wurden. Bitte aktiviere \"PRESENCE INTENT\", \"SERVER MEMBERS INTENT\" und \"MESSAGE CONTENT INTENT\" in deinem Discord-Developer-Dashboard: %url", + "module-disable": "Das Modul %m wurde deaktiviert, da %r", + "migrate-success": "Migration von %o zu %m erfolgreich abgeschlossen.", + "migrate-start": "Migration von %o zu %m gestarted... Bitte den Bot nicht anhalten", + "global-command-sync": "Synchronisierte globale Anwendungsbefehle", + "guild-command-sync": "Synchronisierte Serveranwendungsbefehle", + "guild-command-no-sync-required": "Serveranwendungsbefehle sind auf dem neuesten Stand – keine Synchronisierung erforderlich", + "global-command-no-sync-required": "Globale Anwendungsbefehle sind auf dem neuesten Stand – keine Synchronisierung erforderlich" + }, + "scnx": { + "activating": "Aktiviere SCNX-Integration...", + "notLongerInSCNX": "Dieser Server wurde deaktiviert oder ist nicht länger bei SCNX. Beende Prozess.", + "activated": "SCNX Integration erfolgreich aktiviert", + "loggedInAs": "Eigener Bot %b auf Server %s mit Version %v und dem Plan %p eingeloggt", + "choose-roles": "Rollen auswählen", + "localeUpdate": "Locales aktualisiert", + "localeUpdateSkip": "Locales-Aktualisierung übersprungen", + "localeFetchFailed": "Locales zur Aktualisierung konnten nicht geladen werden", + "issueTrackingActivated": "Automatische Fehlermeldungen erfolgreich aktiviert. Dein Bot wird automatisch Fehler, die du nicht beheben kannst, an die Entwickler melden.", + "newVersion": "**⬆ Neue Version verfügbar: %v 🎉**\n\nUm diese Änderungen zu übernehmen, starte bitte deinen Bot in deinem SCNX-Dashboard neu.\nUpdates wie dieses sollten so schnell wie möglich angewandt werden, da sie Bug-Fixes, Verbesserungen und Optimierungen enthalten. Einen detaillierten Change-Log findest du auf unserem Discord-Server." + }, + "reload": { + "reloading-config": "Konfiguration wird neu geladen...", + "reloading-config-with-name": "Nutzer %tag lädt die Konfiguration neu...", + "reloaded-config": "Konfiguration erfolgreich neu geladen.\nVon %totalModules Modulen sind %enabled aktiviert und %configDisabled wurden deaktiviert, weil ihre Konfiguration fehlerhaft war.", + "reload-failed": "Das Neuladen der Konfiguration ist fehlgeschlagen. Bot fährt herunter.", + "reload-successful-syncing-commands": "Erfolgreich neu geladen, Befehle werden synchronisiert...", + "reload-failed-message": "**Fehlgeschlagen**\n```%r```\n**Lies bitte deinen Log, um mehr zu erfahren**\nDer Bot fährt nun herunter, bye :wave:", + "command-description": "Lädt die Konfiguration neu" + }, + "config": { + "checking-config": "Konfiguration wird überprüft...", + "done-with-checking": "Konfiguration erfolgreich überprüft.", + "creating-file": "Konfiguration %m/%f existiert aktuell nicht, wird aber gleich erstellt...", + "checking-of-field-failed": "Ein Fehler bei der Überprüfung von %fieldName in %m/%f ist aufgetreten", + "saved-file": "Konfiguration %f in %m wurde erfolgreich generiert.", + "moduleconf-regeneration": "Modul-Konfiguration wird regeniert, es werden keine Einstellungen überschrieben.", + "moduleconf-regeneration-success": "Module-Konfiguration wurde regeniert.", + "channel-not-found": "Kanal mit ID \"%id\" wurde nicht gefunden", + "user-not-found": "Discord-Account mit ID \"%id\" wurde nicht gefunden", + "channel-not-on-guild": "Kanal mit ID \"%id\" ist nicht auf deinem Server", + "role-not-found": "Rolle mit ID \"%id\" wurde nicht auf deinem Server gefunden", + "config-reload": "Gesamte Konfiguration wird neugeladen...", + "channel-invalid-type": "Kanal mit ID \"%id\" hat einen Typen, der nicht für dieses Feld verwendet werden darf" + }, + "helpers": { + "timestamp": "%dd.%mm.%yyyy um %hh:%min", + "you-did-not-run-this-command": "Du hast diesen Befehl nicht ausgeführt. Führe den Command selber aus, um Navigations-Knöpfe benutzen zu können.", + "next": "Weiter", + "back": "Zurück", + "toggle-data-fetch-error": "SC Network Release: Toggle-Daten konnten nicht geladen werden", + "toggle-data-fetch": "SC Network Release: Toggle-Daten geladen" + }, + "command": { + "startup": "Der Bot startet gerade. Bitte warte einige Minuten, bevor du diesen Befehl ausführst.", + "not-found": "Befehl nicht gefunden", + "used": "%tag (%id) hat den Befehl /%c %g %s ausgeführt", + "message-used": "%tag (%id) hat den Nachrichten-Befehl %p%c ausgeführt", + "execution-failed": "Die Ausführung von /%c %g %s ist fehlgeschlagen: %e", + "message-execution-failed": "Die Ausführung von %p%c ist fehlgeschlagen: %e", + "autcomplete-execution-failed": "Die Ausführung der Autovervollständigung des Befehls /%c %g %s mit der Option %f ist fehlgeschlagen: %e", + "execution-failed-message": "**🔴 Ein Fehler bei Ausführung des Befehls ist aufgetreten 🔴**\nDies sollte nicht passieren und kann verschiedene Gründe haben.\n\nDer Bot hat automatisch, außer diese Funktion wurde in der Konfiguration deaktiviert, den Fehler an den zuständigen Entwickler gemeldet. Unter Umständen wird dich ein Entwickler bei Rückfragen anschreiben, wenn du Mitglied des [SC Network Discords]() bist.", + "error-giving-role": "Es ist ein Fehler aufgetreten als ich versucht habe dir deine Rollen zu geben ):", + "module-disabled": "Dieser Befehl ist Teil von \"%m\", welches deaktiviert ist. Dies kann entweder von den Server-Admins gewollt (und slash-commands wurden noch nicht synchronisiert) oder durch einen Konfigurationsfehler bedingt sein. Bitte prüfe (oder frag die Admins) die Konfiguration und Logs des Bots zu überprüfen um Details zu erhalten.", + "wrong-guild": "Dieser Befehl ist nur auf den Server **%g** verfügbar." + }, + "help": { + "bot-info-titel": "ℹ️ Bot-Infos", + "bot-info-description": "Dieser Bot wurde vom [ScootKit](https://scootkit.net)-Team und unseren geliebten [Open-Source-Beitragenden](https://github.com/ScootKit/CustomDCBot/graphs/contributors) entwickelt und ist unter der [Business Source License](https://github.com/ScootKit/CustomDCBot/blob/main/LICENSE] lizenziert.", + "stats-title": "📊 Statistiken", + "stats-content": "Aktive Module: %am\nRegistrierte Befehle: %rc\nBot-Version: %v\nLäuft auf Server %si\n[Server-Plan](https://scnx.xyz/plan): %pl\nLetzter Neustart: %lr\nLetze Neuladung: %lrl", + "command-description": "Zeigt dir alle Befehle an", + "slash-commands-title": "Slash-Commands", + "slash-commands-description": "Du hast wahrscheinlich **%c Befehle** durch Nichtnutzung von Slash-Commands verpasst - bitte nutze `/help` als Slash-Command um alle verfügbaren Befehle zu sehen." + }, + "bot-feedback": { + "command-description": "Schick Feedback über den Bot an den Bot Entwickler", + "submitted-successfully": "Vielen Dank für dein Feedback! Wir haben es empfangen und unser Team wird es sich bald anschauen. Sollten wir Rückfragen haben, werden wir dich eventuell per Privatnachricht anschreiben (falls du auf unserem [Support-Server]() bist öffnen wir ein Ticket). Vielen Dank, dass du durch dein Feedback [Eigen Bots]() für alle besser machst.\n\nDein Feedback unterliegt unseren [Nutzungsbedingungen]() und unserer [Datenschutzerklärung]().", + "failed-to-submit": "Ich konnte dein Feedback leider nicht an den Entwickler senden. Dies kann entweder daran legen, das sdu gesperrt wurdest, oder daran, dass es technische Schwierigkeiten gibt. Du kannst weiterhin Vorschläge und Bugs im [Feature-Board]() melden bzw. vorschlagen. Vielen Dank.", + "feedback-description": "Dein Feedback. Es sollte neutral, konstruktiv und hilfreich sein" + }, + "admin-tools": { + "position": "%i hat die Position %p.", + "position-changed": "Die Position von %i wurde zu %p aktualisiert.", + "category-can-not-have-category": "Eine Kategorie kann keine Kategorie haben", + "not-category": "Die Kategorie eines Kanals kann nicht zu einem anderen Nicht-Kategorie-Kanals geändert werden", + "changed-category": "Die Kategorie von %c wurde zu %cat aktualisiert", + "command-description": "Führe Admin-Aktionen als Command aus", + "new-position-description": "Neue Position", + "movechannel-description": "Zeigt oder ändert die Position eines Kanals", + "moverole-description": "Zeigt oder ändert die Position einer Rolle", + "setcategory-description": "Setzt die Kategorie eines Kanals", + "channel-description": "Kanal auf welchem diese Aktion ausgeführt werden soll", + "role-description": "Rolle auf welchem diese Aktion ausgeführt werden soll", + "category-description": "Neue Kategorie des Kanals", + "emoji-too-much-data": "Bitte wähle **nur einen** Emote aus und schreibe nichts anders", + "emoji-import": "Der Emote \"%e\" wurde erfolgreich importiert.", + "stealemote-description": "Leiht einen Emote von einem anderen Server permanent aus", + "emote-description": "Der Emote, der permanent geliehen werden soll" + }, + "welcomer": { + "channel-not-found": "[welcomer] Kanal nicht gefunden: %c" + }, + "birthdays": { + "channel-not-found": "[Geburtstage] Kanal nicht gefunden: %c", + "sync-error": "[Geburtstage] Der Status von %u war auf \"Aktiviert\" gesetzt, aber es gab keinen Synchronisationskandidaten, deswegen wurde die Synchronisation deaktiviert", + "age-hover": "%a Jahre alt", + "sync-enabled-hover": "Geburtstag synchronisiert", + "verified-hover": "Geburtstag verifiziert", + "no-bd-this-month": "In diesem Monat hat niemand Geburtstag ):", + "no-birthday-set": "Du hast aktuell keinen Geburtstag auf diesem Server registriert. Wenn du autoSync aktiviert hast, kann es bis zu 24 Stunden dauern, bis dein Geburtstag auf jedem Server synchronisiert wurde. [Erfahre mehr über Geburtstagssynchronisation]().", + "birthday-status": "Dein Geburtstag ist aktuell auf **%dd.%mm%yyyy** eingestellt%age. %syncstatus", + "your-age": "was bedeutet, dass du **%age%** alt bist", + "sync-on": "Dein Geburtstag wird mit deinem [SC Network-Konto](https://sc-network.net/dashboard) synchronisiert.", + "sync-off": "Dein Geburtstag ist lokal für diesen Server eingestellt und wird nicht synchronisiert", + "no-sync-account": "Es scheint, als hättest du kein [SC Network-Konto](https://sc-network.net/dashboard) oder keine Informationen zu deinem Geburtstag in diesem eingetragen.", + "auto-sync-on": "Es scheint als hättest du automatische Synchronisierung in deinem [SC Network-Konto](https://sc-network.net/dashboard) aktiviert. Das bedeutet, dass dein Geburtstag immer auf jedem Server synchronisiert wird. [Erfahre mehr]().\nDein Geburtstag wird nicht angezeigt? Es kann bis zu 24 Stunden dauern (normalerweise sind es weniger als zwei Stunden), damit dieser synchronisiert wird. Hab also noch etwas Geduld und warte einfach noch ein Bisschen.", + "enabled-sync": "Die Synchronisierung wurde erfolgreich aktiviert :+1:", + "disabled-sync": "Die Synchronisierung wurde erfolgreich deaktiviert. Du kannst nun deinen Geburtstag auf diesem Server ändern oder entfernen.", + "delete-but-sync-is-on": "Du hast Synchronisierung aktiviert. Bitte deaktiviere Synchronisierung um deinen Geburtstag zu entfernen.", + "deleted-successfully": "Geburtstag erfolgreich entfernt.", + "only-sync-allowed": "Dieser Server erlaubt nur mit einem [SC Network-Konto](https://sc-network.net/dashboard) synchronisierte Geburtstage", + "invalid-date": "Ungültiges Datum eingegeben", + "against-tos": "Du musst mindestens 13 Jahre alt sein um Discord zu benutzen. Bitte lies die [Nutzungsbedingungen]() der Plattform und [lösche dein Konto](), wenn du unter 13 bist um nicht gegen die [Nutzungsbedingungen]() von Discord zu verstoßen und warte %waitTime (oder bis zum entsprechenden Alter, sollte dein Land [hier]() gelistet sein) Jahre bevor du ein neues Konto erstellst.", + "too-old": "Du scheinst zu alt für eine lebende Person zu sein", + "command-description": "Bearbeite, sehe und entferne deinen Geburtstag", + "status-command-description": "Zeigt den aktuellen Status deines Geburtstags", + "sync-command-description": "Bearbeite die Synchronisationseinstellungen für diesen Server", + "sync-command-action-description": "Aktion, welche mit deinen Synchronisationseinstellungen ausgeführt werden soll", + "sync-command-action-enable-description": "Synchronisation aktivieren", + "sync-command-action-disable-description": "Synchronisation deaktivieren", + "set-command-description": "Setzt deinen Geburtstag", + "set-command-day-description": "Tag deines Geburtstags", + "set-command-month-description": "Monat deines Geburtstags", + "set-command-year-description": "Jahr deines Geburtstags", + "delete-command-description": "Entfernt deinen Geburtstag von diesem Server", + "migration-happening": "Datenbank-Schema nicht aktuell. Datenbank wird migriert. Starte deinen Bot nicht neu, um Datenverlust zu vermeiden.", + "migration-done": "Datenbank wurde erfolgreich migriert." + }, + "months": { + "1": "Januar", + "2": "Februar", + "3": "März", + "4": "April", + "5": "Mai", + "6": "Juni", + "7": "Juli", + "8": "August", + "9": "September", + "10": "Oktober", + "11": "November", + "12": "Dezember" + }, + "giveaways": { + "no-link": "Kein", + "no-winners": "Keine", + "not-supported-for-news-channel": "Nicht unterstützt für Ankündigungs-Kanäle", + "required-messages": "Du brauchst %mc neue Nachrichten (überprüfe deine Nachrichten mit \\`/gmessages\\`)", + "required-messages-user": "Du musst mindestens %mc neue Nachrichten haben; zum anschauen: (%um/%mc Nachrichten)", + "roles-required": "Du musst mindestens eine dieser Rollen zum beitreten: %r", + "giveaway-ended-successfully": "Gewinnspiel erfolgreich beendet.", + "no-giveaways-found": "Kein Gewinnspiel gefunden", + "gmessages-description": "Überprüfe deine benötigten Nachrichten für ein Gewinnspiel", + "jump-to-message-hover": "Zur Nachricht springen", + "messages": "Nachrichten", + "giveaway-messages": "Gewinnspiel-Nachrichten", + "duration-parsing-failed": "Die Länge des Gewinnspieles konnte nicht interpretiert werden.", + "channel-type-not-supported": "Kanal-Typ nicht unterstützt", + "parameter-parsing-failed": "Ein(ige) Parameter deiner Angaben konnten nicht übernommen werden", + "started-successfully": "Gewinnspiel erfolgreich in %c gestartet.", + "reroll-done": "Erledigt :+1:", + "select-menu-description": "Wird in #%c am %d enden", + "no-giveaways-for-reroll": "Huch, aktuell laufen keine Gewinnspiele. Suchst du eventuell /reroll?", + "select-giveaway-to-end": "Bitte wähle das Gewinnspiel welches du beenden willst.", + "please-select": "Bitte wähle aus", + "gmanage-description": "Verwalte Gewinnspiele", + "gmanage-start-description": "Starte ein neues Gewinnspiel", + "gmanage-channel-description": "Kanal zum Starten des Gewinnspiels", + "gmanage-price-description": "Preis der gewonnen werden kann", + "gmanage-duration-description": "Länge des Gewinnspiels? (z.b: \"2h 40m\" oder \"7d 2h 3m\")", + "gmanage-winnercount-description": "Anzahl der Gewinner, die ausgewählt werden sollen", + "gmanage-requiredmessages-description": "Anzahl neuer (!) Nachrichten, die ein Benutzer vor dem Betreten haben muss", + "gmanage-requiredroles-description": "Rolle, die der Benutzer haben muss, um an der Verlosung teilzunehmen", + "gmanage-sponsor-description": "Setzt einen anderen Gewinnspiel-Starter, hilfreich bei Sponsoren", + "gmanage-sponsorlink-description": "Link zu einem Sponsor, falls zutreffend", + "gend-description": "Beendet ein Gewinnspiel", + "gereroll-description": "Rollt ein beendetes Giveaway erneut aus", + "gereroll-msgid-description": "Nachrichten-ID des Gewinnspiels", + "gereroll-winnercount-description": "Wie viele neue Gewinner sollen ausgewählt werden?", + "migration-happening": "Datenbank-Schema nicht aktuell. Datenbank wird migriert. Starte deinen Bot nicht neu, um Datenverlust zu vermeiden.", + "migration-done": "Datenbank wurde erfolgreich migriert." + }, + "levels": { + "leaderboard-channel-not-found": "Ranglisten-Kanal wurde nicht gefunden, oder hat den falschen Typ", + "leaderboard-notation": "**%p. %u**: Level %l - %xp XP", + "leaderboard": "Rangliste", + "no-user-on-leaderboard": "Es kann keine Rangliste generiert werden, da niemand Erfahrungspunkte hat. Das ist zwar komisch, aber ist halt so ¯\\_(ツ)_/¯", + "and-x-other-users": "und %uc andere Nutzer", + "level": "Level %l", + "users": "Nutzer", + "leaderboard-command-description": "Zeigt die Rangliste des Servers", + "leaderboard-sortby-description": "Ranglistensortierung (Standardwert: %d)", + "no-bio": "Keine Bio gesetzt.", + "no-bio-author": "Keine Bio gesetzt. [Setze Bio](https://sc-network.net/auth?action=set-bio)", + "profile-command-description": "Zeigt das Profil von dir oder einem anderen Nutzer", + "profile-user-description": "Nutzer von dem das Profil angezeigt werden soll (Standardwert: Du)", + "please-send-a-message": "Bitte sende einige Nachrichten, bevor ich Daten über dich anzeigen kann", + "no-role": "Keine Rolle", + "are-you-sure-you-want-to-delete-user-xp": "Okay, willst du dich wirklich mit %u anlegen? Wenn du ihn/sie so sehr hasst kannst du gerne `/manage-levels reset-xp confirm:True user:%ut` ausführen, um diese endgültige nicht umkehrbare Aktion auszuführen.", + "are-you-sure-you-want-to-delete-server-xp": "Willst du wirklich alle Erfahrungspunkte und Levels dieses Servers zurücksetzen? Diese Aktion ist nicht umkehrbar und jeder auf diesem Server wird dich hassen. Hast du dich entschieden, ob es das wert ist? Führe `/manage-levels reset-xp confirm:True` aus", + "user-not-found": "Nutzer nicht gefunden", + "user-deleted-users-xp": "%t hat die Erfahrungspunkte vom Nutzer mit der ID %u zurückgesetzt", + "removed-xp-successfully": "`Erfahrungspunkte und Level von %u erfolgreich zurückgesetzt.`", + "deleted-server-xp": "%u hat die Erfahrungspunkte von allen Nutzern zurückgesetzt", + "successfully-deleted-all-xp-of-users": "Erfolgreich alle Erfahrungspunkte aller Nutzer zurückgesetzt", + "cheat-no-profile": "Dieser Nutzer hat (noch) kein Profil, bitte zwinge ihn eine Nachricht zuschreiben bevor du deine Community betrügst, indem du Levels manipulierst.", + "abuse-detected": "%u versuchte seine Rechte durch Bearbeitung seiner eigenen Erfahrungspunkte zu seinem eigenen Vorteil zu missbrauchen. Dies ist offensichtlicher Missbrauch, ich erwarte, dass Disziplinarmaßnahmen gegenüber dieses Nutzers ergriffen werden.", + "cant-change-your-level-1": "Warte... du meinst das nicht ernst? Doch, oder? Du meinst das ernst... Ich bin sehr enttäuscht von dir, %un... Ich dachte du seist ein fairer Admin, aber wie ich es heute sehen kann bist du es nicht. Du wolltest diesen Befehl zu deinem eigenen Vorteil nutzen und alle Nutzer betrügen. Ich bin wirklich sehr sehr enttäuscht von dir, Ich habe mehr von dir erwartet. Ich werde diesen Zwischenfall an deinen Vorgesetzten melden müssen und - wie ich bereits sagte - Ich bin sehr von dir enttäuscht und ehrlich gesagt - wenn ich die Rechte dazu hätte - würde ich dich von diesem Server bannen, da dieser Zwischenfall beweist, dass du deine Rechte zu deinem eigenen Vorteil nutzt.", + "cant-change-your-level-2": "Und du versuchst es wieder... Das ist sehr sehr traurig, Ich werde dich nochmal melden und erneut betonen, dass das offensichtlich Rechteausnutzung ist. Hab noch einen schönen Tag.", + "manipulated": "%u manipulierte die Erfahrungspunkte von %u auf %v", + "successfully-changed": "Erfolgreich die Erfahrungspunkte dieses Nutzers bearbeitet. Bedenke, da jede Änderung die du am Levelsystem machst das Erlebnis anderer Nutzer auf diesem Server zerstört, da das Levelsystem nicht mehr fair ist.", + "edit-xp-command-description": "Verwalte die Level deines Servers", + "reset-xp-description": "Setze die Erfahrungspunkte eines Nutzers oder des ganzen Servers zurück", + "reset-xp-user-description": "Nutzer zum Erfahrungspunkte zurücksetzen (Standardwert: Ganzer Server)", + "reset-xp-confirm-description": "Willst du wirklich die Daten löschen?", + "edit-xp-user-description": "Nutzer zum Bearbeiten (kannst *nicht* du selbst sein!)", + "edit-xp-value-description": "Neue Erfahrungspunktemenge des Nutzers", + "edit-xp-description": "Betrüge deine Community und bearbeite die Erfahrungspunkte eines Nutzers", + "random-messages-enabled-but-non-configured": "Zufällige Nachrichten sind aktiviert, allerdings wurden keine zufälligen Nachrichten festgelegt. Ignoriere Anweisung.", + "granted-rewards-audit-log": "Rollen aktualisiert um sicherzustellen, dass der/die Nutzer:in die benötigten Rollen hat" + }, + "partner-list": { + "could-not-give-role": "%u konnte keine Rolle gegeben werden", + "could-not-remove-role": "%u konnte keine Rolle entfernt werden", + "partner-not-found": "Der Partner konnte nicht gefunden werden. Bitte überprüfe ob du die richtige Partner-ID verwendest. Die Partner-ID ist nicht mit der Server-ID des Partners identisch. Die Partner-ID findest du [hier](https://gblobscdn.gitbook.com/assets%2F-MNyHzQ4T8hs4m6x1952%2F-MWDvDO9-_JwAGqtD6at%2F-MWDxIcOHB9VcWhjsWt7%2Fscreen_20210320-102628.png?alt=media&token=2f9ac1f7-1a14-445c-b34e-83057789578e) im Partner-Embed.", + "successful-edit": "Partner-Liste erfolgreich bearbeitet.", + "channel-not-found": "Kanal mit der ID %c konnte nicht gefunden werden, oder hat den falschen Typ (es werden nur Textkanäle unterstützt)", + "no-partners": "Es gibt aktuell keine Partner. Das ist komisch, aber es ist halt so ¯\\_(ツ)_/¯\n\nUm einen Partner hinzuzufügen, nutze den Slash-Befehl `/partner add`.", + "information": "Information", + "command-description": "Verwaltet die Partner-Liste des Servers", + "padd-description": "Fügt einen neuen Partner hinzu", + "padd-name-description": "Name des Partners", + "padd-category-description": "Bitte wähle eine der Kategorien, die du in deiner Konfiguration gesetzt hast", + "padd-owner-description": "Inhaber des Partnerservers", + "padd-inviteurl-description": "Einladung zum Partnerserver", + "pedit-description": "Bearbeitet einen existierenden Partner", + "pedit-id-description": "ID des Partners", + "pedit-name-description": "Neuer Name des Partners", + "pedit-inviteurl-description": "Neue Einladung zum Partnerserver", + "pedit-category-description": "Neue Kategorie des Partnerservers", + "pdelete-description": "Entfernt einen existierenden Partner", + "pdelete-id-description": "ID des Partners", + "pedit-owner-description": "Neuer Besitzer des Partner-Servers", + "pedit-staff-description": "Neues zugewiese Teammitglied für diesen Partner-Server" + }, + "ping-on-vc-join": { + "channel-not-found": "Kanal für Benachrichtigungen %c nicht gefunden", + "could-not-send-pn": "Es konnte keine PN an %m gesendet werden" + }, + "suggestions": { + "approved": "Angenommen", + "denied": "Abgelehnt", + "admin-answer": "%status von %u mit folgendem Grund: \"%r\"", + "suggestion-not-found": "Vorschlag nicht gefunden", + "updated-suggestion": "Vorschlag erfolgreich aktualisiert", + "manage-suggestion-command-description": "Verwalte Vorschläge als Admin", + "manage-suggestion-accept-description": "Akzeptiert einen Vorschlag", + "manage-suggestion-deny-description": "Lehnt einen Vorschlag ab", + "manage-suggestion-id-description": "ID des Vorschlags", + "manage-suggestion-comment-description": "Erkläre, warum du diese Entscheidung getroffen hast" + }, + "auto-delete": { + "could-not-fetch-channel": "Der Kanal mit der ID %c konnte nicht gefunden werden", + "could-not-fetch-messages": "Die Nachrichten im Kanal mit der ID %c konnten nicht gefunden werden" + }, + "auto-thread": { + "thread-create-reason": "Dieser Thread wurde aufgrund der Einstellungen von auto-thread erstellt" + }, + "auto-messager": { + "channel-not-found": "Der Kanal mit der ID %id konnte nicht gefunden werden" + }, + "polls": { + "what-have-i-votet": "Für was habe ich abgestimmt?", + "vote": "Abstimmen!", + "vote-this": "Wähle diese Option um hierfür abzustimmen", + "voted-successfully": "Erfolgreich abgestimmt. Danke für deine Teilnahme.", + "not-voted-yet": "Du hast noch nicht abgestimmt, also kann ich dir nicht zeigen für was du abgestimmt hast?", + "you-voted": "Du hast für **%o** abgestimmt.", + "change-opinion": "Du kannst deine Meinung jeder Zeit ändern, indem du einfach etwas anderes über dem Knopf den du gerade angeklickt hast auswählst.", + "command-poll-description": "Erstelle und beende Umfragen", + "command-poll-create-description": "Erstelle eine neue Umfrage", + "command-poll-end-description": "Beende eine existierende Umfrage", + "command-poll-end-msgid-description": "ID der Umfrage", + "command-poll-create-description-description": "Thema / Beschreibung dieser Umfrage", + "command-poll-create-channel-description": "Kanal, in welchem diese Umfrage erstellt werden soll", + "command-poll-create-option-description": "Option Nummer %o", + "command-poll-create-endAt-description": "Dauer der Umfrage (wenn nicht gesetzt ist, wird die Umfrage nicht automatisch beendet)", + "created-poll": "Umfrage erfolgreich in %c erstellt.", + "not-found": "Umfrage konnte nicht gefunden werden", + "ended-poll": "Umfrage erfolgreich beendet", + "not-text-channel": "Du musst einen Textkanal auswählen, der kein Ankündigungskanal ist." + }, + "channel-stats": { + "audit-log-reason-interval": "Dieser Kanal wurde aufgrund des in channel-stats eingestellten Intervalls aktualisiert", + "audit-log-reason-startup": "Dieser Kanal wurde von channel-stats aktualisiert, da der Bot neu gestartet wurde", + "not-voice-channel-info": "Der Kanal ist kein Sprachkanal" + }, + "activities": { + "hook-installed": "Hook um spezielle Aktivitätseinladungen zu erstellen wurde installiert", + "command-description": "Voice-Aktivität auf Discord erstellt", + "type-description": "Typ der Voice-Aktivität" + }, + "info-commands": { + "info-command-description": "Finde Informationen über Teile dieses Servers heraus", + "command-userinfo-description": "Finde mehr Informationen über einen Nutzer auf diesem Server heraus", + "argument-userinfo-user-description": "Benutzer, über den Du Informationen sehen möchten (Standard: Du)", + "command-roleinfo-description": "Weitere Informationen zu einer Rolle auf diesem Server finden", + "argument-roleinfo-role-description": "Rolle, zu der Du Informationen sehen möchten", + "command-channelinfo-description": "Weitere Informationen zu einem Kanal auf diesem Server finden", + "argument-channelinfo-channel-description": "Kanal, über den Du Informationen sehen möchten", + "command-serverinfo-description": "Weitere Informationen zu diesem Server finden", + "information-about-role": "Informationen zur Rolle %r", + "hoisted": "Rechts gelistet", + "mentionable": "Erwähnbar", + "managed": "Verwaltet", + "information-about-channel": "Informationen über den Kanal %c", + "information-about-user": "Information über den Nutzer %u", + "information-about-server": "Informationen über %s", + "boostLevel": "Level", + "boostCount": "Boosts", + "userCount": "Nutzer", + "memberCount": "Mitglieder", + "onlineCount": "Online", + "textChannel": "Text", + "voiceChannel": "Sprach", + "categoryChannel": "Kategorien", + "otherChannel": "Anderes", + "total-invites": "Gesamt", + "active-invites": "Aktiv", + "left-invites": "Übrig" + }, + "channelType": { + "GUILD_TEXT": "Text-Kanal", + "GUILD_VOICE": "Sprach-Kanal", + "GUILD_CATEGORY": "Kategorie", + "GUILD_NEWS": "Ankündigungs-Kanal", + "GUILD_STORE": "Store-Kanal", + "GUILD_NEWS_THREAD": "News-Kanal-Thread", + "GUILD_PUBLIC_THREAD": "Öffentlicher Thread", + "GUILD_PRIVATE_THREAD": "Privater Thread", + "GUILD_STAGE_VOICE": "Bühnen-Kanal", + "DM": "Direkt-Nachricht", + "GROUP_DM": "Gruppen-Direkt-Nachricht", + "UNKNOWN": "Unbekannt" + }, + "stagePrivacy": { + "PUBLIC": "Öffentlich zugänglich", + "GUILD_ONLY": "Nur Servermitglieder können beitreten" + }, + "guildVerification": { + "NONE": "Keins", + "LOW": "Niedrig", + "MEDIUM": "Mittel", + "HIGH": "Hoch", + "VERY_HIGH": "Sehr Hoch" + }, + "boostTier": { + "NONE": "Keins", + "TIER_1": "Level 1", + "TIER_2": "Level 2", + "TIER_3": "Level 3" + }, + "temp-channels": { + "removed-audit-log-reason": "Temp-Channel entfernt, da niemand darin war", + "permission-update-audit-log-reason": "Berechtigungen aktualisiert, um sicherzustellen, dass nur Personen im Sprachkanal den No-Mic-Kanal sehen können", + "created-audit-log-reason": "Temp-Channel für %u erstellt", + "move-audit-log-reason": "Nutzer in seinen/ihren Sprachkanal verschoben", + "no-mic-channel-topic": "Willkommen in %us No-Mic-Kanal. Du wirst diesen Kanal so lang sehen, wie du mit dem Temp-Channel verbunden bist.", + "disconnect-audit-log-reason": "Der alte Kanal des Nutzers konnte nicht gefunden werden - Verbindung wird getrennt - Hoffentlich joint er/sie wieder", + "command-description": "Verwalte deinen Temp-channel", + "mode-subcommand-description": "Ändere den Modus deines Kanals", + "public-option-description": "local public-option-description", + "add-subcommand-description": "Füge Nutzer hinzu, die deinem Channel beitreten können sollen, während er privat ist", + "remove-subcommand-description": "Entferne Nutzer von deinem Kanal", + "add-user-option-description": "Der Nutzer der hinzugefügt werden soll", + "remove-user-option-description": "Der Nutzer der entfernt werden soll", + "list-subcommand-description": "Liste der Nutzer mit Zugang zu deinem Kanal", + "edit-subcommand-description": "Bearbeite deinen Kanal", + "user-limit-option-description": "Ändere die maximale Nutzeranzahl deines Kanals", + "bitrate-option-description": "Ändere die Bitrate deines Kanals (min. 8000)", + "name-option-description": "Ändere den Namen deines Kanals", + "nsfw-option-description": "Ändere ob dein Kanal altersbeschränkt (NSFW) ist oder nicht", + "no-added-user": "Es gibt keine Nutzer die hier angezeigt werden können", + "nothing-changed": "Dein Kanal hatte bereits diese Einstellungen", + "no-disconnect": "Trennen der Verbindung des Nutzers nicht möglich. Dies kann an fehlenden Rechten liegen, oder daran, dass der Nutzer nicht in deinem Kanal ist", + "edit-error": "Beim Bearbeiten deines Kanals ist ein Fehler aufgetreten. Eine oder mehrere deiner Einstellungen konnten nicht angewendet werden. Dies kann an fehlenden Rechten oder einem ungültigen Eingabewert liegen.", + "add-user": "Nutzer hinzufügen", + "remove-user": "Nutzer entfernen", + "list-users": "Nutzer anzeigen", + "private-channel": "Privat", + "public-channel": "Öffentlich", + "edit-channel": "Kanal Bearbeiten", + "add-modal-title": "Füge einen Nutzer zu deinem Kanal hinzu", + "add-modal-prompt": "Der Nutzer den du hinzufügen willst", + "remove-modal-title": "Entferne einen Nutzer von deinem Temp-channel", + "remove-modal-prompt": "Der Nutzer den du entfernen willst", + "edit-modal-title": "Kanal bearbeiten", + "edit-modal-nsfw-prompt": "Temp-channel als altersbeschränkt markieren?", + "edit-modal-nsfw-placeholder": "\"true\" (ja) oder \"false\" (nein)", + "edit-modal-bitrate-prompt": "Bitrate deines Temp-channels", + "edit-modal-bitrate-placeholder": "Eine Zahl; Mindestens 8000", + "edit-modal-limit-prompt": "Limit an Nutzern in deinem Temp-channel", + "edit-modal-limit-placeholder": "Zahl zwischen 0 und 99; 0 = beliebig viele", + "edit-modal-name-prompt": "Wie soll dein Kanal heißen?", + "edit-modal-name-placeholder": "Ein Super Kanalname", + "user-not-found": "Nutzer nicht gefunden" + }, + "guess-the-number": { + "command-description": "Verwalte den Status deines Errate-die-Zahl-Spiels", + "status-command-description": "Zeigt den aktuellen Status eines Errate-die-Zahl-Spiels", + "create-command-description": "Erstelle ein neues Errate-die-Zahl-Spiel in diesem Kanal", + "create-min-description": "Niedrigster Wert, den Nutzer raten können", + "create-max-description": "Höchster Wert, den Nutzer raten können", + "create-number-description": "Zahl, welche Nutzer erraten sollen um zu gewinnen", + "end-command-description": "Beendet das aktuelle Spiel", + "session-already-running": "In diesem Kanal läuft bereits eine Runde. Bitte beende es zuerst mit /guess-the-number end", + "session-not-running": "Aktuell läuft keine Runde.", + "session-ended-successfully": "Runde erfolgreich beendet. Kanal erfolgreich gesperrt.", + "current-session": "Aktuelle Runde", + "number": "Zahl", + "min-val": "Niedrigster Wert", + "max-val": "Höchster Wert", + "owner": "Eigentümer", + "guess-count": "Anzahl der Versuche", + "min-max-discrepancy": "`min` kann nicht größer oder gleich groß wie `max` sein", + "emoji-guide-button": "Was bedeutet die Reaktion unter meinem Versuch?", + "emoji-guide-link": "https://docs.sc-network.net/de/custom-bot-v2/module/guess-the-number#was-bedeuten-die-reaktionen-unter-meinen-nachrichten", + "created-successfully": "Runde erfolgreich erstellt. Nutzer können nun anfangen in diesem Kanal zu raten. Die gewinnende Zahl ist **%n**. Du kannst den Status immer mit `/guess-the-number-status` abfragen. Beachte, dass du als Admin nicht mitraten kannst.", + "game-ended": "Spiel beendet", + "game-started": "Spiel gestartet" + }, + "twitch-notifications": { + "channel-not-found": "Der Kanal mit der ID %c konnte nicht gefunden werden", + "user-not-on-twitch": "Nutzer %u konnte auf Twitch nicht gefunden werden", + "user-not-found": "Der Benutzer mit der ID %u konnte nicht gefunden werden" + }, + "fun": { + "slap-command-description": "Schlägt einen Nutzer ins Gesicht", + "user-argument-description": "Nutzer, auf den diese Aktion auszuführen ist", + "no-no-not-slapping-yourself": "Du kannst dich nicht selbst schlagen lol (also theoretisch schon, aber unsere GIFs können das nicht, also Akzeptiere es einfach ¯\\_(ツ)_/¯)", + "pat-command-description": "Tätschle jemanden nett", + "no-no-not-patting-yourself": "Guter Versuch, aber das machen wir hier nicht", + "no-no-not-kissing-yourself": "Uah, das ist eklig, du solltest versuchen jemanden anderen dafür zu bezahlen (also solltest du nicht, aber es ist besser als dich selbst zu küssen)", + "kiss-command-description": "Küsse jemanden", + "hug-command-description": "Umarme jemanden <3", + "no-no-not-hugging-yourself": "Du bist ziemlich einsam, oder? Versuche einen Baum zu umarmen, das sollte funktionieren. Außer du lebst in einer Wüste. Dann umarme einen Kaktus. Das tut ein bisschen mehr weh, aber vertrau mir.", + "random-command-description": "Hilft dir zufällige Sachen auszusuchen", + "random-number-command-description": "Sagt dir eine zufällige Zahl", + "min-argument-description": "Niedrigste mögliche Zahl (Standard: 1)", + "max-argument-description": "Höchste mögliche Zahl (Standard: 42)", + "random-ikeaname-command-description": "Generiert einen zufälligen IKEA-Namen", + "syllable-count-argument-description": "Anzahl der Silben des generierten Namens (Standard: Zufällig)", + "random-dice-command-description": "Würfle", + "random-coinflip-command-description": "Wirf eine Münze!", + "random-8ball-command-description": "Generiert eine Antwort auf eine Ja/Nein-Frage", + "dice-site-1": "Kopf", + "dice-site-2": "Zahl" + }, + "moderation": { + "moderate-command-description": "Moderiert Nutzer auf deinem Server", + "moderate-notes-command-description": "Setze oder sehe die Kommentare eines Moderators über einen Nutzer", + "moderate-notes-command-view": "Zeige die Notizen zu einem Nutzer an", + "moderate-notes-command-create": "Erstelle eine neue Notiz zu einem Nutzer", + "moderate-notes-command-edit": "Bearbeite eine deiner existierenden Notizen zu einem Nutzer", + "moderate-notes-command-delete": "Lösche eine deiner existierenden Notizen zu einem Nutzer", + "moderate-ban-command-description": "Bannt einen Nutzer von deinem Server", + "moderate-reason-description": "Grund für die Aktion", + "moderate-duration-description": "Dauer der Aktion (Standard: Permanent)", + "mute-max-duration": "Discord begrenzt die Höchstdauer eines Timeouts auf 28 Tage. Bitte gib einen Wert an, der niedriger oder gleich ist", + "moderate-quarantine-command-description": "Versetzt einen Nurzer auf deinem Server in Quarantäne", + "moderate-unquarantine-command-description": "Entfernt einen Nutzer aus der Quarantäne", + "moderate-unban-command-description": "Hebt einen existierenden Bann auf", + "moderate-clear-command-description": "Löscht Nachrichten im aktuellen Kanal", + "moderate-clear-amount-description": "Wie viele Nachrichten sollen gelöscht werden?", + "moderate-kick-command-description": "Kickt einen Nutzer von deinem Server", + "moderate-unwarn-command-description": "Hebt eine Verwarnung auf", + "moderate-mute-command-description": "Schaltet einen Nutzer auf deinem Server stumm", + "moderate-unmute-command-description": "Hebt die Stummschaltung eines Nutzers wieder auf", + "moderate-warn-command-description": "Verwarnt einen Nutzer", + "moderate-lock-command-description": "Sperrt den aktuellen Kanal", + "moderate-unlock-command-description": "Entsperrt den aktuellen Kanal", + "moderate-user-description": "Nutzer, auf den diese Aktion auszuführen ist", + "moderate-userid-description": "ID eines Nutzers", + "moderate-days-description": "Anzahl der zu löschenden Nachrichten", + "invalid-days": "Tage können nur zwischen 0 und 7 sein", + "moderate-notes-description": "Deine Notiz setzen (leer lassen, um Notizen zu sehen)", + "moderate-note-id-description": "ID einer deiner Notizen, die du bearbeiten willst (leer lassen, um neu zu erstellen)", + "moderate-warnid-description": "ID einer Verwarnung (führe /moderate actions aus um diese herauszufinden)", + "moderate-actions-command-description": "Zeigt alle Aktionen gegen einen Nutzer", + "report-command-description": "Meldet einen Nutzer und sendet einen Ausschnitt des Chats an das Serverteam", + "report-reason-description": "Bitte beschreibe was der Nutzer falsch gemacht hat", + "report-user-description": "Nutzer, den du melden willst", + "no-reason": "Nicht gesetzt", + "muterole-not-found": "Die Stummschaltungsrolle konnte nicht gefunden werden. Aktion kann nicht ausgeführt werden", + "quarantinerole-not-found": "Die Quarantänerolle konnte nicht gefunden werden. Aktion kann nicht ausgeführt werden", + "mute-audit-log-reason": "Wurde von %u wegen \"%r\" stumm geschaltet", + "unmute-audit-log-reason": "Die Stummschaltung wurde von %u wegen \"%r\" aufgehoben", + "quarantine-audit-log-reason": "Wurde von %u wegen \"%r\" in Quarantäne versetzt", + "kicked-audit-log-reason": "Wurde von %u wegen \"%r\" gekickt", + "banned-audit-log-reason": "Wurde von %u wegen \"%r\" gebannt", + "unbanned-audit-log-reason": "Wurde von %u wegen \"%r\" entbannt", + "unquarantine-audit-log-reason": "Wurde von %u wegen \"%r\" aus der Quarantäne entfernt", + "action-expired": "Aktion ausgelaufen", + "auto-mod": "Auto-Mod", + "batch-role-remove-failed": "Es konnten nicht alle Rollen von %i entfernt werden (versuche eine nach der anderen zu entfernen): %e", + "batch-role-add-failed": "Es konnten %i nicht alle Rollen gegeben werden (versuche eine nach der anderen zu geben): %e", + "could-not-remove-role": "Rolle %r konnte %i nicht entfernt werden: %e", + "could-not-add-role": "Rolle %r konnte %i nicht gegeben werden: %e", + "reason": "Grund", + "join-gate": "Join-Gate", + "expires-at": "Aktion läuft aus am", + "action": "Aktion", + "case": "Fall", + "victim": "Betroffener Nutzer", + "missing-logchannel": "Log-Kanal konnte nicht gefunden werden", + "reached-warns": "%w Verwarnungen erreicht", + "restored-punishment-audit-log-reason": "Strafe wiederhergestellt", + "anti-join-raid": "ANTI-JOIN-RAID", + "raid-detected": "Raid erkannt", + "joingate-for-everyone": "Join-Gate-Modus: Alle Nutzer abfangen", + "account-age-to-low": "Accounterstellungsalter von %a Tagen ist zu niedrig (es werden mehr als %c Tage benötigt)", + "no-profile-picture": "Account hat kein Profilbild", + "join-gate-fail": "Account hat das Join-Gate nicht bestanden (%r)", + "blacklisted-word": "Hat ein verbotenes Wort in %c geschrieben", + "invite-sent": "Hat einen Einladungslink in %c geschrieben", + "anti-spam": "Anti-Spam", + "reached-messages-in-timeframe": "Hat %m (normale) Nachrichten in unter %t Sekunden erreicht", + "reached-duplicated-content-messages": "Hat %m mit dem selben Inhalt in unter %t Sekunden erreicht", + "reached-ping-messages": "Hat %m mit (Nutzer-) Erwähnungen in unter %t Sekunden erreicht", + "reached-massping-messages": "Hat %m mit Massenerwähnungen in unter %t Sekunden erreicht", + "action-done": "Aktion erfolgreich ausgeführt. AktionsID: #%i", + "expiring-action-done": "Fertig. Aktion wird am %d automatisch auslaufen. AktionsID: #%i", + "cleared-channel": "Kanal erfolgreich geleert.\nHinweis: Nachrichten, die älter als 14 Tage sind werden eventuell mit dieser Methode nicht gelöscht.", + "clear-failed": "Ein Fehler ist aufgetreten. Du kannst nur 100 Nachrichten auf ein Mal löschen.", + "no-quarantine-action-found": "Entschuldigung, aber ich kann keine Aufzeichnungen über Quarantäneaufenthalte dieses Nutzers finden.", + "locked-channel-successfully": "Kanal erfolgreich gesperrt. Nur Moderatoren (und Admins) können hier noch Nachrichten schreiben.", + "unlocked-channel-successfully": "Kanal erfolgreich entsperrt. Berechtigungen wurden auf den Status von vor der Sperrung wiederhergestellt.", + "unlock-audit-log-reason": "Nutzer %u hat diesen Kanal durch Ausführung von /moderate unlock entsperrt", + "warning-not-found": "Verwarnung konnte nicht gefunden werden. Bitte stelle sicher, dass du eine VerwarnungsID und keine NutzerID verwendest.", + "can-not-report-mod": "Du kannst Moderatoren nicht melden.", + "action-description-format": "%reason\nvon %u am %t", + "no-actions-title": "Nicht gefunden", + "no-actions-value": "Es wurden keine Aktionen gegen %u gefunden.", + "actions-embed-title": "Mod-Aktionen gegen %u - Seite %i", + "actions-embed-description": "Du kannst jede Aktion gegen %u hier sehen.", + "report-embed-title": "Neue Meldung", + "report-embed-description": "Ein Nutzer hat einen anderen Nutzer gemeldet. Bitte bearbeite den Fall und führe, wenn nötig, Aktionen aus.", + "reported-user": "Gemeldeter Nutzer", + "report-reason": "Grund der Meldung", + "report-user": "Nutzer, welcher die Meldung eingereicht hat", + "message-log": "Letzte 100 Nachrichten", + "message-log-description": "Du kannst einen verschlüsselten Nachrichten-Log [hier](%u) sehen.", + "channel": "Kanal", + "no-report-pings": "Keine Erwähnungen konfiguriert. Überprüfe deine Konfiguration um dein Team zu benachrichtigen.", + "not-allowed-to-see-own-notes": "Sorry, aber leider darfst du die Notizen über dich nicht selber sehen.", + "note-added": "Notiz erfolgreich hinzugefügt", + "note-edited": "Notiz erfolgreich bearbeitet", + "note-deleted": "Notiz erfolgreich gelöscht", + "note-not-found-or-no-permissions": "Notiz nicht gefunden oder keine Rechte, diese Notiz zu bearbeiten.", + "notes-embed-title": "Notizen über %u", + "info-field-title": "ℹ️ Information", + "no-notes-found": "Keine Notizen über diesen Nutzer gefunden. Erstelle eine neue Notiz mit `/moderate notes` und setze dabei das `notes`-Attribut.", + "more-notes": "%x weitere Moderatoren haben Notizen über diesen Nutzer hinterlassen. Notizen sind in entgegengesetzer Chronologie sortiert, du siehst die neuesten Notizen zuerst.", + "user-notes-field-title": "%t's Notizen", + "user-not-on-server": "Ich kann gegen diesen Nutzer keine Aktionen ausführen, da er gerade nicht auf deinem Server ist.", + "verification": "VERIFIKATION", + "verification-failed": "Verifikation fehlgeschlagen", + "verification-started": "Verifikation wurde begonnen", + "verification-completed": "Verifikation beendet", + "user": "Nutzer", + "manual-verification-needed": "Manuelle Verifikation benötigt", + "verification-deny": "Verifikation ablehnen", + "verification-approve": "Verifikation bestätigen", + "verification-skip": "Verifikation überspringen", + "captcha-verification-pending": "Captcha-Verification steht noch aus. Du kannst entweder warten, bis der Nutzer diese beendet hat, oder sie manuell überspringen.", + "verification-update-proceeded": "Verifikationsstatus erfolgreich aktualisiert", + "verify-channel-set-but-not-found-or-wrong-type": "Der eingestellte Verifikationskanal wurde nicht gefunden oder hat den falschen Typ.", + "generating-message": "Wir bereiten einiges vor; diese Nachricht wird bald bearbeitet...", + "restart-verification-button": "Verifikation neustarten", + "member-not-found": "Dieser Nutzer konnte nicht gefunden werden, vielleicht ist er schon gegangen?", + "already-verified": "Es sieht so aus als wärst du bereits verifiziert... Warum würdest du das wiederholen wollen?", + "restarted-verification": "Ich habe dir eine neue PN zu deinem Verifikationsvorgang gesendet. Bitte lese sie gründlich und folge den darin beschriebenen Anweisungen. Bitte beachte, dass dies nicht die mauelle Verifikation neugestartet hat (wenn sie aktiviert ist), deshalb bringt es nichts diesen Knopf zu spammen.", + "dms-still-disabled": "Es scheint als wären deine PNs immer noch deaktiviert. Bitte aktiviere deine PNs um den Verifikationsprozess zu starten. Dies ist nicht optional, du musst das tun, um Zugriff auf %g zu erhalten.", + "dms-not-enabled-ping": "%p, es scheint als hättest du deine DMs deaktiviert. Bitte aktiviere sie und klicke auf den Knopf unter dieser Nachricht um dich zu verifizieren. Um dies zu tun hast du zwei Minuten Zeit.", + "scam-url-sent": "Gesendete Betrugs-URL in %c" + }, + "counter": { + "created-db-entry": "Datenbankeintrag für %i initialisiert", + "not-a-number": "Das ist keine Zahl. Du kannst hier nicht chatten. Versuche einen Thread zu erstellen, wenn deine Nachricht so wichtig ist.", + "banned-because-of-improper-use": "Ich musste dir den Zugriff auf diesen Kanal verbieten, da du ihn mehrmals falsch verwendet hast.", + "restriction-audit-log": "Dieser Nutzer hat nach fünf Warnungen weiterhin den Zähl-Kanal missbraucht, also habe ich ihn ausgesperrt.", + "only-one-message-per-person": "Nutzer müssen abwechseld zählen: Du kannst nicht zwei mal hintereinander zählen.", + "not-the-next-number": "Das ist nicht die nächste Zahl. Diese wäre **%n**, bitte stelle sicher immer eine Zahl nach der anderen zu zählen.", + "channel-topic-change-reason": "Jemand hat gezählt, also haben wir die Kanalbeschreibung wie in der Konfiguration angegeben bearbeitet" + }, + "tickets": { + "channel-not-found": "Ticket-Erstellungs-Kanal konnte nicht gefunden werden", + "existing-ticket": "Du hast bereits ein geöffnetes Ticket: %c", + "ticket-created-audit-log": "%u hat ein neues Ticket durch klicken des Knopfes erstellt", + "ticket-created": "Das Ticket wurde erfolgreich erstellt und das Team benachrichtigt. Du findest es hier: %c", + "new-ticket-embed-title": "📥 Neues Ticket #%i", + "new-ticket-embed-user": "👤 Nutzer", + "new-ticket-embed-info": "ℹ️ Information", + "close-info": "Dein Problem wurde behoben? Klicke den Knopf unten. Du kannst diese Nachricht immer in den angepinnten Nachrichten finden.", + "no-admin-pings": "Keine Erwähnungen konfiguriert. Überprüfe deine Konfiguration um dein Team zu benachrichtigen.", + "ticket-closed-successfully": "Ticket erfolgreich geschlossen. Dieser Kanal wird in wenigen Sekunden gelöscht. Danke, dass du unseren Support benachrichtigt hast.", + "ticket-closed-audit-log": "%u hat das Ticket geschlossen", + "closing-ticket": "Schließe Ticket, wie von %u angefragt...", + "could-not-dm": "Es konnte keine PN an %u gesendet werden: %r", + "no-log-channel": "Log-Kanal nicht gefunden", + "ticket-log-embed-title": "📎 Ticket %i geschlossen", + "ticket-with-user": "👤 Ticket-Nutzer", + "ticket-log": "Ticket-Protokoll", + "ticket-log-value": "Transkript mit %n Nachrichten kann [hier](%u) gefunden werden.", + "closed-by": "👷 Ticket geschlossen von", + "ticket-type": "☕ Ticket-Thema" + }, + "custom-commands": { + "not-a-booster": "Kein Booster", + "no-nickname": "Kein Nickname", + "ub-parameters-missing": "Einige Parameter für den Filter \"ub\" fehlen", + "filter-not-supported": "Filter \"%t\" wird auf dieser Version nicht unterstützt. Versuche deinen Bot zu updaten.", + "action-not-supported": "Aktion \"%t\" wird auf dieser Version nicht unterstützt. Versuche deinen Bot zu updaten.", + "audit-log": "Eigener Befehl %c wurde ausgeführt (ID: %i)", + "ub-error": "SCNX <-> UnbeliveaBoat hat mit %s und dem Inhalt \"%c\" geantwortet", + "ub-parse-error": "SCNX <-> UnbeliveaBoat hat mit %s geantwortet, allerdings konnte der Inhalt nicht geparst werden.", + "error-executing-action": "Ein Fehler ist bei Ausführung von Aktion %a im eigenen Befehl %c (ID: %i) aufgetreten: %e", + "not-found": "Dieser benutzerdefinierte Befehl existiert nicht mehr. Möglicherweise wurde es gelöscht oder deaktiviert.", + "parameter-not-set": "Dieser Parameter wurde nicht angegeben", + "true": "Richtig", + "false": "Falsch" + }, + "logging": { + "hook-installed": "Installierter Haken für den Empfang von Sonderveranstaltungen" + }, + "akinator": { + "command-description": "Lass akinator einen Charakter / ein Objekt oder ein Tier erraten", + "type-description": "Wähle aus, was Akinator erraten soll (Standart: Charakter)", + "character-name": "Charakter", + "object-name": "Objekt", + "animal-name": "Tier" + }, + "afk-system": { + "command-description": "Verwalte deinen AFK-Status auf diesem Server", + "end-command-description": "Beendet eine laufende AFK-Sitzung", + "start-command-description": "Startet eine neue AFK-Sitzung", + "reason-option-description": "Erkläre, warum du AFK gehst", + "autoend-option-description": "Der Bot wird automatisch dein AFK-Status beenden, sobald du eine Nachricht schreibst (Standart: an)", + "no-running-session": "Es scheint, als wärst du aktuell keine AFK-Sitzung am Laufen.", + "already-running-session": "Du hast bereits eine AFK-Sitzung am Laufen, du kannst sie jederzeit mit `/afk-system end` beenden.", + "afk-nickname-change-audit-log": "Der Nickname des Nutzers wurde aktualisiert, weil er eine AFK-Sitzung gestartet hat", + "can-not-edit-nickname": "Der Nickname von %u konnte nicht geändert werden: %e" + }, + "invite-tracking": { + "hook-installed": "Integration initialisiert, um mehr Informationen über Einladungen zu erhalten", + "log-channel-not-found-but-set": "Der angegebene Log-Kanal %c wurde nicht gefunden.", + "new-member": "Neues Mitglied beigetreten", + "member-leave": "Ein Mitglied hat den Server verlassen", + "invite-type": "Einladungs-Typ", + "member": "Mitglied", + "invite": "Einladung", + "invite-code": "Einladungscode: [%c](%u)", + "invite-channel": "Kanal: %c", + "expires-at": "Läuft ab am: %t", + "created-at": "Erstellt: am %t", + "inviter": "Einlader: %u (%a/%i aktive Einladungen)", + "uses": "Verwendungen: %u", + "createdAt": "Erstellt am: %t", + "max-uses": "Maximal-Verwendungen: %u", + "normal-invite": "Normaler Invite", + "vanity-invite": "Vanity-Einladung", + "missing-permissions": "Ich habe nicht die Rechte, den Inviter zu ermitteln", + "unknown-invite": "Sorry, ich kann nicht herausfinden, welchen Invite dieser Nutzer verwendet hat", + "joined-for-the-x-time": "%u ist dem Server schon %x-zuvor beigetreten, letztes mal am %t.", + "revoke-invite": "Dieser Invite entfernen", + "invite-not-found": "Dieser Invite wurde nicht gefunden... Vielleicht wurde er schon gelöscht?", + "invite-revoked": "Invite wurde erfolgreich entfernt.", + "missing-revoke-permissions": "Sorry, du kannst diesen Invite nicht entfernen: Du brauchst `MANAGE_GUILD` Rechte um diese Aktion durchzuführen.", + "invite-revoke-audit-log": "Dieser Invite wurde von %u entfernt", + "invite-revoked-error": "Invite konnte nicht gelöscht werden %c: %e", + "trace-command-description": "Verfolge die Einladungen eines Nutzers nach", + "argument-user-description": "Nutzer, dessen Einladungen zu verfolgen sind", + "invited-by": "Eingeladen von", + "invited-users": "Eingeladene Nutzer", + "inviter-not-found": "Ich konnte nicht ermitteln, wer diesen Nutzer eingeladen hat.", + "no-users-invited": "Dieses Mitglied hat keine anderen Nutzer eingeladen.", + "and-x-more-users": "und %x Nutzer mehr", + "and-x-more-invites": "und %x mehr Einladungen", + "created-invites": "Erstellte Einladungen", + "not-showing-left-users": "Eingeladene Nutzer, die den Server verlassen haben, werden hier nicht angezeigt.", + "no-invites": "Dieses Mitglied hat keine Einladungen erstellt", + "revoke-user-invite": "Alle Einladungen dieses Nutzers entfernen", + "revoked-invites-successfully": "Alle Einladungen dieses Nutzers wurden entfernt" + }, + "2022-countdown": { + "channel-not-found": "Countdown-Channel wurde nicht gefunden ):", + "days-left": "Noch %x Tage bis 2022", + "hours-left": "Noch %x Stunden bis 2022", + "2022-is-here": "2022 ist hier 🎉", + "channel-edit-audit-log": "Um die Aktualität des Countdowns sicherzustellen, wurde dieser Channel geupdatet." + }, + "tic-tac-toe": { + "command-description": "Spiele tic-tac-toe gegen jemanden im Chat", + "user-description": "Nutzer, gegen den du spielen willst", + "challenge-message": "%t, %u hat dich zu einer Runde tic-tac-toe herausgefordert! Klicke auf den Knopf unter dieser Nachricht um beizutreten! Diese Einladung wird in etwa 2 Minuten ablaufen, also zögere nicht sie anzunehmen.", + "accept-invite": "Spiel beitreten", + "deny-invite": "Nein, danke", + "self-invite-not-possible": "Bist du wirklich so einsam? Selbst Simon, ein total introvertierter Mensch ohne Freunde und Entwickler dieses Bots, kann einen Nutzer zum tic-tac-toe spielen finden... Du solltest das auch schaffen, versuche zum Beispiel %r einzuladen, vielleicht will er/sie eine Runde spielen?", + "invite-expired": "Entschuldigung, %u, %i hat deine Einladung tic-tac-toe zu spielen nicht rechtzeitig angenommen ):", + "invite-denied": "Entschuldigung, %u, aber %i hat deine Einladung tic-tac-toe zu spielen abgelehnt ):", + "you-are-not-the-invited-one": "Entschuldigung, aber diese Einladung gehört dir nicht. Du kannst dein eigenes Spiel mit `/tic-tac-toe` starten.", + "playing-header": "**TIC-TAC-TOE SPIEL LÄUFT GERADE**\n\n%u (🟢) VS %i (🟡)\nAktuell am Zug: %t\n\n%t, klicke einen Knopf mit einem weißen Kreis unter dieser Nachricht um deine Markierung zu setzen", + "win-header": "**TIC-TAC-TOE-GAME BEENDET**\n\n%u (🟢) VS %i (🟡)\n\n%w hat das Spiel gewonnen - GG!\n\n*Du kannst mit `/tic-tac-toe` eine neue Runde starten*", + "draw-header": "**TIC-TAC-TOE-GAME BEENDET**\n\n%u (🟢) VS %i (🟡)\n\nUnentschieden - niemand hat diese Runde gewonnen.", + "not-your-turn": "Du bist gerade nicht dran, hol dir einen Kaffee und versuche es später nochmal" + }, + "economy-system": { + "work-earned-money": "Der Benutzer %u hat %m %c durch Arbeit bekommen", + "crime-earned-money": "Der Benutzer %u hat %m %c durch ein Verbrechen bekommen", + "crime-loose-money": "Der Benutzer %u hat %m %c durch ein Verbrechen bekommen", + "message-drop-earned-money": "Der Benutzer %u hat %m %c gewonnen, indem er eine Nachricht geschrieben hat", + "rob-earned-money": "Der Benutzer %u hat %m %c durch den Raub von %v bekommen", + "weekly-earned-money": "Der Benutzer %u hat %m %c bekommen, indem er seine wöchentliche Belohnung einlöste", + "daily-earned-money": "Der Benutzer %u hat %m %c bekommen, indem er seine wöchentliche Belohnung einlöste", + "admin-self-abuse": "Der Admin %a wollte seine Berechtigungen missbrauchen, indem er sich selbst noch mehr Geld gab! Das kann und darf nicht ignoriert werden!", + "admin-self-abuse-answer": "Was für ein schlechter Administrator du bist, %u. Ich bin enttäuscht von dir! Ich muss das melden. Wenn ich wollte, könnte ich dich bannen!", + "added-money": "%i %c wurde dem Konto von %u hinzugefügt", + "removed-money": "%i %c wurde aus dem Konto von %u entfernt", + "set-money": "Der Kontostand von %u wurde auf %i gesetzt.", + "added-money-log": "Der Benutzer %u hat %i %c zum Konto von %v hinzugefügt", + "removed-money-log": "Der Benutzer %u hat %i %c aus dem Konto von %v entfernt", + "set-money-log": "Der Benutzer %u hat den Kontostand von %v auf %i %c gesetzt", + "command-description-main": "Verwende das Economy-System", + "command-description-work": "Verdiene etwas Geld, indem du arbeiten gehst", + "command-description-crime": "Verdiene etwas Geld, indem du ein Verbrechen begehst", + "command-description-rob": "Einem anderen Mitglied Geld klauen", + "option-description-rob-user": "Mitglied zum Ausrauben", + "command-description-daily": "Löse deinen täglichen Bonus ein", + "command-description-weekly": "Löse deinen wöchentlichen Bonus ein", + "command-description-balance": "Zeige dir den Kontostand von einem Mitglied", + "option-description-user": "Mitglied zum Ausführen einer Aktion", + "command-description-add": "Füge einem Nutzer Geld hinzu", + "command-description-remove": "Entferne Geld von einem Nutzer", + "option-description-amount": "Zu manipulierender Betrag", + "command-description-set": "Kontostand eines Mitglieds einstellen", + "option-description-balance": "Kontostand, welches das Mitglied bekommt", + "message-drop": "Nachrichten-Drop: Du hast %m %c einfach durch Chatten verdient!", + "created-item": "Der Nutzer %u hat einen neuen Shopartikel erstellt: Name: %n, ID: %i", + "item-duplicate": "Das Item existiert schon", + "role-to-high" : "Die angegebene Rolle ist höher, als die höchste Rolle des Bots. Deshalb kann der Bot die Rolle nicht vergeben. Das Item wurde **nicht** erstellt.", + "delete-item": "Der Nutzer %u hat einen Shopartikel entfernt: %i", + "user-purchase": "Der Nutzer %u hat den Shopartikel %i für %p gekauft.", + "shop-command-description": "Benutze das Shop-System", + "shop-command-description-add": "Erstelle ein neuen Artikel im Shop (nur für Administratoren)", + "shop-option-description-itemName": "Name des Artikels", + "shop-option-description-itemID": "ID des Artikels", + "shop-option-description-price": "Preis des Artikels", + "shop-option-description-role": "Rolle, die dem Nutzer, die den Artikel kaufen, zugewiesen wird", + "shop-command-description-buy": "Kaufe einen Artikel", + "shop-command-description-list": "Alle Artikel im Shop auflisten", + "shop-command-description-delete": "Entferne einen Artikel aus dem Shop", + "channel-not-found": "Kann den Ranglisten-Kanal mit der ID %c nicht finden", + "command-description-deposit": "Zahle xyz auf dein Bankkonto ein", + "option-description-amount-deposit": "Einzuzahlender Betrag", + "command-description-withdraw": "Hebe xyz von deinem Bankkonto ab", + "option-description-amount-withdraw": "Auszuzahlender Betrag", + "command-group-description-msg-drop-msg": "Aktiviere/Deaktiviere die Nachrichten-Drop-Nachricht", + "command-description-msg-drop-msg-enable": "Aktiviere die Nachrichten-Drop-Nachricht", + "command-description-msg-drop-msg-disable": "Deaktiviere die Nachrichten-Drop-Nachricht", + "command-description-destroy": "Die gesamte Wirtschaft zerstören (löscht alle Datenbankeinträge)", + "option-description-confirm": "Bitte bestätige, dass du wirklich die gesamte Wirtschaft zerstören willst", + "destroy-cancel-reply": "Glück gehabt. Du hast mich im letzten Moment gestoppt, bevor ich die Wirtschaft zerstört habe", + "destroy-reply": "Ok... Ich werde die gesamte Wirtschaft zerstören", + "destroy": "%u hat die Wirtschaft zerstört", + "migration-happening": "Datenbank-Schema nicht aktuell. Datenbank wird migriert. Starte deinen Bot nicht neu, um Datenverlust zu vermeiden.", + "migration-done": "Datenbank wurde erfolgreich migriert.", + "nothing-selected": "Nichts ausgewählt", + "select-menu-price": "Preis: %p" + }, + "team-list": { + "channel-not-found": "Kanal mit der ID %c konnte nicht gefunden werden, oder hat den falschen Typ (es werden nur Textkanäle unterstützt)", + "role-not-found": "Rolle mit ID %r konnte nicht gefunden werden", + "no-users-with-role": "Kein Mitglied des Servers hat die %r Rolle.", + "no-roles-selected": "Es wurden noch keine Rollen gelistet ):" + }, + "massrole": { + "command-description": "Verwalte die Rollen deiner Mitglieder", + "role-option-remove-description": "Die Rolle, die entfernt werden soll von allen Mitgliedern", + "remove-subcommand-description": "Entferne eine Rolle von allen Mitgliedern", + "remove-all-subcommand-description": "Entferne alle Rollen von allen Mitgliedern", + "role-option-add-description": "Die Rolle, die an alle Mitglieder vergeben wird", + "target-option-description": "Lege fest, ob Bots miteinbezogen werden sollen oder nicht", + "all-users": "Alle Mitglieder", + "bots": "Bots", + "humans": "Mitglieder (keine Bots)", + "add-subcommand-description": "Füge eine Rolle zu allen Mitgliedern hinzu", + "not-admin": "⚠️ Um diesen Befehl zu verwenden musst du zur adminRoles option im SCNX-Dashboard hinzugefügt werden. Falls du der Eigentümer dieses Bots bist, denk daran in deinen Servereinstellungen ebenfalls einen entsprechenden Override einzustellen um Missbrauch dieses Commands zu verhindern.", + "add-reason": "Massen-Rollenvergabe durch %u", + "remove-reason": "Massen-Rollenentfernung durch %u" + }, + "hunt-the-code": { + "display-name-description": "Name des Codes, der dem Mitglied angezeigt wird, wenn er den Code einlöst", + "error-creating-code": "Fehler beim erstellen des Codes \"{{displayName}}\". Eventuell ist der eingegebene Code schon in der Datenbank?", + "code-redeem-description": "Den Code den du einlösen möchtest", + "report-header": "Bericht für das Jage den Code-Spiel am %s", + "admin-command-description": "Verwalte die derzeitige Code-Jagd", + "create-code-description": "Erstelle ein neuen Code für die derzeitige Code-Jagd", + "code-description": "Lege den Code fest, der zum Einlösen verwendet werden soll (Standard: zufällig generiert)", + "code-created": "Code \"%displayName\" erfolgreich erstellt: \"%code\"", + "successful-reset": "Erfolgreich das aktuelle Code-Hunt-Spiel beendet - [hier](%url) ist dein Bericht - speichere die URL, wenn du später darauf zugreifen willst.", + "end-description": "Beendet die derzeitige Code-Jagd (löscht Mitglieder und Codes und erstellt einen Bericht)", + "command-description": "Einlösen oder Daten über die aktuelle Code-Jagd einsehen", + "redeem-description": "Löse ein Code ein den du gefunden hast", + "leaderboard-description": "Sehe dir die Rangliste an", + "profile-description": "Aktuelle Anzahl deiner gefundenen Codes anzeigen", + "no-codes-found": "Keine Codes bis jetzt eingelöst ):", + "no-users": "Es haben noch keine Benutzer Codes eingelöst ):", + "user-header": "Teilnehmende Mitglieder", + "code-header": "Codes", + "report-description": "Erstellt einen Bericht", + "report": "Du kannst den Bericht [hier](%url) finden." + }, + "status-role": { + "fulfilled": "Status-Rollen-Bedingung ist erfüllt", + "not-fulfilled": "Status-Rollen-Bedingung ist nicht mehr erfüllt" + }, + "color-me": { + "create-log-reason": "%user hat seine Boosting-Vorteile durch das erstellen der Rolle eingelöst", + "edit-log-reason": "%user hat seine Boosting-Vorteil-Rolle editiert", + "delete-unboost-log-reason": "%user hat aufgehört zu boosten daher wurde seine Rolle gelöscht", + "delete-manual-log-reason": "%user hat seine Rolle manuell gelöscht", + "command-description": "Fordere eine benutzerdefinierte Rolle als Belohnung für das Boosten an. Cooldown: 24h", + "manage-subcommand-description": "Erstelle oder editiere deine Custom Rolle", + "name-option-description": "Der Name deiner Custom Rolle", + "color-option-description": "Die Farbe deiner Custom Rolle", + "remove-subcommand-description": "Entferne deine Custom Rolle", + "confirm-option-remove-description": "Willst du deine Custom Rolle wirklich löschen? Dies wird keine laufenden Cooldowns zurücksetzen" + }, + "rock-paper-scissors": { + "stone": "Stein", + "paper": "Papier", + "scissors": "Schere", + "won": "gewonnen", + "lost": "verloren", + "tie": "Unentschieden", + "play-again": "Erneut spielen", + "challenge-message": "%t, %u hat dich zu einer Runde Schere Stein Papier herausgefordert! Klicke auf den Knopf unter dieser Nachricht, um beizutreten! Diese Einladung wird in etwa 2 Minuten ablaufen, also zögere nicht, sie anzunehmen.", + "invite-expired": "Entschuldigung, %u, %i hat deine Einladung, Schere Stein Papier zu spielen, nicht rechtzeitig angenommen ):", + "invite-denied": "Entschuldigung, %u, aber %i hat deine Einladung, Schere Stein Papier zu spielen, abgelehnt ):", + "rps-title": "Schere Stein Papier", + "rps-description": "Wähle deine Waffe!", + "its-a-tie-try-again": "Unentschieden! Versuch's nochmal!", + "command-description": "Spiele Schere Stein Papier gegen den Bot oder jemanden im Chat" + }, + "connect-four": { + "tie": "Unentschieden!", + "win": "%u hat das Spiel gewonnen!", + "not-turn": "Entschuldigung, aber du bist nicht an der Reihe!", + "game-message": "Vier-gewinnt-Spiel von %u1 und %u2\nAktuell spielt: %c %t.\n\n%g", + "challenge-message": "%t, %u hat dich zu einer Runde Vier gewinnt herausgefordert! Klicke auf den Knopf unter dieser Nachricht, um beizutreten! Diese Einladung wird in etwa 2 Minuten ablaufen, also zögere nicht, sie anzunehmen.", + "invite-expired": "Entschuldigung, %u, %i hat deine Einladung, Vier gewinnt zu spielen, nicht rechtzeitig angenommen ):", + "invite-denied": "Entschuldigung, %u, aber %i hat deine Einladung, Vier gewinnt zu spielen, abgelehnt ):", + "command-description": "Spiele Vier gewinnt gegen jemanden im Chat", + "field-size-description": "Die Größe des Spielfelds (Standard: 7)", + "challenge-yourself": "Du kannst dich nicht selbst herausfordern!", + "challenge-bot": "Du kannst Bots nicht herausfordern!" + }, + "uno": { + "command-description": "Spiele Uno gegen jemanden im Chat", + "challenge-message": "%u lädt zu einer Runde Uno ein! Klicke auf den Knopf unter dieser Nachricht, um beizutreten! Das Spiel startet %timestamp mit %count Spielern.", + "not-enough-players": "Es sind nicht genug Spieler für eine Runde Uno beigetreten!", + "user-cards": "%u: %cards Karten", + "already-joined": "Du bist bereits beigetreten!", + "view-deck": "Eigene Karten ansehen", + "draw": "Karte ziehen", + "uno": "Uno!", + "turn": "%u ist an der Reihe!", + "update-button": "Aktualisieren", + "use-drawn": "Möchtest du die gezogene Karte verwenden?", + "dont-use-drawn": "Nicht verwenden", + "win": "%u hat das Spiel gewonnen! Es wurden %turns Karten gespielt.", + "win-you": "Du hast das Spiel gewonnen!", + "missing-uno": "⚠️️ Du musst den Uno!-Button nutzen, bevor du die vorletzte Karte legst!", + "choose-color": "Wähle eine Farbe aus:", + "pending-draws": "Lege eine Ziehe 2/4-Karte, sonst musst du %count Karten ziehen!", + "not-ingame": "Du bist nicht in dem Uno-Spiel!", + "skip": "Überspringen", + "reverse": "Reverse", + "color": "Farbwahl", + "draw2": "Ziehe 2", + "colordraw4": "Farbwahl und ziehe 4", + "cant-uno": "Du kannst Uno aktuell nicht nutzen.", + "done-uno": "Du hast Uno gerufen!", + "auto-drawn-skip": "Dein Zug wurde übersprungen, da du die Karten sowieso hättest ziehen müssen.", + "start-game": "Spiel sofort starten", + "not-host": "Du bist nicht der Ersteller des Spiels!", + "max-players": "Das Spiel ist voll!", + "previous-cards": "Vorherige Karten: ", + "used-card": "Du hast die Karte %c bereits verwendet! Nutze den Aktualisieren-Button und spiele eine zulässige Karte.", + "invalid-card": "Du kannst die Karte %c momentan nicht spielen! Bitte spiele eine zulässige Karte.", + "inactive-warn": "%u, du bist bei Uno am Zug!", + "inactive-win": "Das Uno-Spiel wurde beendet. %u hat gewonnen, da alle anderen ausgeschieden sind!" + }, + "quiz": { + "what-have-i-voted": "Was habe ich gewählt?", + "vote": "Abstimmen!", + "vote-this": "Wähle diese Option, wenn du denkst, dass diese richtig ist.", + "voted-successfully": "Erfolgreich ausgewählt. Danke für deine Teilnahme.", + "not-voted-yet": "Du hast noch keine Antwort ausgewählt, also kann ich dir nicht zeigen, für was du abgestimmt hast?", + "you-voted": "Du hast **%o** als Antwort ausgewählt.", + "change-opinion": "Du kannst deine Auswahl jederzeit ändern, indem du einfach etwas anderes über dem Knopf, den du gerade angeklickt hast, auswählst.", + "cannot-change-opinion": "Du kannst deine Auswahl nicht ändern, da der Ersteller diese Funktion deaktiviert hat.", + "select-correct": "Wähle alle richtigen Antworten aus", + "this-correct": "Diese Antwort als richtig markieren", + "cmd-description": "Erstelle oder spiele Quiz", + "cmd-create-normal-description": "Erstelle ein Quiz mit bis zu 10 Antworten", + "cmd-create-bool-description": "Erstelle ein Quiz, bei dem Nutzer nur Ja oder Nein auswählen können", + "cmd-play-description": "Spiele ein Server-Quiz", + "cmd-leaderboard-description": "Zeigt das Quiz-Leaderboard des Servers", + "cmd-create-description-description": "Thema / Beschreibung des Quiz", + "cmd-create-channel-description": "Kanal, in welchem dieses Quiz erstellt werden soll", + "cmd-create-endAt-description": "Relative Dauer des Quiz", + "cmd-create-option-description": "Option Nummer %o", + "cmd-create-canchange-description": "Ob die Teilnehmer ihre Auswahl nachträglich ändern können (Standard: Nein)", + "daily-quiz-limit": "Du hast das Limit von **%l** täglichen Quiz erreicht. Du kannst %timestamp wieder Quiz spielen.", + "created": "Quiz erfolgreich in %c erstellt.", + "correct-highlighted": "Alle richtigen Antworten wurden hervorgehoben.", + "answer-correct": "✅ Deine Antwort war richtig, du hast einen Punkt fürs Leaderboard erhalten!", + "answer-wrong": "❌ Deine Antwort war falsch!", + "bool-true": "Aussage stimmt", + "bool-false": "Aussage stimmt nicht", + "leaderboard-channel-not-found": "Der Leaderboard-Kanal wurde nicht gefunden oder sein Typ ist nicht erlaubt.", + "leaderboard-notation": "**%p. %u**: %xp XP", + "your-rank": "Du hast **%xp** Punkte in Quiz gesammelt!", + "no-rank": "Du hast noch nie ein Quiz erfolgreich beendet!", + "no-quiz": "Es wurden noch keine Quiz erstellt. Serveradmins können auf https://scnx.app/glink?page=bot/configuration?query=quiz&file=quiz%7Cconfigs%2FquizList Quiz hinzufügen.", + "no-permission": "Du hast keine Berechtigung, um Quiz mit dem Befehl zu erstellen." + }, + "starboard": { + "invalid-minstars": "Ungültige Mindestanzahl an Sternen %stars", + "star-limit": "Du hast das stündliche Starboard-Limit von %limitEmoji auf dem Server erreicht, deswegen kannst du nicht auf die Nachricht %msgUrl reagieren.\nProbiers doch %time nochmal!" + }, + "nicknames": { + "owner-cannot-be-renamed": "Der Serverbesitzer (%u) kann nicht umbenannt werden.", + "nickname-error": "Fehler beim Ändern des Nicknamens von %u: %e" + } +} diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 00000000..4cf4437e --- /dev/null +++ b/locales/en.json @@ -0,0 +1,1021 @@ +{ + "main": { + "startup-info": "SCNX-CustomBot v2 - Log-Level: %l", + "missing-moduleconf": "Missing moduleConfig-file. Automatically disabling all modules and overwriting modules.json later", + "sync-db": "Synced database", + "login-error": "Bot could not log in. Error: %e", + "login-error-token": "Bot could not log in because the provided token is invalid. Please update your token.", + "login-error-intents": "Bot could not log in because the intents were not enabled correctly. Please enable \"PRESENCE INTENT\", \"SERVER MEMBERS INTENT\" and \"MESSAGE CONTENT INTENT\" in your Discord-Developer-Dashboard: %url", + "not-invited": "Please invite the bot to your guild before continuing: %inv", + "require-code-grant-active": "You might be unable to invite your bot to your server as you have enabled the \"Require public code grant\" option in your Discord Developer Dashboard. Please disable this option: %d", + "logged-in": "Bot logged in as %tag and is now online.", + "logchannel-wrong-type": "There is no Log-Channel set or it has the wrong type (only text-channels are supported).", + "config-check-failed": "Configuration-Check failed. You can find more information in your log. The bot exited.", + "bot-ready": "The bot initiated successfully and is now listening to commands", + "no-command-permissions": "Could not update guild commands. Please give us permissions to performe this critical action: %inv", + "perm-sync": "Synced permissions for /%c", + "perm-sync-failed": "Failed to synced permissions for /%c: %e", + "loading-module": "Loading module %m", + "hidden-module": "Module %m is hidden, meaning that it is not availible. Skipping…", + "module-disabled": "Module %m is disabled", + "command-loaded": "Loaded command %d/%f", + "command-dir": "Loading commands in %d/%f", + "global-command-sync": "Synced global application commands", + "guild-command-sync": "Synced guild application commands", + "guild-command-no-sync-required": "Guild application commands are up to date - no syncing required", + "global-command-no-sync-required": "Global application commands are up to date - no syncing required", + "event-loaded": "Loaded events %d/%f", + "event-dir": "Loading events in %d/%f", + "model-loaded": "Loaded database model %d/%f", + "model-dir": "Loading database model in %d/%f", + "loaded-cli": "Loaded API-Action %c in %p", + "channel-lock": "Locked channel", + "channel-unlock": "Unlocked channel", + "channel-unlock-data-not-found": "Unlocking channel with ID %c failed because it was never locked (which is weird to begin with).", + "module-disable": "Module %m got disabled because %r", + "migrate-success": "Migration from %o to %m finished successfully.", + "migrate-start": "Migration from %o to %m started... Please do not stop the bot" + }, + "reload": { + "reloading-config": "Reloading configuration…", + "reloading-config-with-name": "User %tag is reloading the configuration…", + "reloaded-config": "Configuration reloaded successfully.\nOut of %totalModules modules, %enabled were enabled, %configDisabled were disabled because their configuration was wrong.", + "reload-failed": "Configuration reloaded failed. Bot shutting down.", + "reload-successful-syncing-commands": "Configuration reloaded successfully, syncing commands, to make sure permissions are up-to-date…", + "reload-failed-message": "**FAILED**\n```%r```\n**Please read your log to find more information**\nThe bot will kill itself now, bye :wave:", + "command-description": "Reloads the configuration" + }, + "config": { + "checking-config": "Checking configurations...", + "done-with-checking": "Done with checking. Out of %totalModules modules, %enabled were enabled, %configDisabled were disabled because their configuration was wrong.", + "creating-file": "Config %m/%f does not exist - I'm going to create it, please stand by...", + "checking-of-field-failed": "An error occurred while checking the content of field \"%fieldName\" in %m/%f", + "saved-file": "Configuration-File %f in %m was saved successfully.", + "moduleconf-regeneration": "Regenerating module configuration, no settings will be overwritten, don't worry.", + "moduleconf-regeneration-success": "Module configuration regeneration successfully finished.", + "channel-not-found": "Channel with ID \"%id\" could not be found", + "user-not-found": "User with ID \"%id\" could not be found", + "channel-not-on-guild": "Channel with ID \"%id\" is not on your guild", + "channel-invalid-type": "Channel with ID \"%id\" has a type that can not be used for this field", + "role-not-found": "Role with ID \"%id\" could not be found on your guild", + "config-reload": "Reloading all configuration..." + }, + "helpers": { + "timestamp": "%dd.%mm.%yyyy at %hh:%min", + "you-did-not-run-this-command": "You did not run this command. If you want to use the buttons, try running the command yourself.", + "next": "Next", + "back": "Back", + "toggle-data-fetch-error": "SC Network Release: Toggle-Data could not be fetched", + "toggle-data-fetch": "SC Network Release: Toggle-Data fetched successfully" + }, + "command": { + "startup": "The bot is currently starting up. Please try again in a few minutes.", + "not-found": "Command not found", + "used": "%tag (%id) used command /%c", + "message-used": "%tag (%id) used command %p%c", + "execution-failed": "Execution of command /%c %g %s failed: %e", + "message-execution-failed": "Execution of command %p%c failed: %e", + "wrong-guild": "This command is only available on the server **%g**.", + "autcomplete-execution-failed": "Execution of auto-complete on command /%c %g %s with option %f failed: %e", + "execution-failed-message": "## \uD83D\uDD34 Command execution failed \uD83D\uDD34\nThis usually happens either due to misconfiguration or due to an error on our site. Please send a screenshot of this message in #support on the [ScootKit Discord](https://scootk.it/dc-en), to resolve this.\n\n### Debugging-Information\n```%e```", + "error-giving-role": "An error occurred when trying to giving to give your your roles ):", + "module-disabled": "This command is part of the \"%m\" which is disabled. This can either be intended by the server-admins (and slash-commands haven't synced yet) or this could be caused by a configuration error. Please check (or ask the admins) to check the bot's configuration and logs for details." + }, + "help": { + "bot-info-titel": "ℹ️ Bot-Info", + "bot-info-description": "This bot is part of [SCNX](https://scnx.xyz/de?ref=custombot_help_embed), a plattform from [ScootKit](https://scootkit.net) allowing the creation of fully customizable for Discord communities, and is being hosted for \"%g\".", + "stats-title": "📊 Stats", + "stats-content": "Active modules: %am\nRegistered commands: %rc\nBot-Version: %v\nRunning on server %si\n[Server-Plan](https://scnx.xyz/plan): %pl\nLast restart: %lr\nLast reload: %lR", + "command-description": "Show every commands", + "slash-commands-title": "Slash-Commands" + }, + "bot-feedback": { + "command-description": "Send feedback about the bot to the bot developer", + "submitted-successfully": "Thanks so much for your feedback! It has been carefully recorded and our team will review it soon. If we have any questions, we may contact you via DM (or if you are on our [Support Server]() we'll open a ticket). Thank you for making [CustomBots]() better for everyone <3\n\nYour feedback is subject to our [Terms of service]() and [Privacy Policy]().", + "failed-to-submit": "Sorry, but I couldn't send your feedback to our staff. This could be, because you got blocked or because of some server issue we are having. You can always report bugs and submit feedback in our [Feature-Board](https://features.sc-network.net). Thank you.", + "feedback-description": "Your feedback. Make sure it's neutral, constructive and helpful" + }, + "admin-tools": { + "position": "%i has the position %p.", + "position-changed": "Changed %i's position to %p.", + "category-can-not-have-category": "A Category can not have a category", + "not-category": "Can not change category of channel to a not category channel", + "changed-category": "%c's category got set to %cat", + "command-description": "Execute some actions for admins via commands", + "new-position-description": "New position", + "movechannel-description": "See the position of a channel or change the position of a channel", + "moverole-description": "See the position of a role or change the position of a role", + "setcategory-description": "Sets the category of a channel", + "channel-description": "Channel on which this action should be executed", + "role-description": "Role on which this action should be executed", + "category-description": "New category of the channel", + "emoji-too-much-data": "Please **only** enter one emoji and nothing else", + "emoji-import": "Imported \"%e\" successfully.", + "stealemote-description": "Steals a emote from another server", + "emote-description": "Emote to steal" + }, + "welcomer": { + "channel-not-found": "[welcomer] Channel not found: %c", + "welcome-yourself-error": "Welcome, nice to meet you! This button is reversed for a special member of this server who want's to say \"Hi\" to you ^^" + }, + "birthdays": { + "channel-not-found": "[birthdays] Channel not found: %c", + "sync-error": "[birthdays] %u's state was set to \"sync\", but there was no syncing candidate, so I disabled the synchronization", + "age-hover": "%a years old", + "sync-enabled-hover": "Birthday synchronized", + "verified-hover": "Birthday verified", + "no-bd-this-month": "No birthdays this month ):", + "no-birthday-set": "You don't currently have a registered birthday on this server. Set a birthday with `/birthday set`.", + "birthday-status": "Your birthday is currently set to **%dd.%mm%yyyy**%age.", + "your-age": "which means that you are **%age** years old", + "sync-on": "Your birthday is being synced via your [SC Network Account](https://sc-network.net/dashboard).", + "sync-off": "Your birthday is set locally on this server and will not be synchronized", + "no-sync-account": "It seems like you either don't have an [SC Network Account]() or you haven't entered any information about your birthday in it yet.", + "auto-sync-on": "It seems that you have autoSync in your [SC Network Account]() enabled. This means that your birthday will be synchronized all the time on every server. [Learn more]().\nYour birthday isn't showing up? It can take up to 24 hours (usually it's less than two hours) for it to be synced, so stay calm and wait just a bit longer.", + "enabled-sync": "Successfully set. The synchronization is now enabled :+1:", + "disabled-sync": "Successfully set. The synchronization is disabled, you can now change or remove your birthday from this server.", + "delete-but-sync-is-on": "You currently have sync enabled. Please disable sync to delete your birthday.", + "deleted-successfully": "Birthday deleted successfully.", + "only-sync-allowed": "This server only allows synchronization of your birthday with a [SC Network Account]()", + "invalid-date": "Invalid date provided", + "against-tos": "You have to be at least 13 years old to use Discord. Please read Discord's [Terms of Service]() and if you are under the age of 13 please [delete your account]() to comply with Discord's [Terms of Service]() and wait %waitTime (or for the age for your country, listed [here]()) years before creating a new account.", + "too-old": "It seems like you are too old to be alive", + "command-description": "View, edit and delete your birthday", + "status-command-description": "Shows the current status of your birthday", + "sync-command-description": "Manage the synchronization on this server", + "sync-command-action-description": "Action which should be performed on your synchronization", + "sync-command-action-enable-description": "Enable synchronization", + "sync-command-action-disable-description": "Disable synchronization", + "set-command-description": "Sets your birthday", + "set-command-day-description": "Day of your birthday", + "set-command-month-description": "Day of your birthday", + "set-command-year-description": "Year of your birthday", + "delete-command-description": "Deletes your birthday from this server", + "migration-happening": "Database-Schema not up-to-date. Migration database... This could take a while. Do not restart your bot to avoid data loss.", + "migration-done": "Successfully migrated database to newest version." + }, + "months": { + "1": "January", + "2": "February", + "3": "March", + "4": "April", + "5": "May", + "6": "June", + "7": "July", + "8": "August", + "9": "September", + "10": "October", + "11": "November", + "12": "December" + }, + "giveaways": { + "no-link": "None", + "no-winners": "None", + "not-supported-for-news-channel": "Not supported for news-channels", + "leave-giveaway": "❌ Leave giveaway", + "giveaway-left": "You have successfully left this giveaway 👍", + "required-messages": "Must have %mc new messages (check with `/gmessages`)", + "required-messages-user": "Have at least %mc new messages (%um/%mc messages)", + "roles-required": "Must have one of this roles to enter: %r", + "giveaway-ended-successfully": "Giveaway ended successfully.", + "no-giveaways-found": "No giveaways found", + "gmessages-description": "See your messages for a giveaway", + "jump-to-message-hover": "Jump to message", + "messages": "Nachrichten", + "giveaway-messages": "Giveaway-Messages", + "duration-parsing-failed": "Duration-Parsing failed.", + "channel-type-not-supported": "Channel-Type not supported", + "parameter-parsing-failed": "Parsing of parameters failed", + "started-successfully": "Started giveaway successfully in %c.", + "reroll-done": "Done :+1:", + "select-menu-description": "Will end in #%c on %d", + "no-giveaways-for-reroll": "They are no currently running giveaways. Maybe you are looking for /reroll?", + "select-giveaway-to-end": "Please select the giveaway which you want to end.", + "please-select": "Please select", + "gmanage-description": "Manage giveaways", + "gmanage-start-description": "Start a new giveaway", + "gmanage-channel-description": "Channel to start the giveaway in", + "gmanage-price-description": "Price that can be won", + "gmanage-duration-description": "Duration of the giveaway (e.g: \"2h 40m\" or \"7d 2h 3m\")", + "gmanage-winnercount-description": "Count of winners that should be selected", + "gmanage-requiredmessages-description": "Count of new (!) messages that a user needs to have before entering", + "gmanage-requiredroles-description": "Role that user need to have to enter the giveaway", + "gmanage-sponsor-description": "Sets a different giveaway-starter, useful if you have a sponsor", + "gmanage-sponsorlink-description": "Link to a sponsor if applicable", + "gend-description": "End a giveaway", + "gereroll-description": "Rerolls an ended giveaway", + "gereroll-msgid-description": "Message-ID of the giveaway", + "gereroll-winnercount-description": "How many new winners there should be", + "migration-happening": "Database not up-to-date. Migrating database...", + "migration-done": "Migrated database successfully." + }, + "levels": { + "leaderboard-channel-not-found": "Leaderboard-Channel not found or wrong type", + "leaderboard-notation": "%p. %u: Level %l - %xp XP", + "leaderboard": "Leaderboard", + "no-user-on-leaderboard": "Can't generate a leaderboard, because no one has any XP which is odd, but that's how it is ¯\\_(ツ)_/¯", + "and-x-other-users": "and %uc other users", + "level": "Level %l", + "users": "Users", + "leaderboard-command-description": "Shows the leaderboard of this guild", + "leaderboard-sortby-description": "How to sort the leaderboard (default: %d)", + "profile-command-description": "Shows the profile of you or an an user", + "profile-user-description": "User to see the profile from (default: you)", + "please-send-a-message": "Please send some messages before I can show you some data", + "no-role": "None", + "are-you-sure-you-want-to-delete-user-xp": "Okay, do you really want to screw with %u? If you hate them so much, feel free to run `/manage-levels reset-xp confirm:True user:%ut` to run this irreversible action.", + "are-you-sure-you-want-to-delete-server-xp": "Do you really want to delete all XP and Levels from this server? This action is irreversible and everyone on this server will hate you. Decided that it's worth it? Enter `/manage-levels reset-xp confirm:True`", + "user-not-found": "User not found", + "user-deleted-users-xp": "%t deleted the XP of the user with id %u", + "removed-xp-successfully": "`Removed %u's XP and level successfully.`", + "deleted-server-xp": "%u deleted the XP of all users", + "successfully-deleted-all-xp-of-users": "Successfully deleted all the XP of all users", + "cheat-no-profile": "This user doesn't have a profile (yet), please force them to write a message before trying to betrayal your community by manipulating level scores.", + "manipulated": "%u manipulated the XP of %m to %v (level %l)", + "successfully-changed": "Successfully edited the XP of %u - they are now **level %l** with **%x XP**.\nRemember, every change you make destroys the experience of other users on this server as the levelsystem isn't fair anymore.", + "edit-xp-command-description": "Manage the levels of your server", + "negative-xp": "This user would have a negative XP value which is not possible", + "negative-level": "This user would have a level below one which is not possible", + "reset-xp-description": "Reset the XP of a user or of the whole server", + "reset-xp-user-description": "User to reset the XP from (default: whole server)", + "reset-xp-confirm-description": "Do you really want to delete the data?", + "edit-xp-user-description": "User to edit", + "edit-xp-value-description": "New XP value of the user", + "edit-xp-description": "Betrays your community and edits a user's XP", + "role-factors-total": "Multiplied together, the user receives **%f times more XP** for every message.", + "edit-level-description": "Betrays your community and edits a user's levels", + "random-messages-enabled-but-non-configured": "You have random messages enabled, but have non configured. Ignoring config.randomMessages configuration.", + "granted-rewards-audit-log": "Updated roles to make sure, they have the level role they need" + }, + "team-list": { + "channel-not-found": "Could not find channel with ID %c or the channel has a wrong type (only text-channels supported)", + "role-not-found": "Could not find role with ID %r", + "no-users-with-role": "No users on this guild have the %r role yet.", + "no-roles-selected": "No roles listed yet.", + "offline": "Offline", + "dnd": "Do not disturb", + "idle": "Away", + "online": "Online" + }, + "partner-list": { + "could-not-give-role": "Could not give role to user %u", + "could-not-remove-role": "Could not remove role from user %u", + "partner-not-found": "Partner could not be found. Please check if you are using the right partner-ID. The partner-ID is not identical with the server-id of the partner. The Partner-ID can be found [here](https://gblobscdn.gitbook.com/assets%2F-MNyHzQ4T8hs4m6x1952%2F-MWDvDO9-_JwAGqtD6at%2F-MWDxIcOHB9VcWhjsWt7%2Fscreen_20210320-102628.png?alt=media&token=2f9ac1f7-1a14-445c-b34e-83057789578e) in the partner-embed.", + "successful-edit": "Edited partner-list successfully.", + "channel-not-found": "Could not find channel with ID %c or the channel has a wrong type (only text-channels supported)", + "no-partners": "There are currently no partners. This is odd, but that's how it is ¯\\_(ツ)_/¯\n\nTo add a partner, run `/partner add` as a slash-command.", + "information": "Information", + "command-description": "Manages the partner-list on this server", + "padd-description": "Add a new partner", + "padd-name-description": "Name of the partner", + "padd-category-description": "Please select one of the categories specified in your configuration", + "padd-owner-description": "Owner of the partnered server", + "padd-inviteurl-description": "Invite to the partnered server", + "pedit-description": "Edits an existing partner", + "pedit-id-description": "ID of the partner", + "pedit-name-description": "New name of the partner", + "pedit-inviteurl-description": "New invite to this partner", + "pedit-category-description": "New category of this partner", + "pedit-owner-description": "New owner of the partner server", + "pedit-staff-description": "New designated staff member for this partner server", + "pdelete-description": "Deletes an exiting partner", + "pdelete-id-description": "ID of the partner" + }, + "ping-on-vc-join": { + "channel-not-found": "Notify channel %c not found", + "could-not-send-pn": "Could not send PN to %m" + }, + "suggestions": { + "suggestion-not-found": "Suggestion not found", + "updated-suggestion": "Successfully updated suggestion", + "suggest-description": "Create and comment on suggestions", + "suggest-content": "Content you want to suggest", + "loading": "A wild new suggestion appeared, loading..", + "manage-suggestion-command-description": "Manage suggestions as an admin", + "manage-suggestion-accept-description": "Accepts a suggestion", + "manage-suggestion-deny-description": "Denies a suggestion", + "manage-suggestion-id-description": "ID of the suggestion", + "manage-suggestion-comment-description": "Explain why you made this choice" + }, + "auto-delete": { + "could-not-fetch-channel": "Could not fetch channel with ID %c", + "could-not-fetch-messages": "Could not fetch messages from channel with ID %c" + }, + "auto-thread": { + "thread-create-reason": "This thread got created, because you configured auto-thread to do so" + }, + "auto-messager": { + "channel-not-found": "Channel with ID %id not found" + }, + "polls": { + "what-have-i-votet": "What have I voted?", + "vote": "Vote!", + "vote-this": "Click on this option to place your vote here", + "voted-successfully": "Successfully voted. Thanks for your participation.", + "not-voted-yet": "You have not voted yet, so I cant show you what you voted?", + "you-voted": "You have voted for **%o**.", + "remove-vote": "Remove my vote", + "removed-vote": "Your vote was removed successfully.", + "change-opinion": "You can change your opinion anytime by just selecting something else above the button you just clicked.", + "command-poll-description": "Create and end polls", + "command-poll-create-description": "Create a new poll", + "command-poll-end-description": "Ends an existing poll", + "command-poll-end-msgid-description": "ID of the poll", + "command-poll-create-description-description": "Topic / Description of this poll", + "command-poll-create-channel-description": "Channel in which the poll should get created", + "command-poll-create-option-description": "Option number %o", + "command-poll-create-endAt-description": "Duration of the poll (if not set the poll will not end automatically)", + "command-poll-create-public-description": "If enabled (disabled by default) the votes of users will be displayed publicly", + "created-poll": "Successfully created poll in %c.", + "not-found": "Poll could not be found", + "no-votes-for-this-option": "Nobody voted this option yet", + "ended-poll": "Poll ended successfully", + "view-public-votes": "View current voters", + "not-public": "This poll does not appear to be public, no results can be displayed.", + "poll-private": "\uD83D\uDD12 This poll is **anonymous**, meaning that no one can see what you voted (not even the admins).", + "poll-public": "\uD83D\uDD13 This poll is **public**, meaning that everyone can see what you voted.", + "not-text-channel": "You need to select a text-channel that is not an announcement-channel." + }, + "channel-stats": { + "audit-log-reason-interval": "Updated channel because of interval", + "audit-log-reason-startup": "Updated channel because of startup", + "not-voice-channel-info": "Channel \"%c\" (%id) is a %t and not a voice-channel as recommended" + }, + "activities": { + "hook-installed": "Installed hook for generating special activity invites", + "command-description": "Create a in-voice-activity on discord", + "type-description": "Type of the voice activity" + }, + "info-commands": { + "info-command-description": "Find information about parts of this server", + "command-userinfo-description": "Find more information about a user on this server", + "argument-userinfo-user-description": "User you want to see information about (default: you)", + "command-roleinfo-description": "Find more information about a role on this server", + "argument-roleinfo-role-description": "Role you want to see information about", + "command-channelinfo-description": "Find more information about a channel on this server", + "argument-channelinfo-channel-description": "Channel you want to see information about", + "command-serverinfo-description": "Find more information about this server", + "information-about-role": "Information about the role %r", + "hoisted": "Hoisted", + "mentionable": "Mentionable", + "managed": "Managed", + "information-about-channel": "Information about the channel %c", + "information-about-user": "Information about the user %u", + "information-about-server": "Information about %s", + "boostLevel": "Level", + "boostCount": "Boosts", + "userCount": "Users", + "memberCount": "Members", + "onlineCount": "Online", + "textChannel": "Text", + "voiceChannel": "Voice", + "categoryChannel": "Categories", + "otherChannel": "Other", + "total-invites": "Total", + "active-invites": "Active", + "left-invites": "Left" + }, + "channelType": { + "GUILD_TEXT": "Text-Channel", + "GUILD_VOICE": "Voice-Channel", + "GUILD_CATEGORY": "Category", + "GUILD_NEWS": "News-Channel", + "GUILD_STORE": "Store-Channel", + "GUILD_NEWS_THREAD": "News-Channel-Thread", + "GUILD_PUBLIC_THREAD": "Public Thread", + "GUILD_PRIVATE_THREAD": "Private Thread", + "GUILD_STAGE_VOICE": "Stage-Channel", + "DM": "Direct-Message", + "GROUP_DM": "Group-Direct-Message", + "UNKNOWN": "Unknown" + }, + "stagePrivacy": { + "PUBLIC": "Publicly accessible", + "GUILD_ONLY": "Only guild members can join" + }, + "guildVerification": { + "NONE": "None", + "LOW": "Low", + "MEDIUM": "Medium", + "HIGH": "High", + "VERY_HIGH": "Very high" + }, + "boostTier": { + "NONE": "None", + "TIER_1": "Level 1", + "TIER_2": "Level 2", + "TIER_3": "Level 3" + }, + "temp-channels": { + "removed-audit-log-reason": "Removed temp channel, because no one was in it", + "permission-update-audit-log-reason": "Updated permissions, to make sure only people in the VC can see the no-mic-channel", + "created-audit-log-reason": "Created Temp-Channel for %u", + "move-audit-log-reason": "Moved user to their voice channel", + "no-mic-channel-topic": "Welcome to %u's no-mic-channel. You will see this channel as long as you are connected to this temp-channel.", + "disconnect-audit-log-reason": "The old channel of the user could not be found - disconnecting them - hopefully they join again", + "command-description": "Manage your temp-channel", + "mode-subcommand-description": "Change the mode of your channel", + "public-option-description": "If enabled, anyone can join your temp-channel", + "add-subcommand-description": "Add users, that will be able to join your channel, while it is private", + "remove-subcommand-description": "Remove users from you channel", + "add-user-option-description": "The user to be added", + "remove-user-option-description": "The user to be removed", + "list-subcommand-description": "List the users with access to your channel", + "edit-subcommand-description": "Edit various settings of your channel", + "user-limit-option-description": "Change the user-limit of your channel", + "bitrate-option-description": "Change the bitrate of your channel (min. 8000)", + "name-option-description": "Change the name of your channel", + "nsfw-option-description": "Change, whether your channel is age-restricted or not", + "no-added-user": "There are no users to be displayed here", + "nothing-changed": "Your channel already had these settings.", + "no-disconnect": "Couldn't disconnect the user from your channel. This could be due to missing permissions, or the user not being in your voice-channel", + "edit-error": "An error occurred while editing your channel. one or more of your settings couldn't be applied. This could be due to missing permissions or an invalid value.", + "add-user": "Add user", + "remove-user": "Remove user", + "list-users": "List users", + "private-channel": "Private", + "public-channel": "Public", + "edit-channel": "Edit channel", + "add-modal-title": "Add an user to your temp-channel", + "add-modal-prompt": "The user you want to add (tag or user-id)", + "remove-modal-title": "Remove an user from your temp-channel", + "remove-modal-prompt": "The user you want to remove (tag or user-id)", + "edit-modal-title": "Edit your temp-channel", + "edit-modal-nsfw-prompt": "Mark temp-channel as age-restricted?", + "edit-modal-nsfw-placeholder": "\"true\" (yes) or \"false\" (no)", + "edit-modal-bitrate-prompt": "Bitrate of your Temp-channel?", + "edit-modal-bitrate-placeholder": "A number over 8000", + "edit-modal-limit-prompt": "Limit of users in your temp-channel", + "edit-modal-limit-placeholder": "Number between 0 and 99; 0 = unlimited", + "edit-modal-name-prompt": "How should your channel be called?", + "edit-modal-name-placeholder": "A very creative channel name", + "user-not-found": "User not found" + }, + "guess-the-number": { + "command-description": "Manage your guess-the-number-games", + "status-command-description": "Shows the current status of a guess-the-number-game in this channel", + "create-command-description": "Create a new guess-the-number-game in this channel", + "create-min-description": "Minimal value users can guess", + "create-max-description": "Maximal value users can guess", + "create-number-description": "Number users should guess to win", + "end-command-description": "Ends the current game", + "session-already-running": "There is a session already running in this channel. Please end it with /guess-the-number end", + "session-not-running": "There is currently no session running.", + "session-ended-successfully": "Ended session successfully. Locked channel successfully.", + "current-session": "Current session", + "number": "Number", + "min-val": "Min-Value", + "max-val": "Max-Value", + "owner": "Owner", + "guess-count": "Count of guesses", + "min-max-discrepancy": "`min` can't be bigger or equal to `max`", + "max-discrepancy": "`number` can't be bigger than `max`.", + "min-discrepancy": "`number` can't be smaller than `min`.", + "emoji-guide-button": "What does the reaction under my guess mean?", + "guide-wrong-guess": "Your guess was wrong (but your entry was valid)", + "guide-win": "You guessed correctly - you win :tada:", + "guide-admin-guess": "Your guess was invalid, because you are an admin - admins can't participate because they can see the correct number", + "guide-invalid-guess": "Your guess was invalid (e.g. below the minimal / over the maximal number, not a number, …)", + "created-successfully": "Created game successfully. Users can now start guessing in this channel. The winning number is **%n**. You can always check the status by running `/guess-the-number-status`. Note that you as an admin can not guess.", + "game-ended": "Game ended", + "game-started": "Game started" + }, + "massrole": { + "command-description": "Manage roles for all members", + "add-subcommand-description": "Add a role to all members", + "remove-subcommand-description": "Remove a role from all members", + "remove-all-subcommand-description": "Remove all roles from all members", + "role-option-add-description": "The role, that will be given to all members", + "role-option-remove-description": "The role, that will be removed from all members", + "target-option-description": "Determines whether bots should be included or not", + "all-users": "All Users", + "bots": "Bots", + "humans": "Humans", + "not-admin": "⚠ To use this command, you need to be added to the adminRoles option in the SCNX-Dashboard. If you are the owner of this bot please remember to create an override in the guild settings to prevent abuse of this command.", + "add-reason": "Mass role addition by %u", + "remove-reason": "Mass role removal by %u" + }, + "twitch-notifications": { + "channel-not-found": "Channel with ID %c could not be found", + "user-not-on-twitch": "Could not find user %u on twitch" + }, + "hunt-the-code": { + "admin-command-description": "Manage the current Code-Hunt", + "create-code-description": "Create a new code for the current code-hunt", + "display-name-description": "Name of the code that will be displayed to user when they redeem the code", + "code-description": "Set the code that will be used to redeem it (default: randomly generated)", + "code-created": "Code \"%displayName\" successfully created: \"%code\"", + "error-creating-code": "Error creating code \"{{displayName}}\". Maybe the entered code is already in the database?", + "successful-reset": "Successfully ended the current Code-Hunt-Game - [here](%url)'s your report - save the URL if you want to access it later.", + "end-description": "Ends the current Code-Hunt (will clear users and codes and generates a report)", + "command-description": "Redeem or see data about the current Code-Hunt", + "redeem-description": "Redeem a code you found", + "code-redeem-description": "The code you want to redeem", + "leaderboard-description": "See the current leaderboard", + "profile-description": "See your current count of found codes", + "no-codes-found": "No codes redeemed yet ):", + "no-users": "No users redeemed codes yet ):", + "report-header": "Report for the Hunt-The-Code game on %s", + "user-header": "Participating users", + "code-header": "Codes", + "report-description": "Generates a report", + "report": "You can find the report [here](%url)." + }, + "fun": { + "slap-command-description": "Slap a user in the face", + "user-argument-description": "User to performe this action on", + "no-no-not-slapping-yourself": "You can not punch yourself lol (well technically you can, but our gifs do not support that, so deal with it ¯\\_(ツ)_/¯)", + "pat-command-description": "Pat someone nicely", + "no-no-not-patting-yourself": "Well, good try, but we don't do this here", + "no-no-not-kissing-yourself": "Uah, that's gross, you should try paying somebody to do that (well you should not, but better then kissing yourself)", + "kiss-command-description": "Kiss someone", + "hug-command-description": "Hug someone <3", + "no-no-not-hugging-yourself": "You are quite lonely aren't you? Try hugging a tree, that should work. Unless you live in a desert. Then hug a cactus. That's a bit more painful, but trust me.", + "random-command-description": "Helps you select random things", + "random-number-command-description": "Selects a random number", + "min-argument-description": "Minimal number (default: 1)", + "max-argument-description": "Maximal number (default: 42)", + "random-ikeaname-command-description": "Generates a random name for a IKEA-Name", + "syllable-count-argument-description": "Count of syllables to generate name from (default: random)", + "random-dice-command-description": "Roll a dice", + "random-coinflip-command-description": "Flip a coin", + "random-8ball-command-description": "Generates an answer to a yes/no question", + "dice-site-1": "Heads", + "dice-site-2": "Tails" + }, + "moderation": { + "moderate-command-description": "Moderate users on your server", + "moderate-notes-command-description": "Set or see moderator's notes of a user", + "moderate-notes-command-view": "View a user's notes", + "moderate-notes-command-create": "Create a new note about a user", + "moderate-notes-command-edit": "Edit one of your existing notes about a user", + "moderate-notes-command-delete": "Delete one of your existing notes about a user", + "moderate-ban-command-description": "Ban a user on your server", + "moderate-reason-description": "Reason for your action", + "moderate-proof-description": "Proof for your action", + "report-user-not-found-on-guild": "This user could not be found on \"%s\". You can only report users that are members of our server.", + "proof": "Proof", + "report-proof-description": "Attach an optional (image) proof to your report", + "file": "File uploaded", + "anti-grief-reason": "Too many actions of type \"%type\" in the last %h hours. Maximum amount allowed: %n", + "anti-grief-user-message": "Sorry, but it seems like you are abusing your moderative powers. We've taken actions to prevent this from happening.", + "moderate-duration-description": "Duration of the action (max: 28 days, default: 14 days)", + "mute-max-duration": "Discord limits the maximal duration of a timeout to 28 days. Please enter an amount equal or less than this", + "moderate-quarantine-command-description": "Quarantine a user on your server", + "moderate-unquarantine-command-description": "Removes a user from the quarantine", + "moderate-unban-command-description": "Revokes an existing ban", + "moderate-clear-command-description": "Clears messages in the current channel", + "moderate-clear-amount-description": "How many messages should get cleared?", + "moderate-kick-command-description": "Kick a user from your server", + "moderate-unwarn-command-description": "Revokes a warning", + "moderate-mute-command-description": "Mute a user on your server", + "moderate-unmute-command-description": "Unmutes a user on your server", + "moderate-warn-command-description": "Warn a user", + "moderate-channel-mute-description": "Mutes a user from the current channel", + "moderate-unchannel-mute-description": "Removes a channel-mute from this channel", + "moderate-lock-command-description": "Lock the current channel", + "moderate-unlock-command-description": "Unlock the current channel", + "moderate-user-description": "User on who the action should get performed", + "moderate-userid-description": "ID of a user", + "moderate-days-description": "Number of days of messages to delete", + "invalid-days": "Days can only be between 0 and 7 (inclusive)", + "moderate-notes-description": "Notes to set / update", + "moderate-note-id-description": "ID of one of your notes you want to edit (leave blank to create a new one)", + "moderate-warnid-description": "ID of a warn (run /moderate actions to get it)", + "moderate-actions-command-description": "Show all recorded actions against a user", + "report-command-description": "Reports a user and sends a snapshot of the chat to server staff", + "report-reason-description": "Please describe what the user did wrong", + "report-user-description": "User you want to report", + "no-reason": "Not set", + "muterole-not-found": "Could not find muterole. Can not perform this action", + "quarantinerole-not-found": "Could not find quarantinerole. Can not perform this action", + "mute-audit-log-reason": "Got muted by %u because of \"%r\"", + "unmute-audit-log-reason": "Got unmuted by %u because of \"%r\"", + "quarantine-audit-log-reason": "Got quarantined by %u because of \"%r\"", + "kicked-audit-log-reason": "Got kicked by %u because of \"%r\"", + "banned-audit-log-reason": "Got banned by %u because of \"%r\"", + "channelmute-audit-log-reason": "Got channel-mutet by %u of \"%r\"", + "unchannelmute-audit-log-reason": "The Channel-Mute got removed by %u of \"%r\"", + "unbanned-audit-log-reason": "Got unbanned by %u because of \"%r\"", + "unquarantine-audit-log-reason": "Got unquarantined by %u because of \"%r\"", + "action-expired": "Action expired", + "auto-mod": "Auto-Mod", + "batch-role-remove-failed": "Could not remove all roles from %i (trying to remove roles one by one): %e", + "batch-role-add-failed": "Could not add all roles to %i (trying to remove roles one by one): %e", + "could-not-remove-role": "Could not remove role %r from %i: %e", + "could-not-add-role": "Could not add role %r to %i: %e", + "reason": "Reason", + "join-gate": "Join-Gate", + "expires-at": "Action expires on", + "action": "Action", + "case": "Case", + "victim": "Victim", + "missing-logchannel": "LogChannel could not be found", + "reached-warns": "Reached %w warns", + "restored-punishment-audit-log-reason": "Restored punishment", + "anti-join-raid": "ANIT-JOIN-RAID", + "raid-detected": "Raid detected", + "joingate-for-everyone": "Join-Gate-Modus: Catch all users", + "account-age-to-low": "Account creation age of %a days is to low (required are more then %c)", + "no-profile-picture": "Account has no profile picture (required)", + "join-gate-fail": "Account failed Join-Gate (%r)", + "blacklisted-word": "Posted blacklisted word in %c", + "invite-sent": "Sent invite in %c", + "scam-url-sent": "Sent scam-url in %c", + "anti-spam": "Anti-Spam", + "reached-messages-in-timeframe": "Reached %m (normal) messages in less than %t seconds", + "reached-duplicated-content-messages": "Reached %m messages with the same content in less than %t", + "reached-ping-messages": "Reached %m messages with (user) pings in less then %t seconds", + "reached-massping-messages": "Reached %m messages with mass pings in less than %t seconds", + "action-done": "Executed action successfully. Action-ID: #%i", + "expiring-action-done": "Done. Action will expire on %d. Action-ID: #%i", + "cleared-channel": "Cleared channel successfully.\nNote: Messages older than 14 days can not be deleted using this method.", + "clear-failed": "An error occurred. You can only delete 100 messages at once.", + "no-quarantine-action-found": "Sorry, but I couldn't find any records of quarantining this users.", + "locked-channel-successfully": "Locked channel successfully. Only moderators (and admins) can write messages here now.", + "unlocked-channel-successfully": "Unlocked channel successfully. Permissions got restored to the permission-state before the lock occurred.", + "unlock-audit-log-reason": "User %u unlocked this channel by running /moderate unlock", + "warning-not-found": "I could not find this warning. Please make sure you are actually using a warning-id and not a userid.", + "can-not-report-mod": "You can not report moderators.", + "action-description-format": "%reason\nby %u on %t", + "no-actions-title": "None found", + "no-actions-value": "No actions against %u found.", + "actions-embed-title": "Mod-Actions against %u - Site %i", + "actions-embed-description": "You can find every action against %u here.", + "report-embed-title": "New report", + "report-embed-description": "A user reported another user. Please review the case and take actions if needed.", + "reported-user": "Reported user", + "report-reason": "Reason for the report", + "report-user": "User who submitted report", + "message-log": "Last 100 messages", + "message-log-description": "You can find an encrypted message-log [here](%u).", + "channel": "Channel", + "no-report-pings": "No pings configured. Check your configuration to ping your staff.", + "not-allowed-to-see-own-notes": "Sorry, but you are not allowed to see your own notes.", + "note-added": "Note added successfully", + "note-edited": "Edited note successfully", + "note-deleted": "Note deleted successfully", + "note-not-found-or-no-permissions": "Note not found or no permissions to edit this note.", + "notes-embed-title": "Notes about %u", + "info-field-title": "ℹ️ Information", + "no-notes-found": "No notes about this user. Create a new note with `/moderate notes create` and set the notes attribute.", + "more-notes": "%x other moderator also added notes about this user. Notes are sorted in reverse chronology, so you will see the newest notes first.", + "user-notes-field-title": "%t's notes", + "user-not-on-server": "I can't perform this action on this user, as they are not currently on your server.", + "verification": "VERIFICATION", + "verification-failed": "Verification failed", + "verification-started": "Verification got started", + "verification-completed": "Verification completed", + "user": "User", + "manual-verification-needed": "Manual verification needed", + "verification-deny": "Deny verification", + "verification-approve": "Approve verification", + "verification-skip": "Skip verification", + "captcha-verification-pending": "Captcha-Verification is pending. You can either wait for the user to complete it or skip it manually.", + "verification-update-proceeded": "Successfully update verification status", + "verify-channel-set-but-not-found-or-wrong-type": "The configured verify-channel could not be found or it's type is not supported.", + "generating-message": "We are preparing some stuff, this message should get edited shortly...", + "restart-verification-button": "Restart verification process", + "member-not-found": "This user could not be found, maybe they already left?", + "already-verified": "Seems like you are already verified... Why would you want to repeat this process?", + "restarted-verification": "I have send you another DM about your verification prozess. Please read it carefully and follow the actions described in it. Please not that this action did not re-trigger the manual-verification (if enabled), so spamming this button is useless.", + "dms-still-disabled": "It seems like your DMs are still disabled. Please enable your DMs to start the verification. This is not optional, you need to do this in order to get access to %g.", + "dms-not-enabled-ping": "%p, it seems like you have your DMs disabled. Please enable them and hit the button below this message to verify yourself. You have two minutes to complete this process." + }, + "counter": { + "created-db-entry": "Initialized database entry for %i", + "not-a-number": "This is not a number. You can not chat here. Try creating a thread if your message is that important.", + "restriction-audit-log": "This user proceeded to abuse the counter channel after five warnings, so we locked them out.", + "only-one-message-per-person": "Users have to take turns counting: You can not count two times in a row.", + "not-the-next-number": "That's not the next number. The next number is **%n**, please make sure you are counting up one by one.", + "channel-topic-change-reason": "Someone counted, so we updated the description as required by the configuration" + }, + "tickets": { + "channel-not-found": "Ticket-Create-Channel could not be found", + "existing-ticket": "You already have a ticket open: %c", + "ticket-created-audit-log": "%u created a new ticket by clicking the button", + "ticket-created": "Successfully created ticket and notified staff. Head over to it: %c", + "no-admin-pings": "No pings configured. Check your configuration to ping your staff.", + "ticket-closed-successfully": "Closed ticket successfully. This channel will be deleted in a few seconds, thanks for reaching out to our support.", + "ticket-closed-audit-log": "%u closed ticket", + "closing-ticket": "Closing ticket as requested by %u...", + "ticket-with-user": "👤 Ticket-User", + "could-not-dm": "Could not DM %u: %r", + "no-log-channel": "Log-Channel not found", + "ticket-log-embed-title": "📎 Ticket %i closed", + "ticket-log": "Ticket-Log", + "ticket-type": "☕ Ticket-Topic", + "ticket-log-value": "Transcript with %n messages can be found [here](%u).", + "closed-by": "👷 Ticket closed by" + }, + "custom-commands": { + "not-found": "This custom-command does not longer exist. It might have been deleted or deactivated.", + "parameter-not-set": "This parameter did not get specified", + "true": "True", + "no-roles-default": "⚠\uFE0F You do not have enough permissions to execute this custom command because you are missing the roles required to execute this command.", + "fix-no-reply": "**⚠️ This Custom-Command is not properly set up**\nTo fix this, add the \"Reply to message or interaction\"-Action to this Custom-Command and reload your configuration.", + "false": "False" + }, + "reminders": { + "command-description": "Set a reminder for yourself", + "in-description": "After what time should we remind you? (eg. \"2h 30m\")", + "what-description": "What should we remind you about?", + "dm-description": "Should we send you a DM instead of reminding your in this channel?", + "one-minute-in-future": "Your reminder needs to be at least one minute in the future", + "reminder-set": "Reminder set. We'll remind you at %d." + }, + "akinator": { + "command-description": "Let akinator guess a character/object/animal", + "type-description": "Select what akinator should guess (default: character)", + "character-name": "Character", + "object-name": "Object", + "animal-name": "Animal" + }, + "afk-system": { + "command-description": "Manage your AFK-Status on this guild", + "end-command-description": "End your current AFK-Session", + "start-command-description": "Start a new AFK-Session", + "reason-option-description": "Explain why you started this session", + "autoend-option-description": "If enabled, the bot will auto-end your AFK Session when your write a message (default: enabled)", + "no-running-session": "You don't have any session running.", + "already-running-session": "You already have an AFK-Session running, try ending it with `/afk-system end`.", + "afk-nickname-change-audit-log": "Updated user nickname because they started an AFK-Session", + "can-not-edit-nickname": "Can not edit nickname of %u: %e" + }, + "invite-tracking": { + "hook-installed": "Installed hook to receive more information about invites", + "log-channel-not-found-but-set": "Log-Channel %c not found, but it's set in your configuration.", + "new-member": "New member joined", + "member-leave": "Member left", + "invite-type": "Invite-Type", + "member": "Member", + "invite": "Invite", + "invite-code": "Invite-Code: [%c](%u)", + "invite-channel": "Channel: %c", + "expires-at": "Expires at: %t", + "created-at": "Created at: %t", + "inviter": "Invited by: %u (%a/%i active invites)", + "uses": "Uses: %u", + "createdAt": "Created at: %t", + "max-uses": "Max-Uses: %u", + "normal-invite": "Normal Invite", + "vanity-invite": "Vanity-Invite", + "missing-permissions": "I don't have enough permissions to determine the invite", + "unknown-invite": "Sorry, but I couldn't determine the invite this person used", + "joined-for-the-x-time": "%u joined this server %x times before this, the last one was %t.", + "revoke-invite": "Revoke this invite", + "invite-not-found": "This invite could not be found... Maybe it already got revoked?", + "invite-revoked": "Invite revoked successfully.", + "missing-revoke-permissions": "Sorry, but you can't revoke this invite: Missing `MANAGE_GUILD` permission.", + "invite-revoke-audit-log": "%u revoked this invite", + "invite-revoked-error": "Could not revoke invite %c: %e", + "trace-command-description": "Trace the invites of a user", + "argument-user-description": "User to trace invites from", + "invited-by": "Invited by", + "invited-users": "Invited users", + "inviter-not-found": "Could not determine who invited this user.", + "no-users-invited": "This user hasn't invited any other users.", + "and-x-more-users": "And %x more users", + "and-x-more-invites": "And %x more invites", + "created-invites": "Created invites", + "not-showing-left-users": "Invited users who left are not displayed here.", + "no-invites": "This user has create no invites", + "revoke-user-invite": "Revoke all user's invites", + "revoked-invites-successfully": "All invites from this user got revoked successfully" + }, + "tic-tac-toe": { + "command-description": "Play tic-tac-toe against someone in the chat", + "user-description": "User to play against", + "challenge-message": "%t, %u challenged you to a game of tic-tac-toe! Hit the button below to join the battle! This invitation will expire in about 2 minutes, to don't hesitate to much.", + "accept-invite": "Join game", + "deny-invite": "No thanks", + "self-invite-not-possible": "Are you really that lonely? Even Simon, a complete introvert with no friends and developer of this bot, can find another user to play tic-tac-toe with... You should be able to do that too, try inviting %r for example, maybe they want to play a round?", + "invite-expired": "Sorry, %u, %i didn't accept your request to play tic-tac-toe in time ):", + "invite-denied": "Sorry, %u, but %i denied your request to play a round of tic-tac-toe ):", + "you-are-not-the-invited-one": "Sorry, but this invite doesn't belong to you. You can start your own game with `/tic-tac-toe`.", + "playing-header": "**TIC-TAC-TOE GAME IS RUNNING**\n\n%u (🟢) VS %i (🟡)\nCurrently on turn: %t\n\n%t, click a button with a white circle below to place your marker", + "win-header": "**TIC-TOE-GAME ENDED**\n\n%u (🟢) VS %i (🟡)\n\n%w won the game - GG!\n\n*You can start a new round by using `/tic-tac-toe`*", + "draw-header": "**TIC-TOE-GAME ENDED**\n\n%u (🟢) VS %i (🟡)\n\nDraw - no one won this game.", + "not-your-turn": "It's not your turn, take a coffee and return later" + }, + "duel": { + "command-description": "Play duel against someone in the chat", + "user-description": "User to play against", + "challenge-message": "%t, %u challenged you to a game of duel! Hit the button below to join the battle! This invitation will expire in about 2 minutes, to don't hesitate to much.", + "accept-invite": "Join game", + "deny-invite": "No thanks", + "self-invite-not-possible": "Are you really that lonely? Even Simon, a complete introvert with no friends and developer of this bot, can find another user to play duel with... You should be able to do that too, try inviting %r for example, maybe they want to play a round?", + "invite-expired": "Sorry, %u, %i didn't accept your request to play duel in time ):", + "invite-denied": "Sorry, %u, but %i denied your request to play a round of duel ):", + "you-are-not-the-invited-one": "Sorry, but this invite doesn't belong to you. You can start your own game with `/duel`.", + "game-running-header": "🎮 Game running", + "what-do-you-want-to-do": "**Select your action!**", + "pending": "⏳ Waiting for selection…", + "ready": "✅ Ready", + "continues-info": "The game continues once both parties have selected their next action.", + "how-does-this-game-work": "Wondering how this game works? Read our short explanation [here]().", + "use-gun": "Use gun", + "guard": "Guard", + "reload": "Load gun", + "game-ended": "🎮 Game ended", + "no-bullets": "Sorry, but you haven't loaded any bullets yet, so you can't use your gun yet.", + "bullets-full": "Sorry, but your gun only has place for 5 bullets at a time.", + "gun-gun": "Both %g1 and %g1 draw their guns. They stare each other and their eyes and slowly lower their weapons. No, the duell won't be resolved if both die - there can only be one winner.", + "guard-gun": "%g1 draws their gun and shoot - %d1 dodged the bullet successfully.", + "guard-guard": "Both %d1 and %d2 wait for each other to fire the shot - but nothing happens.", + "reload-gun": "While %r1 starts reloading their gun, %g1 draws their weapon and shoots - it's a head-shot. %r1 drops to the ground. %g1 should celebrate because they won, but they are left feeling bad for murdering their old friend.", + "guard-over-reload-gun": "As this is %r1's fifth guard in a row, they are tired and are to slow - %g1 shoots them directly into their head and %r1 drops to the ground. It's a win for %g1 - but at what price?", + "reload-reload": "Both %r1 and %r2 stare each other in the eyes while taking a short break to load one bullet each in their chamber.", + "reload-guard": "%d1 prepares to doge a bullet - but %r1 uses the time to load their weapon - no shots get fired.", + "ended-state": "This game ended. You can start a new duel with `/duel`.", + "not-your-game": "You are not one of players - you can start a new game with `/duel`." + }, + "economy-system": { + "work-earned-money": "The user %u gained %m %c by working", + "crime-earned-money": "The user %u gained %m %c by committing a crime", + "message-drop-earned-money": "The user %u gained %m %c by getting a message drop", + "rob-earned-money": "The user %u gained %m %c by robbing from %v", + "weekly-earned-money": "The user %u gained %m %c by cashing in their weekly reward", + "daily-earned-money": "The user %u gained %m %c by cashing in their weekly reward", + "admin-self-abuse": "The admin %a wanted to abuse their permissions by giving them self even more money! This can't and should not be ignored!", + "admin-self-abuse-answer": "What a bad admin you are, %u. I'm disappointed with you! I need to report this. If I wish I could ban you!", + "added-money": "%i %c has been added to the balance of %u", + "removed-money": "%i %c has been removed from the balance of %u", + "set-money": "The balance of %u has been set to %i.", + "added-money-log": "The user %u added %i %c to the balance of %v", + "removed-money-log": "The user %u removed %i %c from the balance of %v", + "set-money-log": "The user %u set %v's balance to %i %c", + "command-description-main": "Use the economy-system", + "command-description-work": "Earn some cash by working", + "command-description-crime": "Earn some cash by committing a crime", + "command-description-rob": "Rob some cash from another user", + "option-description-rob-user": "User to rob from", + "crime-loose-money": "The user %u lost %m %c by committing a crime", + "command-description-daily": "Cash in your daily rewards", + "command-description-weekly": "Cash in your weekly rewards", + "command-description-balance": "Show the balance of a user", + "option-description-user": "User to execute action upon", + "command-description-add": "Add some cash to a user", + "command-description-remove": "Remove some cash from a user", + "option-description-amount": "Amount to manipulate", + "command-description-set": "Set a user's balance", + "option-description-balance": "Balance to set user to", + "message-drop": "Message-Drop: You earned %m %c simply by chatting!", + "created-item": "The user %u has created a new shop item: %i", + "item-duplicate": "The item already exist", + "role-to-high": "The specified role is higher than the highest role of the bot. Therefore the bot can't give the role to users. The item was **not** created.", + "delete-item": "The user %u has deleted the shop item %i", + "user-purchase": "The user %u has purchased the shop item %i for %p.", + "shop-command-description": "Use the shop-system", + "shop-command-description-add": "Create a new item in the shop (admins only)", + "shop-option-description-itemName": "Name of the item", + "shop-option-description-itemID": "ID of the Item", + "shop-option-description-price": "Price of the item", + "shop-option-description-role": "Role to give to users who buy the item", + "shop-command-description-buy": "Buy an item", + "shop-command-description-list": "List all items in the shop", + "shop-command-description-delete": "Remove an item from the shop", + "channel-not-found": "Can't find the leaderboard channel with the ID %c", + "command-description-deposit": "Deposit xyz to your bank", + "option-description-amount-deposit": "Amount to deposit", + "command-description-withdraw": "Withdraw xyz from your Bank", + "option-description-amount-withdraw": "Amount to withdraw", + "command-group-description-msg-drop-msg": "Enable/ Disable the Message-Drop-Message", + "command-description-msg-drop-msg-enable": "Enable the Message-Drop-Message", + "command-description-msg-drop-msg-disable": "Disable the Message-Drop-Message", + "command-description-destroy": "Destroy the whole economy (deletes all Database-Entries)", + "option-description-confirm": "Confirm, that you really want to destroy the whole economy", + "destroy-cancel-reply": "You're lucky. You stopped me in the last moment before I destroyed the economy", + "destroy-reply": "Ok... I'll destroy the whole economy", + "destroy": "%u destroyed the economy", + "migration-happening": "Database not up-to-date. Migrating database...", + "migration-done": "Migrated database successfully.", + "nothing-selected": "Select an item to buy it", + "select-menu-price": "Price: %p" + }, + "status-role": { + "fulfilled": "Status-role condition is fulfilled", + "not-fulfilled": "Status-role condition is no longer fulfilled" + }, + "color-me": { + "create-log-reason": "%user redeemed their boosting-rewards by requesting the creation of this role", + "edit-log-reason": "%user edited their boosting-reward-role", + "delete-unboost-log-reason": "%user stopped boosting, so their role got deleted", + "delete-manual-log-reason": "%user deleted their role manually", + "command-description": "Request a Custom role as a reward for boosting. This has a cooldown of 24 hours", + "manage-subcommand-description": "Create or edit your custom role", + "name-option-description": "The name of your custom role", + "color-option-description": "The color of your custom role", + "remove-subcommand-description": "Remove your custom role", + "icon-option-description": "Your role-icon", + "confirm-option-remove-description": "Do you really want to delete your custom role? This will not reset any running cooldowns" + }, + "rock-paper-scissors": { + "stone": "Stone", + "paper": "Paper", + "scissors": "Scissors", + "won": "won", + "lost": "lost", + "tie": "tie", + "play-again": "Play again", + "challenge-message": "%t, %u challenged you to a game of rock-paper-scissors! Hit the button below to join the game! This invitation will expire in about 2 minutes, so don't hesitate to much.", + "invite-expired": "Sorry, %u, %i didn't accept your request to play rock-paper-scissors in time ):", + "invite-denied": "Sorry, %u, but %i denied your request to play a round of rock-paper-scissors ):", + "rps-title": "Rock Paper Scissors", + "rps-description": "Choose your weapon!", + "its-a-tie-try-again": "It's a tie! Try again!", + "command-description": "Play rock-paper-scissors against the bot or someone in the chat" + }, + "connect-four": { + "tie": "It's a tie!", + "win": "%u has won the game!", + "not-turn": "Sorry, but it's not your turn!", + "game-message": "Connect Four game of %u1 and %u2\nCurrent turn: %c %t.\n\n%g", + "challenge-message": "%t, %u challenged you to a game of Connect Four! Hit the button below to join the game! This invitation will expire in about 2 minutes, so don't hesitate to much.", + "invite-expired": "Sorry, %u, %i didn't accept your request to play Connect Four in time ):", + "invite-denied": "Sorry, %u, but %i denied your request to play a round of Connect Four ):", + "command-description": "Play Connect Four against the bot or someone in the chat", + "field-size-description": "The size of the playfield (default: 7)", + "challenge-yourself": "You cannot challenge yourself!", + "challenge-bot": "You cannot challenge bots!" + }, + "uno": { + "command-description": "Play Uno against users in the chat", + "challenge-message": "%u invites to a round of Uno! Click the button below this message to join! The game starts %timestamp with %count players.", + "not-enough-players": "Not enough players joined for a round of Uno!", + "user-cards": "%u: %cards cards", + "already-joined": "You're already in!", + "view-deck": "View deck", + "draw": "Draw card", + "uno": "Uno!", + "turn": "It's %u turn!", + "update-button": "Update", + "use-drawn": "Do you want to use the drawn card?", + "dont-use-drawn": "Dont use", + "win": "%u won the game! %turns cards were played.", + "win-you": "You've won the game!", + "missing-uno": "⚠️️ You must use the Uno! button before you use your second last card!", + "choose-color": "Select a color:", + "pending-draws": "Use a Draw 2/4 card, otherwise you have to draw %count cards!", + "not-ingame": "You're not in this game!", + "skip": "Skip", + "reverse": "Reverse", + "color": "Color choice", + "draw2": "Draw 2", + "colordraw4": "Color choice and draw 4", + "cant-uno": "You cannot use Uno currently.", + "done-uno": "You've called Uno!", + "auto-drawn-skip": "Your turn was skipped because you would have had to draw the cards anyway.", + "start-game": "Start game now", + "not-host": "You're not the host of the game!", + "max-players": "The game is full!", + "previous-cards": "Previous cards: ", + "used-card": "You've already used the card %c! Use the Update button and play a valid card.", + "invalid-card": "You cannot play the card %c right now! Please select a valid card.", + "inactive-warn": "%u, it's your turn in the uno game!", + "inactive-win": "The uno game has ended. %u won as all others have been eliminated!" + }, + "quiz": { + "what-have-i-voted": "What have I voted?", + "vote": "Vote!", + "vote-this": "Select this option if you think it's correct.", + "voted-successfully": "Selected successfully.", + "not-voted-yet": "You have not selected an option yet, so I cant show you what you selected?", + "you-voted": "You've selected **%o** as correct answer.", + "change-opinion": "You can change your opinion at any time by selecting another option above the button you just clicked.", + "cannot-change-opinion": "You cannot change your selection as the creator of this quiz disabled it.", + "select-correct": "Select all correct answers", + "this-correct": "Mark this answer as correct", + "cmd-description": "Create or play server quiz", + "cmd-create-normal-description": "Create a quiz with up to 10 answers", + "cmd-create-bool-description": "Create a quiz with true or false answers", + "cmd-play-description": "Play a server quiz", + "cmd-leaderboard-description": "Shows the quiz leaderboard of the server", + "cmd-create-description-description": "Title / description of the quiz", + "cmd-create-channel-description": "Channel in which the quiz should be created", + "cmd-create-endAt-description": "How long the quiz will last", + "cmd-create-option-description": "Option number %o", + "cmd-create-canchange-description": "If the players can change their opinion after voting (default: no)", + "daily-quiz-limit": "You've reached the limit of **%l** daily playable quizzes. You can play again %timestamp.", + "created": "Quiz created successfully in %c.", + "correct-highlighted": "All correct answers were highlighted.", + "answer-correct": "✅ Your answer was correct and you've received one point for the leaderboard!", + "answer-wrong": "❌ Your answer was wrong!", + "bool-true": "Statement is correct", + "bool-false": "Statement is wrong", + "leaderboard-channel-not-found": "The leaderboard channel couldn't be found or it's type is invalid.", + "leaderboard-notation": "**%p. %u**: %xp XP", + "your-rank": "You've collected **%xp** points in quiz!", + "no-rank": "You've never finished a quiz successfully!", + "no-quiz": "No quizzes have been created for this server. Trusted admins can create them on https://scnx.app/glink?page=bot/configuration?query=quiz&file=quiz%7Cconfigs%2FquizList .", + "no-permission": "You don't have enough permissions to create quiz using the command." + }, + "starboard": { + "invalid-minstars": "Invalid minimum stars %stars", + "star-limit": "You've reached the hourly starboard limit of %limitEmoji on the server which is why you cannot react on the message %msgUrl .\nTry again %time!" + }, + "nicknames": { + "owner-cannot-be-renamed": "The owner of the server (%u) cannot be renamed.", + "nickname-error": "An error occurred while trying to change the nickname of %u: %e" + } +} \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 00000000..9ca9b0e8 --- /dev/null +++ b/main.js @@ -0,0 +1,520 @@ +const Discord = require('discord.js'); +const client = new Discord.Client({ + partials: ['MESSAGE', 'GUILD_MEMBER', 'GUILD_SCHEDULED_EVENT', 'MESSAGE', 'REACTION', 'USER', 'CHANNEL'], // Most of these are not needed, but enabling them does not increase CPU / RAM usage and does not introduce problems, as we handle them in the event emitter system + allowedMentions: {parse: ['users', 'roles']}, // Disables @everyone mentions because everyone hates them + intents: [Discord.Intents.FLAGS.GUILDS, 'GUILD_BANS', 'DIRECT_MESSAGES', 'GUILD_MESSAGES', 'MESSAGE_CONTENT', 'GUILD_VOICE_STATES', 'GUILD_PRESENCES', 'GUILD_INVITES', 'GUILD_EMOJIS_AND_STICKERS', 'GUILD_MESSAGE_REACTIONS', 'GUILD_EMOJIS_AND_STICKERS', 'GUILD_MEMBERS', 'GUILD_WEBHOOKS'] +}); +client.intervals = []; +client.jobs = []; +const fs = require('fs'); +const {Sequelize} = require('sequelize'); +const log4js = require('log4js'); +const jsonfile = require('jsonfile'); +const readline = require('readline'); + +// Parsing parameters +let config; +let confDir = `${__dirname}/config`; +let dataDir = `${__dirname}/data`; +const args = process.argv.slice(2); +let scnxSetup = false; // If enabled some other (closed-sourced) files get imported and executed +if (process.argv.includes('--scnx-enabled')) scnxSetup = true; +client.scnxSetup = scnxSetup; +if (args[0] === '--help' || args[0] === '-h') { + process.exit(); +} +if (args[0] && args[1]) { + confDir = args[0]; + dataDir = args[1]; +} +client.locale = process.argv.find(a => a.startsWith('--lang')) ? (process.argv.find(a => a.startsWith('--lang')).split('--lang=')[1] || 'de') : 'en'; +module.exports.client = client; +log4js.configure({ + pm2: process.argv.includes('--pm2-setup'), + appenders: { + out: { + type: 'logLevelFilter', + appender: 'output', + maxLevel: 'error', + level: 'debug' + }, + output: { + type: 'stdout', layout: { + type: 'pattern', + pattern: '[%p] %m' + } + }, + err: { + type: 'logLevelFilter', + appender: 'erroutput', + level: 'error' + }, + erroutput: { + type: 'stderr', layout: { + type: 'pattern', + pattern: '[%p] %m' + } + } + }, + categories: { + default: {appenders: ['out', 'err'], level: 'debug'} + } +}); +const logger = log4js.getLogger(); +logger.level = scnxSetup ? 'debug' : (process.env.LOGLEVEL || 'debug'); + +// Loading config +try { + config = jsonfile.readFileSync(`${confDir}/config.json`); +} catch (e) { + logger.fatal('Missing config.json! Run "npm run generate-config " (Parameter ConfDir is optional) to generate it'); + process.exit(1); +} + +const models = {}; // Object with all models + +client.modules = {}; +client.guildID = config['guildID']; +client.config = config; +client.configDir = confDir; +client.dataDir = dataDir; +client.configurations = {}; +logger.level = config.logLevel || process.env.LOGLEVEL || 'debug'; +client.logger = logger; +module.exports.logger = logger; +const configChecker = require('./src/functions/configuration'); +const {compareArrays, checkForUpdates, formatDiscordUserName} = require('./src/functions/helpers'); +const {localize} = require('./src/functions/localize'); +logger.info(localize('main', 'startup-info', {l: logger.level})); + +let moduleConf = {}; +try { + moduleConf = jsonfile.readFileSync(`${confDir}/modules.json`); +} catch (e) { + logger.info(localize('main', 'missing-moduleconf')); +} + +// Connecting to Database +const db = new Sequelize({ + dialect: 'sqlite', + storage: `${dataDir}/database.sqlite`, + logging: false +}); + +const commands = []; + +async function startUp() { + if (config.timezone !== process.env.TZ) { + process.env.TZ = config.timezone; + logger.info(`Successfully set timezone to ${config.timezone}. The time is ${new Date().toLocaleString(client.locale)}.`); + } + if (scnxSetup) client.scnxHost = client.config.scnxHostOverwirde || 'https://scnx.app'; + await loadModelsInDir('/src/models'); + await loadModules(); + await loadEventsInDir('./src/events'); + await db.sync(); + logger.info(localize('main', 'sync-db')); + if (scnxSetup) await require('./src/functions/scnx-integration').beforeInit(client); + await client.login(config.token).catch(async (e) => { + if (e.code === 'TOKEN_INVALID') { + if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { + type: 'CORE_FAILURE', + errorDescription: 'invalid_token' + }); + logger.fatal(localize('main', 'login-error-token')); + } else if (e.code === 'DISALLOWED_INTENTS') { + if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { + type: 'CORE_FAILURE', + errorDescription: 'disallowed_intents' + }); + logger.fatal(localize('main', 'login-error-intents', {url: `https://discord.com/developers/applications/`})); + } else logger.fatal(localize('main', 'login-error', {e})); + process.exit(); + }); + if ((await client.application.fetch()).botRequireCodeGrant) { + if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { + type: 'CORE_ISSUE', + errorDescription: 'require_code_grant_active', + errorData: {settingsURL: `https://discord.com/developers/applications/${client.user.id}/bot`} + }); + logger.error(localize('main', 'require-code-grant-active', {d: `https://discord.com/developers/applications/${client.user.id}/bot`})); + } + client.guild = await client.guilds.fetch(config.guildID).catch(() => { + }); + if (!client.guild) { + if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { + type: 'CORE_FAILURE', + errorDescription: 'bot_not_on_guild', + errorData: {inviteURL: `https://discord.com/oauth2/authorize?client_id=${client.user.id}&guild_id=${config.guildID}&disable_guild_select=true&permissions=8&scope=bot%20applications.commands`} + }); + logger.error(localize('main', 'not-invited', {inv: `https://discord.com/oauth2/authorize?client_id=${client.user.id}&guild_id=${config.guildID}&disable_guild_select=true&permissions=8&scope=bot%20applications.commands`})); + if (scnxSetup) { + console.log('Waiting for being added to server…'); + client.once('guildCreate', () => startUp()); + return; + } else process.exit(1); + } + logger.info(localize('main', 'logged-in', {tag: formatDiscordUserName(client.user)})); + loadCLIFile('/src/cli.js'); + client.models = models; + client.moduleConf = moduleConf; + client.logChannel = await client.channels.fetch(config.logChannelID).catch(() => { + }); + if (!client.logChannel || client.logChannel.type !== 'GUILD_TEXT') { + logger.warn(localize('main', 'logchannel-wrong-type')); + client.logChannel = null; + if (scnxSetup) { + const {reportIssue} = require('./src/functions/scnx-integration'); + await reportIssue(client, { + type: 'CORE_FAILURE', + errorDescription: 'log_channel_not_set_or_wrong_type' + }); + } + } + await configChecker.loadAllConfigs(client).catch(async (e) => { + if (client.logChannel) await client.logChannel.send('⚠️ ' + localize('main', 'config-check-failed')); + console.log(e); + logger.fatal(localize('main', 'config-check-failed')); + process.exit(1); + }); + await loadCommandsInDir('./src/commands'); + if (client.scnxSetup) { + try { + client.config.customCommands = jsonfile.readFileSync(`${client.configDir}/custom-commands.json`); + } catch (e) { + client.config.customCommands = []; + } + require('./src/functions/scnx-integration').verifyCustomCommands(client); + } + await syncCommandsIfNeeded(); + client.commands = commands; + client.strings = jsonfile.readFileSync(`${confDir}/strings.json`); + client.botReadyAt = new Date(); + client.emit('botReady'); + if (scnxSetup) await require('./src/functions/scnx-integration').init(client); + logger.info(localize('main', 'bot-ready')); + if (client.logChannel) client.logChannel.send('🚀 ' + localize('main', 'bot-ready')); + await checkForUpdates(client); +} + +// Starting bot +db.authenticate().then(startUp); + +// CLI-COMMANDS +const cliCommands = []; +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); +rl.on('line', (input) => { + if (!client.botReadyAt) { + return console.error('The bot is not ready yet. Please wait until the bot gets ready to use the cli.'); + } + const command = cliCommands.find(c => c.command === input.split(' ')[0].toLowerCase()); + if (!command) return console.error(`Command "${command}" not found. See all commands with "help".`); + if (command.module && !(client.modules[command.module] || {}).enabled) return console.error(`${command.command} belongs to the module ${command.module}, which is disabled. Enable the module in modules.json and reload the configuration to use this command.`); + if (!command) return console.error('Command not found. Use "help" to see all available commands.'); + + console.log('\n'); + command.run({ + input, + args: input.split(' '), + client, + cliCommands + }); +}); + +/** + * Syncs commands if needed + * @returns {Promise} + */ +async function syncCommandsIfNeeded() { + const enabledCommands = commands.filter(c => { + if (!c.module) return true; + return client.modules[c.module].enabled; + }); + + /** + * Handels a sync failure + * @param e Exception + * @returns {Promise} + */ + async function handleSyncFailure(e) { + logger.debug(e); + if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { + type: 'CORE_FAILURE', + errorDescription: 'commands_sync_failed', + errorData: {inviteURL: `https://discord.com/oauth2/authorize?client_id=${client.user.id}&guild_id=${config.guildID}&disable_guild_select=true&permissions=8&scope=bot%20applications.commands`} + }); + logger.fatal(localize('main', 'no-command-permissions', {inv: `https://discord.com/oauth2/authorize?client_id=${client.user.id}&guild_id=${config.guildID}&disable_guild_select=true&permissions=8&scope=bot%20applications.commands`})); + process.exit(1); + } + + const oldGuildCommands = await (await client.guilds.fetch(config.guildID)).commands.fetch().catch(handleSyncFailure); + const oldGlobalCommands = await client.application.commands.fetch().catch(handleSyncFailure); + const ranCommands = []; // Commands with all functions run + for (const orgCmd of enabledCommands) { + const command = {...orgCmd}; + if (typeof command.options === 'function') command.options = await command.options(client); + if (command.options) { + const options = []; + for (const option of command.options) { + if (option.options && typeof option.options === 'function') option.options = await option.options(client); + options.push(option); + } + command.options = options; + } + ranCommands.push(command); + } + + /** + * Checks if two application commands need to be synced + * @param {ApplicationCommands} oldCommands Currently synced commands + * @param {ApplicationCommands} commandsToCheck New synced commands + * @returns {boolean} Returns true if syncronisation is needed + */ + function commandsNeedSync(oldCommands, commandsToCheck) { + let needSync = false; + if (oldCommands.size !== commandsToCheck.length) needSync = true; + if (!needSync) for (const command of commandsToCheck) { + const oldCommand = oldCommands.find(c => c.name === command.name); + if (!oldCommand) { + needSync = true; + break; + } + + if (oldCommand.description !== command.description || (oldCommand.options || []).length !== (command.options || []).length) { + needSync = true; + break; + } + + if (oldCommand.defaultMemberPermissions) oldCommand.defaultMemberPermissions = oldCommand.defaultMemberPermissions.toArray(); + if ((command.defaultMemberPermissions || []).length !== (oldCommand.defaultMemberPermissions || []).length) { + needSync = true; + break; + } + for (const permission of (command.defaultMemberPermissions || [])) { + if (!(oldCommand.defaultMemberPermissions || []).includes(permission)) { + needSync = true; + break; + } + } + + for (const option of (command.options || [])) { + const oldOptionOption = (oldCommand.options || []).find(o => o.name === option.name); + if (!oldOptionOption) { + needSync = true; + break; + } + if (checkOption(oldOptionOption, option)) { + needSync = true; + break; + } + } + + /** + * Checks if two command options are identical + * @private + * @param {Object} oldOption Old options + * @param {Object} newOption New options + * @returns {Boolean} If synchronisation is needed + */ + function checkOption(oldOption, newOption) { + if (oldOption.name !== newOption.name || oldOption.autocomplete !== newOption.autocomplete || oldOption.description !== newOption.description || oldOption.type !== newOption.type || (typeof oldOption.required === 'undefined' ? false : oldOption.required) !== (typeof newOption.required === 'undefined' ? false : newOption.required)) return true; + if (!compareArrays(oldOption.choices || [], newOption.choices || [])) return true; + if ((oldOption.options || []).length !== (newOption.options || []).length) return true; + for (const option of (newOption.options || [])) { + const oldOptionOption = (oldOption.options || []).find(o => o.name === option.name); + if (!oldOptionOption) return true; + if (checkOption(oldOptionOption, option)) return true; + } + return false; + } + } + return needSync; + } + + let guildCommands = config.syncCommandGlobally ? [] : ranCommands; + const globalCommands = config.syncCommandGlobally ? ranCommands : []; + if (scnxSetup) guildCommands = [...guildCommands, ...await require('./src/functions/scnx-integration').generateCustomSlashCommands(client, guildCommands)]; + if (commandsNeedSync(oldGuildCommands, guildCommands)) { + await client.application.commands.set(guildCommands, config.guildID).catch(handleSyncFailure); + logger.info(localize('main', 'guild-command-sync')); + } else logger.info(localize('main', 'guild-command-no-sync-required')); + if (commandsNeedSync(oldGlobalCommands, globalCommands)) { + await client.application.commands.set(globalCommands, null).catch(handleSyncFailure); + logger.info(localize('main', 'global-command-sync')); + } else logger.info(localize('main', 'global-command-no-sync-required')); +} + +module.exports.syncCommandsIfNeeded = syncCommandsIfNeeded; + +/** + * Load every database model in a directory + * @param {String} dir Directory to load models from + * @param {String} moduleName Name of module currently loading from + * @returns {Promise} + * @private + */ +async function loadModelsInDir(dir, moduleName = null) { + return new Promise(async resolve => { + await fs.readdir(`${__dirname}/${dir}`, (async (err, files) => { + if (err) { + logger.fatal(err); + process.exit(1); + } + for await (const file of files) { + const model = require(`${__dirname}/${dir}/${file}`); + await model.init(db); + if (moduleName) { + if (!models[moduleName]) models[moduleName] = {}; + models[moduleName][model.config.name] = model; + } else models[model.config.name] = model; + logger.debug(localize('main', 'model-loaded', {d: dir, f: file})); + } + resolve(); + })); + }); +} + + +const events = {}; + +/** + * Load all events from a directory + * @param {String} dir Directory to load events from + * @param {String} moduleName Name of module currently loading from + * @returns {Promise} + * @private + */ +async function loadEventsInDir(dir, moduleName = null) { + fs.readdir(`${__dirname}/${dir}`, (err, files) => { + if (err) return logger.error(err); + files.forEach(f => { + fs.lstat(`${__dirname}/${dir}/${f}`, async (err, stats) => { + if (!stats) return; + if (stats.isFile()) { + const eventFunction = require(`${__dirname}/${dir}/${f}`); + const eventName = f.split('.')[0]; + if (moduleName) { + if (client.modules[moduleName]) { + if (!client.modules[moduleName]['events']) client.modules[moduleName]['events'] = []; + client.modules[moduleName]['events'].push(f.split('.js').join('')); + } + } + if (!events[eventName]) { + events[eventName] = []; + client.on(eventName, (...cArgs) => { + for (const eData of events[eventName]) { + try { + if (!client.botReadyAt && !eData.eventFunction.ignoreBotReadyCheck) continue; + if (!eData.eventFunction.allowPartial && cArgs.filter(f => f && f.partial).length !== 0) continue; + if (!eData.moduleName) return eData.eventFunction.run(client, ...cArgs); + if (client.modules[eData.moduleName].enabled) eData.eventFunction.run(client, ...cArgs); + } catch (e) { + if (client.captureException) client.captureException(e, { + module: eData.moduleName, + event: eventName + }); + client.logger.error(`Error on event ${(eData.moduleName ? eData.moduleName + '/' : '') + eventName}: ${e}`); + } + } + }); + } + events[eventName].push({eventFunction, moduleName}); + logger.debug(localize('main', 'event-loaded', {d: dir, f: f})); + } else { + logger.debug(localize('main', 'event-dir', {d: dir, f: f})); + await loadEventsInDir(`${dir}/${f}/`); + } + }); + }); + }); +} + +/** + * Load a CLI-File + * @private + * @param {String} path Path to the CLI-File + * @param {String} moduleName Name of the module + * @returns {void} + */ +function loadCLIFile(path, moduleName = null) { + const file = require(`${__dirname}/${path}`); + for (const command of file.commands) { + command.originalName = command.command; + command.module = moduleName; + cliCommands.push(command); + command.command = command.command.toLowerCase(); + logger.debug(localize('main', 'loaded-cli', {c: command.command, p: path})); + } +} + +/** + * Load every command in a directory + * @param {String} dir Directory to load commands from + * @param {String} moduleName Name of module currently loading from + * @returns {Promise} + * @private + */ +async function loadCommandsInDir(dir, moduleName = null) { + const files = fs.readdirSync(`${__dirname}/${dir}`); + for (const f of files) { + const stats = fs.lstatSync(`${__dirname}/${dir}/${f}`); + if (!stats) return logger.error('No stats returned'); + if (stats.isFile()) { + const props = require(`${__dirname}/${dir}/${f}`); + commands.push({ + name: props.config.name, + description: props.config.description, + restricted: props.config.restricted, + defaultMemberPermissions: props.config.defaultMemberPermissions || null, + options: props.config.options || [], + subcommands: props.subcommands, + beforeSubcommand: props.beforeSubcommand, + run: props.run, + autoComplete: props.autoComplete, + module: moduleName + }); + } + } +} + +/** + * Load all modules + * @returns {Promise} + */ +async function loadModules() { + if (!fs.existsSync(`${__dirname}/modules/`)) fs.mkdirSync(`${__dirname}/modules/`); + const files = fs.readdirSync(`${__dirname}/modules/`); + const missingModules = []; + for (const f of files) { + logger.debug(localize('main', 'loading-module', {m: f})); + const moduleConfig = jsonfile.readFileSync(`${__dirname}/modules/${f}/module.json`); + if (moduleConfig.hidden) { + logger.debug(localize('main', 'hidden-module', {m: f})); + continue; + } + client.modules[f] = {}; + if (typeof moduleConf[f] === 'undefined') { + missingModules.push(f); + } + client.modules[f].enabled = !!moduleConf[f]; + client.modules[f].userEnabled = !!moduleConf[f]; + client.modules[f].config = moduleConfig; + client.configurations[f] = {}; + if (moduleConfig['models-dir']) await loadModelsInDir(`./modules/${f}${moduleConfig['models-dir']}`, f); + if (moduleConfig['commands-dir']) await loadCommandsInDir(`./modules/${f}${moduleConfig['commands-dir']}`, f); + if (moduleConfig['events-dir']) await loadEventsInDir(`./modules/${f}${moduleConfig['events-dir']}`, f); + if (moduleConfig['on-load-event']) require(`./modules/${f}/${moduleConfig['on-load-event']}`).onLoad(client); + if (moduleConfig['cli']) loadCLIFile(`./modules/${f}/${moduleConfig['cli']}`, f); + } + if (missingModules.length !== 0) { + logger.info(localize('config', 'moduleconf-regeneration')); + for (const moduleName of missingModules) { + moduleConf[moduleName] = false; + } + jsonfile.writeFileSync(`${confDir}/modules.json`, moduleConf, {spaces: 2}); + logger.info(localize('config', 'moduleconf-regeneration-success')); + } +} \ No newline at end of file diff --git a/modules/admin-tools/commands/admin.js b/modules/admin-tools/commands/admin.js new file mode 100644 index 00000000..ae138fd4 --- /dev/null +++ b/modules/admin-tools/commands/admin.js @@ -0,0 +1,111 @@ +const {localize} = require('../../../src/functions/localize'); + +module.exports.subcommands = { + 'movechannel': async function (interaction) { + const channel = interaction.options.getChannel('channel', true); + if (!interaction.options.get('new-position')) return interaction.reply({ + content: localize('admin-tools', 'position', {i: channel.toString(), p: channel.position}), + ephemeral: true + }); + await channel.setPosition(interaction.options.getInteger('new-position')); + await interaction.reply({ + content: localize('admin-tools', 'position-changed', {i: channel.toString(), p: channel.position}), + ephemeral: true + }); + }, + 'moverole': async function (interaction) { + const role = interaction.options.getRole('role', true); + if (!interaction.options.get('new-position')) return interaction.reply({ + content: localize('admin-tools', 'position', {i: role.toString(), p: role.position}), + ephemeral: true + }); + await role.setPosition(interaction.options.getInteger('new-position')); + await interaction.reply({ + content: localize('admin-tools', 'position-changed', {i: role.toString(), p: role.position}), + ephemeral: true + }); + }, + 'setcategory': async function (interaction) { + const channel = interaction.options.getChannel('channel', true); + if (channel.type === 'GUILD_CATEGORY') return interaction.reply({ + content: '⚠️ ' + localize('admin-tools', 'category-can-not-have-category'), + ephemeral: true + }); + const category = interaction.options.getChannel('category', true); + if (category.type !== 'GUILD_CATEGORY') return interaction.reply({ + content: '⚠️ ' + localize('admin-tools', 'not-category'), + ephemeral: true + }); + await channel.setParent(category); + interaction.reply({ + ephemeral: true, + content: localize('admin-tools', 'changed-category', {cat: category.toString(), c: channel.toString()}) + }); + } +}; + +module.exports.config = { + name: 'admin', + description: localize('admin-tools', 'command-description'), + defaultMemberPermissions: ['ADMINISTRATOR'], + options: [ + { + type: 'SUB_COMMAND', + name: 'movechannel', + description: localize('admin-tools', 'movechannel-description'), + options: [ + { + type: 'CHANNEL', + required: true, + name: 'channel', + description: localize('admin-tools', 'channel-description') + }, + { + type: 'INTEGER', + required: false, + name: 'new-position', + description: localize('admin-tools', 'new-position-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'moverole', + description: localize('admin-tools', 'moverole-description'), + options: [ + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('admin-tools', 'role-description') + }, + { + type: 'INTEGER', + required: true, + name: 'new-position', + description: localize('admin-tools', 'new-position-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'setcategory', + description: localize('admin-tools', 'setcategory-description'), + options: [ + { + type: 'CHANNEL', + required: true, + name: 'channel', + channelTypes: ['GUILD_TEXT', 'GUILD_VOICE', 'GUILD_NEWS', 'GUILD_STAGE_VOICE'], + description: localize('admin-tools', 'channel-description') + }, + { + type: 'CHANNEL', + required: true, + name: 'category', + description: localize('admin-tools', 'category-description') + } + ] + } + ] +}; \ No newline at end of file diff --git a/modules/admin-tools/commands/stealemote.js b/modules/admin-tools/commands/stealemote.js new file mode 100644 index 00000000..2211c4fd --- /dev/null +++ b/modules/admin-tools/commands/stealemote.js @@ -0,0 +1,32 @@ +const {localize} = require('../../../src/functions/localize'); +const {formatDiscordUserName} = require('../../../src/functions/helpers'); + +module.exports.run = async function (interaction) { + const content = interaction.options.getString('emote', true); + let emote = content.replace('<', '').replace('>', ''); + emote = emote.split(':'); + if (!emote[2] || !emote[1]) return interaction.reply({ + content: '⚠️ ' + localize('admin-tools', 'emoji-too-much-data'), + ephemeral: true + }); + emote = await interaction.guild.emojis.create(`https://cdn.discordapp.com/emojis/${emote[2]}`, emote[1], {reason: `Emoji imported by ${formatDiscordUserName(interaction.user)}`}); + await interaction.reply({ + content: localize('admin-tools', 'emoji-import', {e: emote.toString()}), + ephemeral: true + }); +}; + +module.exports.config = { + name: 'stealemote', + defaultMemberPermissions: ['MANAGE_EMOJIS_AND_STICKERS'], + description: localize('admin-tools', 'stealemote-description'), + + options: [ + { + type: 'STRING', + name: 'emote', + description: localize('admin-tools', 'emote-description'), + required: true + } + ] +}; \ No newline at end of file diff --git a/modules/admin-tools/config.json b/modules/admin-tools/config.json new file mode 100644 index 00000000..9be995c6 --- /dev/null +++ b/modules/admin-tools/config.json @@ -0,0 +1,15 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "commandsWarnings": { + "normal": [ + "/admin", + "/stealemote" + ] + }, + "content": [] +} diff --git a/modules/admin-tools/module.json b/modules/admin-tools/module.json new file mode 100644 index 00000000..c2e2f30d --- /dev/null +++ b/modules/admin-tools/module.json @@ -0,0 +1,23 @@ +{ + "name": "admin-tools", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/admin-tools", + "commands-dir": "/commands", + "config-example-files": [ + "config.json" + ], + "tags": [ + "administration" + ], + "humanReadableName": { + "en": "Admin-Tools" + }, + "description": { + "en": "Simple tools for admins - move channels and roles via commands or copy an emoji from another server to your server.", + "de": "Einfache Tools für Admins, um Channel und Rollen per Command zu verschieben und Emojis zu leihen." + } +} \ No newline at end of file diff --git a/modules/afk-system/commands/afk.js b/modules/afk-system/commands/afk.js new file mode 100644 index 00000000..5608c656 --- /dev/null +++ b/modules/afk-system/commands/afk.js @@ -0,0 +1,86 @@ +const {localize} = require('../../../src/functions/localize'); +const {embedType, truncate, formatDiscordUserName} = require('../../../src/functions/helpers'); + +module.exports.subcommands = { + 'end': async function (interaction) { + const session = await interaction.client.models['afk-system']['AFKUser'].findOne({ + where: { + userID: interaction.user.id + } + }); + if (!session) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('afk-system', 'no-running-session') + }); + if (session.nickname) await interaction.member.setNickname(session.nickname, '[afk-system] ' + localize('afk-system', 'afk-nickname-change-audit-log')).catch(e => { + interaction.client.logger.warn(localize('afk-system', 'can-not-edit-nickname', { + e, + u: formatDiscordUserName(interaction.user) + })); + }); + else await interaction.member.setNickname(null, '[afk-system] ' + localize('afk-system', 'afk-nickname-change-audit-log')).catch(e => { + interaction.client.logger.warn(localize('afk-system', 'can-not-edit-nickname', { + e, + u: formatDiscordUserName(interaction.user) + })); + }); + await session.destroy(); + interaction.reply(embedType(interaction.client.configurations['afk-system']['config']['sessionEndedSuccessfully'], {}, {ephemeral: true})); + }, + 'start': async function(interaction) { + const session = await interaction.client.models['afk-system']['AFKUser'].findOne({ + where: { + userID: interaction.user.id + } + }); + if (session) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('afk-system', 'already-running-session') + }); + await interaction.client.models['afk-system']['AFKUser'].create({ + userID: interaction.user.id, + nickname: interaction.member.nickname, + afkMessage: interaction.options.getString('reason'), + autoEnd: typeof interaction.options.getBoolean('auto-end') === 'boolean' ? interaction.options.getBoolean('auto-end') : true + }); + await interaction.member.setNickname('[AFK] ' + truncate(interaction.member.nickname || interaction.user.username, 32 - 6)).catch(e => { + interaction.client.logger.warn(localize('afk-system', 'can-not-edit-nickname', { + e, + u: formatDiscordUserName(interaction.user) + })); + }); + interaction.reply(embedType(interaction.client.configurations['afk-system']['config']['sessionStartedSuccessfully'], {}, {ephemeral: true})); + } +}; + +module.exports.config = { + name: 'afk', + description: localize('afk-system', 'command-description'), + + options: [ + { + type: 'SUB_COMMAND', + name: 'end', + description: localize('afk-system', 'end-command-description') + }, + { + type: 'SUB_COMMAND', + name: 'start', + description: localize('afk-system', 'start-command-description'), + options: [ + { + type: 'STRING', + required: false, + name: 'reason', + description: localize('afk-system', 'reason-option-description') + }, + { + type: 'BOOLEAN', + required: false, + name: 'auto-end', + description: localize('afk-system', 'autoend-option-description') + } + ] + } + ] +}; \ No newline at end of file diff --git a/modules/afk-system/config.json b/modules/afk-system/config.json new file mode 100644 index 00000000..8245760a --- /dev/null +++ b/modules/afk-system/config.json @@ -0,0 +1,129 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "sessionEndedSuccessfully", + "humanName": { + "en": "AFK-Session ended successfully", + "de": "AFK-Sitzung erfolgreich beendet" + }, + "default": { + "en": "✅ Your AFK status has been removed. Welcome back!", + "de": "✅ Dein Status ist jetzt nicht mehr \"AFK\". Willkommen zurück!" + }, + "description": { + "en": "This message gets send if a user ended their AFK-session successfully.", + "de": "Diese Nachricht wird gesendet, wenn der Nutzer seine AFK-Sitzung erfolgreich beendet." + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "sessionStartedSuccessfully", + "humanName": { + "en": "AFK-Session started successfully", + "de": "AFK-Sitzung erfolgreich gestartet" + }, + "default": { + "en": "✅ Your status has been updated to AFK. If another member mentions you while your AFK, we're going to notify them about your status.", + "de": "✅ Dein Status wurde auf \"AFK\" aktualisiert. Wenn dich ein anderer Nutzer erwähnt, während du AFK bist, werden wir ihn über deinen Status informieren." + }, + "description": { + "de": "This message gets send if a user started their session successfully.", + "en": "Diese Nachricht wird Nutzern angezeigt, wenn sie ihren Status auf AFK wechseln." + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "afkUserWithReason", + "humanName": { + "en": "User is AFK with reason", + "de": "Nutzer ist mit Begründung AFK" + }, + "default": { + "en": "ℹ %user% is currently AFK and specified the following reason: \"%reason%\".", + "de": "ℹ %user% ist aktuell AFK und hat folgenden Grund angegeben: \"%reason%\"." + }, + "description": { + "en": "This message gets send if a pinged user is currently AFK with a previously specified reason.", + "de": "Diese Nachricht wird gesendet, wenn ein Nutzer erwähnt wird, der AFK ist und zuvor eine Begründung dafür angegeben hat." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "reason", + "description": { + "de": "Begründung für die Abwesenheit des Nutzers", + "en": "Reason for their absence" + } + }, + { + "name": "user", + "description": { + "de": "Erwähnung des AFK Nutzers", + "en": "Mention of the user who is AFK" + } + } + ] + }, + { + "name": "afkUserWithoutReason", + "humanName": { + "en": "User is AFK without reason", + "de": "Nutzer ist ohne Begründung AFK" + }, + "default": { + "en": "ℹ %user% is currently AFK.", + "de": "ℹ %user% ist aktuell AFK." + }, + "description": { + "en": "This message gets send if a pinged user is currently AFK without a previously specified reason.", + "de": "Diese Nachricht wird gesendet, wenn ein Nutzer erwähnt wird, der AFK ist und zuvor keine Begründung dafür angegeben hat." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "de": "Erwähnung des AFK Nutzers", + "en": "Mention of the user who is AFK" + } + } + ] + }, + { + "name": "autoEndMessage", + "humanName": { + "en": "AFK Session ended automatically", + "de": "AFK Sitzung automatisch beendet" + }, + "default": { + "en": "Welcome back \uD83D\uDC4B!\nYou are not longer AFK because you wrote a message. You can start a new session with `/afk start` and disable `auto-end` if you don't want your sessions to be ended automatically.", + "de": "Willkommen zurück \uD83D\uDC4B!\nDu bist nun nicht mehr AFK, da du eine Nachricht geschrieben hast. Um eine neue Sitzung zu starten gebe bitte `/afk start` ein; solltest du dieses Verhalten deaktivieren wollen, setze außerdem den `auto-end` Parameter." + }, + "description": { + "en": "This message gets send if a user who is AFK and hasn't disabled auto-ending their sessions posts a message on the server.", + "de": "Diese Nachricht wird verschickt, wenn ein Nutzer, der aktuell AFK ist und automatisches Beenden von Sitzungen nicht deaktiviert hat, eine Nachricht auf dem Server sendet." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Mention of the user who was AFK", + "de": "Erwähnung des Nutzers, der AFK war" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/afk-system/events/messageCreate.js b/modules/afk-system/events/messageCreate.js new file mode 100644 index 00000000..29a63fac --- /dev/null +++ b/modules/afk-system/events/messageCreate.js @@ -0,0 +1,43 @@ +const {embedType, formatDiscordUserName} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function(client, message) { + if (!message.guild) return; + if (message.author.bot) return; + if (!client.botReadyAt) return; + if (message.guild.id !== client.guildID) return; + if (message.content.startsWith(client.config.prefix)) return; + const userAFK = await client.models['afk-system']['AFKUser'].findOne({ + where: { + userID: message.author.id, + autoEnd: true + } + }); + if (userAFK) { + if (userAFK.nickname) await message.member.setNickname(userAFK.nickname, '[afk-system] ' + localize('afk-system', 'afk-nickname-change-audit-log')).catch(e => { + message.client.logger.warn(localize('afk-system', 'can-not-edit-nickname', { + e, + u: formatDiscordUserName(message.author) + })); + }); + else await message.member.setNickname(null, '[afk-system] ' + localize('afk-system', 'afk-nickname-change-audit-log')).catch(e => { + message.client.logger.warn(localize('afk-system', 'can-not-edit-nickname', { + e, + u: formatDiscordUserName(message.author) + })); + }); + await userAFK.destroy(); + await message.reply(embedType(client.configurations['afk-system']['config']['autoEndMessage'], {'%user%': message.author.toString()}, {allowedMentions: {parse: []}})); + } + for (const member of message.mentions.members.values()) { + if (member.id === message.author.id) continue; + const afkUser = await client.models['afk-system']['AFKUser'].findOne({ + where: { + userID: member.id + } + }); + if (!afkUser) continue; + if (afkUser.afkMessage) message.reply(embedType(client.configurations['afk-system']['config']['afkUserWithReason'], {'%reason%': afkUser.afkMessage, '%user%': member.toString()}, {allowedMentions: {parse: []}})); + else message.reply(embedType(client.configurations['afk-system']['config']['afkUserWithoutReason'], {'%user%': member.toString()}, {allowedMentions: {parse: []}})); + } +}; \ No newline at end of file diff --git a/modules/afk-system/models/User.js b/modules/afk-system/models/User.js new file mode 100644 index 00000000..19793829 --- /dev/null +++ b/modules/afk-system/models/User.js @@ -0,0 +1,27 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class AFKUser extends Model { + static init(sequelize) { + return super.init({ + userID: { + type: DataTypes.STRING, + primaryKey: true + }, + afkMessage: DataTypes.TEXT, + nickname: DataTypes.STRING, + autoEnd: { + type: DataTypes.BOOLEAN, + defaultValue: true + } + }, { + tableName: 'afk-system_AFKUserV2', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'AFKUser', + 'module': 'afk-system' +}; \ No newline at end of file diff --git a/modules/afk-system/module.json b/modules/afk-system/module.json new file mode 100644 index 00000000..aa1a47d9 --- /dev/null +++ b/modules/afk-system/module.json @@ -0,0 +1,25 @@ +{ + "name": "afk-system", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "commands-dir": "/commands", + "models-dir": "/models", + "events-dir": "/events", + "config-example-files": [ + "config.json" + ], + "tags": [ + "tools" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/afk-system", + "humanReadableName": { + "en": "AFK-System" + }, + "description": { + "en": "Allow users to set their AFK-Status and notify other users if they try to reach them", + "de": "Erlaubt es deinen Nutzern, ihren AFK-Status zu setzen und benachrichtigt andere Nutzer, wenn sie diesen versuchen zu erreichen" + } +} \ No newline at end of file diff --git a/modules/anti-ghostping/config.json b/modules/anti-ghostping/config.json new file mode 100644 index 00000000..b06f072e --- /dev/null +++ b/modules/anti-ghostping/config.json @@ -0,0 +1,83 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "awaitBotMessages", + "humanName": { + "de": "Botnachrichten abwarten", + "en": "Wait for Bot-Messages" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled, the bot will wait ~2 Seconds to make sure no bot like NQN deleted the messages and answered afterwards", + "de": "Wenn diese Option aktiviert ist, wird der Bot ~2 Sekunden warten, um sicherzustellen, dass kein Bot wie NQN die Nachricht gelöscht und danach geantwortet hat" + }, + "type": "boolean" + }, + { + "name": "ignoredChannels", + "humanName": { + "en": "Ignored Channels", + "de": "Ignorierte Channel" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "If a ghost ping gets send in one of these configured channels, the bot will not run anti-ghost-ping", + "de": "Wenn ein Ghost-Ping in einem dieser konfigurierten Channel gesendet wird, wird der Bot nicht anti-ghost-ping ausführen" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "youJustGotGhostPinged", + "humanName": { + "en": "Ghostping-Message", + "de": "Ghostping-Nachricht" + }, + "default": { + "en": "%mentions%,\nYou just got ghost-pinged by %authorMention% with the following message: \"%msgContent%\"", + "de": "%mentions%,\nDu wurdest gerade von %authorMention% mit folgender Nachricht geghost-pinged: \"%msgContent%\"" + }, + "description": { + "en": "This message gets send if a member pings another user and deletes the message afterwards", + "de": "Diese Nachricht wird gesendet, wenn ein Nutzer einen anderen Nutzer pingt und die Nachricht danach löscht" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "mentions", + "description": { + "en": "Mentions of every user that got pinged in the original message", + "de": "Erwähnung von jedem, in der Originalnachricht gepingten, Nutzer" + } + }, + { + "name": "authorMention", + "description": { + "en": "Mention of the original message-author.", + "de": "Erwähnung des Autors der Originalnachricht." + } + }, + { + "name": "msgContent", + "description": { + "en": "Content of the original message", + "de": "Inhalt der Originalnachricht" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/anti-ghostping/events/messageCreate.js b/modules/anti-ghostping/events/messageCreate.js new file mode 100644 index 00000000..271bdc1b --- /dev/null +++ b/modules/anti-ghostping/events/messageCreate.js @@ -0,0 +1,13 @@ +const msgsWithMention = {}; +module.exports.run = async function (client, msg) { + if (!client.botReadyAt) return; + if (!msg.guild) return; + if (msg.guild.id !== client.config.guildID) return; + const moduleConfig = client.configurations['anti-ghostping']['config']; + if (moduleConfig.ignoredChannels.includes(msg.channel.id)) return; + if (msg.mentions.members.filter(f => f.id !== msg.author.id && !f.user.bot).size !== 0) msgsWithMention[msg.id] = msg; + setTimeout(() => { + msgsWithMention[msg.id] = null; + }, 60000); +}; +module.exports.messageWithMentions = msgsWithMention; \ No newline at end of file diff --git a/modules/anti-ghostping/events/messageDelete.js b/modules/anti-ghostping/events/messageDelete.js new file mode 100644 index 00000000..d175233f --- /dev/null +++ b/modules/anti-ghostping/events/messageDelete.js @@ -0,0 +1,36 @@ +const {embedType} = require('../../../src/functions/helpers'); + +module.exports.run = async function (client, msg) { + if (!client.botReadyAt) return; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + const {messageWithMentions} = require(`${__dirname}/messageCreate.js`); + if (!messageWithMentions[msg.id]) return; + const moduleStrings = client.configurations['anti-ghostping']['config']; + if (messageWithMentions[msg.id].author.bot) return; + if (messageWithMentions[msg.id].guild.id !== client.config.guildID) return; + if (!moduleStrings.awaitBotMessages) return executeGhostPingMessage(); + setTimeout(async () => { + const messages = await msg.channel.messages.fetch({after: msg.id}); + if (messages.filter(m => m.author.bot).size !== 0) return; + await executeGhostPingMessage(); + }, 2000); + + /** + * Executes the ghostping message + * @private + * @return {Promise} + */ + async function executeGhostPingMessage() { + let mentionString = ''; + messageWithMentions[msg.id].mentions.members.filter(f => f.id !== messageWithMentions[msg.id].author.id && !f.user.bot).forEach(m => { + mentionString = mentionString + `<@${m.id}>, `; + }); + mentionString = mentionString.substring(0, mentionString.length - 2); + await msg.channel.send(embedType(moduleStrings.youJustGotGhostPinged, { + '%mentions%': mentionString, + '%msgContent%': messageWithMentions[msg.id].content, + '%authorMention%': messageWithMentions[msg.id].author.toString() + })); + } +}; \ No newline at end of file diff --git a/modules/anti-ghostping/module.json b/modules/anti-ghostping/module.json new file mode 100644 index 00000000..86ca6e5f --- /dev/null +++ b/modules/anti-ghostping/module.json @@ -0,0 +1,23 @@ +{ + "name": "anti-ghostping", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "events-dir": "/events", + "config-example-files": [ + "config.json" + ], + "tags": [ + "moderation" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/anti-ghostping", + "humanReadableName": { + "en": "Anti-Ghostping" + }, + "description": { + "en": "This module detects ghost-pings and sends a message if one occurs", + "de": "Dieses Modul erkennt automatisch Ghost-Pings und schickt eine Nachricht, wenn einer erkannt wird" + } +} \ No newline at end of file diff --git a/modules/auto-delete/channels.json b/modules/auto-delete/channels.json new file mode 100644 index 00000000..2bbc3530 --- /dev/null +++ b/modules/auto-delete/channels.json @@ -0,0 +1,78 @@ +{ + "description": { + "en": "Set up channels to delete text-messages from", + "de": "Stelle hier Text-Kanäle ein, aus welchen gelöscht werden soll" + }, + "humanName": { + "en": "Text-Channels", + "de": "Text-Kanäle" + }, + "filename": "channels.json", + "configElements": true, + "content": [ + { + "name": "channelID", + "humanName": { + "en": "Channel", + "de": "Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "The Channel you want messages to be deleted from.", + "de": "Wähle den Kanal aus, aus welchen Nachrichten automatisch gelöscht werden sollen." + }, + "type": "channelID", + "content": [ + "GUILD_TEXT", + "GUILD_NEWS" + ] + }, + { + "name": "timeout", + "humanName": { + "en": "Timeout", + "de": "Timeout" + }, + "default": { + "en": "5" + }, + "description": { + "en": "Timeout (in minutes) after which the messages in a channel will be deleted.", + "de": "Timeout (in Minuten), nachdem die Nachrichten in einem Kanal automatisch gelöscht werden sollen." + }, + "type": "integer" + }, + { + "name": "purgeOnStart", + "humanName": { + "en": "Purge On Start", + "de": "Kanal leeren beim Bot Start" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled every (excluding pinned) message (max 100) in this channel gets deleted when the bot starts.", + "de": "Wenn aktiviert, werden alle (außer angepinnte) Nachrichten (max 100) aus dem gewälten Kanal, beim Start des Bots, gelöscht." + }, + "type": "boolean" + }, + { + "name": "keepMessageCount", + "default": { + "en": 0 + }, + "humanName": { + "en": "Amount of messages to keep", + "de": "Anzahl von zu behaltenden Nachrichten" + }, + "type": "integer", + "description": { + "en": "Set up a number here to always have x messages in your channel left (newest messages are kept). The number has to below 50.", + "de": "Stelle hier eine Anzahl an Nachrichten ein, die auch nach einer Löschung in dem Kanal behalten werden sollen (neuere Nachrichten werden behalten). Die Zahl muss unter 50 liegen." + } + } + ] +} \ No newline at end of file diff --git a/modules/auto-delete/events/botReady.js b/modules/auto-delete/events/botReady.js new file mode 100644 index 00000000..9f9a4d5c --- /dev/null +++ b/modules/auto-delete/events/botReady.js @@ -0,0 +1,70 @@ +const {localize} = require('../../../src/functions/localize'); +module.exports.run = async function (client) { + const channels = client.configurations['auto-delete']['channels']; + const voiceChannels = client.configurations['auto-delete']['voice-channels']; + + const uniqueConfigChannels = findUniqueChannels(channels); + const uniqueConfigVoiceChannels = findUniqueChannels(voiceChannels); + + client.modules['auto-delete'].uniqueChannels = uniqueConfigChannels.filter((channel) => { + const channelConfigObject = uniqueConfigVoiceChannels.find((voiceChannel) => voiceChannel.channelID === channel.channelID); + return !channelConfigObject; + }); + + for (const channel of client.modules['auto-delete'].uniqueChannels) { + if (!channel.purgeOnStart) continue; + + const dcChannel = await client.channels.fetch(channel.channelID).catch(() => { + }); + if (!dcChannel) return client.logger.error(`[auto-delete] ${localize('auto-delete', 'could-not-fetch-channel', {c: channel.channelID})}`); + + const channelMessages = (await dcChannel.messages.fetch().catch(() => { + })).sort((a, b) => a.createdAt < b.createdAt ? 1 : -1); + if (!channelMessages) { + return client.logger.error(`[auto-delete] ${localize('auto-delete', 'could-not-fetch-messages', {c: channel.channelID})}`); + } + if (channelMessages.size === 0) continue; + + const idsToKeep = []; + if (parseInt(channel.keepMessageCount) !== 0) { + for (const message of channelMessages.values()) { + if (idsToKeep.length !== parseInt(channel.keepMessageCount)) idsToKeep.push(message.id); + } + } + dcChannel.bulkDelete(channelMessages.filter(m => !idsToKeep.includes(m.id) && !m.pinned && m.deletable), true); + } + + for (const voiceChannel of uniqueConfigVoiceChannels) { + if (!voiceChannel.purgeOnStart) continue; + + const dcVoiceChannel = await client.channels.fetch(voiceChannel.channelID).catch(() => { + }); + if (!dcVoiceChannel) return client.logger.error(`[auto-delete] ${localize('auto-delete', 'could-not-fetch-channel', {c: voiceChannel.channelID})}`); + if (dcVoiceChannel.members.size > 0) continue; + + const channelMessages = await dcVoiceChannel.messages.fetch().catch(() => { + }); + if (!channelMessages) { + return client.logger.error(`[auto-delete] ${localize('auto-delete', 'could-not-fetch-messages', {c: voiceChannel.channelID})}`); + } + if (channelMessages.size === 0) continue; + + dcVoiceChannel.bulkDelete(channelMessages, true); + } +}; + +/** + * Finds and deletes duplicates in Array (Last Writer wins) + * @param {String} arrayToFilter Array of Channels + * @returns {Array} Filtered Array of Channels + * @private + */ +function findUniqueChannels(arrayToFilter) { + const uniqueConfigChannelIds = {}; + + for (let i = 0; i < arrayToFilter.length; i++) { + uniqueConfigChannelIds[arrayToFilter[i].channelID] = i; + } + + return arrayToFilter.filter((channel, index) => uniqueConfigChannelIds[channel.channelID] === index); +} \ No newline at end of file diff --git a/modules/auto-delete/events/messageCreate.js b/modules/auto-delete/events/messageCreate.js new file mode 100644 index 00000000..aa81a592 --- /dev/null +++ b/modules/auto-delete/events/messageCreate.js @@ -0,0 +1,23 @@ +module.exports.run = async function (client, msg) { + if (!client.botReadyAt) return; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + if (!msg.member) return; + if (!client.modules['auto-delete'].uniqueChannels) return; + + const channel = client.modules['auto-delete'].uniqueChannels.find(c => c.channelID === msg.channel.id); + if (!channel) return; + setTimeout(async () => { + if (parseInt(channel.keepMessageCount) === 0) { + if (msg.deletable && !msg.pinned) msg.delete().catch(() => { + }); + return; + } + const oldMessages = (await msg.channel.messages.fetch({ + before: msg.id, + limit: parseInt(channel.keepMessageCount) + })).sort((a, b) => a.createdAt < b.createdAt ? 1 : -1); + if (oldMessages.length < parseInt(channel.keepMessageCount)) return; + if (oldMessages.last().deletable && !oldMessages.last().pinned) await oldMessages.last().delete(); + }, parseInt(channel.timeout) * 60000); +}; \ No newline at end of file diff --git a/modules/auto-delete/events/voiceStateUpdate.js b/modules/auto-delete/events/voiceStateUpdate.js new file mode 100644 index 00000000..c4616057 --- /dev/null +++ b/modules/auto-delete/events/voiceStateUpdate.js @@ -0,0 +1,29 @@ +const {localize} = require('../../../src/functions/localize'); +module.exports.run = async function (client, oldState) { + if (!client.botReadyAt) return; + + const voiceChannels = client.configurations['auto-delete']['voice-channels']; + + const channelConfigEntry = voiceChannels.find((vc) => oldState.channelId === vc.channelID); + if (!channelConfigEntry) return; + + const channel = await client.channels.fetch(channelConfigEntry.channelID).catch(() => { + }); + if (!channel) { + return client.logger.error(`[auto-delete] ${localize('auto-delete', 'could-not-fetch-channel', {c: channelConfigEntry.channelID})}`); + } + if (channel.type !== 'GUILD_VOICE') return; + if (channel.members.size > 0) return; + + const channelMessages = await channel.messages.fetch().catch(() => { + }); + if (!channelMessages) { + return client.logger.error(`[auto-delete] ${localize('auto-delete', 'could-not-fetch-messages', {c: channelConfigEntry.channelID})}`); + } + if (channelMessages.size === 0) return; + + setTimeout(async () => { + channel.bulkDelete(channelMessages, true).catch(() => { + }); + }, parseInt(channelConfigEntry.timeout) * 1000 * 60); +}; \ No newline at end of file diff --git a/modules/auto-delete/module.json b/modules/auto-delete/module.json new file mode 100644 index 00000000..8a0b73ef --- /dev/null +++ b/modules/auto-delete/module.json @@ -0,0 +1,25 @@ +{ + "name": "auto-delete", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/auto-delete", + "events-dir": "/events", + "config-example-files": [ + "channels.json", + "voice-channels.json" + ], + "tags": [ + "administration" + ], + "humanReadableName": { + "en": "Auto-Message-Delete", + "de": "Automatisches Löschen" + }, + "description": { + "en": "This module allows you to delete messages from a channel after a specified timeout to keep your channel clean", + "de": "Halte deine Channel sauber, in dem du alle Nachrichten nach einem bestimmten Intervall in einem Channel löschst" + } +} \ No newline at end of file diff --git a/modules/auto-delete/voice-channels.json b/modules/auto-delete/voice-channels.json new file mode 100644 index 00000000..1d116f33 --- /dev/null +++ b/modules/auto-delete/voice-channels.json @@ -0,0 +1,62 @@ +{ + "description": { + "en": "Set up voice-channels to delete messages from", + "de": "Stelle hier Sprach-Kanäle ein, aus welchen gelöscht werden soll" + }, + "humanName": { + "en": "Voice-Channels", + "de": "Sprach-Kanäle" + }, + "filename": "voice-channels.json", + "configElements": true, + "content": [ + { + "name": "channelID", + "humanName": { + "en": "Voice-Channel", + "de": "Sprachkanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "The Voice-Channel you want the auto-deleter to clear if there are no channel members left.", + "de": "Wähle den Sprachkanal aus, den der Bot leeren soll, sobald keine Mitglieder mehr im Sprachkanal sind." + }, + "type": "channelID", + "content": [ + "GUILD_VOICE" + ] + }, + { + "name": "timeout", + "humanName": { + "en": "Timeout", + "de": "Timeout" + }, + "default": { + "en": "5" + }, + "description": { + "en": "Timeout (in minutes) after which the messages in a Voice-Channel are deleted after the last member left the channel. Entering '0' will result in an instant deletion.", + "de": "Timeout (in Minuten), nachdem die Nachrichten gelöscht werden, wenn das letzte Mitglied den Sprachkanal verlassen hat. Wenn du eine '0' verwendest, werden die Nachrichten sofort gelöscht." + }, + "type": "integer" + }, + { + "name": "purgeOnStart", + "humanName": { + "en": "Purge On Start", + "de": "Kanal leeren beim Bot Start" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled every message (max 100) in this channel gets deleted when the bot starts and no members are left in the channel", + "de": "Wenn aktiviert, werden alle Nachrichten (max 100) aus dem gewälten Sprachkanal gelöscht (beim Start des Bots), sofern keine Mitglieder in dem Sprachkanal sind." + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/auto-messager/cronjob.json b/modules/auto-messager/cronjob.json new file mode 100644 index 00000000..6a9c020c --- /dev/null +++ b/modules/auto-messager/cronjob.json @@ -0,0 +1,76 @@ +{ + "description": { + "en": "Advanced users can unleash the full potential of automatic message with cronejobs", + "de": "Nur für fortgeschrittene Nutzer - mit cronjob's kannst hast du die volle Kontrolle über die Nachrichten" + }, + "elementLimits": { + "STARTER": 2, + "ACTIVE_GUILD": 5, + "PRO": 15 + }, + "humanName": { + "en": "Cronjob (advanced)", + "de": "Cronjobs (fortgeschritten)" + }, + "configElementName": { + "de": { + "one": "Automatische Nachricht", + "more": "Automatische Nachrichten" + }, + "en": { + "one": "Automatic message", + "more": "Automatic messages" + } + }, + "filename": "cronjob.json", + "configElements": true, + "content": [ + { + "name": "channelID", + "humanName": { + "de": "Kanal", + "en": "Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "ID of the channel in which the message should be send", + "de": "ID des Kanals, in welchen die Nachricht gesendet werden soll" + }, + "type": "channelID" + }, + { + "name": "message", + "humanName": { + "de": "Nachricht", + "en": "Message" + }, + "default": { + "en": "" + }, + "description": { + "en": "Message that should be send", + "de": "Nachricht, die gesendet werden soll" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "expression", + "humanName": { + "de": "Ausdruck", + "en": "Expression" + }, + "default": { + "en": "1 6 1-31 * *", + "de": "1 6 1-31 * *" + }, + "description": { + "en": "The message gets scheduled for this expression", + "de": "Die Nachricht wird für diesen Ausdruck geplant" + }, + "type": "string" + } + ] +} \ No newline at end of file diff --git a/modules/auto-messager/daily.json b/modules/auto-messager/daily.json new file mode 100644 index 00000000..e07a440d --- /dev/null +++ b/modules/auto-messager/daily.json @@ -0,0 +1,94 @@ +{ + "description": { + "en": "You can send on a daily basic here - this can be once a week or month", + "de": "Hier kannst du Nachrichten auf täglicher Basis versenden lassen - das kann auch nur einmal pro Woche oder Monat sein" + }, + "elementLimits": { + "STARTER": 2, + "ACTIVE_GUILD": 5, + "PRO": 15 + }, + "configElementName": { + "de": { + "one": "Automatische Nachricht", + "more": "Automatische Nachrichten" + }, + "en": { + "one": "Automatic message", + "more": "Automatic messages" + } + }, + "humanName": { + "en": "Daily Basic", + "de": "Tägliche Basis" + }, + "filename": "daily.json", + "configElements": true, + "content": [ + { + "name": "channelID", + "humanName": { + "de": "Kanal", + "en": "Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "ID of the channel in which the message should be send", + "de": "ID des Kanals, in welchen die Nachricht gesendet werden soll" + }, + "type": "channelID" + }, + { + "name": "message", + "humanName": { + "de": "Nachricht", + "en": "Message" + }, + "default": { + "en": "" + }, + "description": { + "en": "Message that should be send", + "de": "Nachricht, die gesendet werden soll" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "limitWeekDaysTo", + "humanName": { + "de": "Wochentage begrenzen auf", + "en": "Limit Week-Days to" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "If one or more values are set, the message will only get send when the current week-day is included in this field", + "de": "Wenn ein oder mehrere Werte gesetzt sind, wird die Nachricht nur gesendet, wenn der aktuelle Wochentag hier enthalten ist" + }, + "type": "array", + "content": "integer" + }, + { + "name": "limitDaysTo", + "humanName": { + "de": "Tage begrenzen auf", + "en": "Limit days to" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "If one or more values are set, the message will only get send when the current day (of the month) is included in this field", + "de": "Wenn ein oder mehrere Werte gesetzt sind, wird die Nachricht nur gesendet, wenn der aktuelle Tag (des Monats) hier enthalten ist" + }, + "type": "array", + "content": "integer" + } + ] +} \ No newline at end of file diff --git a/modules/auto-messager/events/botReady.js b/modules/auto-messager/events/botReady.js new file mode 100644 index 00000000..c18ca5f9 --- /dev/null +++ b/modules/auto-messager/events/botReady.js @@ -0,0 +1,48 @@ +const schedule = require('node-schedule'); +const {embedType} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (client) { + const hourly = client.configurations['auto-messager']['hourly']; + const hourlyJob = schedule.scheduleJob('1 * * * *', async () => { + for (const obj of hourly) { + obj.limitHoursTo = obj.limitHoursTo.map(Number); + if (obj.limitHoursTo.length !== 0 && !obj.limitHoursTo.includes(new Date().getHours())) continue; + const c = client.channels.cache.get(obj.channelID); + if (!c) { + client.logger.error(`[auto-messager] ${localize('auto-messager', 'channel-not-found', {id: obj.channelID})}`); + continue; + } + await c.send(embedType(obj.message)); + } + }); + client.jobs.push(hourlyJob); + + const daily = client.configurations['auto-messager']['daily']; + const dailyJob = schedule.scheduleJob('1 6 * * *', async () => { + for (const obj of daily) { + obj.limitWeekDaysTo = obj.limitWeekDaysTo.map(Number); + obj.limitDaysTo = obj.limitDaysTo.map(Number); + if (obj.limitWeekDaysTo.length !== 0 && !obj.limitWeekDaysTo.includes(new Date().getDay() + 1)) continue; + if (obj.limitDaysTo.length !== 0 && !obj.limitDaysTo.includes(new Date().getDate())) continue; + const c = client.channels.cache.get(obj.channelID); + if (!c) { + client.logger.error(`[auto-messager] ${localize('auto-messager', 'channel-not-found', {id: obj.channelID})}`); + continue; + } + await c.send(embedType(obj.message)); + } + }); + client.jobs.push(dailyJob); + + const cronjob = client.configurations['auto-messager']['cronjob']; + for (const job of cronjob) { + client.jobs.push(schedule.scheduleJob(job.expression, async () => { + const c = client.channels.cache.get(job.channelID); + if (!c) { + return client.logger.error(`[auto-messager] ${localize('auto-messager', 'channel-not-found', {id: obj.channelID})}`); + } + await c.send(embedType(job.message)); + })); + } +}; \ No newline at end of file diff --git a/modules/auto-messager/hourly.json b/modules/auto-messager/hourly.json new file mode 100644 index 00000000..3237ce96 --- /dev/null +++ b/modules/auto-messager/hourly.json @@ -0,0 +1,77 @@ +{ + "description": { + "en": "You can send messages on an hourly basic here - this can be once or 24 times a day", + "de": "Hier kannst du Nachrichten auf stündlicher Basis schicken lassen - das kann alles von 1-24x pro Tag sein" + }, + "humanName": { + "en": "Hourly basic", + "de": "Stündliche Basis" + }, + "elementLimits": { + "STARTER": 1, + "ACTIVE_GUILD": 4, + "PRO": 14 + }, + "configElementName": { + "de": { + "one": "Automatische Nachricht", + "more": "Automatische Nachrichten" + }, + "en": { + "one": "Automatic message", + "more": "Automatic messages" + } + }, + "filename": "hourly.json", + "configElements": true, + "content": [ + { + "name": "channelID", + "humanName": { + "de": "Kanal", + "en": "Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "ID of the channel in which the message should be send", + "de": "ID des Kanals, in welchen die Nachricht gesendet werden soll" + }, + "type": "channelID" + }, + { + "name": "message", + "humanName": { + "de": "Nachricht", + "en": "Message" + }, + "default": { + "en": "" + }, + "description": { + "en": "Message that should be send", + "de": "Nachricht, die gesendet werden soll" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "limitHoursTo", + "humanName": { + "de": "Stunden begrenzen auf", + "en": "Limit hours to" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "If one or more values are set, the message will only get send when the current hour is included in this field", + "de": "Wenn ein oder mehrere Werte gesetzt sind, wird die Nachricht nur gesendet, wenn die aktuelle Stunde hier enthalten ist" + }, + "type": "array", + "content": "integer" + } + ] +} \ No newline at end of file diff --git a/modules/auto-messager/module.json b/modules/auto-messager/module.json new file mode 100644 index 00000000..e332ccd6 --- /dev/null +++ b/modules/auto-messager/module.json @@ -0,0 +1,26 @@ +{ + "name": "auto-messager", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/auto-messager", + "events-dir": "/events", + "config-example-files": [ + "hourly.json", + "daily.json", + "cronjob.json" + ], + "tags": [ + "tools" + ], + "humanReadableName": { + "en": "Automatic Messages", + "de": "Automatische Nachrichten" + }, + "description": { + "en": "You can - with this module - send automatic messages", + "de": "Dieses Modul erlaubt dir es dir, automatisch versenden zu lassen" + } +} \ No newline at end of file diff --git a/modules/auto-publisher/config.json b/modules/auto-publisher/config.json new file mode 100644 index 00000000..2535e581 --- /dev/null +++ b/modules/auto-publisher/config.json @@ -0,0 +1,75 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "mode", + "humanName": { + "en": "Message-Publishing-Mode", + "de": "Nachrichten-Veröffentlichung-Modus" + }, + "default": { + "en": "all" + }, + "description": { + "en": "Modus in which this module should operate", + "de": "Modus in welchem dieses Modul arbeiten sollte" + }, + "type": "select", + "content": [ + "all", + "whitelist", + "blacklist" + ] + }, + { + "name": "blacklist", + "humanName": { + "en": "Blacklist" + }, + "default": { + "en": [] + }, + "description": { + "en": "Channel to be ignored (only if Message-Publishing-Mode = \"blacklist\")", + "de": "Kanäle, die ignoriert werden sollen (nur wenn Nachrichten-Veröffentlichung-Modus = \"blacklist\")" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "whitelist", + "humanName": { + "en": "Whitelist" + }, + "default": { + "en": [] + }, + "description": { + "en": "Channel in which messages should get published (only if Message-Publishing-Mode = \"whitelist\")", + "de": "Kanäle, in denen Nachrichten veröffentlicht werden sollen (nur wenn Message-Publishing-Mode = \"whitelist\")" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "ignoreBots", + "humanName": { + "en": "Ignore bots?", + "de": "Bots ignorieren?" + }, + "default": { + "en": true + }, + "description": { + "en": "Should bots get ignored when they post a message", + "de": "Sollen Bots ignoriert werden, wenn sie eine Nachricht senden" + }, + "type": "boolean" + } + ] +} diff --git a/modules/auto-publisher/events/messageCreate.js b/modules/auto-publisher/events/messageCreate.js new file mode 100644 index 00000000..8c10f69b --- /dev/null +++ b/modules/auto-publisher/events/messageCreate.js @@ -0,0 +1,21 @@ +module.exports.run = async (client, msg) => { + if (!msg.guild) return; + if (!client.botReadyAt) return; + if (msg.guild.id !== client.guildID) return; + if (msg.content.startsWith(client.config.prefix)) return; + if (msg.channel.type === 'GUILD_NEWS') { + const config = client.configurations['auto-publisher']['config']; + if (config.ignoreBots && msg.author.bot) return; + if (!config.blacklist) config.blacklist = []; + if (!config.whitelist) config.blacklist = []; + if (!config.mode) config.mode = 'all'; + if (config.mode === 'blacklist' && config.blacklist.includes(msg.channel.id)) return; + if (config.mode === 'whitelist' && !config.whitelist.includes(msg.channel.id)) return; + if (msg.crosspostable) await msg.crosspost(); + await msg.react('✅').then((r) => { + setTimeout(() => { + r.remove(); + }, 2500); + }); + } +}; \ No newline at end of file diff --git a/modules/auto-publisher/module.json b/modules/auto-publisher/module.json new file mode 100644 index 00000000..958a7891 --- /dev/null +++ b/modules/auto-publisher/module.json @@ -0,0 +1,24 @@ +{ + "name": "auto-publisher", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/auto-publisher", + "events-dir": "/events", + "config-example-files": [ + "config.json" + ], + "tags": [ + "tools" + ], + "humanReadableName": { + "en": "Automatic Publishing", + "de": "Automatische Veröffentlichung" + }, + "description": { + "en": "Publishes messages in announcement channels", + "de": "Veröffentlicht Nachrichten in Ankündigungskanälen" + } +} \ No newline at end of file diff --git a/modules/auto-react/configs/config.json b/modules/auto-react/configs/config.json new file mode 100644 index 00000000..6e6dad18 --- /dev/null +++ b/modules/auto-react/configs/config.json @@ -0,0 +1,101 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "channels", + "humanName": { + "en": "Channels", + "de": "Kanäle" + }, + "default": { + "en": {} + }, + "description": { + "en": "Here you can add channels and the reactions in it (you can add multiple emojis with | between each one)", + "de": "Du kannst hier Kanal-IDs und die dazugehörigen Emojis eintragen (mehrere Emojis müssen mit einem | getrennt werden)" + }, + "type": "keyed", + "content": { + "key": "userID", + "value": "string" + } + }, + { + "name": "members", + "humanName": { + "en": "Mentions", + "de": "Erwähnungen" + }, + "default": { + "en": {} + }, + "description": { + "en": "Here you can add members and the reactions on their mentions of them (you can add multiple emojis with | between each one)", + "de": "Du kannst hier NutzerIDs und die dazugehörigen Emojis auf Erwähnungen dieser eintragen (mehrere Emojis müssen mit einem | getrennt werden" + }, + "type": "keyed", + "content": { + "key": "userID", + "value": "string" + } + }, + { + "name": "authors", + "humanName": { + "en": "Authors", + "de": "Autoren" + }, + "default": { + "en": {} + }, + "description": { + "en": "Here you can add members and the reactions on their messages in it (you can add multiple emojis with | between each one)", + "de": "Du kannst hier NutzerIDs und die dazugehörigen Emojis auf deren Nachrichten eintragen (mehrere Emojis müssen mit einem | getrennt werden" + }, + "type": "keyed", + "content": { + "key": "userID", + "value": "string" + } + }, + { + "name": "categories", + "humanName": { + "en": "Categories", + "de": "Kategorien" + }, + "default": { + "en": {} + }, + "description": { + "en": "Here you can add categories and the reactions in it (you can add multiple emojis with | between each one)", + "de": "Du kannst hier Kategorien und die dazugehörigen Emojis eintragen (mehrere Emojis müssen mit einem | getrennt werden)" + }, + "type": "keyed", + "content": { + "key": "channelID", + "value": "string" + } + }, + { + "name": "forcedMentionMatching", + "default": { + "en": true + }, + "type": "boolean", + "humanName": { + "en": "Only react to @mentions?", + "de": "Nur auf @Erwähnungen reagieren?" + }, + "description": { + "en": "If disabled, the bot will also react to mentions in inline-replies or otherwise in addition to conventional @mentions.", + "de": "Wenn deaktiviert, wird der Bot auch auf Erwähnungen in Inline-Antworten oder anderweitigen Erwähnungen, zusätzlich zu den gewöhnlichen @Erwähnungen, reagieren." + } + } + ] +} \ No newline at end of file diff --git a/modules/auto-react/configs/replies.json b/modules/auto-react/configs/replies.json new file mode 100644 index 00000000..713075cc --- /dev/null +++ b/modules/auto-react/configs/replies.json @@ -0,0 +1,42 @@ +{ + "description": {}, + "humanName": { + "en": "Replies", + "de": "Antworten" + }, + "filename": "replies.json", + "configElements": true, + "content": [ + { + "name": "members", + "humanName": { + "en": "User", + "de": "Nutzer" + }, + "default": { + "en": "" + }, + "description": { + "en": "Here you can add a member to be replied on mentions of them", + "de": "Du kannst hier einen Nutzer für Antworten auf Erwähnungen dessen eintragen" + }, + "type": "string" + }, + { + "name": "reply", + "humanName": { + "en": "Reply", + "de": "Antwort" + }, + "default": { + "en": "" + }, + "description": { + "en": "Here you can add the reply message", + "de": "Du kannst hier die Antwort eintragen" + }, + "type": "string", + "allowEmbed": true + } + ] +} \ No newline at end of file diff --git a/modules/auto-react/events/messageCreate.js b/modules/auto-react/events/messageCreate.js new file mode 100644 index 00000000..25dc6702 --- /dev/null +++ b/modules/auto-react/events/messageCreate.js @@ -0,0 +1,94 @@ +const {embedType} = require('../../../src/functions/helpers'); +module.exports.run = async (client, msg) => { + if (!client.botReadyAt) return; + if (msg.interaction || msg.system || !msg.author || !msg.guild || msg.guild.id !== client.config.guildID) return; + await checkChannel(msg); + await checkMembers(msg); + await checkCategory(msg); + await checkAuthor(msg); + await checkMembersReply(msg); +}; + +/** + * Checks for member pings on a message and reacts with the configured emotes + * @private + * @param msg [Message](https://discord.js.org/#/docs/main/stable/class/Message) + * @returns {Promise} + */ +async function checkMembers(msg) { + const moduleConfig = msg.client.configurations['auto-react']['config']; + if (!msg.mentions.members) return; + for (const m of msg.mentions.members.values()) { + if (!msg.content.replaceAll('!', '').includes(`<@${m.id}>`) && moduleConfig.forcedMentionMatching) continue; + if (moduleConfig.members[m.id]) moduleConfig.members[m.id].split('|').forEach(emoji => { + msg.react(emoji).catch(() => { + }); + }); + } +} + +/** + * Checks if a message need reactions (and reacts if needed) because it was send in a configured channel + * @private + * @param msg [Message](https://discord.js.org/#/docs/main/stable/class/Message) + * @returns {Promise} + */ +async function checkChannel(msg) { + const moduleConfig = msg.client.configurations['auto-react']['config']; + if (!moduleConfig.channels[msg.channel.id]) return; + moduleConfig.channels[msg.channel.id].split('|').forEach(emoji => { + msg.react(emoji).catch(() => { + }); + }); +} + +/** + * Checks if a message need reactions (and reacts if needed) because it was send in a configured category + * @private + * @param msg [Message](https://discord.js.org/#/docs/main/stable/class/Message) + * @returns {Promise} + */ +async function checkCategory(msg) { + const moduleConfig = msg.client.configurations['auto-react']['config']; + if (!moduleConfig.categories[msg.channel.parentId]) return; + moduleConfig.categories[msg.channel.parentId].split('|').forEach(emoji => { + msg.react(emoji).catch(() => { + }); + }); +} + +/** + * Checks for member pings in a message and replys with the configured message + * @private + * @param msg + * @returns {Promise} + */ +async function checkMembersReply(msg) { + const moduleConfig = msg.client.configurations['auto-react']['replies']; + if (!msg.mentions.users) return; + if (msg.author.id === msg.client.user.id) return; + for (const m of msg.mentions.users.values()) { + if (!msg.content.replaceAll('!', '').includes(`<@${m.id}>`) && msg.client.configurations['auto-react']['config'].forcedMentionMatching) continue; + const matches = moduleConfig.filter(c => c.members === m.id); + for (const element of matches) { + await msg.reply(embedType(element.reply, {}, {ephemeral: true})).catch(() => { + }); + } + } +} + + +/** + * Checks if a message need reactions (and reacts if needed) because it was send in a configured channel + * @private + * @param msg [Message](https://discord.js.org/#/docs/main/stable/class/Message) + * @returns {Promise} + */ +async function checkAuthor(msg) { + const moduleConfig = msg.client.configurations['auto-react']['config']; + if (!moduleConfig.authors[msg.author.id]) return; + moduleConfig.authors[msg.author.id].split('|').forEach(emoji => { + msg.react(emoji).catch(() => { + }); + }); +} \ No newline at end of file diff --git a/modules/auto-react/module.json b/modules/auto-react/module.json new file mode 100644 index 00000000..c97ca445 --- /dev/null +++ b/modules/auto-react/module.json @@ -0,0 +1,25 @@ +{ + "name": "auto-react", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/auto-react", + "events-dir": "/events", + "config-example-files": [ + "configs/config.json", + "configs/replies.json" + ], + "tags": [ + "fun" + ], + "humanReadableName": { + "en": "Automatic reactions", + "de": "Automatisches Reagieren" + }, + "description": { + "en": "Automatically reacts with selected emojis in selected channels or if a user gets pinged", + "de": "Reagiert automatisch mit ausgewählten Emojs in einem ausgewählten Channel und bei Pings" + } +} \ No newline at end of file diff --git a/modules/auto-thread/config.json b/modules/auto-thread/config.json new file mode 100644 index 00000000..1062e8b2 --- /dev/null +++ b/modules/auto-thread/config.json @@ -0,0 +1,64 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "channels", + "humanName": { + "en": "Channels", + "de": "Kanäle" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Here you can add channels in which the bot should create a thread under every message", + "de": "Hier kannst du Kanäle hinzufügen, in welchen der Bot automatisch unter jeder Nachricht einen Thread erstellen soll" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "threadName", + "humanName": { + "de": "Threadname" + }, + "default": { + "en": "Comments", + "de": "Kommentare" + }, + "description": { + "en": "Name of every thread", + "de": "Name jedes Threads" + }, + "type": "string" + }, + { + "name": "threadArchiveDuration", + "humanName": { + "de": "Archivierungsdauer" + }, + "default": { + "en": "MAX", + "de": "MAX" + }, + "description": { + "en": "Inactivity after which a thread is automatically archived (in minutes, some values are limited by guild boost level; select \"max\" for the longest possible duration)", + "de": "Inaktivität nach welcher ein Thread automatisch archiviert wird (in Minutes, manche Werte sind durch das Boostlevel des Servers eingeschränkt; verwende \"max\" für die längstmögliche Dauer)" + }, + "type": "select", + "content": [ + "MAX", + "60", + "1440", + "4320", + "10080" + ] + } + ] +} diff --git a/modules/auto-thread/events/messageCreate.js b/modules/auto-thread/events/messageCreate.js new file mode 100644 index 00000000..276cf745 --- /dev/null +++ b/modules/auto-thread/events/messageCreate.js @@ -0,0 +1,13 @@ +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async (client, msg) => { + if (!client.botReadyAt) return; + if (msg.interaction || msg.system) return; + const moduleConfig = client.configurations['auto-thread']['config']; + if (!(moduleConfig.channels || []).includes(msg.channel.id)) return; + if (!msg.hasThread) await msg.startThread({ + name: moduleConfig.threadName, + autoArchiveDuration: moduleConfig.threadArchiveDuration, + reason: `[auto-thread] ${localize('auto-thread', 'thread-create-reason')}` + }); +}; \ No newline at end of file diff --git a/modules/auto-thread/module.json b/modules/auto-thread/module.json new file mode 100644 index 00000000..8416f7f4 --- /dev/null +++ b/modules/auto-thread/module.json @@ -0,0 +1,24 @@ +{ + "name": "auto-thread", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "events-dir": "/events", + "config-example-files": [ + "config.json" + ], + "tags": [ + "tools" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/auto-thread", + "humanReadableName": { + "en": "Automatic Thread-Creation", + "de": "Automatisches Thread-Erstellen" + }, + "description": { + "en": "Automatically creates a thread under each message that gets posted in a selected channel", + "de": "Erstellt einen Thread unter jeder Nachricht, die in einem bestimmten Channel gesendet wird" + } +} \ No newline at end of file diff --git a/modules/betterstatus/config.json b/modules/betterstatus/config.json new file mode 100644 index 00000000..65919e83 --- /dev/null +++ b/modules/betterstatus/config.json @@ -0,0 +1,223 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "categories": [ + { + "name": "interval", + "humanname-de": "Intervalle", + "humanname-en": "Intervall", + "description-de": "Intervalle erlauben es dir, den Status des Bots alle paar Sekunden automatisch zu ändern!", + "description-en": "You can use intervalls to automatically change the Status of the bot", + "categoryToggle": "enableInterval" + } + ], + "content": [ + { + "name": "enableInterval", + "humanName": { + "en": "Enable interval?", + "de": "Interval aktivieren?" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled the bot will change its status every x seconds", + "de": "Wenn aktiviert wird sich der Status des Bots alle x Sekunden ändern" + }, + "type": "boolean" + }, + { + "name": "intervalStatuses", + "dependsOn": "enableInterval", + "humanName": { + "en": "Interval-Statuses", + "de": "Interval-Status" + }, + "default": { + "en": [] + }, + "description": { + "en": "Statuses from which the bot should randomly choose one", + "de": "Die Status von denen der Bot einen zufällig wählen soll" + }, + "type": "array", + "content": "string", + "params": [ + { + "name": "onlineMemberCount", + "description": { + "en": "Count of online members on your guild (will not work if presence intent not enabled)", + "de": "Anzahl der Online-Mitglieder auf deinem Server" + } + }, + { + "name": "memberCount", + "description": { + "en": "Count of members on your guild", + "de": "Anzahl der Mitglieder auf deinem Server" + } + }, + { + "name": "randomMemberTag", + "description": { + "en": "Tag of one random member on your guild", + "de": "Erwähnung eines zufälligen Nutzern auf deinem Server" + } + }, + { + "name": "randomOnlineMemberTag", + "description": { + "en": "Tag of one random member who is online on your guild", + "de": "Erwähnung eines zufälligen online Nutzern auf deinem Server" + } + }, + { + "name": "channelCount", + "description": { + "en": "Count of channels on your guild", + "de": "Anzahl Channel auf deinem Server" + } + }, + { + "name": "roleCount", + "description": { + "en": "Count of roles on your guild", + "de": "Anzahl Rollen auf deinem Server" + } + } + ] + }, + { + "name": "activityType", + "humanName": { + "en": "Activity-Type", + "de": "Aktivität-Typ" + }, + "default": { + "en": "PLAYING" + }, + "description": { + "en": "Type of the user activity", + "de": "Type der Aktivität deines Bots" + }, + "type": "select", + "content": [ + "CUSTOM", + "PLAYING", + "WATCHING", + "STREAMING", + "COMPETING", + "LISTENING" + ] + }, + { + "name": "botStatus", + "humanName": { + "en": "Bot-Status", + "de": "Bot-Status" + }, + "default": { + "en": "online" + }, + "description": { + "en": "Status of your bot", + "de": "Status deines Bots" + }, + "type": "select", + "content": [ + "idle", + "online", + "dnd" + ] + }, + { + "name": "interval", + "humanName": { + "en": "Status-Interval", + "de": "Statusänderung-Interval" + }, + "default": { + "en": 15 + }, + "description": { + "en": "The interval in seconds (at least 10 seconds)", + "de": "Das Intervall der Statusänderungen in Sekunden (mindestens 10 Sekunden)" + }, + "type": "integer" + }, + { + "name": "changeOnUserJoin", + "humanName": { + "en": "Change status on user join?", + "de": "Beim Beitreten Status ändern?" + }, + "default": { + "en": false + }, + "description": { + "en": "If the status should be changed if someone joins your guild", + "de": "Wenn aktiviert wird sich der Status des Bots ändern, wenn jemand deinem Server beitritt" + }, + "type": "boolean" + }, + { + "name": "userJoinStatus", + "dependsOn": "changeOnUserJoin", + "humanName": { + "en": "User-Join-Status", + "de": "Nutzer-Join-Status" + }, + "default": { + "en": "Welcome %tag%!" + }, + "description": { + "en": "Status that will be set if a user joins", + "de": "Dieser Status wird gesetzt, wenn jemand deinem Server beitritt" + }, + "type": "string", + "params": [ + { + "name": "tag", + "description": { + "en": "Tag of the new user", + "de": "Tag des Nutzers" + } + }, + { + "name": "username", + "description": { + "en": "Username of the new user", + "de": "Nutzername des Nutzers" + } + }, + { + "name": "memberCount", + "description": { + "en": "New member count of your guild", + "de": "Anzahl der Mitglieder auf deinem Server" + } + } + ] + }, + { + "name": "streamingLink", + "type": "string", + "humanName": { + "en": "Steaming-Link", + "de": "Stream-Link" + }, + "default": { + "en": "" + }, + "description": { + "de": "Wird angezeigt, wenn der Aktivität-Typ auf streaming ist und der Link von Discord unterstützt wird", + "en": "Will be shown, if the activity-typ is streaming and your link is supported by Discord" + } + } + ] +} \ No newline at end of file diff --git a/modules/betterstatus/events/botReady.js b/modules/betterstatus/events/botReady.js new file mode 100644 index 00000000..b73caff1 --- /dev/null +++ b/modules/betterstatus/events/botReady.js @@ -0,0 +1,49 @@ +const {formatDiscordUserName} = require('../../../src/functions/helpers'); +module.exports.run = async function (client) { + const moduleConf = client.configurations['betterstatus']['config']; + + await client.user.setActivity(await replaceStatusString(client.config['user_presence']), { + type: moduleConf['activityType'] + }); + + if (moduleConf.enableInterval) { + const interval = setInterval(async () => { + await client.user.setActivity(await replaceStatusString(moduleConf['intervalStatuses'][moduleConf['intervalStatuses'].length * Math.random() | 0]), + { + type: moduleConf['activityType'], + url: (moduleConf['streamingLink'] && moduleConf.activityType === 'STREAMING') ? moduleConf['streamingLink'] : null + }); + }, moduleConf.interval < 5 ? 5000 : moduleConf.interval * 1000); // At least 5 seconds to prevent rate limiting + client.intervals.push(interval); + } + + if (moduleConf.botStatus !== 'ONLINE') { + await client.user.setPresence({status: moduleConf.botStatus}); + } + + if (moduleConf.activityType !== 'PLAYING' && !moduleConf.enableInterval) { + await client.user.setActivity(client.config.user_presence, { + type: moduleConf.activityType, + url: (moduleConf['streamingLink'] && moduleConf.activityType === 'STREAMING') ? moduleConf['streamingLink'] : null + }); + } + + /** + * @private + * Replace status variables + * @param statusString String to run the replacer on + * @returns {Promise} + */ + async function replaceStatusString(statusString) { + if (!statusString) return 'Invalid status'; + const members = await (await client.guild.fetch()).members.fetch({withPresences: true, force: true}); + const randomOnline = members.filter(m => m.presence && !m.user.bot).random(); + const random = members.filter(m => !m.user.bot).random(); + return statusString.replaceAll('%memberCount%', client.guild.memberCount) + .replaceAll('%onlineMemberCount%', members.filter(m => m.presence && !m.user.bot).size) + .replaceAll('%randomOnlineMemberTag%', randomOnline ? formatDiscordUserName(randomOnline.user) : formatDiscordUserName(client.user)) + .replaceAll('%randomMemberTag%', `${random.user.username}#${random.user.discriminator}`) + .replaceAll('%channelCount%', client.guild.channels.cache.size) + .replaceAll('%roleCount%', (await client.guild.roles.fetch()).size); + } +}; \ No newline at end of file diff --git a/modules/betterstatus/events/guildMemberAdd.js b/modules/betterstatus/events/guildMemberAdd.js new file mode 100644 index 00000000..d48806e1 --- /dev/null +++ b/modules/betterstatus/events/guildMemberAdd.js @@ -0,0 +1,22 @@ +const {formatDiscordUserName} = require('../../../src/functions/helpers'); +exports.run = async (client, member) => { + const moduleConf = client.configurations['betterstatus']['config']; + + /** + * @private + * Replace status variables + * @param configElement Configuration Element + * @returns {String} + */ + function replaceMemberJoinStatusString(configElement) { + return configElement.replaceAll('%tag%', formatDiscordUserName(member.user)) + .replaceAll('%username%', member.user.username) + .replaceAll('%memberCount%', member.guild.memberCount); + } + + if (moduleConf['changeOnUserJoin']) { + await client.user.setActivity(replaceMemberJoinStatusString(moduleConf['userJoinStatus']), { + type: moduleConf['activityType'] + }); + } +}; \ No newline at end of file diff --git a/modules/betterstatus/module.json b/modules/betterstatus/module.json new file mode 100644 index 00000000..5fe9f6fa --- /dev/null +++ b/modules/betterstatus/module.json @@ -0,0 +1,23 @@ +{ + "name": "betterstatus", + "author": { + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox", + "scnxOrgID": "1" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/betterstatus", + "events-dir": "/events", + "config-example-files": [ + "config.json" + ], + "tags": [ + "bot" + ], + "humanReadableName": { + "en": "Betterstatus" + }, + "description": { + "en": "Give you more features to make your status even better - change it when someone joins, change it every x seconds and more!", + "de": "Mache den Status deines Bots noch besser - Nutze Variablen, Intervalle und vieles mehr!" + } +} \ No newline at end of file diff --git a/modules/birthday/birthday.js b/modules/birthday/birthday.js new file mode 100644 index 00000000..01d454c6 --- /dev/null +++ b/modules/birthday/birthday.js @@ -0,0 +1,197 @@ +/** + * Manages the birthday-embed + * @module Birthdays + * @author Simon Csaba + */ +const {embedType, disableModule, truncate, embedTypeV2, formatDiscordUserName} = require('../../src/functions/helpers'); +const {MessageEmbed} = require('discord.js'); +const {AgeFromDate} = require('age-calculator'); +const {localize} = require('../../src/functions/localize'); + +/** + * Generate the BirthdayEmbed in the configured channel + * @param {Client} client Client + * @param {boolean} notifyUsers If enabled the bot will notify users who have birthday today + * @returns {Promise} + */ +generateBirthdayEmbed = async function (client, notifyUsers = false) { + const moduleConf = client.configurations['birthday']['config']; + + const channel = await client.channels.fetch(moduleConf['channelID']).catch(() => { + }); + if (!channel) return disableModule('birthdays', localize('birthdays', 'channel-not-found', {c: moduleConf.channelID})); + if (!moduleConf.enableBirthdayEmbed) { + if (notifyUsers) await notifyBirthdayUsers(); + return; + } + const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); + await channel.guild.members.fetch({force: true}); + + if (notifyUsers && !moduleConf.notificationChannelOverwriteID) { + for (const m of messages.filter(msg => msg.id !== messages.last().id)) { + if (m.deletable) await m.delete(); // Removing old messages + } + } + + const embeds = [ + new MessageEmbed() + .setTitle(moduleConf['birthdayEmbed']['title']) + .setDescription(moduleConf['birthdayEmbed']['description']) + .setColor(moduleConf['birthdayEmbed']['color']) + .setAuthor({name: client.user.username, iconURL: client.user.avatarURL()}) + .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) + .addFields([ + { + name: localize('months', '1'), + value: await getUserStringForMonth(client, channel, 1), + inline: true + }, + { + name: localize('months', '2'), + value: await getUserStringForMonth(client, channel, 2), + inline: true + }, + { + name: localize('months', '3'), + value: await getUserStringForMonth(client, channel, 3), + inline: true + }, + { + name: localize('months', '4'), + value: await getUserStringForMonth(client, channel, 4), + inline: true + }, + { + name: localize('months', '5'), + value: await getUserStringForMonth(client, channel, 5), + inline: true + }, + { + name: localize('months', '6'), + value: await getUserStringForMonth(client, channel, 6), + inline: true + }, + { + name: localize('months', '7'), + value: await getUserStringForMonth(client, channel, 7), + inline: true + }, + { + name: localize('months', '8'), + value: await getUserStringForMonth(client, channel, 8), + inline: true + }, + { + name: localize('months', '9'), + value: await getUserStringForMonth(client, channel, 9), + inline: true + }, + { + name: localize('months', '10'), + value: await getUserStringForMonth(client, channel, 10), + inline: true + }, + { + name: localize('months', '11'), + value: await getUserStringForMonth(client, channel, 11), + inline: true + }, + { + name: localize('months', '12'), + value: await getUserStringForMonth(client, channel, 12), + inline: true + }]) + ]; + + if ((moduleConf['birthdayEmbed']['thumbnail'] || '').replaceAll(' ', '')) embeds[0].setThumbnail(moduleConf['birthdayEmbed']['thumbnail']); + if ((moduleConf['birthdayEmbed']['image'] || '').replaceAll(' ', '')) embeds[0].setImage(moduleConf['birthdayEmbed']['image']); + if (!client.strings.disableFooterTimestamp) embeds[0].setTimestamp(); + + if (messages.last()) await messages.last().edit({embeds}); + else channel.send({embeds}); + + if (notifyUsers) await notifyBirthdayUsers(); + + /** + * Notifies users who have birthday + * @returns {Promise} + */ + async function notifyBirthdayUsers() { + const birthdayUsers = await client.models['birthday']['User'].findAll({ + where: { + month: new Date().getMonth() + 1, + day: new Date().getDate() + } + }); + if (!birthdayUsers) return; + + if (moduleConf['birthday_role']) { + const guildMembers = await channel.guild.members.fetch(); + for (const member of guildMembers.values()) { + if (!member) return; + if (member.roles.cache.has(moduleConf['birthday_role'])) { + await member.roles.remove(moduleConf['birthday_role']); + } + } + } + + const birthdayMessageChannel = moduleConf.notificationChannelOverwriteID ? await client.guild.channels.fetch(moduleConf.notificationChannelOverwriteID) : channel; + + for (const user of birthdayUsers) { + const member = channel.guild.members.cache.get(user.id); + if (!member) return; + if (user.year) { + birthdayMessageChannel.send(await embedTypeV2(moduleConf['birthday_message_with_age'], { + '%age%': new Date().getFullYear() - user.year, + '%tag%': formatDiscordUserName(member.user), + '%username%': member.user.username, + '%avatarURL%': member.user.avatarURL() || member.user.defaultAvatarURL, + '%mention%': `<@${user.id}>` + })); + } else { + birthdayMessageChannel.send(await embedTypeV2(moduleConf['birthday_message'], { + '%tag%': formatDiscordUserName(member.user), + '%avatarURL%': member.user.avatarURL() || member.user.defaultAvatarURL, + '%mention%': `<@${user.id}>` + })); + } + if (moduleConf['birthday_role']) await member.roles.add(moduleConf['birthday_role']); + } + } +}; + +module.exports.generateBirthdayEmbed = generateBirthdayEmbed; + +/** + * Get UserString for a month + * @private + * @param {Client} client Client + * @param {Channel} channel Channel to send embed in + * @param {Number} month Month to render results from + * @returns {Promise} + */ +async function getUserStringForMonth(client, channel, month) { + const monthData = await client.models['birthday']['User'].findAll({ + where: { + month: month + } + }); + monthData.sort((a, b) => { + return a.day - b.day; + }); + let string = ''; + for (const user of monthData) { + let dateString = `${user.day}.${month}${user.year ? `.${user.year}` : ''}`; + if (user.year && !client.configurations['birthday']['config'].disableSync) { + const age = new AgeFromDate(new Date(user.year, user.month - 1, user.day)).age; + if (age < 13 || age > 125) { + await user.destroy(); + continue; + } + dateString = `[${dateString}](https://sc-network.net/age?age=${age} "${localize('birthdays', 'age-hover', {a: age})}")`; + } + if (channel.guild.members.cache.get(user.id)) string = string + `${dateString}: ${client.configurations['birthday']['config'].useTags ? formatDiscordUserName(channel.guild.members.cache.get(user.id).user) : channel.guild.members.cache.get(user.id).user.toString()}\n`; + } + if (string.length === 0) string = localize('birthdays', 'no-bd-this-month'); + return truncate(string, 1024); +} \ No newline at end of file diff --git a/modules/birthday/commands/birthday.js b/modules/birthday/commands/birthday.js new file mode 100644 index 00000000..bea02177 --- /dev/null +++ b/modules/birthday/commands/birthday.js @@ -0,0 +1,127 @@ +const {generateBirthdayEmbed} = require('../birthday'); +const {AgeFromDateString, AgeFromDate} = require('age-calculator'); +const {embedType} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.beforeSubcommand = async function (interaction) { + interaction.birthday = await interaction.client.models['birthday']['User'].findOne({ + where: { + id: interaction.user.id + } + }); +}; + +module.exports.subcommands = { + 'status': async function (interaction) { + if (!interaction.birthday) return interaction.reply({ + ephemeral: true, + content: '⚠️️ ' + localize('birthdays', 'no-birthday-set') + }); + interaction.reply({ + ephemeral: true, + content: localize('birthdays', 'birthday-status', { + dd: interaction.birthday.day, + mm: interaction.birthday.month, + yyyy: (interaction.birthday.year ? `.${interaction.birthday.year}` : ''), + age: interaction.birthday.year ? ', ' + (localize('birthdays', 'your-age', {age: new AgeFromDateString(`${interaction.birthday.year}-${interaction.birthday.month - 1}-${interaction.birthday.day}`).age})) : '' + }) + }); + + }, + 'delete': async function (interaction) { + if (!interaction.birthday) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('birthdays', 'no-birthday-set') + }); + await interaction.birthday.destroy(); + interaction.birthday = null; + interaction.reply({ + ephemeral: true, + content: '🗑️ ' + localize('birthdays', 'deleted-successfully') + }); + interaction.regenerateEmbed = true; + }, + 'set': async function (interaction) { + const day = interaction.options.getInteger('day', true); + const month = interaction.options.getInteger('month', true); + const year = interaction.options.getInteger('year'); + + if ((day > 31 || day < 1) || (month > 12 || month < 1)) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('birthdays', 'invalid-date') + }); + + if (year) { + const age = new AgeFromDate(new Date(year, month - 1, day)).age; + if (age < 13) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('birthdays', 'against-tos', {waitTime: 13 - age}) + }); + if (age > 125) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('birthdays', 'too-old') + }); + } + + if (!interaction.birthday) { + interaction.birthday = await interaction.client.models.birthday['User'].create({ + id: interaction.user.id + }); + } + + interaction.birthday.day = day; + interaction.birthday.month = month; + interaction.birthday.year = year; + interaction.birthday.sync = false; + interaction.regenerateEmbed = true; + + await interaction.reply(embedType(interaction.client.configurations['birthday']['config']['successfully_changed'], {}, {ephemeral: true})); + } +}; + +module.exports.run = async function (interaction) { + if (interaction.birthday) await interaction.birthday.save(); + if (interaction.regenerateEmbed) await generateBirthdayEmbed(interaction.client); +}; + +module.exports.config = { + name: 'birthday', + description: localize('birthdays', 'command-description'), + + options: [{ + type: 'SUB_COMMAND', + name: 'status', + description: localize('birthdays', 'status-command-description') + }, + { + type: 'SUB_COMMAND', + name: 'set', + description: localize('birthdays', 'set-command-description'), + options: [ + { + type: 'INTEGER', + required: true, + name: 'day', + description: localize('birthdays', 'set-command-day-description') + }, + { + type: 'INTEGER', + required: true, + name: 'month', + description: localize('birthdays', 'set-command-month-description') + }, + { + type: 'INTEGER', + required: false, + name: 'year', + description: localize('birthdays', 'set-command-year-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'delete', + description: localize('birthdays', 'delete-command-description') + } + ] +}; \ No newline at end of file diff --git a/modules/birthday/config.json b/modules/birthday/config.json new file mode 100644 index 00000000..b2a4bec2 --- /dev/null +++ b/modules/birthday/config.json @@ -0,0 +1,241 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "channelID", + "humanName": { + "en": "Birthday-Channel", + "de": "Geburtstag-Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel to run send the Birthday-Embed (and notifications, if not overwritten) in", + "de": "Kanal, in welchem das Geburtstags-Embed (und Benachrichtigung, falls nicht überschrieben) versendet werden soll" + }, + "type": "channelID" + }, + { + "name": "notificationChannelOverwriteID", + "allowNull": true, + "humanName": { + "en": "(optional) Notification-Channel", + "de": "(optional) Benachrichtigung-Kanal" + }, + "type": "channelID", + "description": { + "de": "Kanal, in welchen Nutzern zu ihrem Geburtstag gratuliert werden soll. Wenn dieses Feld leer ist, wird der Geburtstags-Kanal verwendet. In diesem Kanal werden die Geburtstags-Nachrichten vom Vortag, im Gegensatz zum Geburtstags-Kanal, nicht jeden Tag automatisch geleert.", + "en": "Channel in which \"Happy birthday\"-messages should get send. If this field is empty, the message will get send in the Birthday-Channel. Old birthday notifications won't get removed automatically from this channel, in contrast to the Birthday-Channel." + }, + "default": { + "en": "" + } + }, + { + "name": "enableBirthdayEmbed", + "humanName": { + "en": "Birthday-Embed enabled", + "de": "Birthday-Embed aktiviert" + }, + "default": { + "en": true + }, + "description": { + "en": "If enabled, a messages (which will update itself) will be sent in the Birthday-Channel, which contains all Birthdays", + "de": "Wenn aktiviert, wird in den Geburtstag-Channel einen Nachricht gesendet (aktualisiert sich automatisch), welche alle Geburtstage enthält" + }, + "type": "boolean" + }, + { + "name": "birthday_message", + "allowGeneratedImage": true, + "humanName": { + "en": "Giveaway-Message", + "de": "Geburtstags-Nachricht" + }, + "default": { + "en": "Happy birthday, %mention%!" + }, + "description": { + "en": "Message that gets send if the user has not set a birthday", + "de": "Diese Nachricht wird verschickt, wenn der Nutzer kein Geburtsjahr angegeben hat und Geburtstag hat" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "avatarURL", + "isImage": true, + "description": { + "en": "Avatar of the user", + "de": "Profilbild des Nutzers" + } + }, + { + "name": "username", + "description": { + "en": "Username of the user", + "de": "Nutzername des Nutzers" + } + }, + { + "name": "mention", + "description": { + "en": "Mention of the user", + "de": "Erwähnung des Nutzers" + } + } + ] + }, + { + "name": "birthday_message_with_age", + "allowGeneratedImage": true, + "humanName": { + "en": "Giveaway-Message with age", + "de": "Geburtstags-Nachricht mit Alter" + }, + "default": { + "en": "Happy birthday, %mention%! You are now %age% years old!", + "de": "Alles Gute zum %age%ten Geburtstag, %mention%!" + }, + "description": { + "en": "Message that gets send if the user has not set a birthday", + "de": "Diese Nachricht wird verschickt, wenn der Nutzer kein Geburtsjahr angegeben hat und Geburtstag hat" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "avatarURL", + "isImage": true, + "description": { + "en": "Avatar of the user", + "de": "Profilbild des Nutzers" + } + }, + { + "name": "username", + "description": { + "en": "Username of the user", + "de": "Nutzername des Nutzers" + } + }, + { + "name": "mention", + "description": { + "en": "Mention of the user", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "age", + "description": { + "en": "New age of user", + "de": "Neues Alter des Nutzers" + } + } + ] + }, + { + "name": "birthday_role", + "humanName": { + "en": "Birthday-Role", + "de": "Geburtstags-Rolle" + }, + "default": { + "en": "" + }, + "description": { + "en": "Role that is given to users when they have birthday (Leave out to disable)", + "de": "Diese Rolle wird an Leute vergeben, die Geburtstag haben und wieder entfernt, wenn ihr Geburtstag vorbei ist (Leer lassen, um zu deaktivieren) [Tipp: Stelle diese Rolle so ein, dass sie ganz oben angezeigt wird, denn Geburtstage sind etwas besonderes ^^]" + }, + "type": "roleID", + "allowNull": true + }, + { + "name": "successfully_changed", + "humanName": { + "en": "\"Successfully changed\"-Message", + "de": "\"Erfolgreich geändert\"-Nachricht" + }, + "default": { + "en": "Successfully changed record!", + "de": "Die Änderungen wurden gespeichert!" + }, + "description": { + "en": "Message that gets send when the bot changes an item", + "de": "Diese Nachricht wird verschickt, wenn eine Änderung übernommen wurde." + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "birthdayEmbed", + "humanName": { + "en": "Birthday-Embed", + "de": "Geburtstags-Embed" + }, + "default": { + "en": { + "title": "Birthdays", + "color": "GREEN", + "thumbnail": " ", + "image": " ", + "description": "Here you can find every birthday - add yours with !birthday [Year]" + }, + "de": { + "title": "Geburtstage", + "color": "GREEN", + "thumbnail": " ", + "image": " ", + "description": "Hier siehst du die Geburtstage unserer Mitglieder - du kannst deinen Geburtstag mit `/birthday set [Year]` hinzufügen." + } + }, + "description": { + "en": "Change settings of the birthday-embed here", + "de": "Passe hier das Geburtstage-Embed an (Du kannst einige Optionen gerne leer lassen)" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + }, + { + "name": "useTags", + "humanName": { + "en": "Use User's Tags instead of their Mention", + "de": "Nutze den Tag der Nutzer, anstatt eine Erwähnung" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, the bot will use the tag of users in the birthday embed instead of their mention.", + "de": "Wenn aktiviert, wird im Geburtags-Embed der Tag des Nutzers angezeigt und nicht eine Erwähnung (bei großen Servern empfohlen)" + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/birthday/events/botReady.js b/modules/birthday/events/botReady.js new file mode 100644 index 00000000..12ac4178 --- /dev/null +++ b/modules/birthday/events/botReady.js @@ -0,0 +1,24 @@ +const {generateBirthdayEmbed} = require('../birthday'); +const schedule = require('node-schedule'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (client) { + // Migration + const dbVersion = await client.models['DatabaseSchemeVersion'].findOne({where: {model: 'birthday_User'}}); + if (!dbVersion) { + client.logger.info('[birthdays] ' + localize('birthdays', 'migration-happening')); + const data = await client.models['birthday']['User'].findAll({attributes: ['id', 'month', 'day', 'year', 'sync']}); + await client.models['birthday']['User'].sync({force: true}); + for (const user of data) { + await client.models['birthday']['User'].create(user); + } + client.logger.info('[giveaways] ' + localize('birthdays', 'migration-done')); + await client.models['DatabaseSchemeVersion'].create({model: 'birthday_User', version: 'V1'}); + } + + await generateBirthdayEmbed(client); + const job = schedule.scheduleJob('1 0 * * *', async () => { // Every day at 00:01 https://crontab.guru/#0_0_*_*_* + await generateBirthdayEmbed(client, true); + }); + client.jobs.push(job); +}; \ No newline at end of file diff --git a/modules/birthday/events/guildMemberRemove.js b/modules/birthday/events/guildMemberRemove.js new file mode 100644 index 00000000..f351205b --- /dev/null +++ b/modules/birthday/events/guildMemberRemove.js @@ -0,0 +1,5 @@ +const {generateBirthdayEmbed} = require('../birthday'); + +module.exports.run = async function (client) { + await generateBirthdayEmbed(client); +}; \ No newline at end of file diff --git a/modules/birthday/models/User.js b/modules/birthday/models/User.js new file mode 100644 index 00000000..8540246a --- /dev/null +++ b/modules/birthday/models/User.js @@ -0,0 +1,32 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class BirthdayUser extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.STRING, + primaryKey: true + }, + month: DataTypes.INTEGER, + day: DataTypes.INTEGER, + year: DataTypes.INTEGER, + verified: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + sync: { + type: DataTypes.BOOLEAN, + defaultValue: false + } + }, { + tableName: 'birthday_usersV2', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'User', + 'module': 'birthday' +}; \ No newline at end of file diff --git a/modules/birthday/module.json b/modules/birthday/module.json new file mode 100644 index 00000000..848fa034 --- /dev/null +++ b/modules/birthday/module.json @@ -0,0 +1,26 @@ +{ + "name": "birthday", + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/birthday", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "commands-dir": "/commands", + "models-dir": "/models", + "events-dir": "/events", + "config-example-files": [ + "config.json" + ], + "tags": [ + "community" + ], + "humanReadableName": { + "en": "Birthday-Calendar", + "de": "Geburtstags-Kalender" + }, + "description": { + "en": "Let users set their birthday and congratulate them when they have birthday", + "de": "Lasse deine Nutzer ihre Geburtstage eintragen und gratuliere automatisch, wenn sie Geburtstag haben!" + } +} \ No newline at end of file diff --git a/modules/channel-stats/channels.json b/modules/channel-stats/channels.json new file mode 100644 index 00000000..dde44a0b --- /dev/null +++ b/modules/channel-stats/channels.json @@ -0,0 +1,188 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "configElementName": { + "de": { + "one": "Statistik-Kanal", + "more": "Statistik-Kanäle" + }, + "en": { + "one": "Statistics-Channel", + "more": "Statistics-Channels" + } + }, + "filename": "channels.json", + "configElements": true, + "content": [ + { + "name": "channelID", + "humanName": { + "de": "Kanal", + "en": "Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "ID of the voice channel", + "de": "ID des Sprachkanals" + }, + "type": "channelID" + }, + { + "name": "channelName", + "humanName": { + "de": "Kanalname", + "en": "Channel-Name" + }, + "default": { + "en": "" + }, + "description": { + "en": "Name of Channel", + "de": "Name des Kanals" + }, + "type": "string", + "params": [ + { + "name": "userCount", + "description": { + "en": "Total count of users on your server", + "de": "Anzahl an Nutzern auf dem Server" + } + }, + { + "name": "memberCount", + "description": { + "en": "Total count of members (not bots) on your server", + "de": "Anzahl an Mitgliedern (keine Bots) auf dem Server" + } + }, + { + "name": "onlineUserCount", + "description": { + "en": "Total count of online (dnd or online status) users on your server", + "de": "Anzahl an Mitgliedern (keine Bots) auf dem Server, welche Online sind (Bitte nicht stören oder Online Status)" + } + }, + { + "name": "channelCount", + "description": { + "en": "Total count of channels on your server", + "de": "Anzahl der Kanäle auf dem Server" + } + }, + { + "name": "roleCount", + "description": { + "en": "Total count of roles on your server", + "de": "Anzahl der Rollen auf dem Server" + } + }, + { + "name": "botCount", + "description": { + "en": "Count of Bots on your server", + "de": "Anzahl der Bots auf dem Server" + } + }, + { + "name": "dndCount", + "description": { + "en": "Count of members (not bots) with DND as status", + "de": "Anzahl der Mitglieder (keine Bots) mit Bitte nicht stören als Status" + } + }, + { + "name": "onlineMemberCount", + "description": { + "en": "Count of members (not bots) with online (and only online) as status", + "de": "Anzahl der Mitglieder (keine Bots) mit Online (und NUR Online) als Status" + } + }, + { + "name": "awayCount", + "description": { + "en": "Count of members (not bots) with away status", + "de": "Anzahl der Mitglieder (keine Bots) mit \"Abwesend\" Status" + } + }, + { + "name": "offlineCount", + "description": { + "en": "Count of members (not bots) with offline status", + "de": "Anzahl der Mitglieder (keine Bots) mit \"Offline\" status" + } + }, + { + "name": "guildBoosts", + "description": { + "en": "Show how often this guild was boosted", + "de": "Zeigt, wie oft der Server geboostet wurde" + } + }, + { + "name": "boostLevel", + "description": { + "en": "Shows the current boost-level of this guild", + "de": "Zeigt das aktuelle Boost-Level des Servers" + } + }, + { + "name": "boosterCount", + "description": { + "en": "Count of boosters on this guild", + "de": "Anzahl an Boostern auf dem Server" + } + }, + { + "name": "emojiCount", + "description": { + "en": "Count of emojis on this guild", + "de": "Anzahl an Emojis auf dem Server" + } + }, + { + "name": "currentTime", + "description": { + "en": "Current time and date", + "de": "Aktuelles Datum und Uhrzeit" + } + }, + { + "name": "userWithRoleCount-", + "description": { + "en": "Count of members with a specific role (replace \"\" with an actual role-id)", + "de": "Anzahl von Nutzern mit einer bestimmen Rolle (bitte \"\" mit einer echten Rollen-ID ersetzen)" + } + }, + { + "name": "onlineUserWithRoleCount-", + "description": { + "en": "Count of members with a specific role who are online (replace \"\" with an actual role-id)", + "de": "Anzahl von Nutzern mit einer bestimmen Rolle, die online sind (bitte \"\" mit einer echten Rollen-ID ersetzen)" + } + } + ] + }, + { + "name": "updateInterval", + "humanName": { + "de": "Aktualisierungsintervall", + "en": "Update-Interval" + }, + "default": { + "en": "15", + "de": 15 + }, + "description": { + "en": "You can set an interval here in which the bot should update the channels. Must be higher than seven; in minutes.", + "de": "Du kannst hier ein Intervall einstellen, in welchem der Bot die Kanäle aktualisieren soll. Muss höher als sieben sein; in Minuten." + }, + "type": "integer" + } + ] +} \ No newline at end of file diff --git a/modules/channel-stats/events/botReady.js b/modules/channel-stats/events/botReady.js new file mode 100644 index 00000000..e321a87e --- /dev/null +++ b/modules/channel-stats/events/botReady.js @@ -0,0 +1,73 @@ +const {formatDate} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async (client) => { + const channels = client.configurations['channel-stats']['channels']; + for (const channel of channels) { + const dcChannel = await client.channels.fetch(channel.channelID).catch(() => { + }); + if (!dcChannel) continue; + if (dcChannel.type !== 'GUILD_VOICE' && dcChannel.type !== 'GUILD_CATEGORY') client.logger.warn(`[channel-stats] ` + localize('channel-stats', 'not-voice-channel-info', { + c: dcChannel.name, + id: dcChannel.id, + t: dcChannel.type + })); + const res = await channelNameReplacer(client, dcChannel, channel.channelName); + if (res !== dcChannel.name) dcChannel.setName(res, '[channel-stats] ' + localize('channel-stats', 'audit-log-reason-startup')); + client.intervals.push(setInterval(async () => { + const repName = await channelNameReplacer(client, dcChannel, channel.channelName); + if (repName !== dcChannel.name) dcChannel.setName(repName, '[channel-stats] ' + localize('channel-stats', 'audit-log-reason-interval')); + }, (channel.updateInterval || 5) < 5 * 60000 ? 5 * 60000 : channel.updateInterval * 60000)); + } +}; + +/** + * Replaces the variables in channel names + * @private + * @param {Client} client Client + * @param {Channel} channel Channel + * @param {String} input Input to be replaced + * @return {Promise} + */ +async function channelNameReplacer(client, channel, input) { + const users = await channel.guild.members.fetch(); + const members = users.filter(u => !u.user.bot); + + /** + * Replaces the first member-with-role-count parameters of the input + * @private + */ + function replaceFirst() { + if (input.includes('%userWithRoleCount-')) { + const id = input.split('%userWithRoleCount-')[1].split('%')[0]; + if (input.includes(`%userWithRoleCount-${id}%`)) { + input = input.replaceAll(`%userWithRoleCount-${id}%`, users.filter(f => f.roles.cache.has(id)).size.toString()); + replaceFirst(); + } + } + if (input.includes('%onlineUserWithRoleCount-')) { + const id = input.split('%onlineUserWithRoleCount-')[1].split('%')[0]; + if (input.includes(`%onlineUserWithRoleCount-${id}%`)) { + input = input.replaceAll(`%onlineUserWithRoleCount-${id}%`, users.filter(f => f.roles.cache.has(id) && f.presence && (f.presence || {}).status !== 'offline').size.toString()); + replaceFirst(); + } + } + } + + replaceFirst(); + return input.split('%userCount%').join(users.size) + .split('%memberCount%').join(members.size) + .split('%onlineUserCount%').join(users.filter(u => u.presence && (u.presence || {}).status !== 'offline').size) + .split('%onlineMemberCount%').join(members.filter(u => u.presence && (u.presence || {}).status !== 'offline').size) + .split('%channelCount%').join(channel.guild.channels.cache.size) + .split('%roleCount%').join(channel.guild.roles.cache.size) + .split('%botCount%').join(users.filter(m => m.user.bot).size) + .split('%dndCount%').join(members.filter(u => u.presence && (u.presence || {}).status === 'dnd').size) + .split('%awayCount%').join(members.filter(m => m.presence && (m.presence || {}).status === 'idle').size) + .split('%offlineCount%').join(members.filter(m => !m.presence || (m.presence || {}).status === 'offline').size) + .split('%guildBoosts%').join(channel.guild.premiumSubscriptionCount || '0') + .split('%boostLevel%').join(localize('boostTier', channel.guild.premiumTier)) + .split('%boosterCount%').join(members.filter(m => !!m.premiumSinceTimestamp).size) + .split('%emojiCount%').join(channel.guild.emojis.cache.size) + .split('%currentTime%').join(formatDate(new Date(), true)).trim(); +} \ No newline at end of file diff --git a/modules/channel-stats/module.json b/modules/channel-stats/module.json new file mode 100644 index 00000000..e2a3bf61 --- /dev/null +++ b/modules/channel-stats/module.json @@ -0,0 +1,24 @@ +{ + "name": "channel-stats", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "events-dir": "/events", + "config-example-files": [ + "channels.json" + ], + "tags": [ + "administration" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/channel-stats", + "humanReadableName": { + "en": "Channel-Stats", + "de": "Channel-Statistiken" + }, + "description": { + "en": "Create channels containing stats about your server - updated automatically.", + "de": "Modul, um Channel mit automatisch aktualisierenden Namen mit Statistiken zu erstellen" + } +} \ No newline at end of file diff --git a/modules/color-me/commands/color-me.js b/modules/color-me/commands/color-me.js new file mode 100644 index 00000000..fa21a26f --- /dev/null +++ b/modules/color-me/commands/color-me.js @@ -0,0 +1,258 @@ +const {localize} = require('../../../src/functions/localize'); +const {client} = require('../../../main'); +const {embedType, dateToDiscordTimestamp} = require('../../../src/functions/helpers'); +let roleColor; +let roleIcon; +let pos; +let cooldownModel; +let cancel = false; +let iconW = true; + +module.exports.beforeSubcommand = async function (interaction) { + await interaction.deferReply({ephemeral: true}); +}; + +module.exports.subcommands = { + 'manage': async function (interaction) { + if (interaction.options.getAttachment('icon') !== null) { + if (client.guild.features.includes('ROLE_ICONS')) { + roleIcon = interaction.options.getAttachment('icon').url; + } else { + roleIcon = null; + iconW = false; + } + } + const moduleConf = interaction.client.configurations['color-me']['config']; + const moduleStrings = interaction.client.configurations['color-me']['strings']; + const moduleModel = interaction.client.models['color-me']['Role']; + + if (moduleConf.rolePosition) { + pos = interaction.guild.roles.resolve(moduleConf.rolePosition).position; + } else { + pos = 0; + } + if (await cooldown(moduleConf['updateCooldown'] * 3600000, interaction.user.id)) { + let role = await moduleModel.findOne({ + attributes: ['roleID'], + raw: true, + where: { + userID: interaction.user.id + } + }); + if (role) { + role = role.roleID; + await color(interaction, moduleStrings); + if (cancel) return; + if (interaction.guild.roles.cache.find(r => r.id === role)) { + role = interaction.guild.roles.resolve(role); + role.edit( + { + name: interaction.options.getString('name'), + color: roleColor, + icon: roleIcon, + reason: localize('color-me', 'edit-log-reason', { + user: interaction.user.username + }) + } + ); + if (iconW) { + await interaction.editReply(embedType(moduleStrings['updated'], {})); + } else { + await interaction.editReply(embedType(moduleStrings['updatedNoIcon'], {})); + } + } else { + if (interaction.guild.roles.cache.size < 250) { + role = await interaction.guild.roles.create( + { + name: interaction.options.getString('name'), + color: roleColor, + icon: roleIcon, + hoist: moduleConf.listRoles, + permissions: '', + position: pos, + mentionable: false, + reason: localize('color-me', 'create-log-reason', { + user: interaction.user.username + }) + } + ); + } else { + await interaction.editReply(embedType(moduleStrings['roleLimit'], {})); + } + await moduleModel.update({ + userID: interaction.user.id, + roleID: role.id, + name: role.name, + color: role.hexColor, + timestamp: new Date() + }, { + where: { + userID: interaction.user.id + } + }); + if (!interaction.member.roles.cache.has(role)) { + await interaction.member.roles.add(role); + } + if (iconW) { + await interaction.editReply(embedType(moduleStrings['updated'], {})); + } else { + await interaction.editReply(embedType(moduleStrings['updatedNoIcon'], {})); + } + } + } else { + await color(interaction, moduleStrings); + if (cancel) return; + try { + role = await interaction.guild.roles.create( + { + name: interaction.options.getString('name'), + color: roleColor, + icon: roleIcon, + hoist: moduleConf.listRoles, + permissions: '', + position: pos, + mentionable: false, + reason: localize('color-me', 'create-log-reason', { + user: interaction.user.username + }) + } + ); + await moduleModel.create({ + userID: interaction.user.id, + roleID: role.id, + name: role.name, + color: role.hexColor, + timestamp: new Date() + }); + await interaction.member.roles.add(role); + if (iconW) { + await interaction.editReply(embedType(moduleStrings['created'], {})); + } else { + await interaction.editReply(embedType(moduleStrings['createdNoIcon'], {})); + } + } catch (e) { + await interaction.editReply(embedType(moduleStrings['roleLimit'], {})); + } + + } + } else { + cooldownModel = await moduleModel.findOne({ + where: { + userId: interaction.member.id + } + }); + await interaction.editReply((embedType(moduleStrings['cooldown'], { + '%cooldown%': dateToDiscordTimestamp(new Date(cooldownModel.timestamp.getTime() + moduleConf['updateCooldown'] * 3600000), 'R') + }))); + } + }, + + + 'remove': async function (interaction) { + const moduleStrings = interaction.client.configurations['color-me']['strings']; + const moduleModel = interaction.client.models['color-me']['Role']; + let role = await moduleModel.findOne({ + attributes: ['roleID'], + raw: true, + where: { + userID: interaction.member.id + } + }); + if (role) { + role = role.roleID; + if (interaction.guild.roles.cache.find(r => r.id === role)) { + role = interaction.guild.roles.resolve(role); + role.delete(localize('color-me', 'delete-manual-log-reason', { + user: interaction.member.user.username + })); + await interaction.editReply(await embedType(moduleStrings['removed'], {})); + } + } + } +}; + +module.exports.config = { + name: 'color-me', + description: localize('color-me', 'command-description'), + defaultPermission: false, + options: [ + { + type: 'SUB_COMMAND', + name: 'manage', + description: localize('color-me', 'manage-subcommand-description'), + options: [ + { + type: 'STRING', + required: true, + name: 'name', + description: localize('color-me', 'name-option-description') + }, + { + type: 'STRING', + required: false, + name: 'color', + description: localize('color-me', 'color-option-description') + }, + { + type: 'ATTACHMENT', + required: false, + name: 'icon', + description: localize('color-me', 'icon-option-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'remove', + description: localize('color-me', 'remove-subcommand-description'), + options: [ + { + type: 'BOOLEAN', + required: true, + name: 'confirm', + description: localize('color-me', 'confirm-option-remove-description') + } + ] + } + ] +}; + +/** + * Gets a color from the String of a command option + */ +async function color(interaction, moduleStrings) { + if (interaction.options.getString('color')) { + roleColor = interaction.options.getString('color'); + if (!roleColor.startsWith('#')) { + roleColor = '#' + roleColor; + } + if (!(/^#[0-9A-F]{6}$/i).test(roleColor)) { + await interaction.editReply(await embedType(moduleStrings['invalidColor'], {})); + cancel = true; + } + } else { + roleColor = 'DEFAULT'; + } +} + +/** + ** Function to handle the cooldown stuff + * @private + * @param {number} duration The duration of the cooldown (in ms) + * @param {userId} userId Id of the User + * @returns {Promise} + */ +async function cooldown(duration, userId) { + const model = client.models['color-me']['Role']; + cooldownModel = await model.findOne({ + where: { + userId: userId + } + }); + if (cooldownModel && cooldownModel.timestamp) { + // check cooldown duration + return cooldownModel.timestamp.getTime() + duration <= Date.now(); + } else { + return true; + } +} \ No newline at end of file diff --git a/modules/color-me/configs/config.json b/modules/color-me/configs/config.json new file mode 100644 index 00000000..c5668c2a --- /dev/null +++ b/modules/color-me/configs/config.json @@ -0,0 +1,88 @@ +{ + "description": { + "en": "Configure the function of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "recreateRole", + "humanName": { + "en": "Recreate roles", + "de": "Rollen wiederherstellen" + }, + "default": { + "en": true + }, + "description": { + "en": "Should the role be created again if the user boosts again?", + "de": "Soll die Rolle wiederhergestellt werden, wenn ein Nutzer erneut boostet?" + }, + "type": "boolean" + }, + { + "name": "listRoles", + "humanName": { + "en": "Separate roles in member-list", + "de": "Rollen in Mitgliederliste separieren" + }, + "default": { + "en": false + }, + "description": { + "en": "Should the role be listed separately in the member-list?", + "de": "Soll die Rolle in der Mitgliederliste separat gelistet werden?" + }, + "type": "boolean" + }, + { + "name": "removeOnUnboost", + "humanName": { + "en": "Remove role on unboost", + "de": "Rolle bei Unboost entfernen" + }, + "default": { + "en": false + }, + "description": { + "en": "Should the role be deleted automatically, if the user stops boosting your server? (disable, if also non-boosters should be able to use this command)", + "de": "Soll die Rolle automatisch gelöscht werden, wenn der Nutzer den Server nicht mehr boostet? (deaktivieren, wenn auch nicht-Booster den Befehl verwenden können sollen)" + }, + "type": "boolean" + }, + { + "name": "updateCooldown", + "humanName": { + "en": "Role update cooldown", + "de": "Rollenbearbeitungscooldown" + }, + "default": { + "en": 24 + }, + "description": { + "en": "The amount of time a user needs to wait util they can edit their role again (in hours)", + "de": "Die Dauer, die Benutzer*innen warten müssen, bevor ihre Rolle wieder bearbeitet werden kann (in Stunden)" + }, + "type": "integer" + }, + { + "name": "rolePosition", + "humanName": { + "en": "Role position", + "de": "Rollenposition" + }, + "default": { + "en": "" + }, + "description": { + "en": "The role, beneath which the custom-roles should be created", + "de": "Die Rolle unter welcher die Custom-Rollen erstellt werden sollen" + }, + "type": "roleID" + } + ] +} \ No newline at end of file diff --git a/modules/color-me/configs/strings.json b/modules/color-me/configs/strings.json new file mode 100644 index 00000000..cbc205b2 --- /dev/null +++ b/modules/color-me/configs/strings.json @@ -0,0 +1,158 @@ +{ + "description": { + "en": "Edit the messages and strings of the module here", + "de": "Stelle hier die Nachrichten des Modules ein" + }, + "humanName": { + "en": "Messages", + "de": "Nachrichten" + }, + "filename": "strings.json", + "content": [ + { + "name": "created", + "humanName": { + "en": "Role created", + "de": "Rolle erstellt" + }, + "default": { + "en": "Your role was created successfully.", + "de": "Deine Rolle wurde erfolgreich erstellt." + }, + "description": { + "en": "This messages gets send when a booster sucessfully created their custom role", + "de": "Diese Nachricht wird verschickt, wenn ein Booster seine/ihre Custom-Rolle erstellt hat" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "createdNoIcon", + "humanName": { + "en": "Role created without icon", + "de": "Rolle ohne Icon erstellt" + }, + "default": { + "en": "Your role was created successfully, but your role icon was not used, as this requires the guild to be boost level 2 or higher.", + "de": "Deine Rolle wurde erfolgreich erstellt, aber dein Rollen-Icon wurde nicht verwendet, da hierfür der Server mindestens Boostlevel 2 besitzen muss." + }, + "description": { + "en": "This message gets send when a booster successfully created their custom role, but the guild has not enough boosts to use role icons", + "de": "Diese Nachricht wird verschickt, wenn ein Booster seine/ihre Custom-Rolle erstellt hat, der Server aber nicht ausreichend Boosts hat, um Rollenicons zu verwenden" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "updated", + "humanName": { + "en": "Role updated", + "de": "Rolle aktualisiert" + }, + "default": { + "en": "Your role was updated successfully.", + "de": "Deine Rolle wurde erfolgreich aktualisiert." + }, + "description": { + "en": "This messages gets send when a booster sucessfully updates their custom role", + "de": "Diese Nachricht wird verschickt, wenn ein Booster seine/ihre Custom-Rolle aktualisiert hat" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "updatedNoIcon", + "humanName": { + "en": "Role updated without icon", + "de": "Rolle ohne Icon aktualisiert" + }, + "default": { + "en": "Your role was updated successfully, but your role icon was not used, as this requires the guild to be boost level 2 or higher.", + "de": "Deine Rolle wurde erfolgreich aktualisiert aber dein Rollen-Icon wurde nicht verwendet, da hierfür der Server mindestens Boostlevel 2 besitzen muss." + }, + "description": { + "en": "This messages gets send when a booster sucessfully updates their custom role, but the guild has not enough boosts to use role icons", + "de": "Diese Nachricht wird verschickt, wenn ein Booster seine/ihre Custom-Rolle aktualisiert hat, der Server aber nicht genug boosts hat um Rollenicons zu verwenden" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "removed", + "humanName": { + "en": "Role removed", + "de": "Rolle entfernt" + }, + "default": { + "en": "Your role was removed successfully.", + "de": "Deine Rolle wurde erfolgreich entfernt." + }, + "description": { + "en": "This messages gets send when a booster deleted their custom role", + "de": "Diese Nachricht wird verschickt, wenn ein Booster seine/ihre Custom-Rolle entfernt hat" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "roleLimit", + "humanName": { + "en": "Role-limit reached", + "de": "Rollenlimit erreicht" + }, + "default": { + "en": "Your role couldn't be created. This could be, because this server has reached the maximum of roles set by Discord. Ask the staff to delete an unnecessary role to make space for your role or try again later.", + "de": "Deine Rolle konnte nicht erstellt werden. Das kann daran liegen, dass dieser Server die von Discord vorgegebene maximale Rollenzahl erreicht hat. Frag das Team nach der Löschung einer überflüssigen Rolle um Platz zu machen, oder versuche es später erneut." + }, + "description": { + "en": "This messages gets send when a booster-role couldn't be created", + "de": "Diese Nachricht wird verschickt, wenn eine Booster-Rolle nicht erstellt werden konnte" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "cooldown", + "humanName": { + "en": "Cooldown", + "de": "Cooldown" + }, + "default": { + "en": "Your role couldn't be edited, since you have to wait until %cooldown% for the cooldown to expire.", + "de": "Deine Rolle konnte nicht bearbeitet werden, da du bis %cooldown% warten musst, dass der Cooldown abläuft." + }, + "description": { + "en": "This messages gets send when a booster-role couldn't be edited, since the user is on cooldown", + "de": "Diese Nachricht wird verschickt, wenn eine Booster-Rolle nicht bearbeitet werden konnte, da der Nutzer den Cooldown abwarten muss" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "cooldown", + "description": { + "en": "Timestamp the cooldown expires at", + "de": "Zeitpunkt, an welchem der Cooldown abläuft" + } + } + ] + }, + { + "name": "invalidColor", + "humanName": { + "en": "Invalid Color", + "de": "Falsche Farbe" + }, + "default": { + "en": "The color you provided is not a valid HEX-Code.", + "de": "Die angegebene Farbe ist kein gültiger HEX-Code." + }, + "description": { + "en": "This messages gets send when the user provides a wrong color code", + "de": "Diese Nachricht wird verschickt, wenn der Nutzer einen falschen Farbcode angibt" + }, + "type": "string", + "allowEmbed": true + } + ] +} diff --git a/modules/color-me/events/guildMemberUpdate.js b/modules/color-me/events/guildMemberUpdate.js new file mode 100644 index 00000000..4f6af385 --- /dev/null +++ b/modules/color-me/events/guildMemberUpdate.js @@ -0,0 +1,74 @@ +const {localize} = require('../../../src/functions/localize'); +let pos; + +module.exports.run = async function (client, oldGuildMember, newGuildMember) { + + if (!client.botReadyAt) return; + if (newGuildMember.guild.id !== client.guild.id) return; + + const moduleConf = client.configurations['color-me']['config']; + if (moduleConf.rolePosition) { + pos = newGuildMember.guild.roles.resolve(moduleConf.rolePosition).position; + } else { + pos = 0; + } + + if (moduleConf.removeOnUnboost) { + if (oldGuildMember.premiumSince && !newGuildMember.premiumSince) { + let role = await client.models['color-me']['Role'].findOne({ + attributes: ['roleID'], + raw: true, + where: { + userID: newGuildMember.id + } + }); + if (role) { + role = role.roleID; + if (newGuildMember.guild.roles.cache.find(r => r.id === role)) { + role = newGuildMember.guild.roles.resolve(role); + role.delete(localize('color-me', 'delete-unboost-log-reason', { + user: newGuildMember.user.username + })); + } + } + } + } + if (moduleConf.recreateRole) { + if (!oldGuildMember.premiumSince && newGuildMember.premiumSince) { + const data = await client.models['color-me']['Role'].findOne({ + attributes: ['roleID', 'name', 'color'], + raw: true, + where: { + userID: newGuildMember.id + } + }); + if (data) { + let role = data.roleID; + const name = data.name; + const color = data.color; + if (!newGuildMember.guild.roles.cache.find(r => r.id === role)) { + role = await client.guild.roles.create( + { + name: name, + color: color, + hoist: moduleConf.listRoles, + position: pos, + permissions: '', + mentionable: false, + reason: localize('color-me', 'create-log-reason', { + user: newGuildMember.user.username + }) + } + ); + await client.models['color-me']['Role'].update({ + roleID: role.id + }, { + where: { + userID: newGuildMember.user.id + } + }); + } + } + } + } +}; \ No newline at end of file diff --git a/modules/color-me/models/Role.js b/modules/color-me/models/Role.js new file mode 100644 index 00000000..36beee64 --- /dev/null +++ b/modules/color-me/models/Role.js @@ -0,0 +1,27 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class Role extends Model { + static init(sequelize) { + return super.init({ + id: { + autoIncrement: true, + type: DataTypes.INTEGER, + primaryKey: true + }, + userID: DataTypes.STRING, + roleID: DataTypes.STRING, + name: DataTypes.STRING, + color: DataTypes.STRING, + timestamp: DataTypes.DATE + }, { + tableName: 'colorme_Role', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'Role', + 'module': 'color-me' +}; \ No newline at end of file diff --git a/modules/color-me/module.json b/modules/color-me/module.json new file mode 100644 index 00000000..e64fa361 --- /dev/null +++ b/modules/color-me/module.json @@ -0,0 +1,26 @@ +{ + "name": "color-me", + "humanReadableName": { + "en": "Color me" + }, + "author": { + "name": "hfgd", + "link": "https://github.com/hfgd123", + "scnxOrgID": "2" + }, + "openSourceURL": "https://github.com/hfgd123/CustomDCBot/tree/main/modules/color-me", + "commands-dir": "/commands", + "events-dir": "/events", + "models-dir": "/models", + "config-example-files": [ + "configs/config.json", + "configs/strings.json" + ], + "tags": [ + "community" + ], + "description": { + "en": "Simple module to reward users who have boosted your server with a custom role!", + "de": "Einfaches Modul, um Nutzer mit einer eigenen Rolle zu belohnen, die deinen Server boosten!" + } +} \ No newline at end of file diff --git a/modules/connect-four/commands/connect-four.js b/modules/connect-four/commands/connect-four.js new file mode 100644 index 00000000..3b57a711 --- /dev/null +++ b/modules/connect-four/commands/connect-four.js @@ -0,0 +1,289 @@ +const {localize} = require('../../../src/functions/localize'); +const {MessageActionRow, MessageButton} = require('discord.js'); +const footer = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟']; + +/** + * Builds the game message + * @param {Array} grid + * @param {Integer} fieldSize + * @param {String} color + * @param {String} userTurn + * @param {String} username1 + * @param {String} username2 + * @returns {String} + */ +function gameMessage(grid, fieldSize, color, userTurn, username1, username2) { + return localize('connect-four', 'game-message', { + u1: '**' + username1 + '**', + u2: '**' + username2 + '**', + c: ':' + color + '_circle:', + t: userTurn, + g: grid.map(k => k.join('')).join('\n') + '\n' + footer.slice(0, fieldSize).join('') + }); +} + +/** + * Checks if the user has won diagonally + * @param {Array} grid + * @param {Integer} position + * @param {Integer} y + * @returns {String} + */ +function checkWinDiag(grid, position, y) { + const diagonal = []; + let runningCheck = true; + let runningPush = false; + let i = y - 1; + let j = position - 1; + while (runningCheck) { + i++; + j++; + if (i === grid.length || j === grid.length + 1) { + runningCheck = false; + runningPush = true; + } + } + while (runningPush) { + i--; + j--; + diagonal.push([i, j]); + if (i === 0 || j === -1) runningPush = false; + } + + return diagonal; +} + +/** + * Checks if the user has won diagonally left + * @param {Array} grid + * @param {Integer} position + * @param {Integer} y + * @returns {Array} + */ +function checkWinDiagLeft(grid, position, y) { + const diagonal = []; + let runningCheck = true; + let runningPush = false; + let i = y - 1; + let j = position + 1; + while (runningCheck) { + i++; + j--; + if (i === grid.length || j === -1) { + runningCheck = false; + runningPush = true; + } + } + while (runningPush) { + i--; + j++; + diagonal.push([i, j]); + if (i === 0 || j === grid.length) runningPush = false; + } + + return diagonal; +} + +/** + * Checks for a tie and if a player has won + * @param {Array} grid + * @param {String} color + * @param {Integer} position + * @param {Integer} y + * @returns {String} + */ +function checkWin(grid, color, position, y) { + let streak = []; + for (const i in grid) { + for (const j in grid[i]) { + if (grid[i][j].includes('_circle')) streak.push(grid[i][j]); + else streak = []; + if (streak.length === grid.length * grid[0].length) return 'tie'; + } + } + + const diagonal = [checkWinDiag(grid, position, y), checkWinDiagLeft(grid, position, y)]; + for (const dir in diagonal) { + streak = []; + for (const index in diagonal[dir]) { + const field = diagonal[dir][index]; + if (grid[field[0]][field[1]] === ':' + color + '_circle:') streak.push(field); + else streak = []; + if (streak.length === 4) { + streak.forEach(k => { + grid[k[0]][k[1]] = ':' + color + '_square:'; + }); + return color; + } + } + } + + for (const i in grid) { + streak = []; + for (const j in grid[i]) { + if (grid[i][j] === ':' + color + '_circle:') streak.push([i, j]); + else streak = []; + if (streak.length === 4) { + streak.forEach(k => { + grid[k[0]][k[1]] = ':' + color + '_square:'; + }); + return color; + } + } + } + + streak = []; + for (const i in grid) { + if (grid[i][position] === ':' + color + '_circle:') streak.push([i, position]); + else streak = []; + if (streak.length === 4) { + streak.forEach(k => { + grid[k[0]][k[1]] = ':' + color + '_square:'; + }); + return color; + } + } +} + +module.exports.run = async function (interaction) { + const member = interaction.options.getMember('user'); + if (member.id === interaction.user.id) return interaction.reply({ + content: localize('connect-four', 'challenge-yourself'), + ephemeral: true + }); + if (member.user.bot) return interaction.reply({ + content: localize('connect-four', 'challenge-bot'), + ephemeral: true + }); + + const msg = await interaction.reply({ + content: localize('connect-four', 'challenge-message', {t: member.toString(), u: interaction.user.toString()}), + allowedMentions: { + users: [member.id] + }, + fetchReply: true, + components: [ + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + style: 'PRIMARY', + customId: 'accept-invite', + label: localize('tic-tac-toe', 'accept-invite') + }, + { + type: 'BUTTON', + style: 'SECONDARY', + customId: 'deny-invite', + label: localize('tic-tac-toe', 'deny-invite') + } + ] + } + ] + }); + const confirmed = await msg.awaitMessageComponent({ + filter: i => i.user.id === member.id, + componentType: 'BUTTON', + time: 120000 + }).catch(() => { + }); + if (!confirmed) return msg.edit({ + content: localize('connect-four', 'invite-expired', { + u: interaction.user.toString(), + i: '<@' + member.id + '>' + }), components: [] + }); + if (confirmed.customId === 'deny-invite') return confirmed.update({ + content: localize('connect-four', 'invite-denied', { + u: interaction.user.toString(), + i: '<@' + member.id + '>' + }), components: [] + }); + + const fieldSize = interaction.options.getInteger('field_size') || 7; + + const grid = new Array(fieldSize - 1).fill(); + for (const i in grid) grid[i] = new Array(fieldSize).fill('⬜'); + + const row1 = new MessageActionRow(); + const row2 = new MessageActionRow(); + for (let i = 1; i < fieldSize + 1; i++) { + (i <= 5 ? row1 : row2).addComponents( + new MessageButton() + .setCustomId('c4_' + i) + .setLabel('' + i) + .setStyle('PRIMARY') + ); + } + + let color = Math.random() > 0.5 ? 'red' : 'blue'; + let user = ''; + if (color === 'blue') user = '<@' + interaction.user.id + '>'; + else user = '<@' + member.id + '>'; + + confirmed.update({ + content: gameMessage(grid, fieldSize, color, user, member.user.username, interaction.user.username), + components: fieldSize > 5 ? [row1, row2] : [row1] + }); + + const collector = msg.createMessageComponentCollector({ + componentType: 'BUTTON', + filter: i => i.user.id === interaction.user.id || i.user.id === member.id + }); + collector.on('collect', i => { + if ((color === 'blue' && i.user.id !== interaction.user.id) || (color === 'red' && i.user.id !== member.id)) return i.reply({ + content: localize('connect-four', 'not-turn'), + ephemeral: true + }); + const position = parseInt(i.customId.replace('c4_', '')) - 1; + + for (let j = grid.length - 1; j >= 0; j--) { + if (grid[j][position] === '⬜') { + grid[j][position] = ':' + color + '_circle:'; + const winner = checkWin(grid, color, position, j); + if (winner) { + let wintext = localize('connect-four', 'tie'); + if (winner === 'blue') wintext = localize('connect-four', 'win', {u: '<@' + interaction.user.id + '>'}); + else if (winner === 'red') wintext = localize('connect-four', 'win', {u: '<@' + member.id + '>'}); + + return i.update({ + content: wintext + '\n\n' + grid.map(k => k.join('')).join('\n') + '\n' + footer.slice(0, fieldSize).join(''), + components: [] + }); + } + + if (color === 'blue') { + user = '<@' + member.id + '>'; + color = 'red'; + } else { + user = '<@' + interaction.user.id + '>'; + color = 'blue'; + } + return i.update(gameMessage(grid, fieldSize, color, user, member.user.username, interaction.user.username)); + } + } + }); +}; + + +module.exports.config = { + name: 'connect-four', + description: localize('connect-four', 'command-description'), + + options: [ + { + type: 'USER', + name: 'user', + description: localize('tic-tac-toe', 'user-description'), + required: true + }, + { + type: 'INTEGER', + name: 'field_size', + description: localize('connect-four', 'field-size-description'), + minValue: 4, + maxValue: 10 + } + ] +}; \ No newline at end of file diff --git a/modules/connect-four/module.json b/modules/connect-four/module.json new file mode 100644 index 00000000..cbfbf594 --- /dev/null +++ b/modules/connect-four/module.json @@ -0,0 +1,23 @@ +{ + "name": "connect-four", + "humanReadableName": { + "en": "Connect Four", + "de": "Vier gewinnt" + }, + "author": { + "scnxOrgID": "60", + "name": "TomatoCake", + "link": "https://github.com/DEVTomatoCake" + }, + "description": { + "en": "Let your users play Connect Four against each other!", + "de": "Lasse Nutzer auf deinem Server Vier gewinnt gegeneinander spielen" + }, + "commands-dir": "/commands", + "noConfig": true, + "releaseDate": "0", + "tags": [ + "fun" + ], + "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/connect-four" +} \ No newline at end of file diff --git a/modules/counter/config.json b/modules/counter/config.json new file mode 100644 index 00000000..81ec6017 --- /dev/null +++ b/modules/counter/config.json @@ -0,0 +1,230 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "channels", + "humanName": { + "de": "Kanäle", + "en": "Channels" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "ID of channels with the counter game", + "de": "ID der Kanäle mit dem Zählspiel" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "channelDescription", + "humanName": { + "de": "Kanalbeschreibung", + "en": "Channel-Description" + }, + "default": { + "en": "Next number %x%", + "de": "Nächste Zahl: %x%" + }, + "description": { + "en": "Text which should be set after someone counted (leave blank to disable)", + "de": "Text, welcher gesetzt werden soll, nachdem jemand gezählt hat (leer lassen zum deaktivieren)" + }, + "type": "string", + "allowNull": true, + "params": [ + { + "name": "x", + "description": { + "en": "Next number users should count", + "de": "Nächste Zahl, welche die Nutzer zählen sollen" + } + } + ] + }, + { + "name": "success-reaction", + "humanName": { + "de": "Erfolgsreaktion", + "en": "Success-Reaction" + }, + "default": { + "en": "✅", + "de": "✅" + }, + "description": { + "en": "Reaction which the bot should give when someone counts successfully", + "de": "Reaktion welche der Bot geben soll, wenn jemand erfolgreich gezählt hat" + }, + "type": "emoji" + }, + { + "name": "restartOnWrongCount", + "default": { + "en": false + }, + "humanName": { + "de": "Spiel neustarten, wenn sich jemand verzählt", + "en": "Restart game, if user miscounts" + }, + "description": { + "en": "If enabled, the game will restarts if a user sends a number that is not in ordner", + "de": "Wenn aktiviert, wird das Spiel neustarten, wenn ein Nutzer eine Zahl sendet, die nicht in die Reihenfolge passt" + }, + "type": "boolean" + }, + { + "name": "restartOnWrongCountMessage", + "dependsOn": "restartOnWrongCount", + "default": { + "de": "Aufgrund der Inkompetenz von %mention% muss das Spiel neugestartet werden - die nächste Zahl ist **%i%**.", + "en": "Due to the incompetence of %mention%, the game had to restart - the next number is **%i%**." + }, + "humanName": { + "en": "Message when game gets restarted", + "de": "Nachricht, wenn das Spiel neugestartet werden" + }, + "type": "string", + "allowEmbed": true, + "description": { + "en": "This message will be sent when the game gets restarted due to a miscount.", + "de": "Diese Nachricht wird gesendet, wenn das Spiel aufgrund einer Verzählung neugestartet wird." + }, + "params": [ + { + "name": "mention", + "description": { + "de": "Erwähnung des Nutzers", + "en": "Mention of the users" + } + }, + { + "name": "i", + "description": { + "de": "Nächste Nummer", + "en": "Next number" + } + } + ] + }, + { + "name": "onlyOneMessagePerUser", + "default": { + "en": true + }, + "humanName": { + "de": "Nutzer müssen abwechselnd zählen", + "en": "Only one continuous message per user" + }, + "description": { + "en": "If enabled, users can not count more than one number continuously", + "de": "Wenn aktiviert, können Nutzer nicht mehr als eine Nummer nacheinander zählen" + }, + "type": "boolean" + }, + { + "name": "wrong-input-message", + "humanName": { + "de": "Nachricht bei falscher Eingabe", + "en": "Message on wrong input" + }, + "default": { + "en": "⚠️ %err%" + }, + "description": { + "en": "Message that gets send if a user provides an invalid input", + "de": "Nachricht, welche gesendet wird, wenn ein Nutzer eine ungültige Nachricht sendet" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "err", + "description": { + "en": "Description of what they did wrong", + "de": "Beschreibung, was der Nutzer falsch gemacht hat" + } + } + ] + }, + { + "name": "strikeAmount", + "default": { + "en": 5 + }, + "humanName": { + "de": "Amount of wrong messages to trigger action", + "en": "Anzahl von falschen Nachrichten, um eine Aktion auszulösen" + }, + "description": { + "en": "This is the amount of wrong messages a user has to send to trigger action. Once this amount is reached, the bot will either, depending on your configuration, give a role or disable the SEND_MESSAGES permission for a user (set to 0 to disable)", + "de": "Dies ist die Anzahl von falschen Nachrichten, die ein Nutzer senden muss, um eine Aktion auszulösen. Sobald diese Anzahl erreicht ist, wird der Bot, je nach Konfiguration, entweder dem Nutzer eine Rolle geben oder ihm die \"Nachrichten verfassen\"-Berechtigung entfernen (auf 0 setzen zum Deaktivieren)" + }, + "type": "integer" + }, + { + "name": "giveRoleInsteadOfPermissionRemoval", + "default": { + "en": false + }, + "humanName": { + "de": "Rolle bei Sperrung vergeben, anstatt Rechte zu entfernen", + "en": "Give role on action, instead of removing permission" + }, + "description": { + "en": "If enabled, a role will be given to the user (once their reach the configured action amount of wrong messages) instead of the removal of the \"Send Messages\"-permission in the counter channel", + "de": "Wenn aktiviert, wird dem Nutzer (sobald er die benötigte Anzahl von falschen Nachrichten erreicht hat) eine Rolle gegeben, anstatt die \"Nachrichten verfassen\"-Berechtigung im Kanal zu entfernen" + }, + "type": "boolean" + }, + { + "name": "strikeRole", + "dependsOn": "giveRoleInsteadOfPermissionRemoval", + "default": { + "en": false + }, + "humanName": { + "de": "Rolle, die bei Sperrung vergeben wird", + "en": "Role given when amount is being reached" + }, + "description": { + "en": "This role will be given to users when they reach the configured amount of wrong messages", + "de": "Diese Rolle wird dem Nutzer gegeben, sobald die konfigurierte Anzahl von falschen Nachrichten erreicht wird" + }, + "type": "roleID" + }, + { + "name": "strikeMessage", + "default": { + "de": "%mention%, ich musste dir den Zugriff auf diesen Kanal verbieten, da du ihn mehrmals falsch verwendet hast.", + "en": "%mention%, I had to restrict your access to this channel because you repeatedly used it improperly." + }, + "humanName": { + "en": "Message when user gets actioned", + "de": "Nachricht, wenn ein Nutzer gesperrt wird" + }, + "type": "string", + "allowEmbed": true, + "description": { + "en": "This message will be sent when a user reach the configured amount of wrong messages and gets actioned", + "de": "Diese Rolle wird versendet, sobald die konfigurierte Anzahl von falschen Nachrichten erreicht wird und ein Nutzer bestraft wird" + }, + "params": [ + { + "name": "mention", + "description": { + "de": "Erwähnung des Nutzers", + "en": "Mention of the users" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/counter/events/botReady.js b/modules/counter/events/botReady.js new file mode 100644 index 00000000..f362ab84 --- /dev/null +++ b/modules/counter/events/botReady.js @@ -0,0 +1,19 @@ +const {localize} = require('../../../src/functions/localize'); +module.exports.run = async function(client) { + const moduleConfig = client.configurations['counter']['config']; + for (const cID of moduleConfig['channels']) { + const channel = await client.models['counter']['CountChannel'].findOne({ + where: { + channelID: cID + } + }); + if (!channel) { + await client.models['counter']['CountChannel'].create({ + channelID: cID, + currentNumber: 0, + userCounts: {} + }); + client.logger.debug('[counter] ' + localize('counter', 'created-db-entry', {i: cID})); + } + } +}; \ No newline at end of file diff --git a/modules/counter/events/messageCreate.js b/modules/counter/events/messageCreate.js new file mode 100644 index 00000000..1e51ecf0 --- /dev/null +++ b/modules/counter/events/messageCreate.js @@ -0,0 +1,98 @@ +const {localize} = require('../../../src/functions/localize'); +const {embedType} = require('../../../src/functions/helpers'); + +const invalidMessages = {}; + +module.exports.run = async function (client, msg) { + if (!client.botReadyAt) return; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + if (!msg.member) return; + if (msg.author.bot) return; + + const moduleConfig = client.configurations['counter']['config']; + if (!moduleConfig.channels.includes(msg.channel.id)) return; + const object = await client.models['counter']['CountChannel'].findOne({ + where: { + channelID: msg.channel.id + } + }); + if (!object) return; + + if (!parseInt(msg.content)) return wrongMessage(localize('counter', 'not-a-number')); + if (object.lastCountedUser === msg.author.id && moduleConfig.onlyOneMessagePerUser) return wrongMessage(localize('counter', 'only-one-message-per-person')); + if (parseInt(object.currentNumber) + 1 !== parseInt(msg.content)) { + if (parseInt(object.currentNumber) !== parseInt(msg.content) && moduleConfig.restartOnWrongCount) { + object.currentNumber = 0; + object.lastCountedUser = null; + object.userCounts = {}; + await object.save(); + invalidMessages[msg.author.id]++; + return msg.reply(embedType(moduleConfig.restartOnWrongCountMessage, { + '%i%': 1, + '%mention%': msg.author.toString() + })); + } + return wrongMessage(localize('counter', 'not-the-next-number', {n: parseInt(object.currentNumber) + 1}), true); + } + + object.currentNumber++; + object.lastCountedUser = msg.author.id; + const userCounts = object.userCounts; + object.userCounts = {}; + if (!userCounts[msg.author.id]) userCounts[msg.author.id] = 0; + userCounts[msg.author.id]++; + object.userCounts = userCounts; + await object.save(); + const benefits = client.configurations['counter']['milestones']; + for (const benefit of benefits.filter(b => parseInt(b.userMessageCount) === userCounts[msg.author.id])) { + if (benefit.giveRoles.length !== 0) await msg.member.roles.add(benefit.giveRoles); + if (benefit.sendMessage) { + const ben = await msg.reply(embedType(benefit.sendMessage)); + setTimeout(() => { + ben.delete(); + }, 5000); + } + } + + let reactions; + if (msg.content === '42') reactions = [await msg.react('❓')]; + else if (msg.content === '420') reactions = [await msg.react('🚬')]; + else if (msg.content === '100') reactions = [await msg.react('💯')]; + else if (msg.content === '112' || msg.content === '911') reactions = [await msg.react('🚑')]; + else if (msg.content === '69') reactions = [await msg.react('🇳'), await msg.react('🇮'), await msg.react('🇨'), await msg.react('🇪')]; + else reactions = [await msg.react(moduleConfig['success-reaction'])]; + setTimeout(async () => { + for (const reaction of reactions) await reaction.remove(); + }, 5000); + if (moduleConfig.channelDescription) await msg.channel.setTopic(moduleConfig.channelDescription.split('%x%').join(object.currentNumber + 1), '[counter] ' + localize('counter', 'channel-topic-change-reason')); + + /** + * Tells the user that they did something wrong + * @private + * @param {String} reason Reason for their warning + * @param {Boolean} skipStrike If enabled, the user won't receive a strike + * @return {Promise} + */ + async function wrongMessage(reason, skipStrike = false) { + const answer = await msg.reply(embedType(moduleConfig['wrong-input-message'], {'%err%': reason})); + setTimeout(async () => { + await answer.delete(); + await msg.delete(); + }, 8000); + if (!skipStrike || parseInt(moduleConfig.strikeAmount) === 0) return; + console.log(invalidMessages); + if (!invalidMessages[msg.author.id]) invalidMessages[msg.author.id] = 0; + invalidMessages[msg.author.id]++; + if (invalidMessages[msg.author.id] >= parseInt(moduleConfig.strikeAmount)) { + if (moduleConfig.giveRoleInsteadOfPermissionRemoval) await msg.member.roles.add(moduleConfig.strikeRole, '[counter] ' + localize('counter', 'restriction-audit-log')); + else await msg.channel.permissionOverwrites.create(msg.author, { + SEND_MESSAGES: false + }, {reason: '[counter] ' + localize('counter', 'restriction-audit-log')}); + const ban = await answer.reply(embedType(moduleConfig.strikeMessage, {'%mention%': msg.author.toString()})); + setTimeout(async () => { + await ban.delete(); + }, 8000); + } + } +}; \ No newline at end of file diff --git a/modules/counter/milestones.json b/modules/counter/milestones.json new file mode 100644 index 00000000..c73c9e61 --- /dev/null +++ b/modules/counter/milestones.json @@ -0,0 +1,70 @@ +{ + "description": { + "en": "Reward your users, when they reach certain goals", + "de": "Belohne deine Nutzer, wenn diese bestimmte Ziele erreichen" + }, + "humanName": { + "en": "Milestones", + "de": "Ziele" + }, + "configElementName": { + "de": { + "one": "Ziel", + "more": "Ziele" + }, + "en": { + "one": "Milestone", + "more": "Milestones" + } + }, + "filename": "milestones.json", + "configElements": true, + "content": [ + { + "name": "userMessageCount", + "humanName": { + "de": "Nachrichtenzahl" + }, + "default": { + "en": "" + }, + "description": { + "en": "Count of valid counter-messages the users has to archive this goal", + "de": "Anzahl der gültigen Zähl-Nachrichten, die der Nutzer schreiben muss, um dieses Ziel zu erreichen" + }, + "type": "integer" + }, + { + "name": "giveRoles", + "humanName": { + "de": "Rollen" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "These roles are given to the user if they archive this goal (optional)", + "de": "Diese Rollen werden an den Nutzer vergeben, wenn er dieses Ziel erreicht (optional)" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "sendMessage", + "humanName": { + "de": "Nachricht" + }, + "default": { + "en": "" + }, + "description": { + "en": "This message gets send when they archive this goal", + "de": "Diese Nachricht wird gesendet, wenn er dieses Ziel erreicht" + }, + "type": "string", + "allowNull": true, + "allowEmbed": true + } + ] +} \ No newline at end of file diff --git a/modules/counter/models/CountChannel.js b/modules/counter/models/CountChannel.js new file mode 100644 index 00000000..fb297400 --- /dev/null +++ b/modules/counter/models/CountChannel.js @@ -0,0 +1,27 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class CountChannel extends Model { + static init(sequelize) { + return super.init({ + channelID: { + type: DataTypes.STRING, + primaryKey: true + }, + currentNumber: DataTypes.INTEGER, + lastCountedUser: DataTypes.STRING, + userCounts: { + type: DataTypes.JSON, + defaultValue: {} + } + }, { + tableName: 'counter_countChannel', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'CountChannel', + 'module': 'counter' +}; \ No newline at end of file diff --git a/modules/counter/module.json b/modules/counter/module.json new file mode 100644 index 00000000..9d9b7f0e --- /dev/null +++ b/modules/counter/module.json @@ -0,0 +1,26 @@ +{ + "name": "counter", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "events-dir": "/events", + "models-dir": "/models", + "config-example-files": [ + "config.json", + "milestones.json" + ], + "tags": [ + "fun" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/counter", + "humanReadableName": { + "en": "Count-Game", + "de": "Zähl-Spiel" + }, + "description": { + "en": "Allow your users to count together", + "de": "Erlaubt es deinen Nutzern, zusammen zu zählen!" + } +} \ No newline at end of file diff --git a/modules/duel/commands/duel.js b/modules/duel/commands/duel.js new file mode 100644 index 00000000..d5aedf78 --- /dev/null +++ b/modules/duel/commands/duel.js @@ -0,0 +1,194 @@ +const {localize} = require('../../../src/functions/localize'); +const {MessageEmbed} = require('discord.js'); + +module.exports.run = async function (interaction) { + const member = interaction.options.getMember('user', true); + if (member.user.id === interaction.user.id) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('duel', 'self-invite-not-possible', {r: `<@${((await interaction.guild.members.fetch({withPresences: true})).filter(u => u.presence && u.user.id !== interaction.user.id && !u.user.bot).random() || {user: {id: 'RickAstley'}}).user.id}>`}) + }); + const rep = await interaction.reply({ + content: localize('duel', 'challenge-message', { + t: member.toString(), + u: interaction.user.toString() + }) + '\n*' + localize('duel', 'how-does-this-game-work') + '*', + allowedMentions: { + users: [member.user.id] + }, + fetchReply: true, + components: [ + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + style: 'PRIMARY', + customId: 'duel-accept-invite', + label: localize('duel', 'accept-invite') + }, + { + type: 'BUTTON', + style: 'SECONDARY', + customId: 'duel-deny-invite', + label: localize('duel', 'deny-invite') + } + ] + } + ] + }); + let started = false; + let ended = false; + let endReason = null; + let currentAnswers = {}; + const bullets = {}; + const guardAfterEachOther = {}; + bullets[interaction.user.id] = 0; + bullets[member.user.id] = 0; + guardAfterEachOther[interaction.user.id] = 0; + guardAfterEachOther[member.user.id] = 0; + const a = rep.createMessageComponentCollector({componentType: 'BUTTON'}); + setTimeout(() => { + if (started || a.ended) return; + endReason = localize('duel', 'invite-expired', {u: interaction.user.toString(), i: member.toString()}); + a.stop(); + }, 120000); + + let lastRoundString = ''; + + a.on('collect', (i) => { + if (!started) { + if (i.user.id !== member.id) return i.reply({ + ephemeral: true, + content: '⚠️ ' + localize('duel', 'you-are-not-the-invited-one') + }); + if (i.customId === 'duel-deny-invite') { + endReason = localize('duel', 'invite-denied', { + u: interaction.user.toString(), + i: member.toString() + }); + return a.stop(); + } + started = true; + } + + if (!i.customId.includes('invite')) { + if (i.user.id !== interaction.user.id && i.user.id !== member.user.id) return i.reply({ + ephemeral: true, + content: '⚠️ ' + localize('duel', 'not-your-game') + }); + const action = i.customId.replaceAll('duel-', ''); + if (currentAnswers[i.user.id]) { + if (currentAnswers[i.user.id] === 'gun') bullets[i.user.id]++; + if (currentAnswers[i.user.id] === 'reload') bullets[i.user.id]--; + } + if (action === 'reload') { + if (bullets[i.user.id] === 5) return i.reply({ + ephemeral: true, + content: '⚠️ ' + localize('duel', 'bullets-full') + }); + bullets[i.user.id]++; + } + if (action === 'gun') { + if (bullets[i.user.id] === 0) return i.reply({ + ephemeral: true, + content: '⚠️ ' + localize('duel', 'no-bullets') + }); + else bullets[i.user.id]--; + } + currentAnswers[i.user.id] = action; + + if (currentAnswers[member.user.id] && currentAnswers[interaction.user.id]) { + guardAfterEachOther[member.user.id] = currentAnswers[member.user.id] === 'guard' ? (guardAfterEachOther[member.user.id] + 1) : 0; + guardAfterEachOther[interaction.user.id] = currentAnswers[interaction.user.id] === 'guard' ? (guardAfterEachOther[interaction.user.id] + 1) : 0; + let guardOver = false; + if (currentAnswers[member.user.id] === 'gun' && guardAfterEachOther[interaction.user.id] >= 5) currentAnswers[interaction.user.id] = 'reload'; + if (currentAnswers[interaction.user.id] === 'gun' && guardAfterEachOther[member.user.id] >= 5) currentAnswers[member.user.id] = 'reload'; + if ((currentAnswers[interaction.user.id] === 'gun' && guardAfterEachOther[member.user.id] >= 5) || currentAnswers[member.user.id] === 'gun' && guardAfterEachOther[interaction.user.id] >= 5) guardOver = true; + const answers = [currentAnswers[member.user.id], currentAnswers[interaction.user.id]].sort((a, b) => ['reload', 'guard', 'gun'].indexOf(a) - ['reload', 'guard', 'gun'].indexOf(b)); + const params = {}; + const actionTo = { + 'reload': 'r', + 'guard': 'd', + 'gun': 'g' + }; + params[actionTo[currentAnswers[member.user.id]] + '1'] = member.user.toString(); + params[actionTo[currentAnswers[interaction.user.id]] + (params[actionTo[currentAnswers[interaction.user.id]] + '1'] ? '2' : '1')] = interaction.user.toString(); + lastRoundString = localize('duel', (guardOver ? 'guard-over-' : '') + answers.join('-'), params); + if (answers.join('-') === 'reload-gun') ended = true; + currentAnswers = {}; + } + } + + + let stateString = '\n\n' + localize('duel', 'what-do-you-want-to-do') + `\n${member.toString()}: ${localize('duel', currentAnswers[member.user.id] ? 'ready' : 'pending')}\n${interaction.user.toString()}: ${localize('duel', currentAnswers[interaction.user.id] ? 'ready' : 'pending')}\n\n${localize('duel', 'continues-info')}`; + + let mentions = undefined; + if (!ended && !currentAnswers[interaction.user.id] && currentAnswers[member.user.id]) mentions = [interaction.user.id]; + if (!ended && !currentAnswers[member.user.id] && currentAnswers[interaction.user.id]) mentions = [member.user.id]; + const embed = new MessageEmbed() + .setTitle(localize('duel', ended ? 'game-ended' : 'game-running-header')) + .setColor(ended ? 0x2ECC71 : (!mentions ? 0xD35400 : 0xE67E22)) + .setDescription(lastRoundString + (!ended ? stateString : '\n\n' + localize('duel', 'ended-state')) + '\n*' + localize('duel', 'how-does-this-game-work') + '*') + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}); + + i.update({ + content: ended ? 'GGs!' : `<@${member.user.id}> vs <@${interaction.user.id}>`, + embeds: [ + embed + ], + allowedMentions: { + users: mentions + }, + components: ended ? [] : [ + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + customId: 'duel-gun', + style: 'SECONDARY', + emoji: '🔫', + label: localize('duel', 'use-gun') + }, + { + type: 'BUTTON', + customId: 'duel-guard', + style: 'SECONDARY', + emoji: '🛡️', + label: localize('duel', 'guard') + }, + { + type: 'BUTTON', + customId: 'duel-reload', + style: 'SECONDARY', + emoji: '🔄', + label: localize('duel', 'reload') + } + ] + } + ] + }); + }); + a.on('end', () => { + rep.edit({ + content: endReason, + components: [] + }); + } + ); +}; + + +module.exports.config = { + name: 'duel', + description: localize('duel', 'command-description'), + + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('duel', 'user-description') + } + ] +}; \ No newline at end of file diff --git a/modules/duel/module.json b/modules/duel/module.json new file mode 100644 index 00000000..09eaf569 --- /dev/null +++ b/modules/duel/module.json @@ -0,0 +1,23 @@ +{ + "name": "duel", + "humanReadableName": { + "en": "Duel" + }, + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "description": { + "en": "Let users play the game \"Duel\" on your discord", + "de": "Erlaubt es deinen Nutzern, das Spiel \"Duel\" auf deinem Discord zu spielen" + }, + "commands-dir": "/commands", + "noConfig": true, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/duel", + "tags": [ + "fun" + ], + "earlyAccess": false, + "holidayGift": true +} \ No newline at end of file diff --git a/modules/economy-system/cli.js b/modules/economy-system/cli.js new file mode 100644 index 00000000..ecc38dd6 --- /dev/null +++ b/modules/economy-system/cli.js @@ -0,0 +1,61 @@ +const {editBalance} = require('../economy-system/economy-system'); + +module.exports.commands = [ + { + command: 'add', + description: 'Add xyz to the balance of a user. (args: 1. UserId, 2. amount to add)', + run: function (input) { + const client = input.client; + const args = input.args; + client.logger.debug(`Received CLI Command: ${input}`); + if (!client.configurations['economy-system']['config']['allowCheats']) return console.log('This command isn`t activated.'); + editBalance(client, args[1], 'add', parseInt(args[2])); + client.logger.info(`[economy-system] ${args[2]} has been added to the balance of the user ${args[1]}`); + if (client.logChannel) client.logChannel.send(`[economy-system] ${args[2]} has been added to the balance of the user ${args[1]}`); + } + }, + { + command: 'remove', + description: 'Remove xyz fom the balance of a user. (args: 1. UserId, 2. amount to remove)', + run: function (input) { + const client = input.client; + const args = input.args; + client.logger.debug(`Receved CLI Command: ${input}`); + if (!client.configurations['economy-system']['config']['allowCheats']) return console.log('This command isn`t activated.'); + editBalance(client, args[1], 'remove', parseInt(args[2])); + client.logger.info(`[economy-system] ${args[2]} has been removed from the balance of the user ${args[1]}`); + if (client.logChannel) client.logChannel.send(`[economy-system] ${args[2]} has been removed from the balance of the user ${args[1]}`); + } + }, + { + command: 'set', + description: 'Set the balance of a user to xyz. (args: 1. UserId, 2. new balance)', + run: function (input) { + const client = input.client; + const args = input.args; + client.logger.debug(`Receved CLI Command: ${input}`); + if (!client.configurations['economy-system']['config']['allowCheats']) return console.log('This command isn`t activated.'); + editBalance(client, args[1], 'set', parseInt(args[2])); + client.logger.info(`[economy-system] The balance of the user ${args[1]} has been set to ${args[2]}`); + if (client.logChannel) client.logChannel.send(`[economy-system] The balance of the user ${args[1]} has been set to ${args[2]}`); + } + }, + { + command: 'balance', + description: 'Show all balances from the DataBase', + run: async function (input) { + input.client.logger.debug(`Receved CLI Command: ${input}`); + const balances = await input.client.models['economy-system']['Balance'].findAll(); + const balanceArr = []; + if (balances.length !== 0) { + balances.sort(function (x, y) { + return y.dataValues.balance - x.dataValues.balance; + }); + for (let i = 0; i < balances.length; i++) { + balanceArr.push({ id: balances[i].dataValues.id, balance: balances[i].dataValues.balance }); + } + } + console.table(balanceArr); + } + } +]; \ No newline at end of file diff --git a/modules/economy-system/commands/economy-system.js b/modules/economy-system/commands/economy-system.js new file mode 100644 index 00000000..c444da32 --- /dev/null +++ b/modules/economy-system/commands/economy-system.js @@ -0,0 +1,540 @@ +const {editBalance, editBank, createLeaderboard} = require('../economy-system'); +const { + embedType, + randomIntFromInterval, + randomElementFromArray, + formatDiscordUserName +} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.beforeSubcommand = async function (interaction) { + interaction.str = interaction.client.configurations['economy-system']['strings']; + interaction.config = interaction.client.configurations['economy-system']['config']; +}; + +/** + * Function to handle the cooldown stuff + * @private + * @param {string} command The command + * @param {int} duration The duration of the cooldown (in ms) + * @param {userId} userId Id of the User + * @param {Client} client Client + * @returns {Promise} + */ +async function cooldown (command, duration, userId, client) { + const model = client.models['economy-system']['cooldown']; + const cooldownModel = await model.findOne({ + where: { + userId: userId, + command: command + } + }); + if (cooldownModel) { + // check cooldown duration + if (cooldownModel.timestamp.getTime() + duration > Date.now()) return false; + cooldownModel.timestamp = new Date(); + await cooldownModel.save(); + return true; + } else { + // create the model + await model.create({ + userId: userId, + command: command, + timestamp: new Date() + }); + return true; + } +} + +module.exports.subcommands = { + 'work': async function (interaction) { + if (!await cooldown('work', interaction.config['workCooldown'] * 60000, interaction.user.id, interaction.client)) return interaction.reply(embedType(interaction.str['cooldown'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + const moneyToAdd = randomIntFromInterval(parseInt(interaction.config['maxWorkMoney']), parseInt(interaction.config['minWorkMoney'])); + await editBalance(interaction.client, interaction.user.id, 'add', moneyToAdd); + interaction.reply(embedType(randomElementFromArray(interaction.str['workSuccess']), {'%earned%': `${moneyToAdd} ${interaction.config['currencySymbol']}`}, {ephemeral: !interaction.config['publicCommandReplies']})); + createLeaderboard(interaction.client); + interaction.client.logger.info('[economy-system] ' + localize('economy-system', 'work-earned-money', { + u: formatDiscordUserName(interaction.user), + m: moneyToAdd, + c: interaction.config['currencySymbol'] + })); + if (interaction.client.logChannel) interaction.client.logChannel.send('[economy-system] ' + localize('economy-system', 'work-earned-money', { + u: formatDiscordUserName(interaction.user), + m: moneyToAdd, + c: interaction.config['currencySymbol'] + })); + }, + 'crime': async function (interaction) { + if (!await cooldown('crime', interaction.config['crimeCooldown'] * 60000, interaction.user.id, interaction.client)) return interaction.reply(embedType(interaction.str['cooldown'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + let money; + if (Math.floor(Math.random() * 2) === 0) { + const user = await interaction.client.models['economy-system']['Balance'].findOne({ + where: { + id: interaction.user.id + } + }); + money = (user?.balance || 0) / 2; + if (money === 0) { + await editBank(interaction.client, interaction.user.id, 'remove', parseInt(interaction.config['maxCrimeMoney'])); + } else { + await editBalance(interaction.client, interaction.user.id, 'remove', money); + } + interaction.reply(embedType(randomElementFromArray(interaction.str['crimeFail']), {'%loose%': `${money} ${interaction.config['currencySymbol']}`}, {ephemeral: !interaction.config['publicCommandReplies']})); + interaction.client.logger.info('[economy-system] ' + localize('economy-system', 'crime-loose-money', { + u: formatDiscordUserName(interaction.user), + m: money, + c: interaction.config['currencySymbol'] + })); + if (interaction.client.logChannel) interaction.client.logChannel.send('[economy-system] ' + localize('economy-system', 'crime-loose-money', { + u: formatDiscordUserName(interaction.user), + m: money, + c: interaction.config['currencySymbol'] + })); + } else { + const money = randomIntFromInterval(parseInt(interaction.config['maxCrimeMoney']), parseInt(interaction.config['minCrimeMoney'])); + await editBalance(interaction.client, interaction.user.id, 'add', money); + interaction.reply(embedType(randomElementFromArray(interaction.str['crimeSuccess']), {'%earned%': `${money} ${interaction.config['currencySymbol']}`}, {ephemeral: !interaction.config['publicCommandReplies']})); + createLeaderboard(interaction.client); + interaction.client.logger.info('[economy-system] ' + localize('economy-system', 'crime-earned-money', { + u: formatDiscordUserName(interaction.user), + m: money, + c: interaction.config['currencySymbol'] + })); + if (interaction.client.logChannel) interaction.client.logChannel.send('[economy-system] ' + localize('economy-system', 'crime-earned-money', { + u: formatDiscordUserName(interaction.user), + m: money, + c: interaction.config['currencySymbol'] + })); + } + }, + 'rob': async function (interaction) { + const user = await interaction.options.getUser('user'); + const robbedUser = await interaction.client.models['economy-system']['Balance'].findOne({ + where: { + id: user.id + } + }); + if (!robbedUser) return interaction.reply(embedType(interaction.str['userNotFound']), {'%user%': formatDiscordUserName(user)}, {ephemeral: !interaction.config['publicCommandReplies']}); + if (!await cooldown('rob', interaction.config['robCooldown'] * 60000, interaction.user.id, interaction.client)) return interaction.reply(embedType(interaction.str['cooldown'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + let toRob = parseInt(robbedUser.balance) * (parseInt(interaction.config['robPercent']) / 100); + if (toRob >= parseInt(interaction.config['maxRobAmount'])) toRob = parseInt(interaction.config['maxRobAmount']); + await editBalance(interaction.client, interaction.user.id, 'add', toRob); + await editBalance(interaction.client, user.id, 'remove', toRob); + interaction.reply(embedType(interaction.str['robSuccess'], { + '%earned%': `${toRob} ${interaction.config['currencySymbol']}`, + '%user%': `<@${user.id}>` + }, {ephemeral: !interaction.config['publicCommandReplies']})); + createLeaderboard(interaction.client); + interaction.client.logger.info('[economy-system] ' + localize('economy-system', 'crime-earned-money', { + u: formatDiscordUserName(interaction.user), + v: formatDiscordUserName(user), + m: toRob, + c: interaction.config['currencySymbol'] + })); + if (interaction.client.logChannel) interaction.client.logChannel.send('[economy-system] ' + localize('economy-system', 'crime-earned-money', { + v: formatDiscordUserName(user), + u: formatDiscordUserName(interaction.user), + m: toRob, + c: interaction.config['currencySymbol'] + })); + }, + 'add': async function (interaction) { + if (!interaction.client.configurations['economy-system']['config']['admins'].includes(interaction.user.id) && !interaction.client.config['botOperators'].includes(interaction.user.id)) return interaction.reply(embedType(interaction.client.strings['not_enough_permissions'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + console.log(interaction.options.getUser('user').id); + console.log(interaction.user.id); + if (interaction.options.getUser('user').id === interaction.user.id && !interaction.client.configurations['economy-system']['config']['selfBalance']) { + if (interaction.client.logChannel) interaction.client.logChannel.send(localize('economy-system', 'admin-self-abuse')); + return interaction.reply({ + content: localize('economy-system', 'admin-self-abuse-answer'), + ephemeral: !interaction.config['publicCommandReplies'] + }); + } + await editBalance(interaction.client, await interaction.options.getUser('user').id, 'add', parseInt(interaction.options.get('amount')['value'])); + interaction.reply({ + content: localize('economy-system', 'added-money', { + i: interaction.options.get('amount')['value'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'], + u: formatDiscordUserName(interaction.options.getUser('user')) + }), + ephemeral: !interaction.config['publicCommandReplies'] + }); + + interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'added-money-log', { + v: formatDiscordUserName(interaction.options.getUser('user')), + i: interaction.options.get('amount')['value'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'], + u: formatDiscordUserName(interaction.user) + })); + if (interaction.client.logChannel) interaction.client.logChannel.send(`[economy-system] ` + localize('economy-system', 'added-money-log', { + v: formatDiscordUserName(interaction.options.getUser('user')), + i: interaction.options.get('amount')['value'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'], + u: formatDiscordUserName(interaction.user) + })); + }, + 'remove': async function (interaction) { + if (!interaction.client.configurations['economy-system']['config']['admins'].includes(interaction.user.id) && !interaction.client.config['botOperators'].includes(interaction.user.id)) return interaction.reply(embedType(interaction.client.strings['not_enough_permissions'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + if (interaction.options.getUser('user').id === interaction.user.id && !interaction.client.configurations['economy-system']['config']['selfBalance']) { + if (interaction.client.logChannel) interaction.client.logChannel.send(localize('economy-system', 'admin-self-abuse')); + return interaction.reply({ + content: localize('economy-system', 'admin-self-abuse-answer'), + ephemeral: !interaction.config['publicCommandReplies'] + }); + } + await editBalance(interaction.client, interaction.options.getUser('user').id, 'remove', parseInt(interaction.options.get('amount')['value'])); + interaction.reply({ + content: localize('economy-system', 'removed-money', { + i: interaction.options.get('amount')['value'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'], + u: formatDiscordUserName(interaction.options.getUser('user')) + }), + ephemeral: !interaction.config['publicCommandReplies'] + }); + interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'removed-money-log', { + v: formatDiscordUserName(interaction.options.getUser('user')), + i: interaction.options.get('amount')['value'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'], + u: formatDiscordUserName(interaction.user) + })); + if (interaction.client.logChannel) interaction.client.logChannel.send(`[economy-system] ` + localize('economy-system', 'removed-money-log', { + v: formatDiscordUserName(interaction.options.getUser('user')), + i: interaction.options.get('amount')['value'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'], + u: formatDiscordUserName(interaction.user) + })); + }, + 'set': async function (interaction) { + if (!interaction.client.configurations['economy-system']['config']['admins'].includes(interaction.user.id) && !interaction.client.config['botOperators'].includes(interaction.user.id)) return interaction.reply(embedType(interaction.client.strings['not_enough_permissions'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + if (interaction.options.getUser('user').id === interaction.user.id && !interaction.client.configurations['economy-system']['config']['selfBalance']) { + if (interaction.client.logChannel) interaction.client.logChannel.send(localize('economy-system', 'admin-self-abuse')); + return interaction.reply({ + content: localize('economy-system', 'admin-self-abuse-answer'), + ephemeral: !interaction.config['publicCommandReplies'] + }); + } + await editBalance(interaction.client, interaction.options.getUser('user').id, 'set', parseInt(interaction.options.get('balance')['value'])); + interaction.reply({ + content: localize('economy-system', 'set-money', { + i: interaction.options.get('balance')['value'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'], + u: formatDiscordUserName(interaction.options.getUser('user')) + }), + ephemeral: !interaction.config['publicCommandReplies'] + }); + interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'set-money-log', { + v: formatDiscordUserName(interaction.options.getUser('user')), + i: interaction.options.get('balance')['value'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'], + u: formatDiscordUserName(interaction.user) + })); + if (interaction.client.logChannel) interaction.client.logChannel.send(`[economy-system] ` + localize('economy-system', 'set-money-log', { + v: formatDiscordUserName(interaction.options.getUser('user')), + i: interaction.options.get('balance')['value'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'], + u: formatDiscordUserName(interaction.user) + })); + }, + 'daily': async function (interaction) { + if (!await cooldown('daily', 86400000, interaction.user.id, interaction.client)) return interaction.reply(embedType(interaction.str['cooldown'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + await editBalance(interaction.client, interaction.user.id, 'add', parseInt(interaction.client.configurations['economy-system']['config']['dailyReward'])); + interaction.reply(embedType(interaction.str['dailyReward'], {'%earned%': `${interaction.client.configurations['economy-system']['config']['dailyReward']} ${interaction.client.configurations['economy-system']['config']['currencySymbol']}`}, {ephemeral: !interaction.config['publicCommandReplies']})); + interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'daily-earned-money', { + u: formatDiscordUserName(interaction.user), + m: interaction.client.configurations['economy-system']['config']['dailyReward'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'] + })); + if (interaction.client.logChannel) interaction.client.logChannel.send(`[economy-system] ` + localize('economy-system', 'daily-earned-money', { + u: formatDiscordUserName(interaction.user), + m: interaction.client.configurations['economy-system']['config']['dailyReward'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'] + })); + }, + 'weekly': async function (interaction) { + if (!await cooldown('weekly', 604800000, interaction.user.id, interaction.client)) return interaction.reply(embedType(interaction.str['cooldown'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + await editBalance(interaction.client, interaction.user.id, 'add', parseInt(interaction.client.configurations['economy-system']['config']['weeklyReward'])); + interaction.reply(embedType(interaction.str['weeklyReward'], {'%earned%': `${interaction.client.configurations['economy-system']['config']['weeklyReward']} ${interaction.client.configurations['economy-system']['config']['currencySymbol']}`}, {ephemeral: !interaction.config['publicCommandReplies']})); + interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'weekly-earned-money', { + u: formatDiscordUserName(interaction.user), + m: interaction.client.configurations['economy-system']['config']['dailyReward'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'] + })); + if (interaction.client.logChannel) interaction.client.logChannel.send(`[economy-system] ` + localize('economy-system', 'weekly-earned-money', { + u: formatDiscordUserName(interaction.user), + m: interaction.client.configurations['economy-system']['config']['dailyReward'], + c: interaction.client.configurations['economy-system']['config']['currencySymbol'] + })); + }, + 'balance': async function (interaction) { + let user = interaction.options.getUser('user'); + if (!user) user = interaction.user; + const balanceV = await interaction.client.models['economy-system']['Balance'].findOne({ + where: { + id: user.id + } + }); + if (!balanceV) return interaction.reply(embedType(interaction.str['userNotFound']), {'%user%': formatDiscordUserName(user)}, {ephemeral: !interaction.config['publicCommandReplies']}); + interaction.reply(embedType(interaction.str['balanceReply'], { + '%user%': formatDiscordUserName(user), + '%balance%': `${balanceV['balance']} ${interaction.client.configurations['economy-system']['config']['currencySymbol']}`, + '%bank%': `${balanceV['bank']} ${interaction.client.configurations['economy-system']['config']['currencySymbol']}`, + '%total%': `${parseInt(balanceV['balance']) + parseInt(balanceV['bank'])} ${interaction.client.configurations['economy-system']['config']['currencySymbol']}` + }, {ephemeral: !interaction.config['publicCommandReplies']})); + }, + 'deposit': async function (interaction) { + let amount = interaction.options.get('amount')['value']; + const user = await interaction.client.models['economy-system']['Balance'].findOne({ + where: { + id: interaction.user.id + } + }); + if (amount === 'all') amount = user.balance; + if (isNaN(amount)) return interaction.reply(embedType(interaction.str['NaN'], {'%input%': amount}, {ephemeral: !interaction.config['publicCommandReplies']})); + await editBank(interaction.client, interaction.user.id, 'deposit', amount); + interaction.reply(embedType(interaction.str['depositMsg'], {'%amount%': amount}, {ephemeral: !interaction.config['publicCommandReplies']})); + }, + 'withdraw': async function (interaction) { + let amount = interaction.options.get('amount')['value']; + const user = await interaction.client.models['economy-system']['Balance'].findOne({ + where: { + id: interaction.user.id + } + }); + if (amount === 'all') amount = user.bank; + if (isNaN(amount)) return interaction.reply(embedType(interaction.str['NaN'], {'%input%': amount}, {ephemeral: !interaction.config['publicCommandReplies']})); + await editBank(interaction.client, interaction.user.id, 'withdraw', amount); + interaction.reply(embedType(interaction.str['withdrawMsg'], {'%amount%': amount}, {ephemeral: !interaction.config['publicCommandReplies']})); + }, + 'msg_drop_msg': { + 'enable': async function (interaction) { + const user = await interaction.client.models['economy-system']['dropMsg'].findOne({ + where: { + id: interaction.user.id + } + }); + if (!user) return interaction.reply(embedType(interaction.str['msgDropAlreadyEnabled'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + await user.destroy(); + interaction.reply(embedType(interaction.str['msgDropEnabled'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + }, + 'disable': async function (interaction) { + const user = await interaction.client.models['economy-system']['dropMsg'].findOne({ + where: { + id: interaction.user.id + } + }); + if (user) return interaction.reply(embedType(interaction.str['msgDropAlreadyDisabled'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + await interaction.client.models['economy-system']['dropMsg'].create({ + id: interaction.user.id + }); + interaction.reply(embedType(interaction.str['msgDropDisabled'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + } + }, + 'destroy': async function (interaction) { + if (!interaction.client.configurations['economy-system']['config']['admins'].includes(interaction.user.id) && !interaction.client.config['botOperators'].includes(interaction.user.id)) return interaction.reply(embedType(interaction.client.strings['not_enough_permissions'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); + if (!interaction.options.getBoolean('confirm')) return interaction.reply({ + content: localize('economy-system', 'destroy-cancel-reply'), + ephemeral: !interaction.config['publicCommandReplies'] + }); + interaction.reply({ + content: localize('economy-system', 'destroy-reply'), + ephemeral: !interaction.config['publicCommandReplies'] + }); + interaction.client.logger.info(`[economy-system] Destroying the whole economy, as requested by ${formatDiscordUserName(interaction.user)}`); + if (interaction.client.logChannel) interaction.client.logChannel.send(`[economy-system] Destroying the whole economy, as requested by ${formatDiscordUserName(interaction.user)}`); + const cooldownModels = await interaction.client.models['economy-system']['cooldown'].findAll(); + if (cooldownModels.length !== 0) { + cooldownModels.forEach(async (element) => { + await element.destroy(); + }); + } + const msgDropModels = await interaction.client.models['economy-system']['dropMsg'].findAll(); + if (msgDropModels.length !== 0) { + msgDropModels.forEach(async (element) => { + await element.destroy(); + }); + } + const shopModels = await interaction.client.models['economy-system']['Shop'].findAll(); + if (shopModels.length !== 0) { + shopModels.forEach(async (element) => { + await element.destroy(); + }); + } + const userModels = await interaction.client.models['economy-system']['Balance'].findAll(); + if (userModels.length !== 0) { + userModels.forEach(async (element) => { + await element.destroy(); + }); + } + interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'destroy', {u: formatDiscordUserName(interaction.user)})); + if (interaction.client.logChannel) interaction.client.logChannel.send(`[economy-system] ` + localize('economy-system', 'destroy', {u: formatDiscordUserName(interaction.user)})); + } +}; + +module.exports.config = { + name: 'economy', + description: localize('economy-system', 'command-description-main'), + + options: function (client) { + const array = [{ + type: 'SUB_COMMAND', + name: 'work', + description: localize('economy-system', 'command-description-work') + }, + { + type: 'SUB_COMMAND', + name: 'crime', + description: localize('economy-system', 'command-description-crime') + }, + { + type: 'SUB_COMMAND', + name: 'rob', + description: localize('economy-system', 'command-description-rob'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('economy-system', 'option-description-rob-user') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'daily', + description: localize('economy-system', 'command-description-daily') + }, + { + type: 'SUB_COMMAND', + name: 'weekly', + description: localize('economy-system', 'command-description-weekly') + }, + { + type: 'SUB_COMMAND', + name: 'balance', + description: localize('economy-system', 'command-description-balance'), + options: [ + { + type: 'USER', + required: false, + name: 'user', + description: localize('economy-system', 'option-description-user') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'deposit', + description: localize('economy-system', 'command-description-deposit'), + options: [ + { + type: 'STRING', + required: true, + name: 'amount', + description: localize('economy-system', 'option-description-amount-deposit') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'withdraw', + description: localize('economy-system', 'command-description-withdraw'), + options: [ + { + type: 'STRING', + required: true, + name: 'amount', + description: localize('economy-system', 'option-description-amount-withdraw') + } + ] + }, + { + type: 'SUB_COMMAND_GROUP', + name: 'msg_drop_msg', + description: localize('economy-system', 'command-group-description-msg-drop-msg'), + options: [ + { + type: 'SUB_COMMAND', + name: 'enable', + description: localize('economy-system', 'command-description-msg-drop-msg-enable') + }, + { + type: 'SUB_COMMAND', + name: 'disable', + description: localize('economy-system', 'command-description-msg-drop-msg-disable') + } + ] + }]; + if (client.configurations['economy-system']['config']['allowCheats']) { + array.push({ + type: 'SUB_COMMAND', + name: 'add', + description: localize('economy-system', 'command-description-add'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('economy-system', 'option-description-user') + }, + { + type: 'INTEGER', + required: true, + name: 'amount', + description: localize('economy-system', 'option-description-amount') + } + ] + }); + array.push({ + type: 'SUB_COMMAND', + name: 'remove', + description: localize('economy-system', 'command-description-remove'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('economy-system', 'option-description-user') + }, + { + type: 'INTEGER', + required: true, + name: 'amount', + description: localize('economy-system', 'option-description-amount') + } + ] + }); + array.push({ + type: 'SUB_COMMAND', + name: 'set', + description: localize('economy-system', 'command-description-set'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('economy-system', 'option-description-user') + }, + { + type: 'INTEGER', + required: true, + name: 'balance', + description: localize('economy-system', 'option-description-balance') + } + ] + }); + array.push({ + type: 'SUB_COMMAND', + name: 'destroy', + description: localize('economy-system', 'command-description-destroy'), + options: [ + { + type: 'BOOLEAN', + required: false, + name: 'confirm', + description: localize('economy-system', 'option-description-confirm') + } + ] + }); + } + return array; + } +}; \ No newline at end of file diff --git a/modules/economy-system/commands/shop.js b/modules/economy-system/commands/shop.js new file mode 100644 index 00000000..44751f37 --- /dev/null +++ b/modules/economy-system/commands/shop.js @@ -0,0 +1,124 @@ +const {createShopItem, createShopMsg, deleteShopItem, shopMsg, buyShopItem} = require('../economy-system'); +const {localize} = require('../../../src/functions/localize'); + +/** + * @param {*} interaction Interaction + * @returns {boolean} Result + */ +async function checkPerms(interaction) { + const result = interaction.client.configurations['economy-system']['config']['shopManagers'].includes(interaction.user.id) || interaction.client.config['botOperators'].includes(interaction.user.id); + if (!result) { + await interaction.reply({ + content: interaction.client.strings['not_enough_permissions'], + ephemeral: !interaction.client.configurations['economy-system']['config']['publicCommandReplies'] + }); + } + return result; +} + +module.exports.subcommands = { + 'add': async function (interaction) { + if (!await checkPerms(interaction)) return; + await interaction.deferReply({ephemeral: !interaction.client.configurations['economy-system']['config']['publicCommandReplies']}); + await createShopItem(interaction); + await shopMsg(interaction.client); + }, + 'buy': async function (interaction) { + const name = await interaction.options.getString('item-name'); + const id = await interaction.options.getString('item-id'); + await interaction.deferReply({ephemeral: !interaction.client.configurations['economy-system']['config']['publicCommandReplies']}); + await buyShopItem(interaction, id, name); + }, + 'list': async function (interaction) { + const msg = await createShopMsg(interaction.client, interaction.guild, !interaction.client.configurations['economy-system']['config']['publicCommandReplies']); + interaction.reply(msg); + }, + 'delete': async function (interaction) { + if (!await checkPerms(interaction)) return; + await interaction.deferReply({ephemeral: !interaction.client.configurations['economy-system']['config']['publicCommandReplies']}); + await deleteShopItem(interaction); + await shopMsg(interaction.client); + } +}; + +module.exports.config = { + name: 'shop', + description: localize('economy-system', 'shop-command-description'), + defaultPermission: true, + options: [ + { + type: 'SUB_COMMAND', + name: 'add', + description: localize('economy-system', 'shop-command-description-add'), + options: [ + { + type: 'STRING', + required: true, + name: 'item-name', + description: localize('economy-system', 'shop-option-description-itemName') + }, + { + type: 'STRING', + required: true, + name: 'item-id', + description: localize('economy-system', 'shop-option-description-itemID') + }, + { + type: 'INTEGER', + required: true, + name: 'price', + description: localize('economy-system', 'shop-option-description-price') + }, + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('economy-system', 'shop-option-description-role') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'buy', + description: localize('economy-system', 'shop-command-description-buy'), + options: [ + { + type: 'STRING', + name: 'item-name', + description: localize('economy-system', 'shop-option-description-itemName'), + required: false + }, + { + type: 'STRING', + name: 'item-id', + description: localize('economy-system', 'shop-option-description-itemID'), + required: false + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'list', + description: localize('economy-system', 'shop-command-description-list') + }, + { + type: 'SUB_COMMAND', + name: 'delete', + description: localize('economy-system', 'shop-command-description-delete'), + options: [ + { + type: 'STRING', + name: 'item-name', + description: localize('economy-system', 'shop-option-description-itemName'), + required: false + }, + { + type: 'STRING', + name: 'item-id', + description: localize('economy-system', 'shop-option-description-itemID'), + required: false + } + ] + } + ] +}; \ No newline at end of file diff --git a/modules/economy-system/configs/config.json b/modules/economy-system/configs/config.json new file mode 100644 index 00000000..892087bf --- /dev/null +++ b/modules/economy-system/configs/config.json @@ -0,0 +1,364 @@ +{ + "description": { + "en": "Configure here, how the module should behave", + "de": "Stelle hier ein, wie sich das Modul verhalten soll" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "admins", + "humanName": { + "en": "Administrators", + "de": "Administratoren" + }, + "default": { + "en": [] + }, + "description": { + "en": "Users who can perform admin only actions e.g. manage the balance of users (Bot Operators always have this permission)", + "de": "Benutzer*innen die Admin only Aktionen ausführen können, wie z.B. die Balance von Benutzern ändern (Bot-Operatoren haben immer diese Berechtigung)" + }, + "type": "array", + "content": "integer" + }, + { + "name": "allowCheats", + "humanName": {}, + "default": { + "en": false + }, + "description": { + "en": "Allow admins to edit the balance of users (for a fair system not recommended!)" + }, + "type": "boolean" + }, + { + "name": "selfBalance", + "humanName": {}, + "default": { + "en": false + }, + "description": { + "en": "Allow admins to edit their own balance (for a fair system not recommended! DON'T DO THIS!!!!!)" + }, + "type": "boolean" + }, + { + "name": "shopManagers", + "humanName": { + "en": "shop-managers", + "de": "Shop-Verwaltung" + }, + "default": { + "en": [] + }, + "description": { + "en": "The Ids of the shop managers (Bot Operators have this permission always)", + "de": "Die Benutzer-IDs der Shopverwaltung (Bot-Operatoren haben immer diese Berechtigung)" + }, + "type": "array", + "content": "integer" + }, + { + "name": "startMoney", + "humanName": { + "en": "Start Money", + "de": "Start Geld" + }, + "default": { + "en": 100 + }, + "description": { + "en": "The amount of money that is given to a new user", + "de": "Das Geld, welches einem neuen Benutzer gegeben wird" + }, + "type": "integer" + }, + { + "name": "currencyName", + "humanName": { + "en": "currency name", + "de": "Währungsbezeichnung" + }, + "default": { + "en": "" + }, + "description": { + "en": "The name of the currency", + "de": "Der Name der Währung" + }, + "type": "string" + }, + { + "name": "currencySymbol", + "humanName": { + "en": "Symbol of the currency", + "de": "Symbol der Währung" + }, + "default": { + "en": "💰" + }, + "description": { + "en": "The symbol of the currency", + "de": "Das Symbol der Währung" + }, + "type": "string" + }, + { + "name": "maxWorkMoney", + "humanName": { + "en": "max work money", + "de": "Maximaler Arbeits Lohn" + }, + "default": { + "en": 100 + }, + "description": { + "en": "The highest amount of money you can get for working", + "de": "Der höchte Betrag, den man fürs Arbeiten bekommen kann" + }, + "type": "integer" + }, + { + "name": "minWorkMoney", + "humanName": { + "en": "min work money", + "de": "Mainimaler Arbeits Lohn" + }, + "default": { + "en": 20 + }, + "description": { + "en": "The lowest amount of money you can get for working", + "de": "Der niedrigste Betrag, den man fürs Arbeiten bekommen kann" + }, + "type": "integer" + }, + { + "name": "workCooldown", + "humanName": { + "en": "work cooldown", + "de": "Arbeits Cooldown" + }, + "default": { + "en": 20 + }, + "description": { + "en": "The amount of time a user needs to wait util they can use the work command again (in minutes)", + "de": "Die Dauer, die Benutzer*innen warten müssen, bevor der Arbeits-Command wieder ausgeführt werden kann (in Minuten)" + }, + "type": "integer" + }, + { + "name": "maxCrimeMoney", + "humanName": { + "en": "max crime money", + "de": "Maximales Verbrechens Geld" + }, + "default": { + "en": 1000 + }, + "description": { + "en": "The highest amount of money you can get for crime", + "de": "Das maximale Geld, was man dafür bekommen kann, ein Verbrechen zu begehen" + }, + "type": "integer" + }, + { + "name": "minCrimeMoney", + "humanName": { + "en": "min crime money", + "de": "Minimales Verbrechens Geld" + }, + "default": { + "en": 100 + }, + "description": { + "en": "The lowest amount of money you can get for crime", + "de": "Das minimale Geld, was man dafür bekommen kann, ein Verbrechen zu begehen" + }, + "type": "integer" + }, + { + "name": "crimeCooldown", + "humanName": { + "en": "crime cooldown", + "de": "Verbrechens Cooldown" + }, + "default": { + "en": 30 + }, + "description": { + "en": "The amount of time a user needs to wait util they can use the crime command again (in minutes)", + "de": "Die Dauer, die Benutzer*innen warten müssen, bevor der Verbrechens-Command wieder ausgeführt werden kann (in Minuten)" + }, + "type": "integer" + }, + { + "name": "maxRobAmount", + "humanName": { + "en": "max rob amount", + "de": "Maximale Raub Beute" + }, + "default": { + "en": 400 + }, + "description": { + "en": "The highest amount of money that a user can rob", + "de": "Das maximale Geld, was man durch Rauben bekommen kann" + }, + "type": "integer" + }, + { + "name": "robPercent", + "humanName": { + "en": "rob percent", + "de": "Raub Prozent" + }, + "default": { + "en": 10 + }, + "description": { + "en": "The amount that can get robed in percent", + "de": "Das maximale Geld, was bei einem Raub erbeutet werden kann, in Prozent" + }, + "type": "integer" + }, + { + "name": "robCooldown", + "humanName": { + "en": "rob cooldown", + "de": "Raub Cooldown" + }, + "default": { + "en": 60 + }, + "description": { + "en": "The amount of time a user needs to wait util they can use the rob command again (in minutes)", + "de": "Die Zeit die Benutzer warten müssen, bis sie den Raub-Command nochmal ausführen können (in Minuten)" + }, + "type": "integer" + }, + { + "name": "leaderboardChannel", + "humanName": { + "en": "leaderboard-channel", + "de": "Leaderboard-Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "The if of the channel for the leaderboard. On this leaderboard everyone can see who has the most money.", + "de": "Die ID des Kanals für das Leaderboard. Hier kann jeder sehen, wer das meiste Geld hat" + }, + "type": "channelID" + }, + { + "name": "shopChannel", + "humanName": { + "en": "shop channel", + "de": "Shop Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "The id of the channel for the shop-Message. This message shows the items of the shop", + "de": "Die ID des Kanals für die Shop-Nachricht. Diese Nachricht zeigt alle Items des Shops" + }, + "type": "channelID", + "allowNull": true + }, + { + "name": "msgDropsIgnoredChannels", + "humanName": { + "en": "message-drops ignored channels", + "de": "Ignorierte Message-Drop Kanäle" + }, + "default": { + "en": [] + }, + "description": { + "en": "List of Channels where Users can't get message-drops", + "de": "Liste an Kanälen, in denen Benutzer keine Message-Drops bekommen können" + }, + "type": "array", + "content": "string" + }, + { + "name": "messageDrops", + "humanName": {}, + "default": { + "en": 25 + }, + "description": { + "en": "Chance to get money for a message (Chance: 1/ This value). Set to 0 to disable message drops" + }, + "type": "integer" + }, + { + "name": "messageDropsMax", + "humanName": {}, + "default": { + "en": 50 + }, + "description": { + "en": "The max amount of money in a message Drop" + }, + "type": "integer" + }, + { + "name": "messageDropsMin", + "humanName": {}, + "default": { + "en": 5 + }, + "description": { + "en": "The min amount of money in a message Drop" + }, + "type": "integer" + }, + { + "name": "dailyReward", + "humanName": {}, + "default": { + "en": 25 + }, + "description": { + "en": "The daily reward" + }, + "type": "integer" + }, + { + "name": "weeklyReward", + "humanName": {}, + "default": { + "en": 100 + }, + "description": { + "en": "The weekly reward" + }, + "type": "integer" + }, + { + "name": "publicCommandReplies", + "humanName": { + "en": "Public Command-Replies", + "de": "Öffentliche Command-Antworten" + }, + "default": { + "en": false + }, + "description": { + "en": "Should the Command-replies be displayed for everyone?", + "de": "Sollen die Command-Antworten für alle angezeigt werden?" + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/economy-system/configs/strings.json b/modules/economy-system/configs/strings.json new file mode 100644 index 00000000..e2a81938 --- /dev/null +++ b/modules/economy-system/configs/strings.json @@ -0,0 +1,686 @@ +{ + "description": { + "en": "Configure messages of this module here", + "de": "Passe hier die Nachrichten des Modules an" + }, + "humanName": { + "en": "Messages", + "de": "Nachrichten" + }, + "filename": "strings.json", + "content": [ + { + "name": "notFound", + "humanName": { + "en": "not found message", + "de": "Nicht gefunden Nachricht" + }, + "default": { + "en": "This item could not be found", + "de": "Dieses Item konnte nicht gefunden werden" + }, + "description": { + "en": "The message that is send if the item wasn't found", + "de": "Die Nachricht, die gesendet wird, wenn das Item nicht gefunden wird" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "notEnoughMoney", + "humanName": { + "en": "not enough money", + "de": "Nicht genug Geld" + }, + "default": { + "en": "You haven't enough money to buy this Item", + "de": "Du hast nicht genug Geld, um dieses Item zu kaufen" + }, + "description": { + "en": "The message that is send if the user haven't enough money to buy an item", + "de": "Die Nachricht, die gesendet wird, wenn ein Benutzer nicht genug geld hat, um ein Item zu kaufen" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "shopMsg", + "humanName": { + "en": "shop message", + "de": "Shop-Nachricht" + }, + "default": { + "en": { + "title": "Shop", + "description": "%shopItems%" + } + }, + "description": { + "en": "Message for the shop. The Items gets added at the end", + "de": "Die Nachricht, die den aktuellen Shop anzeigt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "shopItems", + "description": { + "en": "All items of the shop (format specified below)", + "de": "Alle Items des Shops (Format wird unten angegeben)" + } + } + ] + }, + { + "name": "itemString", + "humanName": { + "en": "item string", + "de": "Item Text" + }, + "default": { + "en": "**%id%** %itemName%, **price**: %price%, **sellcount**: %sellcount%\n", + "de": "**%id%** %itemName%: **Preis**: %price%, **Verkäufe**: %sellcount%\n" + }, + "description": { + "en": "String for the items for the shop message", + "de": "Text für die Items für die Shop-Nachricht" + }, + "type": "string", + "allowEmbed": false, + "params": [ + { + "name": "id", + "description": { + "en": "Id of the item", + "de": "ID des Items" + } + }, + { + "name": "itemName", + "description": { + "en": "Name of the item", + "de": "Name des Items" + } + }, + { + "name": "price", + "description": { + "en": "Price of the item", + "de": "Preis des Items" + } + }, + { + "name": "sellcount", + "description": { + "en": "Count of the sales of the item", + "de": "Anzahl, wie häufig das Item verkauft wurde" + } + } + ] + }, + { + "name": "cooldown", + "humanName": { + "en": "cooldown", + "de": "Cooldown" + }, + "default": { + "en": "Please wait before using this command again" + }, + "description": { + "en": "This message gets send when a user is currently in cooldown" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "workSuccess", + "humanName": {}, + "default": { + "en": [ + "You worked and earned **%earned%**" + ] + }, + "description": { + "en": "Array of messages from which one random gets send when a user works successfully" + }, + "type": "array", + "content": "string", + "allowEmbed": true, + "params": [ + { + "name": "earned", + "description": { + "en": "Money that the user had earned" + } + } + ] + }, + { + "name": "crimeSuccess", + "humanName": {}, + "default": { + "en": [ + "You stole a wallet and earned **%earned%**" + ] + }, + "description": { + "en": "Array of messages from which one random gets send when a user commits a crime successfully" + }, + "type": "array", + "content": "string", + "allowEmbed": true, + "params": [ + { + "name": "earned", + "description": { + "en": "Money that the user had earned" + } + } + ] + }, + { + "name": "crimeFail", + "humanName": {}, + "default": { + "en": [ + "You've stolen a wallet and get caught.You loose **%loose%**" + ] + }, + "description": { + "en": "Array of messages from which one random gets send when a user fails to do some crime" + }, + "type": "array", + "content": "string", + "allowEmbed": true, + "params": [ + { + "name": "loose", + "description": { + "en": "Money that the user looses" + } + } + ] + }, + { + "name": "robSuccess", + "humanName": {}, + "default": { + "en": "You robed %user% earned **%earned%**" + }, + "description": { + "en": "This message gets send when a user robs another user successfully" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "earned", + "description": { + "en": "Money that the user had earned" + } + }, + { + "name": "user", + "description": { + "en": "The user that gets robed by you" + } + } + ] + }, + { + "name": "leaderboardEmbed", + "humanName": {}, + "default": { + "en": { + "title": "Leaderboard", + "color": "GREEN", + "thumbnail": " ", + "image": " ", + "description": "Here you can see who has the most money" + } + }, + "description": { + "en": "Configure the leaderboard embed here" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true, + "allowEmbed": true + }, + { + "name": "dailyReward", + "humanName": {}, + "default": { + "en": "You earned **%earned%** by collecting your daily reward" + }, + "description": { + "en": "Message that gets send after the user has claimed the daily reward" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "earned", + "description": { + "en": "Money that the user had earned" + } + } + ] + }, + { + "name": "weeklyReward", + "humanName": {}, + "default": { + "en": "You earned **%earned%** by collecting your weekly reward" + }, + "description": { + "en": "Message that gets send after the user has claimed the weekly reward" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "earned", + "description": { + "en": "Money that the user had earned" + } + } + ] + }, + { + "name": "balanceReply", + "humanName": {}, + "default": { + "en": { + "title": "Balance of %user%", + "fields": [ + { + "name": "Balance:", + "value": "%balance%" + }, + { + "name": "Bank:", + "value": "%bank%" + }, + { + "name": "Total:", + "value": "%total%" + } + ] + } + }, + "description": { + "en": "Reply for the balance command" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "balance", + "description": { + "en": "Current balance of the user" + } + }, + { + "name": "bank", + "description": { + "en": "Current value that the user has on the bank" + } + }, + { + "name": "total", + "description": { + "en": "Total balance of the user" + } + }, + { + "name": "user", + "description": { + "en": "Username and discriminator of the User" + } + } + ] + }, + { + "name": "userNotFound", + "humanName": {}, + "default": { + "en": "I can't find the user **%user%**" + }, + "description": { + "en": "The message that gets sent when the bot can't find a user" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "User that can't been found" + } + } + ] + }, + { + "name": "buyMsg", + "humanName": {}, + "default": { + "en": "You got the item **%item%**" + }, + "description": { + "en": "Message that gets send when a user buys something in the shop" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "item", + "description": { + "en": "Name of the item" + } + } + ] + }, + { + "name": "itemCreate", + "humanName": {}, + "default": { + "en": "Successfully created the item %name% with the id %id%. It costs %price% and you get the role %role%" + }, + "description": { + "en": "Message that gets send when a new shop item gets created" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "name", + "description": { + "en": "Name of the created item" + } + }, + { + "name": "id", + "description": { + "en": "Id of the created item" + } + }, + { + "name": "price", + "description": { + "en": "Price of the created item" + } + }, + { + "name": "role", + "description": { + "en": "Role that everyone gets who buys the item" + } + } + ] + }, + { + "name": "itemDelete", + "humanName": {}, + "default": { + "en": "Successfully deleted the item %name%." + }, + "description": { + "en": "Message that gets send when a new shop item gets deleted" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "name", + "description": { + "en": "Name of the deleted item", + "de": "Name des gelöschten Items" + } + }, + { + "name": "id", + "description": { + "en": "Id of the deleted item", + "de": "ID des gelöschten Items" + } + } + ] + }, + { + "name": "depositMsg", + "humanName": { + "en": "deposit message" + }, + "default": { + "en": "Successfully deposited **%amount%** to your bank" + }, + "description": { + "en": "The reply when a user deposits money to the bank" + }, + "type": "string", + "params": [ + { + "name": "amount", + "description": {} + } + ] + }, + { + "name": "withdrawMsg", + "humanName": { + "en": "withdraw message" + }, + "default": { + "en": "Successfully withdrew **%amount%** from your bank" + }, + "description": { + "en": "The reply when a user withdraws money from the bank" + }, + "type": "string", + "params": [ + { + "name": "amount", + "description": {} + } + ] + }, + { + "name": "msgDropMsg", + "humanName": { + "en": "message drop message" + }, + "default": { + "en": [ + "Message-Drop: You earned %earned% simply by chatting!" + ] + }, + "description": { + "en": "The message that gets sent on a message-drop" + }, + "type": "array", + "content": "string", + "params": [ + { + "name": "erned", + "description": {} + } + ] + }, + { + "name": "NaN", + "humanName": { + "en": "not a number" + }, + "default": { + "en": "**%input%** isn't a number" + }, + "description": { + "en": "Message that gets send if the bot needs a number but gets something different" + }, + "type": "string", + "params": [ + { + "name": "input", + "description": {} + } + ] + }, + { + "name": "msgDropAlreadyEnabled", + "humanName": { + "en": "message-drop already enabled" + }, + "default": { + "en": "The Mesage-Drop message is already enabled!" + }, + "description": { + "en": "Message that gets send if a User trys to enable the Message-Drop message, but it's already enabled" + }, + "type": "string" + }, + { + "name": "msgDropEnabled", + "humanName": { + "en": "message-drop enabled" + }, + "default": { + "en": "Successfully enabled the Message-Drop message" + }, + "description": { + "en": "Message that gets send when a User enables the Message-Drop message" + }, + "type": "string" + }, + { + "name": "msgDropAlreadyDisabled", + "humanName": { + "en": "message-drop already disabled" + }, + "default": { + "en": "The Mesage-Drop message is already disabled!" + }, + "description": { + "en": "Message that gets send if a User trys to disable the Message-Drop message, but it's already disabled" + }, + "type": "string" + }, + { + "name": "msgDropDisabled", + "humanName": { + "en": "message-drop disabled" + }, + "default": { + "en": "Successfully disabled the Message-Drop message" + }, + "description": { + "en": "Message that gets send when a User disables the Message-Drop message" + }, + "type": "string" + }, + { + "name": "rebuyItem", + "humanName": { + "en": "rebuy message", + "de": "Erneutkaufen Nachricht" + }, + "default": { + "en": "You already own this Item", + "de": "Du hast dieses Item bereits gekauft" + }, + "description": { + "en": "The message that is send when the user trys to buy an Item that he already own", + "de": "Die Nachricht, die gesendet wird, wenn der Nutzer das Item bereits besitzt" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "multipleMatches", + "humanName": { + "en": "multiple matches", + "de": "mehrere Treffer" + }, + "default": { + "en": "Multiple items match the query", + "de": "Mehrere Items entsprechen der Suche" + }, + "description": { + "en": "The message that gets send when multiple items match the query", + "de": "Die Nachricht, die gesendet wird, wenn mehrere Items der Suche entsprechen" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "noMatches", + "humanName": { + "en": "no matches", + "de": "keine Treffer" + }, + "default": { + "en": "The item with the id %id%/ the name %name% doesn't exists", + "de": "Das Item mit der ID %id%/ dem Namen %name% wurde nicht gefunden" + }, + "description": { + "en": "The message that gets send when the item can't be found", + "de": "Die Nachricht, die gesendet wird, wenn das Item nicht gefunden wird" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "id", + "description": { + "en": "The specified ID", + "de": "Die angegebene ID" + } + }, + { + "name": "name", + "description": { + "en": "The specified name", + "de": "Der angegebene Name" + } + } + ] + }, + { + "name": "itemDuplicate", + "humanName": { + "en": "item duplicate", + "de": "Item Duplikat" + }, + "default": { + "en": "There's already an item with the id %id% or the name %name%", + "de": "Es gibt schon ein Item mit der ID %id% oder dem Namen %name%" + }, + "description": { + "en": "The message that gets send when an item with the specified id or name already exists", + "de": "Die Nachricht, die gesendet wird, wenn ein Item mit dem angegebenen Namen oder der angegebenen ID schon existiert" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "id", + "description": { + "en": "The specified ID", + "de": "Die angegebene ID" + } + }, + { + "name": "name", + "description": { + "en": "The specified name", + "de": "Der angegebene Name" + } + } + ] + } + ] +} diff --git a/modules/economy-system/economy-system.js b/modules/economy-system/economy-system.js new file mode 100644 index 00000000..5b9c4417 --- /dev/null +++ b/modules/economy-system/economy-system.js @@ -0,0 +1,500 @@ +/** + * Basic functions for the economy system + * @module economy-system + * @author jateute + */ +const { MessageEmbed } = require('discord.js'); +const {embedType, inputReplacer} = require('../../src/functions/helpers'); +const {localize} = require('../../src/functions/localize'); +const { Op } = require('sequelize'); + +/** + * add a User to DB + * @param {Client} client Client + * @param {string} id Id of the user + * @returns {promise} + */ +async function createUser(client, id) { + const moduleConfig = client.configurations['economy-system']['config']; + client.models['economy-system']['Balance'].create({ + id: id, + balance: 0, + bank: moduleConfig['startMoney'] + }); +} + +/** + * Trys to find a user and if the user doesn't exists, creates the user + * @param {Client} client client + * @param {string} id ID of the user + * @returns {Promise} + */ +async function getUser(client, id) { + let user = await client.models['economy-system']['Balance'].findOne({ + where: { + id: id + } + }); + if (!user) { + await createUser(client, id); + user = await client.models['economy-system']['Balance'].findOne({ + where: { + id: id + } + }); + } + return user; +} + +/** + * Add/ Remove xyz from balance/ set balance to + * @param {Client} client Client + * @param {string} id UserId of the user which is effected + * @param {string} action The action which is should be performed (add/ remove/ set) + * @param {number} value The value which is added/ removed to/ from the balance/ to which the balance gets set + * @returns {Promise} + */ +async function editBalance(client, id, action, value) { + const user = await getUser(client, id); + let newBalance = 0; + switch (action) { + case 'add': + newBalance = parseInt(user.balance) + parseInt(value); + user.balance = newBalance; + await user.save(); + await leaderboard(client); + break; + + case 'remove': + newBalance = parseInt(user.balance) - parseInt(value); + if (newBalance <= 0) newBalance = 0; + user.balance = newBalance; + await user.save(); + await leaderboard(client); + break; + + case 'set': + user.balance = parseInt(value); + await user.save(); + await leaderboard(client); + break; + + default: + client.logger.error(`[economy-system] ${action} This action is invalid`); + break; + } +} + +/** + * Function to edit the amount on the Bank of a user + * @param {Client} client Client + * @param {string} id UserId of the user which is effected + * @param {string} action The action which is should be performed (deposit/ withdraw) + * @param {number} value The value which is added/ removed to/ from the balance/ to which the balance gets set + * @returns {Promise} + */ +async function editBank(client, id, action, value) { + const user = await getUser(client, id); + let newBank = 0; + switch (action) { + case 'deposit': + if (parseInt(user.balance) <= parseInt(value)) value = user.balance; + newBank = parseInt(user.bank) + parseInt(value); + user.bank = newBank; + await user.save(); + editBalance(client, id, 'remove', value); + await leaderboard(client); + break; + + case 'withdraw': + if (parseInt(value) >= parseInt(user.bank)) value = user.bank; + newBank = parseInt(user.bank) - parseInt(value); + if (newBank <= 0) newBank = 0; + user.bank = newBank; + await user.save(); + await editBalance(client, id, 'add', value); + await leaderboard(client); + break; + + default: + client.logger.error(`[economy-system] ${action} This action is invalid`); + break; + } +} + +/** + * Function to create a new Item for the shop + * @param {string} pId The id of the item + * @param {string} pName The name of the item + * @param {number} pPrice The price of the item + * @param {Role} pRole The role which is added to everyone who buys this item + * @param {Client} client Client + * @returns {Promise} + */ +async function createShopItemAPI(pId, pName, pPrice, pRole, client) { + return new Promise(async (resolve) => { + const model = client.models['economy-system']['Shop']; + const itemModel = await model.findOne({ + where: { + [Op.or]: [ + {name: pName}, + {id: pId} + ] + } + }); + if (itemModel) { + resolve(localize('economy-system', 'item-duplicate')); + } else { + await model.create({ + id: pId, + name: pName, + price: pPrice, + role: pRole + }); + client.logger.info(`[economy-system] ` + localize('economy-system', 'created-item', { + u: 'API/ CLI', + n: pName, + i: pId + })); + if (client.logChannel) client.logChannel.send(`[economy-system] ` + localize('economy-system', 'created-item', { + u: 'API/ CLI', + n: pName, + i: pId + })); + await shopMsg(client); + resolve(localize('economy-system', 'created-item')); + } + }); +} + +/** + * Function to create a new Item for the shop + * @param {*} interaction Interaction + * @returns {Promise} + */ +async function createShopItem(interaction) { + return new Promise(async (resolve) => { + const name = await interaction.options.get('item-name')['value']; + const id = await interaction.options.get('item-id', true)['value']; + const role = await interaction.options.getRole('role', true); + const price = await interaction.options.getInteger('price'); + const model = interaction.client.models['economy-system']['Shop']; + if (interaction.guild.me.roles.highest.comparePositionTo(role) <= 0) return await interaction.editReply(localize('economy-system', 'role-to-high')); + const itemModel = await model.findOne({ + where: { + [Op.or]: [ + {name: name}, + {id: id} + ] + } + }); + if (itemModel) { + await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['itemDuplicate'], { + '%id%': id, + '%name%': name + })); + resolve(localize('economy-system', 'item-duplicate')); + } else { + await model.create({ + id: id, + name: name, + price: price, + role: role['id'] + }); + await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['itemCreate'], { + '%name%': name, + '%id%': id, + '%price%': price, + '%role%': role.name + })); + + interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'created-item', { + u: interaction.user.tag, + n: name, + i: id + })); + if (interaction.client.logChannel) interaction.client.logChannel.send(`[economy-system] ` + localize('economy-system', 'created-item', { + u: interaction.user.tag, + n: name, + i: id + })); + await shopMsg(interaction.client); + resolve(localize('economy-system', 'created-item')); + } + }); +} + +/** + * Function to buy an item + * @param {*} interaction Interaction + * @param {*} id Id of the item + * @param {*} name Name of the item + */ +async function buyShopItem(interaction, id, name) { + if (!interaction) return; + const item = await interaction.client.models['economy-system']['Shop'].findAll({ + where: { + [Op.or]: [ + {name: name}, + {id: id} + ] + } + }); + if (item.length < 1) return await interaction.editReply({ + content: interaction.client.configurations['economy-system']['strings']['notFound'] + }); + else if (item.length > 1) return await interaction.editReply({ + content: interaction.client.configurations['economy-system']['strings']['multipleMatches'] + }); + + if (interaction.member.roles.cache.has(item[0]['role'])) return await interaction.editReply({ + content: interaction.client.configurations['economy-system']['strings']['rebuyItem'] + }); + let user = await interaction.client.models['economy-system']['Balance'].findOne({ + where: { + id: interaction.user.id + } + }); + if (!user) { + createUser(interaction.client, interaction.user.id); + user = await interaction.client.models['economy-system']['Balance'].findOne({ + where: { + id: interaction.user.id + } + }); + } + if (user.balance < item[0]['price']) return await interaction.editReply({ + content: interaction.client.configurations['economy-system']['strings']['notEnoughMoney'] + }); + await interaction.member.roles.add(item[0]['role']); + await editBalance(interaction.client, interaction.user.id, 'remove', item[0]['price']); + leaderboard(interaction.client); + await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['buyMsg'], {'%item%': item[0]['name']})); + interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'user-purchase', { + u: interaction.user.tag, + i: item[0]['name'], + p: item[0]['price'] + })); + if (interaction.client.logChannel) interaction.client.logChannel.send(`[economy-system] ` + localize('economy-system', 'user-purchase', { + u: interaction.user.tag, + i: item[0]['name'], + p: item[0]['price'] + })); + await shopMsg(interaction.client); +} + +/** + * Function to delete a shop-item + * @param {string} pName Name of the item + * @param {string} pId ID if the item + * @param {Client} client Client + * @returns {Promise} + */ +async function deleteShopItemAPI(pName, pId, client) { + return new Promise(async (resolve) => { + const model = await client.models['economy-system']['Shop'].findAll({ + where: { + [Op.or]: [ + {name: pName}, + {id: pId} + ] + } + }); + if (model.length > 1) { + resolve('More than one item was found'); + } else if (model.length < 1) { + resolve('No item was found'); + } else { + await model[0].destroy(); + client.logger.info(`[economy-system] ` + localize('economy-system', 'delete-item', { + u: 'API/ CLI', + i: pName + })); + if (client.logChannel) client.logChannel.send(`[economy-system] ` + localize('economy-system', 'delete-item', { + u: 'API/ CLI', + i: pName + })); + await shopMsg(client); + resolve(`Deleted the item ${pName}/ ${pId} successfully`); + } + }); +} + + +/** + * Function to delete a shop-item + * @param {*} interaction Interaction + * @returns {Promise} + */ +async function deleteShopItem(interaction) { + return new Promise(async (resolve) => { + const nameOption = interaction.options.get('item-name'); + const idOption = interaction.options.get('item-id'); + let model; + if (nameOption && idOption) { + model = await interaction.client.models['economy-system']['Shop'].findAll({ + where: { + [Op.or]: [ + {name: nameOption['value']}, + {id: idOption['value']} + ] + } + }); + }else if (nameOption) { + model = await interaction.client.models['economy-system']['Shop'].findAll({ + where: { + name: nameOption['value'] + } + }); + } + else if (idOption) { + model = await interaction.client.models['economy-system']['Shop'].findAll({ + where: { + id: idOption['value'] + } + }); + } else { + await interaction.editReply("Please use the id or the name!") + } + + if (model.length > 1) { + await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['multipleMatches'])); + resolve(); + } else if (model.length < 1) { + await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['noMatches'], {'%id%': idOption ? idOption['value'] : '-', '%name%': nameOption ? nameOption['value'] : '-'})); + resolve(); + } else { + await model[0].destroy(); + await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['itemDelete'], {'%name%': model[0]['name'], '%id%': model[0]['id']})); + interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'delete-item', { + u: interaction.user.tag, + i: model.name + })); + if (interaction.client.logChannel) interaction.client.logChannel.send(`[economy-system] ` + localize('economy-system', 'delete-item', { + u: interaction.user.tag, + i: model.name + })); + await shopMsg(interaction.client); + resolve(`Deleted the item ${model.name} successfully`); + } + }); +} + +/** + * Create the shop message + * @param {Client} client Client + * @param {object} guild Object of the guild + * @param {boolean} ephemeral Should the message be ephemeral? + * @returns {string} + */ +async function createShopMsg(client, guild, ephemeral) { + const items = await client.models['economy-system']['Shop'].findAll(); + let string = ''; + const options = []; + for (let i = 0; i < items.length; i++) { + const roles = await guild.roles.fetch(items[i].dataValues.role); + string = `${string}${inputReplacer({'%id%': items[i].dataValues.id, '%itemName%': items[i].dataValues.name, '%price%': `${items[i].dataValues.price} ${client.configurations['economy-system']['config']['currencySymbol']}`, '%sellcount%': roles ? roles.members.size : '0'}, client.configurations['economy-system']['strings']['itemString'])}`; + options.push({ + label: items[i].dataValues.name, + description: localize('economy-system', 'select-menu-price', { + p: `${items[i].dataValues.price} ${client.configurations['economy-system']['config']['currencySymbol']}` + }), + value: items[i].dataValues.id + }); + } + let components = []; + if (items.length > 0) { + components = [{ + type: 'ACTION_ROW', + components: [{ + type: 3, + placeholder: localize('economy-system', 'nothing-selected'), + 'min_values': 1, + 'max_values': 1, + options: options, + 'custom_id': 'economy-system_shop-select' + }] + }]; + } + return embedType(client.configurations['economy-system']['strings']['shopMsg'], {'%shopItems%': string}, { ephemeral: ephemeral, components: components }); +} + +/** + * Create a shop message in the configured channel + * @param {Client} client Client + */ +async function shopMsg(client) { + if (!client.configurations['economy-system']['config']['shopChannel'] || client.configurations['economy-system']['config']['shopChannel'] === '') return; + const channel = await client.channels.fetch(client.configurations['economy-system']['config']['shopChannel']); + if (!channel) return client.logger.error(`[economy-system] ` + localize('economy-system', 'channel-not-found', {c: moduleConfig['leaderboardChannel']})); + const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); + if (messages.last()) await messages.last().edit(await createShopMsg(client, channel.guild, false)); + else channel.send(await createShopMsg(client, channel.guild, false)); +} + +/** + * Gets the ten users with the most money + * @param {object} object Object of the users + * @param {Client} client Client + * @returns {string} + * @private + */ +async function topTen(object, client) { + if (object.length === 0) return; + object.sort(function (x, y) { + return (y.dataValues.balance + y.dataValues.bank) - (x.dataValues.balance + x.dataValues.bank); + }); + let retStr = ''; + let items = 10; + if (object.length < items) items = object.length; + for (let i = 0; i < items; i++) { + retStr = `${retStr}<@!${object[i].dataValues.id}>: ${object[i].dataValues.balance + object[i].dataValues.bank} ${client.configurations['economy-system']['config']['currencySymbol']}\n`; + } + return retStr; +} + +/** + * Create/ update the money Leaderboard + * @param {Client} client Client + * @returns {promise} + */ +async function leaderboard(client) { + const moduleConfig = client.configurations['economy-system']['config']; + const moduleStr = client.configurations['economy-system']['strings']; + if (!moduleConfig['leaderboardChannel'] || moduleConfig['leaderboardChannel'] === '') return; + const channel = await client.channels.fetch(moduleConfig['leaderboardChannel']).catch(() => { + }); + if (!channel) return client.logger.fatal(`[economy-system] ` + localize('economy-system', 'channel-not-found')); + + const model = await client.models['economy-system']['Balance'].findAll(); + + const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); + + const embed = new MessageEmbed() + .setTitle(moduleStr['leaderboardEmbed']['title']) + .setDescription(moduleStr['leaderboardEmbed']['description']) + .setTimestamp() + .setColor(moduleStr['leaderboardEmbed']['color']) + .setAuthor({name: client.user.username, iconURL: client.user.avatarURL()}) + .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}); + + if (model.length !== 0) embed.addFields({name: 'Leaderboard:', value: await topTen(model, client)}); + if ((moduleStr['leaderboardEmbed']['thumbnail'] || '').replaceAll(' ', '')) embed.setThumbnail(moduleStr['leaderboardEmbed']['thumbnail']); + if ((moduleStr['leaderboardEmbed']['image'] || '').replaceAll(' ', '')) embed.setImage(moduleStr['leaderboardEmbed']['image']); + + if (messages.last()) await messages.last().edit({embeds: [embed]}); + else channel.send({embeds: [embed]}); +} + + +module.exports.editBalance = editBalance; +module.exports.editBank = editBank; +module.exports.createUser = createUser; +module.exports.buyShopItem = buyShopItem; +module.exports.createShopItemAPI = createShopItemAPI; +module.exports.createShopItem = createShopItem; +module.exports.deleteShopItemAPI = deleteShopItemAPI; +module.exports.deleteShopItem = deleteShopItem; +module.exports.createShopMsg = createShopMsg; +module.exports.shopMsg = shopMsg; +module.exports.createLeaderboard = leaderboard; \ No newline at end of file diff --git a/modules/economy-system/events/botReady.js b/modules/economy-system/events/botReady.js new file mode 100644 index 00000000..35fce70e --- /dev/null +++ b/modules/economy-system/events/botReady.js @@ -0,0 +1,49 @@ +const {createLeaderboard, shopMsg} = require('../economy-system'); +const schedule = require('node-schedule'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (client) { + // Migration + const dbVersionUser = await client.models['DatabaseSchemeVersion'].findOne({where: {model: 'economy_User'}}); + if (!dbVersionUser) { + client.logger.info('[economy-system] ' + localize('economy-system', 'migration-happening')); + const data = await client.models['economy-system']['Balance'].findAll({attributes: ['id', 'balance']}); + await client.models['economy-system']['Balance'].sync({force: true}); + for (const user of data) { + await client.models['economy-system']['Balance'].create(user); + } + client.logger.info('[economy-system] ' + localize('economy-system', 'migration-done')); + await client.models['DatabaseSchemeVersion'].create({model: 'economy_User', version: 'V1'}); + } + const dbVersionCooldown = await client.models['DatabaseSchemeVersion'].findOne({where: {model: 'economy_Cooldown'}}); + if (!dbVersionCooldown) { + client.logger.info('[economy-system] ' + localize('economy-system', 'migration-happening')); + const data = await client.models['economy-system']['cooldown'].findAll({attributes: ['id', 'command']}); + await client.models['economy-system']['cooldown'].sync({force: true}); + for (const user of data) { + await client.models['economy-system']['cooldown'].create(user); + } + client.logger.info('[economy-system] ' + localize('economy-system', 'migration-done')); + await client.models['DatabaseSchemeVersion'].create({model: 'economy_Cooldown', version: 'V1'}); + } + const dbVersionShop = await client.models['DatabaseSchemeVersion'].findOne({where: {model: 'economy_Shop'}}); + if (!dbVersionShop) { + client.logger.info('[economy-system] ' + localize('economy-system', 'migration-happening')); + const data = await client.models['economy-system']['Shop'].findAll({attributes: ['name', 'price', 'role']}); + await client.models['economy-system']['Shop'].sync({force: true}); + let i = 0; + for (const item of data) { + item['dataValues']['id'] = i; + await client.models['economy-system']['Shop'].create(item['dataValues']); + i++; + } + client.logger.info('[economy-system] ' + localize('economy-system', 'migration-done')); + await client.models['DatabaseSchemeVersion'].create({model: 'economy_Shop', version: 'V1'}); + } + await shopMsg(client); + await createLeaderboard(client); + const job = schedule.scheduleJob('1 0 * * *', async () => { // Every day at 00:01 https://crontab.guru/#0_0_*_*_ + await createLeaderboard(client); + }); + client.jobs.push(job); +}; \ No newline at end of file diff --git a/modules/economy-system/events/interactionCreate.js b/modules/economy-system/events/interactionCreate.js new file mode 100644 index 00000000..db0b4399 --- /dev/null +++ b/modules/economy-system/events/interactionCreate.js @@ -0,0 +1,9 @@ +const { buyShopItem } = require('../economy-system'); + +module.exports.run = async function (client, interaction) { + if (!client.botReadyAt) return; + if (interaction.guild.id !== client.config.guildID) return; + if (!interaction.isSelectMenu()) return; + if (interaction.customId !== 'economy-system_shop-select') return; + buyShopItem(interaction, interaction.values[0], null); +}; \ No newline at end of file diff --git a/modules/economy-system/events/messageCreate.js b/modules/economy-system/events/messageCreate.js new file mode 100644 index 00000000..aeef0ea6 --- /dev/null +++ b/modules/economy-system/events/messageCreate.js @@ -0,0 +1,39 @@ +const {editBalance} = require('../economy-system'); +const {localize} = require('../../../src/functions/localize'); +const {formatDiscordUserName} = require('../../../src/functions/helpers'); + +module.exports.run = async function (client, message) { + if (!client.botReadyAt) return; + if (!message.guild) return; + if (message.author.bot) return; + if (message.guild.id !== client.config.guildID) return; + + const config = client.configurations['economy-system']['config']; + + if (config['messageDrops'] === 0) return; + if (config['msgDropsIgnoredChannels'].includes(message.channel.id)) return; + if (Math.floor(Math.random() * config['messageDrops']) !== 1) return; + const toAdd = Math.floor(Math.random() * (config['messageDropsMax'] - config['messageDropsMin'])) + config['messageDropsMin']; + await editBalance(client, message.author.id, 'add', toAdd); + const sendMsg = await client.models['economy-system']['dropMsg'].findOne({ + where: { + id: message.author.id + } + }); + if (!sendMsg) { + const msg = await message.reply({content: localize('economy-system', 'message-drop', {m: toAdd, c: config['currencySymbol']})}); + setTimeout(() => { + msg.delete(); + }, 5000); + } + client.logger.info(`[economy-system] ` + localize('economy-system', 'message-drop-earned-money', { + m: toAdd, + u: formatDiscordUserName(message.author), + c: config['currencySymbol'] + })); + if (client.logChannel) client.logChannel.send(`[economy-system] ` + localize('economy-system', 'message-drop-earned-money', { + m: toAdd, + u: formatDiscordUserName(message.author), + c: config['currencySymbol'] + })); +}; \ No newline at end of file diff --git a/modules/economy-system/models/cooldowns.js b/modules/economy-system/models/cooldowns.js new file mode 100644 index 00000000..90b07679 --- /dev/null +++ b/modules/economy-system/models/cooldowns.js @@ -0,0 +1,20 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class EconomyCooldown extends Model { + static init(sequelize) { + return super.init({ + userId: DataTypes.STRING, + command: DataTypes.STRING, + timestamp: DataTypes.DATE + }, { + tableName: 'economy_cooldowns', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'cooldown', + 'module': 'economy-system' +}; \ No newline at end of file diff --git a/modules/economy-system/models/dropMsg.js b/modules/economy-system/models/dropMsg.js new file mode 100644 index 00000000..4fab110f --- /dev/null +++ b/modules/economy-system/models/dropMsg.js @@ -0,0 +1,21 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class DropMsg extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.STRING, + primaryKey: true + } + }, { + tableName: 'economy_dropMsg', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'dropMsg', + 'module': 'economy-system' +}; \ No newline at end of file diff --git a/modules/economy-system/models/shop.js b/modules/economy-system/models/shop.js new file mode 100644 index 00000000..3ff087cd --- /dev/null +++ b/modules/economy-system/models/shop.js @@ -0,0 +1,24 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class ShopItems extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.STRING, + primaryKey: true + }, + name: DataTypes.STRING, + price: DataTypes.INTEGER, + role: DataTypes.TEXT + }, { + tableName: 'economy_shop', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'Shop', + 'module': 'economy-system' +}; \ No newline at end of file diff --git a/modules/economy-system/models/user.js b/modules/economy-system/models/user.js new file mode 100644 index 00000000..7c3830ee --- /dev/null +++ b/modules/economy-system/models/user.js @@ -0,0 +1,23 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class EconomyUser extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.STRING, + primaryKey: true + }, + balance: DataTypes.INTEGER, + bank: DataTypes.INTEGER + }, { + tableName: 'economy_user', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'Balance', + 'module': 'economy-system' +}; \ No newline at end of file diff --git a/modules/economy-system/module.json b/modules/economy-system/module.json new file mode 100644 index 00000000..9be79097 --- /dev/null +++ b/modules/economy-system/module.json @@ -0,0 +1,28 @@ +{ + "name": "economy-system", + "beta": true, + "author": { + "scnxOrgID": "4", + "name": "jateute", + "link": "https://github.com/jateute" + }, + "openSourceURL": "https://github.com/jateute/CustomDCBot/tree/main/modules/economy-system", + "commands-dir": "/commands", + "events-dir": "/events", + "models-dir": "/models", + "cli": "cli.js", + "config-example-files": [ + "configs/config.json", + "configs/strings.json" + ], + "tags": [ + "community" + ], + "humanReadableName": { + "en": "Economy" + }, + "description": { + "en": "A simple economy-system, containing a shop system, message-drops and commands to earn money", + "de": "Ein einfaches economy-system mit einem Shop, Nachrichten-Drops und Befehlen, um Geld zu verdienen" + } +} \ No newline at end of file diff --git a/modules/fun/commands/hug.js b/modules/fun/commands/hug.js new file mode 100644 index 00000000..b79878a0 --- /dev/null +++ b/modules/fun/commands/hug.js @@ -0,0 +1,26 @@ +const {embedType, randomElementFromArray} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (interaction) { + const moduleConfig = interaction.client.configurations['fun']['config']; + const user = interaction.options.getUser('user', true); + if (user.id === interaction.user.id) return interaction.reply({content: localize('fun', 'no-no-not-hugging-yourself'), ephemeral: true}); + interaction.reply(embedType(moduleConfig.hugMessage, { + '%authorID%': interaction.user.id, + '%userID%': user.id, + '%imgUrl%': randomElementFromArray(moduleConfig.hugImages) + })); +}; + +module.exports.config = { + name: 'hug', + description: localize('fun', 'hug-command-description'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('fun', 'user-argument-description'), + required: true + } + ] +}; \ No newline at end of file diff --git a/modules/fun/commands/kiss.js b/modules/fun/commands/kiss.js new file mode 100644 index 00000000..29b4de58 --- /dev/null +++ b/modules/fun/commands/kiss.js @@ -0,0 +1,26 @@ +const {embedType, randomElementFromArray} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (interaction) { + const moduleConfig = interaction.client.configurations['fun']['config']; + const user = interaction.options.getUser('user', true); + if (user.id === interaction.user.id) return interaction.reply({content: localize('fun', 'no-no-not-kissing-yourself'), ephemeral: true}); + interaction.reply(embedType(moduleConfig.kissMessage, { + '%authorID%': interaction.user.id, + '%userID%': user.id, + '%imgUrl%': randomElementFromArray(moduleConfig.kissImages) + })); +}; + +module.exports.config = { + name: 'kiss', + description: localize('fun', 'kiss-command-description'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('fun', 'user-argument-description'), + required: true + } + ] +}; \ No newline at end of file diff --git a/modules/fun/commands/pat.js b/modules/fun/commands/pat.js new file mode 100644 index 00000000..d44dd825 --- /dev/null +++ b/modules/fun/commands/pat.js @@ -0,0 +1,26 @@ +const {embedType, randomElementFromArray} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (interaction) { + const moduleConfig = interaction.client.configurations['fun']['config']; + const user = interaction.options.getUser('user', true); + if (user.id === interaction.user.id) return interaction.reply({content: localize('fun', 'no-no-not-patting-yourself'), ephemeral: true}); + interaction.reply(embedType(moduleConfig.patMessage, { + '%authorID%': interaction.user.id, + '%userID%': user.id, + '%imgUrl%': randomElementFromArray(moduleConfig.patImages) + })); +}; + +module.exports.config = { + name: 'pat', + description: localize('fun', 'pat-command-description'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('fun', 'user-argument-description'), + required: true + } + ] +}; \ No newline at end of file diff --git a/modules/fun/commands/random.js b/modules/fun/commands/random.js new file mode 100644 index 00000000..c613a469 --- /dev/null +++ b/modules/fun/commands/random.js @@ -0,0 +1,86 @@ +const {localize} = require('../../../src/functions/localize'); +const {embedType, randomIntFromInterval, randomElementFromArray} = require('../../../src/functions/helpers'); +const {generateIkeaName} = require('@scderox/ikea-name-generator'); + +module.exports.subcommands = { + 'number': function (interaction) { + interaction.reply(embedType(interaction.client.configurations['fun']['config']['randomNumberMessage'], + { + '%min%': interaction.options.getNumber('min') || 1, + '%max%': interaction.options.getNumber('max') || 42, + '%number%': randomIntFromInterval(interaction.options.getNumber('min') || 1, interaction.options.getNumber('max') || 42) + }, + {ephemeral: true} + )); + }, + 'ikea-name': function (interaction) { + let count = interaction.options.getNumber('syllable-count') || Math.floor(Math.random() * 4) + 1; + if (count && count > 20) count = 20; + interaction.reply(embedType(interaction.client.configurations['fun']['config']['ikeaMessage'], {'%name%': generateIkeaName(count)}, {ephemeral: true})); + }, + 'dice': function (interaction) { + interaction.reply(embedType(interaction.client.configurations['fun']['config']['diceRollMessage'], {'%number%': randomIntFromInterval(1, 6)}, {ephemeral: true})); + }, + 'coinflip': function (interaction) { + interaction.reply(embedType(interaction.client.configurations['fun']['config']['coinFlipMessage'], {'%site%': localize('fun', `dice-site-${randomIntFromInterval(1, 2)}`)}, {ephemeral: true})); + }, + '8ball': function (interaction) { + interaction.reply(embedType(interaction.client.configurations['fun']['config']['8ballMessage'], { + '%answer%': randomElementFromArray(interaction.client.configurations['fun']['config']['8BallMessages']) + }, {ephemeral: true})); + } +}; + +module.exports.config = { + name: 'random', + description: localize('fun', 'random-command-description'), + options: [ + { + type: 'SUB_COMMAND', + name: 'number', + description: localize('fun', 'random-number-command-description'), + options: [ + { + type: 'NUMBER', + name: 'min', + description: localize('fun', 'min-argument-description'), + required: false + }, + { + type: 'NUMBER', + name: 'max', + description: localize('fun', 'max-argument-description'), + required: false + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'ikea-name', + description: localize('fun', 'random-ikeaname-command-description'), + options: [ + { + type: 'NUMBER', + name: 'syllable-count', + description: localize('fun', 'syllable-count-argument-description'), + required: false + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'dice', + description: localize('fun', 'random-dice-command-description') + }, + { + type: 'SUB_COMMAND', + name: 'coinflip', + description: localize('fun', 'random-coinflip-command-description') + }, + { + type: 'SUB_COMMAND', + name: '8ball', + description: localize('fun', 'random-8ball-command-description') + } + ] +}; \ No newline at end of file diff --git a/modules/fun/commands/slap.js b/modules/fun/commands/slap.js new file mode 100644 index 00000000..36f5f5f7 --- /dev/null +++ b/modules/fun/commands/slap.js @@ -0,0 +1,26 @@ +const {embedType, randomElementFromArray} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (interaction) { + const moduleConfig = interaction.client.configurations['fun']['config']; + const user = interaction.options.getUser('user', true); + if (user.id === interaction.user.id) return interaction.reply({content: localize('fun', 'no-no-not-slapping-yourself'), ephemeral: true}); + interaction.reply(embedType(moduleConfig.slapMessage, { + '%authorID%': interaction.user.id, + '%userID%': user.id, + '%imgUrl%': randomElementFromArray(moduleConfig.slapImages) + })); +}; + +module.exports.config = { + name: 'slap', + description: localize('fun', 'slap-command-description'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('fun', 'user-argument-description'), + required: true + } + ] +}; \ No newline at end of file diff --git a/modules/fun/config.json b/modules/fun/config.json new file mode 100644 index 00000000..0731b906 --- /dev/null +++ b/modules/fun/config.json @@ -0,0 +1,428 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "ikeaMessage", + "humanName": { + "de": "IKEA-Nachricht" + }, + "default": { + "en": "Here's a ikea-product-name: %name%", + "de": "Hier ist ein IKEA-Produkt-Name: %name%" + }, + "description": { + "en": "Message that gets send when someone uses !ikea", + "de": "Nachricht welche gesendet wird, wenn jemand /random ikea benutzt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "name", + "description": { + "en": "Randomly generated name of an ikea product (probably not real)", + "de": "Zufällig generierter Name eines IKEA-Produkts (wahrscheinlich nicht real)" + } + } + ] + }, + { + "name": "randomNumberMessage", + "humanName": { + "de": "Zufallszahl-Nachricht" + }, + "default": { + "en": "Here your random number between %min% and %max%: %number%", + "de": "Hier ist deine Zufallszahl zwischen %min% und %max%: %number%" + }, + "description": { + "en": "Message that gets send when someone uses !random", + "de": "Nachricht, welche gesendet wird, wenn jemand /random number benutzt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "min", + "description": { + "en": "Minimal value", + "de": "Niedrigster Wert" + } + }, + { + "name": "max", + "description": { + "en": "Maximal value", + "de": "Höchster Wert" + } + }, + { + "name": "number", + "description": { + "en": "Generated number", + "de": "Generierte Zahl" + } + } + ] + }, + { + "name": "diceRollMessage", + "humanName": { + "de": "Würfel-Nachricht" + }, + "default": { + "en": "🎲 %number%", + "de": "🎲 %number%" + }, + "description": { + "en": "Message that gets send when someone uses !dice", + "de": "Nachricht, welche gesendet wird, wenn jemand /random dice benutzt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "number", + "description": { + "en": "Generated number", + "de": "Generierte Zahl" + } + } + ] + }, + { + "name": "coinFlipMessage", + "humanName": { + "de": "Münzwurf-Nachricht" + }, + "default": { + "en": "\uD83E\uDE99 %site%", + "de": "\uD83E\uDE99 %site%" + }, + "description": { + "en": "Message that gets send when someone uses /random coinfilp", + "de": "Nachricht, welche gesendet wird, wenn jemand /random coinfilp benutzt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "site", + "description": { + "de": "Seite, auf den die Münze gefallen ist", + "en": "Site on which the coin landed" + } + } + ] + }, + { + "name": "hugMessage", + "humanName": { + "de": "Umarmungsnachricht" + }, + "default": { + "en": "<@%authorID%> hugs <@%userID%>\n%imgUrl%", + "de": "<@%authorID%> umarmt <@%userID%>\n%imgUrl%" + }, + "description": { + "de": "Nachricht, welche gesendet wird, wenn jemand /hug benutzt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "authorID", + "description": { + "en": "ID of the user who ran this command", + "de": "ID des Nutzers, welcher den Befehl aus" + } + }, + { + "name": "userID", + "description": { + "en": "ID of the user that gets hugged", + "de": "ID des umarmten Nutzers" + } + }, + { + "name": "imgUrl", + "description": { + "en": "Randomly selected URL to a image", + "de": "Zufällig ausgewählte URL zu einem Bild" + }, + "isImage": true + } + ] + }, + { + "name": "hugImages", + "humanName": { + "de": "Umarmungsbilder" + }, + "default": { + "en": [ + "https://media1.tenor.com/images/94989f6312726739893d41231942bb1b/tenor.gif?itemid=14106856", + "https://media1.tenor.com/images/d7529f6003b20f3b21f1c992dffb8617/tenor.gif?itemid=4782499", + "https://media1.tenor.com/images/fd47e55dfb49ae1d39675d6eff34a729/tenor.gif?itemid=12687187" + ], + "de": [ + "https://media1.tenor.com/images/94989f6312726739893d41231942bb1b/tenor.gif?itemid=14106856", + "https://media1.tenor.com/images/d7529f6003b20f3b21f1c992dffb8617/tenor.gif?itemid=4782499", + "https://media1.tenor.com/images/fd47e55dfb49ae1d39675d6eff34a729/tenor.gif?itemid=12687187" + ] + }, + "description": { + "de": "Bilder aus welchen, wenn jemand /hug ausführt, zufällig ausgewählt wird" + }, + "type": "array", + "content": "imgURL" + }, + { + "name": "kissMessage", + "humanName": { + "de": "Kuss-Nachrichten" + }, + "default": { + "en": "<@%authorID%> kissed <@%userID%>\n%imgUrl%", + "de": "<@%authorID%> küsst <@%userID%>\n%imgUrl%" + }, + "description": { + "de": "Nachricht, welche gesendet wird, wenn jemand /kiss benutzt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "authorID", + "description": { + "en": "ID of the user who ran this command", + "de": "ID des Nutzers, welcher den Befehl aus" + } + }, + { + "name": "userID", + "description": { + "en": "ID of the user that gets kissed", + "de": "ID des geküssten Nutzers" + } + }, + { + "name": "imgUrl", + "description": { + "en": "Randomly selected URL to a image", + "de": "Zufällig ausgewählte URL zu einem Bild" + }, + "isImage": true + } + ] + }, + { + "name": "kissImages", + "humanName": { + "de": "Kussbilder" + }, + "default": { + "en": [ + "https://media1.tenor.com/images/ef9687b36e36605b375b4e9b0cde51db/tenor.gif?itemid=12498627", + "https://media1.tenor.com/images/2d2a1af1568277f2bc52467f984cb697/tenor.gif?itemid=14190535", + "https://media1.tenor.com/images/78095c007974aceb72b91aeb7ee54a71/tenor.gif?itemid=5095865" + ], + "de": [ + "https://media1.tenor.com/images/ef9687b36e36605b375b4e9b0cde51db/tenor.gif?itemid=12498627", + "https://media1.tenor.com/images/2d2a1af1568277f2bc52467f984cb697/tenor.gif?itemid=14190535", + "https://media1.tenor.com/images/78095c007974aceb72b91aeb7ee54a71/tenor.gif?itemid=5095865" + ] + }, + "description": { + "de": "Bilder aus welchen, wenn jemand /kiss ausführt, zufällig ausgewählt wird" + }, + "type": "array", + "content": "imgURL" + }, + { + "name": "slapMessage", + "humanName": { + "de": "Schlag-Nachricht" + }, + "default": { + "en": "<@%authorID%> slapped <@%userID%>\n%imgUrl%", + "de": "<@%authorID%> schlägt <@%userID%>\n%imgUrl%" + }, + "description": { + "de": "Nachricht, welche gesendet wird, wenn jemand /slap benutzt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "authorID", + "description": { + "en": "ID of the user who ran this command", + "de": "ID des Nutzers, welcher den Befehl aus" + } + }, + { + "name": "userID", + "description": { + "en": "ID of the user that gets slapped", + "de": "ID des geschlagenen Nutzers" + } + }, + { + "name": "imgUrl", + "description": { + "en": "Randomly selected URL to a image", + "de": "Zufällig ausgewählte URL zu einem Bild" + }, + "isImage": true + } + ] + }, + { + "name": "slapImages", + "humanName": { + "de": "Schlag-Bilder" + }, + "default": { + "en": [ + "https://media1.tenor.com/images/3c161bd7d6c6fba17bb3e5c5ecc8493e/tenor.gif?itemid=5196956", + "https://media1.tenor.com/images/73adef04dadf613cb96ed3b2c8a192b4/tenor.gif?itemid=9631495", + "https://media.tenor.com/images/bfda4a429071a7fa51c7e45685849f76/tenor.gif", + "https://media1.tenor.com/images/97624764cb41414ad2c60d2028c19394/tenor.gif?itemid=16739345", + "https://media1.tenor.com/images/03ea2379718496fbbd144c5bc50f8e96/tenor.gif?itemid=18908545" + ], + "de": [ + "https://media1.tenor.com/images/3c161bd7d6c6fba17bb3e5c5ecc8493e/tenor.gif?itemid=5196956", + "https://media1.tenor.com/images/73adef04dadf613cb96ed3b2c8a192b4/tenor.gif?itemid=9631495", + "https://media.tenor.com/images/bfda4a429071a7fa51c7e45685849f76/tenor.gif", + "https://media1.tenor.com/images/97624764cb41414ad2c60d2028c19394/tenor.gif?itemid=16739345", + "https://media1.tenor.com/images/03ea2379718496fbbd144c5bc50f8e96/tenor.gif?itemid=18908545" + ] + }, + "description": { + "de": "Bilder aus welchen, wenn jemand /slap ausführt, zufällig ausgewählt wird" + }, + "type": "array", + "content": "imgURL" + }, + { + "name": "patMessage", + "humanName": { + "de": "Tätschel-Nachricht" + }, + "default": { + "en": "<@%authorID%> patted <@%userID%>\n%imgUrl%", + "de": "<@%authorID%> tätschelt <@%userID%>\n%imgUrl%" + }, + "description": { + "de": "Nachricht, welche gesendet wird, wenn jemand /pat benutzt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "authorID", + "description": { + "en": "ID of the user who ran this command", + "de": "ID des Nutzers, welcher den Befehl aus" + } + }, + { + "name": "userID", + "description": { + "en": "ID of the user that gets patted", + "de": "ID des getätschelten Nutzers" + } + }, + { + "name": "imgUrl", + "description": { + "en": "Randomly selected URL to a image", + "de": "Zufällig ausgewählte URL zu einem Bild" + }, + "isImage": true + } + ] + }, + { + "name": "patImages", + "humanName": { + "de": "Tätschel-Bilder" + }, + "default": { + "en": [ + "https://media1.tenor.com/images/da8f0e8dd1a7f7db5298bda9cc648a9a/tenor.gif?itemid=12018819", + "https://media1.tenor.com/images/f5176d4c5cbb776e85af5dcc5eea59be/tenor.gif?itemid=5081286", + "https://media.tenor.com/images/0e5b7f4be25e309ecaafff8700438a72/tenor.gif", + "https://media1.tenor.com/images/be0c22e0af951aa7fa8753381663eb2c/tenor.gif?itemid=15824856" + ], + "de": [ + "https://media1.tenor.com/images/da8f0e8dd1a7f7db5298bda9cc648a9a/tenor.gif?itemid=12018819", + "https://media1.tenor.com/images/f5176d4c5cbb776e85af5dcc5eea59be/tenor.gif?itemid=5081286", + "https://media.tenor.com/images/0e5b7f4be25e309ecaafff8700438a72/tenor.gif", + "https://media1.tenor.com/images/be0c22e0af951aa7fa8753381663eb2c/tenor.gif?itemid=15824856" + ] + }, + "description": { + "de": "Bilder aus welchen, wenn jemand /pat ausführt, zufällig ausgewählt wird" + }, + "type": "array", + "content": "imgURL" + }, + { + "name": "8ballMessage", + "humanName": { + "de": "8ball-Nachricht" + }, + "default": { + "en": "%answer%", + "de": "Das Orakel hat gesprochen... %answer%" + }, + "description": { + "de": "Nachricht, welche gesendet wird, wenn jemand /random 8ball benutzt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "The oracle has spoken... answer", + "description": { + "en": "Answer to the question" + } + } + ] + }, + { + "name": "8BallMessages", + "humanName": { + "de": "8ball-Antworten" + }, + "default": { + "en": [ + "Yes", + "No", + "Maybe", + "Try again", + "42 is the answer" + ], + "de": [ + "Yes", + "No", + "Maybe", + "Try again", + "42 is the answer" + ] + }, + "description": { + "de": "Mögliche Antworten für /random 8ball" + }, + "type": "array", + "content": "string" + } + ] +} \ No newline at end of file diff --git a/modules/fun/module.json b/modules/fun/module.json new file mode 100644 index 00000000..36fe7e39 --- /dev/null +++ b/modules/fun/module.json @@ -0,0 +1,24 @@ +{ + "name": "fun", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "commands-dir": "/commands", + "config-example-files": [ + "config.json" + ], + "tags": [ + "fun" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/fun", + "humanReadableName": { + "en": "Fun-Commands", + "de": "Fun-Befehle" + }, + "description": { + "en": "Some random fun commands like /hug or /random", + "de": "Einige Spaß-Commands, wie /hug oder /random" + } +} \ No newline at end of file diff --git a/modules/giveaways/commands/giveaway.js b/modules/giveaways/commands/giveaway.js new file mode 100644 index 00000000..1093aa30 --- /dev/null +++ b/modules/giveaways/commands/giveaway.js @@ -0,0 +1,196 @@ +const {truncate} = require('../../../src/functions/helpers'); +const {createGiveaway, endGiveaway} = require('../giveaways'); +const durationParser = require('parse-duration'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.subcommands = { + 'start': async function (interaction) { + if (interaction.options.getString('duration') === 0) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('giveaways', 'duration-parsing-failed') + }); + if (interaction.options.getChannel('channel').type !== 'GUILD_TEXT' && interaction.options.getChannel('channel').type !== 'GUILD_NEWS') return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('giveaways', 'duration-parsing-failed') + }); + if (interaction.options.getInteger('winner-count') < 1 || interaction.options.getString('prize').length < 2) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('giveaways', 'parameter-parsing-failed') + }); + const requirements = []; + if (interaction.options.getInteger('required-messages')) requirements.push({ + type: 'messages', + messageCount: interaction.options.getInteger('required-messages') + }); + if (interaction.options.getRole('required-role')) requirements.push(({ + type: 'roles', + roles: [interaction.options.getRole('required-role').id] + })); + await createGiveaway(interaction.options.getUser('sponsor') || interaction.user, interaction.options.getChannel('channel'), interaction.options.getString('prize'), new Date(durationParser(interaction.options.getString('duration') + new Date().getTime())), interaction.options.getInteger('winner-count'), requirements, interaction.options.getString('sponsorlink')); + interaction.reply({ + ephemeral: true, + content: localize('giveaways', 'started-successfully', {c: interaction.options.getChannel('channel').toString()}) + }); + }, + 'reroll': async function (interaction) { + const giveaway = await interaction.client.models['giveaways']['Giveaway'].findOne({ + where: {messageID: interaction.options.getString('msg-id', true)} + }); + if (!giveaway) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('giveaways', 'no-giveaways-found') + }); + await endGiveaway(giveaway.id, null, false, interaction.options.getInteger('winner-count')); + await interaction.reply({ + ephemeral: true, + content: localize('giveaways', 'reroll-done') + }); + }, + 'end': async function (interaction) { + const giveaway = await interaction.client.models['giveaways']['Giveaway'].findOne({ + where: {messageID: interaction.options.getString('msg-id', true)} + }); + if (!giveaway) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('giveaways', 'no-giveaways-found') + }); + await endGiveaway(giveaway.id, null, true); + await interaction.reply({ + ephemeral: true, + content: localize('giveaways', 'giveaway-ended-successfully') + }); + } +}; + +module.exports.autoComplete = { + 'end': { + 'msg-id': autoCompleteMsgID + }, + 'reroll': { + 'msg-id': autoCompleteMsgID + } +}; + +/** + * @private + * Runs auto complete on the msg-id option + * @param {Interaction} interaction + * @return {Promise} + */ +async function autoCompleteMsgID(interaction) { + const giveaways = await interaction.client.models['giveaways']['Giveaway'].findAll({ + where: { + ended: !(interaction.options['_subcommand'] === 'end') + }, + order: [['createdAt', 'DESC']], + limit: 25 + }); + const matches = []; + interaction.value = interaction.value.toLowerCase(); + for (const match of giveaways.filter(g => g.messageID.includes(interaction.value) || g.prize.toLowerCase().includes(interaction.value) || ((interaction.client.guild.channels.cache.get(g.channelID) || {name: g.channelID}).name).includes(interaction.value))) { + matches.push({ + value: match.messageID, + name: truncate(`${(interaction.client.guild.channels.cache.get(match.channelID) || {name: match.channelID}).name}: ${match.prize}`, 100) + }); + } + interaction.respond(matches); +} + +module.exports.config = { + name: 'gmanage', + defaultMemberPermissions: ['MANAGE_MESSAGES'], + description: localize('giveaways', 'gmanage-description'), + + options: [ + { + type: 'SUB_COMMAND', + name: 'start', + description: localize('giveaways', 'gmanage-start-description'), + options: [ + { + type: 'CHANNEL', + name: 'channel', + required: true, + channelTypes: ['GUILD_TEXT', 'GUILD_NEWS'], + description: localize('giveaways', 'gmanage-channel-description') + }, + { + type: 'STRING', + name: 'prize', + required: true, + description: localize('giveaways', 'gmanage-price-description') + }, + { + type: 'STRING', + name: 'duration', + required: true, + description: localize('giveaways', 'gmanage-duration-description') + }, + { + type: 'INTEGER', + name: 'winner-count', + required: true, + description: localize('giveaways', 'gmanage-winnercount-description') + }, + { + type: 'INTEGER', + name: 'required-messages', + required: false, + description: localize('giveaways', 'gmanage-requiredmessages-description') + }, + { + type: 'ROLE', + name: 'required-role', + required: false, + description: localize('giveaways', 'gmanage-requiredroles-description') + }, + { + type: 'USER', + name: 'sponsor', + required: false, + description: localize('giveaways', 'gmanage-sponsor-description') + }, + { + type: 'STRING', + name: 'sponsorlink', + required: false, + description: localize('giveaways', 'gmanage-sponsorlink-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'end', + description: localize('giveaways', 'gend-description'), + options: [ + { + type: 'STRING', + name: 'msg-id', + required: true, + autocomplete: true, + description: localize('giveaways', 'gereroll-msgid-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'reroll', + description: localize('giveaways', 'gereroll-description'), + options: [ + { + type: 'STRING', + name: 'msg-id', + required: true, + autocomplete: true, + description: localize('giveaways', 'gereroll-msgid-description') + }, + { + type: 'INTEGER', + name: 'winner-count', + required: false, + description: localize('giveaways', 'gereroll-winnercount-description') + } + ] + } + ] +}; \ No newline at end of file diff --git a/modules/giveaways/commands/gmessages.js b/modules/giveaways/commands/gmessages.js new file mode 100644 index 00000000..c48a1b37 --- /dev/null +++ b/modules/giveaways/commands/gmessages.js @@ -0,0 +1,34 @@ +const {localize} = require('../../../src/functions/localize'); +module.exports.run = async function (interaction) { + const giveaways = await interaction.client.models['giveaways']['Giveaway'].findAll({ + where: { + ended: false, + countMessages: true + }, + order: [['createdAt', 'DESC']], + limit: 15 + }); + if (giveaways.length === 0) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('giveaways', 'no-giveaways-found') + }); + let gwMessages = ''; + for (const giveaway of giveaways) { + const channel = interaction.channel.guild.channels.cache.get(giveaway.channelID); + if (!channel) continue; + const message = await channel.messages.fetch(giveaway.messageID).catch(() => { + }); + if (!message) continue; + gwMessages = gwMessages + `[${giveaway.prize}](${message.url} "${localize('giveaways', 'jump-to-message-hover')}") in ${channel.toString()}: ${giveaway.messageCount[interaction.user.id] || 0}/${giveaway.requirements.find(r => r.type === 'messages').messageCount} ${localize('giveaways', 'messages')}`; + } + interaction.reply({ + ephemeral: true, + content: `**${localize('giveaways', 'giveaway-messages')}**\n\n${gwMessages}` + }); +}; + +module.exports.config = { + name: 'gmessages', + description: localize('giveaways', 'gmessages-description'), + defaultPermission: true +}; \ No newline at end of file diff --git a/modules/giveaways/configs/config.json b/modules/giveaways/configs/config.json new file mode 100644 index 00000000..b9cf1977 --- /dev/null +++ b/modules/giveaways/configs/config.json @@ -0,0 +1,157 @@ +{ + "description": { + "en": "Configure the function of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "commandsWarnings": { + "normal": [ + "/gmanage" + ] + }, + "content": [ + { + "name": "bypassRoles", + "humanName": { + "en": "Giveaway-Requirement-Bypass-Roles", + "de": "Gewinnspiel-Voraussetzungen-Ignorierung-Rollen" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Roles who can participate in giveaways even if they don't meet the requirements", + "de": "Rollen, die an Gewinnspielen teilnehmen können, ohne die Bedingungen erfüllen zu müssen" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "messageCountMode", + "humanName": { + "en": "Message-Count-Mode", + "de": "Nachrichten-Zähl-Modus" + }, + "default": { + "en": "all", + "de": "all" + }, + "description": { + "en": "Modus in which messages should get counted", + "de": "Modus, in welchem Nachrichten gezählt werden sollen" + }, + "type": "select", + "content": [ + "all", + "blacklist", + "whitelist" + ] + }, + { + "name": "blacklist", + "humanName": { + "en": "Blacklist" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Channel in which messages should get ignored (only if messageCountMode = \"blacklist\")", + "de": "Channel in welchen Nachrichten nicht gezählt werden sollen (nur wenn messageCountMode = \"blacklist\")" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "whitelist", + "humanName": { + "en": "Whitelist" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Channel in which messages should get counted (only if messageCountMode = \"whitelist\")", + "de": "Channel in welchen Nachrichten gezählt werden sollen (nur wenn messageCountMode = \"whitelist\")" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "multipleEntries", + "humanName": { + "en": "Multiple Entries", + "de": "Zusätzliche Gewinnchancen" + }, + "default": { + "en": {}, + "de": {} + }, + "description": { + "en": "Allow certain users with a specified role to enter multiple times.\n⚠️ Please remember that allowing multiple entries for users who invited other users is against Discord's Terms of Service", + "de": "Erlaubt es, Nutzern mit einer bestimmten Rollen mehre Gewinnchancen zu geben.\n⚠️ Please remember that allowing multiple entries for users who invited other users is against Discord's Terms of Service" + }, + "type": "keyed", + "content": { + "key": "roleID", + "value": "integer" + } + }, + { + "name": "entryDeniedRoles", + "humanName": { + "en": "Entry denied roles", + "de": "Teilnahme verboten Rollen" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Members with these roles won't be able to join your giveaway.", + "de": "Mitglieder mit diesen Rollen werden nicht an Gewinnspielen teilnehmen können." + }, + "type": "array", + "content": "roleID" + }, + { + "name": "winRoles", + "humanName": { + "en": "Win roles", + "de": "Gewinner Rollen" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "These roles will be assigned to the winners of giveaways, regardless of the giveaway price. These role will not be removed when rerolling winners.", + "de": "Rollen, die an die Gewinner von Gewinnspielen vergeben wird, egal was der Preis des Gewinnspiels ist. Die Rolle wird beim erneuten Auslösen nicht entfernt." + }, + "type": "array", + "content": "roleID" + }, + { + "name": "sendDMOnWin", + "humanName": { + "en": "Send DM-message to winner", + "de": "PN-Nachricht an Gewinner senden" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled the bot will send each winner a DM when they win.", + "de": "Wenn aktiviert wird der Bot eine Nachricht an den Gewinner senden, wenn diese gewinnen." + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/giveaways/configs/strings.json b/modules/giveaways/configs/strings.json new file mode 100644 index 00000000..3dd3d30a --- /dev/null +++ b/modules/giveaways/configs/strings.json @@ -0,0 +1,529 @@ +{ + "description": { + "en": "Edit the messages and strings of the module here", + "de": "Stelle hier die Nachrichten des Modules ein" + }, + "humanName": { + "en": "Nachrichten", + "de": "Nachrichten" + }, + "filename": "strings.json", + "content": [ + { + "name": "giveaway_message", + "humanName": { + "en": "Giveaway-Message", + "de": "Gewinnspiel-Nachricht" + }, + "default": { + "en": { + "title": "Neues Gewinnspiel 🎉", + "description": "**Prize**: %prize%\n**Winners**: %winners%\n**Organiser**: %organiser%\n**Sponsor-Website**: <%sponsorLink%>\n\n**Currently valid entries**: %entryCount% (%enteredCount% users)\n**Ends at**: %endAtDiscordFormation%\n\nPress the big button below to participate!", + "color": "GREEN" + }, + "de": { + "title": "New Giveaway 🎉", + "description": "**Preis**: %prize%\n**Anzahl Gewinner**: %winners%\n**Veranstalter**: %organiser%\n**Sponsor-Webseite**: <%sponsorLink%>\n\n**Gültige Teilnahmen**: %entryCount% (%enteredCount% Nutzer)\n**Endet am**: %endAtDiscordFormation%\n\nKlicke auf den Knopf unten, um teilzunehmen!", + "color": "GREEN" + } + }, + "description": { + "en": "Message that gets send in the giveaway channel if a new giveaway gets created", + "de": "Diese Nachricht wird verschickt, wenn ein Gewinnspiel gestartet wird." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "prize", + "description": { + "en": "Prize of the giveaway", + "de": "Preis des Gewinnspiels" + } + }, + { + "name": "endAtDiscordFormation", + "description": { + "en": "When using this variable, Discord will automatically format the timestamp in the client of the user", + "de": "Beim Nutzen dieser Variable wird Discord direkt beim Nutzer die Zeit rendern (Beispiel: \"In 4 Stunden\")" + } + }, + { + "name": "endAt", + "description": { + "en": "Date of the end of the giveaway", + "de": "Datum und Uhrzeit, wenn das Gewinnspiel endet" + } + }, + { + "name": "winners", + "description": { + "en": "Count of possible winners", + "de": "Anzahl möglicher Gewinner" + } + }, + { + "name": "sponsorLink", + "description": { + "en": "Link of the sponsor, if specified.", + "de": "Link des Sponsoren, wenn angegeben." + } + }, + { + "name": "organiser", + "description": { + "en": "Mention of the organiser of the giveaway", + "de": "Erwähnung des Veranstalters des Gewinnspieles" + } + }, + { + "name": "enteredCount", + "description": { + "en": "Count of users who entered this giveaway already", + "de": "Anzahl an Teilnehmern am Gewinnspiel" + } + }, + { + "name": "entryCount", + "description": { + "en": "Count of valid entries", + "de": "Anzahl von gültigen Teilnahmen" + } + } + ] + }, + { + "name": "giveaway_message_with_requirements", + "humanName": { + "en": "Giveaway-Message with requirements", + "de": "Gewinnspiel-Nachricht mit Voraussetzungen" + }, + "default": { + "en": { + "title": "New Giveaway 🎉", + "description": "Prize: %prize%\nEnds at: %endAtDiscordFormation%\nWinners: %winners%\nOrganiser: %organiser%\nSponsor-Website: %sponsorLink%\n\n__Requirements__\n%requirements%\n\nCurrently valid entries: %entryCount% (%enteredCount% users)\nPress the big button under this message to join the giveaway!", + "color": "GREEN" + }, + "de": { + "title": "Neues Gewinnspiel 🎉", + "description": "Preis: %prize%\nLäuft bis: %endAtDiscordFormation%\nAnzahl Gewinner: %winners%\nVeranstalter: %organiser%\nSponsor-Website: %sponsorLink%\n__3aussetzungen__\n%requirements%\nGültige Teilnahmen: %entryCount% (von %enteredCount% Teilnehmern)\n\nDrücke auf den großen Knopf unten, um teilzunehmen.", + "color": "GREEN" + } + }, + "description": { + "en": "Message that gets send in the giveaway channel if a new giveaway gets created", + "de": "Diese Nachricht wird in den Gewinnspiel-Channel versendet, wenn ein Gewinnspiel mit Voraussetzungen gestartet wird" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "prize", + "description": { + "en": "Prize of the giveaway", + "de": "Preis des Gewinnspiels" + } + }, + { + "name": "endAtDiscordFormation", + "description": { + "en": "When using this variable, Discord will automatically format the timestamp in the client of the user", + "de": "Beim Nutzen dieser Variable wird Discord direkt beim Nutzer die Zeit rendern (Beispiel: \"In 4 Stunden\")" + } + }, + { + "name": "endAt", + "description": { + "en": "Date of the end of the giveaway", + "de": "Datum und Uhrzeit, wenn das Gewinnspiel endet" + } + }, + { + "name": "winners", + "description": { + "en": "Count of possible winners", + "de": "Anzahl möglicher Gewinner" + } + }, + { + "name": "sponsorLink", + "description": { + "en": "Link of the sponsor, if specified.", + "de": "Link des Sponsoren, wenn angegeben." + } + }, + { + "name": "organiser", + "description": { + "en": "Mention of the organiser of the giveaway", + "de": "Erwähnung des Veranstalters des Gewinnspieles" + } + }, + { + "name": "enteredCount", + "description": { + "en": "Count of users who entered this giveaway already", + "de": "Anzahl an Teilnehmern am Gewinnspiel" + } + }, + { + "name": "entryCount", + "description": { + "en": "Count of valid entries", + "de": "Anzahl von gültigen Teilnahmen" + } + }, + { + "name": "requirements", + "description": { + "en": "Requirements for this giveaway", + "de": "Voraussetzungen für dieses Gewinnspiel" + } + } + ] + }, + { + "name": "requirementsNotPassed", + "humanName": { + "en": "Requirement-Not-Passed-Message", + "de": "Gewinnspiel-Voraussetzungen-Nicht-Erfüllt-Nachricht" + }, + "default": { + "en": "I am sorry but you did not pass the requirement-check for this giveaway.\nYou need to fulfill these requirements:\n%requirements%", + "de": "Huch, scheint als würdest du die Voraussetzungen für dieses Gewinnspiel nicht erfüllen.\nDu musst folgende Voraussetzungen noch erfüllen:\n%requirements%" + }, + "description": { + "en": "Message that will be displayed to users when they try to join a giveaway even when they do not meet the requirements.", + "de": "Nachrichten, die angezeigt wird, wenn ein Nutzer an einem Gewinnspiel teilnehmen will, obwohl er die Bedingungen zur Teilnahme nicht erfüllt." + }, + "type": "string", + "allowEmbed": "true", + "params": [ + { + "name": "requirements", + "description": { + "en": "Requirements of this giveaway", + "de": "Voraussetzungen des Gewinnspieles, die der Nutzer noch erfüllen muss" + } + } + ] + }, + { + "name": "giveaway_message_edit_after_winning", + "humanName": { + "en": "Giveaway-Message after message ended", + "de": "Gewinnspiel-Nachricht nach Beendung des Gewinnspiels" + }, + "default": { + "en": { + "title": "Giveaway ended", + "description": "Price: %price%\nEnded at: %endAtDiscordFormation%\nWinners: %winners%\nCurrently valid entries: %entryCount% (%enteredCount% users)\nOrganiser: %organiser%\nSponsor-Website: %sponsorLink%", + "color": "RED" + }, + "de": { + "title": "Gewinnspiel beended", + "description": "Price: Preis: %prize%\nEnddatum: %endAtDiscordFormation%\nGewinner: %winners%\nVeranstalter: %organiser%\nGültige Teilnahmen: %entryCount% (von %enteredCount% Nutzern)\nSponsor-Website: %sponsorLink", + "color": "RED" + } + }, + "description": { + "en": "Message that gets send after a giveaway ended", + "de": "Wenn ein Gewinnspiel endet wird die Gewinnspiel-Nachricht zu dieser Nachricht editiert." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "prize", + "description": { + "en": "Prize of the giveaway", + "de": "Preis des Gewinnspieles" + } + }, + { + "name": "sponsorLink", + "description": { + "en": "Link of the sponsor, if specified.", + "de": "Link des Sponsoren, wenn angegeben." + } + }, + { + "name": "endAtDiscordFormation", + "description": { + "en": "When using this variable, Discord will automatically format the timestamp in the client of the user", + "de": "Beim Nutzen dieser Variable wird Discord direkt beim Nutzer die Zeit rendern (Beispiel: \"In 4 Stunden\")" + } + }, + { + "name": "endAt", + "description": { + "en": "Date of the end of the giveaway", + "de": "End-Datum des Gewinnspieles" + } + }, + { + "name": "winners", + "description": { + "en": "Winners of this giveaway", + "de": "Gewinner des Gewinnspieles" + } + }, + { + "name": "organiser", + "description": { + "en": "Mention of the organiser of the giveaway", + "de": "Erwähnung des Veranstalters" + } + }, + { + "name": "enteredCount", + "description": { + "en": "Count of users who entered this giveaway already", + "de": "Anzahl von Teilnahmen" + } + }, + { + "name": "entryCount", + "description": { + "en": "Count of valid entries", + "de": "Anzahl von Teilnehmern" + } + } + ] + }, + { + "name": "winner_message", + "humanName": { + "en": "Win-Message", + "de": "Gewinn-Nachricht" + }, + "default": { + "en": "%winners% won this giveaway. Shoot a DM at %organiser% to claim your prize!", + "de": "%winners% haben **%prize%** in folgendem Gewinnspiel gewonnen: %url%. Schreib %organiser% eine PN, um den Preis zu erhalten!" + }, + "description": { + "en": "Message that gets send when the giveaway ends.", + "de": "Diese Nachricht wird verschickt, wenn das Gewinnspiel endet" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "winners", + "description": { + "en": "Winners of the giveaway", + "de": "Gewinner des Gewinnspieles" + } + }, + { + "name": "sponsorLink", + "description": { + "en": "Link of the sponsor, if specified.", + "de": "Link des Sponsoren, wenn angegeben." + } + }, + { + "name": "organiser", + "description": { + "en": "Mention of the organiser of the giveaway", + "de": "Erwähnung des Veranstalters des Gewinnspieles" + } + }, + { + "name": "prize", + "description": { + "en": "Prize of the giveaway", + "de": "Preis des Gewinnspieles" + } + } + ] + }, + { + "name": "no_winner_message", + "humanName": { + "en": "No-Winner-Message", + "de": "Kein-Gewinner-Nachricht" + }, + "default": { + "en": "No winner could be determined for this giveaway ):", + "de": "Dieses Gewinnspiel hatte keinen Gewinner ): ):" + }, + "description": { + "en": "Message that gets send when the giveaway ends and no winner could be determined.", + "de": "Diese Nachricht wird gesendet, wenn ein Gewinnspiel endet, aber kein Gewinner gefunden wurde" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "sponsorLink", + "description": { + "en": "Link of the sponsor, if specified.", + "de": "Link zum Sponsor, falls angeben" + } + }, + { + "name": "organiser", + "description": { + "en": "Mention of the organiser of the giveaway", + "de": "Erwähnung des Veranstalters" + } + }, + { + "name": "prize", + "description": { + "en": "Prize of the giveaway", + "de": "Preis des Gewinnspiels" + } + } + ] + }, + { + "name": "confirmationMessage", + "humanName": { + "en": "Confirmation-Message", + "de": "Teilnahme-Bestätigung-Nachricht" + }, + "default": { + "en": "Giveaway entered successfully with **%entries% entry(s)**.", + "de": "Gewinnspiel mit **%entries% Teilnahme(n)** beigetreten." + }, + "description": { + "en": "Message that gets shown to the user after they enter the giveaway successfully.", + "de": "Nachricht die angezeigt wird, wenn ein Nutzer teilnimmt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "price", + "description": { + "en": "Price of the giveaway", + "de": "Preis des Gewinnspiels" + } + }, + { + "name": "entries", + "description": { + "en": "Count of entries the user has to this giveaway", + "de": "Anzahl an Teilnahmen am Gewinnspiel" + } + } + ] + }, + { + "name": "alreadyEnteredMessage", + "humanName": { + "en": "Already Entered Message", + "de": "Bereits teilgenommen-Nachricht" + }, + "default": { + "en": "You are already in this giveaway with **%entries% entry(s)**.", + "de": "Du nimmst bereits mit **%entries% Teilnahme(n)** teil." + }, + "description": { + "en": "Message that gets shown to the user when someone tries to enter when they already are in", + "de": "Nachricht, die angezeigt wird, wenn ein Nutzer teilnehmen will, obwohl er bereits teilgenommen hat" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "price", + "description": { + "en": "Price of the giveaway", + "de": "Preis des Gewinnspiels" + } + }, + { + "name": "entries", + "description": { + "en": "Count of entries the user has to this giveaway", + "de": "Anzahl an Teilnahmen am Gewinnspiel" + } + } + ] + }, + { + "name": "deniedRoleMessage", + "humanName": { + "en": "User has forbidden roles message", + "de": "Nutzer mit verbotenen Rollen Nachricht" + }, + "default": { + "en": "⚠\uFE0F You can't participate in giveaways on this server because you have one or more forbidden roles.", + "de": "⚠\uFE0F Du kannst an keinem Gewinnspiel auf diesem Server teilnehmen, da du eine oder mehrere Rollen hast, die die Teilnahme verbieten." + }, + "description": { + "en": "Message that users with one of the configured entry denied roles will see when they try to join a giveaway.", + "de": "Nachricht, die angezeigt wird, wenn ein Nutzer teilnehmen will, obwohl er eine Rolle hat, die nicht teilnehmen dürfen" + }, + "type": "string", + "allowEmbed": true, + "params": [] + }, + { + "name": "buttonContent", + "humanName": { + "en": "Button-Content", + "de": "Knopf-Inhalt" + }, + "default": { + "en": "Join giveaway 🎉", + "de": "Gewinnspiel beitreten 🎉" + }, + "description": { + "en": "Content of the button under giveaways", + "de": "Inhalt des Teilnehmen-Knopfes" + }, + "type": "string" + }, + { + "name": "winner_DM_message", + "humanName": { + "en": "Winner-DM-Message", + "de": "Gewinner-PN-Nachricht" + }, + "default": { + "en": "Congrats, you won this giveaway: %url%", + "de": "Herzlichen Glückwunsch, du hast folgendes Gewinnspiel gewonnen: %url%" + }, + "description": { + "en": "Nachricht, die an den Nutzer gesendet wird, wenn er gewinnt (wenn aktiviert).", + "de": "Message that gets send when to the winner when they win (if enabled)." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "sponsorLink", + "description": { + "en": "Link of the sponsor, if specified.", + "de": "Link des Sponsoren, wenn angegeben." + } + }, + { + "name": "organiser", + "description": { + "en": "Mention of the organiser of the giveaway", + "de": "Erwähnung des Veranstalters des Gewinnspieles" + } + }, + { + "name": "prize", + "description": { + "en": "Prize of the giveaway", + "de": "Preis des Gewinnspiels" + } + }, + { + "name": "url", + "description": { + "en": "Url to the giveaway", + "de": "Url zum Gewinnspiel" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/giveaways/events/botReady.js b/modules/giveaways/events/botReady.js new file mode 100644 index 00000000..90a86b5e --- /dev/null +++ b/modules/giveaways/events/botReady.js @@ -0,0 +1,30 @@ +const {endGiveaway} = require('../giveaways'); +const {scheduleJob} = require('node-schedule'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async (client) => { + // Migration + const dbVersion = await client.models['DatabaseSchemeVersion'].findOne({where: {model: 'giveaways_Giveaway'}}); + if (!dbVersion) { + client.logger.info('[giveaways] ' + localize('giveaways', 'migration-happening')); + await client.models['giveaways']['Giveaway'].sync({force: true}); + client.logger.info('[giveaways] ' + localize('giveaways', 'migration-done')); + await client.models['DatabaseSchemeVersion'].create({model: 'giveaways_Giveaway', version: 'V1'}); + } + + const giveaways = await client.models['giveaways']['Giveaway'].findAll({ + where: { + ended: false + } + }); + for (const g of giveaways) { + if (parseInt(g.endAt) < new Date().getTime()) { + await endGiveaway(g.id, null, true); + continue; + } + const job = scheduleJob(new Date(parseInt(g.endAt)), async () => { + await endGiveaway(g.id, job, true); + }); + client.jobs.push(job); + } +}; \ No newline at end of file diff --git a/modules/giveaways/events/interactionCreate.js b/modules/giveaways/events/interactionCreate.js new file mode 100644 index 00000000..8685f96a --- /dev/null +++ b/modules/giveaways/events/interactionCreate.js @@ -0,0 +1,154 @@ +const {calculateUserEntries, checkRequirements} = require('../giveaways'); +const {embedType, formatDate} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +const toBeProcessed = []; + +exports.run = async (client, interaction) => { + if (!interaction.client.botReadyAt) return; + if (!interaction.isButton()) return; + if (!interaction.customId.startsWith('giveaway-l') && interaction.customId !== 'giveaway') return; + await interaction.deferReply({ephemeral: true}); + toBeProcessed.push(interaction); + startProcessing(); +}; + +let processing = false; + +/** + * This is here to prevent race conditions leading to unregistered entries. It's bad I know, but it gets the job done. I should rewrite the whole system + */ +async function startProcessing() { + if (processing) return; + for (const k in toBeProcessed) { + await processReply(toBeProcessed[k]); + delete toBeProcessed[k]; + } + processing = false; + if (toBeProcessed.filter(f => f !== null).length !== 0) await startProcessing(); +} + +async function processReply(interaction) { + const client = interaction.client; + const moduleStrings = interaction.client.configurations['giveaways']['strings']; + if (interaction.customId.startsWith('giveaway-l')) { + const giveaway = await client.models['giveaways']['Giveaway'].findOne({ + where: { + id: interaction.customId.replaceAll('giveaway-l-', '') + } + }); + if (!giveaway) return; + const entries = {...giveaway.entries}; + delete entries[interaction.user.id]; + giveaway.entries = {...entries}; + await giveaway.save(); + interaction.editReply({content: localize('giveaways', 'giveaway-left')}).then(() => { + }); + interaction.channel.messages.fetch(giveaway.messageID).then(m => updateGiveaway(giveaway, m).then(() => { + })); + + return; + } + if (interaction.customId !== 'giveaway') return; + + const giveaway = await client.models['giveaways']['Giveaway'].findOne({ + where: { + messageID: interaction.message.id + } + }); + + if (interaction.member.roles.cache.find(r => (interaction.client.configurations['giveaways']['config'].entryDeniedRoles || []).includes(r.id))) return interaction.editReply(embedType(moduleStrings['deniedRoleMessage'], {})); + + if (giveaway.requirements.length === 0) return await enterUser(); + + const [failedRequirements, notPassedRequirementsString] = await checkRequirements(interaction.member, giveaway); + if (failedRequirements) { + interaction.editReply(embedType(moduleStrings['requirementsNotPassed'], { + '%requirements%': notPassedRequirementsString + })); + } else await enterUser(); + + /** + * Enters this user to this giveaway + * @private + * @returns {Promise} + */ + async function enterUser() { + if (giveaway.entries[interaction.user.id]) return interaction.editReply(embedType(moduleStrings.alreadyEnteredMessage, { + '%price%': giveaway.price, + '%entries%': calculateUserEntries(interaction.member) + }, { + components: [{ + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + style: 'DANGER', + label: 'Leave giveaway', + customId: `giveaway-l-${giveaway.id}` + }] + }] + })); + const entries = giveaway.entries; + giveaway.entries = {}; // Thx sequelize + entries[interaction.user.id] = calculateUserEntries(interaction.member); + giveaway.entries = entries; + await giveaway.save(); + interaction.editReply(embedType(moduleStrings.confirmationMessage, { + '%price%': giveaway.price, + '%entries%': calculateUserEntries(interaction.member) + })).then(() => { + }); + + interaction.channel.messages.fetch(giveaway.messageID).then(m => updateGiveaway(giveaway, m).then(() => { + })); + } + + async function updateGiveaway(giveaway, message) { + const enteredUsers = []; + let totalEntries = 0; + for (const userID in giveaway.entries) { + totalEntries = totalEntries + giveaway.entries[userID]; + if (!enteredUsers.includes(userID)) enteredUsers.push(userID); + } + const components = [{ + type: 'ACTION_ROW', + components: [{type: 'BUTTON', label: moduleStrings.buttonContent, style: 'PRIMARY', customId: 'giveaway'}] + }]; + const endAt = new Date(parseInt(giveaway.endAt)); + + if (giveaway.requirements.length !== 0) { + let requirementString = ''; + giveaway.requirements.forEach((r) => { + if (r.type === 'messages') requirementString = requirementString + `• ${localize('giveaways', 'required-messages', {mc: r.messageCount})}\n`; + if (r.type === 'roles') { + let rolesString = ''; // Surely there is a better way to to this kind of stuff, but I am to stupid to find it + r.roles.forEach(rID => rolesString = rolesString + `<@&${rID}> `); + requirementString = rolesString + `• ${localize('giveaways', 'roles-required', {r: rolesString})}\n`; + } + }); + + await message.edit(embedType(moduleStrings['giveaway_message_with_requirements'], { + '%prize%': giveaway.prize, + '%winners%': giveaway.winnerCount, + '%requirements%': requirementString, + '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), + '%endAt%': formatDate(endAt), + '%endAtDiscordFormation%': ``, + '%organiser%': `<@${giveaway.organiser}>`, + '%entryCount%': interaction.channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : totalEntries, + '%enteredCount%': interaction.channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : enteredUsers.length + }, {components})); + } else { + await message.edit(embedType(moduleStrings['giveaway_message'], { + '%prize%': giveaway.prize, + '%winners%': giveaway.winnerCount, + '%endAtDiscordFormation%': ``, + '%endAt%': formatDate(endAt), + '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), + '%organiser%': `<@${giveaway.organiser}>`, + '%entryCount%': interaction.channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : totalEntries, + '%enteredCount%': interaction.channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : enteredUsers.length + }, {components})); + } + } +} \ No newline at end of file diff --git a/modules/giveaways/events/messageCreate.js b/modules/giveaways/events/messageCreate.js new file mode 100644 index 00000000..0137f6c3 --- /dev/null +++ b/modules/giveaways/events/messageCreate.js @@ -0,0 +1,31 @@ +module.exports.run = async function (client, message) { + if (!client.botReadyAt) return; + if (!message.guild) return; + if (message.author.bot) return; + if (message.guild.id !== client.config.guildID) return; + const config = client.configurations['giveaways']['config']; + + if (!config.blacklist) config.blacklist = []; + if (!config.whitelist) config.blacklist = []; + if (!config.messageCountMode) config.messageCountMode = 'all'; + if (config.messageCountMode === 'blacklist' && config.blacklist.includes(message.channel.id)) return; + if (config.messageCountMode === 'whitelist' && !config.whitelist.includes(message.channel.id)) return; + + const giveaways = await client.models['giveaways']['Giveaway'].findAll({ + where: { + ended: false, + countMessages: true + } + }); + + for (const giveaway of giveaways) { + if (giveaway.requirements.find(r => r.type === 'messages')) { + const messages = giveaway.messageCount; + giveaway.messageCount = null; + if (!messages[message.author.id]) messages[message.author.id] = 0; + messages[message.author.id] = (parseInt(messages[message.author.id]) + 1).toString(); + giveaway.messageCount = messages; + await giveaway.save(); + } + } +}; \ No newline at end of file diff --git a/modules/giveaways/giveaways.js b/modules/giveaways/giveaways.js new file mode 100644 index 00000000..781776e6 --- /dev/null +++ b/modules/giveaways/giveaways.js @@ -0,0 +1,276 @@ +/** + * Manages giveaways + * @module Giveaways + * @author Simon Csaba + */ +const {formatDate, randomElementFromArray} = require('../../src/functions/helpers'); +const {scheduleJob} = require('node-schedule'); +const {embedType} = require('../../src/functions/helpers'); +const {localize} = require('../../src/functions/localize'); + +/** + * Create a new giveaway + * @param {User} organiser User who organized this giveaway + * @param {Channel} channel Channel in which this giveaway should take place + * @param {String} prize Prize which should be given away + * @param {Date} endAt Date on which the giveaway should end + * @param {Number} winners Count of winners the bot should select + * @param {Array} requirements Array of requirements + * @param {String} sponsorLink Link to the sponsor's website (if applicable) + * @returns {Promise} + */ +module.exports.createGiveaway = async function (organiser, channel, prize, endAt, winners, requirements = [], sponsorLink = null) { + const moduleStrings = channel.client.configurations['giveaways']['strings']; + let m; + const components = [{ + type: 'ACTION_ROW', + components: [{type: 'BUTTON', label: moduleStrings.buttonContent, style: 'PRIMARY', customId: 'giveaway'}] + }]; + if (requirements.length === 0) m = await channel.send(embedType(moduleStrings['giveaway_message'], { + '%prize%': prize, + '%winners%': winners, + '%endAtDiscordFormation%': ``, + '%endAt%': formatDate(endAt), + '%sponsorLink%': sponsorLink || localize('giveaways', 'no-link'), + '%organiser%': `<@${organiser.id}>`, + '%entryCount%': channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : 0, + '%enteredCount%': channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : 0 + }, {components})); + else { + let requirementString = ''; + requirements.forEach((r) => { + if (r.type === 'messages') requirementString = requirementString + `* ${localize('giveaways', 'required-messages', {mc: r.messageCount})}\n`; + if (r.type === 'roles') { + let rolesString = ''; // Surely there is a better way to to this kind of stuff, but I am to stupid to find it + r.roles.forEach(rID => rolesString = rolesString + `<@&${rID}> `); + requirementString = requirementString + `* ${localize('giveaways', 'roles-required', {r: rolesString})}\n`; + } + }); + m = await channel.send(embedType(moduleStrings['giveaway_message_with_requirements'], { + '%prize%': prize, + '%winners%': winners, + '%requirements%': requirementString, + '%sponsorLink%': sponsorLink || localize('giveaways', 'no-link'), + '%endAt%': formatDate(endAt), + '%endAtDiscordFormation%': ``, + '%organiser%': `<@${organiser.id}>`, + '%entryCount%': channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : 0, + '%enteredCount%': channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : 0 + }, {components})); + } + const dbItem = await channel.client.models['giveaways']['Giveaway'].create({ + endAt: endAt.getTime(), + winnerCount: winners, + prize: prize, + requirements: requirements, + countMessages: !!requirements.find(e => e.type === 'messages'), + messageCount: {}, + sponsorWebsite: sponsorLink, + organiser: organiser.id, + messageID: m.id, + channelID: channel.id + }); + const job = scheduleJob(endAt, async () => { + await endGiveaway(dbItem.id, job, true); + }); + channel.client.jobs.push(job); +}; + +/** + * Ends a giveaway + * @param {Number} gID ID of the giveaway to end + * @param {Job} job Job which should get canceled after the giveaway ends + * @param {Boolean} checkIfGiveawayEnded If enabled the function will return early when this giveaway already ended + * @param {Number} maxWinCount Number of persons who can win this giveaway (overwrites Giveaway.winner) + * @returns {Promise} + */ +async function endGiveaway(gID, job = null, checkIfGiveawayEnded = false, maxWinCount = null) { + const {client} = require('../../main'); + const moduleStrings = client.configurations['giveaways']['strings']; + const moduleConfig = client.configurations['giveaways']['config']; + + const giveaway = await client.models['giveaways']['Giveaway'].findOne({ + where: { + id: gID + } + }); + if (!giveaway) return; + if (job) job.cancel(); + if (checkIfGiveawayEnded && giveaway.ended) return; + + const channel = await client.channels.fetch(giveaway.channelID).catch(() => { + }); + if (!channel) return; + const message = await channel.messages.fetch(giveaway.messageID).catch(() => { + }); + if (!message) return; + giveaway.ended = true; + await giveaway.save(); + if (job) job.cancel(); + + const winners = []; + let userEntries = []; + let enteredUsers = 0; + + for (const id in giveaway.entries) { + const member = await channel.guild.members.fetch(id).catch(() => { + }); + if (!member) continue; + const [failedReqCheck] = await checkRequirements(member, giveaway); + if (failedReqCheck) continue; + enteredUsers++; + for (let i = 0; i < calculateUserEntries(member); i++) userEntries.push(id); + } + + const entries = userEntries.length; + if (userEntries.length === 0) { + await editMessage(localize('giveaways', 'no-winners')); + return await message.reply(embedType(moduleStrings['no_winner_message'], { + '%prize%': giveaway.prize, + '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), + '%organiser%': `<@${giveaway.organiser}>` + }, {})); + } + + + if (maxWinCount) giveaway.winnerCount = maxWinCount; + if (enteredUsers < giveaway.winnerCount) giveaway.winnerCount = enteredUsers; + + for (let winnerCount = 0; winnerCount < giveaway.winnerCount; winnerCount++) { + const winner = randomElementFromArray(userEntries); + winners.push(winner); + userEntries = userEntries.filter(u => u !== winner); + } + + + let winnersstring = ''; + for (const winner of winners) { + winnersstring = winnersstring + `<@${winner}> `; + } + + await message.reply(embedType(moduleStrings['winner_message'], { + '%prize%': giveaway.prize, + '%winners%': winnersstring, + '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), + '%organiser%': `<@${giveaway.organiser}>` + })); + + await editMessage(winnersstring); + + for (const winnerID of winners) { + const member = channel.guild.members.cache.get(winnerID); + if (member) { + if (moduleConfig.winRoles) member.roles.add(moduleConfig.winRoles).then(() => { + }).catch(() => { + }); + if (moduleConfig.sendDMOnWin) { + member.send(embedType(moduleStrings['winner_DM_message'], { + '%prize%': giveaway.prize, + '%winners%': winnersstring, + '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), + '%organiser%': `<@${giveaway.organiser}>`, + '%url%': message.url + })).then(() => { + }).catch(() => { + }); + } + } + } + + /** + * Edits the message if needed + * @private + * @param {String} winners Winnerstring + * @returns {Promise} + */ + async function editMessage(winnerString) { + const endAt = new Date(parseInt(giveaway.endAt)); + if (!maxWinCount) { + const components = [{ + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + label: moduleStrings.buttonContent, + style: 'PRIMARY', + customId: 'giveaway', + disabled: true + }] + }]; + await message.edit( + embedType(moduleStrings['giveaway_message_edit_after_winning'], { + '%prize%': giveaway.prize, + '%endAt%': formatDate(endAt), + '%endAtDiscordFormation%': ``, + '%winners%': winnerString, + '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), + '%organiser%': `<@${giveaway.organiser}>`, + '%entryCount%': entries, + '%enteredCount%': enteredUsers + }, {components}) + ); + } + } +} + +module.exports.endGiveaway = endGiveaway; + +/** + * Checks if a [GuildMember](https://discord.js.org/#/docs/main/stable/class/GuildMember) passes the requirements for a giveaway + * @param {GuildMember} member Guild member + * @param {Object} giveaway Giveaway in which the user has to pass the requiremetns + * @returns {Promise} Returns array with these values: 1. if the users passes the requirements 2. Which requirements where not passed in a human-readable string + */ +async function checkRequirements(member, giveaway) { + let failedRequirements = false; + let notPassedRequirementsString = ''; + const moduleConfig = member.client.configurations['giveaways']['config']; + if (member.roles.cache.find(r => (moduleConfig.entryDeniedRoles || []).includes(r.id))) return [true, '']; + if (member.roles.cache.find(r => (moduleConfig.bypassRoles || []).includes(r.id))) { + return [failedRequirements, notPassedRequirementsString]; + } + for (const requirement of giveaway.requirements) { + switch (requirement.type) { + case 'roles': + let passedRoleRequirement = false; + let rolesString = ''; + for (const r of requirement.roles) { + rolesString = rolesString + `<@&${r}> `; + if (member.roles.cache.get(r)) passedRoleRequirement = true; + } + if (!passedRoleRequirement) { + notPassedRequirementsString = notPassedRequirementsString + `\t• ${localize('giveaways', 'roles-required', {r: rolesString})}\n`; + failedRequirements = true; + } + break; + case 'messages': + if (!giveaway.messageCount[member.user.id]) giveaway.messageCount[member.user.id] = 0; + if (parseInt(giveaway.messageCount[member.user.id]) < parseInt(requirement.messageCount)) { + notPassedRequirementsString = notPassedRequirementsString + `\t• ${localize('giveaways', 'required-messages-user', { + um: giveaway.messageCount[member.user.id], + mc: requirement.messageCount + })}\n`; + failedRequirements = true; + } + break; + } + } + return [failedRequirements, notPassedRequirementsString]; +} + +module.exports.checkRequirements = checkRequirements; + +/** + * Calculate the entries of a GuildMember + * @param {GuildMember} member [GuildMember](https://discord.js.org/#/docs/main/stable/class/GuildMember) + * @returns {number} Entries this user has + */ +function calculateUserEntries(member) { + const moduleConfig = member.client.configurations['giveaways']['config']; + let entries = 1; + for (const rID in moduleConfig.multipleEntries) { + if (member.roles.cache.get(rID)) entries = entries + parseFloat(moduleConfig.multipleEntries[rID]); + } + return entries; +} + +module.exports.calculateUserEntries = calculateUserEntries; \ No newline at end of file diff --git a/modules/giveaways/models/Giveaway.js b/modules/giveaways/models/Giveaway.js new file mode 100644 index 00000000..cdb2806e --- /dev/null +++ b/modules/giveaways/models/Giveaway.js @@ -0,0 +1,48 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class Giveaway extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + endAt: { + type: DataTypes.STRING + }, + ended: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + prize: DataTypes.STRING, + requirements: { + type: DataTypes.JSON, + defaultValue: [] + }, + countMessages: { // Yeah, I could get that from the requirements, but it's easier to fetch giveaways this way + type: DataTypes.BOOLEAN, + defaultValue: false + }, + messageCount: DataTypes.JSON, + entries: { + type: DataTypes.JSON, + defaultValue: {} + }, + sponsorWebsite: DataTypes.STRING, + winnerCount: DataTypes.INTEGER, + organiser: DataTypes.STRING, + messageID: DataTypes.STRING, + channelID: DataTypes.STRING + }, { + tableName: 'giveaways_giveaways', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'Giveaway', + 'module': 'giveaways' +}; \ No newline at end of file diff --git a/modules/giveaways/module.json b/modules/giveaways/module.json new file mode 100644 index 00000000..17ecee72 --- /dev/null +++ b/modules/giveaways/module.json @@ -0,0 +1,27 @@ +{ + "name": "giveaways", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/giveaways", + "commands-dir": "/commands", + "models-dir": "/models", + "events-dir": "/events", + "config-example-files": [ + "configs/strings.json", + "configs/config.json" + ], + "tags": [ + "community" + ], + "humanReadableName": { + "en": "Giveaways", + "de": "Gewinnspiele" + }, + "description": { + "en": "Easily create a giveaway in your server", + "de": "Erstelle einfach Gewinnspiele auf deinem Server" + } +} \ No newline at end of file diff --git a/modules/guess-the-number/commands/manage.js b/modules/guess-the-number/commands/manage.js new file mode 100644 index 00000000..162fe28e --- /dev/null +++ b/modules/guess-the-number/commands/manage.js @@ -0,0 +1,133 @@ +const {localize} = require('../../../src/functions/localize'); +const {randomIntFromInterval, embedType, lockChannel, unlockChannel} = require('../../../src/functions/helpers'); + +module.exports.beforeSubcommand = async function (interaction) { + if (interaction.member.roles.cache.filter(m => interaction.client.configurations['guess-the-number']['config'].adminRoles.includes(m.id)).size === 0) return interaction.reply({ + ephemeral: true, + content: '⚠️ To use this command, you need to be added to the adminRoles option in the SCNX-Dashboard.' + }); +}; + +module.exports.subcommands = { + 'end': async function(interaction) { + if (interaction.replied) return; + const item = await interaction.client.models['guess-the-number']['Channel'].findOne({where: {channelID: interaction.channel.id, ended: false}}); + if (!item) return interaction.reply({ + content: '⚠️ ' + localize('guess-the-number', 'session-not-running'), + ephemeral: true + }); + await lockChannel(interaction.channel, interaction.client.configurations['guess-the-number']['config'].adminRoles, '[guess-the-number] ' + localize('guess-the-number', 'game-ended')); + await item.destroy(); + interaction.reply({ + content: localize('guess-the-number', 'session-ended-successfully'), + ephemeral: true + }); + }, + 'status': async function(interaction) { + if (interaction.replied) return; + const item = await interaction.client.models['guess-the-number']['Channel'].findOne({where: {channelID: interaction.channel.id, ended: false}}); + if (!item) return interaction.reply({ + content: '⚠️ ' + localize('guess-the-number', 'session-not-running'), + ephemeral: true + }); + interaction.reply({ + content: `**${localize('guess-the-number', 'current-session')}**\n\n${localize('guess-the-number', 'number')}: ${item.number}\n${localize('guess-the-number', 'min-val')}: ${item.min}\n${localize('guess-the-number', 'max-val')}: ${item.max}\n${localize('guess-the-number', 'owner')}: <@${item.ownerID}>\n${localize('guess-the-number', 'guess-count')}: ${item.guessCount}`, + ephemeral: true, + allowedMentions: {parse: []} + }); + }, + 'create': async function(interaction) { + if (interaction.replied) return; + if (await interaction.client.models['guess-the-number']['Channel'].findOne({where: {channelID: interaction.channel.id, ended: false}})) return interaction.reply({ + content: '⚠️ ' + localize('guess-the-number', 'session-already-running'), + ephemeral: true + }); + if (interaction.options.getInteger('min') >= interaction.options.getInteger('max')) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('guess-the-number', 'min-max-discrepancy') + }); + const number = interaction.options.getInteger('number') || randomIntFromInterval(interaction.options.getInteger('min'), interaction.options.getInteger('max')); + if (number > interaction.options.getInteger('max')) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('guess-the-number', 'max-discrepancy') + }); + if (number < interaction.options.getInteger('min')) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('guess-the-number', 'min-discrepancy') + }); + await interaction.client.models['guess-the-number']['Channel'].create({ + channelID: interaction.channel.id, + number, + min: interaction.options.getInteger('min'), + max: interaction.options.getInteger('max'), + ownerID: interaction.user.id, + ended: false + }); + const pins = await interaction.channel.messages.fetchPinned(); + for (const pin of pins.values()) { + if (pin.author.id !== interaction.client.user.id) continue; + await pin.unpin(); + } + const m = await interaction.channel.send(embedType(interaction.client.configurations['guess-the-number']['config'].startMessage, {'%min%': interaction.options.getInteger('min'), '%max%': interaction.options.getInteger('max')}, {components: [{ + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + label: localize('guess-the-number', 'emoji-guide-button'), + style: 'SECONDARY', + customId: 'gtn-reaction-meaning' + }] + }]})); + await m.pin(); + + await unlockChannel(interaction.channel, '[guess-the-number] ' + localize('guess-the-number', 'game-started')); + + await interaction.reply({ + ephemeral: true, + content: localize('guess-the-number', 'created-successfully', {n: number}) + }); + } +}; + +module.exports.config = { + name: 'guess-the-number', + description: localize('guess-the-number', 'command-description'), + + defaultMemberPermissions: ['MANAGE_MESSAGES'], + options: [ + { + type: 'SUB_COMMAND', + name: 'status', + description: localize('guess-the-number', 'status-command-description') + }, + { + type: 'SUB_COMMAND', + name: 'create', + description: localize('guess-the-number', 'create-command-description'), + options: [ + { + type: 'INTEGER', + name: 'min', + required: true, + description: localize('guess-the-number', 'create-min-description') + }, + { + type: 'INTEGER', + name: 'max', + required: true, + description: localize('guess-the-number', 'create-max-description') + }, + { + type: 'INTEGER', + name: 'number', + required: false, + description: localize('guess-the-number', 'create-number-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'end', + description: localize('guess-the-number', 'end-command-description') + } + ] +}; \ No newline at end of file diff --git a/modules/guess-the-number/config.json b/modules/guess-the-number/config.json new file mode 100644 index 00000000..2522419b --- /dev/null +++ b/modules/guess-the-number/config.json @@ -0,0 +1,152 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "commandsWarnings": { + "special": [ + { + "name": "/guess-the-number", + "info": { + "en": "You need to first set the permissions in your server settings for this command and after that add them under \"adminRoles\" here.", + "de": "Du musst zuerst die Rechte in deinen Server-Einstellungen einstellen und danach diese unter \"AdminRollen\" hinzufügen." + } + } + ] + }, + "content": [ + { + "name": "adminRoles", + "humanName": { + "de": "Adminrollen", + "en": "Admin-Roles" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Every role that can manage game sessions", + "de": "Jede Rolle, welche Spielrunden verwalten kann" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "startMessage", + "humanName": { + "de": "Startnachricht", + "en": "Start-Message" + }, + "default": { + "en": { + "title": "Guess the Number - Game started", + "description": "Guess a number between %min% and %max%. Good look!" + }, + "de": { + "title": "Errate die Zahl - Das Spiel beginnt", + "description": "Errate eine Zahl zwischen %min% und %max%. Viel Glück!" + } + }, + "description": { + "de": "Nachricht, die am Anfang einer Runde gesendet wird", + "en": "Message that gets send when a new round gets started" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "min", + "description": { + "en": "Minimal value to guess", + "de": "Niedrigester möglichster Wert" + } + }, + { + "name": "max", + "description": { + "en": "Maximal value to guess", + "de": "Höchster möglichster Wert" + } + } + ] + }, + { + "name": "endMessage", + "humanName": { + "de": "Endnachricht", + "en": "End-Message" + }, + "default": { + "en": { + "title": "Guess the Number - Game ended", + "description": "Good game everyone!\nThe winner is %winner%.\nThe number was **%number%**.\nThere were around **%guessCount% guesses** in total." + }, + "de": { + "title": "Errate die Zahl - Das Spiel ist beendet", + "description": "Gutes Spiel!\nDer Gewinner ist %winner%.\nDie Zahl war **%number%**.\nInsgesamt wurde **%guessCount% mal** geraten." + } + }, + "description": { + "de": "Nachricht, die am Ende einer Runde gesendet wird", + "en": "Message that gets send when a round ends" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "min", + "description": { + "en": "Minimal value to guess", + "de": "Niedrigester möglichster Wert" + } + }, + { + "name": "max", + "description": { + "en": "Maximal value to guess", + "de": "Höchster möglichster Wert" + } + }, + { + "name": "winner", + "description": { + "en": "@-mention of the winner", + "de": "@-Erwähnung des Gewinners" + } + }, + { + "name": "guessCount", + "description": { + "en": "Count of guesses in this game session", + "de": "Anzahl der Versuche in dieser Runde" + } + }, + { + "name": "number", + "description": { + "en": "Winning number", + "de": "Nummer, die gesucht wurde" + } + } + ] + }, + { + "name": "higherLowerReactions", + "type": "boolean", + "humanName": { + "de": "Reagiere mit Höher / Geringer Emojis", + "en": "React with Lower / Higher reactions" + }, + "default": { + "en": false + }, + "description": { + "de": "Wenn aktiviert, reagiert der Bot bei falschen Versuchen mit ⬇ (wenn die gesuchte Zahl unter der gesendeten Zahl ist) oder mit ⬆ (wenn die gesuchte Zahl größer als die gesendete Zahl ist). Falls deaktiviert, wird der Bot nur mit ❌ bei falschen Versuchen reagieren.", + "en": "If enabled, the bot will react with ⬇ (if the guess is higher than the correct number) or with ⬆ (if the guess is lower than the correct number) on wrong guesses. If disabled, the bot will just react with ❌ on wrong guesses." + } + } + ] +} \ No newline at end of file diff --git a/modules/guess-the-number/events/interactionCreate.js b/modules/guess-the-number/events/interactionCreate.js new file mode 100644 index 00000000..14419e60 --- /dev/null +++ b/modules/guess-the-number/events/interactionCreate.js @@ -0,0 +1,7 @@ +const {localize} = require('../../../src/functions/localize'); +module.exports.run = async function (client, interaction) { + if (interaction.customId === 'gtn-reaction-meaning') return interaction.reply({ + ephemeral: true, + content: `## ${localize('guess-the-number', 'emoji-guide-button')}\n* :x:: ${localize('guess-the-number', 'guide-wrong-guess')}\n* :white_check_mark:: ${localize('guess-the-number', 'guide-win')}\n* :no_entry_sign:: ${localize('guess-the-number', 'guide-invalid-guess')}\n* :no_entry:: ${localize('guess-the-number', 'guide-admin-guess')}` + }); +}; \ No newline at end of file diff --git a/modules/guess-the-number/events/messageCreate.js b/modules/guess-the-number/events/messageCreate.js new file mode 100644 index 00000000..5ffaa251 --- /dev/null +++ b/modules/guess-the-number/events/messageCreate.js @@ -0,0 +1,41 @@ +const {embedType, lockChannel} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async (client, msg) => { + if (!client.botReadyAt) return; + if (msg.author.bot) return; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + const game = await client.models['guess-the-number']['Channel'].findOne({ + where: { + channelID: msg.channel.id, + ended: false + } + }); + if (!game) return; + if (msg.member.roles.cache.filter(m => m.client.configurations['guess-the-number']['config'].adminRoles.includes(m.id)).size !== 0) return msg.react('⛔'); + const parsedInt = parseInt(msg.content); + if (isNaN(parsedInt)) return msg.react('🚫'); + if (parsedInt < game.min || parsedInt > game.max) return msg.react('🚫'); + game.guessCount++; + await game.save(); + if (parsedInt !== game.number) { + if (client.configurations['guess-the-number']['config']['higherLowerReactions']) { + if (game.number < parsedInt) await msg.react('⬇'); + else await msg.react('⬆'); + return; + } + return msg.react('❌'); + } + await msg.react('✅'); + game.ended = true; + await game.save(); + await lockChannel(msg.channel, client.configurations['guess-the-number']['config'].adminRoles, '[guess-the-number] ' + localize('guess-the-number', 'game-ended')); + await msg.reply(embedType(client.configurations['guess-the-number']['config']['endMessage'], { + '%min%': game.min, + '%max%': game.max, + '%winner%': msg.author.toString(), + '%guessCount%': game.guessCount, + '%number%': game.number + })); +}; \ No newline at end of file diff --git a/modules/guess-the-number/models/Channel.js b/modules/guess-the-number/models/Channel.js new file mode 100644 index 00000000..9eadeac3 --- /dev/null +++ b/modules/guess-the-number/models/Channel.js @@ -0,0 +1,33 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class GuessTheNumberChannel extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + channelID: DataTypes.STRING, + number: DataTypes.INTEGER, + min: DataTypes.INTEGER, + max: DataTypes.INTEGER, + ownerID: DataTypes.STRING, + winnerID: DataTypes.STRING, + ended: DataTypes.BOOLEAN, + guessCount: { + type: DataTypes.INTEGER, + defaultValue: 0 + } + }, { + tableName: 'guess_the_number_Channel', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'Channel', + 'module': 'guess-the-number' +}; \ No newline at end of file diff --git a/modules/guess-the-number/module.json b/modules/guess-the-number/module.json new file mode 100644 index 00000000..c5230e4f --- /dev/null +++ b/modules/guess-the-number/module.json @@ -0,0 +1,26 @@ +{ + "name": "guess-the-number", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "commands-dir": "/commands", + "models-dir": "/models", + "events-dir": "/events", + "config-example-files": [ + "config.json" + ], + "tags": [ + "fun" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/guess-the-number", + "humanReadableName": { + "en": "Guess the number", + "de": "Errate die Nummer" + }, + "description": { + "en": "Select a number and let your users guess", + "de": "Wähle eine Nummer und lass deine Mitglieder diese erraten" + } +} \ No newline at end of file diff --git a/modules/hunt-the-code/commands/hunt-the-code-admin.js b/modules/hunt-the-code/commands/hunt-the-code-admin.js new file mode 100644 index 00000000..2df97745 --- /dev/null +++ b/modules/hunt-the-code/commands/hunt-the-code-admin.js @@ -0,0 +1,121 @@ +const {localize} = require('../../../src/functions/localize'); +const {randomString, postToSCNetworkPaste, formatDiscordUserName} = require('../../../src/functions/helpers'); + +module.exports.subcommands = { + 'create-code': function (interaction) { + interaction.client.models['hunt-the-code']['Code'].create({ + code: (interaction.options.getString('code') || (randomString(3) + '-' + randomString(3) + '-' + randomString(3))).toUpperCase(), + displayName: interaction.options.getString('display-name') + }).then((codeObject) => { + interaction.reply({ + ephemeral: true, + content: '✅ ' + localize('hunt-the-code', 'code-created', { + displayName: interaction.options.getString('display-name'), + code: codeObject.code + }) + }); + }).catch(() => { + interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('hunt-the-code', 'error-creating-code', {displayName: interaction.options.getString('display-name')}) + }); + }); + }, + 'end': async function (interaction) { + await interaction.deferReply({ephemeral: true}); + const url = await generateReport(interaction.client); + await interaction.client.models['hunt-the-code']['Code'].destroy({ + truncate: true + }); + await interaction.client.models['hunt-the-code']['User'].destroy({ + truncate: true + }); + await interaction.editReply({ + content: '✅ ' + localize('hunt-the-code', 'successful-reset', {url}) + }); + }, + 'report': async function (interaction) { + await interaction.deferReply({ephemeral: true}); + const url = await generateReport(interaction.client); + await interaction.editReply({ + content: localize('hunt-the-code', 'report', {url}) + }); + } +}; + +/** + * Generate a report of the current Code-Hunt-Session + * @param {Client} client Client + * @returns {Promise} URL to Report + */ +async function generateReport(client) { + let reportString = `# ${localize('hunt-the-code', 'report-header', {s: client.guild.name})}\n`; + const codes = await client.models['hunt-the-code']['Code'].findAll({ + order: [ + ['foundCount', 'DESC'] + ] + }); + const users = await client.models['hunt-the-code']['User'].findAll({ + order: [ + ['foundCount', 'DESC'] + ] + }); + reportString = reportString + `\n## ${localize('hunt-the-code', 'user-header')}\n| Rank | Tag | ID | Amount found | Codes |\n| --- | --- | --- | --- | --- |\n`; + for (const i in users) { + const user = users[i]; + const u = await client.users.fetch(user.id); + reportString = reportString + `| ${parseInt(i) + 1}. | ${formatDiscordUserName(u)} | ${u.id} | ${user.foundCount} | ${user.foundCodes.join(', ')} |\n`; + } + reportString = reportString + `\n## ${localize('hunt-the-code', 'code-header')}\n| Rank | Code | Display-Name | Times found |\n| --- | --- | --- | --- |\n`; + for (const i in codes) { + const code = codes[i]; + reportString = reportString + `| ${parseInt(i) + 1}. | ${code.code} | ${code.displayName} | ${code.foundCount} |\n`; + } + reportString = reportString + `\n



Generated at ${new Date().toLocaleString(client.locale)}.`; + return await postToSCNetworkPaste(reportString, { + expire: '1month', + burnafterreading: 0, + opendiscussion: 1, + textformat: 'markdown', + output: 'text', + compression: 'zlib' + }); +} + +module.exports.config = { + name: 'hunt-the-code-admin', + defaultMemberPermissions: ['MANAGE_MESSAGES'], + description: localize('hunt-the-code', 'admin-command-description'), + + options: [ + { + type: 'SUB_COMMAND', + name: 'create-code', + description: localize('hunt-the-code', 'create-code-description'), + options: [ + { + type: 'STRING', + name: 'display-name', + required: true, + description: localize('hunt-the-code', 'display-name-description') + }, + { + type: 'STRING', + name: 'code', + required: false, + description: localize('hunt-the-code', 'code-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'end', + description: localize('hunt-the-code', 'end-description') + }, + { + type: 'SUB_COMMAND', + name: 'report', + description: localize('hunt-the-code', 'report-description') + } + ] +}; \ No newline at end of file diff --git a/modules/hunt-the-code/commands/hunt-the-code.js b/modules/hunt-the-code/commands/hunt-the-code.js new file mode 100644 index 00000000..11f344d7 --- /dev/null +++ b/modules/hunt-the-code/commands/hunt-the-code.js @@ -0,0 +1,114 @@ +const {localize} = require('../../../src/functions/localize'); +const {embedType} = require('../../../src/functions/helpers'); +const {MessageEmbed} = require('discord.js'); + +module.exports.subcommands = { + 'redeem': async function (interaction) { + const moduleStrings = interaction.client.configurations['hunt-the-code']['strings']; + const codeObject = await interaction.client.models['hunt-the-code']['Code'].findOne({ + where: { + code: interaction.options.getString('code').toUpperCase() + } + }); + if (!codeObject) return interaction.reply(embedType(moduleStrings.codeNotFoundMessage, {}, {ephemeral: true})); + const [user] = await interaction.client.models['hunt-the-code']['User'].findOrCreate({ + where: { + id: interaction.user.id + } + }); + if (user.foundCodes.includes(codeObject.code)) return interaction.reply(embedType(moduleStrings.codeAlreadyRedeemed, { + '%userCodesCount%': user.foundCount, + '%displayName%': codeObject.displayName, + '%codeUseCount%': codeObject.foundCount + }, {ephemeral: true})); + user.foundCount++; + user.foundCodes = [...user.foundCodes, codeObject.code]; + await user.save(); + codeObject.foundCount++; + interaction.reply(embedType(moduleStrings.codeRedeemed, { + '%displayName%': codeObject.displayName, + '%codeUseCount%': codeObject.foundCount, + '%userCodesCount%': user.foundCount + }, {ephemeral: true})); + await codeObject.save(); + }, + 'profile': async function (interaction) { + const [user] = await interaction.client.models['hunt-the-code']['User'].findOrCreate({ + where: { + id: interaction.user.id + } + }); + const codes = await interaction.client.models['hunt-the-code']['Code'].findAll({ + attributes: ['displayName', 'code'] + }); + let foundCodes = ''; + for (const code of user.foundCodes) { + const codeObject = codes.find(c => c.code === code); + if (!codeObject) continue; + foundCodes = foundCodes + `\n• ${codeObject.displayName}`; + } + if (!foundCodes) foundCodes = localize('hunt-the-code', 'no-codes-found'); + interaction.reply(embedType(interaction.client.configurations['hunt-the-code']['strings'].profileMessage, { + '%username%': interaction.user.username, + '%foundCount%': user.foundCount, + '%allCodesCount%': codes.length, + '%foundCodes%': foundCodes + }, {ephemeral: true})); + }, + 'leaderboard': async function (interaction) { + const moduleStrings = interaction.client.configurations['hunt-the-code']['strings']; + const users = await interaction.client.models['hunt-the-code']['User'].findAll({ + attributes: ['id', 'foundCount'], + order: [ + ['foundCount', 'DESC'] + ], + limit: 20 + }); + let userString = ''; + for (const user of users) { + userString = userString + `\n<@${user.id}>: ${user.foundCount}`; + } + if (userString === '') userString = localize('hunt-the-code', 'no-users'); + const embed = new MessageEmbed() + .setDescription(userString) + .setTitle(moduleStrings.leaderboardMessage.title) + .setImage(moduleStrings.leaderboardMessage.image || null) + .setThumbnail(moduleStrings.leaderboardMessage.thumbnail || null) + .setColor(moduleStrings.leaderboardMessage.color); + interaction.reply({ + ephemeral: true, + embeds: [embed] + }); + } +}; + +module.exports.config = { + name: 'hunt-the-code', + description: localize('hunt-the-code', 'command-description'), + + options: [ + { + type: 'SUB_COMMAND', + name: 'redeem', + description: localize('hunt-the-code', 'redeem-description'), + options: [ + { + type: 'STRING', + name: 'code', + required: true, + description: localize('hunt-the-code', 'code-redeem-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'leaderboard', + description: localize('hunt-the-code', 'leaderboard-description') + }, + { + type: 'SUB_COMMAND', + name: 'profile', + description: localize('hunt-the-code', 'profile-description') + } + ] +}; \ No newline at end of file diff --git a/modules/hunt-the-code/models/Code.js b/modules/hunt-the-code/models/Code.js new file mode 100644 index 00000000..39fc1580 --- /dev/null +++ b/modules/hunt-the-code/models/Code.js @@ -0,0 +1,27 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class HuntTheCodeCode extends Model { + static init(sequelize) { + return super.init({ + code: { + type: DataTypes.STRING, + primaryKey: true + }, + creatorID: DataTypes.STRING, + displayName: DataTypes.STRING, + foundCount: { + type: DataTypes.INTEGER, + defaultValue: 0 + } + }, { + tableName: 'hunt-the-code_Code', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'Code', + 'module': 'hunt-the-code' +}; \ No newline at end of file diff --git a/modules/hunt-the-code/models/User.js b/modules/hunt-the-code/models/User.js new file mode 100644 index 00000000..929872c0 --- /dev/null +++ b/modules/hunt-the-code/models/User.js @@ -0,0 +1,29 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class HuntTheCodeUser extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.STRING, + primaryKey: true + }, + foundCount: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + foundCodes: { + type: DataTypes.JSON, + defaultValue: [] + } + }, { + tableName: 'hunt-the-code_User', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'User', + 'module': 'hunt-the-code' +}; \ No newline at end of file diff --git a/modules/hunt-the-code/module.json b/modules/hunt-the-code/module.json new file mode 100644 index 00000000..e77b5926 --- /dev/null +++ b/modules/hunt-the-code/module.json @@ -0,0 +1,25 @@ +{ + "name": "hunt-the-code", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/hunt-the-code", + "commands-dir": "/commands", + "models-dir": "/models", + "config-example-files": [ + "strings.json" + ], + "tags": [ + "community" + ], + "humanReadableName": { + "en": "Hunt the code", + "de": "Sammel die Codes" + }, + "description": { + "en": "Hide codes and let your users collect them", + "de": "Verstecke Codes und lasse sie von deinen Nutzern sammeln" + } +} \ No newline at end of file diff --git a/modules/hunt-the-code/strings.json b/modules/hunt-the-code/strings.json new file mode 100644 index 00000000..aa0f1986 --- /dev/null +++ b/modules/hunt-the-code/strings.json @@ -0,0 +1,207 @@ +{ + "description": { + "en": "Edit the messages and strings of the module here", + "de": "Stelle hier die Nachrichten des Modules ein" + }, + "humanName": { + "en": "Messages", + "de": "Nachrichten" + }, + "filename": "strings.json", + "content": [ + { + "name": "codeNotFoundMessage", + "humanName": { + "en": "Code-not-found Message", + "de": "Code-nicht-gefunden Nachricht" + }, + "default": { + "en": "⚠️ Sorry, this code is invalid ):", + "de": "⚠️ Dieser Code ist leider ungültig" + }, + "description": { + "en": "This message gets send, when an invalid code is being redeemed", + "de": "Diese Nachricht wird verschickt, wenn ein ungültiger Code eingelöst wird" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "codeAlreadyRedeemed", + "humanName": { + "en": "Code-already-Redeemed Message", + "de": "Code-bereits-eingelöst Nachricht" + }, + "default": { + "en": "Good news, you have already redeemed this code", + "de": "Gute Nachrichten, du hast diesen Code bereits eingelöst" + }, + "description": { + "en": "This message gets send, when a user tries to redeem a code that is already in their inventory", + "de": "Diese Nachricht wird verschickt, wenn ein Nutzer einen Code einlösen will, den er bereits eingelöst hat" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "displayName", + "description": { + "en": "Display-Name of the code that the user wants to redeem", + "de": "Anzeige-Name des Codes, denn der Nutzer einlösen möchte" + } + }, + { + "name": "codeUseCount", + "description": { + "en": "Count of times this code has already been redeemed", + "de": "Anzahl von Nutzer, die diesen Code bereits eingelöst haben" + } + }, + { + "name": "userCodesCount", + "description": { + "en": "Count of codes this user already has redeemed", + "de": "Anzahl der Codes, die dieser Nutzer bereits eingelöst hat" + } + } + ] + }, + { + "name": "codeRedeemed", + "humanName": { + "en": "Code-Redeemed Message", + "de": "Code-eingelöst Nachricht" + }, + "default": { + "en": "Good job, you have successfully redeemed the code **%displayName%**", + "de": "Gute Arbeit, du hast erfolgreich den Code **%displayName%** eingelöst." + }, + "description": { + "en": "This message gets send, when a user tries redeems a code", + "de": "Diese Nachricht wird verschickt, wenn ein Nutzer einen Code einlöst" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "displayName", + "description": { + "en": "Display-Name of the code", + "de": "Anzeige-Name des Codes, denn der Nutzer einlöst" + } + }, + { + "name": "codeUseCount", + "description": { + "en": "Count of times this code has already been redeemed", + "de": "Anzahl von Nutzer, die diesen Code bereits eingelöst haben" + } + }, + { + "name": "userCodesCount", + "description": { + "en": "Count of codes this user already has redeemed", + "de": "Anzahl der Codes, die dieser Nutzer bereits eingelöst hat" + } + } + ] + }, + { + "name": "profileMessage", + "humanName": { + "en": "Profile-Message", + "de": "Profil-Nachricht" + }, + "default": { + "en": { + "title": "Your profile, %username%!", + "fields": [ + { + "name": "Found codes", + "value": "%foundCodes%", + "inline": true + }, + { + "name": "Progress", + "value": "%foundCount%/%allCodesCount% found", + "inline": true + } + ] + }, + "de": { + "title": "Dein Profil %username%!", + "fields": [ + { + "name": "Gefunde Codes", + "value": "%foundCodes%", + "inline": true + }, + { + "name": "Fortschritt", + "value": "%foundCount%/%allCodesCount% gefunden", + "inline": true + } + ] + } + }, + "description": { + "en": "This message gets send, when a user opens their profile", + "de": "Diese Nachricht wird versendet, wenn ein Nutzer sein Profil öffnet" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "foundCodes", + "description": { + "en": "All codes that this user has found", + "de": "Alle Codes, die der Nutzer gefunden hat" + } + }, + { + "name": "username", + "description": { + "en": "Username of the user running the command", + "de": "Nutzername des Nutzers, der den Befehl ausführt" + } + }, + { + "name": "foundCount", + "description": { + "en": "Count of found codes", + "de": "Anzahl aller gefunden Codes" + } + }, + { + "name": "allCodesCount", + "description": { + "en": "Count of all available codes", + "de": "Anzahl aller verfügbaren Codes" + } + } + ] + }, + { + "name": "leaderboardMessage", + "humanName": { + "en": "Leaderboard-Message", + "de": "Leaderboard-Nachricht" + }, + "default": { + "en": { + "title": "Leaderboard", + "color": "GREEN", + "thumbnail": "", + "image": "" + } + }, + "description": {}, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + } + ] +} \ No newline at end of file diff --git a/modules/info-commands/commands/info.js b/modules/info-commands/commands/info.js new file mode 100644 index 00000000..c642b30f --- /dev/null +++ b/modules/info-commands/commands/info.js @@ -0,0 +1,259 @@ +const {localize} = require('../../../src/functions/localize'); +const { + embedType, + pufferStringToSize, + dateToDiscordTimestamp, + formatDiscordUserName, formatNumber +} = require('../../../src/functions/helpers'); +const {MessageEmbed} = require('discord.js'); +const {AgeFromDate} = require('age-calculator'); +const {stringNames} = require('../../invite-tracking/events/guildMemberJoin'); + +// THIS IS PAIN. Rewrite it as soon as possible + +module.exports.subcommands = { + 'server': async function (interaction) { + const moduleStrings = interaction.client.configurations['info-commands']['strings']; + const embed = new MessageEmbed() + .setTitle(localize('info-commands', 'information-about-server', {s: interaction.guild.name})) + .setColor('GOLD') + .setThumbnail(interaction.guild.iconURL()) + .setImage(interaction.guild.bannerURL()) + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}); + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); + if (interaction.guild.afkChannel) embed.addField(moduleStrings.serverinfo.afkChannel, `<#${interaction.guild.afkChannelID}> (${interaction.guild.afkTimeout}s)`, true); + if (interaction.guild.description) embed.setDescription(interaction.guild.description); + embed.addField(moduleStrings.serverinfo.id, '`' + interaction.guild.id + '`', true); + const owner = await interaction.guild.fetchOwner(); + embed.addField(moduleStrings.serverinfo.owner, `<@${owner.id}> \`${owner.id}\``, true); + embed.addField(moduleStrings.serverinfo.boosts, `${localize('info-commands', 'boostLevel')}: ${localize('boostTier', interaction.guild.premiumTier)}\n${localize('info-commands', 'boostCount')}: ${interaction.guild.premiumSubscriptionCount}`, true); + embed.addField(moduleStrings.serverinfo.emojiCount, interaction.guild.emojis.cache.size.toString(), true); + if (interaction.guild.stickers.cache.size !== 0) embed.addField(moduleStrings.serverinfo.stickerCount, interaction.guild.stickers.cache.size.toString(), true); + embed.addField(moduleStrings.serverinfo.roleCount, interaction.guild.roles.cache.size.toString(), true); + if (interaction.guild.rulesChannelID) embed.addField(moduleStrings.serverinfo.rulesChannel, `<#${interaction.guild.rulesChannelID}>`, true); + if (interaction.guild.systemChannelID) embed.addField(moduleStrings.serverinfo.dcSystemChannel, `<#${interaction.guild.systemChannelID}>`, true); + embed.addField(moduleStrings.serverinfo.verificationLevel, localize('guildVerification', interaction.guild.verificationLevel), true); + const bans = await interaction.guild.bans.fetch(); + embed.addField(moduleStrings.serverinfo.banCount, bans.size.toString(), true); + embed.addField(moduleStrings.serverinfo.createdAt, ``, true); + const members = await interaction.guild.members.fetch(); + embed.addField(moduleStrings.serverinfo.members, `\`\`\`| ${localize('info-commands', 'userCount')} | ${localize('info-commands', 'memberCount')} | Online |\n| ${pufferStringToSize(members.size, localize('info-commands', 'userCount').length)} | ${pufferStringToSize(members.filter(m => !m.user.bot).size, localize('info-commands', 'memberCount').length)} | ${pufferStringToSize(members.filter(m => m.presence && (m.presence || {}).status !== 'offline').size, localize('info-commands', 'onlineCount').length)} |\`\`\``); + embed.addField(moduleStrings.serverinfo.channels, `\`\`\`| ${localize('info-commands', 'textChannel')} | ${localize('info-commands', 'voiceChannel')} | ${localize('info-commands', 'categoryChannel')} | ${localize('info-commands', 'otherChannel')} |\n| ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type === 'GUILD_TEXT').size.toString(), localize('info-commands', 'textChannel').length)} | ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type === 'GUILD_VOICE').size.toString(), localize('info-commands', 'voiceChannel').length)} | ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type === 'GUILD_CATEGORY').size.toString(), localize('info-commands', 'categoryChannel').length)} | ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type !== 'GUILD_VOICE' && c.type !== 'GUILD_TEXT' && c.type !== 'GUILD_CATEGORY').size.toString(), localize('info-commands', 'otherChannel').length)} |\`\`\``); + let featuresstring = ''; + interaction.guild.features.forEach(f => { + featuresstring = featuresstring + `${f[0].toUpperCase() + f.toLowerCase().substring(1)}, `; + }); + if (featuresstring !== '') featuresstring = featuresstring.substring(0, featuresstring.length - 2); + else featuresstring = moduleStrings.serverinfo.noFeaturesEnabled; + embed.addField(moduleStrings.serverinfo.features, `\`\`\`${featuresstring}\`\`\``); + interaction.reply({embeds: [embed], ephemeral: true}); + }, + 'channel': async function (interaction) { + const moduleStrings = interaction.client.configurations['info-commands']['strings']; + const channel = interaction.options.getChannel('channel') || interaction.channel; + const embed = new MessageEmbed() + .setTitle(localize('info-commands', 'information-about-channel', {c: channel.name})) + .addField(moduleStrings.channelInfo.type, localize('channelType', channel.type.toString()), true) + .addField(moduleStrings.channelInfo.id, channel.id, true) + .addField(moduleStrings.channelInfo.createdAt, ``, true) + .addField(moduleStrings.channelInfo.name, channel.name, true) + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) + .setColor('GREEN'); + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); + if (channel.parent) embed.addField(moduleStrings.channelInfo.parent, channel.parent.name, true); + if (channel.position) embed.addField(moduleStrings.channelInfo.position, channel.position.toString(), true); + if (channel.topic) embed.setDescription(channel.topic); + if (channel.type.includes('THREAD')) { + if (channel.archiveTimestamp !== channel.createdTimestamp) embed.addField(moduleStrings.channelInfo.threadArchivedAt, ``, true); + if (channel.autoArchiveDuration) embed.addField(moduleStrings.channelInfo.threadAutoArchiveDuration, `${channel.autoArchiveDuration}min`, true); + if (channel.ownerId) embed.addField(moduleStrings.channelInfo.threadOwner, `<@${channel.ownerId}>`, true); + if (channel.messageCount && channel.messageCount < 50) embed.addField(moduleStrings.channelInfo.threadMessages, channel.messageCount.toString(), true); + if (channel.memberCount && channel.memberCount < 50) embed.addField(moduleStrings.channelInfo.threadMemberCount, channel.memberCount.toString(), true); + } + if (channel.type === 'GUILD_STAGE_VOICE' && channel.stageInstance && !(channel.stageInstance || {}).deleted) { + embed.addField(moduleStrings.channelInfo.stageInstanceName, channel.stageInstance.topic, true); + embed.addField(moduleStrings.channelInfo.stageInstancePrivacy, localize('stagePrivacy', channel.stageInstance.privacyLevel.toString()), true); + } + if (channel.members && channel.members.size !== 0 && (channel.type === 'GUILD_VOICE' || channel.type === 'GUILD_STAGE_VOICE')) { + let memberString = ''; + channel.members.forEach(m => { + memberString = memberString + `<@${m.user.id}>, `; + }); + memberString = memberString.substring(0, memberString.length - 2); + embed.addField(moduleStrings.channelInfo.membersInChannel, memberString); + } + interaction.reply({embeds: [embed], ephemeral: true}); + }, + 'role': async function (interaction) { + const moduleStrings = interaction.client.configurations['info-commands']['strings']; + const role = interaction.options.getRole('role', true); + const embed = new MessageEmbed() + .setTitle(localize('info-commands', 'information-about-role', {r: role.name})) + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) + .addField(moduleStrings.roleInfo.createdAt, ``, true) + .addField(moduleStrings.roleInfo.position, role.position.toString(), true) + .addField(moduleStrings.roleInfo.id, role.id, true) + .addField(moduleStrings.roleInfo.name, role.name, true) + .setColor(role.color || 'GREEN'); + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); + if (role.color) embed.addField(moduleStrings.roleInfo.color, role.hexColor, true); + if (role.members) { + embed.addField(moduleStrings.roleInfo.memberWithThisRoleCount, role.members.size.toString(), true); + if (role.members.size <= 10 && role.members.size !== 0) { + let memberstring = ''; + role.members.forEach(m => { + memberstring = memberstring + `<@${m.id}>, `; + }); + memberstring = memberstring.substring(0, memberstring.length - 2); + embed.addField(moduleStrings.roleInfo.memberWithThisRole, memberstring); + } + } + let permissionstring = ''; + if (role.permissions.toArray().includes('ADMINISTRATOR')) permissionstring = 'ADMINISTRATOR'; + else { + role.permissions.toArray().forEach(p => { + permissionstring = permissionstring + `${p}, `; + }); + permissionstring = permissionstring.substring(0, permissionstring.length - 2); + } + embed.addField(moduleStrings.roleInfo.permissions, '```' + permissionstring + '```'); + let features = ''; + if (role.hoist) features = features + `• ${localize('info-commands', 'hoisted')}\n`; + if (role.mentionable) features = features + `• ${localize('info-commands', 'mentionable')}\n`; + if (role.managed) features = features + `• ${localize('info-commands', 'managed')}\n`; + embed.setDescription(features); + interaction.reply({ephemeral: true, embeds: [embed]}); + }, + 'user': async function (interaction) { + const moduleStrings = interaction.client.configurations['info-commands']['strings']; + const member = interaction.options.getMember('user') || interaction.member; + if (!member) return interaction.reply(embedType(moduleStrings['user_not_found'], {}, {ephemeral: true})); + let birthday = null; + let levelUserData = null; + if (interaction.client.models['birthday']) { + birthday = await interaction.client.models['birthday']['User'].findOne({ + where: { + id: member.user.id + } + }); + } + if (interaction.client.models['levels']) { + levelUserData = await interaction.client.models['levels']['User'].findOne({ + where: { + userID: member.user.id + } + }); + } + + const embed = new MessageEmbed() + .setTitle(localize('info-commands', 'information-about-user', {u: formatDiscordUserName(member.user)})) + .setColor(member.displayColor || 'GREEN') + .setThumbnail(member.user.avatarURL({dynamic: true})) + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) + .addField(moduleStrings.userinfo.tag, formatDiscordUserName(member.user), true) + .addField(moduleStrings.userinfo.id, member.user.id, true) + .addField(moduleStrings.userinfo.createdAt, ` ()`, true) + .addField(moduleStrings.userinfo.joinedAt, ` ()`, true); + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); + if (member.user.presence) embed.addField(moduleStrings.userinfo.currentStatus, member.user.presence.status, true); + if (member.nickname) embed.addField(moduleStrings.userinfo.nickname, member.nickname, true); + if (member.premiumSince) embed.addField(moduleStrings.userinfo.boosterSince, dateToDiscordTimestamp(member.premiumSince), true); + if (member.displayColor) embed.addField(moduleStrings.userinfo.displayColor, member.displayHexColor, true); + if (member.voice.channel) embed.addField(moduleStrings.userinfo.currentVoiceChannel, member.voice.channel.toString(), true); + if (member.roles.highest) embed.addField(moduleStrings.userinfo.highestRole, `<@&${member.roles.highest.id}>`, true); + if (member.roles.hoist) embed.addField(moduleStrings.userinfo.hoistRole, `<@&${member.roles.hoist.id}>`, true); + if (birthday) { + let dateString = `${birthday.day}.${birthday.month}${birthday.year ? `.${birthday.year}` : ''}`; + if (birthday.year) { + const age = new AgeFromDate(new Date(birthday.year, birthday.month - 1, birthday.day)).age; + dateString = `[${dateString}](https://sc-network.net/age?age=${age} "${localize('birthdays', 'age-hover', {a: age})}")`; + } + embed.addField(moduleStrings.userinfo.birthday, dateString, true); + } + if (levelUserData) { + embed.addField(moduleStrings.userinfo.xp, `${formatNumber(levelUserData.xp)}/${formatNumber(levelUserData.level * 750 + ((levelUserData.level - 1) * 500))}`, true); + embed.addField(moduleStrings.userinfo.level, levelUserData.level.toString(), true); + embed.addField(moduleStrings.userinfo.messages, levelUserData.messages.toString(), true); + } + if (interaction.client.models['invite-tracking']) { + const invitedUsers = await interaction.client.models['invite-tracking']['UserInvite'].findAll({ + where: { + inviter: member.user.id + } + }); + const userInvites = await interaction.client.models['invite-tracking']['UserInvite'].findAll({ + where: { + userID: member.user.id, + left: false + }, + order: [['createdAt', 'DESC']] + }); + if (userInvites[0]) embed.addField(moduleStrings.userinfo['invited-by'], `${localize('invite-tracking', stringNames[userInvites[0].inviteType])}${userInvites[0].inviter ? ` by <@${userInvites[0].inviter}>` : ''}`, true); + embed.addField(moduleStrings.userinfo.invites, `\`\`\`| ${localize('info-commands', 'total-invites')} | ${localize('info-commands', 'active-invites')} | ${localize('info-commands', 'left-invites')} |\n| ${pufferStringToSize(invitedUsers.length.toString(), localize('info-commands', 'total-invites').length)} | ${pufferStringToSize(invitedUsers.filter(i => !i.left).length.toString(), localize('info-commands', 'active-invites').length)} | ${pufferStringToSize(invitedUsers.filter(i => i.left).length.toString(), localize('info-commands', 'left-invites').length)} |\`\`\``); + } + let permstring = ''; + member.permissions.toArray().forEach(p => { + if (!member.permissions.toArray().includes('ADMINISTRATOR')) permstring = permstring + `${p}, `; + }); + if (member.permissions.toArray().includes('ADMINISTRATOR')) permstring = 'ADMINISTRATOR '; + if (permstring !== '') permstring = permstring.substring(0, permstring.length - 2); + else permstring = moduleStrings.userinfo.noPermissions; + embed.addField(moduleStrings.userinfo.permissions, `\`\`\`${permstring}\`\`\``); + interaction.reply({ + embeds: [embed], + ephemeral: true + }); + } +}; + +module.exports.config = { + name: 'info', + description: localize('info-commands', 'info-command-description'), + + options: [ + { + type: 'SUB_COMMAND', + name: 'user', + description: localize('info-commands', 'command-userinfo-description'), + options: [ + { + type: 'USER', + name: 'user', + required: false, + description: localize('info-commands', 'argument-userinfo-user-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'role', + description: localize('info-commands', 'command-roleinfo-description'), + options: [ + { + type: 'ROLE', + name: 'role', + required: true, + description: localize('info-commands', 'argument-roleinfo-role-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'channel', + description: localize('info-commands', 'command-channelinfo-description'), + options: [ + { + type: 'CHANNEL', + name: 'channel', + required: false, + description: localize('info-commands', 'argument-channelinfo-channel-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'server', + description: localize('info-commands', 'command-serverinfo-description') + } + ] +}; \ No newline at end of file diff --git a/modules/info-commands/module.json b/modules/info-commands/module.json new file mode 100644 index 00000000..1f8020d8 --- /dev/null +++ b/modules/info-commands/module.json @@ -0,0 +1,24 @@ +{ + "name": "info-commands", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "commands-dir": "/commands", + "config-example-files": [ + "strings.json" + ], + "tags": [ + "moderation" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/info-commands", + "humanReadableName": { + "en": "Info-Commands", + "de": "Info-Befehle" + }, + "description": { + "en": "Adds info-commands with information about specific parts of your server", + "de": "Fügt viele Info-Commands zu deinen Server hinzu" + } +} \ No newline at end of file diff --git a/modules/info-commands/strings.json b/modules/info-commands/strings.json new file mode 100644 index 00000000..eea6fb9c --- /dev/null +++ b/modules/info-commands/strings.json @@ -0,0 +1,293 @@ +{ + "description": {}, + "humanName": {}, + "filename": "strings.json", + "content": [ + { + "name": "serverinfo", + "humanName": { + "de": "Serverinfo" + }, + "default": { + "en": { + "id": "ID", + "owner": "Owner", + "boosts": "Boosts", + "emojiCount": "Emoji-Count", + "region": "Region", + "roleCount": "Role-Count", + "rulesChannel": "Rules-Channel", + "dcSystemChannel": "Discord-System-Channel", + "verificationLevel": "Verification-Level", + "banCount": "Banns", + "createdAt": "Created at", + "members": "Members", + "channels": "Channels", + "features": "Features", + "noFeaturesEnabled": "No features enabled", + "afkChannel": "AFK-Channel", + "stickerCount": "Sticker-Count" + }, + "de": { + "id": "ID", + "owner": "Eigentümer", + "boosts": "Boosts", + "emojiCount": "Emoji-Anzahl", + "region": "Region", + "roleCount": "Rollen-Anzahl", + "rulesChannel": "Regelkanal", + "dcSystemChannel": "Discord-Systemkanal", + "verificationLevel": "Verifizierungsstufe", + "banCount": "Banns", + "createdAt": "Erstellt am", + "members": "Mitglieder", + "channels": "Kanäle", + "features": "Funktionen", + "noFeaturesEnabled": "Keine Funktionen aktiviert", + "afkChannel": "AFK-Kanal", + "stickerCount": "Sticker-Anzahl" + } + }, + "description": { + "en": "You can change the parts of the serverinfo-command here", + "de": "Hier kannst du die Teile des serverinfo-Befehls anpassen" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + }, + { + "name": "userinfo", + "humanName": { + "de": "Userinfo" + }, + "default": { + "en": { + "id": "ID", + "tag": "Tag", + "currentStatus": "Current status", + "createdAt": "Account created at", + "joinedAt": "Joined Server at", + "nickname": "Nickname", + "boosterSince": "Server-Booster since", + "displayColor": "Display-Color", + "currentVoiceChannel": "Current Voice-Channel", + "highestRole": "Highest role", + "hoistRole": "Hoisted role", + "birthday": "Birthday", + "permissions": "Permissions", + "xp": "XP", + "invited-by": "Invited by", + "invites": "Invites", + "level": "Level", + "messages": "Messages", + "noPermissions": "This user does not have any permissions ):" + }, + "de": { + "id": "ID", + "tag": "Tag", + "currentStatus": "Aktueller Status", + "createdAt": "Account erstellt am", + "joinedAt": "Server beigetreten am", + "nickname": "Nickname", + "boosterSince": "Server-Booster seit", + "displayColor": "Anzeigefarbe", + "currentVoiceChannel": "Aktueller Sprachkanal", + "highestRole": "Höchste Rolle", + "hoistRole": "Gelistete Rolle", + "birthday": "Geburtstag", + "permissions": "Berechtigungen", + "xp": "XP", + "level": "Level", + "messages": "Nachrichten", + "noPermissions": "Dieser Nutzer hat keine Berechtigungen ):", + "invited-by": "Invited by", + "invites": "Invites" + } + }, + "description": { + "en": "You can change the parts of the userinfo-command here", + "de": "Hier kannst du die Teile des userinfo-Befehls anpassen" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + }, + { + "name": "channelInfo", + "humanName": { + "de": "Channelinfo" + }, + "default": { + "en": { + "id": "ID", + "createdAt": "Created at", + "type": "Type", + "name": "Name", + "parent": "Category", + "topic": "Topic", + "position": "Current position", + "stageInstanceName": "Stage topic", + "stageInstancePrivacy": "Stage Privacy", + "threadArchivedAt": "Thread archived at", + "threadAutoArchiveDuration": "Thread auto Archive Duration", + "threadOwner": "Thread-Owner", + "threadMessages": "Messages in thread", + "threadMemberCount": "Members in this thread", + "membersInChannel": "Members currently in this channel" + }, + "de": { + "id": "ID", + "createdAt": "Erstellt am", + "type": "Typ", + "name": "Name", + "parent": "Kategorie", + "topic": "Kanalbeschreibung", + "position": "Aktuelle Position", + "stageInstanceName": "Stage Thema", + "stageInstancePrivacy": "Stage Privacy", + "threadArchivedAt": "Thread archiviert am", + "threadAutoArchiveDuration": "Automatische Threadarchivierungsdauer", + "threadOwner": "Thread-Besitzer", + "threadMessages": "Nachrichten im Thread", + "threadMemberCount": "Mitglieder in diesem Thread", + "membersInChannel": "Mitglieder, die sich aktuell in diesem Kanal befinden" + } + }, + "description": { + "en": "You can change the parts of the channelinfo-command here", + "de": "Hier kannst du die Teile des channelinfo-Befehls anpassen" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + }, + { + "name": "roleInfo", + "humanName": { + "de": "Roleinfo" + }, + "default": { + "en": { + "id": "ID", + "createdAt": "Created at", + "color": "Color", + "name": "Name", + "position": "Current position", + "memberWithThisRoleCount": "Count of members with this role", + "memberWithThisRole": "Members with this role", + "permissions": "Permissions" + }, + "de": { + "id": "ID", + "createdAt": "Erstellt am", + "color": "Farbe", + "name": "Name", + "position": "Aktuelle Position", + "memberWithThisRoleCount": "Anzahl der Mitglieder mit dieser Rolle", + "memberWithThisRole": "Mitglieder mit dieser Rolle", + "permissions": "Berechtigungen" + } + }, + "description": { + "en": "You can change the parts of the roleinfo-command here", + "de": "Hier kannst du die Teile des serverinfo-Befehls anpassen" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + }, + { + "name": "user_not_found", + "humanName": { + "de": "Nutzer nicht gefunden" + }, + "default": { + "en": "I could not find this user - try using an ID or a mention", + "de": "Dieser Nutzer konnte nicht gefunden werden - versuche eine ID oder eine Erwähnung zu verwenden" + }, + "description": { + "en": "Message that gets send if the user provided an invalid userid", + "de": "Nachricht, die gesendet wird, wenn der Nutzer eine Ungültige NutzerID angibt" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "channel_not_found", + "humanName": { + "de": "Kanal nicht gefunden" + }, + "default": { + "en": "I could not find this channel - try using an ID or a mention", + "de": "Dieser Kanal konnte nicht gefunden werden - versuche eine ID oder eine Erwähnung zu verwenden" + }, + "description": { + "en": "Message that gets send if the user provided an invalid userid", + "de": "Nachricht, die gesendet wird, wenn der Nutzer eine Ungültige KanalID angibt" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "role_not_found", + "humanName": { + "de": "Rolle nicht gefunden" + }, + "default": { + "en": "I could not find this role - try using an ID or a mention", + "de": "Diese Rolle konnte nicht gefunden werden - versuche eine ID oder eine Erwähnung zu verwenden" + }, + "description": { + "en": "Message that gets send if the user provided an invalid roleid", + "de": "Nachricht, die gesendet wird, wenn der Nutzer eine Ungültige RollenID angibt" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "avatarMsg", + "humanName": { + "de": "Avatar-Nachricht" + }, + "default": { + "en": "Here is the avatar: (Please reminder that the image may be protected under copyright-law)", + "de": "Hier ist der Avatar: (Bitte beachte, dass das Bild eventuell urheberrechtlich geschützt ist)" + }, + "description": { + "en": "Message that gets send if the user requested an avatar", + "de": "Nachricht, die gesendet wird, wenn ein Nutzer einen Avatar anfragt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "avatarUrl", + "description": { + "en": "URL to the avatar", + "de": "URL zum Avatar" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the requested user", + "de": "Tag des gewünschten Nutzers" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/invite-tracking/commands/trace-invites.js b/modules/invite-tracking/commands/trace-invites.js new file mode 100644 index 00000000..957d68eb --- /dev/null +++ b/modules/invite-tracking/commands/trace-invites.js @@ -0,0 +1,75 @@ +const {localize} = require('../../../src/functions/localize'); +const {stringNames} = require('../events/guildMemberJoin'); + +module.exports.run = async function (interaction) { + await interaction.deferReply({ephemeral: true}); + const user = interaction.options.getUser('user', true); + const invitedUsers = await interaction.client.models['invite-tracking']['UserInvite'].findAll({ + where: { + inviter: user.id, + left: false + } + }); + const userInvites = await interaction.client.models['invite-tracking']['UserInvite'].findAll({ + where: { + userID: user.id, + left: false + }, + order: [['createdAt', 'DESC']] + }); + + let content = `**${localize('invite-tracking', 'invited-by')}**\n`; + if (!userInvites[0]) content = content + localize('invite-tracking', 'inviter-not-found'); + else content = content + `${localize('invite-tracking', stringNames[userInvites[0].inviteType])}${userInvites[0].inviter ? ` by <@${userInvites[0].inviter}>` : ''}${userInvites[0].inviteCode ? ` via code [${userInvites[0].inviteCode}](https://discord.gg/${userInvites[0].inviteCode})` : ''}`; + + content = content + `\n\n**${localize('invite-tracking', 'invited-users')}**\n`; + if (invitedUsers.length === 0) content = content + localize('invite-tracking', 'no-users-invited'); + else { + let i = 0; + for (const invite of invitedUsers) { + i++; + if (i > 10) continue; + content = content + `<@${invite.userID}>\n`; + } + if (i > 10) content = content + localize('invite-tracking', 'and-x-more-users', {x: i - 10}) + '\n'; + } + + content = content + `\n**${localize('invite-tracking', 'created-invites')}**\n`; + const guildInvites = await interaction.guild.invites.fetch(); + let y = 0; + for (const invite of guildInvites.filter(i => i.inviter.id === user.id).values()) { + y++; + if (y > 5) continue; + content = content + `[${invite.code}](${invite.url}) (${invite.uses}${invite.maxUses ? `/${invite.maxUses}` : ''} uses) to ${invite.channel.toString()}\n`; + } + if (y > 5) content = content + localize('invite-tracking', 'and-x-more-invites', {x: y - 5}) + '\n'; + if (y === 0) content = content + `${localize('invite-tracking', 'no-invites')}\n`; + + content = content + `\n*${localize('invite-tracking', 'not-showing-left-users')}*`; + await interaction.editReply({content, allowedMentions: {parse: []}, components: [{ + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + label: '🗑️ ' + localize('invite-tracking', 'revoke-user-invite'), + style: 'DANGER', + customId: `uinv-rev-${user.id}` + } + ] + }]}); +}; + +module.exports.config = { + name: 'trace-invites', + defaultMemberPermissions: ['MODERATE_MEMBERS'], + description: localize('invite-tracking', 'trace-command-description'), + + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('invite-tracking', 'argument-user-description') + } + ] +}; \ No newline at end of file diff --git a/modules/invite-tracking/config.json b/modules/invite-tracking/config.json new file mode 100644 index 00000000..a0a03397 --- /dev/null +++ b/modules/invite-tracking/config.json @@ -0,0 +1,28 @@ +{ + "description": {}, + "humanName": {}, + "filename": "config.json", + "commandsWarnings": { + "normal": [ + "/trace-invites" + ] + }, + "content": [ + { + "name": "logchannel-id", + "humanName": { + "en": "Invite-Log-Channel", + "de": "Invite-Log-Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel in which information about new members should get logged (optional)", + "de": "Kanal, in welchem Informationen über neue Nutzer geloggt werden sollen (optional)" + }, + "type": "channelID", + "allowNull": true + } + ] +} \ No newline at end of file diff --git a/modules/invite-tracking/events/guildMemberJoin.js b/modules/invite-tracking/events/guildMemberJoin.js new file mode 100644 index 00000000..436bf804 --- /dev/null +++ b/modules/invite-tracking/events/guildMemberJoin.js @@ -0,0 +1,79 @@ +const {localize} = require('../../../src/functions/localize'); +const {MessageEmbed} = require('discord.js'); +const {dateToDiscordTimestamp} = require('../../../src/functions/helpers'); + +const stringNames = { + normal: 'normal-invite', + vanity: 'vanity-invite', + permissions: 'missing-permissions', + unknown: 'unknown-invite' +}; +module.exports.stringNames = stringNames; + +module.exports.run = async (client, member, type, invite) => { + if (!client.botReadyAt) return; + if (member.guild.id !== client.guild.id) return; + + const moduleConfig = client.configurations['invite-tracking']['config']; + + const beforeInvites = await client.models['invite-tracking']['UserInvite'].findAll({ + where: { + userID: member.user.id + }, + order: [['createdAt', 'DESC']] + }); + + await client.models['invite-tracking']['UserInvite'].create({ + inviteCode: invite ? invite.code : null, + inviteType: type, + inviter: invite ? invite.inviter.id : null, + userID: member.user.id + }); + + if (moduleConfig['logchannel-id']) { + const c = client.channels.cache.get(moduleConfig['logchannel-id']); + if (!c) return client.logger.error(localize('invite-tracking', 'log-channel-not-found-but-set', {c: moduleConfig['logchannel-id']})); + const components = []; + const embed = new MessageEmbed() + .setTitle('📥 ' + localize('invite-tracking', 'new-member')) + .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) + .setColor('GREEN') + .addField(localize('invite-tracking', 'member'), `${member.toString()} (\`${member.user.id}\`)`, true) + .addField(localize('invite-tracking', 'invite-type'), localize('invite-tracking', stringNames[type]), true); + if (client.strings.disableFooterTimestamp) embed.setTimestamp(); + if (beforeInvites.length !== 0) embed.setDescription(localize('invite-tracking', 'joined-for-the-x-time', {u: member.user.username, x: beforeInvites.length, t: dateToDiscordTimestamp(beforeInvites[0].createdAt)})); + if (invite) { + const fetchedInvite = await member.guild.invites.fetch({code: invite.code, force: true}).catch(() => {}); + if (fetchedInvite) invite = fetchedInvite; + let inviteString = localize('invite-tracking', 'invite-code', {c: invite.code, u: invite.url}); + if (invite.channel) inviteString = inviteString + '\n' + localize('invite-tracking', 'invite-channel', {c: invite.channel.toString()}); + if (invite.createdAt) inviteString = inviteString + '\n' + localize('invite-tracking', 'created-at', {t: dateToDiscordTimestamp(invite.createdAt)}); + if (invite.expiresAt) inviteString = inviteString + '\n' + localize('invite-tracking', 'expires-at', {t: dateToDiscordTimestamp(invite.expiresAt)}); + if (invite.inviter) { + const userInvites = await client.models['invite-tracking']['UserInvite'].findAll({ + where: { + inviter: invite.inviter.id + } + }); + inviteString = inviteString + '\n' + localize('invite-tracking', 'inviter', { + u: invite.inviter.toString(), + i: userInvites.length, + a: userInvites.filter(i => !i.left).length + }); + } + if (invite.uses) inviteString = inviteString + '\n' + localize('invite-tracking', 'uses', {u: invite.uses}); + if (invite.maxUses) inviteString = inviteString + '\n' + localize('invite-tracking', 'max-uses', {u: invite.maxUses}); + components.push({ + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + label: '🗑️ ' + localize('invite-tracking', 'revoke-invite'), + style: 'DANGER', + customId: `inv-rev-${invite.code}` + }] + }); + embed.addField(localize('invite-tracking', 'invite'), inviteString); + } + c.send({embeds: [embed], components}); + } +}; \ No newline at end of file diff --git a/modules/invite-tracking/events/guildMemberRemove.js b/modules/invite-tracking/events/guildMemberRemove.js new file mode 100644 index 00000000..b041c0fd --- /dev/null +++ b/modules/invite-tracking/events/guildMemberRemove.js @@ -0,0 +1,60 @@ +const {localize} = require('../../../src/functions/localize'); +const {MessageEmbed} = require('discord.js'); +const {dateToDiscordTimestamp, formatDiscordUserName} = require('../../../src/functions/helpers'); +const {stringNames} = require('./guildMemberJoin'); + +module.exports.run = async (client, member) => { + if (!client.botReadyAt) return; + if (member.guild.id !== client.guild.id) return; + + await client.models['invite-tracking']['UserInvite'].update({left: true}, { + where: { + userID: member.user.id + } + }); + + const moduleConfig = client.configurations['invite-tracking']['config']; + if (moduleConfig['logchannel-id']) { + const userInvites = await client.models['invite-tracking']['UserInvite'].findAll({ + where: { + userID: member.user.id + }, + order: [['createdAt', 'DESC']] + }); + const invite = userInvites[0]; + if (!invite) return; + const c = client.channels.cache.get(moduleConfig['logchannel-id']); + if (!c) return client.logger.error(localize('invite-tracking', 'log-channel-not-found-but-set', {c: moduleConfig['logchannel-id']})); + const embed = new MessageEmbed() + .setTitle('📤 ' + localize('invite-tracking', 'member-leave')) + .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) + .setColor('RED') + .addField(localize('invite-tracking', 'member'), `${formatDiscordUserName(member.user)} (\`${member.user.id}\`)`, true) + .addField(localize('invite-tracking', 'invite-type'), localize('invite-tracking', stringNames[invite.inviteType]), true); + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + if (invite.inviteCode) { + let guildInvite = await member.guild.invites.fetch({ code: invite.inviteCode, force: true }).catch(() => {}); + if (!guildInvite) guildInvite = {}; + + let inviteString = localize('invite-tracking', 'invite-code', {c: invite.inviteCode, u: 'https://discord.gg/' + invite.inviteCode}); + + if (guildInvite.channel) inviteString = inviteString + '\n' + localize('invite-tracking', 'invite-channel', {c: guildInvite.channel.toString()}); + if (guildInvite.createdAt) inviteString = inviteString + '\n' + localize('invite-tracking', 'created-at', {t: dateToDiscordTimestamp(guildInvite.createdAt)}); + if (guildInvite.expiresAt) inviteString = inviteString + '\n' + localize('invite-tracking', 'expires-at', {t: dateToDiscordTimestamp(guildInvite.expiresAt)}); + + if (invite.inviter) { + const inviterInvites = await client.models['invite-tracking']['UserInvite'].findAll({ + where: { + inviter: invite.inviter + } + }); + inviteString = inviteString + '\n' + localize('invite-tracking', 'inviter', {u: `<@${invite.inviter}>`, i: inviterInvites.length, a: inviterInvites.filter(i => !i.left).length}); + } + + if (guildInvite.uses) inviteString = inviteString + '\n' + localize('invite-tracking', 'uses', {u: guildInvite.uses}); + if (guildInvite.maxUses) inviteString = inviteString + '\n' + localize('invite-tracking', 'max-uses', {u: guildInvite.maxUses}); + embed.addField(localize('invite-tracking', 'invite'), inviteString); + } + c.send({embeds: [embed]}); + } +}; \ No newline at end of file diff --git a/modules/invite-tracking/events/interactionCreate.js b/modules/invite-tracking/events/interactionCreate.js new file mode 100644 index 00000000..11640ca2 --- /dev/null +++ b/modules/invite-tracking/events/interactionCreate.js @@ -0,0 +1,48 @@ +const {localize} = require('../../../src/functions/localize'); +const {formatDiscordUserName} = require('../../../src/functions/helpers'); +exports.run = async (client, interaction) => { + if (!interaction.client.botReadyAt) return; + if (!interaction.isButton()) return; + if (interaction.customId.startsWith('uinv-rev')) { + await interaction.deferReply({ephemeral: true}); + const guildInvites = await interaction.guild.invites.fetch(); + try { + for (const invite of guildInvites.filter(i => i.inviter.id === interaction.customId.replaceAll('uinv-rev-', '')).values()) { + await invite.delete(localize('invite-tracking', 'invite-revoke-audit-log', {u: formatDiscordUserName(interaction.user)})); + } + await interaction.editReply({ + content: localize('invite-tracking', 'revoked-invites-successfully') + }); + } catch (e) { + client.logger.warn(localize('invite-tracking', 'invite-revoked-error', {e})); + await interaction.editReply({ + content: '⚠️ ' + localize('invite-tracking', 'invite-revoked-error', { + e, + c + }) + }); + } + return; + } + if (!interaction.customId.startsWith('inv-rev-')) return; + if (!interaction.member.permissions.has('MANAGE_GUILD')) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('invite-tracking', 'missing-revoke-permissions') + }); + const code = interaction.customId.replaceAll('inv-rev-', ''); + const invite = await client.guild.invites.fetch(code).catch(() => {}); + if (!invite) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('invite-tracking', 'invite-not-found') + }); + await interaction.message.edit({embeds: [interaction.message.embeds[0]], components: []}); + invite.delete(localize('invite-tracking', 'invite-revoke-audit-log', {u: formatDiscordUserName(interaction.user)})).then(() => { + interaction.reply({ephemeral: true, content: localize('invite-tracking', 'invite-revoked')}); + }).catch((e) => { + client.logger.warn(localize('invite-tracking', 'invite-revoked-error', {e, c: code})); + interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('invite-tracking', 'invite-revoked-error', {e, c: code}) + }); + }); +}; \ No newline at end of file diff --git a/modules/invite-tracking/models/UserInvite.js b/modules/invite-tracking/models/UserInvite.js new file mode 100644 index 00000000..16a67d55 --- /dev/null +++ b/modules/invite-tracking/models/UserInvite.js @@ -0,0 +1,30 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class UserInvite extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + left: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + inviteCode: DataTypes.STRING, + inviteType: DataTypes.STRING, + inviter: DataTypes.STRING, + userID: DataTypes.STRING + }, { + tableName: 'invite-tracking_UserInvite', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'UserInvite', + 'module': 'invite-tracking' +}; \ No newline at end of file diff --git a/modules/invite-tracking/module.json b/modules/invite-tracking/module.json new file mode 100644 index 00000000..2e89723e --- /dev/null +++ b/modules/invite-tracking/module.json @@ -0,0 +1,26 @@ +{ + "name": "invite-tracking", + "humanReadableName": { + "en": "Invite-Tracking" + }, + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "events-dir": "/events", + "commands-dir": "/commands", + "models-dir": "/models", + "config-example-files": [ + "config.json" + ], + "on-load-event": "onLoad.js", + "tags": [ + "moderation" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/invite-tracking", + "description": { + "en": "Track who invited who", + "de": "Track, wer wen eingeladen hat" + } +} \ No newline at end of file diff --git a/modules/invite-tracking/onLoad.js b/modules/invite-tracking/onLoad.js new file mode 100644 index 00000000..1092406a --- /dev/null +++ b/modules/invite-tracking/onLoad.js @@ -0,0 +1,18 @@ +const InvitesTracker = require('@androz2091/discord-invites-tracker'); +const {localize} = require('../../src/functions/localize'); + +module.exports.onLoad = function (client) { + if (!client.inviteHook) { + const tracker = InvitesTracker.init(client, { + fetchGuilds: true, + fetchVanity: true, + fetchAuditLogs: true, + activeGuilds: [client.config.guildID] + }); + client.inviteHook = true; + localize('invite-tracking', 'hook-installed'); + tracker.on('guildMemberAdd', async (member, type, invite) => { + client.emit('guildMemberJoin', member, type, invite); + }); + } +}; \ No newline at end of file diff --git a/modules/levels/commands/leaderboard.js b/modules/levels/commands/leaderboard.js new file mode 100644 index 00000000..38758acf --- /dev/null +++ b/modules/levels/commands/leaderboard.js @@ -0,0 +1,126 @@ +const { + sendMultipleSiteButtonMessage, + truncate, + formatNumber, + formatDiscordUserName +} = require('../../../src/functions/helpers'); +const {MessageEmbed} = require('discord.js'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (interaction) { + const moduleStrings = interaction.client.configurations['levels']['strings']; + const moduleConfig = interaction.client.configurations['levels']['config']; + const sortBy = interaction.options.getString('sort-by') || moduleConfig.sortLeaderboardBy; + const users = await interaction.client.models['levels']['User'].findAll({ + order: [ + ['xp', 'DESC'] + ] + }); + if (users.length === 0) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('levels', 'no-user-on-leaderboard') + }); + const thisUser = users.find(u => u.userID === interaction.user.id); + + const sites = []; + + /** + * Adds a site + * @private + * @param {Array} fields + */ + function addSite(fields) { + const embed = new MessageEmbed() + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) + .setColor('GREEN') + .setThumbnail(interaction.guild.iconURL()) + .setTitle(moduleStrings.leaderboardEmbed.title) + .setDescription(moduleStrings.leaderboardEmbed.description) + .addField('\u200b', '\u200b') + .addFields(fields) + .addField('\u200b', '\u200b') + .addField(moduleStrings.leaderboardEmbed.your_level, moduleStrings.leaderboardEmbed.you_are_level_x_with_x_xp.split('%level%').join(thisUser['level']).split('%xp%').join(formatNumber(thisUser['xp']))); + sites.push(embed); + } + + if (sortBy === 'levels') { + const levels = {}; + const levelArray = []; + for (const user of users) { + if (!levels[user.level]) { + levels[user.level] = []; + levelArray.push(user.level); + } + levels[user.level].push(user); + } + let currentSiteFields = []; + let i = 0; + levelArray.sort(function (a, b) { + return b - a; + }); + for (const level of levelArray) { + i++; + let userString = ''; + let userCount = 0; + for (const user of levels[level]) { + const member = interaction.guild.members.cache.get(user.userID); + if (!member) continue; + userCount++; + if (userCount < 6) userString = userString + `${userCount}. ${moduleConfig['useTags'] ? formatDiscordUserName(member.user) : member.user.toString()}: ${formatNumber(user.xp)}\n`; + } + if (userCount > 5) userString = userString + localize('levels', 'and-x-other-users', {uc: userCount - 5}); + if (userCount !== 0) currentSiteFields.push({name: localize('levels', 'level', {l: level}), value: userString, inline: true}); + if (i === Object.keys(levels).length || currentSiteFields.length === 6) { + addSite(currentSiteFields); + currentSiteFields = []; + } + } + } else { + let userString = ''; + let i = 0; + for (const user of users) { + const member = interaction.guild.members.cache.get(user.userID); + if (!member) continue; + i++; + userString = userString + localize('levels', 'leaderboard-notation', { + p: i, + u: moduleConfig['useTags'] ? formatDiscordUserName(member.user) : member.user.toString(), + l: user.level, + xp: formatNumber(user.xp) + }) + '\n'; + if (i === users.filter(u => interaction.guild.members.cache.get(u.userID)).length || i % 20 === 0) { + addSite([{ + name: localize('levels', 'users'), + value: truncate(userString, 1024) + }]); + userString = ''; + } + } + } + + sendMultipleSiteButtonMessage(interaction.channel, sites, [interaction.user.id], interaction); +}; + +module.exports.config = { + name: 'leaderboard', + description: localize('levels', 'leaderboard-command-description'), + options: function (client) { + return [ + { + type: 'STRING', + name: 'sort-by', + description: localize('levels', 'leaderboard-sortby-description', {d: client.configurations['levels']['config']['sortLeaderboardBy']}), + required: false, + choices: [ + { + name: 'levels', + value: 'levels' + }, { + name: 'xp', + value: 'xp' + } + ] + } + ]; + } +}; \ No newline at end of file diff --git a/modules/levels/commands/manage-levels.js b/modules/levels/commands/manage-levels.js new file mode 100644 index 00000000..b58d69b5 --- /dev/null +++ b/modules/levels/commands/manage-levels.js @@ -0,0 +1,352 @@ +const {registerNeededEdit} = require('../leaderboardChannel'); +const {localize} = require('../../../src/functions/localize'); +const {formatDiscordUserName} = require('../../../src/functions/helpers'); + +async function runXPAction(interaction, newXP) { + const member = interaction.options.getMember('user'); + let user = await interaction.client.models['levels']['User'].findOne({ + where: { + userID: member.user.id + } + }); + if (!user) { + user = await interaction.client.models['levels']['User'].create({ + userID: member.user.id, + messages: 0, + xp: 0 + }); + } + user.xp = newXP(user.xp); + if (user.xp < 0) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('levels', 'negative-xp') + }); + + function runXPCheck() { + const nextLevelXp = user.level * 750 + ((user.level - 1) * 500); + if (nextLevelXp <= user.xp) { + user.level = user.level + 1; + if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { + if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { + for (const role of Object.values(interaction.client.configurations.levels.config.reward_roles)) { + if (member.roles.cache.has(role)) member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); + } + } + member.roles.add(interaction.client.configurations.levels.config.reward_roles[user.level.toString()]); + } + runXPCheck(); + } + } + + runXPCheck(); + + + await user.save(); + interaction.client.logger.info(localize('levels', 'manipulated', { + u: formatDiscordUserName(interaction.user), + m: formatDiscordUserName(member.user), + l: user.level, + v: user.xp + })); + if (interaction.client.logChannel) await interaction.client.logChannel.send(localize('levels', 'manipulated', { + u: formatDiscordUserName(interaction.user), + m: formatDiscordUserName(member.user), + l: user.level, + v: user.xp + })); + await interaction.reply({ + ephemeral: true, + content: localize('levels', 'successfully-changed', { + l: user.level, + u: member.user.toString(), + x: user.xp + }) + }); +} + +async function runLevelAction(interaction, newLevel) { + const member = interaction.options.getMember('user'); + const user = await interaction.client.models['levels']['User'].findOne({ + where: { + userID: member.user.id + } + }); + if (!user) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('levels', 'cheat-no-profile') + }); + user.level = newLevel(user.level); + if (user.level < 1) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('levels', 'negative-level') + }); + user.xp = (user.level - 1) * 750 + ((user.level - 2) * 500); + if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { + if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { + for (const role of Object.values(interaction.client.configurations.levels.config.reward_roles)) { + if (member.roles.cache.has(role)) member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); + } + } + member.roles.add(interaction.client.configurations.levels.config.reward_roles[user.level.toString()]); + } + + + await user.save(); + interaction.client.logger.info(localize('levels', 'manipulated', { + u: formatDiscordUserName(interaction.user), + m: formatDiscordUserName(member.user), + l: user.level, + v: user.xp + })); + if (interaction.client.logChannel) await interaction.client.logChannel.send(localize('levels', 'manipulated', { + u: formatDiscordUserName(interaction.user), + m: formatDiscordUserName(member.user), + l: user.level, + v: user.xp + })); + await interaction.reply({ + ephemeral: true, + content: localize('levels', 'successfully-changed', { + l: user.level, + u: member.user.toString(), + x: user.xp + }) + }); +} + +module.exports.subcommands = { + 'reset-xp': async function (interaction) { + const type = interaction.options.getUser('user') ? 'user' : 'server'; + if (!interaction.options.getBoolean('confirm')) return interaction.reply({ + ephemeral: 'true', + content: type === 'user' ? localize('levels', 'are-you-sure-you-want-to-delete-user-xp', { + u: interaction.options.getUser('user').toString(), + ut: formatDiscordUserName(interaction.options.getUser('user')) + }) + : localize('levels', 'are-you-sure-you-want-to-delete-server-xp') + }); + await interaction.deferReply(); + if (type === 'user') { + const user = await interaction.client.models['levels']['User'].findOne({ + where: { + userID: interaction.options.getUser('user').id + } + }); + if (!user) return interaction.editReply('⚠️ ' + localize('levels', 'user-not-found')); + interaction.client.logger.info(localize('levels', 'user-deleted-users-xp', { + t: formatDiscordUserName(interaction.user), + u: user.userID + })); + if (interaction.client.logChannel) await interaction.client.logChannel.send(localize('levels', 'user-deleted-users-xp', { + t: formatDiscordUserName(interaction.user), + u: user.userID + })); + await user.destroy(); + await interaction.editReply(localize('levels', 'removed-xp-successfully')); + } else { + const users = await interaction.client.models['levels']['User'].findAll(); + for (const user of users) await user.destroy(); + interaction.client.logger.info(localize('levels', 'deleted-server-xp', {u: formatDiscordUserName(interaction.user)})); + if (interaction.client.logChannel) await interaction.client.logChannel.send(localize('levels', 'deleted-server-xp', {u: formatDiscordUserName(interaction.user)})); + await interaction.editReply(localize('levels', 'successfully-deleted-all-xp-of-users')); + } + }, + 'edit-xp': { + 'set': async function (interaction) { + await runXPAction(interaction, () => { + return interaction.options.getNumber('value'); + }); + }, + 'add': async function (interaction) { + await runXPAction(interaction, (u) => { + return u + interaction.options.getNumber('value'); + }); + }, + 'remove': async function (interaction) { + await runXPAction(interaction, (u) => { + return u - interaction.options.getNumber('value'); + }); + } + }, + 'edit-level': { + 'set': async function (interaction) { + await runLevelAction(interaction, () => { + return interaction.options.getNumber('value'); + }); + }, + 'add': async function (interaction) { + await runLevelAction(interaction, (u) => { + return u + interaction.options.getNumber('value'); + }); + }, + 'remove': async function (interaction) { + await runLevelAction(interaction, (u) => { + return u - interaction.options.getNumber('value'); + }); + } + } +}; + +module.exports.run = function () { + registerNeededEdit(); +}; + +module.exports.config = { + name: 'manage-levels', + defaultMemberPermissions: ['MODERATE_MEMBERS'], + description: localize('levels', 'edit-xp-command-description'), + + options: function (client) { + const array = [{ + type: 'SUB_COMMAND', + name: 'reset-xp', + description: localize('levels', 'reset-xp-description'), + options: [ + { + type: 'USER', + required: false, + name: 'user', + description: localize('levels', 'reset-xp-user-description') + }, + { + type: 'BOOLEAN', + required: false, + name: 'confirm', + description: localize('levels', 'reset-xp-confirm-description') + } + ] + }]; + if (client.configurations['levels']['config']['allowCheats']) { + + array.push({ + type: 'SUB_COMMAND_GROUP', + name: 'edit-xp', + description: localize('levels', 'edit-xp-description'), + options: [ + { + type: 'SUB_COMMAND', + name: 'add', + description: localize('levels', 'edit-xp-description'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('levels', 'edit-xp-user-description') + }, + { + type: 'NUMBER', + required: true, + name: 'value', + description: localize('levels', 'edit-xp-value-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'remove', + description: localize('levels', 'edit-xp-description'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('levels', 'edit-xp-user-description') + }, + { + type: 'NUMBER', + required: true, + name: 'value', + description: localize('levels', 'edit-xp-value-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'set', + description: localize('levels', 'edit-xp-description'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('levels', 'edit-xp-user-description') + }, + { + type: 'NUMBER', + required: true, + name: 'value', + description: localize('levels', 'edit-xp-value-description') + } + ] + } + ] + }); + array.push({ + type: 'SUB_COMMAND_GROUP', + name: 'edit-level', + description: localize('levels', 'edit-level-description'), + options: [ + { + type: 'SUB_COMMAND', + name: 'add', + description: localize('levels', 'edit-xp-description'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('levels', 'edit-xp-user-description') + }, + { + type: 'NUMBER', + required: true, + name: 'value', + description: localize('levels', 'edit-xp-value-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'remove', + description: localize('levels', 'edit-xp-description'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('levels', 'edit-xp-user-description') + }, + { + type: 'NUMBER', + required: true, + name: 'value', + description: localize('levels', 'edit-xp-value-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'set', + description: localize('levels', 'edit-xp-description'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('levels', 'edit-xp-user-description') + }, + { + type: 'NUMBER', + required: true, + name: 'value', + description: localize('levels', 'edit-xp-value-description') + } + ] + } + ] + }); + } + return array; + } +}; \ No newline at end of file diff --git a/modules/levels/commands/profile.js b/modules/levels/commands/profile.js new file mode 100644 index 00000000..066705b7 --- /dev/null +++ b/modules/levels/commands/profile.js @@ -0,0 +1,55 @@ +const {embedType, formatDate, formatNumber} = require('../../../src/functions/helpers'); +const {MessageEmbed} = require('discord.js'); +const {localize} = require('../../../src/functions/localize'); +const {getMemberRoleFactor} = require('../events/messageCreate'); + +module.exports.run = async function (interaction) { + const moduleStrings = interaction.client.configurations['levels']['strings']; + const moduleConfig = interaction.client.configurations['levels']['config']; + + let member = interaction.member; + if (interaction.options.getUser('user')) member = await interaction.guild.members.fetch(interaction.options.getUser('user').id); + + const user = await interaction.client.models['levels']['User'].findOne({ + where: { + userID: member.user.id + } + }); + if (!user) return interaction.reply(embedType(moduleStrings['user_not_found'], {}, {ephemeral: true})); + + const nextLevelXp = user.level * 750 + ((user.level - 1) * 500); + + const embed = new MessageEmbed() + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) + .setColor(moduleStrings.embed.color || 'GREEN') + .setThumbnail(member.user.avatarURL({dynamic: true})) + .setTitle(moduleStrings.embed.title.replaceAll('%username%', member.user.username)) + .setDescription(moduleStrings.embed.description.replaceAll('%username%', member.user.username)) + .addField(moduleStrings.embed.messages, formatNumber(user.messages), true) + .addField(moduleStrings.embed.xp, `${formatNumber(user.xp)}/${formatNumber(nextLevelXp)}`, true) + .addField(moduleStrings.embed.level, user.level.toString(), true); + + const roleFactor = getMemberRoleFactor(interaction.member); + if (roleFactor !== 1) { + let roleString = ''; + for (const role of interaction.member.roles.cache.filter(f => moduleConfig['multiplication_roles'][f.id]).values()) { + roleString = roleString + `\n* <@&${role.id}>: ${moduleConfig['multiplication_roles'][role.id]}x`; + } + embed.addField(moduleStrings.embed.roleFactor, `${roleString}\n${localize('levels', 'role-factors-total', {f: roleFactor})}`, true); + } + embed.addField(moduleStrings.embed.joinedAt, formatDate(member.joinedAt), true); + interaction.reply({ephemeral: true, embeds: [embed]}); +}; + +module.exports.config = { + name: 'profile', + description: localize('levels', 'profile-command-description'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('levels', 'profile-user-description'), + required: false + } + ] +}; \ No newline at end of file diff --git a/modules/levels/configs/config.json b/modules/levels/configs/config.json new file mode 100644 index 00000000..fc27c43f --- /dev/null +++ b/modules/levels/configs/config.json @@ -0,0 +1,282 @@ +{ + "description": { + "en": "Configure the function of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "commandsWarnings": { + "normal": [ + "/manage-levels" + ] + }, + "content": [ + { + "name": "min-xp", + "humanName": { + "en": "XP given at least", + "de": "Mindestens gegebenes XP" + }, + "default": { + "en": 25, + "de": 25 + }, + "description": { + "en": "How much XP the user gets at least", + "de": "So viel XP bekommt ein Benutzer mindestens" + }, + "type": "integer" + }, + { + "name": "max-xp", + "humanName": { + "en": "XP given at most", + "de": "Maximal gegebenes XP" + }, + "default": { + "en": 65, + "de": 65 + }, + "description": { + "en": "How much XP the user gets at most", + "de": "So viel XP bekommt ein Benutzer maximal" + }, + "type": "integer" + }, + { + "name": "cooldown", + "humanName": { + "en": "Cooldown" + }, + "default": { + "en": 1500, + "de": 1500 + }, + "description": { + "en": "In ms. How much cooldown there is between each XP getting", + "de": "In Millisekunden! So viel Zeit muss ein Nutzer zwischen jeder Nachricht mit XP warten" + }, + "type": "integer" + }, + { + "name": "level_up_channel_id", + "humanName": { + "en": "Level-Up-Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel in which Level-Up-Messages should get send. (Leave empty to disable)", + "de": "Channel, in den die Level-Up-Nachrichten gesendet werden. (Leerlassen, um sie einfach in den aktuellen Channel zu schicken))" + }, + "type": "channelID", + "allowNull": true + }, + { + "name": "sortLeaderboardBy", + "humanName": { + "en": "Leaderboard-Sort-Category", + "de": "Ranglisten-Sortierung" + }, + "default": { + "en": "levels", + "de": "levels" + }, + "description": { + "en": "How the leaderboard should be sorted", + "de": "Wähle aus, wie der /leaderboard-command aussehen werden soll" + }, + "type": "select", + "content": [ + "levels", + "xp" + ] + }, + { + "name": "blacklisted_channels", + "humanName": { + "en": "Blacklisted Channels", + "de": "Channel ohne XP" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Blacklisted-Channels in which users can not earn XP", + "de": "Channel, in denen kein XP gesammelt werden kann" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "blacklistedRoles", + "humanName": { + "en": "Blacklisted roles", + "de": "Rollen, die kein XP sammeln" + }, + "type": "array", + "content": "roleID", + "default": { + "en": [] + }, + "description": { + "de": "Diese Rollen werden kein XP für ihre Nachrichten erhalten", + "en": "These roles won't receive XP when writing messages" + } + }, + { + "name": "reward_roles", + "humanName": { + "en": "Level Reward roles", + "de": "Level-Belohnung-Rollen" + }, + "default": { + "en": {}, + "de": {} + }, + "description": { + "en": "Level, bei denen der Nutzer eine Rolle bekommt. Parameter 1: Level, Parameter 2: Rollen-ID", + "de": "Level at which users should get roles" + }, + "type": "keyed", + "content": { + "key": "integer", + "value": "roleID" + } + }, + { + "name": "multiplication_roles", + "humanName": { + "en": "XP Multiplication Roles", + "de": "XP-Multiplikator Rollen" + }, + "default": { + "en": {}, + "de": {} + }, + "description": { + "en": "Allows you to configure roles that have a higher multiplication factor than normal (default value is 1). If a user has more than one of the configured roles, the multiplication factors get multiplied together before multiplying the result with the amount of XP the user receives for their message.", + "de": "Erlaubt es dir, den Multiplikationsfaktor von bestimmten Rollen anzupassen. Standardmäßig haben Rollen einen Wert von 1. Bevor der XP Wert für eine Nachricht an den Nutzer gegeben wird, werden alle Faktoren von Rollen miteinander multipliziert und das Ergebnis dann mal den XP-Wert genommen." + }, + "type": "keyed", + "content": { + "key": "roleID", + "value": "float" + } + }, + { + "name": "onlyTopLevelRole", + "humanName": { + "en": "Only keep highest Level-Role", + "de": "Nur die höchste Level-Rolle behalten" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, all previous level roles a user had will get removed, when they advance to a new level.", + "de": "Wenn aktiviert, werden alle vorherigen Level-Rollen, die ein Nutzer hatte, entfernt, wenn dieser ein neues Level erreicht." + }, + "type": "boolean" + }, + { + "name": "reset-on-leave", + "humanName": { + "en": "Rest Level on leave", + "de": "Level beim Verlassen zurücksetzen" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, all levels and the XP of a user will be deleted, when they leave your server.", + "de": "Wenn aktiviert, werden alle Level und das XP eines Nutzers gelöscht, wenn er den Server verlässt." + }, + "type": "boolean" + }, + { + "name": "randomMessages", + "humanName": { + "en": "Random messages", + "de": "Zufällige Nachrichten" + }, + "default": { + "en": false + }, + "description": { + "en": "Wenn aktiviert wird das Modul die Level-Up-Nachricht zufällig auswählen und nicht die in Nachrichten angegebene verwenden", + "de": "If enabled the module will randomly select a messages from random-levelup-messages and ignore the one set in strings" + }, + "type": "boolean" + }, + { + "name": "leaderboard-channel", + "humanName": { + "en": "Leaderboard-Channel", + "de": "Ranglisten-Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "Wenn gesetzt wird der Bot in diesen Channel eine Nachricht senden, welche die aktuellen Level der Nutzern enthält", + "de": "If set, the bot will send a messages in this channel with the current leaderboard and edit it every five minutes" + }, + "type": "channelID", + "content": [ + "GUILD_TEXT" + ], + "allowNull": true + }, + { + "name": "useTags", + "humanName": { + "en": "Use User's Tags instead of their Mention in the Leaderboard-Channel-Embed", + "de": "Nutze den Tag der Nutzer, anstatt eine Erwähnung im Ranglisten-Channel-Embed" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, the bot will use the tag of users in the Leaderboard-Channel-Embed instead of their mention.", + "de": "Wenn aktiviert, wird im Ranglisten-Channel-Embed der Tag des Nutzers angezeigt und nicht eine Erwähnung (bei großen Servern empfohlen)" + }, + "type": "boolean" + }, + { + "name": "allowCheats", + "humanName": { + "en": "Cheats" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled admins can change the XP of other users (not recommended (please leave it of if you want to have a fair levelsystem!!!))", + "de": "Wenn aktiviert können Administratoren die XP von anderen Nutzern editieren (nicht empfohlen, wenn du einen coolen, fairen Server haben willst (wirklich nicht!!!)))" + }, + "type": "boolean" + }, + { + "name": "disableSCNetworkProfile", + "humanName": { + "en": "Disable SC Network Profiles", + "de": "Deaktiviert SC Network Profile" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled admins can change the XP of other users (not recommended (please leave it of if you want to have a fair levelsystem!!!))", + "de": "Wenn aktiviert können Administratoren die XP von anderen Nutzern editieren (nicht empfohlen, wenn du einen coolen, fairen Server haben willst (wirklich nicht!!!)))" + }, + "type": "boolean", + "pro": true + } + ] +} \ No newline at end of file diff --git a/modules/levels/configs/random-levelup-messages.json b/modules/levels/configs/random-levelup-messages.json new file mode 100644 index 00000000..4f632475 --- /dev/null +++ b/modules/levels/configs/random-levelup-messages.json @@ -0,0 +1,94 @@ +{ + "description": { + "en": "If enabled, the bot will randomly select a message from here", + "de": "Wenn aktiviert, wird der Bot zufällige eine Nachricht von hier auswählen" + }, + "humanName": { + "en": "Random-Level-Up-Messages", + "de": "Zufällige Level-Up-Nachrichten" + }, + "filename": "random-levelup-messages.json", + "configElements": true, + "content": [ + { + "name": "type", + "humanName": { + "de": "Nachrichtentyp" + }, + "default": { + "en": "normal", + "de": "normal" + }, + "description": { + "en": "Type of this message", + "de": "Typ dieser Nachricht" + }, + "type": "select", + "content": [ + "normal", + "with-reward" + ] + }, + { + "name": "message", + "humanName": { + "de": "Nachrichten" + }, + "allowGeneratedImage": true, + "default": { + "en": "" + }, + "description": { + "en": "Messages which should be send", + "de": "Nachrichten, die gesendet werden sollen" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "mention", + "description": { + "en": "Mention of the user", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "avatarURL", + "isImage": true, + "description": { + "en": "Avatar of the user", + "de": "Profilbild des Nutzers" + } + }, + { + "name": "username", + "description": { + "en": "Username of the user", + "de": "Nutzername des Nutzers" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "newLevel", + "description": { + "en": "New level of the user", + "de": "Neues Level des Nutzers" + } + }, + { + "name": "role", + "description": { + "en": "Mention of the role (No ping, only if type = with-reward)", + "de": "Erwähnung der Rolle (Kein \"Ping\", nur, wenn Nachrichtentyp = with-reward)" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/levels/configs/special-levelup-messages.json b/modules/levels/configs/special-levelup-messages.json new file mode 100644 index 00000000..0808788f --- /dev/null +++ b/modules/levels/configs/special-levelup-messages.json @@ -0,0 +1,89 @@ +{ + "description": { + "en": "If enabled, the bot will randomly select a message from here", + "de": "Wenn aktiviert, wird der Bot zufällige eine Nachricht von hier auswählen" + }, + "humanName": { + "en": "Selected messages", + "de": "Bestimmte Nachrichten" + }, + "filename": "special-levelup-messages.json", + "configElements": true, + "content": [ + { + "name": "level", + "humanName": { + "de": "Level" + }, + "default": { + "en": "" + }, + "description": { + "en": "Level at which this messages should get send", + "de": "Level, bei welchem diese Nachricht gesendet werden soll" + }, + "type": "integer" + }, + { + "name": "message", + "allowGeneratedImage": true, + "humanName": { + "de": "Nachricht" + }, + "default": { + "en": "" + }, + "description": { + "en": "Messages which should be send", + "de": "Nachricht, die gesendet werden soll" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "mention", + "description": { + "en": "Mention of the user", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "avatarURL", + "isImage": true, + "description": { + "en": "Avatar of the user", + "de": "Profilbild des Nutzers" + } + }, + { + "name": "username", + "description": { + "en": "Username of the user", + "de": "Nutzername des Nutzers" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "newLevel", + "description": { + "en": "New level of the user", + "de": "Neues Level des Nutzers" + } + }, + { + "name": "role", + "description": { + "en": "Mention of the role (No ping, only if level has reward)", + "de": "Erwähnung der Rolle (Kein \"Ping\", nur, wenn das Level eine Belohnung hat)" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/levels/configs/strings.json b/modules/levels/configs/strings.json new file mode 100644 index 00000000..2cfcd899 --- /dev/null +++ b/modules/levels/configs/strings.json @@ -0,0 +1,301 @@ +{ + "description": { + "en": "Edit the messages and strings of the module here", + "de": "Stelle hier die Nachrichten des Modules ein" + }, + "humanName": { + "en": "Messages", + "de": "Nachrichten" + }, + "filename": "strings.json", + "content": [ + { + "name": "user_not_found", + "humanName": { + "en": "User not found", + "de": "Nutzer nicht gefunden" + }, + "default": { + "en": "⚠️ We do not have any records of this user", + "de": "⚠️ Dieser Nutzer hat anscheinend noch keine Nachricht verschickt" + }, + "description": { + "en": "This messages gets send if someone checks a profile of a user when the user never send a message", + "de": "Diese Nachricht wird verschickt, wenn ein eine Person sein Profil sehen will, aber noch kein XP hat" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "embed", + "humanName": { + "de": "Profilembed" + }, + "default": { + "en": { + "title": "%username%'s Profile", + "description": "You can find %username%'s profile here.", + "messages": "Message-Count", + "xp": "XP", + "level": "Level", + "joinedAt": "Joined server", + "roleFactor": "Role Factor(s)", + "color": "GREEN" + }, + "de": { + "title": "%username%'s Profil", + "description": "Das Profil von %username% findest du hier.", + "messages": "Nachrichten-Anzahl", + "roleFactor": "Rollen-Faktor(en)", + "xp": "XP", + "level": "Level", + "joinedAt": "Server beigetreten", + "color": "GREEN" + } + }, + "description": { + "en": "Embed which gets send if !profile gets executed", + "de": "Embed das gesendet wird, wenn /profile ausgeführt wird" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + }, + { + "name": "leaderboardEmbed", + "humanName": { + "de": "Ranglisten-Embed" + }, + "default": { + "en": { + "title": "Leaderboard", + "description": "You can find the level of every user here", + "and_x_more_people": "And %count% other members", + "more_level": "More Levels", + "x_levels_are_not_shown": "And **%count% Level** are not being displayed", + "your_level": "Your Level", + "you_are_level_x_with_x_xp": "You are currently on **Level %level%** with **%xp% XP**. See more with `/profile`.", + "joinedAt": "Joined server", + "color": "GREEN" + }, + "de": { + "title": "Rangliste", + "description": "Hier findest du das Level von jedem Nutzer", + "and_x_more_people": "Und %count% andere Mitglieder", + "more_level": "Mehr Level", + "x_levels_are_not_shown": "Und **%count% Level** werden nicht angezeigt", + "your_level": "Dein Level", + "you_are_level_x_with_x_xp": "Du bist aktuell auf **Level %level%** mit **%xp% XP**. Siehe mehr mit `/profile`.", + "joinedAt": "Server beigetreten", + "color": "GREEN" + } + }, + "description": { + "en": "This embed gets send if !leaderboard (!lb) gets executed", + "de": "Dieses Embed wird gesendet, wenn /leaderboard (/lb) ausgeführt wird" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + }, + { + "name": "level_up_message", + "allowGeneratedImage": true, + "humanName": { + "de": "Level-Up-Nachricht" + }, + "default": { + "en": "Level Up! Your new level is **%newLevel%**!", + "de": "Level Up! Dein neues Level ist jetzt %newLevel%" + }, + "description": { + "en": "This messages gets send if a user levels up (gets overwritten if randomMessages is enabled)", + "de": "Diese Nachricht wird gesendet, wenn ein Nutzer ein Level aufsteigt (Wird überschrieben, wenn \"Zufällige Nachrichten\" aktiviert ist)" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "mention", + "description": { + "en": "Mention of the user", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "avatarURL", + "isImage": true, + "description": { + "en": "Avatar of the user", + "de": "Profilbild des Nutzers" + } + }, + { + "name": "username", + "description": { + "en": "Username of the user", + "de": "Nutzername des Nutzers" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "newLevel", + "description": { + "en": "New level of the user", + "de": "Neues Level des Nutzers" + } + } + ] + }, + { + "name": "level_up_message_with_reward", + "allowGeneratedImage": true, + "humanName": { + "de": "Level-Up-Nachricht mit Belohnung" + }, + "default": { + "en": "Level Up! Your new level is **%newLevel%**! You received %role%.", + "de": "Level Up! Dein neues Level ist **%newLevel%**! Du erhältst %role%." + }, + "description": { + "en": "This messages gets send if a user levels up and gets a role (gets overwritten if randomMessages is enabled)", + "de": "Diese Nachricht wird gesendet, wenn ein Nutzer ein Level aufsteigt und eine Rolle erhält (Wird überschrieben, wenn \"Zufällige Nachrichten\" aktiviert ist)" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "mention", + "description": { + "en": "Mention of the user", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "avatarURL", + "isImage": true, + "description": { + "en": "Avatar of the user", + "de": "Profilbild des Nutzers" + } + }, + { + "name": "username", + "description": { + "en": "Username of the user", + "de": "Nutzername des Nutzers" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "newLevel", + "description": { + "en": "New level of the user", + "de": "Neues Level des Nutzers" + } + }, + { + "name": "role", + "description": { + "en": "Mention of the role (No ping)", + "de": "Erwähnung der Rolle (Ohne \"Ping\")" + } + } + ] + }, + { + "name": "liveLeaderBoardEmbed", + "humanName": { + "de": "Echtzeit-Rangliste" + }, + "default": { + "en": { + "title": "Live Leaderboard", + "description": "Find all the users levels here. Updated every five minutes.", + "color": "GREEN", + "button": "👤 Show my level" + }, + "de": { + "title": "Echtzeit-Rangliste", + "description": "Hier findest du das Level von jedem Nutzer. Diese Liste wird alle fünf Minuten aktualisiert.", + "color": "GREEN", + "button": "👤 Mein Level anzeigen" + } + }, + "description": { + "en": "Embed which gets send to the leaderboard-channel and gets updated", + "de": "Embed, welches in den Ranglisten-Kanal gesendet und danach geupdated wird" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + }, + { + "name": "leaderboard-button-answer", + "humanName": { + "de": "Nachricht bei Klick auf den Knopf unter dem Live-Leaderboard" + }, + "default": { + "en": "Hi, %name%, you are currently on **level %level%** with **%userXP%**/%nextLevelXP% **XP**. Learn more with `/profile`.", + "de": "Hi, %name%, du bist aktuell auf **Level %level%** mit **%userXP%**/%nextLevelXP% **Erfahrungspunkten**. Erfahre mehr mit `/profile`." + }, + "description": { + "en": "This messages gets send if a user clicks on the button below the live-leaderboard", + "de": "Diese Nachricht wird gesendet, wenn ein Nutzer den Knopf unter dem Live-Embed drückt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "name", + "description": { + "en": "Username of the user", + "de": "Username des Nutzers" + } + }, + { + "name": "level", + "description": { + "en": "Level of the user", + "de": "Level des Nutzers" + } + }, + { + "name": "userXP", + "description": { + "en": "XP of the user", + "de": "XP des Nutzers" + } + }, + { + "name": "nextLevelXP", + "description": { + "en": "XP of the next level", + "de": "Benötigtes XP zum nächsten Level" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/levels/events/botReady.js b/modules/levels/events/botReady.js new file mode 100644 index 00000000..dc80fc2f --- /dev/null +++ b/modules/levels/events/botReady.js @@ -0,0 +1,10 @@ +const {updateLeaderBoard} = require('../leaderboardChannel'); + +module.exports.run = async function (client) { + if (!client.configurations['levels']['config']['leaderboard-channel']) return; + await updateLeaderBoard(client, true); + const interval = setInterval(() => { + updateLeaderBoard(client); + }, 300042); + client.intervals.push(interval); +}; \ No newline at end of file diff --git a/modules/levels/events/guildMemberRemove.js b/modules/levels/events/guildMemberRemove.js new file mode 100644 index 00000000..36876d4e --- /dev/null +++ b/modules/levels/events/guildMemberRemove.js @@ -0,0 +1,13 @@ +const {updateLeaderBoard} = require('../leaderboardChannel'); + +module.exports.run = async function (client, member) { + if (!client.configurations['levels']['config']['reset-on-leave']) return; + const user = await client.models['levels']['User'].findOne({ + where: { + userID: member.user.id + } + }); + if (!user) return; + await user.destroy(); + await updateLeaderBoard(client); +}; \ No newline at end of file diff --git a/modules/levels/events/interactionCreate.js b/modules/levels/events/interactionCreate.js new file mode 100644 index 00000000..f9242494 --- /dev/null +++ b/modules/levels/events/interactionCreate.js @@ -0,0 +1,24 @@ +const {localize} = require('../../../src/functions/localize'); +const {embedType, formatNumber} = require('../../../src/functions/helpers'); + +module.exports.run = async function (client, interaction) { + if (!interaction.client.botReadyAt) return; + if (!interaction.isButton()) return; + if (interaction.customId !== 'show-level-on-liveleaderboard-click') return; + const user = await interaction.client.models['levels']['User'].findOne({ + where: { + userID: interaction.user.id + } + }); + if (!user) return interaction.reply({ + ephemeral: true, + content: localize('levels', 'please-send-a-message') + }); + const nextLevelXp = user.level * 750 + ((user.level - 1) * 500); + interaction.reply(embedType(client.configurations['levels']['strings']['leaderboard-button-answer'], { + '%name%': interaction.user.username, + '%level%': user.level, + '%userXP%': formatNumber(user.xp), + '%nextLevelXP%': formatNumber(nextLevelXp) + }, {ephemeral: true})); +}; \ No newline at end of file diff --git a/modules/levels/events/messageCreate.js b/modules/levels/events/messageCreate.js new file mode 100644 index 00000000..c9cd2803 --- /dev/null +++ b/modules/levels/events/messageCreate.js @@ -0,0 +1,108 @@ +const { + embedType, + randomIntFromInterval, + randomElementFromArray, + embedTypeV2, formatDiscordUserName +} = require('../../../src/functions/helpers'); +const {registerNeededEdit} = require('../leaderboardChannel'); +const {localize} = require('../../../src/functions/localize'); +const cooldown = new Set(); +let currentlyLevelingUp = []; + +function getMemberRoleFactor(member) { + let roleFactor = 1; + for (const role of member.roles.cache.filter(f => member.client.configurations['levels']['config']['multiplication_roles'][f.id]).values()) { + roleFactor = roleFactor * parseFloat(member.client.configurations['levels']['config']['multiplication_roles'][role.id]); + } + return roleFactor; +} + +module.exports.getMemberRoleFactor = getMemberRoleFactor; + +module.exports.run = async (client, msg) => { + if (!client.botReadyAt) return; + if (msg.author.bot || msg.system) return; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + if (cooldown.has(msg.author.id)) return; + + const moduleConfig = client.configurations['levels']['config']; + const moduleStrings = client.configurations['levels']['strings']; + + if (msg.content.includes(client.config.prefix)) return; + if (moduleConfig.blacklisted_channels.includes(msg.channel.id) || moduleConfig.blacklisted_channels.includes(msg.channel.parentId)) return; + if (msg.member.roles.cache.filter(r => moduleConfig.blacklistedRoles.includes(r.id)).size !== 0) return; + let xp = randomIntFromInterval(moduleConfig['min-xp'], moduleConfig['max-xp']); + let user = await client.models['levels']['User'].findOne({ + where: { + userID: msg.author.id + } + }); + if (!user) { + user = await client.models['levels']['User'].create({ + userID: msg.author.id, + messages: 0, + xp: 0 + }); + } + user.messages = user.messages + 1; + const nextLevelXp = user.level * 750 + ((user.level - 1) * 500); + + xp = xp * getMemberRoleFactor(msg.member); + user.xp = user.xp + xp; + + if (nextLevelXp <= user.xp && !currentlyLevelingUp.includes(msg.author.id)) { + currentlyLevelingUp.push(msg.author.id); + user.level = user.level + 1; + const channel = client.channels.cache.find(c => c.id === moduleConfig.level_up_channel_id); + + const specialMessage = client.configurations['levels']['special-levelup-messages'].find(m => m.level === user.level); + const isRewardMessage = !!moduleConfig.reward_roles[user.level.toString()]; + const randomMessages = client.configurations['levels']['random-levelup-messages'].filter(m => m.type === (isRewardMessage ? 'with-reward' : 'normal')); + + let messageToSend = moduleStrings.level_up_message; + if (isRewardMessage) messageToSend = moduleStrings.level_up_message_with_reward; + + if (moduleConfig.randomMessages) { + if (moduleConfig.randomMessages.length === 0) client.warn('[levels] ' + localize('levels', 'random-messages-enabled-but-non-configured')); + else if (randomMessages.length !== 0) messageToSend = randomElementFromArray(randomMessages).message; + } + + if (isRewardMessage) { + if (moduleConfig.onlyTopLevelRole) { + for (const role of Object.values(moduleConfig.reward_roles)) { + if (msg.member.roles.cache.has(role)) await msg.member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); + } + } + await msg.member.roles.add(moduleConfig.reward_roles[user.level.toString()], '[levels]' + localize('levels', 'granted-rewards-audit-log')).catch(); + } + if (specialMessage) messageToSend = specialMessage.message; + + await sendLevelUpMessage(await embedTypeV2(messageToSend, { + '%mention%': `<@${msg.author.id}>`, + '%avatarURL%': msg.author.avatarURL() || msg.author.defaultAvatarURL, + '%username%': msg.author.username, + '%newLevel%': user.level, + '%role%': isRewardMessage ? `<@&${moduleConfig.reward_roles[user.level.toString()]}>` : localize('levels', 'no-role'), + '%tag%': formatDiscordUserName(msg.author) + }, {allowedMentions: {parse: ['users']}})); + currentlyLevelingUp = currentlyLevelingUp.filter(f => f !== msg.author.id); + + /** + * Sends the level up messages + * @private + * @param {Object} content Content of the message + */ + async function sendLevelUpMessage(content) { + if (channel) await channel.send(content); + else await msg.reply(content); + } + } + + cooldown.add(msg.author.id); + registerNeededEdit(); + setTimeout(() => { + cooldown.delete(msg.author.id); + }, moduleConfig.cooldown); + await user.save(); +}; \ No newline at end of file diff --git a/modules/levels/leaderboardChannel.js b/modules/levels/leaderboardChannel.js new file mode 100644 index 00000000..8730dff7 --- /dev/null +++ b/modules/levels/leaderboardChannel.js @@ -0,0 +1,77 @@ +/** + * Manages the live-leaderboard + * @module Levels-Leaderboard + * @author Simon Csaba + */ +const {MessageEmbed} = require('discord.js'); +const {localize} = require('../../src/functions/localize'); +const {formatDiscordUserName} = require('../../src/functions/helpers'); +let changed = false; + +/** + * Updates the leaderboard in the leaderboard channel + * @param {Client} client Client + * @param {Boolean} force If enabled the embed will update even if there was no registered change + * @returns {Promise} + */ +module.exports.updateLeaderBoard = async function (client, force = false) { + if (!client.configurations['levels']['config']['leaderboard-channel']) return; + if (!force && !changed) return; + const moduleStrings = client.configurations['levels']['strings']; + const channel = await client.channels.fetch(client.configurations['levels']['config']['leaderboard-channel']).catch(() => { + }); + if (!channel || channel.type !== 'GUILD_TEXT') return client.logger.error('[levels] ' + localize('levels', 'leaderboard-channel-not-found')); + const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id && !msg.system); + + const users = await client.models['levels']['User'].findAll({ + order: [ + ['xp', 'DESC'] + ], + limit: 15 + }); + + let leaderboardString = ''; + let i = 0; + for (const user of users) { + const member = channel.guild.members.cache.get(user.userID); + if (!member) continue; + i++; + leaderboardString = leaderboardString + localize('levels', 'leaderboard-notation', { + p: i, + u: client.configurations['levels']['config']['useTags'] ? formatDiscordUserName(member.user) : member.user.toString(), + l: user.level, + xp: user.xp + }) + '\n'; + } + if (leaderboardString.length === 0) leaderboardString = localize('levels', 'no-user-on-leaderboard'); + + const embed = new MessageEmbed() + .setTitle(moduleStrings.liveLeaderBoardEmbed.title) + .setDescription(moduleStrings.liveLeaderBoardEmbed.description) + .setColor(moduleStrings.liveLeaderBoardEmbed.color) + .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) + .setThumbnail(channel.guild.iconURL()) + .addField(localize('levels', 'leaderboard'), leaderboardString); + + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + + const components = [{ + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + label: moduleStrings.liveLeaderBoardEmbed.button, + style: 'SUCCESS', + customId: 'show-level-on-liveleaderboard-click' + }] + }]; + + if (messages.first()) await messages.first().edit({embeds: [embed], components}); + else await channel.send({embeds: [embed], components}); +}; + +/** + * Register if a change in the leaderboard occurred + */ +module.exports.registerNeededEdit = function () { + if (!changed) changed = true; +}; \ No newline at end of file diff --git a/modules/levels/models/User.js b/modules/levels/models/User.js new file mode 100644 index 00000000..324e7218 --- /dev/null +++ b/modules/levels/models/User.js @@ -0,0 +1,31 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class LevelsUser extends Model { + static init(sequelize) { + return super.init({ + userID: { + type: DataTypes.STRING, + primaryKey: true + }, + xp: { + type: DataTypes.INTEGER + }, + messages: { + type: DataTypes.INTEGER + }, + level: { + type: DataTypes.INTEGER, + defaultValue: 1 + } + }, { + tableName: 'levels_users', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'User', + 'module': 'levels' +}; \ No newline at end of file diff --git a/modules/levels/module.json b/modules/levels/module.json new file mode 100644 index 00000000..4fbb00c6 --- /dev/null +++ b/modules/levels/module.json @@ -0,0 +1,28 @@ +{ + "name": "levels", + "humanReadableName": { + "en": "Level-System" + }, + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/levels", + "commands-dir": "/commands", + "events-dir": "/events", + "models-dir": "/models", + "config-example-files": [ + "configs/config.json", + "configs/strings.json", + "configs/random-levelup-messages.json", + "configs/special-levelup-messages.json" + ], + "tags": [ + "community" + ], + "description": { + "en": "Easy to use levelsystem with a lot of customization!", + "de": "Einfaches Level-System mit vielen Anpassungsmöglichkeiten!" + } +} \ No newline at end of file diff --git a/modules/massrole/commands/massrole.js b/modules/massrole/commands/massrole.js new file mode 100644 index 00000000..7aca5215 --- /dev/null +++ b/modules/massrole/commands/massrole.js @@ -0,0 +1,309 @@ +const {localize} = require('../../../src/functions/localize'); +const {embedType} = require('../../../src/functions/helpers'); +let target; +let failed; + +module.exports.beforeSubcommand = async function (interaction) { + if (interaction.member.roles.cache.filter(m => interaction.client.configurations['massrole']['config'].adminRoles.includes(m.id)).size === 0) { + return interaction.reply({ephemeral: true, content: localize('massrole', 'not-admin')}); + } +}; + +module.exports.subcommands = { + 'add': async function (interaction) { + if (interaction.replied) return; + const moduleStrings = interaction.client.configurations['massrole']['strings']; + checkTarget(interaction); + if (target === 'all') { + await interaction.deferReply({ephemeral: true}); + for (const member of interaction.guild.members.cache.values()) { + try { + await member.roles.add(interaction.options.getRole('role'), localize('massrole', 'add-reason', {u: interaction.user.tag})); + } catch (e) { + failed++; + } + } + if (failed === 0) { + await interaction.editReply(embedType(moduleStrings.done, {})); + } else { + await interaction.editReply(embedType(moduleStrings.notDone, {})); + failed = 0; + } + } else if (target === 'bots') { + await interaction.deferReply({ ephemeral: true }); + for (const member of interaction.guild.members.cache.values()) { + if (member.user.bot) { + try { + await member.roles.add(interaction.options.getRole('role'), localize('massrole', 'add-reason', {u: interaction.user.tag})); + } catch (e) { + failed++; + } + } + } + if (failed === 0) { + await interaction.editReply(embedType(moduleStrings.done, {})); + } else { + await interaction.editReply(embedType(moduleStrings.notDone, {})); + failed = 0; + } + } else if (target === 'humans') { + await interaction.deferReply({ephemeral: true}); + for (const member of interaction.guild.members.cache.values()) { + if (member.manageable) { + if (!member.user.bot) { + try { + + await member.roles.add(interaction.options.getRole('role'), localize('massrole', 'add-reason', {u: interaction.user.tag})); + } catch (e) { + failed++; + } + } + } + } + if (failed === 0) { + await interaction.editReply(embedType(moduleStrings.done, {})); + } else { + await interaction.editReply(embedType(moduleStrings.notDone, {})); + failed = 0; + } + } + }, + 'remove': async function (interaction) { + if (interaction.replied) return; + const moduleStrings = interaction.client.configurations['massrole']['strings']; + checkTarget(interaction); + if (target === 'all') { + await interaction.deferReply({ ephemeral: true }); + for (const member of interaction.guild.members.cache.values()) { + try { + await member.roles.remove(interaction.options.getRole('role'), localize('massrole', 'remove-reason', {u: interaction.user.tag})); + } catch (e) { + failed++; + } + } + if (failed === 0) { + await interaction.editReply(embedType(moduleStrings.done, {})); + } else { + await interaction.editReply(embedType(moduleStrings.notDone, {})); + failed = 0; + } + + } + if (target === 'bots') { + await interaction.deferReply({ ephemeral: true }); + for (const member of interaction.guild.members.cache.values()) { + if (member.user.bot) { + try { + await member.roles.remove(interaction.options.getRole('role'), localize('massrole', 'remove-reason', {u: interaction.user.tag})); + } catch (e) { + failed++; + } + } + } + if (failed === 0) { + await interaction.editReply(embedType(moduleStrings.done, {})); + } else { + await interaction.editReply(embedType(moduleStrings.notDone, {})); + failed = 0; + } + + } + if (target === 'humans') { + await interaction.deferReply({ ephemeral: true }); + for (const member of interaction.guild.members.cache.values()) { + if (member.manageable) { + if (!member.user.bot) { + try { + await member.roles.remove(interaction.options.getRole('role'), localize('massrole', 'remove-reason', {u: interaction.user.tag})); + } catch (e) { + failed++; + } + } + } + } + if (failed === 0) { + await interaction.editReply(embedType(moduleStrings.done, {})); + } else { + await interaction.editReply(embedType(moduleStrings.notDone, {})); + failed = 0; + } + + } + }, + 'remove-all': async function (interaction) { + if (interaction.replied) return; + const moduleStrings = interaction.client.configurations['massrole']['strings']; + checkTarget(interaction); + if (target === 'all') { + await interaction.deferReply({ ephemeral: true }); + for (const member of interaction.guild.members.cache.values()) { + try { + await member.roles.remove(member.roles.cache.filter(role => !role.managed), localize('massrole', 'remove-reason', {u: interaction.user.tag})); + } catch (e) { + failed++; + } + } + if (failed === 0) { + await interaction.editReply(embedType(moduleStrings.done, {})); + } else { + await interaction.editReply(embedType(moduleStrings.notDone, {})); + failed = 0; + } + } else if (target === 'bots') { + await interaction.deferReply({ ephemeral: true }); + for (const member of interaction.guild.members.cache.values()) { + if (member.manageable) { + if (member.user.bot) { + try { + await member.roles.remove(member.roles.cache.filter(role => !role.managed), localize('massrole', 'remove-reason', {u: interaction.user.tag})); + } catch (e) { + failed++; + } + } + } + } + if (failed === 0) { + await interaction.editReply(embedType(moduleStrings.done, {})); + } else { + await interaction.editReply(embedType(moduleStrings.notDone, {})); + failed = 0; + } + } else if (target === 'humans') { + await interaction.deferReply({ ephemeral: true }); + for (const member of interaction.guild.members.cache.values()) { + if (member.manageable) { + if (!member.user.bot) { + try { + await member.roles.remove(member.roles.cache.filter(role => !role.managed), localize('massrole', 'remove-reason', {u: interaction.user.tag})); + } catch (e) { + failed++; + } + } + } + } + if (failed === 0) { + await interaction.editReply(embedType(moduleStrings.done, {})); + } else { + await interaction.editReply(embedType(moduleStrings.notDone, {})); + failed = 0; + } + } + } +}; + +/** + * Read content of "target"-option + * + */ +function checkTarget(interaction) { + if (!interaction.options.getString('target') || interaction.options.getString('target') === 'all') { + target = 'all'; + } else if (interaction.options.getString('target') === 'bots') { + target = 'bots'; + } else if (interaction.options.getString('target') === 'humans') { + target = 'humans'; + } +} + + +module.exports.config = { + name: 'massrole', + defaultMemberPermissions: ['ADMINISTRATOR'], + description: localize('massrole', 'command-description'), + + options: [ + { + type: 'SUB_COMMAND', + name: 'add', + description: localize('massrole', 'add-subcommand-description'), + options: [ + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('massrole', 'role-option-add-description') + }, + { + type: 'STRING', + required: false, + name: 'target', + choices: [ + { + name: localize('massrole', 'all-users'), + value: 'all' + }, + { + name: localize('massrole', 'bots'), + value: 'bots' + }, + { + name: localize('massrole', 'humans'), + value: 'humans' + } + ], + description: localize('massrole', 'target-option-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'remove', + description: localize('massrole', 'remove-subcommand-description'), + options: [ + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('massrole', 'role-option-remove-description') + }, + { + type: 'STRING', + required: false, + name: 'target', + choices: [ + { + name: localize('massrole', 'all-users'), + value: 'all' + }, + { + name: localize('massrole', 'bots'), + value: 'bots' + }, + { + name: localize('massrole', 'humans'), + value: 'humans' + } + ], + description: localize('massrole', 'target-option-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'remove-all', + description: localize('massrole', 'remove-all-subcommand-description'), + options: [ + { + type: 'STRING', + required: false, + name: 'target', + choices: [ + { + name: localize('massrole', 'all-users'), + value: 'all' + }, + { + name: localize('massrole', 'bots'), + value: 'bots' + }, + { + name: localize('massrole', 'humans'), + value: 'humans' + } + ], + description: localize('massrole', 'target-option-description') + } + ] + } + ] +}; \ No newline at end of file diff --git a/modules/massrole/configs/config.json b/modules/massrole/configs/config.json new file mode 100644 index 00000000..374c73cf --- /dev/null +++ b/modules/massrole/configs/config.json @@ -0,0 +1,40 @@ +{ + "description": { + "en": "Configure the function of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "commandsWarnings": { + "special": [ + { + "name": "/massrole", + "info": { + "en": "You need to first set the permissions in your server settings for this command and after that add them under \"adminRoles\" here.", + "de": "Du musst zuerst die Rechte in deinen Server-Einstellungen einstellen und danach diese unter \"AdminRollen\" hinzufügen." + } + } + ] + }, + "content": [ + { + "name": "adminRoles", + "humanName": { + "de": "Adminrollen" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Every role that can use the massrole command", + "de": "Jede Rolle, welche den Massrole command verwenden kann" + }, + "type": "array", + "content": "roleID" + } + ] +} \ No newline at end of file diff --git a/modules/massrole/configs/strings.json b/modules/massrole/configs/strings.json new file mode 100644 index 00000000..7cfd2da8 --- /dev/null +++ b/modules/massrole/configs/strings.json @@ -0,0 +1,52 @@ +{ + "description": { + "en": "Edit the messages and strings of the module here", + "de": "Stelle hier die Nachrichten des Modules ein" + }, + "humanName": { + "en": "Messages", + "de": "Nachrichten" + }, + "commandsWarnings": { + "normal": [ + "/massrole" + ] + }, + "filename": "strings.json", + "content": [ + { + "name": "done", + "humanName": { + "en": "Action executed", + "de": "Aktion ausgeführt" + }, + "default": { + "en": "The action was executed successfully.", + "de": "Die Aktion wurde erfolgreich ausgeführt." + }, + "description": { + "en": "This messages gets send when a action was executed successfully", + "de": "Diese Nachricht wird verschickt, wenn eine Akton erfolgreich ausgeführt wurde" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "notDone", + "humanName": { + "en": "Action not executed", + "de": "Aktion nicht ausgeführt" + }, + "default": { + "en": "The Action couldn't be executed because the bot has not enough permissions.", + "de": "Die Aktion konnte nicht vollständig ausgeführt werden, da der Bot nicht genug Rechte hat." + }, + "description": { + "en": "This messages gets send when a action was not executed successfully", + "de": "Diese Nachricht wird verschickt, wenn eine Aktion nicht erfolgreich ausgeführt wurde" + }, + "type": "string", + "allowEmbed": true + } + ] +} \ No newline at end of file diff --git a/modules/massrole/module.json b/modules/massrole/module.json new file mode 100644 index 00000000..a9849d7b --- /dev/null +++ b/modules/massrole/module.json @@ -0,0 +1,24 @@ +{ + "name": "massrole", + "humanReadableName": { + "en": "Massrole" + }, + "author": { + "name": "hfgd", + "link": "https://github.com/hfgd123", + "scnxOrgID": "2" + }, + "openSourceURL": "https://github.com/hfgd123/CustomDCBot/tree/main/modules/massrole", + "commands-dir": "/commands", + "config-example-files": [ + "configs/config.json", + "configs/strings.json" + ], + "tags": [ + "tools" + ], + "description": { + "en": "Simple module to manage the roles of many members at once!", + "de": "Einfaches Modul, um die Rollen vieler Nutzer gleichzeitig zu verwalten!" + } +} \ No newline at end of file diff --git a/modules/moderation/__tests__/phishingService.test.js b/modules/moderation/__tests__/phishingService.test.js new file mode 100644 index 00000000..a234b1a8 --- /dev/null +++ b/modules/moderation/__tests__/phishingService.test.js @@ -0,0 +1,54 @@ +const heuristics = require('../phishingHeuristics'); +const { checkPhishing, setCustomPatterns } = require('../phishingService'); + +describe('phishing heuristics', () => { + test('isIpAddress detects IPv4 and IPv6', () => { + expect(heuristics.isIpAddress('127.0.0.1')).toBe(true); + expect(heuristics.isIpAddress('::1')).toBe(true); + expect(heuristics.isIpAddress('example.com')).toBe(false); + }); + + test('domainSimilarity returns info for typos', () => { + const res = heuristics.domainSimilarity('paypal.com', ['paypal.com']); + expect(res).toBeNull(); + const res2 = heuristics.domainSimilarity('paypa1.com', ['paypal.com']); + expect(res2).not.toBeNull(); + expect(res2.match).toBe('paypal.com'); + }); + + test('extractFirstUrlFromMessage picks a URL', () => { + expect(heuristics.extractFirstUrlFromMessage('no link here')).toBeNull(); + expect( + heuristics.extractFirstUrlFromMessage('visit https://example.com now') + ).toBe('https://example.com'); + }); +}); + +describe('checkPhishing integration', () => { + test('flags obvious phishing link', async () => { + const { isPhishing, reasons } = await checkPhishing({ url: 'http://paypal.com.example.tk/login' }); + expect(isPhishing).toBe(true); + expect(reasons.some(r => r.includes('Suspicious TLD'))).toBe(true); + }); + + test('returns false for benign https site', async () => { + const { isPhishing } = await checkPhishing({ url: 'https://example.com' }); + expect(isPhishing).toBe(false); + }); + + test('custom patterns can match', async () => { + setCustomPatterns(['evil']); + const { isPhishing, reasons } = await checkPhishing({ url: 'https://good.com/evil' }); + expect(isPhishing).toBe(true); + expect(reasons.some(r => r.includes('Custom pattern'))).toBe(true); + }); + + test('override configuration works', async () => { + // artificially lower threshold so non-secure example flags + const { isPhishing } = await checkPhishing( + { url: 'https://example.com' }, + { config: { thresholds: { phishingScore: 0 } } } + ); + expect(isPhishing).toBe(true); + }); +}); diff --git a/modules/moderation/commands/moderate.js b/modules/moderation/commands/moderate.js new file mode 100644 index 00000000..ea32e192 --- /dev/null +++ b/modules/moderation/commands/moderate.js @@ -0,0 +1,937 @@ +const {localize} = require('../../../src/functions/localize'); +const { + embedType, dateToDiscordTimestamp, lockChannel, unlockChannel, + sendMultipleSiteButtonMessage, truncate, formatDiscordUserName +} = require('../../../src/functions/helpers'); +const {moderationAction} = require('../moderationActions'); +const durationParser = require('parse-duration'); +const {MessageEmbed} = require('discord.js'); +const {Op} = require('sequelize'); +let guildBanCache; + +module.exports.beforeSubcommand = async function (interaction) { + if (interaction.options.getUser('user')) { + interaction.memberToExecuteUpon = interaction.options.getMember('user'); + if (!interaction.memberToExecuteUpon) { + if (interaction.options['_subcommand'] !== 'ban') return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('moderation', 'user-not-on-server') + }); + else { + interaction.userNotOnServer = true; + interaction.memberToExecuteUpon = { + user: interaction.options.getUser('user'), + id: interaction.options.getUser('user').id, + notFound: true + }; + } + } + if (interaction.memberToExecuteUpon.user.id === interaction.client.user.id) { + interaction.memberToExecuteUpon = null; + return interaction.reply({ + ephemeral: true, + content: '[I\'m sorry, Dave, I\'m afraid I can\'t do that.](https://youtu.be/7qnd-hdmgfk)' + }); + } + } + if (!interaction.replied && interaction.options['_subcommand'] !== 'actions') await interaction.deferReply({ + ephemeral: true + }); +}; + +/** + * Fetches the notes of a user and returns `false` when system already responded + * @private + * @param {Interaction} interaction Interaction + * @returns {Promise} Object of notesUser + */ +async function fetchNotesUser(interaction) { + if (interaction.replied) return false; + if (interaction.options.getUser('user').id === interaction.user.id) { + interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'not-allowed-to-see-own-notes') + }); + return false; + } + let notesUser = await interaction.client.models['moderation']['UserNotes'].findOne({ + where: { + userID: interaction.options.getUser('user').id + } + }); + if (!notesUser) notesUser = await interaction.client.models['moderation']['UserNotes'].create({ + userID: interaction.options.getUser('user').id, + notes: [] + }); + return notesUser; +} + +module.exports.subcommands = { + 'notes': { + 'view': async function (interaction) { + const notesUser = await fetchNotesUser(interaction); + if (!notesUser) return; + const byUser = {}; + let i = 0; + for (const note of notesUser.notes.filter(n => n.content !== '[deleted]').reverse()) { + if (!byUser[note.authorID]) { + i++; + if (i > 24) continue; + byUser[note.authorID] = []; + } + byUser[note.authorID].push(note); + } + const fields = []; + for (const userID in byUser) { + const userTag = formatDiscordUserName((interaction.guild.members.cache.get(userID) || {user: {tag: userID}}).user); + let notesString = ''; + for (const note of byUser[userID]) { + notesString = notesString + `\n#${note.id}: ${dateToDiscordTimestamp(new Date(note.lastUpdateAt), 'R')}: \`${note.content.replaceAll('`', '')}\``; + } + fields.push({ + name: localize('moderation', 'user-notes-field-title', {t: userTag}), + value: truncate(notesString, 1024) + }); + } + if (fields.length === 0) fields.push({ + name: localize('moderation', 'info-field-title'), + value: localize('moderation', 'no-notes-found') + }); + if (fields.length === 24) fields.push({ + name: localize('moderation', 'info-field-title'), + value: localize('moderation', 'more-notes', {x: i - 24}) + }); + const embed = new MessageEmbed() + .setTitle(localize('moderation', 'notes-embed-title', {u: formatDiscordUserName(interaction.options.getUser('user'))})) + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) + .setThumbnail(interaction.options.getUser('user').avatarURL()) + .setColor('GREEN') + .setAuthor({name: interaction.client.user.username, iconURL: interaction.client.user.avatarURL()}) + .setFields(fields); + interaction.editReply({ + embeds: [embed] + }); + }, + 'create': async function (interaction) { + const notesUser = await fetchNotesUser(interaction); + if (!notesUser) return; + const notes = notesUser.notes; + notesUser.notes = []; + notes.push({ + content: interaction.options.getString('notes'), + lastUpdateAt: new Date().getTime(), + createdAt: new Date().getTime(), + authorID: interaction.user.id, + id: notes.length + 1 + }); + notesUser.notes = notes; + await notesUser.save(); + return interaction.editReply({ + content: localize('moderation', 'note-added') + }); + }, + 'edit': async function (interaction) { + const notesUser = await fetchNotesUser(interaction); + if (!notesUser) return; + const notes = notesUser.notes; + notesUser.notes = []; + const noteIndex = notes.findIndex(n => n.id === interaction.options.getInteger('note-id')); + const note = notes[noteIndex]; + if (!note || (note || {}).authorID !== interaction.user.id) return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'note-not-found-or-no-permissions') + }); + notes[noteIndex] = { + content: interaction.options.getString('notes'), + lastUpdateAt: new Date().getTime(), + createdAt: note.createdAt, + authorID: interaction.user.id, + id: note.id + }; + notesUser.notes = notes; + await notesUser.save(); + return interaction.editReply({ + content: localize('moderation', 'note-edited') + }); + }, + 'delete': async function (interaction) { + const notesUser = await fetchNotesUser(interaction); + if (!notesUser) return; + const notes = notesUser.notes; + notesUser.notes = []; + const noteIndex = notes.findIndex(n => n.id === interaction.options.getInteger('note-id')); + const note = notes[noteIndex]; + if (!note || (note || {}).authorID !== interaction.user.id) return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'note-not-found-or-no-permissions') + }); + notes[noteIndex] = { + content: '[deleted]', + lastUpdateAt: new Date().getTime(), + createdAt: note.createdAt, + authorID: interaction.user.id, + id: note.id + }; + notesUser.notes = notes; + await notesUser.save(); + return interaction.editReply({ + content: localize('moderation', 'note-deleted') + }); + } + }, + 'ban': function (interaction) { + if (interaction.replied) return; + if (!interaction.userNotOnServer) if (!checkRoles(interaction, 4)) return; + const parseDuration = interaction.options.getString('duration') ? new Date(new Date().getTime() + durationParser(interaction.options.getString('duration'))) : null; + if (interaction.options.getInteger('days')) if (interaction.options.getInteger('days') < 0 || interaction.options.getInteger('days') > 7) return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'invalid-days') + }); + moderationAction(interaction.client, 'ban', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {days: interaction.options.getInteger('days')}, parseDuration, interaction.options.getAttachment('proof')).then(r => { + guildBanCache = null; + if (r) { + if (parseDuration) interaction.editReply({ + content: localize('moderation', 'expiring-action-done', { + d: dateToDiscordTimestamp(parseDuration), + i: r.actionID + }) + }); + else interaction.editReply({ + content: localize('moderation', 'action-done', {i: r.actionID}) + }); + } else interaction.editReply({content: '⚠️ ' + r}); + }).catch((r) => { + interaction.editReply({content: '⚠️ ' + r}); + }); + }, + 'unban': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 4)) return; + moderationAction(interaction.client, 'unban', interaction.member, interaction.options.getString('id'), interaction.options.getString('reason')).then(r => { + guildBanCache = null; + if (r) interaction.editReply({ + content: localize('moderation', 'action-done', {i: r.actionID}) + }); + else interaction.editReply({content: '⚠️ ' + r}); + }).catch((r) => { + interaction.editReply({content: '⚠️ ' + r}); + }); + }, + 'clear': function (interaction) { + if (!checkRoles(interaction, 3)) return; + interaction.channel.bulkDelete(interaction.options.getInteger('amount') || 50, true).then(() => { + interaction.editReply({ + content: localize('moderation', 'cleared-channel') + }).catch(() => { + interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'clear-failed') + }); + }); + }); + }, + 'quarantine': function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 3)) return; + const parseDuration = interaction.options.getString('duration') ? new Date(new Date().getTime() + durationParser(interaction.options.getString('duration'))) : null; + moderationAction(interaction.client, 'quarantine', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {roles: Array.from(interaction.options.getMember('user').roles.cache.keys())}, parseDuration).then(r => { + if (r) { + if (parseDuration) interaction.editReply({ + content: localize('moderation', 'expiring-action-done', { + d: dateToDiscordTimestamp(parseDuration), + i: r.actionID + }) + }); + else interaction.editReply({ + content: localize('moderation', 'action-done', {i: r.actionID}) + }); + } else interaction.editReply({content: '⚠️ ' + r}); + }).catch((r) => { + interaction.editReply({content: '⚠️ ' + r}); + }); + }, + 'unquarantine': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 3)) return; + const lastAction = await interaction.client.models['moderation']['ModerationAction'].findOne({ + where: { + victimID: interaction.memberToExecuteUpon.user.id, + type: 'quarantine' + }, + order: [['createdAt', 'DESC']] + }); + if (!lastAction) return interaction.editReply({ + ephemeral: true, + content: '⚠️ ' + localize('moderation', 'no-quarantine-action-found') + }); + if (!(lastAction.additionalData.roles instanceof Array)) lastAction.additionalData.roles = []; + moderationAction(interaction.client, 'unquarantine', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {roles: lastAction.additionalData.roles || []}).then(r => { + if (r) { + interaction.editReply({content: localize('moderation', 'action-done', {i: r.actionID})}); + } else interaction.editReply({content: '⚠️ ' + r}); + }).catch((r) => { + interaction.editReply({content: '⚠️ ' + r}); + }); + }, + 'kick': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 3)) return; + moderationAction(interaction.client, 'kick', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {}, null, interaction.options.getAttachment('proof')).then(r => { + if (r) interaction.editReply({ + content: localize('moderation', 'action-done', {i: r.actionID}) + }); + else interaction.editReply({content: '⚠️ ' + r}); + }).catch((r) => { + interaction.editReply({content: '⚠️ ' + r}); + }); + }, + 'mute': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 2)) return; + const parseDuration = new Date(new Date().getTime() + durationParser(interaction.options.getString('duration'))); + if (durationParser(interaction.options.getString('duration')) > 2419200000) return interaction.editReply({ + ephemeral: true, + content: '⚠️ ' + localize('moderation', 'mute-max-duration') + }); + moderationAction(interaction.client, 'mute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {}, parseDuration, interaction.options.getAttachment('proof')).then(r => { + if (r) interaction.editReply({ + content: localize('moderation', 'action-done', {i: r.actionID}) + }); + else interaction.editReply({content: '⚠️ ' + r}); + }).catch((r) => { + interaction.editReply({content: '⚠️ ' + r}); + }); + }, + 'unmute': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 2)) return; + moderationAction(interaction.client, 'unmute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason')).then(r => { + if (r) interaction.editReply({ + content: localize('moderation', 'action-done', {i: r.actionID}) + }); + else interaction.editReply({content: '⚠️ ' + r}); + }).catch((r) => { + interaction.editReply({content: '⚠️ ' + r}); + }); + }, + 'warn': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 1)) return; + moderationAction(interaction.client, 'warn', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {}, null, interaction.options.getAttachment('proof')).then(r => { + if (r) interaction.editReply({ + content: localize('moderation', 'action-done', {i: r.actionID}) + }); + else interaction.editReply({content: '⚠️ ' + r}); + }).catch((r) => { + interaction.editReply({content: '⚠️ ' + r}); + }); + }, + 'channel-mute': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 2)) return; + moderationAction(interaction.client, 'channel-mute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {channel: interaction.channel}, null, interaction.options.getAttachment('proof')).then(r => { + if (r) interaction.editReply({ + content: localize('moderation', 'action-done', {i: r.actionID}) + }); + else interaction.editReply({content: '⚠️ ' + r}); + }).catch((r) => { + interaction.editReply({content: '⚠️ ' + r}); + }); + }, + 'remove-channel-mute': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 2)) return; + moderationAction(interaction.client, 'unchannel-mute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {channel: interaction.channel}).then(r => { + if (r) interaction.editReply({ + content: localize('moderation', 'action-done', {i: r.actionID}) + }); + else interaction.editReply({content: '⚠️ ' + r}); + }).catch((r) => { + interaction.editReply({content: '⚠️ ' + r}); + }); + }, + 'lock': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 2)) return; + await lockChannel(interaction.channel, [...interaction.client.configurations['moderation']['config']['moderator-roles_level2'], ...interaction.client.configurations['moderation']['config']['moderator-roles_level3'], ...interaction.client.configurations['moderation']['config']['moderator-roles_level4']], `[moderation] ${interaction.options.getString('reason') || localize('moderation', 'no-reason')}`); + await interaction.channel.send(embedType(interaction.client.configurations['moderation']['strings']['lock_channel_message'], { + '%user%': formatDiscordUserName(interaction.user), + '%reason%': interaction.options.getString('reason') || localize('moderation', 'no-reason') + })); + await interaction.editReply({ + ephemeral: true, + content: localize('moderation', 'locked-channel-successfully') + }); + }, + 'unlock': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 2)) return; + await unlockChannel(interaction.channel, localize('moderation', 'unlock-audit-log-reason')); + await interaction.channel.send(embedType(interaction.client.configurations['moderation']['strings']['unlock_channel_message'], { + '%user%': formatDiscordUserName(interaction.user) + })); + await interaction.editReply({ + ephemeral: true, + content: localize('moderation', 'unlocked-channel-successfully') + }); + }, + 'actions': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 1)) return; + const actions = await interaction.client.models['moderation']['ModerationAction'].findAll({ + where: { + victimID: interaction.memberToExecuteUpon.id + }, + order: [['createdAt', 'DESC']] + }); + const sites = []; + let fieldCount = 0; + let fieldCache = []; + actions.forEach(action => { + fieldCount++; + fieldCache.push({ + name: `#${action.actionID}: ${action.type}`, + value: localize('moderation', 'action-description-format', { + reason: action.reason, + u: action.memberID, + t: dateToDiscordTimestamp(new Date(action.createdAt)) + }) + }); + if (fieldCount % 3 === 0) { + addSite(fieldCache); + fieldCache = []; + } + }); + if (fieldCache.length !== 0) addSite(fieldCache); + if (sites.length === 0) addSite([{ + name: localize('moderation', 'no-actions-title'), + value: localize('moderation', 'no-actions-title', {u: formatDiscordUserName(interaction.memberToExecuteUpon.user)}) + }]); + + /** + * Adds a new site + * @private + * @param fs + */ + function addSite(fs) { + const embed = new MessageEmbed() + .setColor('YELLOW') + .setAuthor({name: interaction.client.user.username, iconURL: interaction.client.user.avatarURL()}) + .setTitle(localize('moderation', 'actions-embed-title', { + u: formatDiscordUserName(interaction.memberToExecuteUpon.user), + i: sites.length + 1 + })) + .setDescription(localize('moderation', 'actions-embed-description', {u: formatDiscordUserName(interaction.memberToExecuteUpon.user)})) + .setThumbnail(interaction.memberToExecuteUpon.user.avatarURL()) + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) + .addFields(fs); + sites.push(embed); + } + + sendMultipleSiteButtonMessage(interaction.channel, sites, [interaction.user.id], interaction); + }, + 'revoke-warn': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 1)) return; + const action = await interaction.client.models['moderation']['ModerationAction'].findOne({ + where: { + actionID: interaction.options.getString('warn-id') + } + }); + if (!action) return interaction.editReply({ + ephemeral: true, + content: localize('moderation', 'warning-not-found') + }); + moderationAction(interaction.client, 'unwarn', interaction.member, { + id: interaction.options.getString('warn-id'), + user: {id: interaction.options.getString('warn-id'), tag: 'Unknown'} + }, interaction.options.getString('reason')).then(async r => { + if (r) { + await action.destroy(); + interaction.editReply({content: localize('moderation', 'action-done', {i: r.actionID})}); + } else interaction.editReply({content: '⚠️ ' + r}); + }).catch((r) => { + interaction.editReply({content: '⚠️ ' + r}); + }); + } +}; + +module.exports.autoComplete = { + 'revoke-warn': { + 'warn-id': async function (interaction) { + const actions = await interaction.client.models['moderation']['ModerationAction'].findAll({ + where: { + victimID: { + [Op.not]: interaction.user.id + } + } + }); + const returnValue = []; + interaction.value = interaction.value.toLowerCase(); + for (const action of actions.filter(a => a.reason.toLowerCase().includes(interaction.value) || a.victimID.includes(interaction.value) || a.type.toLowerCase().includes(interaction.value) || (interaction.client.guild.members.cache.get(a.victimID) || {user: {tag: a.victimID}}).user.tag.toLowerCase().includes(interaction.value))) { + if (returnValue.length !== 25) returnValue.push({ + value: action.actionID.toString(), + name: truncate(`[${action.type}] ${formatDiscordUserName((interaction.client.guild.members.cache.get(action.victimID) || {user: {tag: action.victimID}}).user)}: ${action.reason}`, 100) + }); + } + interaction.respond(returnValue); + } + }, + 'unban': { + 'id': async function (interaction) { + if (!guildBanCache) { + guildBanCache = await interaction.guild.bans.fetch(); + setTimeout(() => { + guildBanCache = null; + }, 300000); + } + interaction.value = interaction.value.toLowerCase(); + const possibleValues = []; + for (const match of guildBanCache.filter(b => formatDiscordUserName(b.user).toLowerCase().includes(interaction.value) || b.user.username.toLowerCase().includes(interaction.value) || b.user.id.includes(interaction.value)).values()) { + if (possibleValues.length !== 25) possibleValues.push({ + name: formatDiscordUserName(match.user), + value: match.user.id + }); + } + interaction.respond(possibleValues); + } + } +}; + +/** + * Check if the user has the required roles + * @private + * @param {Interaction} interaction Interaction to perform action on + * @param {Number} minLevel Required mod-level + * @return {boolean} + */ +function checkRoles(interaction, minLevel) { + let allowedRoles = []; + for (let i = 1; i <= 5 - minLevel; i++) { + allowedRoles = allowedRoles.concat(interaction.client.configurations['moderation']['config'][`moderator-roles_level${5 - i}`]); + } + if (!interaction.member.roles.cache.find(r => allowedRoles.includes(r.id))) { + const data = embedType(interaction.client.configurations['moderation']['strings']['no_permissions'], { + '%required_level%': minLevel + }, {ephemeral: true}); + if (interaction.deferred) interaction.editReply(data); + else interaction.reply(data); + return false; + } + if (!interaction.memberToExecuteUpon) return true; + if (interaction.memberToExecuteUpon.roles.cache.find(r => allowedRoles.includes(r.id))) { + const data = embedType(interaction.client.configurations['moderation']['strings']['this_is_a_mod'], { + '%required_level%': minLevel + }, {ephemeral: true}); + if (interaction.deferred) interaction.editReply(data); + else interaction.reply(data); + return false; + } + return true; +} + +module.exports.config = { + name: 'moderate', + description: localize('moderation', 'moderate-command-description'), + + defaultMemberPermissions: ['MODERATE_MEMBERS'], + options: [ + { + type: 'SUB_COMMAND_GROUP', + name: 'notes', + description: localize('moderation', 'moderate-notes-command-description'), + options: [ + { + type: 'SUB_COMMAND', + name: 'view', + description: localize('moderation', 'moderate-notes-command-view'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'create', + description: localize('moderation', 'moderate-notes-command-create'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'notes', + required: true, + description: localize('moderation', 'moderate-notes-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'edit', + description: localize('moderation', 'moderate-notes-command-edit'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'INTEGER', + name: 'note-id', + required: true, + description: localize('moderation', 'moderate-note-id-description') + }, + { + type: 'STRING', + name: 'notes', + required: true, + description: localize('moderation', 'moderate-notes-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'delete', + description: localize('moderation', 'moderate-notes-command-delete'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'INTEGER', + name: 'note-id', + required: true, + description: localize('moderation', 'moderate-note-id-description') + } + ] + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'ban', + description: localize('moderation', 'moderate-ban-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'ATTACHMENT', + name: 'proof', + required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), + description: localize('moderation', 'moderate-proof-description') + }, + { + type: 'STRING', + name: 'duration', + required: false, + description: localize('moderation', 'moderate-duration-description') + }, + { + type: 'INTEGER', + name: 'days', + required: false, + description: localize('moderation', 'moderate-days-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'quarantine', + description: localize('moderation', 'moderate-quarantine-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'STRING', + name: 'duration', + required: false, + description: localize('moderation', 'moderate-duration-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'unban', + description: localize('moderation', 'moderate-unban-command-description'), + options: function (client) { + return [{ + type: 'STRING', + name: 'id', + required: true, + autocomplete: true, + description: localize('moderation', 'moderate-userid-description') + }, + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'unquarantine', + description: localize('moderation', 'moderate-unquarantine-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'clear', + description: localize('moderation', 'moderate-clear-command-description'), + options: [{ + type: 'INTEGER', + name: 'amount', + required: false, + description: localize('moderation', 'moderate-clear-amount-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'kick', + description: localize('moderation', 'moderate-kick-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'ATTACHMENT', + name: 'proof', + required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), + description: localize('moderation', 'moderate-proof-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'mute', + description: localize('moderation', 'moderate-mute-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'duration', + required: true, + description: localize('moderation', 'moderate-duration-description') + }, + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'ATTACHMENT', + name: 'proof', + required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), + description: localize('moderation', 'moderate-proof-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'unmute', + description: localize('moderation', 'moderate-unmute-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'warn', + description: localize('moderation', 'moderate-warn-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'ATTACHMENT', + name: 'proof', + required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), + description: localize('moderation', 'moderate-proof-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'channel-mute', + description: localize('moderation', 'moderate-channel-mute-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'ATTACHMENT', + name: 'proof', + required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), + description: localize('moderation', 'moderate-proof-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'remove-channel-mute', + description: localize('moderation', 'moderate-unchannel-mute-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'actions', + description: localize('moderation', 'moderate-actions-command-description'), + options: [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'revoke-warn', + description: localize('moderation', 'moderate-unwarn-command-description'), + options: function (client) { + return [{ + type: 'STRING', + name: 'warn-id', + required: true, + autocomplete: true, + description: localize('moderation', 'moderate-warnid-description') + }, { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'lock', + description: localize('moderation', 'moderate-lock-command-description'), + options: function (client) { + return [ + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'unlock', + description: localize('moderation', 'moderate-unlock-command-description') + } + ] +}; \ No newline at end of file diff --git a/modules/moderation/commands/report.js b/modules/moderation/commands/report.js new file mode 100644 index 00000000..f0da600a --- /dev/null +++ b/modules/moderation/commands/report.js @@ -0,0 +1,83 @@ +const {localize} = require('../../../src/functions/localize'); +const {embedType, messageLogToStringToPaste} = require('../../../src/functions/helpers'); +const {MessageEmbed} = require('discord.js'); + +module.exports.run = async function (interaction) { + const user = interaction.options.getMember('user'); + if (!user) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('moderation', 'report-user-not-found-on-guild', {s: interaction.guild.name}) + }); + if (user.id === interaction.client.user.id) return interaction.reply({ + ephemeral: true, + content: '[I\'m sorry, Dave, I\'m afraid I can\'t do that.](https://youtu.be/7qnd-hdmgfk)' + }); + if (user.roles.cache.find(r => [...interaction.client.configurations['moderation']['config']['moderator-roles_level2'], ...interaction.client.configurations['moderation']['config']['moderator-roles_level1'], ...interaction.client.configurations['moderation']['config']['moderator-roles_level3'], ...interaction.client.configurations['moderation']['config']['moderator-roles_level4']].includes(r.id))) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('moderation', 'can-not-report-mod') + }); + await interaction.deferReply({ephemeral: true}); + const logUrl = await messageLogToStringToPaste(interaction.channel); + let logChannel = interaction.client.configurations['moderation']['config']['report-channel-id'] ? interaction.client.channels.cache.get(interaction.client.configurations['moderation']['config']['report-channel-id']) : null; + if (!logChannel) logChannel = interaction.client.configurations['moderation']['config']['logchannel-id'] ? interaction.client.channels.cache.get(interaction.client.configurations['moderation']['config']['logchannel-id']) : null; + if (!logChannel) logChannel = interaction.client.logChannel; + let pingContent = ''; + interaction.client.configurations['moderation']['config']['roles-to-ping-on-report'].forEach(rid => { + pingContent = pingContent + ` <@&${rid}>`; + }); + if (pingContent === '') pingContent = localize('moderation', 'no-report-pings'); + const fields = []; + const proof = interaction.options.getAttachment('proof'); + if (proof) fields.push({ + name: localize('moderation', 'proof'), + value: `[${localize('moderation', 'file')}](${proof.proxyURL || proof.url})`, + inline: true + }); + logChannel.send({ + embeds: [ + new MessageEmbed() + .setTitle(localize('moderation', 'report-embed-title')) + .setDescription(localize('moderation', 'report-embed-description')) + .addField(localize('moderation', 'reported-user'), interaction.options.getUser('user').toString() + ` \`${interaction.options.getUser('user').id}\``, true) + .addField(localize('moderation', 'message-log'), localize('moderation', 'message-log-description', {u: logUrl}), true) + .addField(localize('moderation', 'channel'), interaction.channel.toString(), true) + .addField(localize('moderation', 'report-reason'), interaction.options.getString('reason')) + .addField(localize('moderation', 'report-user'), interaction.user.toString() + ` \`${interaction.user.id}\``) + .addFields(fields) + .setColor('RED') + .setImage(proof ? (proof.proxyURL || proof.url) : null) + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) + .setAuthor({name: interaction.client.user.username, iconURL: interaction.client.user.avatarURL()}) + .setTimestamp() + ], + content: pingContent + }); + interaction.editReply(embedType(interaction.client.configurations['moderation']['strings']['submitted-report-message'], { + '%mURL%': logUrl, + '%user%': interaction.options.getUser('user').toString() + })); +}; + +module.exports.config = { + name: 'report', + description: localize('moderation', 'report-command-description'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'report-user-description') + }, + { + type: 'STRING', + name: 'reason', + required: true, + description: localize('moderation', 'report-reason-description') + }, + { + type: 'ATTACHMENT', + name: 'proof', + description: localize('moderation', 'report-proof-description') + } + ] +}; \ No newline at end of file diff --git a/modules/moderation/configs/antiGrief.json b/modules/moderation/configs/antiGrief.json new file mode 100644 index 00000000..242a0472 --- /dev/null +++ b/modules/moderation/configs/antiGrief.json @@ -0,0 +1,112 @@ +{ + "description": { + "en": "This system can prevent moderation-tool-abuse by staff-members", + "de": "Dieses System kann Moderation-Tool-Missbrauch von Teammitgliedern verhindern" + }, + "humanName": { + "en": "Anti-Grief-Configuration", + "de": "Anti-Grief-Konfiguration" + }, + "informationBanner": { + "en": "This feature can automatically quarantine moderators that abuse their permissions (banning / warning / kicking more people than you set up). For this to work, place your bot above all other roles and make sure that the quarantine-role is right below it. This ensures that moderators / admins can not just give permissions to the quarantine-role or remove permissions from the bot.", + "de": "Diese Funktion kann automatisch Moderatoren in Quarantäne versetzen, wenn sie ihre Berechtigungen (wenn sie mehr Leute Bannen / Warnen / Kicken als du einstellst). Damit das fehlerfrei funktioniert, musst dein Bot über alle anderen Rollen platziert sein und direkt darunter muss die Quarantäne-Rolle sein. Das stellt sicher, dass Moderatoren / Administratoren nicht einfach der Quarantäne-Rolle Rechte geben können oder dem Bot Rechte entfernen können." + }, + "warningBanner": { + "en": "This feature is currently limited to actions run by the moderation-module. If you've given your moderators native discord-permissions, they can bypass this. We plan to support native actions (+ channel-deletes and other griefing actions) in future.", + "de": "Diese Funktion ist aktuell nur für Aktionen, die mit dem Moderations-Modul durchgeführt wurden, verfügbar. Wenn du deinen Moderatoren native Discord-Berechtigungen gegeben hast, können sie das ganz einfach umgehen. Wir planen, native Aktionen (und Channel-Löschungen oder andere Grief-Aktionen) in der Zukunft zu unterstützen." + }, + "filename": "antiGrief.json", + "content": [ + { + "name": "enabled", + "humanName": { + "de": "Aktiviert", + "en": "Enabled?" + }, + "default": { + "en": false + }, + "description": { + "en": "Enables or disables the anti-join-grief-system", + "de": "Aktiviert oder deaktiviert das Anti-Join-Grief-System" + }, + "type": "boolean", + "elementToggle": true + }, + { + "name": "timeframe", + "humanName": { + "de": "Zeitfenster", + "en": "Timeframe" + }, + "default": { + "en": 3 + }, + "description": { + "en": "Timeframe in hours in which the limits can not be overstepped", + "de": "Zeitfenster in Stunden, in welchem die Limits nicht übertragen werden dürfen" + }, + "type": "integer" + }, + { + "name": "max_warn", + "humanName": { + "de": "Maximale Anzahl von Verwarnungen in dem Zeitfenster", + "en": "Maximal amount of warns in the timeframe" + }, + "default": { + "en": 15 + }, + "description": { + "en": "Maximal amount of warns a moderator can give in the timeframe until they get quarantined", + "de": "Maximale Anzahl von Verwarnungen, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" + }, + "type": "integer" + }, + { + "name": "max_mute", + "humanName": { + "de": "Maximale Anzahl von Mutes in dem Zeitfenster", + "en": "Maximal amount of mutes in the timeframe" + }, + "default": { + "en": 20 + }, + "description": { + "en": "Maximal amount of mutes a moderator can give in the timeframe until they get quarantined", + "de": "Maximale Anzahl von Mutes, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" + }, + "type": "integer" + }, + { + "name": "max_kick", + "humanName": { + "de": "Maximale Anzahl von Kicks in dem Zeitfenster", + "en": "Maximal amount of kicks in the timeframe" + }, + "default": { + "en": 10 + }, + "description": { + "en": "Maximal amount of kicks a moderator can give in the timeframe until they get quarantined", + "de": "Maximale Anzahl von Kicks, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" + }, + "type": "integer" + }, + { + "name": "max_ban", + "humanName": { + "de": "Maximale Anzahl von Bans in dem Zeitfenster", + "en": "Maximal amount of bans in the timeframe" + }, + "default": { + "en": 5 + }, + "description": { + "en": "Maximal amount of bans a moderator can give in the timeframe until they get quarantined", + "de": "Maximale Anzahl von Bans, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" + }, + "type": "integer" + } + ] +} \ No newline at end of file diff --git a/modules/moderation/configs/antiJoinRaid.json b/modules/moderation/configs/antiJoinRaid.json new file mode 100644 index 00000000..985e368b --- /dev/null +++ b/modules/moderation/configs/antiJoinRaid.json @@ -0,0 +1,116 @@ +{ + "description": { + "en": "This system can prevent spammers from raiding your server", + "de": "Dieses System kann es Spammern verhindern, deinen Server zu raiden" + }, + "humanName": { + "en": "Anti-Join-Raid-Configuration", + "de": "Anti-Join-Raid-Konfiguration" + }, + "filename": "antiJoinRaid.json", + "content": [ + { + "name": "enabled", + "humanName": { + "de": "Aktiviert", + "en": "Enabled?" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "Enables or disables the anti-join-raid-system", + "de": "Aktiviert oder deaktiviert das Anti-Join-Raid-System" + }, + "type": "boolean", + "elementToggle": true + }, + { + "name": "timeframe", + "humanName": { + "de": "Zeitfenster", + "en": "Timeframe" + }, + "default": { + "en": 5, + "de": 5 + }, + "description": { + "en": "Timeframe in which join actions should be recorded (in minutes)", + "de": "Zeitfenster, in welchem Serverbeitritte gezählt werden sollen (in Minuten)" + }, + "type": "integer" + }, + { + "name": "maxJoinsInTimeframe", + "humanName": { + "de": "Maximale Beitrittsanzahl", + "en": "Maximal count of new users" + }, + "default": { + "en": 3, + "de": 3 + }, + "description": { + "en": "Count of joins that are allowed to happen in the selected timeframe", + "de": "Anzahl an Serverbeitritten, die im ausgewählten Zeitfenster zugelassen werden" + }, + "type": "integer" + }, + { + "name": "action", + "humanName": { + "de": "Aktion", + "en": "Action" + }, + "default": { + "en": "quarantine", + "de": "quarantine" + }, + "description": { + "en": "Select the action here that should get performed if the anti-join-system gets triggered", + "de": "Wähle hier die Aktion aus, die ausgeführt werden soll, wenn das Anti-Join-Raid-System ausgelöst wird" + }, + "type": "select", + "content": [ + "mute", + "kick", + "quarantine", + "ban", + "give-role" + ] + }, + { + "name": "roleID", + "humanName": { + "de": "Rolle", + "en": "Role" + }, + "default": { + "en": "" + }, + "description": { + "en": "Only if action = give-role. Role that gets given to users who trigger the antiJoinRaid-System", + "de": "Nur verfügbar, wenn Aktion = give-role. Rolle, die Nutzern gegeben wird, die das Anti-Join-Raid-System auslösen" + }, + "type": "roleID" + }, + { + "name": "removeOtherRoles", + "humanName": { + "de": "Andere Rollen entfernen", + "en": "Remove other roles" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "Only if action = give-role. If enabled other roles that have been give to the user get removed after a short interval (and the giving of the role from \"roleID\" will be delayed)", + "de": "Nur verfügbar, wenn Aktion = give-role. Wenn aktiviert, werden andere Rollen die der Nutzer hat nach einem kurzen Zeitraum entfernt (und das Vergeben der Rolle von \"Rolle\" wird verzögert)" + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/moderation/configs/antiSpam.json b/modules/moderation/configs/antiSpam.json new file mode 100644 index 00000000..81a3189d --- /dev/null +++ b/modules/moderation/configs/antiSpam.json @@ -0,0 +1,217 @@ +{ + "description": { + "en": "You can configure here, how your bot should react to spam", + "de": "Du kannst hier einstellen, wie dein Bot auf Spam reagieren soll" + }, + "humanName": { + "en": "Anti-Spam-Configuration", + "de": "Anti-Spam-Konfiguration" + }, + "filename": "antiSpam.json", + "content": [ + { + "name": "enabled", + "humanName": { + "de": "Aktiviert", + "en": "Enabled?" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "Enable or disable the anti spam system", + "de": "Aktiviert oder deaktiviert das Anti-Spam-System" + }, + "type": "boolean", + "elementToggle": true + }, + { + "name": "timeframe", + "humanName": { + "de": "Zeitfenster", + "en": "Timeframe" + }, + "default": { + "en": 5, + "de": 5 + }, + "description": { + "en": "Timeframe after which message objects get deleted (and can not longer be used to detect spam)", + "de": "Zeitfenster, in dem Nachrichten gelöscht werden (und nicht länger zur Erkennung von Spam verwendet werden können)" + }, + "type": "integer" + }, + { + "name": "maxMessagesInTimeframe", + "humanName": { + "de": "Maximale Nachrichten im Zeitfenster", + "en": "Maximal count of messages in timeframe" + }, + "default": { + "en": 10, + "de": 10 + }, + "description": { + "en": "Count of messages that are allowed to be sent in the selected timeframe", + "de": "Anzahl an Nachrichten, die im ausgewählten Zeitfenster erlaubt sind" + }, + "type": "integer" + }, + { + "name": "maxDuplicatedMessagesInTimeframe", + "humanName": { + "de": "Maximale gleiche Nachrichten im Zeitfenster", + "en": "Maximal count of duplicated messages in timeframe" + }, + "default": { + "en": 5, + "de": 5 + }, + "description": { + "en": "Count of identical messages that are allowed to be sent in the selected timeframe", + "de": "Anzahl an gleichen Nachrichten, die im ausgewählten Zeitfenster erlaubt sind" + }, + "type": "integer" + }, + { + "name": "maxPingsInTimeframe", + "humanName": { + "de": "Maximale Pings im Zeitfenster", + "en": "Maximal count of pings in timeframe" + }, + "default": { + "en": 4, + "de": 4 + }, + "description": { + "en": "Count of pings (also counts replies) that are allowed to be sent in the selected timeframe", + "de": "Anzahl an Erwähnungen (zählt auch Antworten), die im ausgewählten Zeitfenster erlaubt sind" + }, + "type": "integer" + }, + { + "name": "maxMassPings", + "humanName": { + "de": "Maximale Massenpings im Zeitfenster", + "en": "Maximal count of mass-pings in timeframe" + }, + "default": { + "en": 3, + "de": 3 + }, + "description": { + "en": "Count of mass pings (= @everyone, @here and roles) that are allowed to be sent in the selected timeframe", + "de": "Anzahl an Massenerwähnungen (= @everyone, @here und Rollen), die im ausgewählten Zeitfenster erlaubt sind" + }, + "type": "integer" + }, + { + "name": "action", + "humanName": { + "de": "Aktion", + "en": "Action" + }, + "default": { + "en": "mute", + "de": "mute" + }, + "description": { + "en": "Select what should happen if someone spams", + "de": "Wähle hier die Aktion aus, die ausgeführt werden soll, wenn jemand spammt" + }, + "type": "select", + "content": [ + "mute", + "warn", + "kick", + "quarantine", + "ban" + ] + }, + { + "name": "sendChatMessage", + "humanName": { + "de": "Chatnachricht senden", + "en": "Send Chat-Message" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled the bot will send a chat message if it has to take action agains a bot", + "de": "Wenn aktiviert, wird der Bot eine Nachricht in den Chat senden, wenn er eine Aktion gegen einen Bot ausführen musste" + }, + "type": "boolean" + }, + { + "name": "message", + "dependsOn": "sendChatMessage", + "humanName": { + "de": "Nachricht", + "en": "Message" + }, + "default": { + "en": "Anti-Spam: I took action against <@%userid%> because of **%reason%**", + "de": "Anti-Spam: Ich habe wegen **%reason%** eine Aktion gegen <@%userid%> ausgeführt" + }, + "description": { + "en": "This will get send in the channel the spam is occurring in when anti-spam gets triggered", + "de": "Das wird in den Kanal gesendet, wenn das Anti-Spam-System ausgelöst wird" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "userid", + "description": { + "en": "ID of the user", + "de": "ID des Nutzers" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the action", + "de": "Grund der Aktion" + } + } + ] + }, + { + "name": "ignoredChannels", + "humanName": { + "de": "Ignorierte Kanäle", + "en": "Whitelisted Channels" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "You can set channels that get ignored here", + "de": "Du kannst hier Kanäle einstellen, die ignoriert werden sollen" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "ignoredRoles", + "humanName": { + "de": "Ignorierte Rollen", + "en": "Whitelisted roles" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "You can set roles that get ignored here", + "de": "Du kannst hier Rollen einstellen, die ignoriert werden sollen" + }, + "type": "array", + "content": "roleID" + } + ] +} \ No newline at end of file diff --git a/modules/moderation/configs/config.json b/modules/moderation/configs/config.json new file mode 100644 index 00000000..d881007d --- /dev/null +++ b/modules/moderation/configs/config.json @@ -0,0 +1,493 @@ +{ + "description": { + "en": "You can set up permissions and features of this module here", + "de": "Du kannst hier die Rechte dieses Modules einstellen" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "commandsWarnings": { + "special": [ + { + "name": "/moderate", + "info": { + "en": "Each moderator needs to be able to execute the /moderate command, so set your permissions in your server-settings accordingly. Additionally, moderator need to be entered into their level below.", + "de": "Jeder Modator muss den /moderate Befehl ausführen können, bitte konfiguriere das in deinen Server-Einstellungen. Zusätzlich muss jede Moderator-Rolle zu ihrem Level unten manuell eingetragen werden." + } + } + ] + }, + "content": [ + { + "name": "logchannel-id", + "humanName": { + "de": "Log-Kanal", + "en": "Log-Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "Moderative actions will get logged in this channel", + "de": "Moderative Aktionen werden in diesem Kanal geloggt" + }, + "type": "channelID" + }, + { + "name": "quarantine-role-id", + "humanName": { + "de": "Quarantäne-Rolle", + "en": "Quarantine-Role" + }, + "default": { + "en": "" + }, + "description": { + "en": "When a user gets quarantined, all of their roles will get removed and this quarantine-role wil get assigned", + "de": "Wenn ein Nutzer in Quarantäne gesteckt wird, werden alle Rollen von diesem entfernt und nur diese hinzugefügt" + }, + "type": "roleID" + }, + { + "name": "report-channel-id", + "default": { + "en": "" + }, + "humanName": { + "en": "Report-Channel", + "de": "Report-Kanal" + }, + "description": { + "en": "Channel in which user-reports should get send. (optional, default: Log-Channel)", + "de": "Kanal, in welchem Nutzer-Reports should get send. (optional, default: Log-Kanal)" + }, + "type": "channelID", + "allowNull": true + }, + { + "name": "remove-all-roles-on-quarantine", + "humanName": { + "de": "Bei Quarantäne alle Rollen entfernen", + "en": "Remove all roles on quarantine" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled all roles from a user get removed if they get quarantined (they get saved an can be restored with /unquarantine)", + "de": "Wenn diese Option aktiviert ist, werden alle Rollen eines Nutzers entfernt, wenn er in Quarantäne gesetzt wird (sie werden gespeichert und mit /unquarantine wiederhergestellt)" + }, + "type": "boolean" + }, + { + "name": "moderator-roles_level1", + "humanName": { + "en": "Moderator-Level 1" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Moderator roles that can perform the following actions: Warn", + "de": "Rollen, die folgende Aktionen ausführen können: Warn" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "moderator-roles_level2", + "humanName": { + "en": "Moderator-Level 2" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Lock, Unlock, Channelmute, Remove-Channel-Mute", + "de": "Rollen, die folgende Aktionen ausführen können: Warn, Mute, Unmute, Channelmute, Channel-Mute entfernen" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "moderator-roles_level3", + "humanName": { + "en": "Moderator-Level 3" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Kick, Clear", + "de": "Rollen, die folgende Aktionen ausführen können: Warn, Mute, Unmute, Kick, Clear" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "moderator-roles_level4", + "humanName": { + "en": "Moderator-Level 4" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Kick, Clear, Ban, Unban", + "de": "Rollen, die folgende Aktionen ausführen können: Warn, Mute, Unmute, Kick, Clear, Ban, Unban" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "roles-to-ping-on-report", + "humanName": { + "de": "Rollenpings bei Report", + "en": "Roles to ping on reports" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Roles that should get pinged in the log-channel when a user reports someone", + "de": "Rollen, die im log-Kanal gepingt werden sollen, wenn ein Nutzer jemanden Reportet" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "require_reason", + "humanName": { + "de": "Begründung erzwingen", + "en": "Fore moderators to set a reason" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "Should moderators be required to set a reason?", + "de": "Sollen Moderatoren verpflichtet werden, eine Begründung anzugeben?" + }, + "type": "boolean" + }, + { + "name": "require_proof", + "humanName": { + "de": "Beweis-Bild erzwingen", + "en": "Fore moderators to upload proof" + }, + "dependsOn": "require_reason", + "default": { + "en": false, + "de": false + }, + "description": { + "en": "Should moderators be required to upload proof for their actions?", + "de": "Sollen Moderatoren verpflichtet werden, einen Beweis hochzuladen?" + }, + "type": "boolean" + }, + { + "name": "action_on_invite", + "humanName": { + "de": "Aktion bei Invite", + "en": "Action on invite" + }, + "default": { + "en": "mute", + "de": "mute" + }, + "description": { + "en": "What should the bot do if someone posts an invite link?", + "de": "Was soll der Bot tun, wenn jemand einen Invite sendet?" + }, + "type": "select", + "content": [ + "none", + "warn", + "mute", + "kick", + "quarantine", + "ban" + ] + }, + { + "name": "action_on_scam_link", + "humanName": { + "de": "Aktion bei Scam-Link", + "en": "Action on Scam-Link" + }, + "default": { + "en": "none", + "de": "mute" + }, + "description": { + "en": "What should the bot do if someone posts an suspicious or confirmed scam link?", + "de": "Was soll der Bot tun, wenn jemand einen Link zu einer verdächtigen oder bestätigten Scam-Seite sendet?" + }, + "type": "select", + "content": [ + "none", + "warn", + "mute", + "kick", + "quarantine", + "ban" + ] + }, + { + "name": "phishing-log-channel-id", + "humanName": { + "de": "Phishing-Protokoll-Kanal", + "en": "Phishing Log Channel" + }, + "default": { + "en": "", + "de": "" + }, + "description": { + "en": "Optional channel where auto phishing detections are logged.", + "de": "Optionaler Kanal, in dem automatische Phishing-Erkennungen protokolliert werden." + }, + "type": "channelID", + "allowNull": true + }, + { + "name": "scam_link_level", + "humanName": { + "de": "Level der Scam-Link-Erkennung", + "en": "Level of Scam-Link-Detection" + }, + "default": { + "en": "confirmed", + "de": "confirmed" + }, + "description": { + "en": "Select the Level of Scam-Link-Filter. \"confirmed\" only contains verified Scam-Domains, while \"suspicious\" may contain not-harmful domains.", + "de": "\"confirmed\" enthält nur Scam-Domains, die wirklich als solche verifiziert wurden, während \"suspicious\" auch nicht-gefährdende Domains beinhalten kann" + }, + "type": "select", + "content": [ + "confirmed", + "suspicious" + ] + }, + { + "name": "whitelisted_channels_for_invite_blocking", + "humanName": { + "de": "Erlaubte Kanäle für Invitesperre", + "en": "Whitelisted channels for invite-ban" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Channels or categories where invite blocking is disabled", + "de": "Kanäle oder Kategorien, in welchen die Invitesperre deaktiviert ist" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "whitelisted_roles_for_invite_blocking", + "humanName": { + "de": "Erlaubte Rollen für Invitesperre", + "en": "Whitelisted roles for invite-ban" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "ID of Roles which are allowed to bypass invite blocking", + "de": "Rollen, welche die Invitesperre umgehen dürfen" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "blacklisted_words", + "humanName": { + "de": "Gesperrte Wörter", + "en": "Blacklisted words" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Words that are blacklisted", + "de": "Wörter, die blockiert sind" + }, + "type": "array", + "content": "string" + }, + { + "name": "phishing-custom-patterns", + "humanName": { + "de": "Eigene Phishing-Muster", + "en": "Custom phishing patterns" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Add your own regexes/keywords/strings to flag as phishing (one entry per line, RegExp syntax supported)", + "de": "Füge eigene Regexe/Stichwörter/String hinzu, die als Phishing markiert werden sollen (jeweils eine pro Zeile, RegExp-Syntax möglich)" + }, + "type": "array", + "content": "string" + }, + { + "name": "action_on_posting_blacklisted_word", + "humanName": { + "de": "Aktion bei gesperrtem Wort", + "en": "Action on blacklisted Word" + }, + "default": { + "en": "mute", + "de": "mute" + }, + "description": { + "en": "What should the bot do if someone posts a blacklisted word?", + "de": "Was soll der Bot tun, wenn jemand ein gesperrtes Wort sagt?" + }, + "type": "select", + "content": [ + "none", + "warn", + "mute", + "kick", + "ban", + "quarantine" + ] + }, + { + "name": "changeNicknames", + "humanName": { + "de": "Nicknamen bei Mute- / Quarantäne ändern", + "en": "Change nicknames on Mute- / Quarantine" + }, + "default": { + "en": false, + "de": false + }, + "description": { + "en": "If enabled, the user will get renamed when they get muted or quarantined", + "de": "Wenn aktiviert, wird der Nutzer umbenannt, wenn er gemutet oder in Quarantäne gesteckt wird" + }, + "type": "boolean" + }, + { + "name": "changeNicknameOnMute", + "dependsOn": "changeNicknames", + "humanName": { + "de": "Neuer Nickname bei Mute", + "en": "New nickname on mute" + }, + "default": { + "en": "%nickname%", + "de": "%nickname%" + }, + "description": { + "en": "The nickname in which the user should be renamed when they get muted", + "de": "Der Nickname, in welchen der Nutzer umbenannt werden soll, wenn er gemuted wird" + }, + "type": "string", + "params": [ + { + "name": "nickname", + "description": { + "en": "Original nickname of the user" + } + } + ] + }, + { + "name": "changeNicknameOnQuarantine", + "humanName": { + "de": "Nickname während der Quarantäne", + "en": "Nickname during quarantine" + }, + "dependsOn": "changeNicknames", + "default": { + "en": "%nickname%", + "de": "%nickname%" + }, + "description": { + "en": "The nickname in which the user should be renamed when they get quarantined" + }, + "type": "string", + "params": [ + { + "name": "nickname", + "description": { + "en": "Original nickname of the user" + } + } + ] + }, + { + "name": "automod", + "humanName": { + "de": "Automod", + "en": "Automod" + }, + "default": { + "en": {}, + "de": {} + }, + "description": { + "en": "You can define here what should happen (options: mute, kick, ban, quarantine) when someone gets x warns. Specify duration by writing : after the action.", + "de": "Du kannst hier festlegen, was passieren soll (optionen: mute, kick, ban), wenn jemand x Verwarnungen bekommt. Länge festlegen, indem : hinter die Aktion geschrieben wird." + }, + "type": "keyed", + "content": { + "key": "integer", + "value": "string" + } + }, + { + "name": "warnsExpire", + "humanName": { + "de": "Sollen Warns automatisch gelöscht werden?", + "en": "Should warns be deleted automatically?" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, warns will be deleted automatically after a certain period of time. Warns expired this way will completely disappear and can not be viewed again after they expired.", + "de": "Wenn aktiviert, werden Warns automatisch nach einer bestimmten Zeitspanne gelöscht. Auf diese Weiße abgelaufene Warns werden komplett verschwinden und können nie erneut gesehen werden." + }, + "type": "boolean" + }, + { + "name": "warnExpiration", + "humanName": { + "de": "Zeit, nach der Warns automatisch ablaufen", + "en": "Time after which warns will be automatically removed" + }, + "default": { + "en": "3 months" + }, + "dependsOn": "warnsExpire", + "description": { + "en": "Warns will be automatically deleted after this value after it's creation. Please note that this action will delete existing warns if they expired. Enter an english value, such as \"1y\" (= 1 year), \"3 Months\" (= 3 Months) oder \"2w\" (= 2 Weeks).", + "de": "Warnungen werden automatisch gelöscht, wenn sie diese Zeitspanne nach Erstellung erreicht haben. Trage einen englischen Wert, wie \"1y\" (= 1 Jahr), \"3 Months\" (= 3 Monate) oder \"2w\" (= 2 Woche) ein." + }, + "type": "string" + } + ] +} \ No newline at end of file diff --git a/modules/moderation/configs/joinGate.json b/modules/moderation/configs/joinGate.json new file mode 100644 index 00000000..dcacd24a --- /dev/null +++ b/modules/moderation/configs/joinGate.json @@ -0,0 +1,147 @@ +{ + "description": { + "en": "This system can prevent suspicious accounts from getting access to your server", + "de": "Dieses System kann verhindern, dass verdächtige Accounts Zugriff erhalten" + }, + "humanName": { + "de": "Join-Gate-Konfiguration", + "en": "Join-Gate-Configuration" + }, + "filename": "joinGate.json", + "content": [ + { + "name": "enabled", + "humanName": { + "de": "Aktiviert?", + "en": "Enabled?" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "Enable or disable the join gate", + "de": "Aktiviere oder deaktiviere das Join-Gate" + }, + "type": "boolean", + "elementToggle": true + }, + { + "name": "allUsers", + "humanName": { + "de": "Alle Nutzer filtern", + "en": "Filter all users" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled all users action against all new users will be taken", + "de": "Wenn aktiviert, werden Aktionen gegen alle neuen Nutzer ausgefüht" + }, + "type": "boolean" + }, + { + "name": "action", + "humanName": { + "de": "Aktion", + "en": "Action" + }, + "default": { + "en": "quarantine", + "de": "quarantine" + }, + "description": { + "en": "Select the action here that should get performed if the join gate gets triggered", + "de": "Wähle hier die Aktion, die ausgeführt werden soll, wenn das Join-Gate ausgelöst wird" + }, + "type": "select", + "content": [ + "mute", + "kick", + "quarantine", + "ban", + "give-role" + ] + }, + { + "name": "roleID", + "humanName": { + "de": "Rolle", + "en": "Role" + }, + "default": { + "en": "" + }, + "description": { + "en": "Only if action = give-role. Role that gets given to users who fail the join gate", + "de": "Nur verfügbar, wenn Aktion = give-role. Rolle, die Nutzern gegeben wird, die das Join-Gate nicht bestehen" + }, + "type": "roleID" + }, + { + "name": "removeOtherRoles", + "humanName": { + "de": "Andere Rollen entfernen", + "en": "Remove other roles" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "Only if action = give-role. If enabled other roles that have been give to the user get removed after a short interval (and the giving of the role from \"roleID\" will be delayed)", + "de": "Nur verfügbar, wenn Aktion = give-role. Wenn aktiviert, werden andere Rollen die der Nutzer hat nach einem kurzen Zeitraum entfernt (und das Vergeben der Rolle von \"Rolle\" wird verzögert)" + }, + "type": "boolean" + }, + { + "name": "minAccountAge", + "humanName": { + "de": "Minimales Accountalter", + "en": "Minimum account age" + }, + "default": { + "en": "3", + "de": 3 + }, + "description": { + "en": "Age of the account of a new user that is required to be set to pass the join gate (in days)", + "de": "Alter des Accounts eines neuen Nutzers, der beitritt, welches benötigt wird um das Join-Gate zu bestehen (in Tagen)" + }, + "type": "integer" + }, + { + "name": "requireProfilePicture", + "humanName": { + "de": "Benötige Profilbild", + "en": "Require profile picture" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled users are required to have a profile picture set to pass the join gate", + "de": "Wenn aktiviert, brauchen Nutzer ein Profilbild um das Join-Gate zu bestehen" + }, + "type": "boolean" + }, + { + "name": "ignoreBots", + "humanName": { + "de": "Ignoriere Bots", + "en": "Ignore bots" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled bots are allowed to pass the join gate without any restrictions", + "de": "Wenn aktiviert, bestehen Bots das Join-Gate ohne Beschränkungen" + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/moderation/configs/strings.json b/modules/moderation/configs/strings.json new file mode 100644 index 00000000..b286accf --- /dev/null +++ b/modules/moderation/configs/strings.json @@ -0,0 +1,519 @@ +{ + "description": { + "en": "Set up which messages your bot should send", + "de": "Stelle hier ein, welche Nachrichten dein Bot schicken soll" + }, + "humanName": { + "en": "Messages", + "de": "Nachrichten" + }, + "filename": "strings.json", + "content": [ + { + "name": "no_permissions", + "humanName": {}, + "default": { + "en": "You can not do that. You need at least moderator level %required_level% to do this", + "de": "You can not do that. You need at least moderator level %required_level% to do this" + }, + "description": { + "en": "Message that gets send if the user doesn't has the required role and/or has not the required mod-level" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "required_level", + "description": { + "en": "Required mod-level to do this." + } + } + ] + }, + { + "name": "user_not_found", + "humanName": {}, + "default": { + "en": "I could not find this user - try using an ID or a mention", + "de": "I could not find this user - try using an ID or a mention" + }, + "description": { + "en": "Message that gets send if the user provided an invalid userid" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "missing_reason", + "humanName": {}, + "default": { + "en": "Please specify an reason", + "de": "Please specify an reason" + }, + "description": { + "en": "Message that gets send if the user does not provide a reason and 'require reason' is activated" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "this_is_a_mod", + "humanName": {}, + "default": { + "en": "You can not perform this action on your college.", + "de": "You can not perform this action on your college." + }, + "description": { + "en": "Message that gets send if the user tries to mute another moderator" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "phishing-log-entry", + "humanName": {}, + "default": { + "en": "**Phishing detected** by %user% in %channel%:\n%content%", + "de": "**Phishing erkannt** von %user% in %channel%:\n%content%" + }, + "description": { + "en": "Log message sent when auto phishing detection triggers", + "de": "Protokollnachricht, die gesendet wird, wenn die automatische Phishing-Erkennung auslöst" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { "name": "user", "description": { "en": "Tag of the user who posted the link" } }, + { "name": "channel", "description": { "en": "Channel where the link was posted" } }, + { "name": "content", "description": { "en": "Original message content" } } + ] + }, + { + "name": "submitted-report-message", + "humanName": {}, + "default": { + "en": "Thanks for reporting %user%. I notified our server team and transmitted them an [encrypted snapshot](<%mURL%>) of the current messages in this channel, so they can see what really happened. Please make sure that our bots and staff can message you, so we can ask you follow-up-questions, if needed.", + "de": "Thanks for reporting %user%. I notified our server team and transmitted them an [encrypted snapshot](<%mURL%>) of the current messages in this channel, so they can see what really happened. Please make sure that our bots and staff can message you, so we can ask you follow-up-questions, if needed." + }, + "description": { + "en": "Message that gets send, if someone reports somebody." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the user they reported" + } + }, + { + "name": "mURL", + "description": { + "en": "URL to the message log" + } + } + ] + }, + { + "name": "mute_message", + "humanName": {}, + "default": { + "en": "You got muted for **%reason%** by %user%!", + "de": "You got muted for **%reason%** by %user%!" + }, + "description": { + "en": "Message that gets send to a user when they got muted" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the mute" + } + } + ] + }, + { + "name": "channel_mute", + "humanName": {}, + "default": { + "en": "You got channel-muted from %channel% for **%reason%** by %user%!" + }, + "description": { + "en": "Message that gets send to a user when they got muted" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the mute" + } + }, + { + "name": "channel", + "description": { + "en": "Channel from which the user got muted" + } + } + ] + }, + { + "name": "remove-channel_mute", + "humanName": {}, + "default": { + "en": "Your channel-mute from %channel% got removed because of **%reason%** by %user%!" + }, + "description": { + "en": "Message that gets send to a user when they got muted" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the mute" + } + }, + { + "name": "channel", + "description": { + "en": "Channel from which the user got unmuted" + } + } + ] + }, + { + "name": "tmpmute_message", + "humanName": {}, + "default": { + "en": "You got temporarily muted for **%reason%** by %user%! This action is going to expire on %date%.", + "de": "You got temporarily muted for **%reason%** by %user%! This action is going to expire on %date%." + }, + "description": { + "en": "Message that gets send to a user when they got temporarily muted" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the mute" + } + }, + { + "name": "date", + "description": { + "en": "Timestamp when this action expires" + } + } + ] + }, + { + "name": "quarantine_message", + "humanName": {}, + "default": { + "en": "You got quarantined for **%reason%** by %user%!", + "de": "You got quarantined for **%reason%** by %user%!" + }, + "description": { + "en": "Message that gets send to a user when they get quarantined" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the mute" + } + } + ] + }, + { + "name": "tmpquarantine_message", + "humanName": {}, + "default": { + "en": "You got quarantined temporarily for **%reason%** by %user%! This action is going to expire on %date%", + "de": "You got quarantined temporarily for **%reason%** by %user%! This action is going to expire on %date%" + }, + "description": { + "en": "Message that gets send to a user when they get quarantined" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the mute" + } + }, + { + "name": "date", + "description": { + "en": "Date when the quarantine is going to be removed automatically" + } + } + ] + }, + { + "name": "unquarantine_message", + "humanName": {}, + "default": { + "en": "You got unquarantined for **%reason%** by %user%!", + "de": "You got unquarantined for **%reason%** by %user%!" + }, + "description": { + "en": "Message that gets send to a user when they get unquarantined" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the mute" + } + } + ] + }, + { + "name": "unmute_message", + "humanName": {}, + "default": { + "en": "You got unmuted for **%reason%** by %user%!", + "de": "You got unmuted for **%reason%** by %user%!" + }, + "description": { + "en": "Message that gets send to a user when they got unmuted" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the unmute" + } + } + ] + }, + { + "name": "kick_message", + "humanName": {}, + "default": { + "en": "You got kicked for **%reason%** by %user%!", + "de": "You got kicked for **%reason%** by %user%!" + }, + "description": { + "en": "Message that gets send to a user when they got kicked" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the kick" + } + } + ] + }, + { + "name": "ban_message", + "humanName": {}, + "default": { + "en": "You got banned for **%reason%** by %user%!", + "de": "You got banned for **%reason%** by %user%!" + }, + "description": { + "en": "Message that gets send to a user when they got banned" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the ban" + } + } + ] + }, + { + "name": "tmpban_message", + "humanName": {}, + "default": { + "en": "You got temporarily banned for **%reason%** by %user%! This action is going to expire on %date%", + "de": "You got temporarily banned for **%reason%** by %user%! This action is going to expire on %date%" + }, + "description": { + "en": "Message that gets send to a user when they got banned temporarily" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the ban" + } + }, + { + "name": "date", + "description": { + "en": "Date on which the ban expires" + } + } + ] + }, + { + "name": "warn_message", + "humanName": {}, + "default": { + "en": "You got warned for **%reason%** by %user%!", + "de": "You got warned for **%reason%** by %user%!" + }, + "description": { + "en": "Message that gets send to a user when they got warned" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the warn" + } + } + ] + }, + { + "name": "lock_channel_message", + "humanName": {}, + "default": { + "en": "This channel got locked because %reason% by %user%", + "de": "This channel got locked because %reason% by %user%" + }, + "description": { + "en": "Message that gets send in a channel if it gets locked" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + }, + { + "name": "reason", + "description": { + "en": "Reason of the lock" + } + } + ] + }, + { + "name": "unlock_channel_message", + "humanName": {}, + "default": { + "en": "This channel got unlocked by %user%", + "de": "This channel got unlocked by %user%" + }, + "description": { + "en": "Message that gets send in a channel if it gets unlocked" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Tag of the moderator" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/moderation/configs/verification.json b/modules/moderation/configs/verification.json new file mode 100644 index 00000000..48bf0cf3 --- /dev/null +++ b/modules/moderation/configs/verification.json @@ -0,0 +1,239 @@ +{ + "description": { + "en": "Require accounts to verify that they are not a robot before accessing your server", + "de": "Zwinge neue Nutzer zu verifizieren, dass sie kein Roboter sind" + }, + "humanName": { + "de": "Verifikation-Konfiguration", + "en": "Verification-Configuration" + }, + "filename": "verification.json", + "content": [ + { + "name": "enabled", + "humanName": { + "de": "Aktiviert?", + "en": "Enabled?" + }, + "default": { + "en": false + }, + "description": { + "en": "If checked, verification on your server will be enabled", + "de": "Wenn aktiviert, wird Verifikation auf deinem Server aktiviert" + }, + "type": "boolean", + "elementToggle": true + }, + { + "name": "verification-needed-role", + "humanName": { + "de": "Rolle für Nutzer, die sich noch verifizieren müssen", + "en": "Role for users with pending verification" + }, + "default": { + "en": "" + }, + "description": { + "en": "Role, which members should be given before they verify themselves", + "de": "Rolle, die Nutzer erhalten, bevor sie sich verifiziert haben" + }, + "type": "roleID", + "allowNull": true + }, + { + "name": "verification-passed-role", + "humanName": { + "de": "Rolle für Nutzer mit bestandener Verifikation", + "en": "Role for users that passed verification" + }, + "default": { + "en": "" + }, + "description": { + "en": "Role, which members should be given after they got verified successfully", + "de": "Rolle, die Nutzern gegeben werden soll, wenn sie sich erfolgreich verifiziert haben" + }, + "type": "roleID", + "allowNull": true + }, + { + "name": "verification-log", + "humanName": {}, + "default": { + "en": "Verification-Log", + "de": "Verifikation-Log" + }, + "description": { + "en": "Channel where all verification-actions should get logged", + "de": "Kanal, in welchem alle Verifikation-Aktionen dokumentiert werden sollen" + }, + "type": "channelID", + "allowNull": true + }, + { + "name": "type", + "humanName": { + "en": "Type of verification", + "de": "Art der Verifikation" + }, + "default": { + "en": "captcha", + "de": "captcha" + }, + "description": { + "en": "How should the verification process be performed on your server?", + "de": "Wie sollen sich Nutzer verifizieren müssen, wenn sie den Server beitreten?" + }, + "type": "select", + "content": [ + "manual", + "captcha" + ] + }, + { + "name": "captchaLevel", + "humanName": { + "en": "Difficulty of captcha", + "de": "Schwäre des Captcha" + }, + "default": { + "en": "medium", + "de": "medium" + }, + "description": { + "en": "How difficult should the captcha sent to users be? (only if \"Type of verification\" = \"captcha\")", + "de": "Wie schwer soll das Captcha sein, dass an Nutzer gesendet wird? (Nur wenn \"Art der Verifikation\" = \"captcha\")" + }, + "type": "select", + "content": [ + "easy", + "medium", + "hard" + ] + }, + { + "name": "actionOnFail", + "humanName": { + "de": "Aktion bei Fehlschlagen der Verifikation", + "en": "Action on failure of verification" + }, + "default": { + "en": "kick", + "de": "kick" + }, + "description": { + "en": "What should happen if someone fails the verification?", + "de": "Was soll passieren, wenn die Verifikation fehlschlägt?" + }, + "type": "select", + "content": [ + "kick", + "quarantine", + "ban", + "mute" + ] + }, + { + "name": "restart-verification-channel", + "humanName": { + "de": "Verifikation-Neustarten-Kanal", + "en": "Restart Verification-Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "(optional) Add support for a channel where users can easily restart their verification process (for example if they had DMs disabled when they tried) and get notified if we couldn't reach them", + "de": "(optional) Kanal in welchem Nutzer ganz einfach den Verifikationsprozess neustarten können (zum Beispiel, wenn der Nutzer PNs deaktiviert hat) und benachrichtigt werden, wenn wir sie nicht erreichen konnten" + }, + "type": "channelID", + "allowNull": true + }, + { + "name": "captcha-message", + "humanName": { + "de": "Captcha-Nachricht", + "en": "Captcha-Message" + }, + "default": { + "en": "Welcome! Please verify that you are a human. You have two minutes to complete this.", + "de": "Willkommen! Bitte verifiziere, dass du kein Bot bist. Du hast zwei Minuten, um dies zu tun." + }, + "description": { + "en": "This message gets sent to users who need to complete a captcha", + "de": "Diese Nachricht wird an den Nutzer gesendet, der ein Captcha durchführen muss" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "manual-verification-message", + "humanName": { + "en": "Manual-Verification-Message", + "de": "Manuelle-Verifikation-Nachricht" + }, + "default": { + "en": "Welcome! A human will be verifying your account shortly. I will update you if I have any news.", + "de": "Willkommen! Ein Mensch wird deinen Account bald überprüfen und dir Zugriff auf den Server geben, bitte gedulde dich. Ich informiere dich bei Neuigkeiten." + }, + "description": { + "en": "This message gets sent to users who need to get verified manually.", + "de": "Diese Nachricht wird an Nutzer geschickt, die manuell verifiziert werden müssen" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "captcha-failed-message", + "humanName": { + "de": "Captcha fehlgeschlagen-Nachricht", + "en": "Captcha failed-Message" + }, + "default": { + "en": "It seems like you failed the verification. This is bad, I will have to take moderative actions against you - sorry fellow bot.", + "de": "Es scheint, als hättest du die Verifikation nicht bestanden. Schade, ich werde moderative Maßnahmen gegen dich ergreifen - entschuldige, Roboter." + }, + "description": { + "en": "This message gets sent when a user fails the verification", + "de": "Diese Nachricht wird an Nutzer gesendet, bei denen die Verifikation fehlgeschlagen ist" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "captcha-succeeded-message", + "humanName": { + "de": "Captcha abgeschlossen-Nachricht", + "en": "Captcha completed-Message" + }, + "default": { + "en": "Thanks! We have verified that you are indeed not a bot, so I granted you access to the whole server! Have fun <3", + "de": "Danke dir! Wir konnten verifizieren, dass du tatsächlich kein Bot bist, also haben wir dir auf den gesamten Server Zugriff gegeben! Viel Spaß <3" + }, + "description": { + "en": "This message gets sent to users when they complete the verification", + "de": "Diese Nachricht wird gesendet, wenn ein Nutzer die Verifikation erfolgreich abgeschlossen hat" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "verify-channel-first-message", + "humanName": { + "de": "Verifkations-Kanal-Info-Nachricht", + "en": "Verification-Channel-Info-Message" + }, + "default": { + "en": "Welcome! I have send you a DM about your verification-process. Please read it carefully. If you have DMs disabled, please activate them and click the button below. This step is required to join this server.", + "de": "Willkommen! Ich habe dir eine PN über den Verifikationsprozess. Bitte lese sie dir genau durch. Wenn du PNs deaktiviert hast, aktiviere sie bitte und klicke den Knopf unten. Dieser Schritt ist notwendig, um dem Server beizutreten." + }, + "description": { + "en": "This message is the introduction message in the verify-channel.", + "de": "Das ist die Informations-Nachricht im Verfikationskanal." + }, + "type": "string", + "allowEmbed": true + } + ] +} \ No newline at end of file diff --git a/modules/moderation/events/botReady.js b/modules/moderation/events/botReady.js new file mode 100644 index 00000000..dda09576 --- /dev/null +++ b/modules/moderation/events/botReady.js @@ -0,0 +1,104 @@ +const {planExpiringAction} = require('../moderationActions'); +const {Op} = require('sequelize'); +const {localize} = require('../../../src/functions/localize'); +const {embedType} = require('../../../src/functions/helpers'); +const {scheduleJob} = require('node-schedule'); +// import phishing service so that we can feed it custom patterns from configuration +const { setCustomPatterns } = require('../phishingService'); +const memberCache = {}; +const durationParser = require('parse-duration'); + +exports.run = async (client) => { + await updateCache(client); + const guild = await client.guilds.fetch(client.config.guildID); + + const actions = await client.models['moderation']['ModerationAction'].findAll({ + where: + { + expiresOn: { + [Op.gt]: new Date() + } + } + }); + for (const action of actions) { + if (!action.expiresOn) continue; + await planExpiringAction(new Date(action.expiresOn), action, guild); + } + + if (client.configurations['moderation']['config'].warnsExpire) { + const j = scheduleJob('42 0 * * *', () => { + deleteExpiredWarns(client).then(() => { + }); + }); + client.jobs.push(j); + deleteExpiredWarns(client).then(() => { + }); + } + + // configure phishing service with custom patterns from settings + const customPatterns = client.configurations['moderation']['config']['phishing-custom-patterns']; + if (Array.isArray(customPatterns) && customPatterns.length > 0) { + setCustomPatterns(customPatterns); + client.logger.info('[moderation] loaded ' + customPatterns.length + ' custom phishing pattern(s)'); + } + + const verificationConfig = client.configurations['moderation']['verification']; + if (!verificationConfig.enabled || !verificationConfig['restart-verification-channel']) return; + const channel = await client.channels.fetch(verificationConfig['restart-verification-channel']).catch(() => { + }); + if (!channel || (channel || {}).type !== 'GUILD_TEXT') return client.logger.error('[moderation] ' + localize('moderation', 'verify-channel-set-but-not-found-or-wrong-type')); + let message = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id).last(); + if (!message) { + message = await channel.send(localize('moderation', 'generating-message')); + await message.pin(); + } + await message.edit(embedType(verificationConfig['verify-channel-first-message'], {}, { + components: [ + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + label: '📨 ' + localize('moderation', 'restart-verification-button'), + customId: `mod-rvp`, + style: 'PRIMARY' + } + ] + } + ] + })); +}; + +/** + * Updates the punishment cache + * @private + * @param {Client} client + * @return {Promise} + */ +async function updateCache(client) { + const moduleConfig = client.configurations['moderation']['config']; + memberCache['quarantine'] = (await (await client.guilds.fetch(client.guildID)).members.fetch()).filter(m => !!m.roles.cache.get(moduleConfig['quarantine-role-id'])); +} + +/** + * Removes expired warns + * @param {Client} client + * @return {Promise} + */ +async function deleteExpiredWarns(client) { + const aD = await client.models['moderation']['ModerationAction'].findAll({ + where: { + createdAt: { + [Op.lt]: new Date(new Date().getTime() - durationParser(client.configurations['moderation']['config']['warnExpiration'])) + }, + type: 'warn' + } + }); + for (const action of aD) { + await action.destroy(); + } + if (aD.length !== 0) client.logger.info(`Deleted ${aD.length} warns because their expired`); +} + +module.exports.updateCache = updateCache; +module.exports.memberCache = memberCache; \ No newline at end of file diff --git a/modules/moderation/events/guildMemberAdd.js b/modules/moderation/events/guildMemberAdd.js new file mode 100644 index 00000000..75ddb88e --- /dev/null +++ b/modules/moderation/events/guildMemberAdd.js @@ -0,0 +1,299 @@ +const {memberCache} = require('./botReady'); +const {moderationAction} = require('../moderationActions'); +const {localize} = require('../../../src/functions/localize'); +const {embedType} = require('../../../src/functions/helpers'); +const {MessageAttachment} = require('discord.js'); +const {client} = require('../../../main'); + +let joinCache = []; + +exports.run = async (client, guildMember) => { + if (guildMember.guild.id !== client.config.guildID) return; + const moduleConfig = client.configurations['moderation']['config']; + + // Anti-Punishment-Bypass + if (!!memberCache.quarantine.get(guildMember.user.id)) { + guildMember.doNotGiveWelcomeRole = true; + await guildMember.roles.add(moduleConfig['quarantine-role-id'], `[moderation] ${localize('moderation', 'restored-punishment-audit-log-reason')}`); + } + + // Anti-Join-Raid + const antiJoinRaidConfig = client.configurations['moderation']['antiJoinRaid']; + if (antiJoinRaidConfig.enabled) { + const timestamp = new Date().getTime(); + joinCache.push({ + id: guildMember.user.id, + timestamp: timestamp + }); + setTimeout(() => { + joinCache = joinCache.filter(e => e.id !== guildMember.user.id && e.timestamp !== timestamp); + }, antiJoinRaidConfig.timeframe * 60000); + + if (joinCache.length >= antiJoinRaidConfig.maxJoinsInTimeframe) await performJoinRaidAction(); + + /** + * Performs anti-join-raid actions + * @private + * @return {Promise} + */ + async function performJoinRaidAction() { + for (const join of joinCache.filter(j => j.id !== guildMember.user.id)) { + const member = await guildMember.guild.members.fetch(join.id).catch(() => { + }); + if (!member) continue; + if (antiJoinRaidConfig.action === 'give-role') { + if (antiJoinRaidConfig.removeOtherRoles) await member.roles.remove(guildMember.roles.cache, `[moderation] [${localize('moderation', 'anti-join-raid')}] ${localize('moderation', 'raid-detected')}`); + await member.roles.add(antiJoinRaidConfig.roleID, `[moderation] [${localize('moderation', 'anti-join-raid')}] ${localize('moderation', 'raid-detected')}`); + } else { + const roles = []; + member.roles.cache.forEach(r => roles.push(r.id)); + await moderationAction(client, antiJoinRaidConfig.action, {user: client.user}, member, `[${localize('moderation', 'anti-join-raid')}] ${localize('moderation', 'raid-detected')}`, {roles: roles}); + } + } + if (antiJoinRaidConfig.action === 'give-role') { + if (antiJoinRaidConfig.removeOtherRoles) { + setTimeout(async () => { + await guildMember.roles.remove(guildMember.roles.cache, `[moderation] [${localize('moderation', 'anti-join-raid')}] ${localize('moderation', 'raid-detected')}`); + await guildMember.roles.add(antiJoinRaidConfig.roleID, `[moderation] [${localize('moderation', 'anti-join-raid')}] ${localize('moderation', 'raid-detected')}`); + }, 4000); + } else await guildMember.roles.add(antiJoinRaidConfig.roleID, `[moderation] [${localize('moderation', 'anti-join-raid')}] ${localize('moderation', 'raid-detected')}`); + return; + } + const roles = []; + guildMember.roles.cache.forEach(r => roles.push(r.id)); + await moderationAction(client, antiJoinRaidConfig.action, {user: client.user}, guildMember, `[${localize('moderation', 'anti-join-raid')}] ${localize('moderation', 'raid-detected')}`, {roles: roles}); + } + } + + // JoinGate + const joinGateConfig = client.configurations['moderation']['joinGate']; + if (joinGateConfig.enabled) await runJoinGate(); + + // Verification + const verificationConfig = client.configurations['moderation']['verification']; + if (verificationConfig.enabled) { + if (guildMember.user.bot) return; + if (verificationConfig['verification-needed-role'].length !== 0) await guildMember.roles.add(verificationConfig['verification-needed-role'], '[moderation] ' + localize('moderation', 'verification-started')); + await sendDMPart(verificationConfig, guildMember).catch(() => dmFail()); + + /** + * Sends a backup message for users who have their dms disabled + * @private + * @returns {Promise} + */ + async function dmFail() { + const channel = await client.channels.fetch(verificationConfig['restart-verification-channel'] || '').catch(() => { + }); + if (!channel || (channel || {}).type !== 'GUILD_TEXT') return client.logger.error('[moderation] ' + localize('moderation', 'verify-channel-set-but-not-found-or-wrong-type')); + const m = await channel.send({ + content: localize('moderation', 'dms-not-enabled-ping', {p: guildMember.toString()}), + + components: [ + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + label: '📨 ' + localize('moderation', 'restart-verification-button'), + customId: `mod-rvp`, + style: 'PRIMARY' + } + ] + } + ] + } + ); + setTimeout(() => { + m.delete().then(() => { + }); + }, 300000); + } + + if (guildMember.guild.channels.cache.get(verificationConfig['verification-log']) && verificationConfig.type === 'manual') { + await guildMember.guild.channels.cache.get(verificationConfig['verification-log']).send({ + embeds: [{ + title: localize('moderation', 'verification'), + color: 'GREEN', + description: `${localize('moderation', 'user')}: ${guildMember.toString()} (\`${guildMember.user.id}\`)\n${localize('moderation', 'manual-verification-needed')}` + }], + components: [ + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + label: '❌ ' + localize('moderation', 'verification-deny'), + customId: `mod-ver-d-${guildMember.user.id}`, + style: 'DANGER' + }, + { + type: 'BUTTON', + label: '✅ ' + localize('moderation', 'verification-approve'), + customId: `mod-ver-p-${guildMember.user.id}`, + style: 'SUCCESS' + } + ] + } + ] + }); + } + } + + /** + * Runs joingate on this GuildMember + * @returns {Promise} + */ + async function runJoinGate() { + if (guildMember.user.bot && joinGateConfig.ignoreBots) return; + if (joinGateConfig.allUsers) return performJoinGateAction(localize('moderation', 'joingate-for-everyone')); + const daysSinceCreation = (new Date().getTime() / 86400000).toFixed(0) - (guildMember.user.createdTimestamp / 86400000).toFixed(0); + if (daysSinceCreation <= joinGateConfig.minAccountAge) return performJoinGateAction(localize('moderation', 'account-age-to-low', { + a: daysSinceCreation, + c: joinGateConfig.minAccountAge + })); + if (!guildMember.user.avatarURL() && joinGateConfig.requireProfilePicture) return performJoinGateAction(localize('moderation', 'no-profile-picture')); + + /** + * Performs the join gate action + * @private + * @param {String} reason Reason for executing the join gate action + * @return {Promise} + */ + async function performJoinGateAction(reason) { + guildMember.joinGateTriggered = true; + if (joinGateConfig.action === 'give-role') { + if (joinGateConfig.removeOtherRoles) { + guildMember.doNotGiveWelcomeRole = true; + await guildMember.roles.remove(guildMember.roles.cache, `[moderation] [${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`); + } + await guildMember.roles.add(joinGateConfig.roleID, `[moderation] [${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`); + return; + } + const roles = []; + guildMember.roles.cache.forEach(r => roles.push(r.id)); + await moderationAction(client, joinGateConfig.action, {user: client.user}, guildMember, `[${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`, {roles: roles}); + } + } +}; + +/** + * Sends a user a DM about their verification + * @param {Object} verificationConfig Configuration of verification + * @param {GuildMember} guildMember GuildMember to send message to + * @returns {Promise} + */ +async function sendDMPart(verificationConfig, guildMember) { + return new Promise(async (resolve, reject) => { + try { + if (verificationConfig.type === 'manual') await guildMember.user.send(embedType(verificationConfig['manual-verification-message'], {})); + else { + if (!guildMember.client.scnxSetup) return guildMember.client.logger.error('[moderation] Captcha Generation is only available if your bot has an SCNX Integration set up.'); + const captcha = await require('../../../src/functions/scnx-integration').generateCaptcha(verificationConfig.captchaLevel); + await guildMember.user.send(embedType(verificationConfig['captcha-message'], {}, { + files: [new MessageAttachment(captcha.buffer, 'you-call-it-captcha-we-call-it-ai-training.png')] + })); + const c = await guildMember.user.createDM(); + const col = c.createMessageCollector({time: 120000}); + let p = false; + let d = null; + if (guildMember.guild.channels.cache.get(verificationConfig['verification-log'])) { + d = await guildMember.guild.channels.cache.get(verificationConfig['verification-log']).send({ + embeds: [{ + title: localize('moderation', 'verification'), + color: 'GREEN', + description: `${localize('moderation', 'user')}: ${guildMember.toString()} (\`${guildMember.user.id}\`)\n${localize('moderation', 'captcha-verification-pending')}` + }], + components: [ + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + label: '⏭️ ' + localize('moderation', 'verification-skip'), + customId: `mod-ver-p-${guildMember.user.id}`, + style: 'SECONDARY' + } + ] + } + ] + }); + const coli = d.createMessageComponentCollector({time: 120000}); + coli.on('collect', () => { + p = true; + }); + coli.on('end', () => { + d.delete(); + }); + } + col.on('collect', (m) => { + if (m.author.id === guildMember.user.id && !p) { + p = true; + if (m.content.toUpperCase() === captcha.solution.toUpperCase()) verificationPassed(guildMember); + else { + client.logger.log(`${guildMember.user.id} failed verification. Entered: "${m.content.toUpperCase()}", expected: "${captcha.solution.toUpperCase()}"`); + verificationFail(guildMember); + } + if (d && !d.deleted) d.delete().catch(() => { + }); + } + }); + col.on('end', () => { + if (!p) { + verificationFail(guildMember); + if (d && !d.deleted) d.delete().catch(() => { + }); + } + }); + } + resolve(); + } catch (e) { + reject(e); + } + }); +} + +module.exports.sendDMPart = sendDMPart; + +/** + * User passes verification, gets their roles and message gets send in log-channel + * @private + * @param {GuildMember} guildMember Member who passed the verification + * @returns {Promise} + */ +async function verificationPassed(guildMember) { + const verificationConfig = guildMember.client.configurations['moderation']['verification']; + if (verificationConfig['verification-needed-role'].length !== 0) await guildMember.roles.remove(verificationConfig['verification-needed-role'], '[' + localize('moderation', 'verification') + '] ' + localize('moderation', 'verification-completed')); + if (verificationConfig['verification-passed-role'].length !== 0) await guildMember.roles.add(verificationConfig['verification-passed-role'], '[' + localize('moderation', 'verification') + '] ' + localize('moderation', 'verification-completed')); + await guildMember.user.send(embedType(verificationConfig['captcha-succeeded-message'])).catch(() => { + }); + if (guildMember.guild.channels.cache.get(verificationConfig['verification-log'])) await guildMember.guild.channels.cache.get(verificationConfig['verification-log']).send({ + embeds: [{ + title: localize('moderation', 'verification'), + color: 'GREEN', + description: `${localize('moderation', 'user')}: ${guildMember.toString()} (\`${guildMember.user.id}\`)\n${localize('moderation', 'verification-completed')}` + }] + }); +} + +module.exports.verificationPassed = verificationPassed; + +/** + * User fails verification, gets moderated and message gets send in log-channel + * @private + * @param {GuildMember} guildMember Member who failed verification + * @returns {Promise} + */ +async function verificationFail(guildMember) { + const verificationConfig = guildMember.client.configurations['moderation']['verification']; + await guildMember.user.send(embedType(verificationConfig['captcha-failed-message'])); + await moderationAction(guildMember.client, verificationConfig.actionOnFail, guildMember.guild.me, guildMember, '[' + localize('moderation', 'verification') + '] ' + localize('moderation', 'verification-failed')); + if (guildMember.guild.channels.cache.get(verificationConfig['verification-log'])) await guildMember.guild.channels.cache.get(verificationConfig['verification-log']).send({ + embeds: [{ + title: localize('moderation', 'verification'), + color: 'GREEN', + description: `${localize('moderation', 'user')}: ${guildMember.toString()} (\`${guildMember.user.id}\`)\n${localize('moderation', 'verification-failed')}` + }] + }); +} + +module.exports.verificationFail = verificationFail; \ No newline at end of file diff --git a/modules/moderation/events/interactionCreate.js b/modules/moderation/events/interactionCreate.js new file mode 100644 index 00000000..2604fc38 --- /dev/null +++ b/modules/moderation/events/interactionCreate.js @@ -0,0 +1,37 @@ +const {verificationPassed, verificationFail, sendDMPart} = require('./guildMemberAdd'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async (client, interaction) => { + if (!interaction.isMessageComponent()) return; + if (interaction.customId === 'mod-rvp') { + const verificationConfig = client.configurations['moderation']['verification']; + if (interaction.member.roles.cache.filter(r => verificationConfig['verification-passed-role'].includes(r.id)).size !== 0) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('moderation', 'already-verified') + }); + sendDMPart(verificationConfig, interaction.member).then(() => { + interaction.reply({ + ephemeral: true, + content: localize('moderation', 'restarted-verification') + }); + }).catch(() => { + interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('moderation', 'dms-still-disabled', {g: interaction.member.guild.name}) + }); + }); + } + if (!interaction.customId.startsWith('mod-ver-')) return; + interaction.customId = interaction.customId.replaceAll('mod-ver-', ''); + const a = interaction.customId.split('-')[0]; + const id = interaction.customId.split('-')[1]; + const member = await interaction.guild.members.fetch(id).catch(() => {}); + if (!member) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('moderation', 'member-not-found') + }); + if (a === 'p') await verificationPassed(member); + else await verificationFail(member); + await interaction.message.edit({embeds: interaction.message.embeds, components: []}); + interaction.reply({ephemeral: true, content: localize('moderation', 'verification-update-proceeded')}); +}; \ No newline at end of file diff --git a/modules/moderation/events/messageCreate.js b/modules/moderation/events/messageCreate.js new file mode 100644 index 00000000..3c565805 --- /dev/null +++ b/modules/moderation/events/messageCreate.js @@ -0,0 +1,139 @@ +const {moderationAction} = require('../moderationActions'); +const {embedType} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); +const stopPhishing = require('stop-discord-phishing'); +// built-in phishing service (uses ML/heuristics plus configurable patterns) +const { checkPhishing } = require('../phishingService'); + +const messageCache = {}; + +module.exports.run = async (client, msg) => { + if (!client.botReadyAt) return; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + if (!msg.member) return; + if (msg.author.bot) return; + + const moduleConfig = client.configurations['moderation']['config']; + const antiSpamConfig = client.configurations['moderation']['antiSpam']; + if (msg.member.roles.cache.find(r => moduleConfig['moderator-roles_level2'].includes(r.id) || moduleConfig['moderator-roles_level3'].includes(r.id) || moduleConfig['moderator-roles_level4'].includes(r.id))) return; + const roles = []; + msg.member.roles.cache.forEach(r => roles.push(r.id)); + + // Anti-Spam + if (antiSpamConfig.enabled) if (!antiSpamConfig.ignoredChannels.includes(msg.channel.id)) { + let whitelisted = false; + antiSpamConfig.ignoredRoles.forEach(r => { + if (msg.member.roles.cache.get(r)) whitelisted = true; + }); + if (!whitelisted) await antiSpam(); + } + + /** + * Runs anti-spam on the message + * @private + * @return {Promise} + */ + async function antiSpam() { + if (!messageCache[msg.author.id]) messageCache[msg.author.id] = []; + messageCache[msg.author.id].push({ + id: msg.id, + content: msg.content, + mentions: Array.from(msg.mentions.members.keys()).length !== 0, + massMentions: msg.mentions.everyone || Array.from(msg.mentions.roles.keys()).length !== 0 + }); + setTimeout(() => { + messageCache[msg.author.id] = messageCache[msg.author.id].filter(m => m.id !== msg.id); + }, antiSpamConfig.timeframe * 1000); + if (messageCache[msg.author.id].length >= antiSpamConfig.maxMessagesInTimeframe) return await performAntiSpamAction(localize('moderation', 'reached-messages-in-timeframe', { + m: antiSpamConfig.maxMessagesInTimeframe, + t: antiSpamConfig.timeframe + })); + if (messageCache[msg.author.id].filter(m => m.content === msg.content).length >= antiSpamConfig.maxDuplicatedMessagesInTimeframe) return await performAntiSpamAction(localize('moderation', 'reached-duplicated-content-messages', { + m: messageCache[msg.author.id].filter(m => m.content === msg.content).length, + t: antiSpamConfig.timeframe + })); + if (messageCache[msg.author.id].filter(m => m.mentions).length >= antiSpamConfig.maxPingsInTimeframe) return await performAntiSpamAction(localize('moderation', 'reached-ping-messages', { + m: messageCache[msg.author.id].filter(m => m.mentions).length, + t: antiSpamConfig.timeframe + })); + if (messageCache[msg.author.id].filter(m => m.massMentions).length >= antiSpamConfig.maxMassPings) return await performAntiSpamAction(localize('moderation', 'reached-massping-messages', { + m: messageCache[msg.author.id].filter(m => m.massMentions).length, + t: antiSpamConfig.timeframe + })); + + /** + * Perform anti spam actions + * @private + * @param {String} reason Reason for executing anti spam actions + * @return {Promise} + */ + async function performAntiSpamAction(reason) { + await moderationAction(client, antiSpamConfig.action, {user: client.user}, msg.member, `[${localize('moderation', 'anti-spam')}]: ${reason}`, {roles: roles}); + if (antiSpamConfig.sendChatMessage) await msg.channel.send(embedType(antiSpamConfig.message, { + '%reason%': reason, + '%userid%': msg.author.id + })); + } + } + + await performBadWordAndInviteProtection(msg); +}; + +/** + * Performs the bad-word and invite protection on a message + * @private + * @param {Message} msg Message to check + * @return {Promise} + */ +async function performBadWordAndInviteProtection(msg) { + const moduleConfig = msg.client.configurations['moderation']['config']; + if (msg.member.roles.cache.find(r => moduleConfig['moderator-roles_level2'].includes(r.id) || moduleConfig['moderator-roles_level3'].includes(r.id) || moduleConfig['moderator-roles_level4'].includes(r.id))) return; + if (moduleConfig['action_on_scam_link'] !== 'none') { + // first run our custom analyzer, which also respects user-defined patterns + const analysis = await checkPhishing({message: msg.content}); + if (analysis.isPhishing) { + // send a log message if channel configured + const logChanId = moduleConfig['phishing-log-channel-id'] || moduleConfig['logchannel-id']; + if (logChanId) { + const logChan = await msg.client.channels.fetch(logChanId).catch(() => null); + if (logChan && logChan.isText()) { + logChan.send(embedType(msg.client.configurations['moderation']['strings']['phishing-log-entry'] || '**Phishing detected**', { + '%user%': msg.author.tag, + '%channel%': msg.channel.toString(), + '%content%': msg.content + })); + } + } + await msg.delete(); + await moderationAction(msg.client, moduleConfig['action_on_scam_link'], msg.client, msg.member, localize('moderation', 'scam-url-sent', {c: msg.channel.toString()}), {roles: msg.member.roles.cache.keys()}); + return; + } + // fallback to old library if desired + if (await stopPhishing.checkMessage(msg.content, moduleConfig['action_on_scam_link'] === 'suspicious')) { + await msg.delete(); + await moderationAction(msg.client, moduleConfig['action_on_scam_link'], msg.client, msg.member, localize('moderation', 'scam-url-sent', {c: msg.channel.toString()}), {roles: msg.member.roles.cache.keys()}); + return; + } + } + let containsBlacklistedWord = false; + moduleConfig['blacklisted_words'].forEach(word => { + if (msg.content.toLowerCase().includes(word.toLowerCase())) containsBlacklistedWord = true; + }); + if (containsBlacklistedWord && !msg.channel.nsfw) { + if (moduleConfig['action_on_posting_blacklisted_word'] !== 'none') { + await msg.delete(); + await moderationAction(msg.client, moduleConfig['action_on_posting_blacklisted_word'], msg.client, msg.member, localize('moderation', 'blacklisted-word', {c: msg.channel.toString()}), {roles: msg.member.roles.cache.keys()}); + } + } + if (moduleConfig['whitelisted_channels_for_invite_blocking'].includes(msg.channel.id) || moduleConfig['whitelisted_channels_for_invite_blocking'].includes(msg.channel.parentId)) return; + if (msg.member.roles.cache.find(r => moduleConfig['whitelisted_roles_for_invite_blocking'].includes(r.id))) return; + if (moduleConfig['action_on_invite'] !== 'none') { + if (msg.content.includes('discord.gg/') || msg.content.includes('discordapp.com/invite/')) { + await msg.delete(); + await moderationAction(msg.client, moduleConfig['action_on_invite'], msg.client, msg.member, localize('moderation', 'invite-sent', {c: msg.channel.toString()}), {roles: msg.member.roles.cache.keys()}); + } + } +} + +module.exports.performBadWordAndInviteProtection = performBadWordAndInviteProtection; \ No newline at end of file diff --git a/modules/moderation/events/messageUpdate.js b/modules/moderation/events/messageUpdate.js new file mode 100644 index 00000000..d1be62e9 --- /dev/null +++ b/modules/moderation/events/messageUpdate.js @@ -0,0 +1,11 @@ +const {performBadWordAndInviteProtection} = require('./messageCreate'); + +exports.run = async (client, oldMsg, msg) => { + if (!client.botReadyAt) return; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + if (!msg.member) return; + if (msg.author.bot) return; + + await performBadWordAndInviteProtection(msg); +}; \ No newline at end of file diff --git a/modules/moderation/models/ModerationAction.js b/modules/moderation/models/ModerationAction.js new file mode 100644 index 00000000..63b647a1 --- /dev/null +++ b/modules/moderation/models/ModerationAction.js @@ -0,0 +1,28 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class ModerationAction extends Model { + static init(sequelize) { + return super.init({ + actionID: { + autoIncrement: true, + type: DataTypes.INTEGER, + primaryKey: true + }, + victimID: DataTypes.STRING, + additionalData: DataTypes.JSON, + type: DataTypes.STRING, + memberID: DataTypes.STRING, + reason: DataTypes.STRING, + expiresOn: DataTypes.DATE + }, { + tableName: 'moderation_ModerationActions3', // v3 + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'ModerationAction', + 'module': 'moderation' +}; \ No newline at end of file diff --git a/modules/moderation/models/UserNotes.js b/modules/moderation/models/UserNotes.js new file mode 100644 index 00000000..6255d6b0 --- /dev/null +++ b/modules/moderation/models/UserNotes.js @@ -0,0 +1,22 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class UserNotes extends Model { + static init(sequelize) { + return super.init({ + userID: { + type: DataTypes.STRING, + primaryKey: true + }, + notes: DataTypes.JSON + }, { + tableName: 'moderation_UserNotes', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'UserNotes', + 'module': 'moderation' +}; \ No newline at end of file diff --git a/modules/moderation/moderationActions.js b/modules/moderation/moderationActions.js new file mode 100644 index 00000000..a2ca1f9d --- /dev/null +++ b/modules/moderation/moderationActions.js @@ -0,0 +1,353 @@ +const {scheduleJob} = require('node-schedule'); +const {embedType, formatDate, dateToDiscordTimestamp, formatDiscordUserName} = require('../../src/functions/helpers'); +const {MessageEmbed} = require('discord.js'); +const {localize} = require('../../src/functions/localize'); +const durationParser = require('parse-duration'); +const {Op} = require('sequelize'); + +/** + * Performs a mod action + * @param {Client} client Client + * @param {String} type Typ of action to run + * @param {User} user User who run this action + * @param {Member} victim Member on who this action should get executed + * @param {String} reason Reason for this action + * @param {Object} additionalData Additional data needed for executing this action + * @param {Date} expiringAt Date when this action should expire + * @param {MessageAttachment} proof Message-Attachment containing proof + * @return {Promise} + */ +async function moderationAction(client, type, user, victim, reason, additionalData = {}, expiringAt = null, proof = null) { + const moduleConfig = client.configurations['moderation']['config']; + const moduleStrings = client.configurations['moderation']['strings']; + const antiGriefConfig = client.configurations['moderation']['antiGrief']; + if (!reason) reason = localize('moderation', 'no-reason'); + return new Promise(async (resolve, reject) => { + const guild = await client.guilds.fetch(client.guildID); + const quarantineRole = await guild.roles.fetch(moduleConfig['quarantine-role-id']).catch(() => { + }); + if (!quarantineRole && (type === 'quarantine' || type === 'unquarantine')) { + client.logger.error(localize('moderation', 'quarantinerole-not-found')); + return reject(localize('moderation', 'quarantinerole-not-found')); + } + if (antiGriefConfig['enabled'] && ['warn', 'mute', 'kick', 'ban'].includes(type)) { + const affectedActions = await client.models['moderation']['ModerationAction'].findAll({ + where: { + createdAt: { + [Op.gte]: new Date(new Date().getTime() - parseInt(antiGriefConfig['timeframe']) * 3600000) + }, + type + } + }); + if ((affectedActions.length + 1) > parseInt(antiGriefConfig[`max_${type}`])) { + await moderationAction(client, 'quarantine', {user: client.user}, user, '[ANTI-GRIEF] ' + localize('moderation', 'anti-grief-reason', { + type, + n: antiGriefConfig[`max_${type}`], + h: antiGriefConfig['timeframe'] + })); + return reject(localize('moderation', 'anti-grief-user-message')); + } + } + switch (type) { + case 'mute': + if (!expiringAt) expiringAt = new Date(new Date().getTime() + 1209600000); + await victim.timeout(expiringAt.getTime() - new Date().getTime(), localize('moderation', 'mute-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })); + sendMessage(victim, embedType(expiringAt ? moduleStrings['tmpmute_message'] : moduleStrings['mute_message'], { + '%reason%': reason, + '%user%': formatDiscordUserName(user.user), + '%date%': expiringAt ? formatDate(expiringAt) : null + })); + if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(moduleConfig['changeNicknameOnMute'].split('%nickname%').join(victim.nickname ? victim.nickname : victim.user.username), '[moderation] ' + localize('moderation', 'mute-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })).catch(() => { + }); + break; + case 'unmute': + if (victim.isCommunicationDisabled()) await victim.timeout(null, localize('moderation', 'unmute-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })); + sendMessage(victim, embedType(moduleStrings['unmute_message'], { + '%reason%': reason, + '%user%': formatDiscordUserName(user.user) + })); + if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(victim.user.username, '[moderation] ' + localize('moderation', 'unmute-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })); + break; + case 'quarantine': + if (!victim.roles.cache.get(quarantineRole.id)) { + if (moduleConfig['remove-all-roles-on-quarantine']) { + await victim.roles.set([quarantineRole], '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })).catch(async e => { + client.logger.log(localize('moderation', 'batch-role-remove-failed', {i: victim.id, e})); + for (const role of victim.roles.cache) { // Remove as much roles as possible + await victim.roles.remove(role, '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })).catch((err) => { + client.logger.warn(localize('moderation', 'could-not-remove-role'), { + err, + r: role.id + }); + }); + } + await victim.roles.add(quarantineRole, '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })); + }); + } else await victim.roles.add(quarantineRole, '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })); + sendMessage(victim, embedType(expiringAt ? moduleStrings['tmpquarantine_message'] : moduleStrings['quarantine_message'], { + '%reason%': reason, + '%user%': formatDiscordUserName(user.user), + '%date%': expiringAt ? formatDate(expiringAt) : null + })); + if (victim.voice) await victim.voice.disconnect(localize('moderation', 'quarantine-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })); + if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(moduleConfig['changeNicknameOnQuarantine'].split('%nickname%').join(victim.nickname ? victim.nickname : victim.user.username), '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })).catch(() => { + }); + } + break; + case 'unquarantine': + await victim.roles.remove(quarantineRole, `[moderation] ` + localize('moderation', 'unquarantine-audit-log-reason', {r: reason})); + if (additionalData && moduleConfig['remove-all-roles-on-quarantine']) { + await victim.roles.add(additionalData.roles, `[moderation] ` + localize('moderation', 'unquarantine-audit-log-reason')).catch(async e => { + client.logger.log(localize('moderation', 'batch-role-add-failed', {i: victim.id, e})); + if (additionalData.roles) { + for (const role of additionalData.roles) { // Give as much roles as possible + await victim.roles.add(role, `[moderation] ` + localize('moderation', 'unquarantine-audit-log-reason')).catch((err) => { + client.logger.warn(localize('moderation', 'could-not-add-role'), {err, r: role.id}); + }); + } + } + }); + } + sendMessage(victim, embedType(moduleStrings['unquarantine_message'], { + '%reason%': reason, + '%user%': formatDiscordUserName(user.user) + })); + if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(victim.user.username).catch(() => { + }); + break; + case 'kick': + sendMessage(victim, embedType(moduleStrings['kick_message'], { + '%reason%': reason, + '%user%': formatDiscordUserName(user.user) + })); + if (victim.kickable) await victim.kick('[moderation] ' + localize('moderation', 'kicked-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })); + break; + case 'ban': + if (!victim.notFound) { + await victim.send(embedType(expiringAt ? moduleStrings['tmpban_message'] : moduleStrings['ban_message'], { + '%reason%': reason, + '%user%': formatDiscordUserName(user.user), + '%date%': expiringAt ? formatDate(expiringAt) : null + })).catch(() => { + }); + if (victim.bannable) await victim.ban({ + days: additionalData.days || 0, + reason: '[moderation] ' + localize('moderation', 'banned-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + }) + }); + } else { + victim.user = {}; + victim.user.tag = victim.id; + victim.user.id = victim.id; + await guild.members.ban(victim.id, { + days: additionalData.days || 0, + reason: '[moderation] ' + localize('moderation', 'banned-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + }) + }); + } + break; + case 'warn': + await victim.send(embedType(moduleStrings['warn_message'], { + '%reason%': reason, + '%user%': formatDiscordUserName(user.user) + })).catch(() => { + }); + const warns = await client.models['moderation']['ModerationAction'].findAll({ + where: { + victimID: victim.id, + type: 'warn' + } + }); + if (moduleConfig['automod'][warns.length + 1]) { + const roles = []; + victim.roles.cache.forEach(role => roles.push(role.id)); + moderationAction(client, moduleConfig['automod'][warns.length + 1].split(':')[0], {user: client.user}, victim, `[${localize('moderation', 'auto-mod')}]: ${localize('moderation', 'reached-warns', {w: warns.length + 1})}`, {roles: roles}, moduleConfig['automod'][warns.length + 1].includes(':') ? new Date(new Date().getTime() + durationParser(moduleConfig['automod'][warns.length + 1].split(':')[1])) : null).then(() => { + }); + } + break; + case 'channel-mute': + await additionalData.channel.permissionOverwrites.edit(victim, {SEND_MESSAGES: false}, { + reason: '[moderation] ' + localize('moderation', 'channelmute-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + }) + }); + await victim.send(embedType(moduleStrings['channel_mute'], { + '%reason%': reason, + '%user%': formatDiscordUserName(user.user), + '%channel%': additionalData.channel.toString() + })).catch(() => { + }); + break; + case 'unchannel-mute': + if (additionalData.channel) await additionalData.channel.permissionOverwrites.delete(victim, { + reason: '[moderation] ' + localize('moderation', 'unchannelmute-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + }) + }); + await victim.send(embedType(moduleStrings['remove-channel_mute'], { + '%reason%': reason, + '%user%': formatDiscordUserName(user.user), + '%channel%': additionalData.channel ? additionalData.channel.toString() : 'Unknown' + })).catch(() => { + }); + break; + case 'unwarn': + break; + case 'unban': + try { + await guild.members.unban(victim, '[moderation] ' + localize('moderation', 'unbanned-audit-log-reason', { + u: formatDiscordUserName(user.user), + r: reason + })); + } catch (e) { + return reject(e); + } + const userid = victim; + const unbannedUser = await client.users.fetch(userid).catch(() => { + }); + victim = {user: unbannedUser, id: userid}; + if (!unbannedUser) { + victim = {}; + victim.user = {}; + victim.user.tag = userid; + victim.user.id = userid; + victim.id = userid; + } + break; + default: + return reject('Option not found'); + } + const modAction = await client.models['moderation']['ModerationAction'].create({ + victimID: victim.id, + memberID: user.id, + reason, + type: type, + additionalData: additionalData, + expiresOn: expiringAt + }); + if (expiringAt) await planExpiringAction(expiringAt, modAction, guild); + let channel = guild.channels.cache.get(moduleConfig['logchannel-id']); + if (!channel) channel = client.logChannel; + if (!channel) { + client.error('[moderation] ' + localize('moderation', 'missing-logchannel')); + } else { + const fields = []; + if (expiringAt) fields.push({ + name: localize('moderation', 'expires-at'), + value: dateToDiscordTimestamp(expiringAt), + inline: true + }); + if (proof) fields.push({ + name: localize('moderation', 'proof'), + value: `[${localize('moderation', 'file')}](${proof.proxyURL || proof.url})`, + inline: true + }); + if (additionalData.channel) fields.push({ + name: localize('moderation', 'channel'), + value: additionalData.channel.toString(), + inline: true + }); + await channel.send({ + // eslint-disable-next-line + embeds: [new MessageEmbed().setColor(expiringAt ? 0xf1c40f : (type.includes('un') ? 0x2ecc71 : 0xe74c3c)).setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }).setTimestamp().setImage(proof ? (proof.proxyURL || proof.url) : null).setAuthor({ + name: formatDiscordUserName(client.user), + iconURL: client + .user.avatarURL() + }).setTitle(`${localize('moderation', 'case')} #${modAction.actionID}`).setThumbnail(client.user.avatarURL()).addField(localize('moderation', 'victim'), `${formatDiscordUserName(victim.user)}\n\`${victim.user.id}\``, true) + .addField('User', `${formatDiscordUserName(user.user)}\n\`${user.user.id}\``, true).addField(localize('moderation', 'action'), expiringAt ? `tmp-${type}` : type, true).addFields(fields).addField(localize('moderation', 'reason'), reason)] + }); + } + const {updateCache} = require('./events/botReady'); + await updateCache(client); + resolve(modAction); + }); +} + +module.exports.moderationAction = moderationAction; + +/** + * Sends a DM ot a user + * @private + * @param {User} user User to send Message to + * @param {Object|String} content Content to send to the user + */ +function sendMessage(user, content) { + user.send(content).catch(() => { + }); +} + +/** + * Plan an expiring moderative action + * @private + * @param {Date} expiringDate Date when action exires + * @param {String} action Type of action + * @param {Guild} guild Guild + * @return {Promise} + */ +async function planExpiringAction(expiringDate, action, guild) { + if (!expiringDate) return; + guild.client.jobs.push(scheduleJob(expiringDate, async () => { + const undoAction = 'un' + action.type; + const undoneModAction = await guild.client.models['moderation']['ModerationAction'].findOne({ + where: { + victimID: action.victimID, + type: undoAction, + createdAt: { + [Op.gte]: action.createdAt + } + } + }); + if (undoneModAction) return; + let member = action.victimID; + if (undoAction !== 'unban') { + member = await guild.members.fetch(action.victimID).catch(() => { + }); + if (!member) return; + } + await moderationAction(guild.client, undoAction, guild.me, member, `[${localize('moderation', 'auto-mod')}] ${localize('moderation', 'action-expired')}`, {roles: action.additionalData.roles}); + })); +} + +module.exports.planExpiringAction = planExpiringAction; \ No newline at end of file diff --git a/modules/moderation/module.json b/modules/moderation/module.json new file mode 100644 index 00000000..f656ec6c --- /dev/null +++ b/modules/moderation/module.json @@ -0,0 +1,32 @@ +{ + "name": "moderation", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "commands-dir": "/commands", + "events-dir": "/events", + "models-dir": "/models", + "config-example-files": [ + "configs/config.json", + "configs/joinGate.json", + "configs/strings.json", + "configs/antiSpam.json", + "configs/antiGrief.json", + "configs/antiJoinRaid.json", + "configs/verification.json" + ], + "tags": [ + "moderation" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/moderation", + "humanReadableName": { + "en": "Moderation & Security", + "de": "Moderation & Sicherheit" + }, + "description": { + "en": "Advanced security- and moderation-system with tons of features", + "de": "Fortgeschrittenes Moderation- und Sicherheit-System mit vielen Funktionen" + } +} \ No newline at end of file diff --git a/modules/moderation/phishingConfig.js b/modules/moderation/phishingConfig.js new file mode 100644 index 00000000..cc5a1453 --- /dev/null +++ b/modules/moderation/phishingConfig.js @@ -0,0 +1,43 @@ +// Central configuration for phishing detection heuristics + +module.exports = { + // list of well-known domains to compare against for typosquatting + legitDomains: [ + 'paypal.com', 'google.com', 'bankofamerica.com', 'apple.com', 'amazon.com', + 'microsoft.com', 'facebook.com', 'twitter.com', 'instagram.com', 'netflix.com', + 'chase.com', 'wellsfargo.com', 'citibank.com', 'usbank.com', 'capitalone.com' + ], + + phishingKeywords: [ + 'login', 'secure', 'account', 'verify', 'update', 'banking', 'password', + 'signin', 'auth', 'recovery', 'billing', 'payment', 'support', 'helpdesk' + ], + + suspiciousTLDs: ['tk', 'ml', 'ga', 'cf', 'gq', 'xyz', 'top', 'club', 'site', 'online'], + + urlShorteners: ['bit.ly', 'tinyurl.com', 'goo.gl', 't.co', 'ow.ly', 'is.gd', 'buff.ly'], + + // scoring thresholds / weights - can be tuned externally + thresholds: { + phishingScore: 50 // score above which we consider a link phishing + }, + + weights: { + ipAddress: 30, + shortener: 20, + typosquatting: 40, + idnHomograph: 35, + idnSimilarity: 20, + suspiciousTld: 15, + keyword: 10, + longUrl: 10, + nonHttps: 25, + dnsFailure: 20, + atSymbol: 30, + encodedChars: 15, + redirect: 20, + googleSafeBrowsing: 50, + virusTotal: 30, + phishTank: 60 + } +}; diff --git a/modules/moderation/phishingHeuristics.js b/modules/moderation/phishingHeuristics.js new file mode 100644 index 00000000..ad665c5b --- /dev/null +++ b/modules/moderation/phishingHeuristics.js @@ -0,0 +1,101 @@ +const stringSimilarity = require('string-similarity'); +const tld = require('tldjs'); +const punycode = require('punycode'); +const { URL } = require('url'); + +// expose basic tld utilities so callers don't have to import tldjs directly +function getDomain(hostname) { + return tld.getDomain ? tld.getDomain(hostname) : hostname; +} +function getSubdomain(hostname) { + return tld.getSubdomain ? tld.getSubdomain(hostname) : ''; +} +function getTld(hostname) { + // tldjs doesn't expose a simple getter in v2; derive manually + const parts = hostname.split('.'); + return parts.length ? parts[parts.length - 1] : ''; +} + +// individual heuristic helpers + +function isIpAddress(hostname) { + return (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname) || /^[\da-f:]+$/i.test(hostname)); +} + +function domainSimilarity(domain, legitDomains, threshold = 0.7) { + for (const legit of legitDomains) { + const sim = stringSimilarity.compareTwoStrings(domain, legit); + if (sim > threshold && domain !== legit) { + return { match: legit, score: sim }; + } + } + return null; +} + +function isIdnHomograph(hostname, legitDomains) { + const puny = punycode.toASCII(hostname); + if (puny !== hostname) { + for (const legit of legitDomains) { + const sim = stringSimilarity.compareTwoStrings(puny.replace(/^xn--/, ''), legit); + if (sim > 0.8) { + return { match: legit, score: sim }; + } + } + return { homograph: true }; + } + return null; +} + +function hasSuspiciousTldOrDeepSubdomain(hostname, suspiciousTLDs) { + const subdomain = getSubdomain(hostname); + const parsedTld = getTld(hostname); + if (subdomain.split('.').length > 2 || suspiciousTLDs.includes(parsedTld)) { + return true; + } + return false; +} + +function containsPhishingKeyword(pathOrQuery, keywords) { + const lower = pathOrQuery.toLowerCase(); + return keywords.filter(kw => lower.includes(kw)); +} + +function isUrlLong(url, length = 100) { + return url.length > length; +} + +function isNonHttps(protocol) { + return protocol !== 'https:'; +} + +function containsAtSymbol(url) { + return url.includes('@'); +} + +function hasEncodedChars(url) { + return /%[0-9A-Fa-f]{2}/.test(url) || /\\x[0-9A-Fa-f]{2}/.test(url); +} + +function extractFirstUrlFromMessage(message) { + const urlRegex = /(https?:\/\/[^\s]+)/g; + const matches = message.match(urlRegex); + return matches ? matches[0] : null; +} + +module.exports = { + isIpAddress, + domainSimilarity, + isIdnHomograph, + hasSuspiciousTldOrDeepSubdomain, + containsPhishingKeyword, + isUrlLong, + isNonHttps, + containsAtSymbol, + hasEncodedChars, + extractFirstUrlFromMessage, + + // tld utilities + getDomain, + getSubdomain, + getTld +}; diff --git a/modules/moderation/phishingService.js b/modules/moderation/phishingService.js new file mode 100644 index 00000000..0c873678 --- /dev/null +++ b/modules/moderation/phishingService.js @@ -0,0 +1,302 @@ +// phishingService.js: core logic extracted from earlier monolith +// dotenv should be configured by application entrypoint (e.g. main.js) +const express = require('express'); +const axios = require('axios'); +const dns = require('dns').promises; // For DNS checks +const { URL } = require('url'); + +// helpers and configuration are separated into modules +const config = require('./phishingConfig'); +const heuristics = require('./phishingHeuristics'); + +// try to load tfjs only if available; otherwise fall back to noop model +let tf; +try { + tf = require('@tensorflow/tfjs-node'); +} catch (e) { + // optional dependency; we'll use dummy model below +} + +// default model loader; tries to load a TensorFlow model if tfjs-node is available, +// otherwise returns a simple logistic regression stub. Users can also inject their own +// model via options to `checkPhishing`. +async function loadModel() { + if (tf && tf.loadLayersModel) { + try { + // placeholder path; in a real deployment you would provide the URL or file + return await tf.loadLayersModel(process.env.PHISH_MODEL_PATH || 'file://model.json'); + } catch (e) { + console.warn('unable to load TF model:', e.message); + // fall through to stub + } + } + + // simple fallback + return { + predict: (features) => { + const sum = features.reduce((a, b) => a + b, 0); + return 1 / (1 + Math.exp(-sum / (features.length || 1))); + } + }; +} + +const app = express(); +app.use(express.json()); + +// customizable patterns and configuration +let customPatterns = []; + +/** + * Update the list of custom patterns from moderation configuration. + * @param {string[]} patterns + */ +function setCustomPatterns(patterns) { + if (Array.isArray(patterns)) customPatterns = patterns; +} + +// API keys are read lazily; store lookup helper +function getApiKey(name) { + return process.env[name] || ''; +} + +async function checkPhishing(input, options = {}) { + // options: { model?, logger?, config? } + const { model: injectedModel, logger = console, config: userCfg = {} } = options; + const cfg = Object.assign({}, config, userCfg); + + // early extraction + let url = input.url || (input.message && heuristics.extractFirstUrlFromMessage(input.message)); + if (!url) { + return { isPhishing: false, riskScore: 0, reasons: ['No URL found'] }; + } + + let parsed; + try { + parsed = new URL(url); + } catch (e) { + return { isPhishing: true, riskScore: 100, reasons: ['Invalid URL format'] }; + } + + const hostname = parsed.hostname; + const domain = heuristics.getDomain(hostname); + const path = parsed.pathname || ''; + const query = parsed.search || ''; + + let riskScore = 0; + const reasons = []; + const features = []; + + // synchronous heuristics + if (heuristics.isIpAddress(hostname)) { + riskScore += cfg.weights.ipAddress; + reasons.push('URL uses IP address (potential obfuscation)'); + features.push(1); + } + + if (cfg.urlShorteners.includes(domain)) { + riskScore += cfg.weights.shortener; + reasons.push('URL shortener detected (often hides phishing)'); + features.push(1); + } + + const typos = heuristics.domainSimilarity(domain, cfg.legitDomains); + if (typos) { + riskScore += cfg.weights.typosquatting; + reasons.push(`Domain similar to ${typos.match} (score ${typos.score})`); + features.push(typos.score); + } + + const idn = heuristics.isIdnHomograph(hostname, cfg.legitDomains); + if (idn) { + riskScore += cfg.weights.idnHomograph; + reasons.push('Internationalized Domain Name (IDN) detected'); + features.push(1); + if (idn.score) { + riskScore += cfg.weights.idnSimilarity; + reasons.push(`Punycode similarity to ${idn.match} (${idn.score})`); + } + } + + if (heuristics.hasSuspiciousTldOrDeepSubdomain(hostname, cfg.suspiciousTLDs)) { + riskScore += cfg.weights.suspiciousTld; + reasons.push('Suspicious TLD or deep subdomain'); + features.push(1); + } + + const hits = heuristics.containsPhishingKeyword(path + query, cfg.phishingKeywords); + hits.forEach(k => { + riskScore += cfg.weights.keyword; + reasons.push(`Phishing keyword '${k}' in URL`); + features.push(1); + }); + + if (heuristics.isUrlLong(url)) { + riskScore += cfg.weights.longUrl; + reasons.push('Excessively long URL'); + features.push(url.length / 100); + } + + if (heuristics.isNonHttps(parsed.protocol)) { + riskScore += cfg.weights.nonHttps; + reasons.push('Non-secure HTTP'); + features.push(1); + } + + if (heuristics.containsAtSymbol(url)) { + riskScore += cfg.weights.atSymbol; + reasons.push('URL contains @'); + features.push(1); + } + + if (heuristics.hasEncodedChars(url)) { + riskScore += cfg.weights.encodedChars; + reasons.push('Encoded characters in URL'); + features.push(1); + } + + // custom patterns + customPatterns.forEach(p => { + try { + const re = new RegExp(p, 'i'); + if (re.test(url) || (input.message && re.test(input.message))) { + riskScore += cfg.weights.shortener; // reuse one weight, could be configurable + reasons.push(`Custom pattern '${p}' matched`); + features.push(1); + } + } catch (_) { + logger.warn(`invalid custom regex: ${p}`); + } + }); + + // asynchronous checks – DNS + remote lookups + const asyncTasks = []; + asyncTasks.push( + dns.resolve(hostname) + .then(arr => { + if (!arr || arr.length === 0) { + riskScore += cfg.weights.dnsFailure; + reasons.push('No DNS resolution'); + } + }) + .catch(() => { + riskScore += cfg.weights.dnsFailure; + reasons.push('DNS resolution failed'); + features.push(1); + }) + ); + + // redirect head check + asyncTasks.push( + axios.head(url, { maxRedirects: 5, timeout: 3000 }) + .then(resp => { + if (resp.request.res.responseUrl && resp.request.res.responseUrl !== url) { + riskScore += cfg.weights.redirect; + reasons.push('URL redirects'); + } + }) + .catch(() => {}) + ); + + // optional external APIs + const safeKey = getApiKey('SAFE_BROWSING_API_KEY'); + if (safeKey) { + asyncTasks.push( + axios.post(`https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${safeKey}`, { + client: { clientId: 'phishingcatcher', clientVersion: '1.0' }, + threatInfo: { + threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING', 'UNWANTED_SOFTWARE', 'POTENTIALLY_HARMFUL_APPLICATION'], + platformTypes: ['ANY_PLATFORM'], + threatEntryTypes: ['URL'], + threatEntries: [{ url }] + } + }) + .then(res => { + if (res.data.matches && res.data.matches.length > 0) { + riskScore += cfg.weights.googleSafeBrowsing; + reasons.push(`Flagged by Google Safe Browsing: ${res.data.matches[0].threatType}`); + } + }) + .catch(err => logger.debug('Safe Browsing error', err)) + ); + } + + const vtKey = getApiKey('VIRUSTOTAL_API_KEY'); + if (vtKey) { + asyncTasks.push( + axios + .get(`https://www.virustotal.com/api/v3/urls/${Buffer.from(url).toString('base64url')}`, { + headers: { 'x-apikey': vtKey } + }) + .then(res => { + const stats = res.data.data.attributes.last_analysis_stats || {}; + const mal = stats.malicious || 0; + const susp = stats.suspicious || 0; + if (mal + susp > 0) { + riskScore += cfg.weights.virusTotal * (mal + susp); + reasons.push(`VirusTotal: ${mal} malicious, ${susp} suspicious`); + } + }) + .catch(err => logger.debug('VirusTotal error', err)) + ); + } + + const phishKey = getApiKey('PHISHTANK_API_KEY'); + if (phishKey) { + asyncTasks.push( + axios + .post('https://checkurl.phishtank.com/checkurl/', { + format: 'json', + url, + app_key: phishKey + }) + .then(res => { + if (res.data.results && res.data.results.in_database && res.data.results.verified && res.data.results.valid) { + riskScore += cfg.weights.phishTank; + reasons.push('Confirmed phishing by PhishTank'); + } + }) + .catch(err => logger.debug('PhishTank error', err)) + ); + } + + // wait for all async checks + await Promise.all(asyncTasks); + + // run ML model if provided or default + const realModel = injectedModel || (await loadModel()); + const mlProb = realModel.predict(features); + riskScore += mlProb * 50; + + riskScore = Math.min(Math.max(riskScore, 0), 100); + + const isPhish = riskScore > (cfg.thresholds.phishingScore || 50); + return { isPhishing: isPhish, riskScore, reasons }; +} + +app.post('/analyze', async (req, res) => { + const input = req.body; // { url: '...' } or { message: '...' } + if (!input.url && !input.message) return res.status(400).json({ error: 'URL or message required' }); + + const result = await checkPhishing(input); + if (result.isPhishing) { + console.log(`Phishing caught: ${input.url || input.message} - Score: ${result.riskScore}`); + // Alert: Integrate Slack, email, or DB logging here + // e.g., await sendAlert(result); + } + + res.json(result); +}); + +// only start the HTTP service when this file is executed directly +if (require.main === module) { + const PORT = process.env.PORT || 3000; + app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); +} + +// export helpers and configuration so the moderation module can configure and reuse logic +module.exports = { + app, + checkPhishing, + setCustomPatterns, + config +}; diff --git a/modules/nicknames/configs/config.json b/modules/nicknames/configs/config.json new file mode 100644 index 00000000..000397da --- /dev/null +++ b/modules/nicknames/configs/config.json @@ -0,0 +1,28 @@ +{ + "description": { + "en": "Configure the function of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "forceDisplayname", + "humanName": { + "en": "Force display name", + "de": "Anzeigenamen erzwingen" + }, + "default": { + "en": false + }, + "description": { + "en": "Use display names of users instead of custom nicknames.", + "de": "Anzeigenamen von Benutzern anstelle von benutzerdefinierten Nicknamen verwenden." + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/nicknames/configs/strings.json b/modules/nicknames/configs/strings.json new file mode 100644 index 00000000..343e8739 --- /dev/null +++ b/modules/nicknames/configs/strings.json @@ -0,0 +1,59 @@ +{ + "description": { + "en": "Set a prefixes and/or suffixes for roles.", + "de": "Setze Präfixe und/oder Suffixe für Rollen." + }, + "humanName": { + "en": "Roles", + "de": "Rollen" + }, + "filename": "strings.json", + "configElements": true, + "content": [ + { + "name": "roleID", + "humanName": { + "en": "Role", + "de": "Rolle" + }, + "default": { + "en": "" + }, + "description": { + "en": "The role you want to set a prefix/suffix for.", + "de": "Die Rolle, für die ein Präfix/Suffix vergeben werden soll." + }, + "type": "roleID" + }, + { + "name": "prefix", + "humanName": { + "en": "Prefix", + "de": "Präfix" + }, + "default": { + "en": "" + }, + "description": { + "en": "The Prefix to be set.", + "de": "Das Präfix." + }, + "type": "string" + }, + { + "name": "suffix", + "humanName": { + "en": "Suffix", + "de": "Suffix" + }, + "default": { + "en": "" + }, + "description": { + "en": "The Suffix to be set.", + "de": "Das Suffix." + }, + "type": "string" + } + ] +} \ No newline at end of file diff --git a/modules/nicknames/events/botReady.js b/modules/nicknames/events/botReady.js new file mode 100644 index 00000000..f1c7cafb --- /dev/null +++ b/modules/nicknames/events/botReady.js @@ -0,0 +1,8 @@ +const {renameMember} = require('../renameMember'); + +module.exports.run = async function (client) { + for (const member of client.guild.members.cache.values()) { + await renameMember(client, member); + } + +} \ No newline at end of file diff --git a/modules/nicknames/events/guildMemberUpdate.js b/modules/nicknames/events/guildMemberUpdate.js new file mode 100644 index 00000000..9cc9750a --- /dev/null +++ b/modules/nicknames/events/guildMemberUpdate.js @@ -0,0 +1,11 @@ +const {renameMember} = require('../renameMember'); + +module.exports.run = async function (client, oldGuildMember, newGuildMember) { + + if (!client.botReadyAt) return; + if (newGuildMember.guild.id !== client.guild.id) return; + if (newGuildMember.nickname === oldGuildMember.nickname && newGuildMember.roles.cache.size === oldGuildMember.roles.cache.size) return; + + await renameMember(client, newGuildMember); + +}; diff --git a/modules/nicknames/models/User.js b/modules/nicknames/models/User.js new file mode 100644 index 00000000..b2f191af --- /dev/null +++ b/modules/nicknames/models/User.js @@ -0,0 +1,22 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class User extends Model { + static init(sequelize) { + return super.init({ + userID: { + type: DataTypes.STRING, + primaryKey: true + }, + nickname: DataTypes.JSON + }, { + tableName: 'nicknames_User', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'User', + 'module': 'nicknames' +}; \ No newline at end of file diff --git a/modules/nicknames/module.json b/modules/nicknames/module.json new file mode 100644 index 00000000..1023e56f --- /dev/null +++ b/modules/nicknames/module.json @@ -0,0 +1,26 @@ +{ + "name": "nicknames", + "humanReadableName": { + "en": "Role-Nicknames", + "de": "Rollen-Nicknamen" + }, + "author": { + "name": "hfgd", + "link": "https://github.com/hfgd123", + "scnxOrgID": "2" + }, + "openSourceURL": "https://github.com/hfgd123/CustomDCBot/tree/main/modules/nicknames", + "events-dir": "/events", + "models-dir": "/models", + "config-example-files": [ + "configs/strings.json", + "configs/config.json" + ], + "tags": [ + "community" + ], + "description": { + "en": "Simple module to edit user nicknames based on roles!", + "de": "Einfaches Modul, um die Nicknames von Nutzern basierend auf ihren Rollen zu bearbeiten!" + } +} \ No newline at end of file diff --git a/modules/nicknames/renameMember.js b/modules/nicknames/renameMember.js new file mode 100644 index 00000000..9e047319 --- /dev/null +++ b/modules/nicknames/renameMember.js @@ -0,0 +1,73 @@ +const {truncate} = require('../../src/functions/helpers'); +const {localize} = require('../../src/functions/localize'); + +renameMember = async function (client, guildMember) { + const roles = client.configurations['nicknames']['strings']; + const config = client.configurations['nicknames']['config']; + const moduleModel = client.models['nicknames']['User']; + + let forceDisplayname = config['forceDisplayname']; + let rolePrefix = ''; + let roleSuffix = ''; + let userRoles = guildMember.roles.cache.sort((a, b) => b.position - a.position).map(r => r.id); + for (const userRole of userRoles) { + let role = roles.find(r => r.roleID === userRole); + if (role) { + rolePrefix = role.prefix; + roleSuffix = role.suffix; + break; + } + } + + + let user = await moduleModel.findOne({ + attributes: ['userID', 'nickname'], + where: { + userID: guildMember.id + } + }); + let memberName; + if (!guildMember.nickname || forceDisplayname) { + memberName = guildMember.user.displayName; + } else { + memberName = guildMember.nickname; + } + + for (const role of roles) { + if (memberName.startsWith(role.prefix)) { + memberName = memberName.replace(role.prefix, ''); + } + if (memberName.endsWith(role.suffix)) { + memberName = memberName.replace(role.suffix, ''); + } + } + + if (user) { + if (memberName !== user.nickname) { + user.nickname = memberName; + await user.save(); + } + } else { + await moduleModel.create({ + userID: guildMember.id, + nickname: memberName + }); + + } + + if (guildMember.displayName === truncate(rolePrefix + memberName, 32-roleSuffix.length).concat(roleSuffix)) return; + if (guildMember.guild.ownerId === guildMember.id) { + client.logger.error('[nicknames] ' + localize('nicknames', 'owner-cannot-be-renamed', {u: guildMember.user.username})) + return; + } + if (guildMember.guild.ownerId === guildMember.id) { + client.logger.error('[nicknames] ' + localize('nicknames', 'owner-cannot-be-renamed', {u: guildMember.user.username})) + return; + } + try { + await guildMember.setNickname(truncate(rolePrefix + memberName, 32-roleSuffix.length).concat(roleSuffix)); + } catch (e) { + client.logger.error('[nicknames] ' + localize('nicknames', 'nickname-error', {u: guildMember.user.username, e: e})) + } +} +module.exports.renameMember = renameMember; \ No newline at end of file diff --git a/modules/partner-list/commands/partner.js b/modules/partner-list/commands/partner.js new file mode 100644 index 00000000..847aeecf --- /dev/null +++ b/modules/partner-list/commands/partner.js @@ -0,0 +1,231 @@ +const {embedType, truncate} = require('../../../src/functions/helpers'); +const {generatePartnerList} = require('../partnerlist'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.beforeSubcommand = async function (interaction) { + await interaction.deferReply({ephemeral: true}); +}; + +module.exports.subcommands = { + 'add': async function (interaction) { + const moduleConf = interaction.client.configurations['partner-list']['config']; + if (moduleConf['category-roles'][interaction.options.getString('category')]) { + const owner = await interaction.guild.members.fetch(interaction.options.getUser('owner')); + await owner.roles.add(moduleConf['category-roles'][interaction.options.getString('category')]).catch(() => { + interaction.client.logger.error('[partner-list] ' + localize('partner-list', 'could-not-give-role', {u: owner.user.id})); + }); + } + if (moduleConf.sendNotificationToPartner) { + interaction.options.getUser('owner').send(embedType(moduleConf['newPartnerDM'], { + '%name%': interaction.options.getString('name'), + '%category%': interaction.options.getString('category') + })).catch(() => { + }); + } + await interaction.client.models['partner-list']['Partner'].create({ + invLink: interaction.options.getString('invite-url'), + teamUserID: interaction.user.id, + userID: interaction.options.getUser('owner').id, + name: interaction.options.getString('name'), + category: interaction.options.getString('category') + }); + await generatePartnerList(interaction.client); + }, + 'delete': async function (interaction) { + const partner = await interaction.client.models['partner-list']['Partner'].findOne({ + where: { + id: interaction.options.getString('id') + } + }); + if (!partner) { + interaction.returnEarly = true; + return interaction.editReply({ + content: localize('partner-list', 'partner-not-found') + }); + } + + const moduleConf = interaction.client.configurations['partner-list']['config']; + const member = await interaction.guild.members.fetch(partner.userID).catch(() => { + }); + + if (member && moduleConf['category-roles'][partner.category]) await member.roles.remove(moduleConf['category-roles'][partner.category]).catch(() => { + interaction.client.logger.error('[partner-list] ' + localize('partner-list', 'could-not-remove-role', {u: member.user.id})); + }); + if (member && moduleConf.sendNotificationToPartner) await member.user.send(embedType(moduleConf.byePartnerDM, { + '%name%': partner.name, + '%category%': partner.category + })).catch(() => {}); + + await partner.destroy(); + await generatePartnerList(interaction.client); + }, + 'edit': async function (interaction) { + const partner = await interaction.client.models['partner-list']['Partner'].findOne({ + where: { + id: interaction.options.getString('id') + } + }); + if (!partner) { + interaction.returnEarly = true; + return interaction.editReply({ + content: localize('partner-list', 'partner-not-found') + }); + } + const moduleConf = interaction.client.configurations['partner-list']['config']; + if (interaction.options.getString('name')) partner.name = interaction.options.getString('name'); + if (interaction.options.getString('invite-url')) partner.invLink = interaction.options.getString('invite-url'); + if (interaction.options.getUser('staff')) partner.teamUserID = interaction.options.getUser('staff').id; + if (interaction.options.getUser('owner')) partner.userID = interaction.options.getUser('owner').id; + if (interaction.options.getString('category')) { + const member = await interaction.guild.members.fetch(partner.userID).catch(() => { + }); + if (member && moduleConf['category-roles'][partner.category]) await member.roles.remove(moduleConf['category-roles'][partner.category]).catch(() => { + interaction.client.logger.error('[partner-list] ' + localize('partner-list', 'could-not-remove-role', {u: member.user.id})); + }); + partner.category = interaction.options.getString('category'); + if (member && moduleConf['category-roles'][partner.category]) await member.roles.add(moduleConf['category-roles'][partner.category]).catch(() => { + interaction.client.logger.error('[partner-list] ' + localize('partner-list', 'could-not-give-role', {u: member.user.id})); + }); + } + + await partner.save(); + await generatePartnerList(interaction.client); + } +}; + +module.exports.autoComplete = { + 'edit': { + 'id': autoCompletePartnerID + }, + 'delete': { + 'id': autoCompletePartnerID + } +}; + +/** + * @private + * Run autocomplete on options with partner id + * @param {Interaction} interaction + * @return {Promise} + */ +async function autoCompletePartnerID(interaction) { + const partnerList = await interaction.client.models['partner-list']['Partner'].findAll({ + order: [['createdAt', 'DESC']] + }); + const matches = []; + interaction.value = interaction.value.toLowerCase(); + for (const match of partnerList.filter(p => p.id.toString().includes(interaction.value) || p.name.toLowerCase().includes(interaction.value) || p.category.toLowerCase().includes(interaction.value))) { + if (matches.length !== 25) matches.push({ + value: match.id.toString(), + name: truncate(`${match.category}: ${match.name}`, 100) + }); + } + interaction.respond(matches); +} + +module.exports.run = async function (interaction) { + if (!interaction.returnEarly) await interaction.editReply({content: ':+1: ' + localize('partner-list', 'successful-edit')}); +}; + +module.exports.config = { + name: 'partner', + description: localize('partner-list', 'command-description'), + + defaultMemberPermissions: ['MANAGE_MESSAGES'], + options: function (client) { + const cats = []; + for (const category of client.configurations['partner-list']['config']['categories']) { + cats.push({name: category, value: category}); + } + return [ + { + type: 'SUB_COMMAND', + name: 'add', + description: localize('partner-list', 'padd-description'), + options: [ + { + type: 'STRING', + name: 'name', + required: true, + description: localize('partner-list', 'padd-name-description') + }, + { + type: 'STRING', + name: 'category', + required: true, + description: localize('partner-list', 'padd-category-description'), + choices: cats + }, + { + type: 'USER', + name: 'owner', + required: true, + description: localize('partner-list', 'padd-owner-description') + }, + { + type: 'STRING', + name: 'invite-url', + required: true, + description: localize('partner-list', 'padd-inviteurl-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'edit', + description: localize('partner-list', 'pedit-description'), + options: [ + { + type: 'STRING', + required: true, + name: 'id', + autocomplete: true, + description: localize('partner-list', 'pedit-id-description') + }, + { + type: 'STRING', + name: 'name', + description: localize('partner-list', 'pedit-name-description') + }, + { + type: 'STRING', + name: 'invite-url', + description: localize('partner-list', 'pedit-inviteurl-description') + }, + { + type: 'STRING', + name: 'category', + choices: cats, + description: localize('partner-list', 'pedit-category-description') + }, + { + type: 'USER', + name: 'owner', + description: localize('partner-list', 'pedit-owner-description') + }, + { + + + type: 'USER', + name: 'staff', + description: localize('partner-list', 'pedit-staff-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'delete', + description: localize('partner-list', 'pdelete-description'), + options: [ + { + type: 'STRING', + name: 'id', + autocomplete: true, + description: localize('partner-list', 'pdelete-id-description'), + required: true + } + ] + } + ]; + } +}; \ No newline at end of file diff --git a/modules/partner-list/config.json b/modules/partner-list/config.json new file mode 100644 index 00000000..1ec295fd --- /dev/null +++ b/modules/partner-list/config.json @@ -0,0 +1,218 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "commandsWarnings": { + "normal": [ + "/partner" + ] + }, + "content": [ + { + "name": "channelID", + "humanName": { + "de": "Kanal", + "en": "Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel in which the partner-list lives", + "de": "Kanal, in welchem die Partner-Liste sein wird" + }, + "type": "channelID" + }, + { + "name": "embed", + "humanName": { + "en": "Partner-List-Embed" + }, + "default": { + "en": { + "title": "Our partners", + "description": "You can find all of our partners here - If you want to be one of our partners message a staff member!", + "partner-string": "#%id%: [%name%](%invite%) (<@%userID%>)", + "color": "GREEN" + }, + "de": { + "title": "Unsere Partner", + "description": "Hier findest du alles über unsere Partner - Wenn du selbst Partner werden möchtest kontaktiere eins unserer Teammitglieder!", + "partner-string": "#%id%: [%name%](%invite%) (<@%userID%>)", + "color": "GREEN" + } + }, + "description": { + "en": "Configuration of the partnership-embed", + "de": "Konfiguration des Partner-Embeds" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true, + "params": [ + { + "name": "invite", + "description": { + "en": "Configured invite to the partner-server (only for \"partner-string\" field)", + "de": "Konfigurierter Invite des Partner-Servers (nur für \"partner-string\" Feld)" + } + }, + { + "name": "name", + "description": { + "en": "Configured name to the partner-server (only for \"partner-string\" field)", + "de": "Konfigurierter Name des Partner-Servers (nur für \"partner-string\" Feld)" + } + }, + { + "name": "userID", + "description": { + "en": "Configured owner-ID to the partner-server (only for \"partner-string\" field)", + "de": "Konfigurierter Owner-ID des Partner-Servers (nur für \"partner-string\" Feld)" + } + }, + { + "name": "teamMemberID", + "description": { + "en": "User who added this partner-server (only for \"partner-string\" field)", + "de": "ID des Nutzers, der den Partner-Server eingetragen hat (nur für \"partner-string\" Feld)" + } + } + ] + }, + { + "name": "categories", + "humanName": { + "en": "Categories", + "de": "Kategorien" + }, + "default": { + "en": [ + "Normal Partners", + "Kooperation", + "Small Partners" + ], + "de": [ + "Normale Partner", + "Kooperation", + "Kleine Partner" + ] + }, + "description": { + "en": "Please specify each category here", + "de": "Bitte liste jede Kategorie hier auf" + }, + "type": "array", + "content": "string" + }, + { + "name": "category-roles", + "humanName": { + "en": "Category-Roles", + "de": "Kategorie-Rollen" + }, + "default": { + "en": {}, + "de": {} + }, + "description": { + "en": "(optional) Role which should be given for a partner in a specific category", + "de": "(optional) Rolle welche Partner in einer bestimmten Kategorie gegeben werden soll" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "roleID" + } + }, + { + "name": "sendNotificationToPartner", + "humanName": { + "en": "Send Partner-Notifications?", + "de": "Partner-Benachrichtigung senden?" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, the bot is going to send a DM to the partner when they get added or removed", + "de": "Wenn aktiviert, sendet der Bot eine PN an Partner, wenn sie hinzugefügt oder entfernt werden" + }, + "type": "boolean" + }, + { + "name": "newPartnerDM", + "dependsOn": "sendNotificationToPartner", + "humanName": { + "de": "Partner-Willkommens-PN", + "en": "Partner-Welcome-DM" + }, + "default": { + "en": "Hello, Hello! You are now a partner - congratulations", + "de": "Hi. Du bist jetzt Partner - Herzlichen Glückwunsch" + }, + "description": { + "en": "This message gets send to new partners.", + "de": "Diese Nachricht wird an neue Partner gesendet." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "name", + "description": { + "en": "Name of the added partner", + "de": "Name des hinzugefügten Partners" + } + }, + { + "name": "category", + "description": { + "en": "Category of the partner", + "de": "Kategorie des Partners" + } + } + ] + }, + { + "name": "byePartnerDM", + "dependsOn": "sendNotificationToPartner", + "humanName": { + "de": "Partner-Entfernung-PN", + "en": "Partner-Removal-DM" + }, + "default": { + "en": "Sorry, but you are no longer a partner ):", + "de": "Leider bist du nicht länger Partner ):" + }, + "description": { + "en": "This message gets send to the partner when they get removed.", + "de": "Diese Nachricht wird an den Partner gesendet, wenn dieser entfernt wird." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "name", + "description": { + "en": "Name of the added partner", + "de": "Name des hinzugefügten Partners" + } + }, + { + "name": "category", + "description": { + "en": "Category of the partner", + "de": "Kategorie des Partners" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/partner-list/events/botReady.js b/modules/partner-list/events/botReady.js new file mode 100644 index 00000000..73ba5325 --- /dev/null +++ b/modules/partner-list/events/botReady.js @@ -0,0 +1,5 @@ +const {generatePartnerList} = require('../partnerlist'); + +module.exports.run = async function (client) { + await generatePartnerList(client); +}; \ No newline at end of file diff --git a/modules/partner-list/models/Partner.js b/modules/partner-list/models/Partner.js new file mode 100644 index 00000000..12877975 --- /dev/null +++ b/modules/partner-list/models/Partner.js @@ -0,0 +1,27 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class Partner extends Model { + static init(sequelize) { + return super.init({ + id: { + autoIncrement: true, + type: DataTypes.INTEGER, + primaryKey: true + }, + invLink: DataTypes.STRING, + teamUserID: DataTypes.STRING, + userID: DataTypes.STRING, + name: DataTypes.STRING, + category: DataTypes.STRING + }, { + tableName: 'partnerlist_Partner', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'Partner', + 'module': 'partner-list' +}; \ No newline at end of file diff --git a/modules/partner-list/module.json b/modules/partner-list/module.json new file mode 100644 index 00000000..c6c8fb72 --- /dev/null +++ b/modules/partner-list/module.json @@ -0,0 +1,26 @@ +{ + "name": "partner-list", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/partnerlist", + "events-dir": "/events", + "commands-dir": "/commands", + "models-dir": "/models", + "config-example-files": [ + "config.json" + ], + "tags": [ + "administration" + ], + "humanReadableName": { + "en": "Partner-List", + "de": "Partner-Liste" + }, + "description": { + "en": "Manage your partnerships with other guilds easily.", + "de": "Erstelle eine Liste mit allen Partnern deines Servers - nach Kategorie sortiert." + } +} \ No newline at end of file diff --git a/modules/partner-list/partnerlist.js b/modules/partner-list/partnerlist.js new file mode 100644 index 00000000..e518340e --- /dev/null +++ b/modules/partner-list/partnerlist.js @@ -0,0 +1,57 @@ +/** + * Manages the Partner-List-Embed + * @module Partner-List + * @author Simon Csaba + */ +const {MessageEmbed} = require('discord.js'); +const {localize} = require('../../src/functions/localize'); +const {disableModule, truncate} = require('../../src/functions/helpers'); + +/** + * Generate the partner-list embed + * @param {Client} client + * @returns {Promise} + */ +async function generatePartnerList(client) { + const moduleConf = client.configurations['partner-list']['config']; + const channel = await client.channels.fetch(moduleConf['channelID']).catch(() => { + }); + if (!channel) return disableModule('partner-list', localize('partner-list', 'channel-not-found', {c: moduleConf['channelID']})); + const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); + const partners = await client.models['partner-list']['Partner'].findAll({}); + const sortedByCategory = {}; + partners.forEach(partner => { + if (!sortedByCategory[partner.category]) sortedByCategory[partner.category] = []; + sortedByCategory[partner.category].push(partner); + }); + const embed = new MessageEmbed() + .setTitle(moduleConf['embed']['title']) + .setAuthor({name: client.user.username, iconURL: client.user.avatarURL()}) + .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) + .setColor(moduleConf['embed']['color']) + .setDescription(moduleConf['embed']['description']); + moduleConf['categories'].forEach(category => { + if (sortedByCategory[category]) { + let string = ''; + sortedByCategory[category].forEach(partner => { + string = string + moduleConf['embed']['partner-string'].split('%invite%').join(partner.invLink).split('%name%').join(partner.name).split('%userID%').join(partner.userID).split('%id%').join(partner.id).split('%teamMemberID%').join(partner.teamUserID) + '\n'; + }); + embed.addField(category, truncate(string, 1020)); + delete sortedByCategory[category]; + } + }); + for (const category in sortedByCategory) { + let string = ''; + sortedByCategory[category].forEach(partner => { + string = string + moduleConf['embed']['partner-string'].split('%invite%').join(partner.invLink).split('%name%').join(partner.name).split('%userID%').join(partner.userID).split('%id%').join(partner.id).split('%teamMemberID%').join(partner.teamUserID) + '\n'; + }); + embed.addField(category, truncate(string, 1020)); + } + + if (partners.length === 0) embed.addField('ℹ ' + localize('partner-list', 'information'), localize('partner-list', 'no-partners')); + + if (messages.last()) await messages.last().edit({embeds: [embed]}); + else channel.send({embeds: [embed]}); +} + +module.exports.generatePartnerList = generatePartnerList; \ No newline at end of file diff --git a/modules/ping-on-vc-join/actual-config.json b/modules/ping-on-vc-join/actual-config.json new file mode 100644 index 00000000..7865aead --- /dev/null +++ b/modules/ping-on-vc-join/actual-config.json @@ -0,0 +1,46 @@ +{ + "description": { + "en": "Configure messages that should get send when a user joins a Voice-Channel", + "de": "Stelle hier Nachrichten ein, die versendet werden, wenn ein Nutzer einem Sprachkanal beitritt" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "actual-config.json", + "content": [ + { + "name": "assignRoleToUsersInVoiceChannels", + "humanName": { + "en": "Assign roles to members connected to voice channels?", + "de": "Nutzer, die mit Sprachkanälen verbunden sind, Rollen zuweisen?" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, users will receive a role when they join a voice channel. This role will be removed when they leave the voice channel (switching voice channels does not trigger a role removal).", + "de": "Wenn aktiviert, werden Nutzer beim Beitritt eines Sprachkanals eine Rolle erhalten. Diese Rolle wird entfernt, wenn sie den Sprachkanal verlassen (Sprachkanäle wechseln zählt nicht)." + }, + "type": "boolean" + }, + { + "name": "voiceRoles", + "dependsOn": "assignRoleToUsersInVoiceChannels", + "humanName": { + "en": "Roles for users that are connected to voice channels", + "de": "Nutzer, die mit Sprachkanälen verbunden sind, Rollen zuweisen?" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Users that are currently connected to a voice channel will be assigned these roles.", + "de": "Nutzer, die aktuell mit einem Sprachkanal verbunden sind, erhalten diese Rolen." + }, + "type": "array", + "content": "roleID" + } + ] +} \ No newline at end of file diff --git a/modules/ping-on-vc-join/config.json b/modules/ping-on-vc-join/config.json new file mode 100644 index 00000000..bd871fac --- /dev/null +++ b/modules/ping-on-vc-join/config.json @@ -0,0 +1,127 @@ +{ + "description": { + "en": "Configure messages that should get send when a user joins a Voice-Channel", + "de": "Stelle hier Nachrichten ein, die versendet werden, wenn ein Nutzer einem Sprachkanal beitritt" + }, + "humanName": { + "en": "Message on Voice Join", + "de": "Nachricht beim Kanalbeitritt" + }, + "filename": "config.json", + "configElements": true, + "content": [ + { + "name": "channels", + "humanName": { + "en": "Channels", + "de": "Auslöserkanäle" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Channel-ID in which this messages should get triggered", + "de": "Kanäle, bei denen der Bot reagieren soll, wenn ein Nutzer joint" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "message", + "humanName": { + "de": "Nachricht", + "en": "Message" + }, + "default": { + "en": "The user %tag% joined the voicechat %vc%", + "de": "Der Nutzer %tag% ist dem Voicechat %vc% beigetreten." + }, + "description": { + "en": "Here you can set the message that should be send if someone joins a selected voicechat", + "de": "Hier kannst du die Nachricht einstellen, die gesendet werden soll, wenn jemand dem Sprachkanal beitritt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "vc", + "description": { + "en": "Name of the voicechat", + "de": "Name des Sprackkanals" + } + }, + { + "name": "mention", + "description": { + "en": "Mention of the user", + "de": "Erwähnung des Nutzers" + } + } + ] + }, + { + "name": "notify_channel_id", + "humanName": { + "de": "Benachrichtigungskanal", + "en": "Notification-Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel where the message should be send", + "de": "Kanal, in welchen die Nachricht gesendet werden soll" + }, + "type": "channelID" + }, + { + "name": "send_pn_to_member", + "humanName": { + "en": "Join-DM", + "de": "Join-PN" + }, + "default": { + "en": false + }, + "description": { + "en": "Should the bot send a PN to the member?", + "de": "Soll der Bot eine PN an den Nutzer schicken?" + }, + "type": "boolean" + }, + { + "name": "pn_message", + "humanName": { + "en": "Join-DM-Message", + "de": "Join-PN-Nachricht" + }, + "default": { + "en": "Hi, I saw you joined the voice chat %vc%. Nice (;", + "de": "Hi, ich habe gesehen, dass du %vc% beigetreten bist. Nice (;" + }, + "description": { + "de": "Diese Nachricht wird an den Nutzer versandt, wenn er einem Voicechat beitritt (wenn \"Join-PN\" aktiviert ist)." + }, + "type": "string", + "dependsOn": "send_pn_to_member", + "allowEmbed": true, + "params": [ + { + "name": "vc", + "description": { + "en": "Name of the voicechat", + "de": "Name des Sprachkanals" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/ping-on-vc-join/events/voiceStateUpdate.js b/modules/ping-on-vc-join/events/voiceStateUpdate.js new file mode 100644 index 00000000..56ec57cd --- /dev/null +++ b/modules/ping-on-vc-join/events/voiceStateUpdate.js @@ -0,0 +1,52 @@ +const {embedType, disableModule, formatDiscordUserName} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +const cooldown = new Set(); + +exports.run = async (client, oldState, newState) => { + if (!client.botReadyAt) return; + const roleConfig = client.configurations['ping-on-vc-join']['actual-config']; + if (roleConfig.assignRoleToUsersInVoiceChannels && roleConfig.voiceRoles.length !== 0) { + console.log(oldState.guildId, newState.guildId); + if (oldState.channel && !newState.channel) newState.member.roles.remove(roleConfig.voiceRoles); + if (!oldState.channel && newState.channel) newState.member.roles.add(roleConfig.voiceRoles); + } + if (!newState.channel) return; + const channel = await client.channels.fetch(newState.channelId); + if (channel.guild.id !== client.guild.id) return; + + const moduleConfig = client.configurations['ping-on-vc-join']['config']; + + const configElement = moduleConfig.find(e => e.channels.includes(channel.id)); + if (!configElement) return; + const member = await client.guild.members.fetch(newState.id); + if (member.user.bot) return; + + if (cooldown.has(member.user.id)) return; + + const notifyChannel = newState.guild.channels.cache.get(configElement['notify_channel_id']); + if (!notifyChannel) return disableModule('partner-list', localize('ping-on-vc-join', 'channel-bot-found', {c: configElement['notify_channel_id']})); + + setTimeout(async () => { // Wait 3 seconds before pinging a role + if (!member.voice) return; + if (member.voice.channelId !== channel.id) return; + await notifyChannel.send(embedType(configElement['message'], { + '%vc%': channel.name, + '%tag%': formatDiscordUserName(member.user), + '%mention%': `<@${member.user.id}>` + })); + + cooldown.add(member.user.id); + setTimeout(() => { + cooldown.delete(member.user.id); + }, 300000); // 5 min + + if (configElement['send_pn_to_member']) { + await member.send(embedType(configElement['pn_message'], { + '%vc%': channel.name + })).catch(() => { + client.logger.info(`[ping-on-vc-join] ` + localize('ping-on-vc-join', 'could-not-send-pn', {m: member.user.id})); + }); + } + }, 3000); +}; \ No newline at end of file diff --git a/modules/ping-on-vc-join/module.json b/modules/ping-on-vc-join/module.json new file mode 100644 index 00000000..9e944642 --- /dev/null +++ b/modules/ping-on-vc-join/module.json @@ -0,0 +1,25 @@ +{ + "name": "ping-on-vc-join", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/ping-on-vc-join", + "events-dir": "/events", + "config-example-files": [ + "config.json", + "actual-config.json" + ], + "tags": [ + "support" + ], + "humanReadableName": { + "en": "Voice-Channel Actions", + "de": "Sprachkanal-Aktionen" + }, + "description": { + "en": "Sends messages when someone joins a voicechat and assign roles to users in Voice-Channels", + "de": "Sende Nachrichten, wenn jemand einem Sprachkanal beitritt und vergebe Rollen an Nutzer in Sprachkanälen" + } +} \ No newline at end of file diff --git a/modules/polls/commands/poll.js b/modules/polls/commands/poll.js new file mode 100644 index 00000000..575af41f --- /dev/null +++ b/modules/polls/commands/poll.js @@ -0,0 +1,153 @@ +const {truncate} = require('../../../src/functions/helpers'); +const durationParser = require('parse-duration'); +const {localize} = require('../../../src/functions/localize'); +const {createPoll, updateMessage} = require('../polls'); + +module.exports.subcommands = { + 'create': async function (interaction) { + if (interaction.options.getChannel('channel', true).type !== 'GUILD_TEXT') interaction.reply({ + content: '⚠️ ' + localize('polls', 'not-text-channel'), + ephemeral: true + }); + let endAt; + if (interaction.options.getString('duration')) endAt = new Date(new Date().getTime() + durationParser(interaction.options.getString('duration'))); + const options = []; + for (let step = 1; step <= 10; step++) { + if (interaction.options.getString(`option${step}`)) options.push(interaction.options.getString(`option${step}`)); + } + await createPoll({ + description: (interaction.options.getBoolean('public') ? '[PUBLIC]' : '') + interaction.options.getString('description', true), + channel: interaction.options.getChannel('channel', true), + endAt: endAt, + options + }, interaction.client); + interaction.reply({ + content: localize('polls', 'created-poll', {c: interaction.options.getChannel('channel').toString()}), + ephemeral: true + }); + }, + 'end': async function (interaction) { + const poll = await interaction.client.models['polls']['Poll'].findOne({ + where: { + messageID: interaction.options.getString('msg-id') + } + }); + if (!poll) return interaction.reply({ + content: '⚠️ ' + localize('polls', 'not-found'), + ephemeral: true + }); + poll.expiresAt = new Date(); + await poll.save(); + await updateMessage(await interaction.guild.channels.cache.get(poll.channelID), poll, interaction.options.getString('msg-id')); + interaction.reply({ + content: localize('polls', 'ended-poll'), + ephemeral: true + }); + } +}; + +module.exports.autoComplete = { + 'end': { + 'msg-id': async function(interaction) { + const polls = []; + const allPolls = await interaction.client.models['polls']['Poll'].findAll(); + for (const poll of allPolls) { + if (!poll.expiresAt) { + polls.push(poll); + continue; + } + if (poll.expiresAt && new Date(poll.expiresAt).getTime() > new Date().getTime()) polls.push(poll); + } + interaction.value = interaction.value.toLowerCase(); + const returnValue = []; + for (const poll of polls.filter(p => p.description.toLowerCase().includes(interaction.value) || p.messageID.toString().includes(interaction.value))) { + if (returnValue.length !== 25) returnValue.push({ + value: poll.messageID, + name: truncate(`#${(interaction.client.guild.channels.cache.get(poll.channelID) || {name: poll.channelID}).name}: ${poll.description.replaceAll('[PUBLIC]', '')}`, 100) + }); + } + interaction.respond(returnValue); + } + } +}; + +module.exports.config = { + name: 'poll', + defaultMemberPermissions: ['MANAGE_MESSAGES'], + description: localize('polls', 'command-poll-description'), + + options: function () { + const options = [ + { + type: 'SUB_COMMAND', + name: 'create', + description: localize('polls', 'command-poll-create-description'), + options: [{ + type: 'STRING', + name: 'description', + required: true, + maxLength: 4096, + description: localize('polls', 'command-poll-create-description-description') + }, + { + type: 'CHANNEL', + name: 'channel', + required: true, + channelTypes: ['GUILD_TEXT'], + description: localize('polls', 'command-poll-create-channel-description') + }, + { + type: 'STRING', + name: 'option1', + required: true, + maxLength: 100, + description: localize('polls', 'command-poll-create-option-description', {o: 1}) + }, + { + type: 'STRING', + name: 'option2', + required: true, + maxLength: 100, + description: localize('polls', 'command-poll-create-option-description', {o: 2}) + }, + { + type: 'STRING', + name: 'duration', + required: false, + description: localize('polls', 'command-poll-create-endAt-description') + }, + { + type: 'BOOLEAN', + name: 'public', + required: false, + description: localize('polls', 'command-poll-create-public-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'end', + description: localize('polls', 'command-poll-end-description'), + options: [ + { + type: 'STRING', + name: 'msg-id', + required: true, + autocomplete: true, + description: localize('polls', 'command-poll-end-msgid-description') + } + ] + } + ]; + for (let step = 1; step <= 7; step++) { + options[0].options.push({ + type: 'STRING', + name: `option${2 + step}`, + required: false, + maxLength: 100, + description: localize('polls', 'command-poll-create-option-description', {o: 2 + step}) + }); + } + return options; + } +}; \ No newline at end of file diff --git a/modules/polls/configs/config.json b/modules/polls/configs/config.json new file mode 100644 index 00000000..63b26001 --- /dev/null +++ b/modules/polls/configs/config.json @@ -0,0 +1,59 @@ +{ + "description": { + "en": "Configure the function of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "commandsWarnings": { + "normal": [ + "/poll" + ] + }, + "content": [ + { + "name": "reactions", + "humanName": { + "de": "Emojis", + "en": "Emojis" + }, + "default": { + "en": { + "1": "1️⃣", + "2": "2️⃣", + "3": "3️⃣", + "4": "4️⃣", + "5": "5️⃣", + "6": "6️⃣", + "7": "7️⃣", + "8": "8️⃣", + "9": "9️⃣" + }, + "de": { + "1": "1️⃣", + "2": "2️⃣", + "3": "3️⃣", + "4": "4️⃣", + "5": "5️⃣", + "6": "6️⃣", + "7": "7️⃣", + "8": "8️⃣", + "9": "9️⃣" + } + }, + "description": { + "en": "You can set the different emojis to use", + "de": "Du kannst die verschiedenen Emojis, die benutzt werden sollen, einstellen" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + } + ] +} \ No newline at end of file diff --git a/modules/polls/configs/strings.json b/modules/polls/configs/strings.json new file mode 100644 index 00000000..4679c9c6 --- /dev/null +++ b/modules/polls/configs/strings.json @@ -0,0 +1,53 @@ +{ + "description": { + "en": "Edit the messages and strings of the module here", + "de": "Stelle hier die Nachrichten des Modules ein" + }, + "humanName": { + "en": "Nachrichten", + "de": "Nachrichten" + }, + "filename": "strings.json", + "content": [ + { + "name": "embed", + "humanName": { + "de": "Embed" + }, + "default": { + "en": { + "title": "New Poll", + "color": "BLUE", + "options": "Today's options", + "liveView": "Live-Views of the results", + "expiresOn": "End of this poll", + "thisPollExpiresOn": "This poll expires on %date%.", + "endedPollTitle": "Poll ended", + "visibility": "Visibility of votes", + "endedPollColor": "RED" + }, + "de": { + "title": "Neue Umfrage", + "color": "BLUE", + "options": "Heutige Auswahlmöglichkeiten", + "liveView": "Live-Anzeige der Ergebnisse", + "expiresOn": "Ende dieser Umfrage", + "visibility": "Sichtbarkeit der Stimmen", + "thisPollExpiresOn": "Diese Umfrage endet am %date%.", + "endedPollTitle": "Umfrage beendet", + "endedPollColor": "RED" + } + }, + "description": { + "en": "You can edit the settings of your embed here", + "de": "Du kannst die Einstellungen des Embeds hier bearbeiten" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + } + ] +} \ No newline at end of file diff --git a/modules/polls/events/botReady.js b/modules/polls/events/botReady.js new file mode 100644 index 00000000..3fb16689 --- /dev/null +++ b/modules/polls/events/botReady.js @@ -0,0 +1,12 @@ +const {updateMessage} = require('../polls'); +const {scheduleJob} = require('node-schedule'); + +module.exports.run = async (client) => { + const polls = await client.models['polls']['Poll'].findAll(); + + polls.forEach(poll => { + if (poll.expiresAt && new Date(poll.expiresAt).getTime() > new Date().getTime()) scheduleJob(new Date(poll.expiresAt), async () => { + await updateMessage(await client.channels.fetch(poll.channelID), poll, poll.messageID); + }); + }); +}; \ No newline at end of file diff --git a/modules/polls/events/interactionCreate.js b/modules/polls/events/interactionCreate.js new file mode 100644 index 00000000..deb6cd8e --- /dev/null +++ b/modules/polls/events/interactionCreate.js @@ -0,0 +1,99 @@ +const {updateMessage} = require('../polls'); +const {localize} = require('../../../src/functions/localize'); +const {MessageEmbed} = require('discord.js'); +const {truncate} = require('../../../src/functions/helpers'); +module.exports.run = async (client, interaction) => { + if (!interaction.message && !(interaction.customId || '').startsWith('polls-rem-vot-')) return; + const poll = await client.models['polls']['Poll'].findOne({ + where: { + messageID: (interaction.customId || '').startsWith('polls-rem-vot-') ? interaction.customId.replaceAll('polls-rem-vot-', '') : (interaction.message || {}).id + } + }); + if (!poll) return; + let expired = false; + if (poll.expiresAt || poll.endAt) { + const date = new Date(poll.expiresAt || poll.endAt); + if (date.getTime() <= new Date().getTime()) expired = true; + } + + if (interaction.isButton() && interaction.customId === 'polls-own-vote') { + let userVoteCat = null; + for (const id in poll.votes) { + if (poll.votes[id].includes(interaction.user.id)) userVoteCat = id; + } + if (!userVoteCat) return interaction.reply({ + content: '⚠️ ' + localize('polls', 'not-voted-yet'), + ephemeral: true + }); + return interaction.reply({ + content: localize('polls', 'you-voted', {o: poll.options[userVoteCat - 1]}) + (!expired ? '\n' + localize('polls', 'change-opinion') : ''), + ephemeral: true, + components: [ + { + type: 'ACTION_ROW', + components: expired ? [] : [ + { + type: 'BUTTON', + style: 'DANGER', + customId: 'polls-rem-vot-' + poll.messageID, + label: '🗑 ' + localize('polls', 'remove-vote') + } + ] + } + ] + }); + } + + if (interaction.isButton() && interaction.customId === 'polls-public-votes') { + if (!poll.description.startsWith('[PUBLIC]')) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('polls', 'not-public') + }); + const embed = new MessageEmbed() + .setTitle(localize('polls', 'view-public-votes')) + .setColor(0xE67E22); + for (const vId in poll.options) { + let voters = []; + for (const voterID of poll.votes[parseInt(vId) + 1] || []) { + voters.push('<@' + voterID + '>'); + } + embed.addField(interaction.client.configurations['polls']['config']['reactions'][parseInt(vId) + 1] + ' ' + poll.options[vId], truncate(voters.join(',') || '*' + localize('polls', 'no-votes-for-this-option') + '*', 1024)); + } + return interaction.reply({ + ephemeral: true, + embeds: [embed] + }); + } + + + if (poll.expiresAt && new Date(poll.expiresAt).getTime() <= new Date().getTime()) return; + if (interaction.isButton() && (interaction.customId || '').startsWith('polls-rem-vot-')) { + const o = poll.votes; + poll.votes = {}; + for (const id in o) { + if (o[(parseInt(id)).toString()] && o[(parseInt(id)).toString()].includes(interaction.user.id)) o[(parseInt(id)).toString()].splice(o[(parseInt(id)).toString()].indexOf(interaction.user.id), 1); + } + poll.votes = o; + await poll.save(); + await updateMessage(interaction.channel, poll, interaction.customId.replaceAll('polls-rem-vot-', '')); + return await interaction.reply({ + content: '✅ ' + localize('polls', 'removed-vote'), + ephemeral: true + }); + } + if (interaction.isSelectMenu() && interaction.customId === 'polls-vote') { + const o = poll.votes; + poll.votes = {}; + for (const id in o) { + if (o[(parseInt(id)).toString()] && o[(parseInt(id)).toString()].includes(interaction.user.id)) o[(parseInt(id)).toString()].splice(o[(parseInt(id)).toString()].indexOf(interaction.user.id), 1); + } + o[(parseInt(interaction.values[0]) + 1).toString()].push(interaction.user.id); + poll.votes = o; + await poll.save(); + await updateMessage(interaction.message.channel, poll, interaction.message.id); + await interaction.reply({ + content: localize('polls', 'voted-successfully'), + ephemeral: true + }); + } +}; \ No newline at end of file diff --git a/modules/polls/models/Poll.js b/modules/polls/models/Poll.js new file mode 100644 index 00000000..cc6e73de --- /dev/null +++ b/modules/polls/models/Poll.js @@ -0,0 +1,26 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class Poll extends Model { + static init(sequelize) { + return super.init({ + messageID: { + type: DataTypes.STRING, + primaryKey: true + }, + description: DataTypes.STRING, // Can start with "[PUBLIC]" to indicate a public poll + options: DataTypes.JSON, + votes: DataTypes.JSON, // {1: ["userIDHere"], 2: ["as"] } + expiresAt: DataTypes.DATE, + channelID: DataTypes.STRING + }, { + tableName: 'polls_Poll', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'Poll', + 'module': 'polls' +}; \ No newline at end of file diff --git a/modules/polls/module.json b/modules/polls/module.json new file mode 100644 index 00000000..b8faf194 --- /dev/null +++ b/modules/polls/module.json @@ -0,0 +1,26 @@ +{ + "name": "polls", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "description": { + "en": "Simple module to create fresh polls on your server!" + }, + "events-dir": "/events", + "commands-dir": "/commands", + "models-dir": "/models", + "config-example-files": [ + "configs/config.json", + "configs/strings.json" + ], + "tags": [ + "community" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/polls", + "humanReadableName": { + "en": "Polls", + "de": "Umfragen" + } +} \ No newline at end of file diff --git a/modules/polls/polls.js b/modules/polls/polls.js new file mode 100644 index 00000000..cb91da9c --- /dev/null +++ b/modules/polls/polls.js @@ -0,0 +1,137 @@ +/** + * Create and manage polls + * @module polls + */ +const {scheduleJob} = require('node-schedule'); +const {MessageEmbed} = require('discord.js'); +const {renderProgressbar, formatDate} = require('../../src/functions/helpers'); +const {localize} = require('../../src/functions/localize'); + +/** + * Creates a new poll + * @param {Object} data Data of the new poll + * @param {Client} client Client + * @return {Promise} + */ +async function createPoll(data, client) { + const votes = {}; + for (const vid in data.options) { + votes[parseInt(vid) + 1] = []; + } + data.votes = votes; + const id = await updateMessage(data.channel, data); + + await client.models['polls']['Poll'].create({ + messageID: id, + description: data.description, + options: data.options, + channelID: data.channel.id, + expiresAt: data.endAt, + votes: votes + }); + + if (data.endAt) { + client.jobs.push(scheduleJob(data.endAt, async () => { + await updateMessage(data.channel, await client.models['polls']['Poll'].findOne({where: {messageID: id}}), id); + })); + } +} + +module.exports.createPoll = createPoll; + +/** + * Updates a poll-message + * @param {TextChannel} channel Channel in which the message is + * @param {Object} data Data-Object (can be DB-Object) + * @param {String} mID ID of already sent message + * @return {Promise<*>} + */ +async function updateMessage(channel, data, mID = null) { + const strings = channel.client.configurations['polls']['strings']; + const config = channel.client.configurations['polls']['config']; + + let m; + if (mID) m = await channel.messages.fetch(mID).catch(() => { + }); + const embed = new MessageEmbed() + .setTitle(strings.embed.title) + .setColor(strings.embed.color) + .setDescription(data.description.replaceAll('[PUBLIC]', '')); + let s = ''; + let p = ''; + let allVotes = 0; + for (const vid in data.votes) { + allVotes = allVotes + data.votes[vid].length; + } + for (const id in data.options) { + if (!data.votes[(parseInt(id) + 1).toString()]) data.votes[(parseInt(id) + 1).toString()] = []; + s = s + `${config.reactions[parseInt(id) + 1]}: ${data.options[id]} \`${data.votes[(parseInt(id) + 1).toString()].length}\`\n`; + const percentage = 100 / allVotes * data.votes[(parseInt(id) + 1).toString()].length; + p = p + `${config.reactions[parseInt(id) + 1]} ` + renderProgressbar(percentage) + ` ${!percentage ? '0' : percentage.toFixed(0)}% (${data.votes[(parseInt(id) + 1).toString()].length}/${allVotes})\n`; + } + embed.addField(strings.embed.options, s); + embed.addField(strings.embed.liveView, p); + embed.addField(strings.embed.visibility, localize('polls', `poll-${data.description.startsWith('[PUBLIC]') ? 'public' : 'private'}`)); + + const options = []; + for (const vId in data.options) { + options.push({ + label: data.options[vId], + value: vId, + description: localize('polls', 'vote-this'), + emoji: config.reactions[parseInt(vId) + 1] + }); + } + let expired = false; + if (data.expiresAt || data.endAt) { + const date = new Date(data.expiresAt || data.endAt); + if (date.getTime() <= new Date().getTime()) { + embed.setColor(strings.embed.endedPollColor); + embed.setTitle(strings.embed.endedPollTitle); + expired = true; + } else { + embed.addField('\u200b', '\u200b'); + embed.addField(strings.embed.expiresOn, strings.embed.thisPollExpiresOn.split('%date%').join(formatDate(date))); + } + } + + const components = [ + /* eslint-disable camelcase */ + { + type: 'ACTION_ROW', + components: [{ + type: 'SELECT_MENU', + disabled: expired, + customId: 'polls-vote', + min_values: 1, + max_values: 1, + placeholder: localize('polls', 'vote'), + options + }] + }, + { + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + customId: 'polls-own-vote', + 'label': localize('polls', 'what-have-i-votet'), + style: 'SUCCESS' + }] + } + ]; + if (data.description.startsWith('[PUBLIC]')) components[1].components.push({ + type: 'BUTTON', + customId: 'polls-public-votes', + label: localize('polls', 'view-public-votes'), + style: 'SECONDARY' + }); + + let r; + if (m) r = await m.edit({embeds: [embed], components}); + else { + r = await channel.send({embeds: [embed], components}); + } + return r.id; +} + +module.exports.updateMessage = updateMessage; \ No newline at end of file diff --git a/modules/quiz/commands/quiz.js b/modules/quiz/commands/quiz.js new file mode 100644 index 00000000..e9c075e1 --- /dev/null +++ b/modules/quiz/commands/quiz.js @@ -0,0 +1,276 @@ +const {MessageEmbed} = require('discord.js'); +const durationParser = require('parse-duration'); +const {formatDate} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); +const {createQuiz} = require('../quizUtil'); + +/** + * Handles quiz create commands + * @param {Discord.ApplicationCommandInteraction} interaction + */ +async function create(interaction) { + const config = interaction.client.configurations['quiz']['config']; + if (!interaction.member.roles.cache.has(config.createAllowedRole)) return interaction.reply({ + content: localize('quiz', 'no-permission'), + ephemeral: true + }); + + let endAt; + let options = []; + let emojis = config.emojis; + if (interaction.options.getSubcommand() === 'create-bool') { + options = [{text: localize('quiz', 'bool-true')}, {text: localize('quiz', 'bool-false')}]; + emojis = [null, emojis.true, emojis.false]; + } else { + for (let step = 1; step <= 10; step++) { + if (interaction.options.getString('option' + step)) options.push({text: interaction.options.getString('option' + step)}); + } + } + + const selectOptions = []; + for (const vId in options) { + selectOptions.push({ + label: options[vId].text, + value: vId, + description: localize('quiz', 'this-correct'), + emoji: emojis[parseInt(vId) + 1] + }); + } + const msg = await interaction.reply({ + components: [{ + type: 'ACTION_ROW', + components: [{ + /* eslint-disable camelcase */ + type: 'SELECT_MENU', + custom_id: 'quiz', + placeholder: localize('quiz', 'select-correct'), + min_values: 1, + max_values: interaction.options.getSubcommand() === 'create-bool' ? 1 : options.length, + options: selectOptions + }] + }], + ephemeral: true, + fetchReply: true + }); + const collector = msg.createMessageComponentCollector({ + filter: i => interaction.user.id === i.user.id, + componentType: 'SELECT_MENU', + max: 1 + }); + collector.on('collect', async i => { + i.values.forEach(option => { + options[option].correct = true; + }); + + if (interaction.options.getString('duration')) endAt = new Date(new Date().getTime() + durationParser(interaction.options.getString('duration'))); + await createQuiz({ + description: interaction.options.getString('description', true), + channel: interaction.options.getChannel('channel', true), + endAt, + options, + canChangeVote: interaction.options.getBoolean('canchange') || false, + type: interaction.options.getSubcommand() === 'create-bool' ? 'bool' : 'normal' + }, interaction.client); + i.update({ + content: localize('quiz', 'created', {c: interaction.options.getChannel('channel').toString()}), + components: [] + }); + }); +} + +module.exports.subcommands = { + 'create': create, + 'create-bool': create, + 'play': async function (interaction) { + let user = await interaction.client.models['quiz']['QuizUser'].findAll({where: {userId: interaction.user.id}}); + if (user.length > 0) user = user[0]; + else user = await interaction.client.models['quiz']['QuizUser'].create({ + userID: interaction.user.id, + dailyQuiz: 0 + }); + + if (user.dailyQuiz >= interaction.client.configurations['quiz']['config'].dailyQuizLimit) { + const now = new Date(); + now.setDate(now.getDate() + 1); + now.setHours(0); + now.setMinutes(0); + now.setSeconds(0); + + return interaction.reply({ + content: localize('quiz', 'daily-quiz-limit', { + l: interaction.client.configurations['quiz']['config'].dailyQuizLimit, + timestamp: formatDate(now) + }), + ephemeral: true + }); + } + if (!interaction.client.configurations['quiz']['quizList'] || interaction.client.configurations['quiz']['quizList'].length === 0) return interaction.reply({ + content: localize('quiz', 'no-quiz'), + ephemeral: true + }); + + const updatedUser = {dailyQuiz: user.dailyQuiz + 1}; + let quiz = {}; + if (interaction.client.configurations['quiz']['config'].mode.toLowerCase() === 'continuous') { + quiz = interaction.client.configurations['quiz']['quizList'][user.nextQuizID] || interaction.client.configurations['quiz']['quizList'][0]; + updatedUser.nextQuizID = interaction.client.configurations['quiz']['quizList'][user.nextQuizID + 1] ? user.nextQuizID + 1 : 0; + } else quiz = interaction.client.configurations['quiz']['quizList'][Math.floor(Math.random() * interaction.client.configurations['quiz']['quizList'].length)]; + + quiz.channel = interaction.channel; + quiz.options = [ + ...quiz.wrongOptions.map(o => ({text: o})), + ...quiz.correctOptions.map(o => ({text: o, correct: true})) + ]; + quiz.endAt = new Date(new Date().getTime() + durationParser(quiz.duration)); + quiz.canChangeVote = false; + quiz.private = true; + createQuiz(quiz, interaction.client, interaction); + + interaction.client.models['quiz']['QuizUser'].update(updatedUser, {where: {userID: interaction.user.id}}); + }, + 'leaderboard': async function (interaction) { + const moduleStrings = interaction.client.configurations['quiz']['strings']; + const users = await interaction.client.models['quiz']['QuizUser'].findAll({ + order: [ + ['xp', 'DESC'] + ], + limit: 15 + }); + + let leaderboardString = ''; + let i = 0; + for (const user of users) { + const member = interaction.guild.members.cache.get(user.userID); + if (!member) continue; + i++; + leaderboardString = leaderboardString + localize('quiz', 'leaderboard-notation', { + p: i, + u: member.user.toString(), + xp: user.xp + }) + '\n'; + } + if (leaderboardString.length === 0) leaderboardString = localize('levels', 'no-user-on-leaderboard'); + + const embed = new MessageEmbed() + .setTitle(moduleStrings.embed.leaderboardTitle) + .setColor(moduleStrings.embed.leaderboardColor) + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) + .setThumbnail(interaction.guild.iconURL()) + .addField(moduleStrings.embed.leaderboardSubtitle, leaderboardString); + + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); + + const components = [{ + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + label: moduleStrings.embed.leaderboardButton, + style: 'SUCCESS', + customId: 'show-quiz-rank' + }] + }]; + + interaction.reply({embeds: [embed], components}); + } +}; + +module.exports.config = { + name: 'quiz', + description: localize('quiz', 'cmd-description'), + + options: function () { + const options = [ + { + type: 'SUB_COMMAND', + name: 'create', + description: localize('quiz', 'cmd-create-normal-description'), + options: [{ + type: 'STRING', + name: 'description', + required: true, + description: localize('quiz', 'cmd-create-description-description') + }, + { + type: 'CHANNEL', + name: 'channel', + required: true, + channelTypes: ['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_VOICE'], + description: localize('quiz', 'cmd-create-channel-description') + }, + { + type: 'STRING', + name: 'duration', + required: true, + description: localize('quiz', 'cmd-create-endAt-description') + }, + { + type: 'STRING', + name: 'option1', + required: true, + description: localize('quiz', 'cmd-create-option-description', {o: 1}) + }, + { + type: 'STRING', + name: 'option2', + required: true, + description: localize('quiz', 'cmd-create-option-description', {o: 2}) + }, + { + type: 'BOOLEAN', + name: 'canchange', + required: false, + description: localize('quiz', 'cmd-create-canchange-description') + }] + }, + { + type: 'SUB_COMMAND', + name: 'create-bool', + description: localize('quiz', 'cmd-create-bool-description'), + options: [{ + type: 'STRING', + name: 'description', + required: true, + description: localize('quiz', 'cmd-create-description-description') + }, + { + type: 'CHANNEL', + name: 'channel', + required: true, + channelTypes: ['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_VOICE'], + description: localize('quiz', 'cmd-create-channel-description') + }, + { + type: 'BOOLEAN', + name: 'canchange', + required: false, + description: localize('quiz', 'cmd-create-canchange-description') + }, + { + type: 'STRING', + name: 'duration', + required: false, + description: localize('quiz', 'cmd-create-endAt-description') + }] + }, + { + type: 'SUB_COMMAND', + name: 'play', + description: localize('quiz', 'cmd-play-description') + }, + { + type: 'SUB_COMMAND', + name: 'leaderboard', + description: localize('quiz', 'cmd-leaderboard-description') + } + ]; + for (let step = 1; step <= 7; step++) { + options[0].options.push({ + type: 'STRING', + name: `option${2 + step}`, + required: false, + description: localize('quiz', 'cmd-create-option-description', {o: 2 + step}) + }); + } + return options; + } +}; \ No newline at end of file diff --git a/modules/quiz/configs/config.json b/modules/quiz/configs/config.json new file mode 100644 index 00000000..3821745c --- /dev/null +++ b/modules/quiz/configs/config.json @@ -0,0 +1,146 @@ +{ + "description": { + "en": "Configure the function of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "commandsWarnings": { + "normal": [ + "/quiz" + ] + }, + "content": [ + { + "name": "emojis", + "humanName": { + "de": "Emojis" + }, + "default": { + "en": { + "1": "1️⃣", + "2": "2️⃣", + "3": "3️⃣", + "4": "4️⃣", + "5": "5️⃣", + "6": "6️⃣", + "7": "7️⃣", + "8": "8️⃣", + "9": "9️⃣", + "true": "✅", + "false": "❌" + }, + "de": { + "1": "1️⃣", + "2": "2️⃣", + "3": "3️⃣", + "4": "4️⃣", + "5": "5️⃣", + "6": "6️⃣", + "7": "7️⃣", + "8": "8️⃣", + "9": "9️⃣", + "true": "✅", + "false": "❌" + } + }, + "description": { + "en": "You can set the emojis to use", + "de": "Du kannst die verschiedenen Emojis, die benutzt werden sollen, einstellen" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + }, + { + "name": "dailyQuizLimit", + "humanName": { + "en": "Daily quiz limit", + "de": "Tägliches Quizlimit" + }, + "default": { + "en": 5 + }, + "description": { + "en": "How many quizzes can be played per day using /quiz play", + "de": "Wie viele Quiz pro Tag mit /quiz play gespielt werden können" + }, + "type": "integer" + }, + { + "name": "leaderboardChannel", + "humanName": { + "en": "Quiz leaderboard channel", + "de": "Quiz-Leaderboard-Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "In which channel the quiz leaderboard is displayed", + "de": "In welchem Kanal das Quiz-Leaderboard angezeigt wird" + }, + "type": "channelID", + "content": [ + "GUILD_TEXT", + "GUILD_ANNOUNCEMENT" + ], + "allowNull": true + }, + { + "name": "createAllowedRole", + "humanName": { + "en": "Role needed to create quizzes", + "de": "Rolle zum Erstellen von Quiz" + }, + "default": { + "en": "" + }, + "description": { + "en": "Which role a user needs to have to be able to create quizzes with /quiz create/create-bool", + "de": "Welche Rolle ein Nutzer haben muss, um Quiz mit /quiz create/create-bool erstellen zu können" + }, + "type": "roleID" + }, + { + "name": "mode", + "humanName": { + "en": "Mode for quiz selection", + "de": "Modus zur Quizauswahl" + }, + "default": { + "en": "Random" + }, + "description": { + "en": "How a /quiz play quiz is selected for users", + "de": "Wie ein /quiz-play-Quiz für Nutzer ausgewählt wird" + }, + "type": "select", + "content": [ + "Random", + "Continuous" + ] + }, + { + "name": "livePreview", + "humanName": { + "en": "Live preview of results", + "de": "Live-Vorschau der Ergebnisse" + }, + "default": { + "en": false + }, + "description": { + "en": "Whether the live preview of results is enabled", + "de": "Ob die Live-Vorschau der Ergebnisse aktiviert ist" + }, + "type": "boolean" + } + ] +} diff --git a/modules/quiz/configs/quizList.json b/modules/quiz/configs/quizList.json new file mode 100644 index 00000000..5b380bd1 --- /dev/null +++ b/modules/quiz/configs/quizList.json @@ -0,0 +1,76 @@ +{ + "description": { + "en": "Create and edit the quizzes of the server", + "de": "Erstelle und bearbeite hier die Quiz des Servers" + }, + "humanName": { + "en": "Edit quiz", + "de": "Quiz bearbeiten" + }, + "configElements": true, + "filename": "quizList.json", + "content": [ + { + "name": "description", + "humanName": { + "en": "Question or statement", + "de": "Frage oder Behauptung" + }, + "default": { + "en": "" + }, + "description": { + "en": "Title/Question of the quiz", + "de": "Titel/Frage des Quiz" + }, + "type": "string" + }, + { + "name": "duration", + "humanName": { + "en": "Time limit", + "de": "Zeitlimit" + }, + "default": { + "en": "1m" + }, + "description": { + "en": "How much time the user has to answer", + "de": "Wie viel Zeit der Nutzer zum Beantworten hat" + }, + "type": "string" + }, + { + "name": "correctOptions", + "humanName": { + "en": "Correct answers", + "de": "Richtige Antworten" + }, + "default": { + "en": [] + }, + "description": { + "en": "Correct answers", + "de": "Richtige Antworten" + }, + "type": "array", + "content": "string" + }, + { + "name": "wrongOptions", + "humanName": { + "en": "Wrong answers", + "de": "Falsche Antworten" + }, + "default": { + "en": [] + }, + "description": { + "en": "Wrong answers", + "de": "Falsche Antworten" + }, + "type": "array", + "content": "string" + } + ] +} \ No newline at end of file diff --git a/modules/quiz/configs/strings.json b/modules/quiz/configs/strings.json new file mode 100644 index 00000000..ffd09041 --- /dev/null +++ b/modules/quiz/configs/strings.json @@ -0,0 +1,59 @@ +{ + "description": { + "en": "Edit the messages and strings of the module here", + "de": "Stelle hier die Nachrichten des Modules ein" + }, + "humanName": { + "en": "Nachrichten", + "de": "Nachrichten" + }, + "filename": "strings.json", + "content": [ + { + "name": "embed", + "humanName": { + "de": "Embed" + }, + "default": { + "en": { + "title": "New quiz - What's right?", + "color": "BLUE", + "options": "Today's options", + "liveView": "Live view of the results", + "expiresOn": "End of this quiz", + "thisQuizExpiresOn": "This quiz expires on %date%.", + "endedQuizTitle": "Quiz ended", + "endedQuizColor": "RED", + "leaderboardTitle": "The best quiz players", + "leaderboardSubtitle": "Quiz leaderboard", + "leaderboardColor": "GREEN", + "leaderboardButton": "View my ranking" + }, + "de": { + "title": "Neues Quiz - Was ist richtig?", + "color": "BLUE", + "options": "Mögliche antworten", + "liveView": "Live-Anzeige der Ergebnisse", + "expiresOn": "Ende dieses Quiz", + "thisQuizExpiresOn": "Dieses Quiz endet am %date%.", + "endedQuizTitle": "Quiz beendet", + "endedQuizColor": "RED", + "leaderboardTitle": "Die besten Quiz-Spieler", + "leaderboardSubtitle": "Quiz-Rangliste", + "leaderboardColor": "GREEN", + "leaderboardButton": "Meine Platzierung ansehen" + } + }, + "description": { + "en": "You can edit the settings of your embed here", + "de": "Du kannst die Einstellungen des Embeds hier bearbeiten" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + } + ] +} diff --git a/modules/quiz/events/botReady.js b/modules/quiz/events/botReady.js new file mode 100644 index 00000000..8ae05dba --- /dev/null +++ b/modules/quiz/events/botReady.js @@ -0,0 +1,28 @@ +const {updateMessage, updateLeaderboard} = require('../quizUtil'); +const {scheduleJob} = require('node-schedule'); + +module.exports.run = async (client) => { + const quizList = await client.models['quiz']['QuizList'].findAll(); + quizList.forEach(quiz => { + if (!quiz.private && quiz.expiresAt && new Date(quiz.expiresAt).getTime() > new Date().getTime()) scheduleJob(new Date(quiz.expiresAt), async () => { + await updateMessage(await client.channels.fetch(quiz.channelID), quiz, quiz.messageID); + }); + }); + + if (client.configurations['quiz']['config'].leaderboardChannel) { + await updateLeaderboard(client, true); + const interval = setInterval(() => { + updateLeaderboard(client); + }, 300042); + client.intervals.push(interval); + } + + const job = scheduleJob('1 0 * * *', async () => { // Every day at 00:01 https://crontab.guru/#0_0_*_*_* + const users = await client.models['quiz']['QuizUser'].findAll(); + users.forEach(user => { + user.dailyQuiz = 0; + user.save(); + }); + }); + client.jobs.push(job); +}; diff --git a/modules/quiz/events/interactionCreate.js b/modules/quiz/events/interactionCreate.js new file mode 100644 index 00000000..1ae02f63 --- /dev/null +++ b/modules/quiz/events/interactionCreate.js @@ -0,0 +1,99 @@ +const {updateMessage, setChanged} = require('../quizUtil'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async (client, interaction) => { + if (!interaction.message) return; + if (interaction.isButton() && interaction.customId === 'show-quiz-rank') { + const user = await client.models['quiz']['QuizUser'].findOne({ + where: { + userID: interaction.user.id + } + }); + if (user) return interaction.reply({content: localize('quiz', 'your-rank', {xp: user.xp}), ephemeral: true}); + else return interaction.reply({content: '⚠️️ ' + localize('quiz', 'no-rank'), ephemeral: true}); + } + + const quiz = await client.models['quiz']['QuizList'].findOne({ + where: { + messageID: interaction.message.id + } + }); + if (!quiz) return; + let expired = false; + if (quiz.expiresAt || quiz.endAt) { + const date = new Date(quiz.expiresAt || quiz.endAt); + if (date.getTime() <= new Date().getTime()) expired = true; + } + + if (interaction.isButton() && interaction.customId === 'quiz-own-vote') { + let userVoteCat = null; + for (const id in quiz.votes) { + if (quiz.votes[id].includes(interaction.user.id)) userVoteCat = id; + } + if (!userVoteCat) return interaction.reply({ + content: '⚠️ ' + localize('quiz', 'not-voted-yet'), + ephemeral: true + }); + let extra = ''; + if (!expired) { + if (quiz.canChangeVote) extra = '\n' + localize('quiz', 'change-opinion'); + else extra = '\n' + localize('quiz', 'cannot-change-opinion'); + } else if (quiz.options[userVoteCat - 1].correct) extra = '\n\n' + localize('quiz', 'answer-correct'); + else extra = '\n\n' + localize('quiz', 'answer-wrong'); + return interaction.reply({ + content: localize('quiz', 'you-voted', {o: quiz.options[userVoteCat - 1].text}) + extra, + ephemeral: true + }); + } + if ((interaction.isSelectMenu() && interaction.customId === 'quiz-vote') || (interaction.isButton() && interaction.customId.startsWith('quiz-vote-'))) { + if (quiz.expiresAt && new Date(quiz.expiresAt).getTime() <= new Date().getTime()) return; + + if (quiz.private) { + const user = await interaction.client.models['quiz']['QuizUser'].findAll({ + where: { + userID: interaction.user.id + } + }); + if (user.length === 0) return; + + let extra = localize('quiz', 'answer-wrong'); + if (quiz.options[interaction.isSelectMenu() ? interaction.values[0] : interaction.customId.split('-')[2]].correct) { + extra = localize('quiz', 'answer-correct'); + interaction.client.models['quiz']['QuizUser'].update({ + dailyXp: user[0].dailyXp + 1, + xp: user[0].xp + 1 + }, {where: {userID: interaction.user.id}}); + setChanged(); + } + + return interaction.update({ + content: localize('quiz', 'you-voted', {o: quiz.options[interaction.isSelectMenu() ? interaction.values[0] : interaction.customId.split('-')[2]].text}) + '\n\n' + extra, + embeds: [], + components: [] + }); + } + + const o = quiz.votes; + quiz.votes = {}; + let back = false; + + for (const id in o) { + if (o[id].includes(interaction.user.id) && !quiz.canChangeVote) { + interaction.reply({content: localize('quiz', 'cannot-change-opinion'), ephemeral: true}); + back = true; + break; + } + if (o[id] && o[id].includes(interaction.user.id)) o[id].splice(o[id].indexOf(interaction.user.id), 1); + } + if (back) return; + o[(parseInt(interaction.isSelectMenu() ? interaction.values[0] : interaction.customId.split('-')[2]) + 1).toString()].push(interaction.user.id); + quiz.votes = o; + quiz.save(); + + updateMessage(interaction.channel, quiz, interaction.message.id); + interaction.reply({ + content: localize('quiz', 'voted-successfully'), + ephemeral: true + }); + } +}; \ No newline at end of file diff --git a/modules/quiz/models/Quiz.js b/modules/quiz/models/Quiz.js new file mode 100644 index 00000000..513e4fe2 --- /dev/null +++ b/modules/quiz/models/Quiz.js @@ -0,0 +1,29 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class QuizList extends Model { + static init(sequelize) { + return super.init({ + messageID: { + type: DataTypes.STRING, + primaryKey: true + }, + description: DataTypes.STRING, + options: DataTypes.JSON, + votes: DataTypes.JSON, // {1: ["userIDHere"], 2: ["as"] } + expiresAt: DataTypes.DATE, + channelID: DataTypes.STRING, + canChangeVote: DataTypes.BOOLEAN, + private: DataTypes.BOOLEAN, + type: DataTypes.STRING // normal, bool + }, { + tableName: 'quiz_Quiz', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'QuizList', + 'module': 'quiz' +}; diff --git a/modules/quiz/models/QuizUser.js b/modules/quiz/models/QuizUser.js new file mode 100644 index 00000000..667c100c --- /dev/null +++ b/modules/quiz/models/QuizUser.js @@ -0,0 +1,37 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class QuizUser extends Model { + static init(sequelize) { + return super.init({ + userID: { + type: DataTypes.STRING, + primaryKey: true + }, + xp: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + dailyXp: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + dailyQuiz: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + nextQuizID: { + type: DataTypes.INTEGER, + defaultValue: 0 + } + }, { + tableName: 'quiz_users', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'QuizUser', + 'module': 'quiz' +}; diff --git a/modules/quiz/module.json b/modules/quiz/module.json new file mode 100644 index 00000000..1951b9c9 --- /dev/null +++ b/modules/quiz/module.json @@ -0,0 +1,28 @@ +{ + "name": "quiz", + "humanReadableName": { + "en": "Quiz Module", + "de": "Quiz-Modul" + }, + "author": { + "scnxOrgID": "60", + "name": "TomatoCake", + "link": "https://github.com/DEVTomatoCake" + }, + "description": { + "en": "Create quiz for your users and let them compete against each other.", + "de": "Erstelle Quiz für deine Nutzer und lasse sie gegeneinander antreten." + }, + "events-dir": "/events", + "commands-dir": "/commands", + "models-dir": "/models", + "config-example-files": [ + "configs/config.json", + "configs/strings.json", + "configs/quizList.json" + ], + "tags": [ + "fun" + ], + "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/quiz" +} \ No newline at end of file diff --git a/modules/quiz/quizUtil.js b/modules/quiz/quizUtil.js new file mode 100644 index 00000000..2e9be56f --- /dev/null +++ b/modules/quiz/quizUtil.js @@ -0,0 +1,250 @@ +/** + * Create and manage quiz + * @module quiz + */ +const {scheduleJob} = require('node-schedule'); +const {MessageEmbed} = require('discord.js'); +const {renderProgressbar, formatDate} = require('../../src/functions/helpers'); +const {localize} = require('../../src/functions/localize'); + +let changed = false; + +/** + * Sets the changed variable to true + */ +function setChanged() { + changed = true; +} + +/** + * Creates a new quiz + * @param {Object} data Data of the new quiz + * @param {Client} client Client + * @param {Discord.ApplicationCommandInteraction} interaction? Interaction if private + * @return {Promise} + */ +async function createQuiz(data, client, interaction) { + const votes = {}; + for (const vid in data.options) { + votes[parseInt(vid) + 1] = []; + } + data.votes = votes; + const id = await updateMessage(data.channel, data, null, data.private ? interaction : null); + + await client.models['quiz']['QuizList'].create({ + messageID: id, + description: data.description, + options: data.options, + channelID: data.channel.id, + expiresAt: data.endAt, + votes, + canChangeVote: data.canChangeVote, + private: data.private || false, + type: data.type + }); + + if (!data.private && data.endAt) { + client.jobs.push(scheduleJob(data.endAt, async () => { + await updateMessage(data.channel, await client.models['quiz']['QuizList'].findOne({where: {messageID: id}}), id); + })); + } +} + +/** + * Updates a quiz-message + * @param {TextChannel} channel Channel in which the message is + * @param {Object} data Data-Object (can be DB-Object) + * @param {String} mID ID of already sent message + * @param {Discord.ApplicationCommandInteraction} interaction? Interaction if private + * @return {Promise<*>} + */ +async function updateMessage(channel, data, mID = null, interaction = null) { + const strings = channel.client.configurations['quiz']['strings']; + const config = channel.client.configurations['quiz']['config']; + let emojis = config.emojis; + if (data.type === 'bool') emojis = [null, emojis.true, emojis.false]; + + let m; + if (mID && !interaction) m = await channel.messages.fetch(mID).catch(() => { + }); + const embed = new MessageEmbed() + .setTitle(strings.embed.title) + .setColor(strings.embed.color) + .setDescription(data.description); + + let allVotes = 0; + const expired = (data.expiresAt || data.endAt) ? data.expiresAt <= Date.now() || data.endAt <= Date.now() : false; + for (const vid in data.votes) { + allVotes = allVotes + data.votes[vid].length; + if (expired) { + if (data.options[parseInt(vid) - 1].correct) data.votes[vid].forEach(async voter => { + const user = await channel.client.models['quiz']['QuizUser'].findAll({ + where: { + userID: voter + } + }); + if (user.length > 0) channel.client.models['quiz']['QuizUser'].update({ + dailyXp: user[0].dailyXp + 1, + xp: user[0].xp + 1 + }, {where: {userID: voter}}); + else channel.client.models['quiz']['QuizUser'].create({userID: voter, dailyXp: 1, xp: 1}); + changed = true; + }); + } + } + + let s = ''; + let p = ''; + for (const id in data.options) { + const highlight = expired && data.options[id].correct ? '**' : ''; + const finishhighlight = data.options[id].correct ? '✅' : '❌'; + const percentage = 100 / allVotes * data.votes[(parseInt(id) + 1).toString()].length; + + s = s + highlight + (expired ? finishhighlight : '') + emojis[parseInt(id) + 1] + ': ' + data.options[id].text + + ((config.livePreview || expired) && !data.private ? ' `' + data.votes[(parseInt(id) + 1).toString()].length + '`' : '') + highlight + '\n'; + p = p + highlight + emojis[parseInt(id) + 1] + ' ' + renderProgressbar(percentage) + ' ' + (percentage ? percentage.toFixed(0) : '0') + + '% (' + data.votes[(parseInt(id) + 1).toString()].length + '/' + allVotes + ')' + highlight + '\n'; + } + embed.addField(strings.embed.options, s); + if ((config.livePreview || expired) && !data.private) embed.addField(strings.embed.liveView, p); + + const options = []; + for (const vId in data.options) { + options.push({ + label: data.options[vId].text, + value: vId, + description: localize('quiz', 'vote-this'), + emoji: emojis[parseInt(vId) + 1] + }); + } + if (data.expiresAt || data.endAt) { + const date = new Date(data.expiresAt || data.endAt); + if (date.getTime() <= Date.now()) { + embed.setColor(strings.embed.endedQuizColor); + embed.setTitle(strings.embed.endedQuizTitle); + embed.addField('\u200b', localize('quiz', 'correct-highlighted')); + } else { + embed.addField('\u200b', '\u200b'); + embed.addField(strings.embed.expiresOn, strings.embed.thisQuizExpiresOn.split('%date%').join(formatDate(date))); + } + } + + const components = []; + /* eslint-disable camelcase */ + if (data.type === 'bool') components.push({ + type: 'ACTION_ROW', components: [ + { + type: 'BUTTON', + customId: 'quiz-vote-0', + label: localize('quiz', 'bool-true'), + style: 'SUCCESS', + disabled: expired + }, + { + type: 'BUTTON', + customId: 'quiz-vote-1', + label: localize('quiz', 'bool-false'), + style: 'DANGER', + disabled: expired + } + ] + }); + else components.push({ + type: 'ACTION_ROW', + components: [{ + type: 'SELECT_MENU', + disabled: expired, + customId: 'quiz-vote', + min_values: 1, + max_values: 1, + placeholder: localize('quiz', 'vote'), + options + }] + }); + if (!data.private) components.push({ + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + customId: 'quiz-own-vote', + label: localize('quiz', 'what-have-i-voted'), + style: 'SECONDARY' + }] + }); + + let r; + if (data.private && interaction) r = await interaction.reply({ + embeds: [embed], + components, + fetchReply: true, + ephemeral: true + }); + else if (m) r = await m.edit({embeds: [embed], components}); + else r = await channel.send({embeds: [embed], components}); + return r.id; +} + +/** + * Updates the quiz leaderboard + * @param {Client} client Client + * @param {Boolean} force If enabled the embed will update even if there was no registered change + * @return {Promise} + */ +async function updateLeaderboard(client, force = false) { + if (!client.configurations['quiz']['config'].leaderboardChannel) return; + if (!force && !changed) return; + const moduleStrings = client.configurations['quiz']['strings']; + const channel = await client.channels.fetch(client.configurations['quiz']['config']['leaderboardChannel']).catch(() => { + }); + if (!channel || channel.type !== 'GUILD_TEXT') return client.logger.error('[quiz] ' + localize('quiz', 'leaderboard-channel-not-found')); + const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); + + const users = await client.models['quiz']['QuizUser'].findAll({ + order: [ + ['xp', 'DESC'] + ], + limit: 15 + }); + + let leaderboardString = ''; + let i = 0; + for (const user of users) { + const member = channel.guild.members.cache.get(user.userID); + if (!member) continue; + i++; + leaderboardString = leaderboardString + localize('quiz', 'leaderboard-notation', { + p: i, + u: member.user.toString(), + xp: user.xp + }) + '\n'; + } + if (leaderboardString.length === 0) leaderboardString = localize('levels', 'no-user-on-leaderboard'); + + const embed = new MessageEmbed() + .setTitle(moduleStrings.embed.leaderboardTitle) + .setColor(moduleStrings.embed.leaderboardColor) + .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) + .setThumbnail(channel.guild.iconURL()) + .addField(moduleStrings.embed.leaderboardSubtitle, leaderboardString); + + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + + const components = [{ + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + label: moduleStrings.embed.leaderboardButton, + style: 'SUCCESS', + customId: 'show-quiz-rank' + }] + }]; + + if (messages.first()) await messages.first().edit({embeds: [embed], components}); + else await channel.send({embeds: [embed], components}); +} + +module.exports = { + setChanged, + createQuiz, + updateMessage, + updateLeaderboard +}; \ No newline at end of file diff --git a/modules/reminders/commands/reminder.js b/modules/reminders/commands/reminder.js new file mode 100644 index 00000000..3462dd3e --- /dev/null +++ b/modules/reminders/commands/reminder.js @@ -0,0 +1,49 @@ +const {localize} = require('../../../src/functions/localize'); +const durationParser = require('parse-duration'); +const {planReminder} = require('../reminders'); +const {formatDate} = require('../../../src/functions/helpers'); + +module.exports.run = async function (interaction) { + const duration = durationParser(interaction.options.getString('in')); + const time = new Date(duration + new Date().getTime()); + if (!time || isNaN(time) || time.getTime() < new Date().getTime() + 55000) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('reminders', 'one-minute-in-future') + }); + const reminderObject = await interaction.client.models['reminders']['Reminder'].create({ + userID: interaction.user.id, + reminderText: interaction.options.getString('what'), + date: time, + channelID: interaction.options.getBoolean('dm') ? 'DM' : interaction.channel.id + }); + planReminder(interaction.client, reminderObject); + interaction.reply({ + ephemeral: true, + content: '✅ ' + localize('reminders', 'reminder-set', {d: formatDate(time)}) + }); +}; + +module.exports.config = { + name: 'remind-me', + description: localize('reminders', 'command-description'), + + options: [ + { + type: 'STRING', + name: 'in', + required: true, + description: localize('reminders', 'in-description') + }, + { + type: 'STRING', + name: 'what', + required: true, + description: localize('reminders', 'what-description') + }, + { + type: 'BOOLEAN', + name: 'dm', + description: localize('reminders', 'dm-description') + } + ] +}; \ No newline at end of file diff --git a/modules/reminders/config.json b/modules/reminders/config.json new file mode 100644 index 00000000..3eb33511 --- /dev/null +++ b/modules/reminders/config.json @@ -0,0 +1,71 @@ +{ + "filename": "config.json", + "description": { + "en": "Configure the behavior of this module here", + "de": "Passe hier die Funktionen des Modules hier an" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "content": [ + { + "name": "notificationMessage", + "type": "string", + "allowEmbed": true, + "humanName": { + "de": "Erinnerung-Nachricht", + "en": "Reminder-Message" + }, + "description": { + "de": "Diese Nachricht wird gesendet, wenn jemand erinnert wird", + "en": "This message gets send when someone gets remaindered" + }, + "default": { + "en": { + "title": "\uD83D\uDD14 Reminder", + "color": "#F1C40F", + "description": "%message%", + "message": "%mention%" + }, + "de": { + "title": "\uD83D\uDD14 Erinnerung", + "color": "#F1C40F", + "description": "%message%", + "message": "%mention%" + } + }, + "params": [ + { + "name": "mention", + "description": { + "en": "Mention of the user", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "message", + "description": { + "en": "Reminder message set by the user", + "de": "Vom Nutzer gesetze Erwähnungsnachricht" + } + }, + { + "name": "userTag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "userAvatarURL", + "isImage": true, + "description": { + "en": "Avatar-URL of the user", + "de": "Profilbild-URL des Nutzers" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/reminders/events/botReady.js b/modules/reminders/events/botReady.js new file mode 100644 index 00000000..aa7d40e5 --- /dev/null +++ b/modules/reminders/events/botReady.js @@ -0,0 +1,12 @@ +const {Op} = require('sequelize'); +const {planReminder} = require('../reminders'); +module.exports.run = async function (client) { + const reminders = await client.models['reminders']['Reminder'].findAll({ + where: { + date: { + [Op.gte]: new Date() + } + } + }); + for (const reminder of reminders) planReminder(client, reminder); +}; \ No newline at end of file diff --git a/modules/reminders/models/Reminder.js b/modules/reminders/models/Reminder.js new file mode 100644 index 00000000..be9d7033 --- /dev/null +++ b/modules/reminders/models/Reminder.js @@ -0,0 +1,28 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class RemindersReminder extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + userID: { + type: DataTypes.STRING + }, + reminderText: DataTypes.STRING, + channelID: DataTypes.STRING, // set to DM to send a DM + date: DataTypes.DATE + }, { + tableName: 'reminders-reminder', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'Reminder', + 'module': 'reminders' +}; \ No newline at end of file diff --git a/modules/reminders/module.json b/modules/reminders/module.json new file mode 100644 index 00000000..d790c2af --- /dev/null +++ b/modules/reminders/module.json @@ -0,0 +1,26 @@ +{ + "name": "reminders", + "humanReadableName": { + "en": "Reminders", + "de": "Erinnerungen" + }, + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "description": { + "en": "Let users set reminders for themselves - either via DMs or Channels", + "de": "Erlaubt es deinen Nutzer Erinnerungen für sich selbst zu setzen - entweder per PNs oder direkt in den Kanal" + }, + "commands-dir": "/commands", + "events-dir": "/events", + "config-example-files": [ + "config.json" + ], + "tags": [ + "community" + ], + "models-dir": "/models", + "holidayGift": true +} \ No newline at end of file diff --git a/modules/reminders/reminders.js b/modules/reminders/reminders.js new file mode 100644 index 00000000..3ce62595 --- /dev/null +++ b/modules/reminders/reminders.js @@ -0,0 +1,22 @@ +const {scheduleJob} = require('node-schedule'); +const {embedType, formatDiscordUserName} = require('../../src/functions/helpers'); + +function planReminder(client, notificationObject) { + if (!notificationObject.date || isNaN(notificationObject.date) || notificationObject.date.getTime() <= new Date().getTime()) return; + const bj = scheduleJob(notificationObject.date, async () => { + const member = await client.guild.members.fetch(notificationObject.userID).catch(() => { + }); + if (!member) return; + const channel = notificationObject.channelID === 'DM' ? await member.user.createDM() : client.guild.channels.cache.get(notificationObject.channelID); + if (!channel) return; + channel.send(embedType(client.configurations['reminders']['config']['notificationMessage'], { + '%mention%': member.user.toString(), + '%message%': notificationObject.reminderText, + '%userTag%': formatDiscordUserName(member.user), + '%userAvatarURL%': member.user.avatarURL() + })); + }); + client.jobs.push(bj); +} + +module.exports.planReminder = planReminder; \ No newline at end of file diff --git a/modules/rock-paper-scissors/commands/rock-paper-scissors.js b/modules/rock-paper-scissors/commands/rock-paper-scissors.js new file mode 100644 index 00000000..ccfce14d --- /dev/null +++ b/modules/rock-paper-scissors/commands/rock-paper-scissors.js @@ -0,0 +1,308 @@ +const {localize} = require('../../../src/functions/localize'); +const {MessageEmbed, MessageActionRow, MessageButton} = require('discord.js'); +const {formatDiscordUserName} = require('../../../src/functions/helpers'); + +const rpsgames = []; +const moves = ['🪨 ' + localize('rock-paper-scissors', 'stone'), '📄 ' + localize('rock-paper-scissors', 'paper'), '✂️ ' + localize('rock-paper-scissors', 'scissors')]; +const movesDouble = [...moves, ...moves]; +const statestyle = { + none: 'PRIMARY', + selected: 'SECONDARY', + [localize('rock-paper-scissors', 'tie')]: 'PRIMARY', + [localize('rock-paper-scissors', 'won')]: 'SUCCESS', + [localize('rock-paper-scissors', 'lost')]: 'DANGER' +}; +const stateemoji = { + none: '⏰', + selected: '✅' +}; + +/** + * Finds the winner of the game + * @param {String} move1 + * @param {String} move2 + * @returns {{win1: string, win2: string}} + */ +function findWinner(move1, move2) { + let win1 = '', win2 = ''; + if (move1 === move2) { + win1 = localize('rock-paper-scissors', 'tie'); + win2 = localize('rock-paper-scissors', 'tie'); + } else { + for (let j = 0; j < moves.length; j++) { + if (move2 === moves[j] && move1 === movesDouble[j + 1]) { + win1 = localize('rock-paper-scissors', 'won'); + win2 = localize('rock-paper-scissors', 'lost'); + } else if (move2 === moves[j] && move1 === movesDouble[j + 2]) { + win1 = localize('rock-paper-scissors', 'lost'); + win2 = localize('rock-paper-scissors', 'won'); + } + } + } + return {win1, win2}; +} + +/** + * Generates a row with the buttons for the game + * @returns {MessageActionRow} + */ +function rpsrow() { + return new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('rps_scissors') + .setLabel(localize('rock-paper-scissors', 'scissors')) + .setStyle('PRIMARY') + .setEmoji('✂️') + ) + .addComponents( + new MessageButton() + .setCustomId('rps_stone') + .setLabel(localize('rock-paper-scissors', 'stone')) + .setStyle('PRIMARY') + .setEmoji('🪨') + ) + .addComponents( + new MessageButton() + .setCustomId('rps_paper') + .setLabel(localize('rock-paper-scissors', 'paper')) + .setStyle('PRIMARY') + .setEmoji('📄') + ); +} + +/** + * Generates a row with a play again button + * @returns {MessageActionRow} + */ +function playagain() { + return new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('rps_playagain') + .setLabel(localize('rock-paper-scissors', 'play-again')) + .setStyle('SECONDARY') + ); +} + +/** + * Generates a row which displays the players and their current state + * @param {User} user1 + * @param {User} user2 + * @param {String} state1 + * @param {String} state2 + * @returns {MessageActionRow} + */ +function generatePlayer(user1, user2, state1, state2) { + return new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('rps_user1') + .setLabel(formatDiscordUserName(user1)) + .setEmoji(stateemoji[state1] || '') + .setStyle(statestyle[state1]) + .setDisabled(true) + ) + .addComponents( + new MessageButton() + .setCustomId('rps_vs') + .setStyle('SECONDARY') + .setEmoji('⚔️') + .setDisabled(true) + ) + .addComponents( + new MessageButton() + .setCustomId('rps_user2') + .setLabel(formatDiscordUserName(user2)) + .setEmoji(stateemoji[state2] || '') + .setStyle(statestyle[state2]) + .setDisabled(true) + ); +} + +/** + * Resets the game + * @param {Object} game + * @returns {[MessageActionRow, MessageActionRow]} + */ +function resetGame(game) { + game.state1 = 'none'; + game.state2 = game.user2.bot ? 'selected' : 'none'; + delete game.selected1; + delete game.selected2; + rpsgames[game.msg] = game; + return [rpsrow(), generatePlayer(game.user1, game.user2, game.state1, game.state2)]; +} + +/** + * Generates a string with the users to mention + * @param {Object} game + * @returns string + */ +function mentionUsers(game) { + let mention = ''; + if (game.state1 === 'none') mention = mention + '<@' + game.user1.id + '>'; + if (!game.user2.bot && game.state2 === 'none') mention = mention + (mention === '' ? '' : ' ') + '<@' + game.user2.id + '>'; + return mention || null; +} + +module.exports.run = async function (interaction) { + const member = interaction.options.getMember('user'); + + let user2; + if (member && interaction.user.id !== member.id) user2 = member.user; + else user2 = interaction.client.user; + + let confirmed; + if (!user2.bot) { + const confirmmsg = await interaction.reply({ + content: localize('rock-paper-scissors', 'challenge-message', { + t: member.toString(), + u: interaction.user.toString() + }), + allowedMentions: { + users: [user2.id] + }, + fetchReply: true, + components: [ + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + style: 'PRIMARY', + customId: 'accept-invite', + label: localize('tic-tac-toe', 'accept-invite') + }, + { + type: 'BUTTON', + style: 'SECONDARY', + customId: 'deny-invite', + label: localize('tic-tac-toe', 'deny-invite') + } + ] + } + ] + }); + confirmed = await confirmmsg.awaitMessageComponent({ + filter: i => i.user.id === user2.id, + componentType: 'BUTTON', + time: 120000 + }).catch(() => { + }); + if (!confirmed) return confirmmsg.update({ + content: localize('rock-paper-scissors', 'invite-expired', { + u: interaction.user.toString(), + i: '<@' + user2.id + '>' + }), components: [] + }); + if (confirmed.customId === 'deny-invite') return confirmed.update({ + content: localize('rock-paper-scissors', 'invite-denied', { + u: interaction.user.toString(), + i: '<@' + user2.id + '>' + }), components: [] + }); + } + + const embed = new MessageEmbed() + .setTitle(localize('rock-paper-scissors', 'rps-title')) + .setDescription(localize('rock-paper-scissors', 'rps-description')); + + const msg = await (confirmed || interaction)[confirmed ? 'update' : 'reply']({ + content: '<@' + interaction.user.id + '>' + (user2.bot ? '' : ' <@' + user2.id + '>'), + embeds: [embed], + components: [rpsrow(), generatePlayer(interaction.user, user2, 'none', user2.bot ? 'selected' : 'none')], + fetchReply: true + }); + + rpsgames[msg.id] = { + user1: interaction.user, + user2, + msg: msg.id, + state1: 'none', + state2: user2.bot ? 'selected' : 'none' + }; + + const collector = msg.createMessageComponentCollector({ + componentType: 'BUTTON', + filter: i => i.user.id === interaction.user.id || i.user.id === user2.id + }); + collector.on('collect', i => { + const game = rpsgames[i.message.id]; + + if (i.customId === 'rps_playagain') return i.update({components: resetGame(game), content: mentionUsers(game)}); + + if (i.user.id === game.user1.id) { + game.state1 = 'selected'; + game.selected1 = i.customId; + } else if (i.user.id === game.user2.id) { + game.state2 = 'selected'; + game.selected2 = i.customId; + } + + rpsgames[i.message.id] = game; + if (!game.selected1 || (!game.selected2 && !user2.bot)) return i.update({ + content: mentionUsers(game), + components: [rpsrow(), generatePlayer(game.user1, game.user2, game.state1, game.state2)] + }); + + let resU1 = ''; + let winResult = {}; + let components = []; + if (user2.bot) { + const picked = moves[Math.floor(Math.random() * moves.length)]; + + if (i.customId === 'rps_stone') resU1 = moves[0]; + else if (i.customId === 'rps_paper') resU1 = moves[1]; + else if (i.customId === 'rps_scissors') resU1 = moves[2]; + + winResult = findWinner(resU1, picked); + game.state1 = winResult.win1; + game.state2 = winResult.win2; + rpsgames[i.message.id] = game; + + if (picked === resU1) embed.setTitle(localize('rock-paper-scissors', 'its-a-tie-try-again')); + else embed.setTitle(localize('rock-paper-scissors', 'rps-title')); + embed.setDescription('<@' + game.user1.id + '>: **' + resU1 + '**' + (resU1 !== picked ? ' (' + game.state1 + ')' : '') + '\n<@' + game.user2.id + '>: **' + picked + '**' + (resU1 !== picked ? ' (' + game.state2 + ')' : '')); + + if (picked === resU1) components = resetGame(game); + else components = [generatePlayer(game.user1, game.user2, game.state1, game.state2), playagain()]; + } else { + let resU2 = ''; + if (game.selected1 === 'rps_stone') resU2 = moves[0]; + else if (game.selected1 === 'rps_paper') resU2 = moves[1]; + else if (game.selected1 === 'rps_scissors') resU2 = moves[2]; + + if (game.selected2 === 'rps_stone') resU1 = moves[0]; + else if (game.selected2 === 'rps_paper') resU1 = moves[1]; + else if (game.selected2 === 'rps_scissors') resU1 = moves[2]; + + winResult = findWinner(resU1, resU2); + game.state1 = winResult.win1; + game.state2 = winResult.win2; + rpsgames[i.message.id] = game; + + if (resU1 === resU2) embed.setTitle(localize('rock-paper-scissors', 'its-a-tie-try-again')); + else embed.setTitle(localize('rock-paper-scissors', 'rps-title')); + embed.setDescription('<@' + game.user1.id + '>: **' + resU2 + '**' + (resU1 !== resU2 ? ' (' + game.state2 + ')' : '') + '\n<@' + game.user2.id + '>: **' + resU1 + '**' + (resU1 !== resU2 ? ' (' + game.state1 + ')' : '')); + + if (resU1 === resU2) components = resetGame(game); + else components = [generatePlayer(game.user1, game.user2, game.state2, game.state1), playagain()]; + } + i.update({content: mentionUsers(game), embeds: [embed], components}); + }); +}; + + +module.exports.config = { + name: 'rock-paper-scissors', + description: localize('rock-paper-scissors', 'command-description'), + + options: [ + { + type: 'USER', + name: 'user', + description: localize('tic-tac-toe', 'user-description') + } + ] +}; \ No newline at end of file diff --git a/modules/rock-paper-scissors/module.json b/modules/rock-paper-scissors/module.json new file mode 100644 index 00000000..6289dabe --- /dev/null +++ b/modules/rock-paper-scissors/module.json @@ -0,0 +1,23 @@ +{ + "name": "rock-paper-scissors", + "humanReadableName": { + "en": "Rock Paper Scissors", + "de": "Schere Stein Papier" + }, + "author": { + "scnxOrgID": "60", + "name": "TomatoCake", + "link": "https://github.com/DEVTomatoCake" + }, + "description": { + "en": "Let your users play Rock Paper Scissors against the bot and each other!", + "de": "Lasse Nutzer auf deinem Server Schere Stein Papier gegen den Bot und gegeneinander spielen" + }, + "commands-dir": "/commands", + "noConfig": true, + "releaseDate": "0", + "tags": [ + "fun" + ], + "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/rock-paper-scissors" +} \ No newline at end of file diff --git a/modules/serverinfo/configs/config.json b/modules/serverinfo/configs/config.json new file mode 100644 index 00000000..7d4b793b --- /dev/null +++ b/modules/serverinfo/configs/config.json @@ -0,0 +1,55 @@ +{ + "description": { + "en": "Configure the function of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "channelID", + "humanName": { + "en": "Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "ID of the channel this module should operate in", + "de": "ID des Channels, in welchem die Nachricht gesendet und bearbeitet werden soll" + }, + "type": "channelID" + }, + { + "name": "embed", + "humanName": { + "en": "Embed" + }, + "default": { + "en": { + "title": "Information about this guild", + "description": "You can find some basic information about our guild here", + "color": "GREEN" + }, + "de": { + "title": "Informationen über diesen Server", + "description": "Hier kannst du alle Informationen über unseren Server finden", + "color": "GREEN" + } + }, + "description": { + "en": "You can configure some of the parameters of the embed here", + "de": "Du kannst hier einige Teile des Embeds anpassen" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + } + ] +} \ No newline at end of file diff --git a/modules/serverinfo/configs/fields.json b/modules/serverinfo/configs/fields.json new file mode 100644 index 00000000..0bd009c2 --- /dev/null +++ b/modules/serverinfo/configs/fields.json @@ -0,0 +1,166 @@ +{ + "description": { + "en": "Change the Embed-Fields of the serverinfo-embed here", + "de": "Stelle hier die Felder des Serverinfo-Embeds ein" + }, + "humanName": { + "en": "Embed-Fields", + "de": "Embed-Felder" + }, + "filename": "fields.json", + "configElements": true, + "content": [ + { + "name": "name", + "humanName": { + "en": "Feld-Name", + "de": "Feldname" + }, + "default": { + "en": "" + }, + "description": { + "en": "Name of the field", + "de": "Name des Feldes" + }, + "type": "string" + }, + { + "name": "content", + "humanName": { + "en": "Field-Content", + "de": "Feldinhalt" + }, + "default": { + "en": "" + }, + "description": { + "en": "Content of this field", + "de": "Inhalt dieses Feldes" + }, + "type": "string", + "params": [ + { + "name": "memberCount", + "description": { + "en": "Member-Count of this guild", + "de": "Anzahl von Mitgliedern auf deinem Server" + } + }, + { + "name": "botCount", + "description": { + "en": "Bot-Count of this guild", + "de": "Anzahl von Bots auf deinem Server" + } + }, + { + "name": "userCount", + "description": { + "en": "User-Count of this guild", + "de": "Anzahl von Nutzern auf deinem Server" + } + }, + { + "name": "onlineMemberCount", + "description": { + "en": "Count of online members on this guild", + "de": "Anzahl von online Mitgliern auf deinem Server" + } + }, + { + "name": "daysSinceCreation", + "description": { + "en": "Count of days passed since the creation of this guild", + "de": "Anzahl von vergangenen Tagen seit Erstellung deines Servers" + } + }, + { + "name": "guildCreationTimestamp", + "description": { + "en": "Show when the guild was created", + "de": "Datum und Uhrzeit, wenn der Server erstellt wurde" + } + }, + { + "name": "guildBoosts", + "description": { + "en": "Show how often this guild was boosted", + "de": "Zeigt die Anzahl von Boots auf dem Server an" + } + }, + { + "name": "boostLevel", + "description": { + "en": "Shows the current boost-level of this guild", + "de": "Zeigt das aktuelle Boost-Level des Servers an" + } + }, + { + "name": "boosterCount", + "description": { + "en": "Count of boosters on this guild", + "de": "Anzahl von Boostern auf deinem Server" + } + }, + { + "name": "channelCount", + "description": { + "en": "Count of channels on this guild", + "de": "Anzahl von Channeln auf deinem Server" + } + }, + { + "name": "roleCount", + "description": { + "en": "Count of roles on this guild", + "de": "Anzahl von Rollen auf deinem Server" + } + }, + { + "name": "emojiCount", + "description": { + "en": "Count of emojis on this guild", + "de": "Anzahl von Emojis auf deinem Server" + } + }, + { + "name": "newline", + "description": { + "en": "Inserts a new line", + "de": "Fügt eine neue Zeile ein (wie der Name schon sagt)" + } + }, + { + "name": "userWithRoleCount-", + "description": { + "en": "Count of members with a specific role (replace \"\" with an actual role-id)", + "de": "Anzahl von Nutzern mit einer bestimmen Rolle (bitte \"\" mit einer echten Rollen-ID ersetzen)" + } + }, + { + "name": "onlineUserWithRoleCount-", + "description": { + "en": "Count of members with a specific role who are online (replace \"\" with an actual role-id)", + "de": "Anzahl von Nutzern mit einer bestimmen Rolle, die online sind (bitte \"\" mit einer echten Rollen-ID ersetzen)" + } + } + ] + }, + { + "name": "inline", + "humanName": { + "en": "Inline Field?", + "de": "In-Zeilen-Feld?" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled the field will be inlined", + "de": "Wenn aktiviert wird das Feld bei Discord in einer Zeile mit anderen Feldern angezeigt" + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/serverinfo/events/botReady.js b/modules/serverinfo/events/botReady.js new file mode 100644 index 00000000..50c24806 --- /dev/null +++ b/modules/serverinfo/events/botReady.js @@ -0,0 +1,94 @@ +/** + * Manages the serverinfo-embed + * @module Partner-List + * @author Simon Csaba + */ +const {formatDate, formatDiscordUserName} = require('../../../src/functions/helpers'); +const {MessageEmbed} = require('discord.js'); +const {localize} = require('../../../src/functions/localize'); + +exports.run = async (client) => { + await generateEmbed(client); + const interval = setInterval(() => { + generateEmbed(client); + }, 300000); + client.intervals.push(interval); +}; + +/** + * Generates the serverinfo embed + * @param {Client} client + * @returns {Promise} + */ +async function generateEmbed(client) { + const config = client.configurations['serverinfo']['config']; + const fieldConfig = client.configurations['serverinfo']['fields']; + const channel = await client.channels.fetch(config.channelID).catch(() => { + }); + if (!channel && (channel || {}).type !== 'GUILD_TEXT') return client.logger.error(`[serverinfo] Could not find channel with id ${config.channelID}`); + const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); + const embed = new MessageEmbed() + .setTitle(config.embed.title) + .setDescription(config.embed.description) + .setColor(config.embed.color) + .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) + .setThumbnail(channel.guild.iconURL()) + .setAuthor({name: formatDiscordUserName(client.user), iconURL: client.user.avatarURL()}); + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + + const guildMembers = await channel.guild.members.fetch({withPresences: true}); + const guildCreationDate = new Date(channel.guild.createdAt); + const guildRoles = await channel.guild.roles.fetch(); + + /** + * Replaces the content with the variables of this module + * @private + * @param {String} content Content to replace variables in + * @returns {String} String with the variables replaced + */ + function replacer(content) { + /** + * Replaces the first member-with-role-count parameters of the input + * @private + */ + function replaceFirst() { + if (content.includes('%userWithRoleCount-')) { + const id = content.split('%userWithRoleCount-')[1].split('%')[0]; + if (content.includes(`%userWithRoleCount-${id}%`)) { + content = content.replaceAll(`%userWithRoleCount-${id}%`, guildMembers.filter(f => f.roles.cache.has(id)).size.toString()); + replaceFirst(); + } + } + if (content.includes('%onlineUserWithRoleCount-')) { + const id = content.split('%onlineUserWithRoleCount-')[1].split('%')[0]; + if (content.includes(`%onlineUserWithRoleCount-${id}%`)) { + content = content.replaceAll(`%onlineUserWithRoleCount-${id}%`, guildMembers.filter(f => f.roles.cache.has(id) && f.presence && (f.presence || {}).status !== 'offline').size.toString()); + replaceFirst(); + } + } + } + + replaceFirst(); + content = content.replaceAll('%memberCount%', guildMembers.size) + .replaceAll('%botCount%', guildMembers.filter(m => m.user.bot).size) + .replaceAll('%userCount%', guildMembers.filter(m => !m.user.bot).size) + .replaceAll('%onlineMemberCount%', guildMembers.filter(m => m.presence && (m.presence || {}).status !== 'offline').size) + .replaceAll('%daysSinceCreation%', ((new Date().getTime() - guildCreationDate.getTime()) / 86400000).toFixed(0)) + .replaceAll('%guildCreationTimestamp%', formatDate(guildCreationDate)) + .replaceAll('%guildBoosts%', channel.guild.premiumSubscriptionCount) + .replaceAll('%boostLevel%', localize('boostTier', channel.guild.premiumTier)) + .replaceAll('%channelCount%', channel.guild.channels.cache.size) + .replaceAll('%roleCount%', guildRoles.size) + .replaceAll('%emojiCount%', channel.guild.emojis.cache.size) + .replaceAll('%newline%', '\n') + .replaceAll('%boosterCount%', guildMembers.filter(m => m.premiumSinceTimestamp).size); + return content; + } + + fieldConfig.forEach(field => { + embed.addField(field.name, replacer(field.content), !!field.inline); + }); + + if (messages.first()) await messages.first().edit({embeds: [embed]}); + else await channel.send({embeds: [embed]}); +} \ No newline at end of file diff --git a/modules/serverinfo/module.json b/modules/serverinfo/module.json new file mode 100644 index 00000000..cc2e23fa --- /dev/null +++ b/modules/serverinfo/module.json @@ -0,0 +1,25 @@ +{ + "name": "serverinfo", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/serverinfo", + "events-dir": "/events", + "config-example-files": [ + "configs/config.json", + "configs/fields.json" + ], + "tags": [ + "community" + ], + "humanReadableName": { + "en": "Server-Information-Channel", + "de": "Serverinformationen" + }, + "description": { + "en": "Simple module to have a channel with a message that shows the users some information about your guild", + "de": "Fortgeschrittenes Modul, um einen Channel zu erstellen, in dem Server-Informationen als Embed angezeigt werden" + } +} \ No newline at end of file diff --git a/modules/starboard/configs/config.json b/modules/starboard/configs/config.json new file mode 100644 index 00000000..7a6ea9ae --- /dev/null +++ b/modules/starboard/configs/config.json @@ -0,0 +1,227 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "channelId", + "humanName": { + "en": "Starboard channel", + "de": "Starboard-Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "In which channel starred messages are sent", + "de": "In welchen Kanal gestarrte Nachrichten gesendet werden" + }, + "type": "channelID" + }, + { + "name": "emoji", + "humanName": { + "en": "Emoji" + }, + "default": { + "en": "⭐" + }, + "description": { + "en": "Which emoji should be used to star messages", + "de": "Mit welchem Emoji Nachrichten gestarrt werden sollen" + }, + "type": "emoji" + }, + { + "name": "message", + "humanName": { + "en": "Message", + "de": "Nachricht" + }, + "default": { + "en": { + "message": "**%stars%** %emoji% in %channelMention%", + "color": "#f5c91b", + "description": "%content%", + "image": "%image%", + "author": { + "name": "%displayName%", + "img": "%userAvatar%", + "url": "%link%" + } + } + }, + "description": { + "en": "This message gets send into the selected channel", + "de": "Diese Nachricht wird in den ausgewählten Kanal gesendet" + }, + "allowEmbed": true, + "type": "string", + "params": [ + { + "name": "stars", + "description": { + "en": "Amount of reactions on the message", + "de": "Anzahl der Reaktionen auf die Nachricht" + } + }, + { + "name": "content", + "description": { + "en": "The content of the starred message", + "de": "Der Inhalt der gestarrten Nachricht" + } + }, + { + "name": "link", + "description": { + "en": "A link to the starred message", + "de": "Ein Link zur gestarrten Nachricht" + } + }, + { + "name": "userID", + "description": { + "en": "The user ID of the author of the starred message", + "de": "Die Nutzer-ID des Autors der gestarrten Nachricht" + } + }, + { + "name": "userName", + "description": { + "en": "The username of the author of the starred message", + "de": "Der Benutzername des Autors der gestarrten Nachricht" + } + }, + { + "name": "displayName", + "description": { + "en": "The nickname of the author", + "de": "Der Nickname des Autors" + } + }, + { + "name": "userTag", + "description": { + "en": "The tag of the author of the starred message", + "de": "Der Tag des Autors der gestarrten Nachricht" + } + }, + { + "name": "userAvatar", + "description": { + "en": "The avatar URL of the message author", + "de": "Die Avatar-URL des Nachrichtenautors" + } + }, + { + "name": "channelName", + "description": { + "en": "The name of the channel the starred message was sent in", + "de": "Der Name des Kanals, in dem die gestarrte Nachricht gesendet wurde" + } + }, + { + "name": "channelMention", + "description": { + "en": "The channel mention of the channel the starred message was sent in", + "de": "Die Kanalerwähnung des Kanals, in dem die gestarrte Nachricht gesendet wurde" + } + }, + { + "name": "emoji", + "description": { + "en": "The set starboard emoji for lazy users", + "de": "Das festgelegte Starboard-Emoji für faule Nutzer" + } + }, + { + "name": "image", + "description": { + "en": "The first attachment or the first image url in the message", + "de": "Der erste Anhang oder die erste Bild-URL in der Nachricht" + } + } + ] + }, + { + "name": "excludedChannels", + "humanName": { + "en": "Excluded channels", + "de": "Ausgenommene Kanäle" + }, + "default": { + "en": [] + }, + "description": { + "en": "In which channels messages cannot be starred", + "de": "In welchen Kanälen Nachrichten nicht gestarrt werden können" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "excludedRoles", + "humanName": { + "en": "Excluded roles", + "de": "Ausgenommene Rollen" + }, + "default": { + "en": [] + }, + "description": { + "en": "Users with these roles cannot star messages", + "de": "Nutzer mit diesen Rollen können keine Nachrichten starren" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "minStars", + "humanName": { + "en": "Minimum stars", + "de": "Mindestanzahl Sterne" + }, + "default": { + "en": 3 + }, + "description": { + "en": "How many star reactions are needed for a message to land on the starboard", + "de": "Wie viele Star-Reaktionen benötigt werden, damit eine Nachricht auf dem Starboard landet" + }, + "type": "integer" + }, + { + "name": "starsPerHour", + "humanName": { + "en": "Stars per user per hour", + "de": "Sterne pro Nutzer pro Stunde" + }, + "default": { + "en": 5 + }, + "description": { + "en": "How many messages a user can star per hour", + "de": "Wie viele Nachrichten ein Nutzer pro Stunde starren kann" + }, + "type": "integer" + }, + { + "name": "selfStar", + "humanName": { + "en": "Self-Star" + }, + "default": { + "en": true + }, + "description": { + "en": "Whether users can star their own messages", + "de": "Ob Nutzer ihre eigenen Nachrichten starren können" + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/starboard/events/botReady.js b/modules/starboard/events/botReady.js new file mode 100644 index 00000000..796704f6 --- /dev/null +++ b/modules/starboard/events/botReady.js @@ -0,0 +1,15 @@ +const {Op} = require('sequelize'); +const schedule = require('node-schedule'); + +module.exports.run = async function (client) { + const job = schedule.scheduleJob('1 0 * * *', async () => { // Every day at 00:01 https://crontab.guru/#0_0_*_*_ + client.models['starboard']['StarUser'].destroy({ + where: { + createdAt: { + [Op.lt]: Date.now() - 1000 * 60 * 60 + } + } + }); + }); + client.jobs.push(job); +}; \ No newline at end of file diff --git a/modules/starboard/events/messageReactionAdd.js b/modules/starboard/events/messageReactionAdd.js new file mode 100644 index 00000000..b7c80509 --- /dev/null +++ b/modules/starboard/events/messageReactionAdd.js @@ -0,0 +1,6 @@ +const handleStarboard = require('../handleStarboard.js'); + +module.exports.run = async (client, msgReaction, user) => { + handleStarboard(client, msgReaction, user, false); +}; +module.exports.allowPartial = true; \ No newline at end of file diff --git a/modules/starboard/events/messageReactionRemove.js b/modules/starboard/events/messageReactionRemove.js new file mode 100644 index 00000000..5165eda4 --- /dev/null +++ b/modules/starboard/events/messageReactionRemove.js @@ -0,0 +1,6 @@ +const handleStarboard = require('../handleStarboard.js'); + +module.exports.run = async (client, msgReaction, user) => { + handleStarboard(client, msgReaction, user, true); +}; +module.exports.allowPartial = true; diff --git a/modules/starboard/handleStarboard.js b/modules/starboard/handleStarboard.js new file mode 100644 index 00000000..0594c2a1 --- /dev/null +++ b/modules/starboard/handleStarboard.js @@ -0,0 +1,103 @@ +const {embedTypeV2, disableModule, formatDiscordUserName} = require('../../src/functions/helpers'); +const {localize} = require('../../src/functions/localize'); +const {Op} = require('sequelize'); + +module.exports = async (client, msgReaction, user, isReactionRemove = false) => { + if (!client.botReadyAt) return; + const msg = msgReaction.message; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + if (msgReaction.partial) msgReaction = await msgReaction.fetch(); + + const starConfig = client.configurations['starboard']['config']; + if (!starConfig || starConfig.emoji !== msgReaction.emoji.toString()) return; + if (isNaN(starConfig.minStars)) return disableModule('starboard', localize('starboard', 'invalid-minstars', {stars: starConfig.minStars})); + + const channel = client.channels.cache.get(starConfig.channelId); + if (!channel) return disableModule('starboard', localize('partner-list', 'channel-not-found', {c: starConfig.channelId})); + if ((msg.channel.nsfw && !channel.nsfw) || starConfig.excludedChannels.includes(msg.channel.id) || starConfig.excludedRoles.some(r => msg.member.roles.cache.has(r))) return; + if (!starConfig.selfStar && user.id === msg.author.id) return msgReaction.users.remove(user.id).catch(() => { + }); + + const starUser = await client.models['starboard']['StarUser'].findAll({ + where: { + userId: user.id, + createdAt: { + [Op.gt]: Date.now() - 1000 * 60 * 60 + } + } + }); + + if (!isReactionRemove) { + if (starUser.length >= starConfig.starsPerHour) { + user.send(localize('starboard', 'star-limit', { + limitEmoji: '**' + starConfig.starsPerHour + '** ' + starConfig.emoji, + msgUrl: msg.url, + time: '' + })).catch(() => { + }); + msgReaction.users.remove(user.id).catch(() => { + }); + return; + } + + await client.models['starboard']['StarUser'].create({ + userId: user.id, + msgId: msg.id + }); + } + + let reactioncount = msgReaction.count; + if (!starConfig.selfStar && msgReaction.users.cache.has(msg.author.id)) reactioncount--; + + const starMsg = await client.models['starboard']['StarMsg'].findOne({ + where: { + msgId: msg.id + } + }); + + const starboardMsg = starMsg ? await channel.messages.fetch(starMsg.starMsg).catch(() => { + }) : null; + if (reactioncount < starConfig.minStars) { + if (isReactionRemove) { + if (starboardMsg) starboardMsg.delete(); + client.models['starboard']['StarMsg'].destroy({ + where: { + msgId: msg.id + } + }); + } + return; + } + + let image = msg.attachments.size > 0 ? msg.attachments.first().url : null; + if (!image) { + const matches = msg.content.match(/https?:\/\/.*\.(?:png|jpg|gif|jpeg|webp)/i); + if (matches) image = matches[0]; + } + + const generatedMsg = await embedTypeV2(starConfig.message, { + '%stars%': msgReaction.count, + '%content%': msg.content, + '%link%': msg.url, + '%userID%': msg.author.id, + '%userName%': msg.author.username, + '%displayName%': msg.member.displayName, + '%userTag%': formatDiscordUserName(msg.author), + '%userAvatar%': msg.member.displayAvatarURL({dynamic: true}), + '%channelName%': msg.channel.name, + '%channelMention%': '<#' + msg.channel.id + '>', + '%emoji%': msgReaction.emoji.toString(), + '%image%': image + }); + + if (starboardMsg) starboardMsg.edit(generatedMsg); + else { + const sentMessage = await channel.send(generatedMsg); + + client.models['starboard']['StarMsg'].create({ + msgId: msg.id, + starMsg: sentMessage.id + }); + } +}; \ No newline at end of file diff --git a/modules/starboard/models/StarMsg.js b/modules/starboard/models/StarMsg.js new file mode 100644 index 00000000..bfe3f4f0 --- /dev/null +++ b/modules/starboard/models/StarMsg.js @@ -0,0 +1,19 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class StarMsg extends Model { + static init(sequelize) { + return super.init({ + msgId: DataTypes.STRING, + starMsg: DataTypes.STRING + }, { + tableName: 'starboard_StarMsg', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'StarMsg', + 'module': 'starboard' +}; diff --git a/modules/starboard/models/StarUser.js b/modules/starboard/models/StarUser.js new file mode 100644 index 00000000..ba1d7b17 --- /dev/null +++ b/modules/starboard/models/StarUser.js @@ -0,0 +1,19 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class StarUser extends Model { + static init(sequelize) { + return super.init({ + userId: DataTypes.STRING, + msgId: DataTypes.STRING + }, { + tableName: 'starboard_StarUser', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'StarUser', + 'module': 'starboard' +}; diff --git a/modules/starboard/module.json b/modules/starboard/module.json new file mode 100644 index 00000000..a4cbd144 --- /dev/null +++ b/modules/starboard/module.json @@ -0,0 +1,24 @@ +{ + "name": "starboard", + "humanReadableName": { + "en": "Starboard" + }, + "author": { + "scnxOrgID": "60", + "name": "TomatoCake", + "link": "https://github.com/DEVTomatoCake" + }, + "description": { + "en": "Let users highlight messages into a starboard channel by reacting.", + "de": "Lass Nutzer Nachrichten durch eine Reaktion in einem Starboard-Kanal hervorheben." + }, + "events-dir": "/events", + "models-dir": "/models", + "config-example-files": [ + "configs/config.json" + ], + "tags": [ + "community" + ], + "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/starboard" +} \ No newline at end of file diff --git a/modules/status-roles/configs/config.json b/modules/status-roles/configs/config.json new file mode 100644 index 00000000..36d2fb0d --- /dev/null +++ b/modules/status-roles/configs/config.json @@ -0,0 +1,62 @@ +{ + "description": { + "en": "Configure the function of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "words", + "humanName": { + "en": "Words", + "de": "Statusinhalt" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Words users should have in their status.", + "de": "Wörter, die Nutzer in ihrem Status haben sollen." + }, + "type": "array", + "content": "string" + }, + { + "name": "roles", + "humanName": { + "en": "Roles", + "de": "Rollen" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Roles to give to users with one of the words in their status", + "de": "Rollen, die an Nutzer mit einem der Wörter im Status vergeben werden sollen" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "remove", + "humanName": { + "en": "Remove all other roles", + "de": "Entferne alle anderen Rollen" + }, + "default": { + "en": false + }, + "description": { + "en": "Remove all other roles from users with one of the words in their status", + "de": "Entferne alle anderen Rollen von Nutzern mit einem der Wörter im Status" + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/status-roles/events/presenceUpdate.js b/modules/status-roles/events/presenceUpdate.js new file mode 100644 index 00000000..e8043fd1 --- /dev/null +++ b/modules/status-roles/events/presenceUpdate.js @@ -0,0 +1,37 @@ +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (client, oldPresence, newPresence) { + + if (!client.botReadyAt) return; + if (newPresence.member.guild.id !== client.guildID) return; + const moduleConfig = client.configurations['status-roles']['config']; + const roles = moduleConfig.roles; + const status = moduleConfig.words; + const member = newPresence.member; + + if (newPresence.activities.length > 0) { + if (newPresence.activities[0].state) { + if (status.some(word => newPresence.activities[0].state.toLowerCase().includes(word.toLowerCase()))) { + if (moduleConfig.remove) await member.roles.remove(member.roles.cache.filter(role => !role.managed)); + return member.roles.add(roles, localize('status-role', 'fulfilled')); + } else { + removeRoles(); + } + } else { + removeRoles(); + } + } else { + removeRoles(); + } + + /** + * Removes the roles of a user who no longer fulfills the criteria + */ + function removeRoles() { + for (let i = 0; i < roles.length; i++) { + if (member.roles.cache.has(roles[i])) { + member.roles.remove(roles[i], localize('status-role', 'not-fulfilled')); + } + } + } +}; \ No newline at end of file diff --git a/modules/status-roles/module.json b/modules/status-roles/module.json new file mode 100644 index 00000000..f83b1f43 --- /dev/null +++ b/modules/status-roles/module.json @@ -0,0 +1,24 @@ +{ + "name": "status-roles", + "author": { + "name": "hfgd", + "link": "https://github.com/hfgd123", + "scnxOrgID": "2" + }, + "openSourceURL": "https://github.com/hfgd123/CustomDCBot/tree/main/modules/status-roles", + "events-dir": "/events", + "config-example-files": [ + "configs/config.json" + ], + "tags": [ + "administration" + ], + "humanReadableName": { + "en": "Status-roles", + "de": "Status-Rollen" + }, + "description": { + "en": "Simple module to reward users who have an invite to your server in their status!", + "de": "Einfaches Modul, um Nutzer zu belohnen, die einen Link zu deinem Server in ihrem Status haben!" + } +} \ No newline at end of file diff --git a/modules/sticky-messages/configs/sticky-messages.json b/modules/sticky-messages/configs/sticky-messages.json new file mode 100644 index 00000000..9722a565 --- /dev/null +++ b/modules/sticky-messages/configs/sticky-messages.json @@ -0,0 +1,60 @@ +{ + "description": { + "en": "Manage the sticky messages here", + "de": "Passe hier die Sticky-Nachrichten an" + }, + "humanName": { + "en": "Sticky messages", + "de": "Sticky-Nachrichten" + }, + "filename": "sticky-messages.json", + "configElements": true, + "content": [ + { + "name": "channelId", + "humanName": { + "en": "Channel", + "de": "Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel-ID in which the message should get send", + "de": "Kanal-ID, in welchem die Nachricht gesendet werden soll" + }, + "type": "channelID" + }, + { + "name": "message", + "humanName": { + "en": "Message", + "de": "Nachricht" + }, + "default": { + "en": "" + }, + "description": { + "en": "Message that should get send", + "de": "Nachricht, die gesendet werden soll" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "respondBots", + "humanName": { + "en": "Respond to bots", + "de": "Antworten auf Bots" + }, + "default": { + "en": false + }, + "description": { + "en": "Whether your bot reacts to messages from other bots in the channel", + "de": "Ob dein Bot auf Nachrichten von anderen Bots in dem Kanal reagiert" + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/sticky-messages/events/botReady.js b/modules/sticky-messages/events/botReady.js new file mode 100644 index 00000000..b8398c6f --- /dev/null +++ b/modules/sticky-messages/events/botReady.js @@ -0,0 +1,16 @@ +const {deleteMessage, sendMessage} = require('./messageCreate.js'); +let configCache = []; + +module.exports.run = async function (client) { + if (configCache.length === 0) { + configCache = client.configurations['sticky-messages']['sticky-messages']; + return; + } + + client.configurations['sticky-messages']['sticky-messages'].forEach(msg => { + if (configCache.find(c => c.channelId === msg.channelId && JSON.stringify(c.message) === JSON.stringify(msg.message))) return; + deleteMessage(client.user.id, client.channels.cache.get(msg.channelId)); + sendMessage(client.channels.cache.get(msg.channelId), msg.message); + }); + configCache = client.configurations['sticky-messages']['sticky-messages']; +}; \ No newline at end of file diff --git a/modules/sticky-messages/events/messageCreate.js b/modules/sticky-messages/events/messageCreate.js new file mode 100644 index 00000000..540ed764 --- /dev/null +++ b/modules/sticky-messages/events/messageCreate.js @@ -0,0 +1,73 @@ +const {embedTypeV2} = require('../../../src/functions/helpers'); +const channelData = {}; +const sendPending = new Set(); + +/** + * Deletes the sticky message sent by the bot + * @param {Snowflake} clientId User ID of the bot + * @param {Discord.TextBasedChannel} channel + */ +async function deleteMessage(clientId, channel) { + if (!channelData[channel.id]) return; + + let message; + message = await channel.messages.fetch(channelData[channel.id].msg).catch(async () => { + const msgs = await channel.messages.fetch({limit: 20}); + message = msgs.find(m => m.author.id === clientId); + }); + if (message) message.delete().catch(() => { + }); +} + +module.exports.deleteMessage = deleteMessage; + +/** + * Sends the message to the channel + * @param {Discord.TextBasedChannel} channel + * @param {Object|String} configMsg The configured message + */ +async function sendMessage(channel, configMsg) { + sendPending.add(channel.id); + channelData[channel.id] = { + msg: null, + timeout: null, + time: Date.now() + }; + const sentMessage = await channel.send(await embedTypeV2(configMsg)); + channelData[channel.id] = { + msg: sentMessage.id, + timeout: null, + time: Date.now() + }; + sendPending.delete(channel.id); +} + +module.exports.sendMessage = sendMessage; + +module.exports.run = async (client, msg) => { + if (!client.botReadyAt) return; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + if (!msg.member) return; + if (msg.author.id === client.user.id && sendPending.has(msg.channel.id)) return; + + const stickyChannels = client.configurations['sticky-messages']['sticky-messages']; + if (!stickyChannels) return; + + const currentConfig = stickyChannels.find(c => c.channelId === msg.channel.id); + if (!currentConfig || !currentConfig.message) return; + if (!currentConfig.respondBots && msg.author.bot) return; + + if (channelData[msg.channel.id]) { + if (channelData[msg.channel.id].time + 5000 > Date.now()) { + if (!channelData[msg.channel.id].timeout) channelData[msg.channel.id].timeout = setTimeout(() => { + deleteMessage(client.user.id, msg.channel); + sendMessage(msg.channel, currentConfig.message); + }, 5000); + return; + } + + deleteMessage(client.user.id, msg.channel); + sendMessage(msg.channel, currentConfig.message); + } else sendMessage(msg.channel, currentConfig.message); +}; \ No newline at end of file diff --git a/modules/sticky-messages/module.json b/modules/sticky-messages/module.json new file mode 100644 index 00000000..efe6db2f --- /dev/null +++ b/modules/sticky-messages/module.json @@ -0,0 +1,24 @@ +{ + "name": "sticky-messages", + "humanReadableName": { + "en": "Sticky messages", + "de": "Sticky-Nachrichten" + }, + "author": { + "scnxOrgID": "60", + "name": "TomatoCake", + "link": "https://github.com/DEVTomatoCake" + }, + "description": { + "en": "Let a set message always appear at the end of a channel.", + "de": "Lasse eine festgelegte Nachricht immer am Ende eines Kanals erscheinen." + }, + "events-dir": "/events", + "config-example-files": [ + "configs/sticky-messages.json" + ], + "tags": [ + "community" + ], + "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/sticky-messages" +} \ No newline at end of file diff --git a/modules/suggestions/commands/manage-suggestion.js b/modules/suggestions/commands/manage-suggestion.js new file mode 100644 index 00000000..dc750532 --- /dev/null +++ b/modules/suggestions/commands/manage-suggestion.js @@ -0,0 +1,129 @@ +const {generateSuggestionEmbed, notifyMembers} = require('../suggestion'); +const {localize} = require('../../../src/functions/localize'); +const {truncate, formatDiscordUserName} = require('../../../src/functions/helpers'); + +module.exports.beforeSubcommand = async function (interaction) { + interaction.suggestion = await interaction.client.models['suggestions']['Suggestion'].findOne({ + where: { + id: interaction.options.getString('id') + } + }); + if (!interaction.suggestion) { + await interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('suggestions', 'suggestion-not-found') + }); + interaction.returnEarly = true; + } else await interaction.deferReply({ephemeral: true}); +}; + +module.exports.subcommands = { + 'accept': async function (interaction) { + interaction.editType = 'approve'; + }, + 'deny': async function (interaction) { + interaction.editType = 'deny'; + } +}; + +module.exports.run = async function (interaction) { + if (interaction.returnEarly) return; + interaction.suggestion.adminAnswer = { + action: interaction.editType, + reason: interaction.options.getString('comment'), + userID: interaction.user.id + }; + await interaction.suggestion.save(); + await generateSuggestionEmbed(interaction.client, interaction.suggestion); + await notifyMembers(interaction.client, interaction.suggestion, 'team', interaction.user.id); + await interaction.editReply({content: '✅ ' + localize('suggestions', 'updated-suggestion')}); +}; + + +module.exports.autoComplete = { + 'comment': { + 'id': autoCompleteSuggestionID + } +}; + +/** + * Auto-Completes a suggestion id + * @param {Interaction} interaction Interaction to auto-complete up on + * @return {Promise} + */ +async function autoCompleteSuggestionID(interaction) { + const suggestions = await interaction.client.models['suggestions']['Suggestion'].findAll({ + order: [['createdAt', 'DESC']] + }); + const returnValue = []; + interaction.value = interaction.value.toLowerCase(); + for (const suggestion of suggestions.filter(s => formatDiscordUserName((interaction.client.guild.members.cache.get(s.suggesterID) || {user: {tag: s.suggesterID}}).user).toLowerCase().includes(interaction.value) || s.suggestion.toLowerCase().includes(interaction.value) || s.id.toString().includes(interaction.value) || s.messageID.includes(interaction.value))) { + if (returnValue.length !== 25) returnValue.push({ + value: suggestion.id.toString(), + name: truncate(`${formatDiscordUserName((interaction.client.guild.members.cache.get(suggestion.suggesterID) || {user: {tag: suggestion.suggesterID}}).user)}: ${suggestion.suggestion}`, 100) + }); + } + interaction.respond(returnValue); +} + +module.exports.autoCompleteSuggestionID = autoCompleteSuggestionID; + + +module.exports.autoComplete = { + 'accept': { + 'id': autoCompleteSuggestionID + }, + 'deny': { + 'id': autoCompleteSuggestionID + } +}; + + +module.exports.config = { + name: 'manage-suggestion', + defaultMemberPermissions: ['MANAGE_MESSAGES'], + description: localize('suggestions', 'manage-suggestion-command-description'), + + options: [ + { + type: 'SUB_COMMAND', + name: 'accept', + description: localize('suggestions', 'manage-suggestion-accept-description'), + options: [ + { + type: 'STRING', + name: 'id', + required: true, + autocomplete: true, + description: localize('suggestions', 'manage-suggestion-id-description') + }, + { + type: 'STRING', + name: 'comment', + required: true, + description: localize('suggestions', 'manage-suggestion-comment-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'deny', + description: localize('suggestions', 'manage-suggestion-deny-description'), + options: [ + { + type: 'STRING', + name: 'id', + required: true, + autocomplete: true, + description: localize('suggestions', 'manage-suggestion-id-description') + }, + { + type: 'STRING', + name: 'comment', + required: true, + description: localize('suggestions', 'manage-suggestion-comment-description') + } + ] + } + ] +}; \ No newline at end of file diff --git a/modules/suggestions/commands/suggestion.js b/modules/suggestions/commands/suggestion.js new file mode 100644 index 00000000..70d1cebc --- /dev/null +++ b/modules/suggestions/commands/suggestion.js @@ -0,0 +1,20 @@ +const {embedType} = require('../../../src/functions/helpers'); +const {createSuggestion} = require('../suggestion'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (interaction) { + await interaction.deferReply({ephemeral: true}); + const suggestionElement = await createSuggestion(interaction.guild, interaction.options.getString('suggestion'), interaction.user); + await interaction.editReply(embedType(interaction.client.configurations['suggestions']['config'].successfullySubmitted, {'%id%': suggestionElement.id})); +}; + +module.exports.config = { + name: 'suggestion', + description: localize('suggestions', 'suggest-description'), + options: [{ + type: 'STRING', + required: true, + name: 'suggestion', + description: localize('suggestions', 'suggest-content') + }] +}; \ No newline at end of file diff --git a/modules/suggestions/config.json b/modules/suggestions/config.json new file mode 100644 index 00000000..f9eacca2 --- /dev/null +++ b/modules/suggestions/config.json @@ -0,0 +1,452 @@ +{ + "description": { + "en": "Configure the function of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "commandsWarnings": { + "normal": [ + "/manage-suggestion" + ] + }, + "content": [ + { + "name": "suggestionChannel", + "humanName": { + "en": "Suggestion-Channel", + "de": "Vorschlagskanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel in which this module should operate", + "de": "Kanal in dem dieses Modul arbeiten soll" + }, + "type": "channelID" + }, + { + "name": "createSuggestionFromMessagesInChannel", + "humanName": { + "en": "Create suggestions from messages in channel", + "de": "Vorschläge von Nachrichten im Kanal erstellen" + }, + "default": { + "en": false, + "de": false + }, + "description": { + "en": "If enabled, the bot will create thread under each suggestion", + "de": "Wenn aktiviert, wird für jede Nachricht im Vorschlag-Kanal ein Vorschlag erstellt" + }, + "type": "boolean" + }, + { + "name": "reactions", + "humanName": { + "en": "Reactions", + "de": "Reaktionen" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Emojis with which the bot should react to a new suggestion", + "de": "Emojis mit denen der Bot auf neue Vorschläge reagieren soll" + }, + "type": "array", + "content": "emoji" + }, + { + "name": "allowUserComment", + "humanName": { + "en": "User-Comments in Threads", + "de": "Nutzerkommentare in Threads" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled, the bot will create thread under each suggestion", + "de": "Wenn aktiviert erstellt der Bot immer einen neuen Thread unter Vorschlägen" + }, + "type": "boolean" + }, + { + "name": "threadName", + "dependsOn": "allowUserComment", + "humanName": { + "en": "Thread-Name" + }, + "default": { + "en": "Comments", + "de": "Kommentare" + }, + "description": { + "en": "Name of the thread", + "de": "Name des Threads" + }, + "type": "string" + }, + { + "name": "successfullySubmitted", + "humanName": { + "en": "\"Successfully submitted\"-Message", + "de": "\"Erfolgreich eingereicht\"-Nachricht" + }, + "default": { + "en": "Suggestion %id% submitted successfully.", + "de": "Vorschlag %id% erfolgreich eingereicht." + }, + "description": { + "en": "This message gets send if a suggestion is submitted successfully.", + "de": "Diese Nachricht wird gesendet, wenn ein Vorschlag erfolgreich eingereicht wurde" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "id", + "description": { + "en": "ID of the suggestion", + "de": "ID des Vorschlags" + } + } + ] + }, + { + "name": "notifyRole", + "humanName": { + "en": "Notification-Role", + "de": "Benachrichtigungsrolle" + }, + "default": { + "en": "" + }, + "description": { + "en": "If set, this role gets pinged when a new suggestion gets created", + "de": "Wenn eine Rolle gesetzt ist, wird diese gepingt wenn ein neuer Vorschlag erstellt wird" + }, + "type": "roleID", + "allowNull": true + }, + { + "name": "sendPNNotifications", + "humanName": { + "en": "Send DM-Notifications", + "de": "PN-Benachrichtigungen senden" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled the creator and all commentators get a notification when something changes on a suggestion", + "de": "Wenn diese Option aktiviert ist, wird der Ersteller benachrichtigt, wenn sich etwas an einem Vorschlag ändert" + }, + "type": "boolean" + }, + { + "name": "teamChange", + "humanName": { + "en": "DM-Status-Notification", + "de": "PN-Statusbenachrichtigung" + }, + "default": { + "en": "Hi, a suggestion you are subscribed to got updated by a team member - read it here %url%", + "de": "Hi, ein von dir abonnierter Vorschlag wurde von einem Teammitglied beantwortet - lese ihn hier %url%" + }, + "description": { + "en": "This message gets send to the creator and all commentators when a suggestion gets updated and sendPNNotifications is enabled", + "de": "Diese Nachricht wird an den Ersteller und alle Nutzer, die einen Kommentar geschrieben haben, gesendet, wenn ein Vorschlag aktualisiert wird und \"PN-Benachrichtigungen senden\" aktiviert ist" + }, + "type": "string", + "dependsOn": "sendPNNotifications", + "allowEmbed": true, + "params": [ + { + "name": "url", + "description": { + "en": "URL to the suggestion", + "de": "URL zum Vorschlag" + } + }, + { + "name": "title", + "description": { + "en": "Title of the suggestion", + "de": "Titel des Vorschlags" + } + } + ] + }, + { + "name": "unansweredSuggestion", + "humanName": { + "en": "Unanswered Suggestion-Message", + "de": "Unbeantwortete Vorschlags-Nachricht" + }, + "default": { + "en": { + "title": "Suggestion #%id%", + "description": "%suggestion%", + "color": "#F1C40F", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Suggestion-Status", + "value": "No admin answered to this suggestion yet" + } + ] + }, + "de": { + "title": "Vorschlag #%id%", + "description": "%suggestion%", + "color": "#F1C40F", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Vorschlagsstatus", + "value": "Es hat noch kein Admin auf diesen Vorschlag geantwortet" + } + ] + } + }, + "description": { + "en": "This will be the messages that will get send when the user creates their suggestion and no admin has responded yet", + "de": "Das wird die Nachricht sein, die gesendet wird, wenn ein Nutzer seinen Vorschlag erstellt hat und noch kein Admin darauf geantwortet hat" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "id", + "description": { + "en": "ID of the suggestion", + "de": "ID des Vorschlags" + } + }, + { + "name": "suggestion", + "description": { + "en": "Content of the suggestion", + "de": "Inhalt des Vorschlags" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user who created this suggestion", + "de": "Tag des Users, der den Vorschlag erstellt hat" + } + }, + { + "name": "avatarURL", + "description": { + "en": "Avatar-URL of the user who created this suggestion", + "de": "Avatar-URL des Users, der den Vorschlag erstellt hat" + }, + "isImage": true + } + ] + }, + { + "name": "deniedSuggestion", + "humanName": { + "en": "Denied Suggestion-Message", + "de": "Abgelehnte Vorschlags-Nachricht" + }, + "default": { + "en": { + "title": "Suggestion #%id%", + "description": "%suggestion%", + "color": "#E74C3C", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Suggestion-Status: DENIED", + "value": "Denied by %adminUser% with the following reason: \"%adminMessage%\"" + } + ] + }, + "de": { + "title": "Vorschlag #%id%", + "description": "%suggestion%", + "color": "#E74C3C", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Vorschlags-Status: ABGELEHNT", + "value": "Abgelehnt von %adminUser% mit folgendem Grund: \"%adminMessage%\"" + } + ] + } + }, + "description": { + "en": "The suggestion will be edited to this message, when an admin denies a suggestion", + "de": "Zu dieser Nachricht wird der Vorschlag editiert, wenn ein Admin den Vorschlag ablehnt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "id", + "description": { + "en": "ID of the suggestion", + "de": "ID des Vorschlags" + } + }, + { + "name": "suggestion", + "description": { + "en": "Content of the suggestion", + "de": "Inhalt des Vorschlags" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user who created this suggestion", + "de": "Tag des Users, der den Vorschlag erstellt hat" + } + }, + { + "name": "avatarURL", + "description": { + "en": "Avatar-URL of the user who created this suggestion", + "de": "Avatar-URL des Users, der den Vorschlag erstellt hat" + }, + "isImage": true + }, + { + "name": "adminUser", + "description": { + "en": "Mention of the administrator who denied this suggestion", + "de": "Erwähnung des Administrators, der den Vorschlag abgelehnt hat" + } + }, + { + "name": "adminMessage", + "description": { + "en": "Message by administrator who denied this suggestion", + "de": "Nachricht des Administrators, der den Vorschlag abgelehnt hat" + } + } + ] + }, + { + "name": "approvedSuggestion", + "humanName": { + "en": "Approved Suggestion-Message", + "de": "Angenommene Vorschlags-Nachricht" + }, + "default": { + "en": { + "title": "Suggestion #%id%", + "description": "%suggestion%", + "color": "#2ECC71", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Suggestion-Status: APPROVED", + "value": "Approved by %adminUser% with the following reason: \"%adminMessage%\"" + } + ] + }, + "de": { + "title": "Vorschlag #%id%", + "description": "%suggestion%", + "color": "#2ECC71", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Vorschlagsstatus: ANGENOMMEN", + "value": "Wurde von %adminUser% mit folgendem Grund angenommen: \"%adminMessage%\"" + } + ] + } + }, + "description": { + "en": "The suggestion will be edited to this message, when an admin approves a suggestion", + "de": "Zu dieser Nachricht wird der Vorschlag editiert, wenn ein Admin den Vorschlag annimt" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "id", + "description": { + "en": "ID of the suggestion", + "de": "ID des Vorschlags" + } + }, + { + "name": "suggestion", + "description": { + "en": "Content of the suggestion", + "de": "Inhalt des Vorschlags" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user who created this suggestion", + "de": "Tag des Users, der den Vorschlag erstellt hat" + } + }, + { + "name": "avatarURL", + "description": { + "en": "Avatar-URL of the user who created this suggestion", + "de": "Avatar-URL des Users, der den Vorschlag erstellt hat" + }, + "isImage": true + }, + { + "name": "adminUser", + "description": { + "en": "Mention of the administrator who approved this suggestion", + "de": "Erwähnung des Administrators, der den Vorschlag angenommen hat" + } + }, + { + "name": "adminMessage", + "description": { + "en": "Message by administrator who approved this suggestion", + "de": "Nachricht des Administrators, der den Vorschlag angenommen hat" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/suggestions/events/messageCreate.js b/modules/suggestions/events/messageCreate.js new file mode 100644 index 00000000..6a6f9e94 --- /dev/null +++ b/modules/suggestions/events/messageCreate.js @@ -0,0 +1,8 @@ +const {createSuggestion} = require('../suggestion'); + +module.exports.run = async function (client, msg) { + if (msg.author.bot || !msg.guild || msg.guild.id !== client.config.guildID) return; + if (!client.configurations['suggestions']['config'].createSuggestionFromMessagesInChannel || client.configurations['suggestions']['config'].suggestionChannel !== msg.channel.id) return; + await msg.delete(); + await createSuggestion(msg.guild, msg.cleanContent, msg.author); +}; \ No newline at end of file diff --git a/modules/suggestions/models/Suggestion.js b/modules/suggestions/models/Suggestion.js new file mode 100644 index 00000000..d1eecebd --- /dev/null +++ b/modules/suggestions/models/Suggestion.js @@ -0,0 +1,27 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class Suggestion extends Model { + static init(sequelize) { + return super.init({ + id: { + autoIncrement: true, + type: DataTypes.INTEGER, + primaryKey: true + }, + suggestion: DataTypes.STRING, + messageID: DataTypes.STRING, + suggesterID: DataTypes.STRING, + comments: DataTypes.JSON, + adminAnswer: DataTypes.JSON + }, { + tableName: 'suggestions_Suggestion', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'Suggestion', + 'module': 'suggestions' +}; \ No newline at end of file diff --git a/modules/suggestions/module.json b/modules/suggestions/module.json new file mode 100644 index 00000000..395821b4 --- /dev/null +++ b/modules/suggestions/module.json @@ -0,0 +1,26 @@ +{ + "name": "suggestions", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/suggestions", + "commands-dir": "/commands", + "models-dir": "/models", + "config-example-files": [ + "config.json" + ], + "tags": [ + "administration" + ], + "humanReadableName": { + "en": "Suggestions", + "de": "Vorschläge" + }, + "events-dir": "/events", + "description": { + "en": "Advanced module to manage suggestions on your guild", + "de": "Modul mit vielen Funktionen, um Vorschläge auf deinem Discord zu managen" + } +} \ No newline at end of file diff --git a/modules/suggestions/suggestion.js b/modules/suggestions/suggestion.js new file mode 100644 index 00000000..76a11b84 --- /dev/null +++ b/modules/suggestions/suggestion.js @@ -0,0 +1,77 @@ +/** + * Manages suggestion-embeds + * @module Suggestions + * @author Simon Csaba + */ +const {embedType, formatDiscordUserName} = require('../../src/functions/helpers'); +const {localize} = require('../../src/functions/localize'); + +module.exports.generateSuggestionEmbed = generateSuggestionEmbed; + +async function generateSuggestionEmbed(client, suggestion) { + const moduleConfig = client.configurations['suggestions']['config']; + const channel = await client.channels.fetch(moduleConfig.suggestionChannel); + const message = await channel.messages.fetch(suggestion.messageID); + const user = await client.users.fetch(suggestion.suggesterID).catch(() => { + }); + + const params = { + '%id%': suggestion.id, + '%suggestion%': suggestion.suggestion, + '%tag%': formatDiscordUserName(user), + '%avatarURL%': user.avatarURL(), + '%adminUser%': suggestion.adminAnswer ? `<@${suggestion.adminAnswer.userID}>` : '', + '%adminMessage%': suggestion.adminAnswer ? suggestion.adminAnswer.reason : '' + }; + let field = 'unansweredSuggestion'; + if (suggestion.adminAnswer) { + if (suggestion.adminAnswer.action === 'approve') field = 'approvedSuggestion'; + else field = 'deniedSuggestion'; + } + await message.edit(embedType(moduleConfig[field], params)); +}; + +/** + * Notifies subscribed members of a suggestion about a change + * @param {Client} client + * @param {Object} suggestion Suggestion-Object + * @param {String} change Type of change + * @param {String} ignoredUserID User-ID of a user who should not get notified (usefully when they trigger the change) + * @returns {Promise} + */ +module.exports.notifyMembers = async function (client, suggestion, change, ignoredUserID = null) { + const moduleConfig = client.configurations['suggestions']['config']; + if (!moduleConfig['sendPNNotifications']) return; + const subscribedMembers = [suggestion.suggesterID]; + if (suggestion.adminAnswer) { + if (!subscribedMembers.includes(suggestion.adminAnswer.userID)) subscribedMembers.push(suggestion.adminAnswer.userID); + } + for (let user of subscribedMembers) { + if (user === ignoredUserID) continue; + user = await client.users.fetch(user).catch(() => { + }); + if (user) { + if (change === 'team') await user.send(embedType(moduleConfig['teamChange'], { + '%title%': suggestion.suggestion, + '%url%': `https://discord.com/channels/${client.guild.id}/${moduleConfig.suggestionChannel}/${suggestion.messageID}` + })).catch(() => { + }); + } + } +}; + +module.exports.createSuggestion = async function (guild, suggestion, user) { + const moduleConfig = guild.client.configurations['suggestions']['config']; + const channel = guild.channels.cache.get(moduleConfig.suggestionChannel); + const suggestionMsg = await channel.send(moduleConfig.notifyRole ? `<@&${moduleConfig.notifyRole}> ` + localize('suggestions', 'loading') : localize('suggestions', 'loading')); + if (moduleConfig.allowUserComment) await suggestionMsg.startThread({name: moduleConfig.threadName}); + if (moduleConfig.reactions) moduleConfig.reactions.forEach(reaction => suggestionMsg.react(reaction)); + const suggestionElement = await guild.client.models['suggestions']['Suggestion'].create({ + suggestion: suggestion, + messageID: suggestionMsg.id, + suggesterID: user.id, + comments: [] + }); + await generateSuggestionEmbed(guild.client, suggestionElement); + return suggestionElement; +}; \ No newline at end of file diff --git a/modules/team-list/config.json b/modules/team-list/config.json new file mode 100644 index 00000000..86c64ce0 --- /dev/null +++ b/modules/team-list/config.json @@ -0,0 +1,130 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "configElements": true, + "content": [ + { + "name": "channelID", + "humanName": { + "en": "Channel", + "de": "Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel-ID to run all operations in it", + "de": "Kanal-ID, in welchem alle Aktionen ausgeführt werden" + }, + "type": "channelID" + }, + { + "name": "roles", + "humanName": { + "en": "Listed Roles", + "de": "Gelistete Rollen" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Roles that should be listed in the embed", + "de": "Jede Rolle, die im Embed gelistet werden soll" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "descriptions", + "humanName": { + "en": "Descriptions of roles", + "de": "Beschreibung von Rollen" + }, + "default": { + "en": [], + "de": {} + }, + "description": { + "en": "Optional description of a listed role (Field 1: Role-ID, Field 2: Description)", + "de": "Optionale Beschreibung einer gelisteten Rolle (Feld 1: Rollen-ID, Feld 2: Beschreibung)" + }, + "type": "keyed", + "content": { + "key": "roleID", + "value": "string" + } + }, + { + "name": "embed", + "humanName": { + "en": "Embed" + }, + "default": { + "en": { + "title": "Our staff", + "description": "Meet our staff here", + "color": "GREEN", + "thumbnail-url": "", + "img-url": "" + }, + "de": { + "title": "Unser Team", + "description": "Hier findest du alle unsere Teammitglieder", + "color": "GREEN", + "thumbnail-url": "", + "img-url": "" + } + }, + "description": { + "en": "Configuration of the member-embed", + "de": "Konfiguration des Partner-Embeds" + }, + "type": "keyed", + "content": { + "key": "string", + "value": "string" + }, + "disableKeyEdits": true + }, + { + "name": "nameOverwrites", + "humanName": { + "en": "Name-Overwrites", + "de": "Name-Overwrites" + }, + "default": { + "en": [], + "de": {} + }, + "description": { + "en": "optional; Allows to overwrite the displayed name of roles (Field 1: Role-ID, Field 2: Displayed Name)", + "de": "optional; Allows to overwrite the displayed name of a role (Feld 1: Rollen-ID, Feld 2: Angezeigter Name)" + }, + "type": "keyed", + "content": { + "key": "roleID", + "value": "string" + } + }, + { + "name": "includeStatus", + "humanName": { + "en": "Include Online-Status of Staff-Members", + "de": "Online-Status von Teammitgliedern anzeigen" + }, + "description": { + "en": "If enabled, the current online status will be displayed in the staffmember-list", + "de": "Wenn aktiviert, wird der aktuelle Status in der Teammitglieder-Liste angezeigt" + }, + "type": "boolean", + "default": { + "en": false + } + } + ] +} \ No newline at end of file diff --git a/modules/team-list/events/botReady.js b/modules/team-list/events/botReady.js new file mode 100644 index 00000000..be5bbae8 --- /dev/null +++ b/modules/team-list/events/botReady.js @@ -0,0 +1,68 @@ +const isEqual = require('is-equal'); +const {disableModule, truncate} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); +const {MessageEmbed} = require('discord.js'); +const schedule = require('node-schedule'); + +const statusIcons = { + 'online': '🟢', + 'dnd': '🔴', + 'idle': '🟡', + 'offline': '⚫' +}; + +module.exports.run = async function (client) { + await updateEmbedsIfNeeded(client); + const job = schedule.scheduleJob('1,16,31,46 * * * *', async () => { + await updateEmbedsIfNeeded(client); + }); + client.jobs.push(job); +}; + +let lastSavedEmbed = null; + +/** + * Updates the embed if needed + * @param client + * @returns {Promise} + */ +async function updateEmbedsIfNeeded(client) { + const channels = client.configurations['team-list']['config']; + for (const channelConfig of channels) { + const embed = new MessageEmbed() + .setColor(channelConfig.embed.color) + .setTitle(channelConfig.embed.title) + .setDescription(channelConfig.embed.description) + .setTimestamp() + .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}); + + if (channelConfig.embed['thumbnail-url']) embed.setThumbnail(channelConfig.embed['thumbnail-url']); + if (channelConfig.embed['img-url']) embed.setImage(channelConfig.embed['img-url']); + + const channel = await client.channels.fetch(channelConfig['channelID']).catch(() => { + }); + if (!channel) return disableModule('team-list', localize('team-list', 'channel-not-found', {c: channelConfig['channelID']})); + const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); + const guildMembers = await channel.guild.members.fetch(); + + const roles = (await channel.guild.roles.fetch()).filter(f => channelConfig.roles.includes(f.id)).sort((a, b) => a.position < b.position ? 1 : -1); + for (const role of roles.values()) { + let userString = ''; + for (const member of guildMembers.filter(m => m.roles.cache.has(role.id)).values()) { + userString = userString + (channelConfig.includeStatus ? `* ${member.user.toString()}: ${statusIcons[(member.presence || {status: 'offline'}).status]} ${localize('team-list', (member.presence || {status: 'offline'}).status)}\n` : `${member.user.toString()}, `); + } + if (userString === '') userString = localize('team-list', 'no-users-with-role', {r: role.toString()}); + else if (!channelConfig.includeStatus) userString = userString.substring(0, userString.length - 2); + + embed.addField(channelConfig['nameOverwrites'][role.id] || role.name, truncate((channelConfig['descriptions'][role.id] ? `${channelConfig['descriptions'][role.id]}\n` : '') + userString, 1024)); + } + + if (embed.fields.length === 0) embed.addField('⚠️', localize('team-list', 'no-roles-selected')); + + if (isEqual(lastSavedEmbed, embed.toJSON())) return; + lastSavedEmbed = embed.toJSON(); + + if (messages.last()) await messages.last().edit({embeds: [embed]}); + else channel.send({embeds: [embed]}); + } +} \ No newline at end of file diff --git a/modules/team-list/module.json b/modules/team-list/module.json new file mode 100644 index 00000000..b1285587 --- /dev/null +++ b/modules/team-list/module.json @@ -0,0 +1,24 @@ +{ + "name": "team-list", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "events-dir": "/events", + "config-example-files": [ + "config.json" + ], + "tags": [ + "administration" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/team-list", + "humanReadableName": { + "en": "Staff-List", + "de": "Teammitglieder-Liste" + }, + "description": { + "en": "List all your staff and explain team-roles in a always up-to-date embed", + "de": "Liste alle deine Teammitglieder und erkläre sie in einem immer up-to-date Embed " + } +} \ No newline at end of file diff --git a/modules/temp-channels/channel-settings.js b/modules/temp-channels/channel-settings.js new file mode 100644 index 00000000..daee26be --- /dev/null +++ b/modules/temp-channels/channel-settings.js @@ -0,0 +1,318 @@ +const {client} = require('../../main'); +const {Op} = require('sequelize'); +const {embedType, formatDiscordUserName} = require('../../src/functions/helpers'); +const {localize} = require('../../src/functions/localize'); + +/** + * @param interaction + * @param callerInfo + * @returns {Promise} + */ +module.exports.channelMode = async function (interaction, callerInfo) { + const moduleConfig = interaction.client.configurations['temp-channels']['config']; + const vc = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + [Op.and]: [ + {id: interaction.member.voice.channelId}, + {creatorID: interaction.member.id} + ] + } + }); + const allowedUsers = vc.allowedUsers.split(','); + const vchann = interaction.guild.channels.cache.get(vc.id); + + let publicTemp = null; + if (callerInfo === 'command') { + publicTemp = interaction.options.getBoolean('public'); + } else if (callerInfo === 'buttonPublic') { + publicTemp = true; + } else if (callerInfo === 'buttonPrivate') { + publicTemp = false; + } + if (publicTemp) { + + await vchann.lockPermissions; + await vchann.permissionOverwrites.delete(vchann.guild.roles.everyone); + await interaction.editReply(embedType(moduleConfig['modeSwitched'], {'%mode%': 'public'}, {ephemeral: true})); + + } else if (!publicTemp) { + + await vchann.lockPermissions; + await vchann.permissionOverwrites.create(vchann.guild.roles.everyone, {'CONNECT': false}); + if (allowedUsers.at(0) !== '') { + for (const user of allowedUsers) { + await vchann.permissionOverwrites.create(interaction.guild.members.cache.get(user), {'CONNECT': true}); + } + } + interaction.editReply(embedType(moduleConfig['modeSwitched'], {'%mode%': 'private'}, {ephemeral: true})); + } + + vc.isPublic = publicTemp; + await vc.save; +}; + +/** + * @param interaction + * @param callerInfo + * @returns {Promise} + */ +module.exports.userAdd = async function (interaction, callerInfo) { + const moduleConfig = interaction.client.configurations['temp-channels']['config']; + const vc = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + [Op.and]: [ + {id: interaction.member.voice.channelId}, + {creatorID: interaction.member.id} + ] + } + }); + let allowedUsers = vc.allowedUsers; + let addedUser = null; + if (callerInfo === 'command') { + addedUser = interaction.options.getUser('user'); + } + if (callerInfo === 'modal') { + const addedUserString = interaction.fields.getTextInputValue('add-modal-input'); + try { + addedUser = interaction.guild.members.cache.find(member => formatDiscordUserName(member.user).replaceAll('@', '') === addedUserString).user; + } catch (e) { + try { + addedUser = await client.users.fetch(addedUserString); + } catch { + interaction.editReply(localize('temp-channels', 'user-not-found')); + return; + } + } + } + + if (allowedUsers === '') { + allowedUsers = addedUser.id; + } else { + allowedUsers = allowedUsers + ',' + addedUser.id; + } + vc.allowedUsers = allowedUsers; + await vc.save(); + const vchann = interaction.guild.channels.cache.get(vc.id); + if (!await vchann.permissionsFor(vchann.guild.roles.everyone).has('CONNECT') || !await vchann.permissionsFor(vchann.guild.roles.everyone).has('VIEW_CHANNEL')) { + await vchann.permissionOverwrites.create(addedUser, {'CONNECT': true, 'VIEW_CHANNEL': true}); + } + await interaction.editReply(embedType(moduleConfig['userAdded'], {'%user%': formatDiscordUserName(addedUser)}, {ephemeral: true})); +}; + +/** + * + * @param interaction + * @param callerInfo + * @returns {Promise} + */ +module.exports.userRemove = async function (interaction, callerInfo) { + const moduleConfig = interaction.client.configurations['temp-channels']['config']; + const vc = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + [Op.and]: [ + {id: interaction.member.voice.channelId}, + {creatorID: interaction.member.id} + ] + } + }); + let allowedUsers = vc.allowedUsers.split(','); + let removedUser = null; + if (callerInfo === 'command') { + removedUser = interaction.options.getUser('user'); + } + if (callerInfo === 'modal') { + const removedUserString = interaction.fields.getTextInputValue('remove-modal-input'); + try { + removedUser = interaction.guild.members.cache.find(member => formatDiscordUserName(member.user).replaceAll('@', '') === removedUserString).user; + } catch (e) { + try { + removedUser = await client.users.fetch(removedUserString); + } catch (f) { + interaction.editReply(localize('temp-channels', 'user-not-found')); + return; + } + } + } + const user = removedUser.id; + allowedUsers = allowedUsers.filter((e => e !== user)); + allowedUsers = allowedUsers.toString(); + vc.allowedUsers = allowedUsers; + await vc.save(); + const vchann = interaction.guild.channels.cache.get(vc.id); + try { + await vchann.permissionOverwrites.delete(removedUser); + } catch (e) { + console.log(e); + } + const usr = interaction.guild.members.cache.get(removedUser.id); + if (usr.voice.channelId === vc.id) { + try { + await usr.voice.disconnect(); + } catch (e) { + interaction.editReply(localize('temp-channels', 'no-disconnect')); + return; + } + } + interaction.editReply(embedType(moduleConfig['userRemoved'], {'%user%': formatDiscordUserName(removedUser)}, {ephemeral: true})); +}; + +module.exports.usersList = async function (interaction) { + const moduleConfig = interaction.client.configurations['temp-channels']['config']; + const vc = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + [Op.and]: [ + {id: interaction.member.voice.channelId}, + {creatorID: interaction.member.id} + ] + } + }); + const allowedUsersArray = vc.allowedUsers.split(','); + let allowedUsers = ''; + for (const user of allowedUsersArray) { + allowedUsers = allowedUsers + '\n • <@' + user + '>'; + } + if (allowedUsersArray.at(0) === '') { + interaction.editReply(localize('temp-channels', 'no-added-user')); + return; + } + interaction.editReply(moduleConfig['listUsers'] + ' ' + allowedUsers); +}; + +module.exports.channelEdit = async function (interaction, callerInfo) { + const moduleConfig = interaction.client.configurations['temp-channels']['config']; + const vc = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + [Op.and]: [ + {id: interaction.member.voice.channelId}, + {creatorID: interaction.member.id} + ] + } + }); + const vchann = interaction.guild.channels.cache.get(vc.id); + let edited = 0; + let vcNsfw = vchann.nsfw; + let vcBitrate = vchann.bitrate; + let vcLimit = vchann.userLimit; + let vcName = vchann.name; + if (callerInfo === 'command') { + if (interaction.options.getInteger('user-limit') >= 0) { + if (interaction.options.getInteger('user-limit') < 0 || interaction.options.getInteger('user-limit') > 99) { + interaction.editReply(localize('temp-channels', 'edit-error')); + return; + } + vcLimit = interaction.options.getInteger('user-limit'); + edited++; + } else vcLimit = vchann.userLimit; + if (interaction.options.getInteger('bitrate')) { + if (interaction.options.getInteger('bitrate') <= 8000 || interaction.options.getInteger('bitrate') >= interaction.guild.maximumBitrate) { + interaction.editReply(localize('temp-channels', 'edit-error')); + return; + } + vcBitrate = interaction.options.getInteger('bitrate'); + edited++; + } else vcBitrate = vchann.bitrate; + if (interaction.options.getString('name')) { + vcName = interaction.options.getString('name'); + edited++; + } else vcName = vchann.name; + if (interaction.options.getBoolean('nsfw')) { + vcNsfw = interaction.options.getBoolean('nsfw'); + edited++; + } else vcNsfw = vchann.nsfw; + } + if (callerInfo === 'modal') { + if (isNaN(interaction.fields.getTextInputValue('edit-modal-limit-input'))) { + interaction.editReply(localize('temp-channels', 'edit-error')); + return; + } + if (interaction.fields.getTextInputValue('edit-modal-limit-input') < 0 || interaction.fields.getTextInputValue('edit-modal-limit-input') > 99) { + interaction.editReply(localize('temp-channels', 'edit-error')); + return; + } + if (isNaN(interaction.fields.getTextInputValue('edit-modal-bitrate-input'))) { + interaction.editReply(localize('temp-channels', 'edit-error')); + return; + } + if (interaction.fields.getTextInputValue('edit-modal-bitrate-input') <= 8000 || interaction.fields.getTextInputValue('edit-modal-bitrate-input') >= interaction.guild.maximumBitrate) { + interaction.editReply(localize('temp-channels', 'edit-error')); + return; + } + + vcLimit = interaction.fields.getTextInputValue('edit-modal-limit-input'); + + vcBitrate = interaction.fields.getTextInputValue('edit-modal-bitrate-input'); + + vcName = interaction.fields.getTextInputValue('edit-modal-name-input'); + + const nsfwInput = interaction.fields.getTextInputValue('edit-modal-nsfw-input'); + vcNsfw = (nsfwInput === 'true'); + edited++; + } + + if (edited !== 0) { + interaction.editReply(embedType(moduleConfig['channelEdited'], {}, {ephemeral: true})); + try { + vchann.edit({userLimit: vcLimit, nsfw: vcNsfw, name: vcName, bitrate: vcBitrate}); + } catch (e) { + interaction.editReply(localize('temp-channels', 'edit-error')); + } + } else { + interaction.editReply(localize('temp-channels', 'nothing-changed')); + } +}; + +module.exports.sendMessage = async function (channel) { + const moduleConfig = client.configurations['temp-channels']['config']; + const components = [{ + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + label: localize('temp-channels', 'add-user'), + style: 'SUCCESS', + customId: 'tempc-add', + emoji: '➕' + }, + { + type: 'BUTTON', + label: localize('temp-channels', 'remove-user'), + style: 'DANGER', + customId: 'tempc-remove', + emoji: '➖' + }, + { + type: 'BUTTON', + label: localize('temp-channels', 'list-users'), + style: 'PRIMARY', + customId: 'tempc-list', + emoji: '📃' + }] + }, + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + label: localize('temp-channels', 'private-channel'), + style: 'SUCCESS', + customId: 'tempc-private', + emoji: '🔒' + }, + { + type: 'BUTTON', + label: localize('temp-channels', 'public-channel'), + style: 'DANGER', + customId: 'tempc-public', + emoji: '🔓' + }, + { + type: 'BUTTON', + label: localize('temp-channels', 'edit-channel'), + style: 'SECONDARY', + customId: 'tempc-edit', + emoji: '📝' + }] + }]; + const message = embedType(moduleConfig['settingsMessage'], {}, {components}); + channel.send(message); +}; \ No newline at end of file diff --git a/modules/temp-channels/commands/temp-channel.js b/modules/temp-channels/commands/temp-channel.js new file mode 100644 index 00000000..44ed867a --- /dev/null +++ b/modules/temp-channels/commands/temp-channel.js @@ -0,0 +1,140 @@ +const {localize} = require('../../../src/functions/localize'); +const {client} = require('../../../main'); +const {Op} = require('sequelize'); +const {channelMode, userAdd, userRemove, usersList, channelEdit} = require('../channel-settings'); + +module.exports.beforeSubcommand = async function (interaction) { + await interaction.deferReply({ephemeral: true}); + const vc = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + [Op.and]: [ + {id: interaction.member.voice.channelId}, + {creatorID: interaction.member.id} + ] + } + }); + + if (!vc) { + interaction.editReply(interaction.client.configurations['temp-channels']['config']['notInChannel']); + interaction.cancel = true; + } else interaction.cancel = false; +}; + +module.exports.subcommands = { + 'mode': async function (interaction) { + if (interaction.cancel) return; + await channelMode(interaction, 'command'); + }, + 'add-user': async function (interaction) { + if (interaction.cancel) return; + await userAdd(interaction, 'command'); + }, + 'remove-user': async function (interaction) { + if (interaction.cancel) return; + await userRemove(interaction, 'command'); + }, + 'list-users': async function (interaction) { + if (interaction.cancel) return; + await usersList(interaction, 'command'); + }, + 'edit': async function (interaction) { + if (interaction.cancel) return; + await channelEdit(interaction, 'command'); + } +}; + +module.exports.config = { + name: 'temp-channel', + description: localize('temp-channels', 'command-description'), + + options: function () { + const moduleConfig = client.configurations['temp-channels']['config']; + const conf = []; + if (moduleConfig['allowUserToChangeMode']) { + conf.push( + { + type: 'SUB_COMMAND', + name: 'mode', + description: localize('temp-channels', 'mode-subcommand-description'), + options: [ + { + type: 'BOOLEAN', + required: true, + name: 'public', + description: localize('temp-channels', 'public-option-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'add-user', + description: localize('temp-channels', 'add-subcommand-description'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('temp-channels', 'add-user-option-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'remove-user', + description: localize('temp-channels', 'remove-subcommand-description'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('temp-channels', 'remove-user-option-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'list-users', + description: localize('temp-channels', 'list-subcommand-description') + } + ); + } + + if (moduleConfig['allowUserToChangeName']) { + conf.push( + { + type: 'SUB_COMMAND', + name: 'edit', + description: localize('temp-channels', 'edit-subcommand-description'), + options: [ + { + type: 'INTEGER', + required: false, + name: 'user-limit', + description: localize('temp-channels', 'user-limit-option-description') + }, + { + type: 'INTEGER', + required: false, + name: 'bitrate', + description: localize('temp-channels', 'bitrate-option-description') + }, + { + type: 'STRING', + required: false, + name: 'name', + description: localize('temp-channels', 'name-option-description') + }, + { + type: 'BOOLEAN', + required: false, + name: 'nsfw', + description: localize('temp-channels', 'nsfw-option-description') + } + ] + } + ); + } + return conf; + } + +}; \ No newline at end of file diff --git a/modules/temp-channels/config.json b/modules/temp-channels/config.json new file mode 100644 index 00000000..274da8a3 --- /dev/null +++ b/modules/temp-channels/config.json @@ -0,0 +1,393 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "channelID", + "humanName": { + "en": "Channel", + "de": "Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "Set the channel here where users have to join to create their temp-channel", + "de": "Gebe hier die ID des Channels ein, in welchem Nutzer joinen müssen, um einen neuen Channel zu erstellen" + }, + "type": "channelID", + "content": [ + "GUILD_VOICE" + ] + }, + { + "name": "allowUserToChangeName", + "humanName": { + "en": "Allow editing the channel", + "de": "Kanaländerungen erlauben" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled the user has the permission to change the name and settings of the voicechanel via both, the Discord-integrated menus and the corresponding /-commands", + "de": "Wenn aktiviert erhält der Ersteller des Channel die Permission \"MANAGE_CHANNEL\" auf diesem Channel, sowie Zugriff auf die entsprechenden Befehle" + }, + "type": "boolean" + }, + { + "name": "timeout", + "humanName": { + "en": "Deletion timeout", + "de": "Löschverzögerung" + }, + "default": { + "en": 3, + "de": 3 + }, + "description": { + "en": "Set a timeout here in which the bot should wait before deleting the voicechannel (in secounds)", + "de": "Die Anzahl von Sekunden nach einem Channel-Leave, die der Bot warten soll, bevor er einen Channel löscht" + }, + "type": "integer", + "allowNull": true + }, + { + "name": "category", + "humanName": { + "en": "Category", + "de": "Kategorie" + }, + "default": { + "en": "" + }, + "description": { + "en": "You can set a category here in which the new channel should be created", + "de": "Gebe hier die ID der Kategorie an, in welcher neue Temp-Channel erstellt werden sollen" + }, + "type": "channelID", + "content": [ + "GUILD_CATEGORY" + ] + }, + { + "name": "channelname_format", + "humanName": { + "en": "Channel name", + "de": "Kanalname" + }, + "default": { + "en": "⏳ %username%", + "de": "⏳ %username%" + }, + "description": { + "en": "Change the format of the channel name here", + "de": "Du kannst das Format des Kanalnamens hier bearbeiten" + }, + "type": "string", + "params": [ + { + "name": "username", + "description": { + "en": "Username of the user", + "de": "Nutzername des Nutzers" + } + }, + { + "name": "nickname", + "description": { + "en": "Nickname of the member", + "de": "Nickname des Mitglieds" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + } + ] + }, + { + "name": "create_no_mic_channel", + "humanName": { + "en": "Create no-mic-channel", + "de": "No-Mic-Kanal erstellen" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled the bot will create a new channel for each voicechannel which can be only seen by users in the voicechannel", + "de": "Wenn aktiviert wird ein No-Mic-Textchannel für jeden Temp-Channel erstellt, auf welchen nur Nutzer Zugriff haben, die im VC sind" + }, + "type": "boolean" + }, + { + "name": "noMicChannelMessage", + "humanName": { + "en": "no-mic-channel-message", + "de": "No-Mic-Kanal-Nachricht" + }, + "default": { + "en": "Welcome to your no-mic-channel - you can only see this channel if you are in the connected voicechat", + "de": "Willkommen im deinem No-Mic-Kanal! Dieser wurde zu deinem Temp-Kanal erstellt, damit du mit Leuten chatten kannst, die kein Mikrofon haben. Beachte, dass dieser Channel nur von Nutzern gesehen werden kann, die im Sprachkanal mit dir sind. Beachte außerdem, dass dieser Channel gelöscht wird, wenn dein VC nicht mehr in Benutzung ist." + }, + "description": { + "en": "You can set a message here that should be send in the no-mic-channel when created", + "de": "Hier kannst du eine Nachricht festlegen, welche in einem No-Mic-Channel gesendet werden soll." + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "send_dm", + "humanName": { + "en": "Send DM", + "de": "PN senden" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "Should the bot send a direct message to a user when a new channel is created for them?", + "de": "Sollte beim Erstellen eines Temp-Channels eine PN an den Nutzer geschrieben werden?" + }, + "type": "boolean" + }, + { + "name": "dm", + "humanName": { + "en": "DM", + "de": "Privatnachricht" + }, + "default": { + "en": "I have created and moved you to your new voice-channel - have fun ^^", + "de": "Tach - ich habe dir nen eigenen Channel erstellt und dich gemovt - Dieser wird nach Inaktivität gelöscht - Have fun^^" + }, + "description": { + "en": "Set the message that should get send to the user if they join the voicechannel", + "de": "Hier kannst du die Nachricht festlegen, die an den Nutzer geschrieben soll (wenn aktiviert)" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "channelname", + "description": { + "en": "Name of the channel", + "de": "Name des Kanals" + } + } + ] + }, + { + "name": "publicChannels", + "humanName": { + "en": "Public channels", + "de": "Öffentliche Channel" + }, + "default": { + "en": true + }, + "description": { + "en": "Should the permissions for channels created by the bot be synced with their category?", + "de": "Sollen die Berechtigungen für vom Bot erstellte Kanäle mit deren Kategorie synchronisiert werden?" + }, + "type": "boolean" + }, + { + "name": "allowUserToChangeMode", + "humanName": { + "en": "Allow change of channel mode", + "de": "Kanaländerungen erlauben" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled the user has the permission to change the access-mode of the voicechanel", + "de": "Wenn aktiviert erhält der Ersteller des Channel die Möglichkeit die Zugriffsberechtigungen für den Kanal festzulegen" + }, + "type": "boolean" + }, + { + "name": "notInChannel", + "humanName": {}, + "default": { + "de": "Du musst in deinem Temp-Channel sein um das zu tun", + "en": "You have to be in your temp-channel to do this" + }, + "description": { + "en": "This message gets sent to a user, who tries to edit their channel, while not being in it", + "de": "Diese Nachricht wird an Nutzer gesendet, die versuchen ihren Kanal zu bearbeiten, während sie sich nicht darin befinden" + }, + "type": "string" + }, + { + "name": "modeSwitched", + "humanName": {}, + "default": { + "en": "The access-mode of your channel has been switched to %mode%", + "de": "Der Zugriffsmodus deines Kanals wurde auf %mode% geändert" + }, + "description": { + "en": "This message gets sent to a user, after they changed the mode of their channel", + "de": "Diese Nachricht wird an Nutzer gesendet, nachdem sie ihren Kanal bearbeitet haben" + }, + "type": "string", + "params": [ + { + "name": "mode", + "description": { + "en": "Mode of the channel", + "de": "Modus des Kanals" + } + } + ] + }, + { + "name": "userAdded", + "humanName": {}, + "default": { + "en": "The user %user% has beed added to your channel. They can now access it whenever they like to", + "de": "Der Nutzer %user% wurde zu deinem Kanal hinzugefügt. Er/Sie hat nun uneingeschränkten Zugang dazu" + }, + "description": { + "en": "This message gets sent to a user, after they added an user to their channel", + "de": "Diese Nachricht wird an Nutzer gesendet, nachdem sie einen Nutzer zu ihrem Kanal hinzugefügt haben" + }, + "type": "string", + "params": [ + { + "name": "user", + "description": { + "en": "The user, that was added", + "de": "Der hinzugefügte Nutzer" + } + } + ] + }, + { + "name": "userRemoved", + "humanName": {}, + "default": { + "en": "The user %user% has beed removed from your channel. They can no longer access it, while your channel is private", + "de": "Der Nutzer %user% wurde von deinem Kanal entfernt. Er/Sie hat nun keinen Zugriff mehr, während dein Kanal privat ist" + }, + "description": { + "en": "This message gets sent to a user, after they removed an user from their channel", + "de": "Diese Nachricht wird an Nutzer gesendet, nachdem sie einen Nutzer von ihrem Kanal entfernt haben" + }, + "type": "string", + "params": [ + { + "name": "user", + "description": { + "en": "The user, that was removed", + "de": "Der Nutzer, der entfernt wurde" + } + } + ] + }, + { + "name": "listUsers", + "humanName": {}, + "default": { + "en": "Here is a list of all the users that have access to your channel:", + "de": "Hier ist eine Liste aller Nutzer mit Zugang zu deinem Kanal:" + }, + "description": { + "de": "Die Nachricht die gesendet wird, wenn ein Nutzer eine Liste der Nutzer mit Zugang zu seinem Temp-Channel anfragt. Dieser Nachricht folgt automatisch eine Liste der Nutzer.", + "en": "The message to be sent, if a user requests a list of the users with access to their channel. This is automatically followed by a list of the users' tags." + }, + "type": "string" + }, + { + "name": "channelEdited", + "humanName": {}, + "default": { + "en": "Your channel was edited", + "de": "Dein Kanal wurde bearbeitet" + }, + "description": { + "en": "The message to be sent, if a user edited their channel", + "de": "Die Nachricht, die gesendet wird, wenn ein Nutzer seinen Kanal bearbeitet" + }, + "type": "string" + }, + { + "name": "edit-error", + "humanName": {}, + "default": { + "en": "An error occured while editing your channel. One or more of your settings could not be applied. This could be due to missing permissions or an invalid value", + "de": "Beim Bearbeiten des Kanals ist ein Fehler aufgetreten. Eine oder mehr deiner Einstellungen konnten nicht angewendet werden. Dies kann an fehlenden Rechten oder einem ungültigen Eingabewert liegen" + }, + "description": { + "en": "The message to be sent, if a user edited their channel, but it failed", + "de": "Die Nachricht, die gesendet wird, wenn das Bearbeiten eines Kanals fehlschlägt" + }, + "type": "string" + }, + { + "name": "settingsChannel", + "humanName": { + "de": "Einstellungskanal", + "en": "Settings channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "You can set a channel here in which the settings menu should be created. Leave this field empty, if you don't want to use this feature.", + "de": "Gebe hier die ID des Kanals an, in welcher das Einstellungsmenü erstellt werden soll. Lass dieses Feld leer, wenn du diese Funktion nicht verwenden willst." + }, + "type": "channelID", + "content": [ + "GUILD_TEXT" + ], + "allowNull": true + }, + { + "name": "useNoMic", + "humanName": { + "de": "No-Mic-Channel für Einstellungen verwenden" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled the settings menu will be sent into the no-mic-channel. If no-mic-channels aren't enabled, the menu will instead be sent to Discord's integrated text-in-voice channels", + "de": "Wenn aktiviert wird das Einstellungsmenü in den No-Mic-Channel gesendet. Wenn No-Mic-Channels nicht aktiviert sind, wird es stattdessen in die in Sprachkanälen integrierten Textkanäle gesendet." + }, + "type": "boolean" + }, + { + "name": "settingsMessage", + "humanName": { + "de": "Einstellungsnachricht" + }, + "default": { + "en": "Change the Settings of your temp-channnel here", + "de": "Ändere die Einstellungen deines Temp-Channels hier" + }, + "description": { + "en": "Set the message that should get send in the channel specified above to let the users change the settings of their temp-channels", + "de": "Hier kannst du die Nachricht festlegen, die in den weiter oben festgelegten Kanal gesendet werden soll, damit Nutzer ihre Temp-Channels bearbeiten können" + }, + "type": "string", + "allowEmbed": true, + "params": [] + } + ] +} \ No newline at end of file diff --git a/modules/temp-channels/events/botReady.js b/modules/temp-channels/events/botReady.js new file mode 100644 index 00000000..4d063d1b --- /dev/null +++ b/modules/temp-channels/events/botReady.js @@ -0,0 +1,67 @@ +const {migrate, embedType} = require('../../../src/functions/helpers'); +const {client} = require('../../../main'); +const {sendMessage} = require('../channel-settings'); +const {localize} = require('../../../src/functions/localize'); +module.exports.run = async function () { + const settingsChannel = client.channels.cache.get(client.configurations['temp-channels']['config']['settingsChannel']); + await migrate('temp-channels', 'TempChannelV1', 'TempChannel'); + + if (settingsChannel) { + const messages = (await settingsChannel.messages.fetch()).filter(msg => msg.author.id === client.user.id); + if (messages.first()) { + const moduleConfig = client.configurations['temp-channels']['config']; + const components = [{ + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + label: localize('temp-channels', 'add-user'), + style: 'SUCCESS', + customId: 'tempc-add', + emoji: '➕' + }, + { + type: 'BUTTON', + label: localize('temp-channels', 'remove-user'), + style: 'DANGER', + customId: 'tempc-remove', + emoji: '➖' + }, + { + type: 'BUTTON', + label: localize('temp-channels', 'list-users'), + style: 'PRIMARY', + customId: 'tempc-list', + emoji: '📃' + }] + }, + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + label: localize('temp-channels', 'public-channel'), + style: 'SUCCESS', + customId: 'tempc-public', + emoji: '🔓' + }, + { + type: 'BUTTON', + label: localize('temp-channels', 'private-channel'), + style: 'DANGER', + customId: 'tempc-private', + emoji: '🔒' + }, + { + type: 'BUTTON', + label: localize('temp-channels', 'edit-channel'), + style: 'SECONDARY', + customId: 'tempc-edit', + emoji: '📝' + }] + }]; + const message = embedType(moduleConfig['settingsMessage'], {}, {components}); + await messages.first().edit(message); + } else await sendMessage(settingsChannel); + } +}; \ No newline at end of file diff --git a/modules/temp-channels/events/channelDelete.js b/modules/temp-channels/events/channelDelete.js new file mode 100644 index 00000000..8f3b8855 --- /dev/null +++ b/modules/temp-channels/events/channelDelete.js @@ -0,0 +1,22 @@ +const {Op} = require('sequelize'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (client, channel) { + if (!client.botReadyAt) return; + if (!channel.id) return; + const dbChannel = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + [Op.or]: { + id: channel.id, + noMicChannel: channel.id + } + } + }); + if (dbChannel) { + const id = dbChannel.noMicChannel || dbChannel.id; + const otherChannel = await client.channels.fetch(id).catch(() => { + }); + if (otherChannel) await otherChannel.delete(`[temp-channels] ${localize('temp-channels', 'removed-audit-log-reason')}`).catch(e => console.error(e)); + await dbChannel.destroy(); + } +}; \ No newline at end of file diff --git a/modules/temp-channels/events/interactionCreate.js b/modules/temp-channels/events/interactionCreate.js new file mode 100644 index 00000000..a57abd5c --- /dev/null +++ b/modules/temp-channels/events/interactionCreate.js @@ -0,0 +1,191 @@ +const {MessageActionRow, Modal, TextInputComponent} = require('discord.js'); +const {usersList, channelMode, userAdd, userRemove, channelEdit} = require('../channel-settings'); +const {localize} = require('../../../src/functions/localize'); +const {Op} = require('sequelize'); + +module.exports.run = async function (client, interaction) { + if (!client.botReadyAt) return; + if (interaction.guild.id !== client.config.guildID) return; + if (interaction.isButton()) { + const vc = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + [Op.and]: [ + {id: interaction.member.voice.channelId}, + {creatorID: interaction.member.id} + ] + } + }); + + + if (interaction.customId === 'tempc-add') { + if (!vc) { + interaction.reply({ + ephemeral: true, + content: interaction.client.configurations['temp-channels']['config']['notInChannel'] + }); + return; + } + const modal = new Modal() + .setCustomId('tempc-add-modal') + .setTitle(localize('temp-channels', 'add-modal-title')); + const userInput = new TextInputComponent() + .setCustomId('add-modal-input') + .setLabel(localize('temp-channels', 'add-modal-prompt')) + .setStyle('SHORT') + .setPlaceholder('User#1234'); + const actionRow = new MessageActionRow().addComponents(userInput); + modal.addComponents(actionRow); + await interaction.showModal(modal); + } + if (interaction.customId === 'tempc-remove') { + if (!vc) { + interaction.reply({ + ephemeral: true, + content: interaction.client.configurations['temp-channels']['config']['notInChannel'] + }); + return; + } + const modal = new Modal() + .setCustomId('tempc-remove-modal') + .setTitle(localize('temp-channels', 'remove-modal-title')); + const userInput = new TextInputComponent() + .setCustomId('remove-modal-input') + .setLabel(localize('temp-channels', 'remove-modal-prompt')) + .setStyle('SHORT') + .setPlaceholder('User#1234'); + const actionRow = new MessageActionRow().addComponents(userInput); + modal.addComponents(actionRow); + await interaction.showModal(modal); + } + if (interaction.customId === 'tempc-list') { + if (!vc) { + interaction.reply({ + ephemeral: true, + content: interaction.client.configurations['temp-channels']['config']['notInChannel'] + }); + return; + } + await interaction.deferReply({ephemeral: true}); + await usersList(interaction); + } + if (interaction.customId === 'tempc-private') { + if (!vc) { + interaction.reply({ + ephemeral: true, + content: interaction.client.configurations['temp-channels']['config']['notInChannel'] + }); + return; + } + await interaction.deferReply({ephemeral: true}); + await channelMode(interaction, 'buttonPrivate'); + } + if (interaction.customId === 'tempc-public') { + if (!vc) { + interaction.reply({ + ephemeral: true, + content: interaction.client.configurations['temp-channels']['config']['notInChannel'] + }); + return; + } + await interaction.deferReply({ephemeral: true}); + await channelMode(interaction, 'buttonPublic'); + } + if (interaction.customId === 'tempc-edit') { + if (!vc) { + interaction.reply({ + ephemeral: true, + content: interaction.client.configurations['temp-channels']['config']['notInChannel'] + }); + return; + } + const vchann = interaction.guild.channels.cache.get(vc.id); + const modal = new Modal() + .setCustomId('tempc-edit-modal') + .setTitle(localize('temp-channels', 'edit-modal-title')); + const nsfwInput = new TextInputComponent() + .setCustomId('edit-modal-nsfw-input') + .setLabel(localize('temp-channels', 'edit-modal-nsfw-prompt')) + .setRequired(true) + .setStyle('SHORT') + .setPlaceholder(localize('temp-channels', 'edit-modal-nsfw-placeholder')) + .setValue(vchann.nsfw.toString()); + + + const bitrateInput = new TextInputComponent() + .setCustomId('edit-modal-bitrate-input') + .setLabel(localize('temp-channels', 'edit-modal-bitrate-prompt')) + .setRequired(true) + .setStyle('SHORT') + .setPlaceholder(localize('temp-channels', 'edit-modal-bitrate-placeholder')) + .setValue(vchann.bitrate.toString()); + + const limitInput = new TextInputComponent() + .setCustomId('edit-modal-limit-input') + .setLabel(localize('temp-channels', 'edit-modal-limit-prompt')) + .setRequired(true) + .setStyle('SHORT') + .setPlaceholder(localize('temp-channels', 'edit-modal-limit-placeholder')) + .setValue(vchann.userLimit.toString()); + + const nameInput = new TextInputComponent() + .setCustomId('edit-modal-name-input') + .setLabel(localize('temp-channels', 'edit-modal-name-prompt')) + .setRequired(true) + .setStyle('SHORT') + .setPlaceholder(localize('temp-channels', 'edit-modal-name-placeholder')) + .setValue(vchann.name); + + const nsfwRow = new MessageActionRow().addComponents(nsfwInput); + const bitrateRow = new MessageActionRow().addComponents(bitrateInput); + const limitRow = new MessageActionRow().addComponents(limitInput); + const nameRow = new MessageActionRow().addComponents(nameInput); + modal.addComponents(bitrateRow); + modal.addComponents(limitRow); + modal.addComponents(nameRow); + modal.addComponents(nsfwRow); + await interaction.showModal(modal); + } + } else if (interaction.isModalSubmit()) { + const vc = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + [Op.and]: [ + {id: interaction.member.voice.channelId}, + {creatorID: interaction.member.id} + ] + } + }); + if (interaction.customId === 'tempc-add-modal') { + if (!vc) { + interaction.reply({ + ephemeral: true, + content: interaction.client.configurations['temp-channels']['config']['notInChannel'] + }); + return; + } + await interaction.deferReply({ephemeral: true}); + await userAdd(interaction, 'modal'); + } + if (interaction.customId === 'tempc-remove-modal') { + if (!vc) { + interaction.reply({ + ephemeral: true, + content: interaction.client.configurations['temp-channels']['config']['notInChannel'] + }); + return; + } + await interaction.deferReply({ephemeral: true}); + await userRemove(interaction, 'modal'); + } + if (interaction.customId === 'tempc-edit-modal') { + if (!vc) { + interaction.reply({ + ephemeral: true, + content: interaction.client.configurations['temp-channels']['config']['notInChannel'] + }); + return; + } + await interaction.deferReply({ephemeral: true}); + await channelEdit(interaction, 'modal'); + } + } +}; \ No newline at end of file diff --git a/modules/temp-channels/events/voiceStateUpdate.js b/modules/temp-channels/events/voiceStateUpdate.js new file mode 100644 index 00000000..47bce223 --- /dev/null +++ b/modules/temp-channels/events/voiceStateUpdate.js @@ -0,0 +1,113 @@ +const {embedType} = require('./../../../src/functions/helpers'); +const {Op} = require('sequelize'); +const {localize} = require('../../../src/functions/localize'); +const {sendMessage} = require('../channel-settings'); +const {formatDiscordUserName} = require('../../../src/functions/helpers'); + +module.exports.run = async function (client, oldState, newState) { + if (!client.botReadyAt) return; + const moduleConfig = client.configurations['temp-channels']['config']; + + if (oldState.channel) { + const oldChannel = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + id: oldState.channel.id + } + }); + if (oldChannel) { + setTimeout(async () => { + const dcOldChannel = await client.channels.fetch(oldChannel.id).catch(() => { + }); + if (dcOldChannel && dcOldChannel.members.size === 0) { + await dcOldChannel.delete(`[temp-channels] ${localize('temp-channels', 'removed-audit-log-reason')}`).catch(() => { + }); + await oldChannel.destroy(); + } + }, moduleConfig['timeout'] * 1000); + } + } + + if (moduleConfig['create_no_mic_channel']) { + const possibleExistingChannel = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + [Op.or]: [ + {id: newState.channel ? newState.channel.id : false}, + {id: oldState.channel ? oldState.channel.id : false} + ] + } + }); + if (possibleExistingChannel) { + const existingNoMicChannel = await newState.guild.channels.cache.get(possibleExistingChannel.noMicChannel); + if (existingNoMicChannel) await existingNoMicChannel.permissionOverwrites.create(newState.member, { + 'VIEW_CHANNEL': newState.channel && newState.channel.id === possibleExistingChannel.id + }, {reason: '[temp-channels] ' + localize('temp-channels', 'permission-update-audit-log-reason')}); + } + } + + if (!newState.channel) return; + + if (newState.channel.id === moduleConfig['channelID']) { + const alreadyExistingChannel = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + creatorID: newState.member.user.id + } + }); + if (alreadyExistingChannel) return newState.setChannel(alreadyExistingChannel.id, `[temp-channels] ` + localize('temp-channels', 'move-audit-log-reason')).catch(() => { + newState.setChannel(null, '[temp-channels] ' + localize('temp-channels', 'disconnect-audit-log-reason')); + alreadyExistingChannel.destroy(); + }); + const newChannel = await newState.guild.channels.create(moduleConfig['channelname_format'] + .split('%username%').join(newState.member.user.username) + .split('%nickname%').join(newState.member.nickname || newState.member.user.username) + .split('%tag%').join(formatDiscordUserName(newState.member.user)), + { + type: 'GUILD_VOICE', + parent: moduleConfig['category'], + reason: '[temp-channels] ' + localize('temp-channels', 'created-audit-log-reason', {u: formatDiscordUserName(newState.member.user)}) + }); + await newState.setChannel(newChannel.id); + if (moduleConfig['allowUserToChangeName']) await newChannel.permissionOverwrites.create(newState.member, {'MANAGE_CHANNELS': true}, { + reason: '[temp-channels] ' + localize('temp-channels', 'created-audit-log-reason', {u: formatDiscordUserName(newState.member.user)}) + }); + if (moduleConfig['send_dm']) await newState.member.user.send(embedType(moduleConfig['dm'], {'%channelname%': newChannel.name})).catch(() => { + }); + + let noMicChannel = null; + if (moduleConfig['create_no_mic_channel']) { + const everyoneRole = await newChannel.guild.roles.cache.find(role => role.name === '@everyone'); + noMicChannel = await newChannel.guild.channels.create(`${newChannel.name}-no-mic`, { + type: 'GUILD_TEXT', + parent: moduleConfig['category'], + topic: localize('temp-channels', 'no-mic-channel-topic', {u: formatDiscordUserName(newState.member.user)}), + reason: '[temp-channels] ' + localize('temp-channels', 'created-audit-log-reason', {u: formatDiscordUserName(newState.member.user)}), + permissionOverwrites: [ + { + id: everyoneRole, + deny: ['VIEW_CHANNEL'] + } + ] + }); + await noMicChannel.permissionOverwrites.create(newState.member, { + 'VIEW_CHANNEL': true + }, { + reason: '[temp-channels] ' + localize('temp-channels', 'created-audit-log-reason', {u: formatDiscordUserName(newState.member.user)}) + }); + await noMicChannel.send(embedType(moduleConfig['noMicChannelMessage'])).then(m => m.pin()); + if (moduleConfig['useNoMic']) { + await sendMessage(noMicChannel); + } + } + await client.models['temp-channels']['TempChannel'].create({ + creatorID: newState.member.user.id, + id: newChannel.id, + noMicChannel: noMicChannel ? noMicChannel.id : null, + allowedUsers: newState.member.user.id, + isPublic: moduleConfig['publicChannels'] + }); + if (moduleConfig['useNoMic']) { + if (!moduleConfig['create_no_mic_channel']) { + await sendMessage(newChannel); + } + } + } +}; \ No newline at end of file diff --git a/modules/temp-channels/locales.json b/modules/temp-channels/locales.json new file mode 100644 index 00000000..3b105afc --- /dev/null +++ b/modules/temp-channels/locales.json @@ -0,0 +1,29 @@ +{ + "en": { + "temp-channels": { + "removed-audit-log-reason": "Removed temp channel, because no one was in it", + "permission-update-audit-log-reason": "Updated permissions, to make sure only people in the VC can see the no-mic-channel", + "created-audit-log-reason": "Created Temp-Channel for %u", + "move-audit-log-reason": "Moved user to their voice channel", + "no-mic-channel-topic": "Welcome to %u's no-mic-channel. You will see this channel as long as you are connected to this temp-channel.", + "disconnect-audit-log-reason": "The old channel of the user could not be found - disconnecting them - hopefully they join again", + "command-description": "Manage your temp-channel", + "mode-subcommand-description": "Change the mode of your channel", + "public-option-description": "local public-option-description", + "add-subcommand-description": "Add users, that will be able to join your channel, while it is private", + "remove-subcommand-description": "Remove users from you channel", + "add-user-option-description": "The user to be added", + "remove-user-option-description": "The user to be removed", + "list-subcommand-description": "List the users with access to your channel", + "edit-subcommand-description": "Edit various settings of yout channel", + "user-limit-option-description": "Change the user-limit of your channel", + "bitrate-option-description": "Change the bitrate of your channel (min. 8000)", + "name-option-description": "Change the name of your channel", + "nsfw-option-description": "Change, whether your channel is age-restricted or not", + "no-added-user": "There are no users to be displayed here", + "nothing-changed": "Your channel already had these settings.", + "no-disconnect": "Couldn't disconnect the user from your channel. This could be due to missing permissions, or the user not being in your voice-channel", + "edit-error": "An error occurred while editing your channel. one or more of your settings couldn't be applied. This could be due to missing permissions or an invalid value." + } + } +} \ No newline at end of file diff --git a/modules/temp-channels/models/TempChannel.js b/modules/temp-channels/models/TempChannel.js new file mode 100644 index 00000000..4858794b --- /dev/null +++ b/modules/temp-channels/models/TempChannel.js @@ -0,0 +1,25 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class TempChannel extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.STRING, + primaryKey: true + }, + creatorID: DataTypes.STRING, + noMicChannel: DataTypes.STRING, + allowedUsers: DataTypes.STRING, + isPublic: DataTypes.BOOLEAN + }, { + tableName: 'temp-channel_TempChannelsv2', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'TempChannel', + 'module': 'temp-channels' +}; \ No newline at end of file diff --git a/modules/temp-channels/models/TempChannelV1.js b/modules/temp-channels/models/TempChannelV1.js new file mode 100644 index 00000000..db26cfc5 --- /dev/null +++ b/modules/temp-channels/models/TempChannelV1.js @@ -0,0 +1,23 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class TempChannelV1 extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.STRING, + primaryKey: true + }, + creatorID: DataTypes.STRING, + noMicChannel: DataTypes.STRING + }, { + tableName: 'temp-channel_TempChannels', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'TempChannelV1', + 'module': 'temp-channels' +}; \ No newline at end of file diff --git a/modules/temp-channels/module.json b/modules/temp-channels/module.json new file mode 100644 index 00000000..6322f971 --- /dev/null +++ b/modules/temp-channels/module.json @@ -0,0 +1,26 @@ +{ + "name": "temp-channels", + "author": { + "scnxOrgID": "2", + "name": "hfgd", + "link": "https://github.com/hfgd123" + }, + "models-dir": "/models", + "events-dir": "/events", + "commands-dir": "/commands", + "config-example-files": [ + "config.json" + ], + "tags": [ + "community" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/temp-channels", + "humanReadableName": { + "en": "Temporary channels", + "de": "Temporäre Channel" + }, + "description": { + "en": "Allow users to quickly create voice channels by joining a voicechannel", + "de": "Erlaube es Nutzern, ihren eigenen Voice-Channel zu erstellen, indem sie einem VC joinen" + } +} \ No newline at end of file diff --git a/modules/tic-tak-toe/commands/tic-tac-toe.js b/modules/tic-tak-toe/commands/tic-tac-toe.js new file mode 100644 index 00000000..8c756271 --- /dev/null +++ b/modules/tic-tak-toe/commands/tic-tac-toe.js @@ -0,0 +1,247 @@ +const {localize} = require('../../../src/functions/localize'); +const {randomElementFromArray} = require('../../../src/functions/helpers'); + +module.exports.run = async function (interaction) { + const member = interaction.options.getMember('user', true); + if (member.user.id === interaction.user.id) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('tic-tac-toe', 'self-invite-not-possible', {r: `<@${((await interaction.guild.members.fetch({withPresences: true})).filter(u => u.presence && u.user.id !== interaction.user.id && !u.user.bot).random() || {user: {id: 'RickAstley'}}).user.id}>`}) + }); + const rep = await interaction.reply({ + content: localize('tic-tac-toe', 'challenge-message', {t: member.toString(), u: interaction.user.toString()}), + allowedMentions: { + users: [member.user.id] + }, + fetchReply: true, + components: [ + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + style: 'PRIMARY', + customId: 'accept-invite', + label: localize('tic-tac-toe', 'accept-invite') + }, + { + type: 'BUTTON', + style: 'SECONDARY', + customId: 'deny-invite', + label: localize('tic-tac-toe', 'deny-invite') + } + ] + } + ] + }); + let started = false; + let ended = false; + let endReason = null; + let gameEndReasonType = null; + let currentUser = randomElementFromArray([interaction.member, member]); + const a = rep.createMessageComponentCollector({componentType: 'BUTTON'}); + setTimeout(() => { + if (started || a.ended) return; + endReason = localize('tic-tac-toe', 'invite-expired', {u: interaction.user.toString(), i: member.toString()}); + a.stop(); + }, 120000); + + const grid = { + 1: { + 1: null, + 2: null, + 3: null + }, + 2: { + 1: null, + 2: null, + 3: null + }, + 3: { + 1: null, + 2: null, + 3: null + } + }; + + /** + * Checks if game ended + * @private + * @returns {boolean} + */ + function checkGameEnded() { + if (ended) return true; + let allPassed = true; + const lastUser = currentUser.user.id === interaction.user.id ? member : interaction.member; + + /** + * Returns values from blocks above, below, left and right if the block is user owned + * @param rID ID of the row + * @param id ID of column + * @private + * @returns {{below: boolean, left: boolean, above: boolean, right: boolean}|void} + */ + function checkBlock(rID, id) { + rID = parseInt(rID); + id = parseInt(id); + const value = grid[rID][id]; + if (value !== lastUser.user.id) return; + let above, below; + if (!grid[rID - 1]) above = null; + else above = grid[rID - 1][id] === value; + if (!grid[rID + 1]) below = null; + else below = grid[rID + 1][id] === value; + const left = typeof grid[rID][id - 1] === 'undefined' ? null : (grid[rID][id - 1] === value); + const right = typeof grid[rID][id + 1] === 'undefined' ? null : (grid[rID][id + 1] === value); + return {above, below, left, right}; + } + + for (const rID in grid) { + for (const id in grid[rID]) { + if (grid[rID][id] === null) allPassed = false; + const cB = checkBlock(rID, id); + if (!cB) continue; + let x = 0; + let y = 0; + if (cB.above) y++; + if (cB.below) y++; + if (cB.left) x++; + if (cB.right) x++; + let diagPass = false; + if (parseInt(rID) === 2 && parseInt(id) === 2) { + if (grid[1][1] === lastUser.user.id && grid[3][3] === lastUser.user.id) diagPass = true; + if (grid[1][3] === lastUser.user.id && grid[3][1] === lastUser.user.id) diagPass = true; + } + if (x === 2 || y === 2 || diagPass) { + ended = true; + gameEndReasonType = 'win'; + currentUser = lastUser; + return true; + } + } + } + + if (allPassed) { + ended = true; + gameEndReasonType = 'draw'; + return true; + } else return false; + } + + /** + * Generate the Game-Components + * @private + * @returns {{components: {style: string, disabled: (boolean|boolean), label: (string), type: string, customId: string}[], type: string}[]} + */ + function generateComponents() { + + /** + * Generates components for a row + * @private + * @param number ID of the row + * @returns {{components: {style: string, disabled: (boolean|boolean), label: (string), type: string, customId: string}[], type: string}} + */ + function generateRow(number) { + + /** + * Generates the components in this row + * @private + * @param cNumber ID of the column + * @returns {{style: string, disabled: (boolean|boolean), label: (string), type: string, customId: string}} + */ + function generateComponent(cNumber) { + return { + type: 'BUTTON', + style: 'SECONDARY', + customId: `${number}-${cNumber}`, + // eslint-disable-next-line no-nested-ternary + label: grid[number][cNumber] === null ? '⚪' : (grid[number][cNumber] === interaction.user.id ? '\uD83D\uDFE2' : '\uD83D\uDFE1'), + disabled: ended ? ended : !!grid[number][cNumber] + }; + } + + return { + type: 'ACTION_ROW', + components: [generateComponent(1), generateComponent(2), generateComponent(3)] + }; + } + + return [generateRow(1), generateRow(2), generateRow(3)]; + } + + a.on('collect', (i) => { + let justStart = false; + if (!started) { + if (i.user.id !== member.id) return i.reply({ + ephemeral: true, + content: '⚠️ ' + localize('tic-tac-toe', 'you-are-not-the-invited-one') + }); + if (i.customId === 'deny-invite') { + endReason = localize('tic-tac-toe', 'invite-denied', { + u: interaction.user.toString(), + i: member.toString() + }); + return a.stop(); + } + justStart = true; + started = true; + } + if (!justStart && currentUser.user.id !== i.user.id) return i.reply({ + ephemeral: true, + content: '⚠️ ' + localize('tic-tac-toe', 'not-your-turn') + }); + if (!i.customId.includes('invite')) { + const x = i.customId.split('-')[0]; + const y = i.customId.split('-')[1]; + grid[x][y] = i.user.id; + currentUser = interaction.user.id === i.user.id ? member : interaction.member; + } + checkGameEnded(); + if (ended) { + if (gameEndReasonType === 'draw') return i.update({ + components: generateComponents(), + allowedMentions: {parse: []}, + content: localize('tic-tac-toe', 'draw-header', {u: interaction.user.toString(), i: member.toString()}) + }); + if (gameEndReasonType === 'win') return i.update({ + components: generateComponents(), + allowedMentions: {users: [currentUser.user.id]}, + content: localize('tic-tac-toe', 'win-header', { + u: interaction.user.toString(), + i: member.toString(), + w: currentUser.toString() + }) + }); + } + i.update({ + content: localize('tic-tac-toe', 'playing-header', { + u: interaction.user.toString(), + i: member.toString(), + t: currentUser.toString() + }), + allowedMentions: {users: [currentUser.user.id]}, + components: generateComponents() + }); + }); + a.on('end', () => { + rep.edit({ + content: endReason, + components: [] + }); + } + ); +}; + + +module.exports.config = { + name: 'tic-tac-toe', + description: localize('tic-tac-toe', 'command-description'), + defaultPermission: true, + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('tic-tac-toe', 'user-description') + } + ] +}; \ No newline at end of file diff --git a/modules/tic-tak-toe/module.json b/modules/tic-tak-toe/module.json new file mode 100644 index 00000000..dc47b733 --- /dev/null +++ b/modules/tic-tak-toe/module.json @@ -0,0 +1,28 @@ +{ + "name": "tic-tak-toe", + "humanReadableName": { + "en": "Tick-Tack-Toe" + }, + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "description": { + "en": "Let your users play Tick-Tac-Toe against each other!", + "de": "Lasse Nutzer auf deinem Server Tick-Tac-Toe gegeneinander spielen" + }, + "commands-dir": "/commands", + "noConfig": true, + "releaseDate": "1641230658000", + "earlyAccessFeatures": [ + "Lasse Nutzer auf deinem Server Tick-Tac-Toe spielen", + "Angenehmes Spiel-Erlebnis durch Nutzung von Buttons, Ping-Nachrichten-Farben und Einladungen", + "(definitiv existierende und nicht erfundene) Studien zeigen, dass Server aktiver werden, wenn sie Minispiele anbieten", + "Teste als einer der ersten das neue Modul und gib uns Feedback!" + ], + "tags": [ + "fun" + ], + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/tic-tak-toe" +} \ No newline at end of file diff --git a/modules/tickets/config.json b/modules/tickets/config.json new file mode 100644 index 00000000..1c24b3e9 --- /dev/null +++ b/modules/tickets/config.json @@ -0,0 +1,301 @@ +{ + "description": { + "en": "Manage the basic settings of this module here", + "de": "Passe die grundlegenden Optionen des Modules hier an" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "configElementName": { + "de": { + "one": "Ticket-Kategorie", + "more": "Ticket-Kategorien" + }, + "en": { + "one": "Ticket-Category", + "more": "Ticket-Categories" + } + }, + "configElements": true, + "filename": "config.json", + "content": [ + { + "name": "name", + "humanName": { + "en": "Name", + "de": "Name" + }, + "default": { + "en": "Support" + }, + "description": { + "en": "Name of the Ticket type. This will be shown to users", + "de": "Name des Tickettypen. Dieser wird Nutzern angezeigt" + }, + "type": "string" + }, + { + "name": "ticket-create-category", + "humanName": { + "en": "Ticket create category", + "de": "Ticketerstellungs-Kategorie" + }, + "default": { + "en": "" + }, + "description": { + "en": "Category in which tickets should get created.", + "de": "Kategorie, in der Tickets erstellt werden sollen." + }, + "type": "channelID", + "content": [ + "GUILD_CATEGORY" + ] + }, + { + "name": "ticket-create-channel", + "humanName": { + "en": "Ticket create category", + "de": "Ticketerstellungs-Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel in which a message with a \"Create Ticket\" button should get send", + "de": "Kanal in den eine Nachticht mit \"Ticket erstellen\" button gesendet werden soll" + }, + "type": "channelID", + "content": [ + "GUILD_TEXT" + ] + }, + { + "name": "ticketRoles", + "humanName": { + "en": "Ticket Roles", + "de": "Ticketrollen" + }, + "default": { + "en": [] + }, + "description": { + "en": "Nutzer, die in Tickets gepingt werden und diese sehen können", + "de": "Users who get pinged in the tickets and who can see tickets" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "logChannel", + "humanName": { + "en": "Log chanenl", + "de": "Log-Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel in which ticket logs should get send", + "de": "Kanal in den Ticket-Logs gesendet werden sollen" + }, + "type": "channelID" + }, + { + "name": "ticket-create-message", + "humanName": { + "en": "Ticket create message", + "de": "Ticketerstellungs-Nachricht" + }, + "default": { + "en": "Click the big button below to contact our staff and create a ticket", + "de": "Klick auf den großen Button unter dieser Nachricht um unser Team zu kontaktieren und ein Ticket zu erstellen" + }, + "description": { + "en": "Message that gets send/edited in the ticket-create-channel", + "de": "Nachricht, die im Ticketerstellungs-Kanal gesendet/bearbeitet wird" + }, + "type": "string", + "allowEmbed": true + }, + { + "name": "sendUserDMAfterTicketClose", + "humanName": { + "en": "Send user DM after ticket is closed", + "de": "Nach schließen PN an Nutzer senden" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled users get a DM from the bot after someone closes the ticket", + "de": "Wenn diese Option aktiviert ist, bekommen Nutzer eine PN, wenn ihr Ticket geschlossen wird" + }, + "type": "boolean" + }, + { + "name": "userDM", + "humanName": { + "en": "User DM", + "de": "Nutzer PN" + }, + "default": { + "en": "Thanks for contacting our support for the ticket-category \"%type%\", here is your transcript: %transcriptURL%", + "de": "Danke, dass du unseren Support für die Kategorie \"%type%\" kontaktiert hast. Hier ist dein Transcript: %transcriptURL%" + }, + "description": { + "en": "This message gets send to the user if sendUserDMAfterTicketClose is enabled", + "de": "Diese Nachricht wird an den Nutzer gesendet, wenn die entsprechende Option aktiviert ist" + }, + "type": "string", + "dependsOn": "sendUserDMAfterTicketClose", + "allowEmbed": true, + "params": [ + { + "name": "transcriptURL", + "description": { + "de": "URL zum Transcript", + "en": "URL to transcript" + } + }, + { + "name": "type", + "description": { + "de": "Name des dieses Ticket Typen", + "en": "Name of this ticket type" + } + } + ] + }, + { + "name": "creation-message", + "humanName": { + "en": "Ticket-Created Message", + "de": "Ticket-Erstellt Nachricht" + }, + "pro": true, + "type": "string", + "allowEmbed": true, + "description": { + "en": "This message will get sent in new tickets. The close buttons will be added.", + "de": "Diese Nachricht wird in neue Tickets gesendet. Der Schließ-Knopf wird hinzugefügt." + }, + "default": { + "en": { + "title": "\uD83D\uDCE5 New ticket #%id%", + "color": "#2ECC71", + "message": "%rolePings%", + "fields": [ + { + "name": "\uD83D\uDC64 User", + "value": "%userMention%", + "inline": true + }, + { + "name": "☕ Ticket-Topic", + "value": "%ticketTopic%", + "inline": true + }, + { + "name": "ℹ\uFE0F Information", + "value": "Your issue got solved? Click the button below. You can always find this message pinned." + } + ] + }, + "de": { + "title": "\uD83D\uDCE5 Neues Ticket #%id%", + "color": "#2ECC71", + "message": "%rolePings%", + "fields": [ + { + "name": "\uD83D\uDC64 Nutzer", + "value": "%userMention%", + "inline": true + }, + { + "name": "☕ Ticket-Thema", + "value": "%ticketTopic%", + "inline": true + }, + { + "name": "ℹ\uFE0F Information", + "value": "Dein Problem wurde behoben? Klicke den Knopf unten. Du kannst diese Nachricht immer in den angepinnten Nachrichten finden." + } + ] + } + }, + "params": [ + { + "name": "id", + "description": { + "de": "Eindeutige Identifikationsnummer des Tickets", + "en": "Unique identification number of the ticket" + } + }, + { + "name": "userMention", + "description": { + "de": "Erwähnung des Nutzers, der das Ticket erstellt hat", + "en": "Mention of the user who created this ticket" + } + }, + { + "name": "rolePings", + "description": { + "de": "Erwähnung der Rollen, die du im \"Ticket-Rollen\"-Feld eingestellt hast", + "en": "Mention of the roles you have selected in the \"Ticket roles\" field" + } + }, + { + "name": "ticketTopic", + "description": { + "de": "Name des Ticket-Themas", + "en": "Name of the Ticket-Topic" + } + }, + { + "name": "userTag", + "description": { + "de": "Tag des Nutzers, der das Ticket erstellt hat", + "en": "Tag of the user who created this ticket" + } + } + ] + }, + { + "name": "ticket-create-button", + "humanName": { + "en": "Ticket create button", + "de": "Ticketerstellungs-Button" + }, + "default": { + "en": "Create ticket 🎫", + "de": "Ticket erstellen 🎫" + }, + "description": { + "en": "Button for creating a ticket", + "de": "Button zum erstellen eines Tickets" + }, + "type": "string", + "pro": true + }, + { + "name": "ticket-close-button", + "humanName": { + "en": "Ticket close button", + "de": "Ticketschließungs-Button" + }, + "default": { + "en": "❎ Close ticket", + "de": "❎ Ticket schließen" + }, + "description": { + "en": "Button for closing a ticket", + "de": "Button um ein Ticket zu schließen" + }, + "type": "string", + "pro": true + } + ] +} \ No newline at end of file diff --git a/modules/tickets/events/botReady.js b/modules/tickets/events/botReady.js new file mode 100644 index 00000000..f2796501 --- /dev/null +++ b/modules/tickets/events/botReady.js @@ -0,0 +1,75 @@ +const {embedType, disableModule, migrate} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (client) { + const moduleConfig = client.configurations['tickets']['config']; + const messageModel = client.models['tickets']['TicketMessage']; + await migrate('tickets', 'TicketV1', 'Ticket'); + for (const element of moduleConfig) { + for (const element2 of moduleConfig) { + if (moduleConfig.indexOf(element) === moduleConfig.indexOf(element2) && moduleConfig.indexOf(element) !== moduleConfig.indexOf(element2)) return disableModule('tickets', localize('tickets', 'button-not-uniqe')); + } + const channel = await client.channels.fetch(element['ticket-create-channel']).catch(() => { + }); + if (!channel || channel.guild.id !== client.config.guildID || channel.type !== 'GUILD_TEXT') return disableModule('tickets', localize('tickets', 'channel-not-found', {c: element['ticket-create-channel']})); + const components = [{ + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + label: element['ticket-create-button'], + style: 'PRIMARY', + customId: 'create-ticket-' + moduleConfig.indexOf(element) + }] + }]; + const message = embedType(element['ticket-create-message'], {}, {components}); + + const sent = await client.models['tickets']['TicketMessage'].findOne({ + where: { + type: moduleConfig.indexOf(element) + } + }); + if (sent) { + const channelMessages = await channel.messages.fetch(sent.messageID).catch(() => { + }); + if (channelMessages && channelMessages.author.id === client.user.id) await channelMessages.edit(message); + else await sendMessage(message, channel, messageModel, moduleConfig, element); + } else { + await sendMessage(message, channel, messageModel, moduleConfig, element); + } + } + +}; + +/** + * Send the ticket-creation-message + * @param message the message to be sent + * @param channel the channel it will be sent to + * @param messageModel the model the ids of the new message and its channel will be saved to + * @param moduleConfig needed to find the right row in the model + * @param element needed to find the right row in the model + * @returns {Promise} + */ +async function sendMessage(message, channel, messageModel, moduleConfig, element) { + const msg = await channel.send(message); + const exists = await messageModel.findOne({ + where: { + type: moduleConfig.indexOf(element) + } + }); + if (exists) { + await messageModel.update({ + messageID: msg.id, + channelID: channel.id + }, { + where: { + type: moduleConfig.indexOf(element) + } + }); + } else { + await messageModel.create({ + messageID: msg.id, + channelID: channel.id, + type: moduleConfig.indexOf(element) + }); + } +} \ No newline at end of file diff --git a/modules/tickets/events/interactionCreate.js b/modules/tickets/events/interactionCreate.js new file mode 100644 index 00000000..f472d765 --- /dev/null +++ b/modules/tickets/events/interactionCreate.js @@ -0,0 +1,152 @@ +const {localize} = require('../../../src/functions/localize'); +const {MessageEmbed} = require('discord.js'); +const { + lockChannel, + messageLogToStringToPaste, + embedType, + formatDiscordUserName +} = require('../../../src/functions/helpers'); + +module.exports.run = async function (client, interaction) { + if (!client.botReadyAt) return; + if (interaction.guild.id !== client.config.guildID) return; + if (!interaction.isButton()) return; + const moduleConfig = client.configurations['tickets']['config']; + for (const element of moduleConfig) { + if (interaction.customId === 'close-ticket' + moduleConfig.indexOf(element)) { + const ticket = await client.models['tickets']['Ticket'].findOne({ + where: { + channelID: interaction.channel.id, + type: moduleConfig.indexOf(element), + open: true + } + }); + if (!ticket) return; + await interaction.channel.send({ + content: localize('tickets', 'closing-ticket', {u: interaction.user.toString()}), + allowedMentions: {parse: []} + }); + await lockChannel(interaction.channel, [], localize('tickets', 'ticket-closed-audit-log', {u: formatDiscordUserName(interaction.user)})); + + interaction.reply({ + ephemeral: true, + content: localize('tickets', 'ticket-closed-successfully') + }); + ticket.open = false; + await ticket.save(); + + const msgLog = await messageLogToStringToPaste(interaction.channel, ticket.msgCount, '1year'); + if (element.sendUserDMAfterTicketClose) { + const user = await client.users.fetch(ticket.userID); + user.send(embedType(element.userDM, { + '%transcriptURL%': msgLog, + '%type%': element.name + })).catch(e => client.logger.warn('[tickets] ' + localize('tickets', 'could-not-dm', { + e, + u: ticket.userID + }))); + } + const logChannel = element.logChannel ? interaction.guild.channels.cache.get(element.logChannel) : client.logChannel; + if (!logChannel) client.logger.error('[tickets] ' + localize('tickets', 'no-log-channel')); + else { + await logChannel.send({ + embeds: [ + new MessageEmbed() + .setColor('DARK_GREEN') + .setTitle(localize('tickets', 'ticket-log-embed-title', {i: ticket.id})) + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }) + .setAuthor({ + name: client.user.username, + iconURL: client.user.avatarURL() + }) + .addField(localize('tickets', 'ticket-with-user'), `<@${ticket.userID}>`, true) + .addField(localize('tickets', 'ticket-type'), element.name, true) + .addField(localize('tickets', 'ticket-log'), localize('tickets', 'ticket-log-value', { + u: msgLog, + n: ticket.msgCount + }), true) + .addField(localize('tickets', 'closed-by'), interaction.user.toString(), true) + ] + }); + } + setTimeout(() => { + interaction.channel.delete(localize('tickets', 'ticket-closed-audit-log', {u: formatDiscordUserName(interaction.user)})); + }, 20000); + } + if (interaction.customId.startsWith('create-ticket-') && parseFloat(interaction.customId.replaceAll('create-ticket-', '')) === moduleConfig.indexOf(element)) { + const existingTicket = await client.models['tickets']['Ticket'].findOne({ + where: { + userID: interaction.user.id, + type: moduleConfig.indexOf(element), + open: true + } + }); + if (existingTicket) { + const ticketChannel = await interaction.guild.channels.fetch(existingTicket.channelID).catch(() => { + }); + if (ticketChannel) return interaction.reply({ + ephemeral: true, + content: localize('tickets', 'existing-ticket', {c: `<#${existingTicket.channelID}>`}) + }); + existingTicket.open = false; + await existingTicket.save(); + } + const overwrites = []; + element.ticketRoles.forEach(rID => { + overwrites.push( + { + id: rID, + type: 'ROLE', + allow: ['SEND_MESSAGES', 'VIEW_CHANNEL', 'READ_MESSAGE_HISTORY' , 'SEND_FILES'] + } + ); + }); + const channel = await interaction.guild.channels.create(formatDiscordUserName(interaction.user).split('#').join('-'), { + parent: element['ticket-create-category'], + topic: `Ticket created by ${interaction.user.toString()} by clicking on a message in ${interaction.channel.toString()}`, + reason: localize('tickets', 'ticket-created-audit-log', {u: formatDiscordUserName(interaction.user)}), + permissionOverwrites: [{ + id: interaction.guild.roles.cache.find(r => r.name === '@everyone'), + deny: ['SEND_MESSAGES', 'VIEW_CHANNEL', 'READ_MESSAGE_HISTORY'] + }, + { + id: interaction.member, + allow: ['SEND_MESSAGES', 'VIEW_CHANNEL', 'READ_MESSAGE_HISTORY'] + }, ...overwrites] + }); + const ticket = await client.models['tickets']['Ticket'].create({ + open: true, + userID: interaction.user.id, + channelID: channel.id, + addedUsers: [interaction.user.id], + type: moduleConfig.indexOf(element) + }); + let pingMsg = ''; + element.ticketRoles.forEach(rID => pingMsg = pingMsg + `<@&${rID}> `); + if (pingMsg === '') pingMsg = localize('tickets', 'no-admin-pings'); + const msg = await channel.send(embedType(element['creation-message'], { + '%id%': ticket.id, + '%userMention%': interaction.user.toString(), + '%ticketTopic%': element.name, + '%rolePings%': pingMsg, + '%userTag%': formatDiscordUserName(interaction.user) + }, {}, [{ + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + label: element['ticket-close-button'], + style: 'PRIMARY', + customId: `close-ticket` + moduleConfig.indexOf(element) + }] + }])); + await msg.pin(); + interaction.reply({ + ephemeral: true, + content: '✅ ' + localize('tickets', 'ticket-created', {c: channel.toString()}) + }); + } + } +}; diff --git a/modules/tickets/events/messageCreate.js b/modules/tickets/events/messageCreate.js new file mode 100644 index 00000000..e343cc1c --- /dev/null +++ b/modules/tickets/events/messageCreate.js @@ -0,0 +1,15 @@ +module.exports.run = async function (client, msg) { + if (!client.botReadyAt) return; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + if (!msg.member) return; + const ticketChannel = await client.models['tickets']['Ticket'].findOne({ + where: { + channelID: msg.channel.id, + open: true + } + }); + if (!ticketChannel) return; + ticketChannel.msgCount++; + await ticketChannel.save(); +}; \ No newline at end of file diff --git a/modules/tickets/models/Message.js b/modules/tickets/models/Message.js new file mode 100644 index 00000000..588f7b6e --- /dev/null +++ b/modules/tickets/models/Message.js @@ -0,0 +1,25 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class TicketMessage extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + messageID: DataTypes.STRING, + channelID: DataTypes.STRING, + type: DataTypes.STRING + }, { + tableName: 'ticket_Messagev1', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'TicketMessage', + 'module': 'tickets' +}; \ No newline at end of file diff --git a/modules/tickets/models/Ticket.js b/modules/tickets/models/Ticket.js new file mode 100644 index 00000000..943923a7 --- /dev/null +++ b/modules/tickets/models/Ticket.js @@ -0,0 +1,38 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class Ticket extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + open: { + type: DataTypes.STRING, + defaultValue: true + }, + userID: DataTypes.STRING, + channelID: DataTypes.STRING, + msgLogURL: DataTypes.STRING, + msgCount: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + addedUsers: { + type: DataTypes.JSON, + defaultValue: [] + }, + type: DataTypes.STRING + }, { + tableName: 'ticket_Ticketv2', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'Ticket', + 'module': 'tickets' +}; \ No newline at end of file diff --git a/modules/tickets/models/TicketV1.js b/modules/tickets/models/TicketV1.js new file mode 100644 index 00000000..86aa2052 --- /dev/null +++ b/modules/tickets/models/TicketV1.js @@ -0,0 +1,37 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class TicketV1 extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + open: { + type: DataTypes.STRING, + defaultValue: true + }, + userID: DataTypes.STRING, + channelID: DataTypes.STRING, + msgLogURL: DataTypes.STRING, + msgCount: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + addedUsers: { + type: DataTypes.JSON, + defaultValue: [] + } + }, { + tableName: 'ticket_Ticketv1', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'TicketV1', + 'module': 'tickets' +}; \ No newline at end of file diff --git a/modules/tickets/module.json b/modules/tickets/module.json new file mode 100644 index 00000000..772f3f6b --- /dev/null +++ b/modules/tickets/module.json @@ -0,0 +1,24 @@ +{ + "name": "tickets", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "events-dir": "/events", + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/tickets", + "models-dir": "/models", + "config-example-files": [ + "config.json" + ], + "tags": [ + "support" + ], + "humanReadableName": { + "en": "Ticket-System" + }, + "description": { + "en": "Let users create tickets to message your staff", + "de": "Lasse deine Nutzer durch Tickets mit deinem Team kommunizieren" + } +} \ No newline at end of file diff --git a/modules/twitch-notifications/configs/config.json b/modules/twitch-notifications/configs/config.json new file mode 100644 index 00000000..f5801ea4 --- /dev/null +++ b/modules/twitch-notifications/configs/config.json @@ -0,0 +1,47 @@ +{ + "description": {}, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "hidden": true, + "content": [ + { + "name": "twitchClientID", + "humanName": {}, + "default": { + "en": "me3ub5wbx2jxlhkvxrc6fbgp8wgixq" + }, + "description": { + "en": "ID of the Client, which is used to check if the Streamer is live" + }, + "hidden": true, + "type": "string" + }, + { + "name": "clientSecret", + "humanName": {}, + "default": { + "en": "58v6r0v2oips1tldmfunrxp5m8xv6r" + }, + "description": { + "en": "Secret of the Twitch-Client, which is used to check if the Streamer is live" + }, + "hidden": true, + "type": "string" + }, + { + "name": "interval", + "humanName": {}, + "default": { + "en": 180 + }, + "description": { + "en": "Interval (in seconds) in which it is tested whether the streamer is live. This value must be higher than 60" + }, + "hidden": true, + "type": "integer" + } + ] +} \ No newline at end of file diff --git a/modules/twitch-notifications/configs/streamers.json b/modules/twitch-notifications/configs/streamers.json new file mode 100644 index 00000000..e0c17559 --- /dev/null +++ b/modules/twitch-notifications/configs/streamers.json @@ -0,0 +1,151 @@ +{ + "description": { + "en": "Configure here, where for what streamer which message should get send", + "de": "Stelle hier ein, bei welchem Streamer in welchen Channel eine Nachricht gesendet werden soll" + }, + "humanName": { + "en": "Streamers", + "de": "Streamers" + }, + "elementLimits": { + "STARTER": 2, + "ACTIVE_GUILD": 5, + "PRO": 15 + }, + "filename": "streamers.json", + "configElements": true, + "content": [ + { + "name": "liveMessage", + "humanName": { + "en": "Live-Messages", + "de": "Live-Nachricht" + }, + "default": { + "en": "Hey, %streamer% is live on Twitch streaming %game%! Check it out: %url%", + "de": "Hi, %streamer% ist Live auf Twitch und streamt %game%! Jetzt anschauen: %url%" + }, + "description": { + "en": "Message that gets send if the streamer goes live", + "de": "Nachricht, die gesendet wird, wenn ein Streamer anfängt zu streamen" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "streamer", + "description": { + "en": "Name of the Streamer", + "de": "Name des Streamers" + } + }, + { + "name": "game", + "description": { + "en": "Game which is streamed", + "de": "Spiel, welches gestreamt wird" + } + }, + { + "name": "url", + "description": { + "en": "Link to the stream", + "de": "Link zum Twitch-Stream" + } + }, + { + "name": "title", + "description": { + "en": "Title of the Stream", + "de": "Titel des Streams" + } + }, + { + "name": "thumbnailUrl", + "description": { + "en": "The Link to the thumbnail of the Stream", + "de": "Link zum Thumbnail des Streams" + }, + "isImage": true + } + ] + }, + { + "name": "liveMessageChannel", + "humanName": { + "en": "Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel in which live-message should get sent", + "de": "Kanal, in welchen Benachrichtigung gesendet werden soll" + }, + "type": "channelID" + }, + { + "name": "streamer", + "humanName": { + "en": "Streamer", + "de": "Streamer" + }, + "default": { + "en": "" + }, + "description": { + "en": "Streamer where a notification should send when they start streaming", + "de": "Steamer, bei denen eine Benachrichtigung gesendet werden soll, wenn sie anfangen, zu streamen" + }, + "type": "string" + }, + { + "name": "liveRole", + "humanName": { + "en": "Use Live-Role", + "de": "Live-Rolle Aktivieren" + }, + "default": { + "en": false + }, + "description": { + "en": "Should the Live-Role be activated?", + "de": "Soll die Live-Rolle aktiviert sein?" + }, + "type": "boolean" + }, + { + "name": "id", + "humanName": { + "en": "Discord-User ID", + "de": "Discord-Benutzer ID" + }, + "default": { + "en": "" + }, + "description": { + "en": "ID of the Discord-Account of the Streamer", + "de": "ID des Discord-Accounts des Streamers" + }, + "type": "userID", + "dependsOn": "liveRole" + }, + { + "name": "role", + "humanName": { + "en": "Live Role", + "de": "Live Rolle" + }, + "default": { + "en": "" + }, + "description": { + "en": "ID of the Role that the Streamer should get, when live", + "de": "ID der Rolle, die der streamer bekommen soll, wenn er live ist" + }, + "type": "roleID", + "allowNull": true, + "dependsOn": "liveRole" + } + ] +} \ No newline at end of file diff --git a/modules/twitch-notifications/events/botReady.js b/modules/twitch-notifications/events/botReady.js new file mode 100644 index 00000000..a0428a56 --- /dev/null +++ b/modules/twitch-notifications/events/botReady.js @@ -0,0 +1,133 @@ +/** + * @module twitch-notifications + */ +const {embedType} = require('../../../src/functions/helpers'); + +const {ApiClient} = require('@twurple/api'); +const {ClientCredentialsAuthProvider} = require('@twurple/auth'); +const {localize} = require('../../../src/functions/localize'); + +/** + * General program + * @param {Client} client Discord js Client + * @param {ApiClient} apiClient Twitch API Client + * @private + */ +function twitchNotifications(client, apiClient) { + const streamers = client.configurations['twitch-notifications']['streamers']; + + /** + * Function to add the Live-Role + * @param {string} userID ID of the User + * @param {String} roleID ID of the Role + * @param {boolean} liveRole Should the live-role be active + */ + async function addLiveRole(userID, roleID, liveRole) { + if (!liveRole) return; + if (!userID || userID === '' || !roleID || roleID === '') return; + await client.guild.members.fetch(); + const member = client.guild.members.cache.get(userID); + if (!member) { + client.logger.error(localize('twitch-notifications', 'user-not-on-twitch', {u: userID})); + return; + } + await member.roles.add(roleID); + } + + /** + * Sends the live-message + * @param {string} username Username of the streamer + * @param {string} game Game that is streamed + * @param {string} thumbnailUrl URL of the thumbnail of the stream + * @param {number} channelID ID of the live-message-channel + * @param {number} i Index of the config-element-object + * @returns {*} + * @private + */ + function sendMsg(username, game, thumbnailUrl, channelID, title, i) { + const channel = client.channels.cache.get(channelID); + if (!channel) return client.logger.fatal(`[twitch-notifications] ` + localize('twitch-notifications', 'channel-not-found', {c: channelID})); + if (!streamers[i]['liveMessage']) return client.logger.fatal(`[twitch-notifications] ` + localize('twitch-notifications', 'message-not-found', {s: username})); + channel.send(embedType(streamers[i]['liveMessage'], { + '%streamer%': username, + '%game%': game, + '%url%': `https://twitch.tv/${username.toLowerCase()}`, + '%thumbnailUrl%': (thumbnailUrl + `?_t=${new Date().getTime()}` || '').replaceAll('{width}', '1920').replaceAll('{height}', '1080'), + '%title%': title + })); + } + + /** + * Checks if the streamer is live + * @param {string} userName Name of the Streamer + * @returns {HelixStream} + * @private + */ + async function isStreamLive(userName) { + const user = await apiClient.users.getUserByName(userName.toLowerCase()); + if (!user) return 'userNotFound'; + return await user.getStream(); + } + + streamers.forEach(start); + + /** + * Starts checking if the streamer is live + * @param {string} value Current Streamer + * @param {number} index Index of current Streamer + * @returns {Promise} + * @private + */ + async function start(value, index) { + const streamer = await client.models['twitch-notifications']['streamer'].findOne({ + where: { + name: value.streamer.toLowerCase() + } + }); + const stream = await isStreamLive(value.streamer); + if (stream === 'userNotFound') { + return client.logger.error(`[twitch-notifications] ` + localize('twitch-notifications', 'user-not-on-twitch', {u: value})); + } else if (stream !== null && !streamer) { + client.models['twitch-notifications']['streamer'].create({ + name: value.streamer.toLowerCase(), + startedAt: stream.startDate.toString() + }); + sendMsg(stream.userDisplayName, stream.gameName, stream.thumbnailUrl, streamers[index]['liveMessageChannel'], stream.title, index); + addLiveRole(streamers[index]['id'], streamers[index]['role'], streamers[index]['liveRole']); + } else if (stream !== null && stream.startDate.toString() !== streamer.startedAt) { + streamer.startedAt = stream.startDate.toString(); + streamer.save(); + sendMsg(stream.userDisplayName, stream.gameName, stream.thumbnailUrl, streamers[index]['liveMessageChannel'], stream.title, index); + addLiveRole(streamers[index]['id'], streamers[index]['role'], streamers[index]['liveRole']); + } else if (stream === null) { + if (!streamers[index]['liveRole']) return; + if (!streamers[index]['id'] || streamers[index]['id'] === '' || !streamers[index]['role'] || streamers[index]['role'] === '') return; + await client.guild.members.fetch(); + const member = client.guild.members.cache.get(streamers[index]['id']); + if (!member) { + client.logger.error(localize('twitch-notifications', 'user-not-on-twitch', {u: streamers[index]['id']})); + return; + } + if (member.roles.cache.has(streamers[index]['role'])) { + await member.roles.remove(streamers[index]['role']); + } + } + } +} + +module.exports.run = async (client) => { + const config = client.configurations['twitch-notifications']['config']; + + const ClientID = config['twitchClientID']; + const ClientSecret = config['clientSecret']; + const authProvider = new ClientCredentialsAuthProvider(ClientID, ClientSecret); + const apiClient = new ApiClient({authProvider}); + + await twitchNotifications(client, apiClient); + const interval = config['interval'] * 1000; + const twitchCheckInterval = setInterval(() => { + twitchNotifications(client, apiClient); + }, interval); + + client.intervals.push(twitchCheckInterval); +}; \ No newline at end of file diff --git a/modules/twitch-notifications/models/Streamer.js b/modules/twitch-notifications/models/Streamer.js new file mode 100644 index 00000000..3eff77a6 --- /dev/null +++ b/modules/twitch-notifications/models/Streamer.js @@ -0,0 +1,22 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class TwitchStreamer extends Model { + static init(sequelize) { + return super.init({ + name: { + type: DataTypes.STRING, + primaryKey: true + }, + startedAt: DataTypes.STRING + }, { + tableName: 'twitch_streamers', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'streamer', + 'module': 'twitch-notifications' +}; \ No newline at end of file diff --git a/modules/twitch-notifications/module.json b/modules/twitch-notifications/module.json new file mode 100644 index 00000000..f2603317 --- /dev/null +++ b/modules/twitch-notifications/module.json @@ -0,0 +1,26 @@ +{ + "name": "twitch-notifications", + "author": { + "name": "jateute", + "link": "https://github.com/jateute", + "scnxOrgID": "4" + }, + "events-dir": "/events", + "models-dir": "/models", + "openSourceURL": "https://github.com/jateute/CustomDCBot/tree/main/modules/twitch-notifications", + "config-example-files": [ + "configs/config.json", + "configs/streamers.json" + ], + "tags": [ + "integrations" + ], + "humanReadableName": { + "en": "Twitch-Notifications", + "de": "Twitch-Benachrichtigungen" + }, + "description": { + "en": "Module that sends a message to a channel, when a streamer goes live on Twitch", + "de": "Sendet eine Nachricht in einen ausgewählten Channel, wenn ein Streamer Live auf Twitch streamt" + } +} \ No newline at end of file diff --git a/modules/uno/commands/uno.js b/modules/uno/commands/uno.js new file mode 100644 index 00000000..7cd7dc4a --- /dev/null +++ b/modules/uno/commands/uno.js @@ -0,0 +1,455 @@ +const {localize} = require('../../../src/functions/localize'); +const {MessageActionRow, MessageButton} = require('discord.js'); + +const cards = [ + '0', + '1', '2', '3', '4', '5', '6', '7', '8', '9', + '1', '2', '3', '4', '5', '6', '7', '8', '9', + localize('uno', 'skip'), localize('uno', 'skip'), + localize('uno', 'reverse'), localize('uno', 'reverse'), + localize('uno', 'draw2'), localize('uno', 'draw2'), + localize('uno', 'color'), + localize('uno', 'colordraw4') +]; +const colorEmojis = {'red': '🟥', 'blue': '🟦', 'green': '🟩', 'yellow': '🟨'}; +const colors = Object.keys(colorEmojis); + +const publicrow = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('uno-deck') + .setLabel(localize('uno', 'view-deck')) + .setStyle('PRIMARY'), + new MessageButton() + .setCustomId('uno-uno') + .setLabel(localize('uno', 'uno')) + .setStyle('PRIMARY') + ); + +/** + * Build a deck for a player + * @param {Object} player + * @param {Object} game + * @param {Boolean} neutral + * @return {MessageActionRow} + */ +function buildDeck(player, game, neutral = false) { + const controlrow = new MessageActionRow(); + if (player.turn && !player.blockRedraw) controlrow.addComponents( + new MessageButton() + .setCustomId('uno-draw') + .setLabel(localize('uno', 'draw')) + .setStyle('SECONDARY') + ); + else controlrow.addComponents( + new MessageButton() + .setCustomId('uno-update') + .setLabel(localize('uno', 'update-button')) + .setStyle('SECONDARY') + ); + + const cardrow1 = new MessageActionRow(); + const cardrow2 = new MessageActionRow(); + const cardrow3 = new MessageActionRow(); + const cardrow4 = new MessageActionRow(); + + player.cards.slice(0, 20).forEach((c, i) => { + let row = cardrow1; + if (i > 4) row = cardrow2; + if (i > 9) row = cardrow3; + if (i > 14) row = cardrow4; + row.addComponents( + new MessageButton() + .setCustomId('uno-card-' + c.name + '-' + c.color + '-' + i) + .setLabel(c.name) + .setEmoji(colorEmojis[c.color]) + .setStyle(!neutral && canUseCard(game, c, player.cards) ? 'PRIMARY' : 'SECONDARY') + .setDisabled(neutral || (player.turn ? !canUseCard(game, c, player.cards) : true)) + ); + }); + + const rows = [controlrow, cardrow1]; + if (cardrow2.components.length > 0) rows.push(cardrow2); + if (cardrow3.components.length > 0) rows.push(cardrow3); + if (cardrow4.components.length > 0) rows.push(cardrow4); + return rows; +} + +/** + * Checks if the player can use a card + * @param {Object} game + * @param {Object} card + * @param {Array} playerCards + * @returns {Boolean} + */ +function canUseCard(game, card, playerCards) { + if (game.pendingDraws > 0 && card.name !== localize('uno', 'draw2') && card.name !== localize('uno', 'colordraw4')) return false; + if (card.name === localize('uno', 'color') || (card.name === localize('uno', 'colordraw4') && game.lastCard.name !== localize('uno', 'draw2') && !playerCards.some(c => c.color === game.lastCard.color))) return true; + return game.lastCard.name === card.name || game.lastCard.color === card.color; +} + +/** + * Selects the next player + * @param {Object} game + * @param {Object} player + * @param {Integer} moves + * @param {Boolean} revSkip + */ +function nextPlayer(game, player, moves = 1, revSkip = false) { + player.turn = false; + let next = game.players[player.n + (game.reversed ? -1 * moves : moves)] || game.players[game.reversed ? game.players.length - 1 : 0]; + if (game.players.length === 2 && revSkip) next = player; + next.turn = true; + next.uno = false; + + + if (game.inactiveTimeout[0]) clearTimeout(game.inactiveTimeout[0]); + if (game.inactiveTimeout[1]) clearTimeout(game.inactiveTimeout[1]); + game.inactiveTimeout[0] = setTimeout(() => { + game.msg.channel.send({ + content: localize('uno', 'inactive-warn', {u: '<@' + next.id + '>'}), + reference: {messageId: game.msg.id, channelId: game.msg.channel.id} + }); + }, 1000 * 60); + game.inactiveTimeout[1] = setTimeout(() => { + nextPlayer(game, next); + game.players = game.players.filter(p => p.id !== next.id); + if (game.players.length <= 1) { + clearTimeout(game.inactiveTimeout[0]); + clearTimeout(game.inactiveTimeout[1]); + return game.msg.edit({ + content: localize('uno', 'inactive-win', {u: '<@' + game.players[0]?.id + '>'}), + components: [] + }); + } + game.msg.edit(gameMsg(game)); + }, 1000 * 60 * 2); +} + +/** + * Handle a button click + * @param {MessageComponentInteraction} i + * @param {Object} player + * @param {Object} game + */ +function perPlayerHandler(i, player, game) { + if (player.turn && game.pendingDraws > 0 && !player.cards.some(c => (c.name === localize('uno', 'draw2') && canUseCard(game, c, player.cards)) || (c.name === localize('uno', 'colordraw4') && canUseCard(game, c, player.cards)))) { + if (game.justChoosingColor) game.justChoosingColor = false; + else { + game.turns++; + if (game.pendingDraws > 0) { + for (let j = 0; j < game.pendingDraws; j++) player.cards.push({ + name: cards[Math.floor(Math.random() * cards.length)], + color: colors[Math.floor(Math.random() * colors.length)] + }); + game.pendingDraws = 0; + } + + nextPlayer(game, player); + game.players[player.n] = player; + i.update({content: localize('uno', 'auto-drawn-skip'), components: buildDeck(player, game)}); + return game.msg.edit(gameMsg(game)); + } + } + if (i.customId === 'uno-update') return i.update({content: null, components: buildDeck(player, game)}); + if (!player.turn) return i.reply({content: localize('connect-four', 'not-turn'), ephemeral: true}); + game.justChoosingColor = false; + + if (game.inactiveTimeout[0]) clearTimeout(game.inactiveTimeout[0]); + if (game.inactiveTimeout[1]) clearTimeout(game.inactiveTimeout[1]); + + game.turns++; + if (game.pendingDraws > 0 && i.customId !== 'uno-dont-use-drawn' && !i.customId.startsWith('uno-color-') && i.customId.startsWith('uno-card-' + localize('uno', 'draw2') + '-') && i.customId.startsWith('uno-card-' + localize('uno', 'colordraw4') + '-')) { + for (let j = 0; j < game.pendingDraws; j++) player.cards.push({ + name: cards[Math.floor(Math.random() * cards.length)], + color: colors[Math.floor(Math.random() * colors.length)] + }); + game.pendingDraws = 0; + } + if (i.customId === 'uno-draw') { + player.cards.push({ + name: cards[Math.floor(Math.random() * cards.length)], + color: colors[Math.floor(Math.random() * colors.length)] + }); + + const c = player.cards[player.cards.length - 1]; + if (canUseCard(game, c, player.cards)) { + player.blockRedraw = true; + i.update({ + content: localize('uno', 'use-drawn'), + components: [ + new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('uno-card-' + c.name + '-' + c.color) + .setLabel(c.name) + .setEmoji(colorEmojis[c.color]) + .setStyle('PRIMARY'), + new MessageButton() + .setCustomId('uno-dont-use-drawn') + .setLabel(localize('uno', 'dont-use-drawn')) + .setStyle('SECONDARY') + ) + ], + ephemeral: true + }); + } else { + nextPlayer(game, player); + i.update({components: buildDeck(player, game)}); + game.msg.edit(gameMsg(game)); + } + } else if (i.customId.startsWith('uno-card-')) { + player.blockRedraw = false; + if (player.cards.length === 2 && !player.uno) { + player.cards.push({ + name: cards[Math.floor(Math.random() * cards.length)], + color: colors[Math.floor(Math.random() * colors.length)] + }); + nextPlayer(game, player); + i.update({content: localize('uno', 'missing-uno'), components: buildDeck(player, game)}); + return game.msg.edit(gameMsg(game)); + } + const name = i.customId.split('-')[2]; + const color = i.customId.split('-')[3]; + if (!canUseCard(game, { + name, + color + }, player.cards)) return i.update({ + content: localize('uno', 'invalid-card', {c: colorEmojis[color] + ' **' + name + '**'}), + components: buildDeck(player, game) + }); + + const toremove = player.cards.find(c => c.name === name && c.color === color); + if (!toremove) return i.update({ + content: localize('uno', 'used-card', {c: colorEmojis[color] + ' **' + name + '**'}), + components: buildDeck(player, game) + }); + player.cards.splice(player.cards.indexOf(toremove), 1); + + if (player.cards.length === 0) { + i.update({content: localize('uno', 'win-you'), components: []}); + return game.msg.edit({ + content: localize('uno', 'win', { + u: '<@' + player.id + '>', + turns: '**' + game.turns + '**' + }), components: [] + }); + } + if (name === localize('uno', 'reverse')) game.reversed = !game.reversed; + + if (name === localize('uno', 'skip')) nextPlayer(game, player, 2, true); + else if (name === localize('uno', 'color') || name === localize('uno', 'colordraw4')) { + if (name === localize('uno', 'colordraw4')) { + game.pendingDraws = game.pendingDraws + 4; + game.justChoosingColor = true; + } + return i.update({ + content: localize('uno', 'choose-color'), components: [ + new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('uno-color-red-' + name) + .setEmoji(colorEmojis.red) + .setStyle('PRIMARY'), + new MessageButton() + .setCustomId('uno-color-blue-' + name) + .setEmoji(colorEmojis.blue) + .setStyle('PRIMARY'), + new MessageButton() + .setCustomId('uno-color-green-' + name) + .setEmoji(colorEmojis.green) + .setStyle('PRIMARY'), + new MessageButton() + .setCustomId('uno-color-yellow-' + name) + .setEmoji(colorEmojis.yellow) + .setStyle('PRIMARY') + ), + ...buildDeck(player, game, true).slice(1) + ] + }); + } else nextPlayer(game, player, 1, name === localize('uno', 'reverse')); + if (name === localize('uno', 'draw2')) game.pendingDraws = game.pendingDraws + 2; + + game.previousCards = [game.previousCards[1], game.previousCards[2], colorEmojis[game.lastCard.color] + ' ' + game.lastCard.name]; + game.lastCard = {name, color}; + i.update({content: null, components: buildDeck(player, game)}); + game.msg.edit(gameMsg(game)); + } else if (i.customId === 'uno-dont-use-drawn' || i.customId.startsWith('uno-color-')) { + player.blockRedraw = false; + if (i.customId.startsWith('uno-color-')) game.lastCard = { + name: i.customId.split('-')[3], + color: i.customId.split('-')[2] + }; + nextPlayer(game, player); + i.update({content: null, components: buildDeck(player, game)}); + game.msg.edit(gameMsg(game)); + } + game.players[player.n] = player; +} + +/** + * Returns the game message + * @param {Object} game + * @returns {String} + */ +function gameMsg(game) { + return { + content: game.players.map(u => localize('uno', 'user-cards', { + u: '<@' + u.id + '>', + cards: '**' + (u.cards.length === 0 ? 7 : u.cards.length) + '**' + })).join(', ') + '\n' + + localize('uno', 'turn', {u: '<@' + game.players.find(p => p.turn).id + '>'}) + '\n' + + (game.previousCards.length > 0 ? localize('uno', 'previous-cards') + game.previousCards.filter(c => c).join(' → ') + '\n' : '') + '\n' + + colorEmojis[game.lastCard.color] + ' **' + game.lastCard.name + '**' + + (game.players.some(p => p.uno) ? '\nUno: ' + game.players.filter(p => p.uno).map(p => '<@' + p.id + '>').join(' ') : '') + + (game.pendingDraws > 0 ? '\n\n⚠️️ ' + localize('uno', 'pending-draws', {count: '**' + game.pendingDraws + '**'}) : ''), + allowedMentions: { + users: [game.players.find(p => p.turn).id] + }, + components: [publicrow] + }; +} + +module.exports.run = async function (interaction) { + const timestamp = ''; + const msg = await interaction.reply({ + content: localize('uno', 'challenge-message', {u: interaction.user.toString(), count: '**1**', timestamp}), + allowedMentions: { + users: [] + }, + fetchReply: true, + components: [ + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + style: 'PRIMARY', + customId: 'uno-join', + label: localize('tic-tac-toe', 'accept-invite') + }, + { + type: 'BUTTON', + style: 'SECONDARY', + customId: 'uno-start', + label: localize('uno', 'start-game') + } + ] + } + ] + }); + + const game = { + players: [{ + id: interaction.user.id, + interaction, + n: 0, + cards: [], + uno: false, + turn: false, + blockRedraw: false + }], + lastCard: { + name: cards[Math.floor(Math.random() * cards.length)], + color: colors[Math.floor(Math.random() * colors.length)] + }, + inactiveTimeout: [], + previousCards: [], + msg, + turns: 0, + reversed: false, + justChoosingColor: false, + pendingDraws: 0 + }; + + /** + * Starts the game + */ + async function startGame() { + if (game.players.length < 2) { + collector.stop(); + return interaction.editReply({content: localize('uno', 'not-enough-players'), components: []}).catch(() => { + }); + } + + game.players[Math.floor(Math.random() * game.players.length)].turn = true; + await interaction.editReply(gameMsg(game)).catch(() => { + }); + game.players.forEach(async p => { + for (let i = 0; i < 7; i++) p.cards.push({ + name: cards[Math.floor(Math.random() * cards.length)], + color: colors[Math.floor(Math.random() * colors.length)] + }); + + const m = await p.interaction.followUp({components: buildDeck(p, game), fetchReply: true, ephemeral: true}); + m.createMessageComponentCollector({componentType: 'BUTTON'}).on('collect', i => perPlayerHandler(i, p, game)); + }); + } + + const timeout = setTimeout(startGame, 179000); + + const collector = msg.createMessageComponentCollector({componentType: 'BUTTON'}); + collector.on('collect', async i => { + if (i.customId === 'uno-join') { + if (game.players.some(p => p.id === i.user.id)) return i.reply({ + content: localize('uno', 'already-joined'), + ephemeral: true + }); + if (game.players.length > 45) return i.reply({content: localize('uno', 'max-players'), ephemeral: true}); + game.players.push({ + id: i.user.id, + interaction: i, + n: game.players.length, + cards: [], + uno: false, + turn: false, + blockRedraw: false + }); + i.update({ + content: localize('uno', 'challenge-message', { + u: interaction.user.toString(), + count: '**' + game.players.length + '**', + timestamp + }), + allowedMentions: { + users: [] + } + }); + } else if (i.customId === 'uno-start') { + if (game.players[0].id !== i.user.id) return i.reply({ + content: localize('uno', 'not-host'), + ephemeral: true + }); + startGame(); + clearTimeout(timeout); + i.deferUpdate(); + } else if (i.customId === 'uno-deck') { + const player = game.players.find(p => p.id === i.user.id); + if (!player) return i.reply({content: localize('uno', 'not-in-game'), ephemeral: true}); + console.log(player); + const m = await i.reply({components: buildDeck(player, game), fetchReply: true, ephemeral: true}); + m.createMessageComponentCollector({componentType: 'BUTTON'}).on('collect', int => perPlayerHandler(int, player, game)); + } else if (i.customId === 'uno-uno') { + const player = game.players.find(p => p.id === i.user.id); + if (!player) return i.reply({content: localize('uno', 'not-in-game'), ephemeral: true}); + + if (player.cards.length === 2) { + player.uno = true; + i.reply({content: localize('uno', 'done-uno'), ephemeral: true}); + } else { + player.cards.push({ + name: cards[Math.floor(Math.random() * cards.length)], + color: colors[Math.floor(Math.random() * colors.length)] + }); + i.reply({content: localize('uno', 'cant-uno'), ephemeral: true}); + } + } + }); +}; + + +module.exports.config = { + name: 'uno', + description: localize('uno', 'command-description'), + defaultPermission: true +}; \ No newline at end of file diff --git a/modules/uno/module.json b/modules/uno/module.json new file mode 100644 index 00000000..d65f1e4d --- /dev/null +++ b/modules/uno/module.json @@ -0,0 +1,22 @@ +{ + "name": "uno", + "humanReadableName": { + "en": "Uno" + }, + "author": { + "scnxOrgID": "60", + "name": "TomatoCake", + "link": "https://github.com/DEVTomatoCake" + }, + "description": { + "en": "Let your users play Uno against each other!", + "de": "Lasse Nutzer auf deinem Server Uno gegeneinander spielen" + }, + "commands-dir": "/commands", + "noConfig": true, + "releaseDate": "0", + "tags": [ + "fun" + ], + "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/uno" +} \ No newline at end of file diff --git a/modules/welcomer/configs/channels.json b/modules/welcomer/configs/channels.json new file mode 100644 index 00000000..1c0bd7f6 --- /dev/null +++ b/modules/welcomer/configs/channels.json @@ -0,0 +1,311 @@ +{ + "description": { + "en": "Configure here in which channel which message should get send", + "de": "Passe hier an, in welchen Kanälen welche Nachricht gesendet werden soll" + }, + "humanName": { + "en": "Channel", + "de": "Kanäle" + }, + "filename": "channels.json", + "configElements": true, + "content": [ + { + "name": "channelID", + "humanName": { + "en": "Channel", + "de": "Channel" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel in which the message should get send", + "de": "Kanal in welchen die Nachricht gesendet werden soll" + }, + "type": "channelID" + }, + { + "name": "type", + "humanName": { + "en": "Channel-Type", + "de": "Kanal-Typ" + }, + "default": { + "en": "" + }, + "description": { + "en": "This sets in which content the channel should get used", + "de": "Dies gibt an, in welchem Kontext dieser Kanal verwendet werden soll" + }, + "type": "select", + "content": [ + "join", + "leave", + "boost", + "unboost" + ] + }, + { + "name": "randomMessages", + "humanName": { + "en": "Random messages?", + "de": "Zufällige Nachrichten?" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled the bot will randomly pick a messages instead of using the message option below", + "de": "Wenn aktiviert wird der Bot eine zufällige Nachricht aus deiner Konfiguration wählen, anstatt die unten" + }, + "type": "boolean" + }, + { + "name": "message", + "humanName": { + "de": "Nachricht", + "en": "Message" + }, + "default": { + "en": "" + }, + "description": { + "en": "Message that should get send", + "de": "Nachricht, die gesendet wird" + }, + "type": "string", + "allowEmbed": true, + "allowGeneratedImage": true, + "params": [ + { + "name": "mention", + "description": { + "en": "Mentions the user", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "memberProfilePictureUrl", + "description": { + "en": "URL of the user's avatar", + "de": "URL zum Avatar des Nutzers" + }, + "isImage": true + }, + { + "name": "servername", + "description": { + "en": "Name of the guild", + "de": "Servername" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "createdAt", + "description": { + "en": "Date when account was created", + "de": "Datum an dem der Account erstellt wurde" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "memberProfilePictureUrl", + "description": { + "en": "URL of the user's avatar", + "de": "URL zum Avatar des Nutzers" + }, + "isImage": true + }, + { + "name": "joinedAt", + "description": { + "en": "Date when user joined guild", + "de": "Datum, an dem der Nutzer den Server betreten hat" + } + }, + { + "name": "guildUserCount", + "description": { + "en": "Count of users on the guild", + "de": "Anzahl von Nutzern auf dem Server" + } + }, + { + "name": "guildMemberCount", + "description": { + "en": "Count of members (without bots) on the guild", + "de": "Anzahl von Nutzern auf dem Server" + } + }, + { + "name": "mention", + "description": { + "en": "Mention of the user who boosted", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "boostCount", + "description": { + "en": "Total count of boosts", + "de": "Gesamte Anzahl an Boosts" + } + }, + { + "name": "guildLevel", + "description": { + "en": "Boost-Level of the guild after the boost", + "de": "Boost-Level nach dem Boost" + } + }, + { + "name": "mention", + "description": { + "en": "Mention of the user who unboosted", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "boostCount", + "description": { + "en": "Total count of boosts", + "de": "Gesamte Anzahl an Boosts" + } + }, + { + "name": "guildLevel", + "description": { + "en": "Boost-Level of the guild after the unboost", + "de": "Boost-Level nach dem Boost" + } + } + ] + }, + { + "name": "welcome-button", + "humanName": { + "en": "Welcome-Button (only if \"Channel-Type\" = \"join\")", + "de": "Willkommens-Knopf (nur wenn \"Channel-Type\" = \"join\")" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, a welcome-button will be attached to the welcome message. When a user clicks on it, the bot will send a welcome-ping in a configured channel. The button can be pressed once.", + "de": "If enabled, a welcome-button will be attached to the welcome message. When a user clicks on it, the bot will send a welcome-ping in a configured channel. The button can be pressed once." + }, + "type": "boolean" + }, + { + "name": "welcome-button-content", + "dependsOn": "welcome-button", + "humanName": { + "en": "Welcome-Button-Content", + "de": "Willkommens-Knopf-Inhalt" + }, + "default": { + "en": "Say hi \uD83D\uDC4B", + "de": "Hallo sagen \uD83D\uDC4B" + }, + "description": { + "en": "Content of the welcome button", + "de": "Inhalt des Willkommens-Knopfes" + }, + "type": "string" + }, + { + "name": "welcome-button-channel", + "dependsOn": "welcome-button", + "humanName": { + "en": "Channel in which the welcome-button should send a message", + "de": "Kanal, in welchen der Willkommens-Knopf die Nachricht senden soll" + }, + "default": { + "en": "", + "de": "" + }, + "description": { + "en": "The bot will send the configured message in this channel when a user presses the button", + "de": "Der Bot wird die konfigurierte Nachricht in diesen Kanal senden, wenn jemand den Knopf drückt" + }, + "type": "channelID" + }, + { + "name": "welcome-button-message", + "dependsOn": "welcome-button", + "humanName": { + "en": "Welcome-Button-Message", + "de": "Willkommens-Knopf-Nachricht" + }, + "default": { + "en": "%clickUserMention% welcomes %userMention% :wave:", + "de": "%clickUserMention% begrüßt %userMention% :wave:" + }, + "allowEmbed": true, + "description": { + "en": "This is the message the bot will send in the configured channel when a user presses the button", + "de": "Der Bot wird in diesen Kanal die Nachricht senden, wenn ein Nutzer den Knopf drückt" + }, + "type": "string", + "params": [ + { + "name": "userMention", + "description": { + "en": "Mention of the user who joined the server", + "de": "Erwähnung des Nutzer, der den Server beigetreten hat" + } + }, + { + "name": "userTag", + "description": { + "en": "Tag of the user who joined the server", + "de": "Tag des Nutzer, der den Server beigetreten hat" + } + }, + { + "name": "userAvatarURL", + "isImage": true, + "description": { + "en": "Avatar of the user who joined the server", + "de": "Avatar des Nutzer, der den Server beigetreten hat" + } + }, + { + "name": "clickUserMention", + "description": { + "en": "Mention of the user who clicked the button", + "de": "Erwähnung des Nutzer, der den Knopf gedrückt hat" + } + }, + { + "name": "clickUserTag", + "description": { + "en": "Tag of the user who clicked the button", + "de": "Tag des Nutzer, der den Knopf gedrückt hat" + } + }, + { + "name": "clickUserAvatarURL", + "isImage": true, + "description": { + "en": "Avatar of the user who clicked the button", + "de": "Avatar des Nutzer, der den Knopf gedrückt hat" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/welcomer/configs/config.json b/modules/welcomer/configs/config.json new file mode 100644 index 00000000..2c3d6df0 --- /dev/null +++ b/modules/welcomer/configs/config.json @@ -0,0 +1,227 @@ +{ + "description": { + "en": "Manage the basic settings of this module here", + "de": "Passe die grundlegenden Optionen des Modules hier an" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "give-roles-on-join", + "humanName": { + "en": "Give roles on join", + "de": "Nutzer Rollen beim Beitreten geben" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Roles to give to a new member", + "de": "Rollen, die neuen Mitgliedern gegeben werden sollen" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "not-send-messages-if-member-is-bot", + "humanName": { + "en": "Ignore bots?", + "de": "Bots ignorieren?" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "Should bots get ignored when they join (or leave) the server", + "de": "Sollen Bots ignoriert werden, wenn sie den Server beitreten (oder diesen verlassen)" + }, + "type": "boolean" + }, + { + "name": "give-roles-on-boost", + "humanName": { + "en": "Zusätzliche Rollen beim Boost geben", + "de": "Give additional roles to boosters" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Roles to give to members who boosts the server", + "de": "Rollen, die Booster haben sollen" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "delete-welcome-message", + "humanName": { + "en": "Delete welcome message", + "de": "Willkommensnachricht löschen" + }, + "default": { + "en": true + }, + "description": { + "en": "Should their welcome message be deleted, if a user leaves the server within 7 days", + "de": "Soll die Willkommensnachricht eines Nutzers, der den Server innerhalb von 7 Tagen wieder verlässt gelöscht werden" + }, + "type": "boolean" + }, + { + "name": "sendDirectMessageOnJoin", + "humanName": { + "en": "Send DM on join? (often experienced by users as spam)", + "de": "PN beim Beitreten schicken? (von Nutzern oft als Spam empfunden)" + }, + "type": "boolean", + "default": { + "en": false + }, + "description": { + "en": "If enabled, a DM will be sent to new users. This is often experienced by them as spam and can decrease your new user retention metrics. Please note that not all users will receive this DM, as a huge chunk has DMs disabled.", + "de": "Wenn aktiviert, wird eine PN an neue Nutzer gesendet. Das wird often als Spam empfunden und kann die Anzahl an Nutzern erhöhen, die direkt nach dem Beitritt deinen Server verlassen. Bitte beachte, dass nicht alle Nutzer diese PN erhalten werden, da ein großer Anzahl diese deaktiviert hat." + } + }, + { + "name": "joinDM", + "dependsOn": "sendDirectMessageOnJoin", + "humanName": { + "en": "Join DM Message", + "de": "Beitritt PN Nachricht" + }, + "allowGeneratedImage": true, + "default": { + "en": "" + }, + "description": { + "en": "Message that should get send to new users via DMs", + "de": "Nachricht, die an neue Nutzer per PN geschickt werden soll" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "mention", + "description": { + "en": "Mentions the user", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "memberProfilePictureUrl", + "description": { + "en": "URL of the user's avatar", + "de": "URL zum Avatar des Nutzers" + }, + "isImage": true + }, + { + "name": "servername", + "description": { + "en": "Name of the guild", + "de": "Servername" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "createdAt", + "description": { + "en": "Date when account was created", + "de": "Datum an dem der Account erstellt wurde" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "memberProfilePictureUrl", + "description": { + "en": "URL of the user's avatar", + "de": "URL zum Avatar des Nutzers" + }, + "isImage": true + }, + { + "name": "joinedAt", + "description": { + "en": "Date when user joined guild", + "de": "Datum, an dem der Nutzer den Server betreten hat" + } + }, + { + "name": "guildUserCount", + "description": { + "en": "Count of users on the guild", + "de": "Anzahl von Nutzern auf dem Server" + } + }, + { + "name": "guildMemberCount", + "description": { + "en": "Count of members (without bots) on the guild", + "de": "Anzahl von Nutzern auf dem Server" + } + }, + { + "name": "mention", + "description": { + "en": "Mention of the user who boosted", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "boostCount", + "description": { + "en": "Total count of boosts", + "de": "Gesamte Anzahl an Boosts" + } + }, + { + "name": "guildLevel", + "description": { + "en": "Boost-Level of the guild after the boost", + "de": "Boost-Level nach dem Boost" + } + }, + { + "name": "mention", + "description": { + "en": "Mention of the user who unboosted", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "boostCount", + "description": { + "en": "Total count of boosts", + "de": "Gesamte Anzahl an Boosts" + } + }, + { + "name": "guildLevel", + "description": { + "en": "Boost-Level of the guild after the unboost", + "de": "Boost-Level nach dem Boost" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/welcomer/configs/random-messages.json b/modules/welcomer/configs/random-messages.json new file mode 100644 index 00000000..1227ecbf --- /dev/null +++ b/modules/welcomer/configs/random-messages.json @@ -0,0 +1,168 @@ +{ + "description": { + "en": "Manage the randomly send messages here", + "de": "Passe hier die Nachrichten an, die zufällig gesendet werden sollen" + }, + "humanName": { + "en": "Random messages", + "de": "Zufällige Nachrichten" + }, + "filename": "random-messages.json", + "configElements": true, + "content": [ + { + "name": "type", + "humanName": { + "en": "Message-Type", + "de": "Nachricht-Type" + }, + "default": { + "en": "" + }, + "description": { + "en": "This sets in which content the message should get send", + "de": "Dies gibt an, in welchem Kontext diese Nachricht versendet werden soll" + }, + "type": "select", + "content": [ + "join", + "leave", + "boost", + "unboost" + ] + }, + { + "name": "message", + "humanName": { + "en": "Message", + "de": "Nachricht" + }, + "allowGeneratedImage": true, + "default": { + "en": "" + }, + "description": { + "en": "Message that should get send", + "de": "Nachricht, die gesendet werden soll" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "mention", + "description": { + "en": "Mentions the user", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "memberProfilePictureUrl", + "description": { + "en": "URL of the user's avatar", + "de": "URL zum Avatar des Nutzers" + }, + "isImage": true + }, + { + "name": "servername", + "description": { + "en": "Name of the guild", + "de": "Servername" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "createdAt", + "description": { + "en": "Date when account was created", + "de": "Datum an dem der Account erstellt wurde" + } + }, + { + "name": "tag", + "description": { + "en": "Tag of the user", + "de": "Tag des Nutzers" + } + }, + { + "name": "memberProfilePictureUrl", + "description": { + "en": "URL of the user's avatar", + "de": "URL zum Avatar des Nutzers" + }, + "isImage": true + }, + { + "name": "joinedAt", + "description": { + "en": "Date when user joined guild", + "de": "Datum, an dem der Nutzer den Server betreten hat" + } + }, + { + "name": "guildUserCount", + "description": { + "en": "Count of users on the guild", + "de": "Anzahl von Nutzern auf dem Server" + } + }, + { + "name": "guildMemberCount", + "description": { + "en": "Count of members (without bots) on the guild", + "de": "Anzahl von Nutzern auf dem Server" + } + }, + { + "name": "mention", + "description": { + "en": "Mention of the user who boosted", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "boostCount", + "description": { + "en": "Total count of boosts", + "de": "Gesamte Anzahl an Boosts" + } + }, + { + "name": "guildLevel", + "description": { + "en": "Boost-Level of the guild after the boost", + "de": "Boost-Level nach dem Boost" + } + }, + { + "name": "mention", + "description": { + "en": "Mention of the user who unboosted", + "de": "Erwähnung des Nutzers" + } + }, + { + "name": "boostCount", + "description": { + "en": "Total count of boosts", + "de": "Gesamte Anzahl an Boosts" + } + }, + { + "name": "guildLevel", + "description": { + "en": "Boost-Level of the guild after the unboost", + "de": "Boost-Level nach dem Boost" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/welcomer/events/guildMemberAdd.js b/modules/welcomer/events/guildMemberAdd.js new file mode 100644 index 00000000..a695f691 --- /dev/null +++ b/modules/welcomer/events/guildMemberAdd.js @@ -0,0 +1,97 @@ +const { + randomElementFromArray, + embedType, + formatDate, + embedTypeV2, + formatDiscordUserName +} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (client, guildMember) { + if (!client.botReadyAt) return; + if (guildMember.guild.id !== client.guild.id) return; + const moduleConfig = client.configurations['welcomer']['config']; + const moduleModel = client.models['welcomer']['User']; + if (guildMember.user.bot && moduleConfig['not-send-messages-if-member-is-bot']) return; + + const args = { + '%mention%': guildMember.toString(), + '%servername%': guildMember.guild.name, + '%tag%': formatDiscordUserName(guildMember.user), + '%guildUserCount%': (await client.guild.members.fetch()).size, + '%guildMemberCount%': (await client.guild.members.fetch()).filter(m => !m.user.bot).size, + '%memberProfilePictureUrl%': guildMember.user.avatarURL() || guildMember.user.defaultAvatarURL, + '%createdAt%': formatDate(guildMember.user.createdAt), + '%guildLevel%': localize('boostTier', client.guild.premiumTier), + '%boostCount%': client.guild.premiumSubscriptionCount, + '%joinedAt%': formatDate(guildMember.joinedAt) + }; + if (moduleConfig.sendDirectMessageOnJoin) guildMember.user.send(await embedTypeV2(moduleConfig.joinDM, args)).then(() => { + }).catch(() => { + }); + + const moduleChannels = client.configurations['welcomer']['channels']; + + if (!guildMember.pending && moduleConfig['give-roles-on-join'].length !== 0) { + setTimeout(async () => { + if (!guildMember.doNotGiveWelcomeRole) { + const m = await guildMember.fetch(true); + m.roles.add(moduleConfig['give-roles-on-join']).then(() => { + }); + } + }, 300); + } + + for (const channelConfig of moduleChannels.filter(c => c.type === 'join')) { + const channel = await guildMember.guild.channels.fetch(channelConfig.channelID).catch(() => { + }); + if (!channel) { + client.logger.error(localize('welcomer', 'channel-not-found', {c: channelConfig.channelID})); + continue; + } + let message; + if (channelConfig.randomMessages) { + message = (randomElementFromArray(client.configurations['welcomer']['random-messages'].filter(m => m.type === 'join')) || {}).message; + } + if (!message) message = channelConfig.message; + + const components = []; + if (channelConfig['welcome-button']) { + components.push({ + type: 'ACTION_ROW', + components: [ + { + label: channelConfig['welcome-button-content'], + customId: 'welcome-' + guildMember.id, + style: 'PRIMARY', + type: 'BUTTON' + } + ] + }); + } + const sentMessage = await channel.send(await embedTypeV2(message || 'Message not found', + args, + {}, + components + )); + const memberModel = await moduleModel.findOne({ + where: { + userId: guildMember.id, + channelID: sentMessage.channelId + } + }); + if (memberModel) { + await memberModel.update({ + messageID: sentMessage.id, + timestamp: new Date() + }); + } else { + await moduleModel.create({ + userID: guildMember.id, + channelID: sentMessage.channelId, + messageID: sentMessage.id, + timestamp: new Date() + }); + } + } +}; \ No newline at end of file diff --git a/modules/welcomer/events/guildMemberRemove.js b/modules/welcomer/events/guildMemberRemove.js new file mode 100644 index 00000000..0fb9ab33 --- /dev/null +++ b/modules/welcomer/events/guildMemberRemove.js @@ -0,0 +1,85 @@ +const { + randomElementFromArray, + embedType, + formatDate, + embedTypeV2, + formatDiscordUserName +} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (client, guildMember) { + if (!client.botReadyAt) return; + if (guildMember.guild.id !== client.guild.id) return; + const moduleConfig = client.configurations['welcomer']['config']; + const moduleModel = client.models['welcomer']['User']; + if (guildMember.user.bot && moduleConfig['not-send-messages-if-member-is-bot']) return; + + const moduleChannels = client.configurations['welcomer']['channels']; + + for (const channelConfig of moduleChannels.filter(c => c.type === 'leave')) { + const channel = await guildMember.guild.channels.fetch(channelConfig.channelID).catch(() => { + }); + if (!channel) { + client.logger.error(localize('welcomer', 'channel-not-found', {c: channelConfig.channelID})); + continue; + } + + let message; + if (channelConfig.randomMessages) { + message = (randomElementFromArray(client.configurations['welcomer']['random-messages'].filter(m => m.type === 'leave')) || {}).message; + } + if (!message) message = channelConfig.message; + + await channel.send(await embedTypeV2(message || 'Message not found', + { + '%mention%': guildMember.toString(), + '%servername%': guildMember.guild.name, + '%tag%': formatDiscordUserName(guildMember.user), + '%guildUserCount%': (await client.guild.members.fetch()).size, + '%guildMemberCount%': (await client.guild.members.fetch()).filter(m => !m.user.bot).size, + '%memberProfilePictureUrl%': guildMember.user.avatarURL() || guildMember.user.defaultAvatarURL, + '%createdAt%': formatDate(guildMember.user.createdAt), + '%guildLevel%': client.guild.premiumTier, + '%boostCount%': client.guild.premiumSubscriptionCount, + '%joinedAt%': formatDate(guildMember.joinedAt) + } + )); + } + if (!moduleConfig['delete-welcome-message']) return; + const memberModels = await moduleModel.findAll({ + where: { + userId: guildMember.id + } + }); + for (const memberModel of memberModels) { + const channel = await guildMember.guild.channels.fetch(memberModel.channelID).catch(() => { + }); + if (await timer(client, guildMember.id)) { + try { + await (await channel.messages.fetch(memberModel.messageID)).delete(); + } catch (e) { + } + } + await memberModel.destroy(); + } +}; + +/** + ** Function to handle the time stuff + * @private + * @param client Client of the bot + * @param {userId} userId Id of the User + * @returns {Promise} + */ +async function timer(client, userId) { + const model = client.models['welcomer']['User']; + const timeModel = await model.findOne({ + where: { + userId: userId + } + }); + if (timeModel) { + // check timer duration + return timeModel.timestamp.getTime() + 604800000 >= Date.now(); + } +} \ No newline at end of file diff --git a/modules/welcomer/events/guildMemberUpdate.js b/modules/welcomer/events/guildMemberUpdate.js new file mode 100644 index 00000000..d1a1ce67 --- /dev/null +++ b/modules/welcomer/events/guildMemberUpdate.js @@ -0,0 +1,70 @@ +const { + randomElementFromArray, + embedType, + formatDate, + embedTypeV2, + formatDiscordUserName +} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); +module.exports.run = async function (client, oldGuildMember, newGuildMember) { + const moduleConfig = client.configurations['welcomer']['config']; + + if (!client.botReadyAt) return; + if (oldGuildMember.pending && !newGuildMember.pending) { + await newGuildMember.roles.add(moduleConfig['give-roles-on-join']); + } + + if (newGuildMember.guild.id !== client.guild.id) return; + + if (!oldGuildMember.premiumSince && newGuildMember.premiumSince) { + await sendBoostMessage('boost'); + } + + if (oldGuildMember.premiumSince && !newGuildMember.premiumSince) { + await sendBoostMessage('unboost'); + } + + /** + * Sends the boost message + * @private + * @param {String} type Type of the boost + * @return {Promise} + */ + async function sendBoostMessage(type) { + const moduleChannels = client.configurations['welcomer']['channels']; + + for (const channelConfig of moduleChannels.filter(c => c.type === type)) { + const channel = await newGuildMember.guild.channels.fetch(channelConfig.channelID).catch(() => { + }); + if (!channel) { + client.logger.error(localize('welcomer', 'channel-not-found', {c: channelConfig.channelID})); + continue; + } + let message; + if (channelConfig.randomMessages) { + message = (randomElementFromArray(client.configurations['welcomer']['random-messages'].filter(m => m.type === type)) || {}).message; + } + if (!message) message = channelConfig.message; + + await channel.send(await embedTypeV2(message || 'Message not found', + { + '%mention%': newGuildMember.toString(), + '%servername%': newGuildMember.guild.name, + '%tag%': formatDiscordUserName(newGuildMember.user), + '%guildUserCount%': (await client.guild.members.fetch()).size, + '%guildMemberCount%': (await client.guild.members.fetch()).filter(m => !m.user.bot).size, + '%memberProfilePictureUrl%': newGuildMember.user.avatarURL() || newGuildMember.user.defaultAvatarURL, + '%createdAt%': formatDate(newGuildMember.user.createdAt), + '%guildLevel%': localize('boostTier', client.guild.premiumTier), + '%boostCount%': client.guild.premiumSubscriptionCount, + '%joinedAt%': formatDate(newGuildMember.joinedAt) + } + )); + + if (moduleConfig['give-roles-on-boost'].length !== 0) { + if (type === 'boost') newGuildMember.roles.add(moduleConfig['give-roles-on-boost']); + else newGuildMember.roles.remove(moduleConfig['give-roles-on-boost']); + } + } + } +}; \ No newline at end of file diff --git a/modules/welcomer/events/interactionCreate.js b/modules/welcomer/events/interactionCreate.js new file mode 100644 index 00000000..e83497f9 --- /dev/null +++ b/modules/welcomer/events/interactionCreate.js @@ -0,0 +1,33 @@ +const {localize} = require('../../../src/functions/localize'); +const {embedType, formatDiscordUserName} = require('../../../src/functions/helpers'); +module.exports.run = async function (client, interaction) { + if (!interaction.isButton()) return; + if (!interaction.customId.startsWith('welcome-')) return; + const userID = interaction.customId.replaceAll('welcome-', ''); + if (userID === interaction.user.id) return interaction.reply({ + ephemeral: true, + content: '👋 ' + localize('welcomer', 'welcome-yourself-error') + }); + const channelConfig = client.configurations['welcomer']['channels'].find(c => c.channelID === interaction.channel.id); + if (!channelConfig) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('welcomer', 'channel-not-found', {c: channelConfig.channelID}) + }); + const sendChannel = interaction.guild.channels.cache.get(channelConfig['welcome-button-channel']); + if (!sendChannel) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('welcomer', 'channel-not-found', {c: channelConfig.sendChannel}) + }); + await interaction.update({ + components: interaction.message.components.filter(f => f.components[0].customId !== interaction.customId) + }); + const user = await client.users.fetch(userID); + sendChannel.send(embedType(channelConfig['welcome-button-message'], { + '%userMention%': user.toString(), + '%userTag%': formatDiscordUserName(user), + '%userAvatarURL%': user.avatarURL(), + '%clickUserMention%': interaction.user.toString(), + '%clickUserTag%': formatDiscordUserName(interaction.user), + '%clickUserAvatarURL%': interaction.user.avatarURL() + })); +}; \ No newline at end of file diff --git a/modules/welcomer/models/User.js b/modules/welcomer/models/User.js new file mode 100644 index 00000000..c078bac2 --- /dev/null +++ b/modules/welcomer/models/User.js @@ -0,0 +1,26 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class WelcomerUser extends Model { + static init(sequelize) { + return super.init({ + id: { + autoIncrement: true, + type: DataTypes.INTEGER, + primaryKey: true + }, + userID: DataTypes.STRING, + channelID: DataTypes.STRING, + messageID: DataTypes.STRING, + timestamp: DataTypes.DATE + }, { + tableName: 'welcomer_User', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'User', + 'module': 'welcomer' +}; \ No newline at end of file diff --git a/modules/welcomer/module.json b/modules/welcomer/module.json new file mode 100644 index 00000000..7abfcc93 --- /dev/null +++ b/modules/welcomer/module.json @@ -0,0 +1,27 @@ +{ + "name": "welcomer", + "author": { + "scnxOrgID": "1", + "name": "SCDerox (SC Network Team)", + "link": "https://github.com/SCDerox" + }, + "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/welcomer", + "events-dir": "/events", + "models-dir": "/models", + "config-example-files": [ + "configs/channels.json", + "configs/random-messages.json", + "configs/config.json" + ], + "tags": [ + "administration" + ], + "humanReadableName": { + "en": "Welcome and Boosts", + "de": "Willkommen und Boosts" + }, + "description": { + "en": "Simple module to say \"Hi\" to new members, give them roles automatically and say \"thanks\" to users who boosted", + "de": "Einfaches Modul zum Begrüßen von neuen Usern, zum automatischen Vergeben von Rollen beim Joinen und zum Bedanken bei Boosts." + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..ae833812 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,19082 @@ +{ + "name": "customdcbot", + "version": "3.8.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "customdcbot", + "version": "3.8.0", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@androz2091/discord-invites-tracker": "1.1.1", + "@pixelfactory/privatebin": "2.6.1", + "@scderox/ikea-name-generator": "1.0.0", + "@twurple/api": "5.3.4", + "@twurple/auth": "5.3.4", + "age-calculator": "1.0.0", + "axios": "^1.4.0", + "bs58": "5.0.0", + "centra": "2.6.0", + "discord-logs": "2.2.1", + "discord.js": "13.17.1", + "dotenv": "16.3.1", + "express": "^4.18.2", + "fs-extra": "11.1.1", + "html-entities": "2.4.0", + "is-equal": "1.6.4", + "isomorphic-webcrypto": "2.3.8", + "jsonfile": "6.1.0", + "log4js": "6.9.1", + "node-schedule": "2.1.1", + "parse-duration": "1.1.0", + "sequelize": "6.33.0", + "sqlite3": "5.1.6", + "stop-discord-phishing": "0.3.3", + "string-similarity": "^4.0.4", + "tldjs": "^2.3.2", + "url-parse": "^1.5.10" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "funding": { + "url": "https://github.com/ScootKit/CustomDCBot?sponsor=1" + }, + "optionalDependencies": { + "bufferutil": "4.0.7", + "erlpack": "github:discord/erlpack", + "utf-8-validate": "6.0.3", + "zlib-sync": "0.1.8" + } + }, + "node_modules/@androz2091/discord-invites-tracker": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@androz2091/discord-invites-tracker/-/discord-invites-tracker-1.1.1.tgz", + "integrity": "sha512-5oGwZNLnQcn+PMqtif84aCjbDdqCYvw0r8brRtlBDQV0HLwfLimD6XSo19HpTQY/1Z6dT1A9nEmvLHHvU8YEjw==" + }, + "node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "devOptional": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", + "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.24.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", + "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", + "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.17.tgz", + "integrity": "sha512-bxH77R5gjH3Nkde6/LuncQoLaP16THYPscurp1S8z7S9ZgezCyV3G8Hc+TZiCmY8pz4fp8CvKSgtJMW0FkLAxA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.17" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.17.tgz", + "integrity": "sha512-nAhoheCMlrqU41tAojw9GpVEKDlTS8r3lzFmF0lP52LwblCPbuFSO7nGIZoIcoU5NIm1ABrna0cJExE4Ay6l2Q==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.17" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "devOptional": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/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==", + "devOptional": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/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==", + "devOptional": true + }, + "node_modules/@babel/highlight/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==", + "devOptional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "devOptional": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", + "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", + "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.1.tgz", + "integrity": "sha512-zPEvzFijn+hRvJuX2Vu3KbEBN39LN3f7tW3MQO2LsIs57B26KU+kUc82BdAktS1VCM6libzh45eKGI65lg0cpA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-decorators": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.22.17.tgz", + "integrity": "sha512-cop/3quQBVvdz6X5SJC6AhUv3C9DrVTM06LUEXimEdWAhCSyOJIr9NiZDU9leHZ0/aiG0Sh7Zmvaku5TWYNgbA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-default-from": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "optional": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.1.tgz", + "integrity": "sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.22.5.tgz", + "integrity": "sha512-ODAqWWXB/yReh/jVQDag/3/tl6lgBueQkk/TcfW/59Oykm4c8a55XloX0CTk2k2VJiFWMgHby9xNX29IbCv9dQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz", + "integrity": "sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", + "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", + "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", + "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz", + "integrity": "sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", + "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", + "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz", + "integrity": "sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", + "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", + "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.22.5.tgz", + "integrity": "sha512-tujNbZdxdG0/54g/oua8ISToaXTFBf8EnSb5PgQSciIXWOWKX3S4+JR7ZE9ol8FZwf9kxitzkGQ+QWeov/mCiA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-flow": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", + "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", + "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", + "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", + "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz", + "integrity": "sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", + "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", + "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", + "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", + "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz", + "integrity": "sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", + "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", + "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz", + "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", + "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/types": "^7.23.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz", + "integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz", + "integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz", + "integrity": "sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", + "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.15.tgz", + "integrity": "sha512-tEVLhk8NRZSmwQ0DJtxxhTrCht1HVo8VaMzYT4w6lwyKBuHsgoioAUA7/6eT2fRfc5/23fuGdlwIxXhRVgWr4g==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.5.tgz", + "integrity": "sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-typescript": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", + "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.15.tgz", + "integrity": "sha512-tZFHr54GBkHk6hQuVA8w4Fmq+MSPsfvMG0vPnOYyTnJpyfMqybL8/MbNCPRT9zc2KBO2pe4tq15g6Uno4Jpoag==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.15", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.15", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.11", + "@babel/plugin-transform-classes": "^7.22.15", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.15", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.11", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-for-of": "^7.22.15", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.11", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.15", + "@babel/plugin-transform-modules-systemjs": "^7.22.11", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-numeric-separator": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.22.15", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.22.15", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.22.15", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.22.15.tgz", + "integrity": "sha512-dB5aIMqpkgbTfN5vDdTRPzjqtWiZcRESNR88QYnoPR+bmdYoluOzMX9tQerTv0XzSgZYctPfO1oc0N5zdog1ew==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-transform-flow-strip-types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.1.tgz", + "integrity": "sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-transform-react-display-name": "^7.24.1", + "@babel/plugin-transform-react-jsx": "^7.23.4", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz", + "integrity": "sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-syntax-jsx": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-typescript": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.15.tgz", + "integrity": "sha512-V3Q3EqoQdn65RCgTLwauZaTfd1ShhwPmbBv+1dkZV/HpCGMKVyn6oFcRlI7RaKqiDQjX2Qd3AuoEguBgdjIKlg==", + "optional": true, + "peer": true, + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.5", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "optional": true, + "peer": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "optional": true, + "peer": true + }, + "node_modules/@babel/runtime": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", + "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "optional": true, + "peer": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@d-fischer/cache-decorators": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@d-fischer/cache-decorators/-/cache-decorators-3.0.3.tgz", + "integrity": "sha512-JmM9OyZY+nNRRsW+bS3i+PSjmXiR3BCBiyHjjvpTWhS373xYtNdWbzxPDtKu2SWpE2lpnGP0QwINe3Uo5BBxDw==", + "dependencies": { + "@d-fischer/shared-utils": "^3.0.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@d-fischer/cross-fetch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@d-fischer/cross-fetch/-/cross-fetch-4.2.1.tgz", + "integrity": "sha512-/tvOWaOFBW2NyLCuJ0Tf2wFaEqZudT9osF/2A7/K4NU+g7MAQfOAEMUizKtg3TTrEfwWLjGic3oOBdbmR3WBKg==", + "dependencies": { + "node-fetch": "^2.6.11" + } + }, + "node_modules/@d-fischer/detect-node": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@d-fischer/detect-node/-/detect-node-3.0.1.tgz", + "integrity": "sha512-0Rf3XwTzuTh8+oPZW9SfxTIiL+26RRJ0BRPwj5oVjZFyFKmsj9RGfN2zuTRjOuA3FCK/jYm06HOhwNK+8Pfv8w==" + }, + "node_modules/@d-fischer/logger": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@d-fischer/logger/-/logger-4.2.3.tgz", + "integrity": "sha512-mJUx9OgjrNVLQa4od/+bqnmD164VTCKnK5B4WOW8TX5y/3w2i58p+PMRE45gUuFjk2BVtOZUg55JQM3d619fdw==", + "dependencies": { + "@d-fischer/detect-node": "^3.0.1", + "@d-fischer/shared-utils": "^3.2.0", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/promise.allsettled": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@d-fischer/promise.allsettled/-/promise.allsettled-2.0.2.tgz", + "integrity": "sha512-xY0vYDwJYFe22MS5ccQ50N4Mcc2nQ8J4eWE5Y354IxZwW32O5uTT6mmhFSuVF6ZrKvzHOCIrK+9WqOR6TI3tcA==", + "dependencies": { + "array.prototype.map": "^1.0.3", + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2", + "get-intrinsic": "^1.0.2", + "iterate-value": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/qs": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@d-fischer/qs/-/qs-7.0.2.tgz", + "integrity": "sha512-yAu3xDooiL+ef84Jo8nLjDjWBRk7RXk163Y6aTvRB7FauYd3spQD/dWvgT7R4CrN54Juhrrc3dMY7mc+jZGurQ==", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/rate-limiter": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@d-fischer/rate-limiter/-/rate-limiter-0.6.2.tgz", + "integrity": "sha512-wgeDJuczBhhQ44E5O+phNIx74WAzOTcqJa8x+fJtDmGocyhQP+To2GumBfINB0Jao+MmRiqUPd4TPoUbe2yISg==", + "dependencies": { + "@d-fischer/logger": "^4.0.0", + "@d-fischer/promise.allsettled": "^2.0.2", + "@d-fischer/shared-utils": "^3.2.0", + "@types/node": "^12.12.5", + "tslib": "^2.0.3" + } + }, + "node_modules/@d-fischer/shared-utils": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@d-fischer/shared-utils/-/shared-utils-3.6.3.tgz", + "integrity": "sha512-Lz+Qk1WJLVoeREOHPZcIDTHOoxecxMSG2sq+x1xWYCH1exqiMKMMx06pXdy15UzHG7ohvQRNXk2oHqZ9EOl9jQ==", + "dependencies": { + "tslib": "^2.4.1" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@discordjs/builders": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.16.0.tgz", + "integrity": "sha512-9/NCiZrLivgRub2/kBc0Vm5pMBE5AUdYbdXsLu/yg9ANgvnaJ0bZKTY8yYnLbsEc/LYUP79lEIdC73qEYhWq7A==", + "deprecated": "no longer supported", + "dependencies": { + "@sapphire/shapeshift": "^3.5.1", + "discord-api-types": "^0.36.2", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.36.3", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.36.3.tgz", + "integrity": "sha512-bz/NDyG0KBo/tY14vSkrwQ/n3HKPf87a0WFW/1M9+tXYK+vp5Z5EksawfCWo2zkAc6o7CClc0eff1Pjrqznlwg==" + }, + "node_modules/@discordjs/collection": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.7.0.tgz", + "integrity": "sha512-R5i8Wb8kIcBAFEPLLf7LVBQKBDYUL+ekb23sOgpkpyGT+V4P7V83wTxcsqmX+PbqHt4cEHn053uMWfRqh/Z/nA==", + "deprecated": "no longer supported", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@expo/bunyan": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@expo/bunyan/-/bunyan-4.0.0.tgz", + "integrity": "sha512-Ydf4LidRB/EBI+YrB+cVLqIseiRfjUI/AeHBgjGMtq3GroraDu81OV7zqophRgupngoL3iS3JUMDMnxO7g39qA==", + "engines": [ + "node >=0.10.0" + ], + "optional": true, + "peer": true, + "dependencies": { + "uuid": "^8.0.0" + }, + "optionalDependencies": { + "mv": "~2", + "safe-json-stringify": "~1" + } + }, + "node_modules/@expo/cli": { + "version": "0.18.10", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.18.10.tgz", + "integrity": "sha512-cuAE060tcX4Mn+sF+tGAchGDsTNzwCUB7ioFGB3OrvxoU3idsqZJPs6xMt5Utuuy7QDGPnOn68H0vC4kDsXkUQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.20.0", + "@expo/code-signing-certificates": "0.0.5", + "@expo/config": "~9.0.0-beta.0", + "@expo/config-plugins": "~8.0.0-beta.0", + "@expo/devcert": "^1.0.0", + "@expo/env": "~0.3.0", + "@expo/image-utils": "^0.5.0", + "@expo/json-file": "^8.3.0", + "@expo/metro-config": "~0.18.0", + "@expo/osascript": "^2.0.31", + "@expo/package-manager": "^1.5.0", + "@expo/plist": "^0.1.0", + "@expo/prebuild-config": "7.0.3", + "@expo/rudder-sdk-node": "1.1.1", + "@expo/spawn-async": "^1.7.2", + "@expo/xcpretty": "^4.3.0", + "@react-native/dev-middleware": "~0.74.75", + "@urql/core": "2.3.6", + "@urql/exchange-retry": "0.3.0", + "accepts": "^1.3.8", + "arg": "5.0.2", + "better-opn": "~3.0.2", + "bplist-parser": "^0.3.1", + "cacache": "^15.3.0", + "chalk": "^4.0.0", + "ci-info": "^3.3.0", + "connect": "^3.7.0", + "debug": "^4.3.4", + "env-editor": "^0.4.1", + "fast-glob": "^3.3.2", + "find-yarn-workspace-root": "~2.0.0", + "form-data": "^3.0.1", + "freeport-async": "2.0.0", + "fs-extra": "~8.1.0", + "getenv": "^1.0.0", + "glob": "^7.1.7", + "graphql": "15.8.0", + "graphql-tag": "^2.10.1", + "https-proxy-agent": "^5.0.1", + "internal-ip": "4.3.0", + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1", + "js-yaml": "^3.13.1", + "json-schema-deref-sync": "^0.13.0", + "lodash.debounce": "^4.0.8", + "md5hex": "^1.0.0", + "minimatch": "^3.0.4", + "node-fetch": "^2.6.7", + "node-forge": "^1.3.1", + "npm-package-arg": "^7.0.0", + "open": "^8.3.0", + "ora": "3.4.0", + "picomatch": "^3.0.1", + "pretty-bytes": "5.6.0", + "progress": "2.0.3", + "prompts": "^2.3.2", + "qrcode-terminal": "0.11.0", + "require-from-string": "^2.0.2", + "requireg": "^0.2.2", + "resolve": "^1.22.2", + "resolve-from": "^5.0.0", + "resolve.exports": "^2.0.2", + "semver": "^7.6.0", + "send": "^0.18.0", + "slugify": "^1.3.4", + "source-map-support": "~0.5.21", + "stacktrace-parser": "^0.1.10", + "structured-headers": "^0.4.1", + "tar": "^6.0.5", + "temp-dir": "^2.0.0", + "tempy": "^0.7.1", + "terminal-link": "^2.1.1", + "text-table": "^0.2.0", + "url-join": "4.0.0", + "wrap-ansi": "^7.0.0", + "ws": "^8.12.1" + }, + "bin": { + "expo-internal": "build/bin/cli" + } + }, + "node_modules/@expo/cli/node_modules/@expo/prebuild-config": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-7.0.3.tgz", + "integrity": "sha512-Kvxy/oQzkxwXLvAmwb+ygxuRn4xUUN2+mVJj3KDe4bRVCNyDPs7wlgdokF3twnWjzRZssUzseMkhp+yHPjAEhA==", + "optional": true, + "peer": true, + "dependencies": { + "@expo/config": "~9.0.0-beta.0", + "@expo/config-plugins": "~8.0.0-beta.0", + "@expo/config-types": "^51.0.0-unreleased", + "@expo/image-utils": "^0.5.0", + "@expo/json-file": "^8.3.0", + "@react-native/normalize-colors": "~0.74.83", + "debug": "^4.3.1", + "fs-extra": "^9.0.0", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "xml2js": "0.6.0" + }, + "peerDependencies": { + "expo-modules-autolinking": ">=0.8.1" + } + }, + "node_modules/@expo/cli/node_modules/@expo/prebuild-config/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "optional": true, + "peer": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/cli/node_modules/@react-native/normalize-colors": { + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.83.tgz", + "integrity": "sha512-jhCY95gRDE44qYawWVvhTjTplW1g+JtKTKM3f8xYT1dJtJ8QWv+gqEtKcfmOHfDkSDaMKG0AGBaDTSK8GXLH8Q==", + "optional": true, + "peer": true + }, + "node_modules/@expo/cli/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@expo/cli/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "optional": true, + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@expo/cli/node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "optional": true, + "peer": true, + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/cli/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==", + "optional": true, + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@expo/cli/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==", + "optional": true, + "peer": true + }, + "node_modules/@expo/cli/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==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@expo/cli/node_modules/expo-modules-autolinking": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-1.11.1.tgz", + "integrity": "sha512-2dy3lTz76adOl7QUvbreMCrXyzUiF8lygI7iFJLjgIQIVH+43KnFWE5zBumpPbkiaq0f0uaFpN9U0RGQbnKiMw==", + "optional": true, + "peer": true, + "dependencies": { + "chalk": "^4.1.0", + "commander": "^7.2.0", + "fast-glob": "^3.2.5", + "find-up": "^5.0.0", + "fs-extra": "^9.1.0" + }, + "bin": { + "expo-modules-autolinking": "bin/expo-modules-autolinking.js" + } + }, + "node_modules/@expo/cli/node_modules/expo-modules-autolinking/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "optional": true, + "peer": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/cli/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "optional": true, + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@expo/cli/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@expo/cli/node_modules/fs-extra/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optional": true, + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@expo/cli/node_modules/fs-extra/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@expo/cli/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "peer": true, + "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/@expo/cli/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/cli/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "optional": true, + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@expo/cli/node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "optional": true, + "peer": true, + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/cli/node_modules/log-symbols/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==", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/cli/node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/cli/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/cli/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "optional": true, + "peer": true, + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/cli/node_modules/ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "optional": true, + "peer": true, + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@expo/cli/node_modules/ora/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==", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/cli/node_modules/ora/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/cli/node_modules/ora/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@expo/cli/node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@expo/cli/node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "optional": true, + "peer": true, + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/cli/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/cli/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@expo/code-signing-certificates": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz", + "integrity": "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==", + "optional": true, + "peer": true, + "dependencies": { + "node-forge": "^1.2.1", + "nullthrows": "^1.1.1" + } + }, + "node_modules/@expo/config": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-9.0.1.tgz", + "integrity": "sha512-0tjaXBstTbXmD4z+UMFBkh2SZFwilizSQhW6DlaTMnPG5ezuw93zSFEWAuEC3YzkpVtNQTmYzxAYjxwh6seOGg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "~7.10.4", + "@expo/config-plugins": "~8.0.0-beta.0", + "@expo/config-types": "^51.0.0-unreleased", + "@expo/json-file": "^8.3.0", + "getenv": "^1.0.0", + "glob": "7.1.6", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "slugify": "^1.3.4", + "sucrase": "3.34.0" + } + }, + "node_modules/@expo/config-plugins": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-8.0.4.tgz", + "integrity": "sha512-Hi+xuyNWE2LT4LVbGttHJgl9brnsdWAhEB42gWKb5+8ae86Nr/KwUBQJsJppirBYTeLjj5ZlY0glYnAkDa2jqw==", + "optional": true, + "peer": true, + "dependencies": { + "@expo/config-types": "^51.0.0-unreleased", + "@expo/json-file": "~8.3.0", + "@expo/plist": "^0.1.0", + "@expo/sdk-runtime-versions": "^1.0.0", + "chalk": "^4.1.2", + "debug": "^4.3.1", + "find-up": "~5.0.0", + "getenv": "^1.0.0", + "glob": "7.1.6", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "slash": "^3.0.0", + "slugify": "^1.6.6", + "xcode": "^3.0.1", + "xml2js": "0.6.0" + } + }, + "node_modules/@expo/config-types": { + "version": "51.0.0", + "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-51.0.0.tgz", + "integrity": "sha512-acn03/u8mQvBhdTQtA7CNhevMltUhbSrpI01FYBJwpVntufkU++ncQujWKlgY/OwIajcfygk1AY4xcNZ5ImkRA==", + "optional": true, + "peer": true + }, + "node_modules/@expo/config/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/devcert": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.1.0.tgz", + "integrity": "sha512-ghUVhNJQOCTdQckSGTHctNp/0jzvVoMMkVh+6SHn+TZj8sU15U/npXIDt8NtQp0HedlPaCgkVdMu8Sacne0aEA==", + "optional": true, + "peer": true, + "dependencies": { + "application-config-path": "^0.1.0", + "command-exists": "^1.2.4", + "debug": "^3.1.0", + "eol": "^0.9.1", + "get-port": "^3.2.0", + "glob": "^7.1.2", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "password-prompt": "^1.0.4", + "rimraf": "^2.6.2", + "sudo-prompt": "^8.2.0", + "tmp": "^0.0.33", + "tslib": "^2.4.0" + } + }, + "node_modules/@expo/devcert/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "optional": true, + "peer": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@expo/devcert/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "optional": true, + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/@expo/env": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-0.3.0.tgz", + "integrity": "sha512-OtB9XVHWaXidLbHvrVDeeXa09yvTl3+IQN884sO6PhIi2/StXfgSH/9zC7IvzrDB8kW3EBJ1PPLuCUJ2hxAT7Q==", + "optional": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "getenv": "^1.0.0" + } + }, + "node_modules/@expo/env/node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@expo/image-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.5.1.tgz", + "integrity": "sha512-U/GsFfFox88lXULmFJ9Shfl2aQGcwoKPF7fawSCLixIKtMCpsI+1r0h+5i0nQnmt9tHuzXZDL8+Dg1z6OhkI9A==", + "optional": true, + "peer": true, + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.0.0", + "fs-extra": "9.0.0", + "getenv": "^1.0.0", + "jimp-compact": "0.16.1", + "node-fetch": "^2.6.0", + "parse-png": "^2.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "tempy": "0.3.0" + } + }, + "node_modules/@expo/image-utils/node_modules/crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/image-utils/node_modules/fs-extra": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz", + "integrity": "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==", + "optional": true, + "peer": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/image-utils/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/image-utils/node_modules/temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", + "integrity": "sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/image-utils/node_modules/tempy": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.3.0.tgz", + "integrity": "sha512-WrH/pui8YCwmeiAoxV+lpRH9HpRtgBhSR2ViBPgpGb/wnYDzp21R4MN45fsCGvLROvY67o3byhJRYRONJyImVQ==", + "optional": true, + "peer": true, + "dependencies": { + "temp-dir": "^1.0.0", + "type-fest": "^0.3.1", + "unique-string": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/image-utils/node_modules/type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@expo/image-utils/node_modules/unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==", + "optional": true, + "peer": true, + "dependencies": { + "crypto-random-string": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/image-utils/node_modules/universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@expo/json-file": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-8.3.3.tgz", + "integrity": "sha512-eZ5dld9AD0PrVRiIWpRkm5aIoWBw3kAyd8VkuWEy92sEthBKDDDHAnK2a0dw0Eil6j7rK7lS/Qaq/Zzngv2h5A==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.2", + "write-file-atomic": "^2.3.0" + } + }, + "node_modules/@expo/metro-config": { + "version": "0.18.3", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.18.3.tgz", + "integrity": "sha512-E4iW+VT/xHPPv+t68dViOsW7egtGIr+sRElcym0iGpC4goLz9WBux/xGzWgxvgvvHEWa21uSZQPM0jWla0OZXg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@babel/parser": "^7.20.0", + "@babel/types": "^7.20.0", + "@expo/config": "~9.0.0-beta.0", + "@expo/env": "~0.3.0", + "@expo/json-file": "~8.3.0", + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "find-yarn-workspace-root": "~2.0.0", + "fs-extra": "^9.1.0", + "getenv": "^1.0.0", + "glob": "^7.2.3", + "jsc-safe-url": "^0.2.4", + "lightningcss": "~1.19.0", + "postcss": "~8.4.32", + "resolve-from": "^5.0.0" + } + }, + "node_modules/@expo/metro-config/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "optional": true, + "peer": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/metro-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "peer": true, + "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/@expo/osascript": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.1.2.tgz", + "integrity": "sha512-/ugqDG+52uzUiEpggS9GPdp9g0U9EQrXcTdluHDmnlGmR2nV/F83L7c+HCUyPnf77QXwkr8gQk16vQTbxBQ5eA==", + "optional": true, + "peer": true, + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "exec-async": "^2.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/package-manager": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.5.2.tgz", + "integrity": "sha512-IuA9XtGBilce0q8cyxtWINqbzMB1Fia0Yrug/O53HNuRSwQguV/iqjV68bsa4z8mYerePhcFgtvISWLAlNEbUA==", + "optional": true, + "peer": true, + "dependencies": { + "@expo/json-file": "^8.3.0", + "@expo/spawn-async": "^1.7.2", + "ansi-regex": "^5.0.0", + "chalk": "^4.0.0", + "find-up": "^5.0.0", + "find-yarn-workspace-root": "~2.0.0", + "js-yaml": "^3.13.1", + "micromatch": "^4.0.2", + "npm-package-arg": "^7.0.0", + "ora": "^3.4.0", + "split": "^1.0.1", + "sudo-prompt": "9.1.1" + } + }, + "node_modules/@expo/package-manager/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==", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "optional": true, + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@expo/package-manager/node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "optional": true, + "peer": true, + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/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==", + "optional": true, + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@expo/package-manager/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==", + "optional": true, + "peer": true + }, + "node_modules/@expo/package-manager/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==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@expo/package-manager/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "optional": true, + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@expo/package-manager/node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "optional": true, + "peer": true, + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "optional": true, + "peer": true, + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "optional": true, + "peer": true, + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@expo/package-manager/node_modules/ora/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "optional": true, + "peer": true, + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@expo/package-manager/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@expo/package-manager/node_modules/sudo-prompt": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.1.1.tgz", + "integrity": "sha512-es33J1g2HjMpyAhz8lOR+ICmXXAqTuKbuXuUWLhOLew20oN9oUCgCJx615U/v7aioZg7IX5lIh9x34vwneu4pA==", + "optional": true, + "peer": true + }, + "node_modules/@expo/package-manager/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/plist": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.1.3.tgz", + "integrity": "sha512-GW/7hVlAylYg1tUrEASclw1MMk9FP4ZwyFAY/SUTJIhPDQHtfOlXREyWV3hhrHdX/K+pS73GNgdfT6E/e+kBbg==", + "optional": true, + "peer": true, + "dependencies": { + "@xmldom/xmldom": "~0.7.7", + "base64-js": "^1.2.3", + "xmlbuilder": "^14.0.0" + } + }, + "node_modules/@expo/rudder-sdk-node": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz", + "integrity": "sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ==", + "optional": true, + "peer": true, + "dependencies": { + "@expo/bunyan": "^4.0.0", + "@segment/loosely-validate-event": "^2.0.0", + "fetch-retry": "^4.1.1", + "md5": "^2.2.1", + "node-fetch": "^2.6.1", + "remove-trailing-slash": "^0.1.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/sdk-runtime-versions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz", + "integrity": "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==", + "optional": true, + "peer": true + }, + "node_modules/@expo/spawn-async": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz", + "integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==", + "optional": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/vector-icons": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-14.0.1.tgz", + "integrity": "sha512-7oIe1RRWmRQXNxmewsuAaIRNAQfkig7EFTuI5T8PCI7T4q/rS5iXWvlzAEXndkzSOSs7BAANrLyj7AtpEhTksg==", + "optional": true, + "peer": true, + "dependencies": { + "prop-types": "^15.8.1" + } + }, + "node_modules/@expo/xcpretty": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.3.1.tgz", + "integrity": "sha512-sqXgo1SCv+j4VtYEwl/bukuOIBrVgx6euIoCat3Iyx5oeoXwEA2USCoeL0IPubflMxncA2INkqJ/Wr3NGrSgzw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "7.10.4", + "chalk": "^4.1.0", + "find-up": "^5.0.0", + "js-yaml": "^4.1.0" + }, + "bin": { + "excpretty": "build/cli.js" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "optional": true, + "peer": true, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "optional": true, + "peer": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "optional": true, + "peer": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "optional": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "optional": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "optional": true, + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "devOptional": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment/node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "devOptional": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "devOptional": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "devOptional": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "devOptional": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/transform/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "optional": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "devOptional": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "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==", + "optional": true, + "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==", + "optional": true, + "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==", + "optional": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz", + "integrity": "sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==", + "dependencies": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.3.tgz", + "integrity": "sha512-VtaY4spKTdN5LjJ04im/d/joXuvLbQdgy5Z4DXF4MFZhQ+MTrejbNMkfZBp1Bs3O5+bFqnJgyGdPuZQflvIa5A==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/json-schema": "^1.1.12", + "pvtsutils": "^1.3.2", + "tslib": "^2.5.0", + "webcrypto-core": "^1.7.7" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@pixelfactory/privatebin": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@pixelfactory/privatebin/-/privatebin-2.6.1.tgz", + "integrity": "sha512-qMPaq6pONB6Xmqpb2PRcmCgfl4MCbdoVMiLzUztLdWStUAP6fKcvtZyYCQYkDvy3oT3oxwPaTxoWDY1f9Tb6PQ==", + "dependencies": { + "axios": "^0.21.1", + "bs58": "^4.0.1", + "byte-base64": "^1.1.0", + "chalk": "^4.1.0", + "commander": "^7.1.0", + "inquirer": "^8.0.0", + "isomorphic-webcrypto": "^2.3.8", + "pako": "^2.0.3", + "pjson": "^1.0.9", + "yaml": "^1.10.0" + }, + "bin": { + "privatebin": "dist/bin/privatebin.js" + } + }, + "node_modules/@pixelfactory/privatebin/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/@pixelfactory/privatebin/node_modules/base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@pixelfactory/privatebin/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/@react-native-community/cli": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-11.3.6.tgz", + "integrity": "sha512-bdwOIYTBVQ9VK34dsf6t3u6vOUU5lfdhKaAxiAVArjsr7Je88Bgs4sAbsOYsNK3tkE8G77U6wLpekknXcanlww==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-clean": "11.3.6", + "@react-native-community/cli-config": "11.3.6", + "@react-native-community/cli-debugger-ui": "11.3.6", + "@react-native-community/cli-doctor": "11.3.6", + "@react-native-community/cli-hermes": "11.3.6", + "@react-native-community/cli-plugin-metro": "11.3.6", + "@react-native-community/cli-server-api": "11.3.6", + "@react-native-community/cli-tools": "11.3.6", + "@react-native-community/cli-types": "11.3.6", + "chalk": "^4.1.2", + "commander": "^9.4.1", + "execa": "^5.0.0", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0", + "graceful-fs": "^4.1.3", + "prompts": "^2.4.0", + "semver": "^7.5.2" + }, + "bin": { + "react-native": "build/bin.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@react-native-community/cli-clean": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-11.3.6.tgz", + "integrity": "sha512-jOOaeG5ebSXTHweq1NznVJVAFKtTFWL4lWgUXl845bCGX7t1lL8xQNWHKwT8Oh1pGR2CI3cKmRjY4hBg+pEI9g==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-tools": "11.3.6", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "prompts": "^2.4.0" + } + }, + "node_modules/@react-native-community/cli-clean/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "optional": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@react-native-community/cli-clean/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-clean/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-clean/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "optional": true, + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-community/cli-config": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-11.3.6.tgz", + "integrity": "sha512-edy7fwllSFLan/6BG6/rznOBCLPrjmJAE10FzkEqNLHowi0bckiAPg1+1jlgQ2qqAxV5kuk+c9eajVfQvPLYDA==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-tools": "11.3.6", + "chalk": "^4.1.2", + "cosmiconfig": "^5.1.0", + "deepmerge": "^4.3.0", + "glob": "^7.1.3", + "joi": "^17.2.1" + } + }, + "node_modules/@react-native-community/cli-debugger-ui": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-11.3.6.tgz", + "integrity": "sha512-jhMOSN/iOlid9jn/A2/uf7HbC3u7+lGktpeGSLnHNw21iahFBzcpuO71ekEdlmTZ4zC/WyxBXw9j2ka33T358w==", + "optional": true, + "peer": true, + "dependencies": { + "serve-static": "^1.13.1" + } + }, + "node_modules/@react-native-community/cli-doctor": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-11.3.6.tgz", + "integrity": "sha512-UT/Tt6omVPi1j6JEX+CObc85eVFghSZwy4GR9JFMsO7gNg2Tvcu1RGWlUkrbmWMAMHw127LUu6TGK66Ugu1NLA==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-config": "11.3.6", + "@react-native-community/cli-platform-android": "11.3.6", + "@react-native-community/cli-platform-ios": "11.3.6", + "@react-native-community/cli-tools": "11.3.6", + "chalk": "^4.1.2", + "command-exists": "^1.2.8", + "envinfo": "^7.7.2", + "execa": "^5.0.0", + "hermes-profile-transformer": "^0.0.6", + "ip": "^1.1.5", + "node-stream-zip": "^1.9.1", + "ora": "^5.4.1", + "prompts": "^2.4.0", + "semver": "^7.5.2", + "strip-ansi": "^5.2.0", + "sudo-prompt": "^9.0.0", + "wcwidth": "^1.0.1", + "yaml": "^2.2.1" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "optional": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "optional": true, + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/sudo-prompt": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", + "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", + "optional": true, + "peer": true + }, + "node_modules/@react-native-community/cli-doctor/node_modules/yaml": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", + "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@react-native-community/cli-hermes": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-11.3.6.tgz", + "integrity": "sha512-O55YAYGZ3XynpUdePPVvNuUPGPY0IJdctLAOHme73OvS80gNwfntHDXfmY70TGHWIfkK2zBhA0B+2v8s5aTyTA==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-platform-android": "11.3.6", + "@react-native-community/cli-tools": "11.3.6", + "chalk": "^4.1.2", + "hermes-profile-transformer": "^0.0.6", + "ip": "^1.1.5" + } + }, + "node_modules/@react-native-community/cli-platform-android": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-11.3.6.tgz", + "integrity": "sha512-ZARrpLv5tn3rmhZc//IuDM1LSAdYnjUmjrp58RynlvjLDI4ZEjBAGCQmgysRgXAsK7ekMrfkZgemUczfn9td2A==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-tools": "11.3.6", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "glob": "^7.1.3", + "logkitty": "^0.7.1" + } + }, + "node_modules/@react-native-community/cli-platform-android/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "optional": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@react-native-community/cli-platform-android/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-platform-android/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-platform-android/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "optional": true, + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-community/cli-platform-ios": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-11.3.6.tgz", + "integrity": "sha512-tZ9VbXWiRW+F+fbZzpLMZlj93g3Q96HpuMsS6DRhrTiG+vMQ3o6oPWSEEmMGOvJSYU7+y68Dc9ms2liC7VD6cw==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-tools": "11.3.6", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-xml-parser": "^4.0.12", + "glob": "^7.1.3", + "ora": "^5.4.1" + } + }, + "node_modules/@react-native-community/cli-platform-ios/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "optional": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@react-native-community/cli-platform-ios/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-platform-ios/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-platform-ios/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "optional": true, + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-community/cli-plugin-metro": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-11.3.6.tgz", + "integrity": "sha512-D97racrPX3069ibyabJNKw9aJpVcaZrkYiEzsEnx50uauQtPDoQ1ELb/5c6CtMhAEGKoZ0B5MS23BbsSZcLs2g==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-server-api": "11.3.6", + "@react-native-community/cli-tools": "11.3.6", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "metro": "0.76.7", + "metro-config": "0.76.7", + "metro-core": "0.76.7", + "metro-react-native-babel-transformer": "0.76.7", + "metro-resolver": "0.76.7", + "metro-runtime": "0.76.7", + "readline": "^1.3.0" + } + }, + "node_modules/@react-native-community/cli-plugin-metro/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "optional": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@react-native-community/cli-plugin-metro/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-plugin-metro/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-plugin-metro/node_modules/metro-runtime": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.76.7.tgz", + "integrity": "sha512-MuWHubQHymUWBpZLwuKZQgA/qbb35WnDAKPo83rk7JRLIFPvzXSvFaC18voPuzJBt1V98lKQIonh6MiC9gd8Ug==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "react-refresh": "^0.4.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@react-native-community/cli-plugin-metro/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "optional": true, + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-community/cli-server-api": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-11.3.6.tgz", + "integrity": "sha512-8GUKodPnURGtJ9JKg8yOHIRtWepPciI3ssXVw5jik7+dZ43yN8P5BqCoDaq8e1H1yRer27iiOfT7XVnwk8Dueg==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-debugger-ui": "11.3.6", + "@react-native-community/cli-tools": "11.3.6", + "compression": "^1.7.1", + "connect": "^3.6.5", + "errorhandler": "^1.5.1", + "nocache": "^3.0.1", + "pretty-format": "^26.6.2", + "serve-static": "^1.13.1", + "ws": "^7.5.1" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@react-native-community/cli-tools": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-11.3.6.tgz", + "integrity": "sha512-JpmUTcDwAGiTzLsfMlIAYpCMSJ9w2Qlf7PU7mZIRyEu61UzEawyw83DkqfbzDPBuRwRnaeN44JX2CP/yTO3ThQ==", + "optional": true, + "peer": true, + "dependencies": { + "appdirsjs": "^1.2.4", + "chalk": "^4.1.2", + "find-up": "^5.0.0", + "mime": "^2.4.1", + "node-fetch": "^2.6.0", + "open": "^6.2.0", + "ora": "^5.4.1", + "semver": "^7.5.2", + "shell-quote": "^1.7.3" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "optional": true, + "peer": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-community/cli-types": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-11.3.6.tgz", + "integrity": "sha512-6DxjrMKx5x68N/tCJYVYRKAtlRHbtUVBZrnAvkxbRWFD9v4vhNgsPM0RQm8i2vRugeksnao5mbnRGpS6c0awCw==", + "optional": true, + "peer": true, + "dependencies": { + "joi": "^17.2.1" + } + }, + "node_modules/@react-native-community/cli/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/@react-native-community/cli/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "optional": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@react-native-community/cli/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "optional": true, + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-community/cli/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@react-native-community/cli/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optional": true, + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@react-native-community/cli/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "optional": true, + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-community/cli/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "optional": true, + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-community/cli/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "optional": true, + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "optional": true, + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-community/cli/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.72.0", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz", + "integrity": "sha512-Im93xRJuHHxb1wniGhBMsxLwcfzdYreSZVQGDoMJgkd6+Iky61LInGEHnQCTN0fKNYF1Dvcofb4uMmE1RQHXHQ==", + "optional": true, + "peer": true + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.74.83.tgz", + "integrity": "sha512-+S0st3t4Ro00bi9gjT1jnK8qTFOU+CwmziA7U9odKyWrCoRJrgmrvogq/Dr1YXlpFxexiGIupGut1VHxr+fxJA==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native/codegen": "0.74.83" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-plugin-codegen/node_modules/@react-native/codegen": { + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.74.83.tgz", + "integrity": "sha512-GgvgHS3Aa2J8/mp1uC/zU8HuTh8ZT5jz7a4mVMWPw7+rGyv70Ba8uOVBq6UH2Q08o617IATYc+0HfyzAfm4n0w==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.0", + "glob": "^7.1.1", + "hermes-parser": "0.19.1", + "invariant": "^2.2.4", + "jscodeshift": "^0.14.0", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/@react-native/babel-plugin-codegen/node_modules/hermes-estree": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.19.1.tgz", + "integrity": "sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==", + "optional": true, + "peer": true + }, + "node_modules/@react-native/babel-plugin-codegen/node_modules/hermes-parser": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.19.1.tgz", + "integrity": "sha512-Vp+bXzxYJWrpEuJ/vXxUsLnt0+y4q9zyi4zUlkLqD8FKv4LjIfOvP69R/9Lty3dCyKh0E2BU7Eypqr63/rKT/A==", + "optional": true, + "peer": true, + "dependencies": { + "hermes-estree": "0.19.1" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.74.83.tgz", + "integrity": "sha512-KJuu3XyVh3qgyUer+rEqh9a/JoUxsDOzkJNfRpDyXiAyjDRoVch60X/Xa/NcEQ93iCVHAWs0yQ+XGNGIBCYE6g==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/plugin-proposal-async-generator-functions": "^7.0.0", + "@babel/plugin-proposal-class-properties": "^7.18.0", + "@babel/plugin-proposal-export-default-from": "^7.0.0", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", + "@babel/plugin-proposal-numeric-separator": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.20.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-default-from": "^7.0.0", + "@babel/plugin-syntax-flow": "^7.18.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", + "@babel/plugin-syntax-optional-chaining": "^7.0.0", + "@babel/plugin-transform-arrow-functions": "^7.0.0", + "@babel/plugin-transform-async-to-generator": "^7.20.0", + "@babel/plugin-transform-block-scoping": "^7.0.0", + "@babel/plugin-transform-classes": "^7.0.0", + "@babel/plugin-transform-computed-properties": "^7.0.0", + "@babel/plugin-transform-destructuring": "^7.20.0", + "@babel/plugin-transform-flow-strip-types": "^7.20.0", + "@babel/plugin-transform-function-name": "^7.0.0", + "@babel/plugin-transform-literals": "^7.0.0", + "@babel/plugin-transform-modules-commonjs": "^7.0.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", + "@babel/plugin-transform-parameters": "^7.0.0", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-react-display-name": "^7.0.0", + "@babel/plugin-transform-react-jsx": "^7.0.0", + "@babel/plugin-transform-react-jsx-self": "^7.0.0", + "@babel/plugin-transform-react-jsx-source": "^7.0.0", + "@babel/plugin-transform-runtime": "^7.0.0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0", + "@babel/plugin-transform-spread": "^7.0.0", + "@babel/plugin-transform-sticky-regex": "^7.0.0", + "@babel/plugin-transform-typescript": "^7.5.0", + "@babel/plugin-transform-unicode-regex": "^7.0.0", + "@babel/template": "^7.0.0", + "@react-native/babel-plugin-codegen": "0.74.83", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/babel-preset/node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.72.7", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.72.7.tgz", + "integrity": "sha512-O7xNcGeXGbY+VoqBGNlZ3O05gxfATlwE1Q1qQf5E38dK+tXn5BY4u0jaQ9DPjfE8pBba8g/BYI1N44lynidMtg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.0", + "flow-parser": "^0.206.0", + "jscodeshift": "^0.14.0", + "nullthrows": "^1.1.1" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.74.83.tgz", + "integrity": "sha512-RGQlVUegBRxAUF9c1ss1ssaHZh6CO+7awgtI9sDeU0PzDZY/40ImoPD5m0o0SI6nXoVzbPtcMGzU+VO590pRfA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.74.83.tgz", + "integrity": "sha512-UH8iriqnf7N4Hpi20D7M2FdvSANwTVStwFCSD7VMU9agJX88Yk0D1T6Meh2RMhUu4kY2bv8sTkNRm7LmxvZqgA==", + "optional": true, + "peer": true, + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.74.83", + "@rnx-kit/chromium-edge-launcher": "^1.0.0", + "chrome-launcher": "^0.15.2", + "connect": "^3.6.5", + "debug": "^2.2.0", + "node-fetch": "^2.2.0", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "selfsigned": "^2.4.1", + "serve-static": "^1.13.1", + "temp-dir": "^2.0.0", + "ws": "^6.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true + }, + "node_modules/@react-native/dev-middleware/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "optional": true, + "peer": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "optional": true, + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.72.11", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.72.11.tgz", + "integrity": "sha512-P9iRnxiR2w7EHcZ0mJ+fmbPzMby77ZzV6y9sJI3lVLJzF7TLSdbwcQyD3lwMsiL+q5lKUHoZJS4sYmih+P2HXw==", + "optional": true, + "peer": true + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.72.1", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.72.1.tgz", + "integrity": "sha512-cRPZh2rBswFnGt5X5EUEPs0r+pAsXxYsifv/fgy9ZLQokuT52bPH+9xjDR+7TafRua5CttGW83wP4TntRcWNDA==", + "optional": true, + "peer": true + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.72.0", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.72.0.tgz", + "integrity": "sha512-285lfdqSXaqKuBbbtP9qL2tDrfxdOFtIMvkKadtleRQkdOxx+uzGvFr82KHmc/sSiMtfXGp7JnFYWVh4sFl7Yw==", + "optional": true, + "peer": true + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.72.8", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz", + "integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==", + "optional": true, + "peer": true, + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "peerDependencies": { + "react-native": "*" + } + }, + "node_modules/@rnx-kit/chromium-edge-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rnx-kit/chromium-edge-launcher/-/chromium-edge-launcher-1.0.0.tgz", + "integrity": "sha512-lzD84av1ZQhYUS+jsGqJiCMaJO2dn9u+RTT9n9q6D3SaKVwWqv+7AoRKqBu19bkwyE+iFRl1ymr40QS90jVFYg==", + "optional": true, + "peer": true, + "dependencies": { + "@types/node": "^18.0.0", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=14.15" + } + }, + "node_modules/@rnx-kit/chromium-edge-launcher/node_modules/@types/node": { + "version": "18.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.33.tgz", + "integrity": "sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@rnx-kit/chromium-edge-launcher/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.2.tgz", + "integrity": "sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@scderox/ikea-name-generator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@scderox/ikea-name-generator/-/ikea-name-generator-1.0.0.tgz", + "integrity": "sha512-tBeB6sfUR6ZzrwWDbjQejuij09+KAQAAI9eba8DKe+ZARDTgbaXhjMU25NyU0AL+qPgBy4Nm8ZPyncy8Ee8Abw==", + "dependencies": { + "compromise": "^13.11.2", + "nlp_compromise": "^4.12.0", + "nlp-syllables": "^0.0.5" + } + }, + "node_modules/@segment/loosely-validate-event": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz", + "integrity": "sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==", + "optional": true, + "peer": true, + "dependencies": { + "component-type": "^1.2.1", + "join-component": "^1.1.0" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "optional": true, + "peer": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "optional": true, + "peer": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "optional": true, + "peer": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "devOptional": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "devOptional": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "devOptional": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@twurple/api": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@twurple/api/-/api-5.3.4.tgz", + "integrity": "sha512-i1THeJ4CgsTSmGtjdtk81gZoxfJxJrke8mJyC62jmgvRK18kEILcdEjjCsboLq4Tvp2js8fs1X2zwd4t8FgsLQ==", + "dependencies": { + "@d-fischer/cache-decorators": "^3.0.0", + "@d-fischer/detect-node": "^3.0.1", + "@d-fischer/logger": "^4.0.0", + "@d-fischer/rate-limiter": "^0.6.1", + "@d-fischer/shared-utils": "^3.4.0", + "@twurple/api-call": "5.3.4", + "@twurple/common": "5.3.4", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "@twurple/auth": "5.3.4" + } + }, + "node_modules/@twurple/api-call": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@twurple/api-call/-/api-call-5.3.4.tgz", + "integrity": "sha512-LSMdS1+K59PwPCcNtphpILnoUBo7xxkJYq0heppQW0F8lu8ANz34NvjjmlV5vZg5NF+VpfAztiYZIi/gRuVgvw==", + "dependencies": { + "@d-fischer/cross-fetch": "^4.0.2", + "@d-fischer/qs": "^7.0.2", + "@d-fischer/shared-utils": "^3.4.0", + "@twurple/common": "5.3.4", + "@types/node-fetch": "^2.5.7", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@twurple/auth": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@twurple/auth/-/auth-5.3.4.tgz", + "integrity": "sha512-qoChaHplRLJQsQaz6bFstR+/6VpnyUUj69jbqcXsJEUsetXWjhLT3cp0JDVKs8r4qMjZptr4XYq8kfHFZJhbHg==", + "dependencies": { + "@d-fischer/logger": "^4.0.0", + "@d-fischer/shared-utils": "^3.4.0", + "@twurple/api-call": "5.3.4", + "@twurple/common": "5.3.4", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@twurple/common": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@twurple/common/-/common-5.3.4.tgz", + "integrity": "sha512-vMpuhoNAlETwOSJDUYrxYBahAaLJWOn0lEs+eeCea32Z6cefvs/qXsQ3p2Kl9aY3bic+wGhHW0uB4Os7ZS3hHA==", + "dependencies": { + "@d-fischer/shared-utils": "^3.4.0", + "klona": "^2.0.4", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "devOptional": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "devOptional": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "devOptional": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, + "node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.5.tgz", + "integrity": "sha512-OZsUlr2nxvkqUFLSaY2ZbA+P1q22q+KrlxWOn/38RX+u5kTkYL2mTujEpzUhGkS+K/QCYp9oagfXG39XOzyySg==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "optional": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "devOptional": true + }, + "node_modules/@types/validator": { + "version": "13.11.1", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.1.tgz", + "integrity": "sha512-d/MUkJYdOeKycmm75Arql4M5+UuXmf4cHdHKsyw1GcvnNgL6s77UkgSgJ8TE/rI5PYsnwYq5jkcWBLuN/MpQ1A==" + }, + "node_modules/@types/ws": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "15.0.15", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.15.tgz", + "integrity": "sha512-IziEYMU9XoVj8hWg7k+UJrXALkGFjWJhn5QFEv9q4p+v40oZhSuC135M38st8XPjICL7Ey4TV64ferBGUoJhBg==", + "optional": true, + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "devOptional": true + }, + "node_modules/@unimodules/core": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@unimodules/core/-/core-7.1.2.tgz", + "integrity": "sha512-lY+e2TAFuebD3vshHMIRqru3X4+k7Xkba4Wa7QsDBd+ex4c4N2dHAO61E2SrGD9+TRBD8w/o7mzK6ljbqRnbyg==", + "deprecated": "replaced by the 'expo' package, learn more: https://blog.expo.dev/whats-new-in-expo-modules-infrastructure-7a7cdda81ebc", + "optional": true, + "dependencies": { + "compare-versions": "^3.4.0" + } + }, + "node_modules/@unimodules/react-native-adapter": { + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/@unimodules/react-native-adapter/-/react-native-adapter-6.3.9.tgz", + "integrity": "sha512-i9/9Si4AQ8awls+YGAKkByFbeAsOPgUNeLoYeh2SQ3ddjxJ5ZJDtq/I74clDnpDcn8zS9pYlcDJ9fgVJa39Glw==", + "deprecated": "replaced by the 'expo' package, learn more: https://blog.expo.dev/whats-new-in-expo-modules-infrastructure-7a7cdda81ebc", + "optional": true, + "dependencies": { + "expo-modules-autolinking": "^0.0.3", + "invariant": "^2.2.4" + } + }, + "node_modules/@urql/core": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@urql/core/-/core-2.3.6.tgz", + "integrity": "sha512-PUxhtBh7/8167HJK6WqBv6Z0piuiaZHQGYbhwpNL9aIQmLROPEdaUYkY4wh45wPQXcTpnd11l0q3Pw+TI11pdw==", + "optional": true, + "peer": true, + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.0", + "wonka": "^4.0.14" + }, + "peerDependencies": { + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@urql/exchange-retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-0.3.0.tgz", + "integrity": "sha512-hHqer2mcdVC0eYnVNbWyi28AlGOPb2vjH3lP3/Bc8Lc8BjhMsDwFMm7WhoP5C1+cfbr/QJ6Er3H/L08wznXxfg==", + "optional": true, + "peer": true, + "dependencies": { + "@urql/core": ">=2.3.1", + "wonka": "^4.0.14" + }, + "peerDependencies": { + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "optional": true, + "peer": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "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==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "optional": true, + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/age-calculator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/age-calculator/-/age-calculator-1.0.0.tgz", + "integrity": "sha512-3+vuEZXhfUpwl70cHJ/0g1r1nxVMzKjuOSuwXZdPRJ/z9vZMUj4/DfzkQwVABSaG0YEdr1zz6hlTR1g3lYvolg==" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "optional": true, + "peer": true + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-fragments": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-fragments/-/ansi-fragments-0.2.1.tgz", + "integrity": "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==", + "optional": true, + "peer": true, + "dependencies": { + "colorette": "^1.0.7", + "slice-ansi": "^2.0.0", + "strip-ansi": "^5.0.0" + } + }, + "node_modules/ansi-fragments/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-fragments/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "optional": true, + "peer": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "devOptional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/appdirsjs": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz", + "integrity": "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==", + "optional": true, + "peer": true + }, + "node_modules/application-config-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/application-config-path/-/application-config-path-0.1.1.tgz", + "integrity": "sha512-zy9cHePtMP0YhwG+CfHm0bgwdnga2X3gZexpdCwEj//dpb+TKajtiC8REEUJUSq6Ab4f9cgNy2l8ObXzCXFkEw==", + "optional": true, + "peer": true + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "optional": true, + "peer": true + }, + "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==", + "optional": true, + "peer": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.map": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.6.tgz", + "integrity": "sha512-nK1psgF2cXqP3wSyCSq0Hc7zwNq3sfljQqaG27r/7a7ooNUnn5nGq6yYWyks9jMO5EoFQ0ax80hSg6oXSRNXaw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "optional": true, + "peer": true + }, + "node_modules/asmcrypto.js": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/asmcrypto.js/-/asmcrypto.js-0.22.0.tgz", + "integrity": "sha512-usgMoyXjMbx/ZPdzTSXExhMPur2FTdz/Vo5PVx2gIaBcdAAJNOFlsdgqveM8Cff7W0v+xrf9BwjOV26JSAF9qA==" + }, + "node_modules/asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "dependencies": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "optional": true, + "peer": true + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "optional": true, + "peer": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "optional": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b64-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/b64-lite/-/b64-lite-1.4.0.tgz", + "integrity": "sha512-aHe97M7DXt+dkpa8fHlCcm1CnskAHrJqEfMI0KN7dwqlzml/aUe1AGt6lk51HzrSfVD67xOso84sOpr+0wIe2w==", + "dependencies": { + "base-64": "^0.1.0" + } + }, + "node_modules/b64u-lite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/b64u-lite/-/b64u-lite-1.1.0.tgz", + "integrity": "sha512-929qWGDVCRph7gQVTC6koHqQIpF4vtVaSbwLltFQo44B1bYUquALswZdBKFfrJCPEnsCOvWkJsPdQYZ/Ukhw8A==", + "dependencies": { + "b64-lite": "^1.4.0" + } + }, + "node_modules/babel-core": { + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", + "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", + "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2", + "core-js-compat": "^3.31.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", + "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-react-native-web": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.19.11.tgz", + "integrity": "sha512-0sHf8GgDhsRZxGwlwHHdfL3U8wImFaLw4haEa60U9M3EiO3bg6u3BJ+1vXhwgrevqSq76rMb5j1HJs+dNvMj5g==", + "optional": true, + "peer": true + }, + "node_modules/babel-plugin-syntax-trailing-function-commas": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz", + "integrity": "sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==", + "optional": true, + "peer": true + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-expo": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-11.0.5.tgz", + "integrity": "sha512-IjqR4B7wnBU55pofLeLGjwUGrWJE1buamgzE9CYpYCNicZmJcNjXUcinQiurXCMuClF2hOff3QfZsLxnGj1UaA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.12.13", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/preset-react": "^7.22.15", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-preset": "~0.74.83", + "babel-plugin-react-native-web": "~0.19.10", + "react-refresh": "^0.14.2" + } + }, + "node_modules/babel-preset-expo/node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-preset-fbjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz", + "integrity": "sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/plugin-proposal-class-properties": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0", + "@babel/plugin-syntax-class-properties": "^7.0.0", + "@babel/plugin-syntax-flow": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "@babel/plugin-transform-arrow-functions": "^7.0.0", + "@babel/plugin-transform-block-scoped-functions": "^7.0.0", + "@babel/plugin-transform-block-scoping": "^7.0.0", + "@babel/plugin-transform-classes": "^7.0.0", + "@babel/plugin-transform-computed-properties": "^7.0.0", + "@babel/plugin-transform-destructuring": "^7.0.0", + "@babel/plugin-transform-flow-strip-types": "^7.0.0", + "@babel/plugin-transform-for-of": "^7.0.0", + "@babel/plugin-transform-function-name": "^7.0.0", + "@babel/plugin-transform-literals": "^7.0.0", + "@babel/plugin-transform-member-expression-literals": "^7.0.0", + "@babel/plugin-transform-modules-commonjs": "^7.0.0", + "@babel/plugin-transform-object-super": "^7.0.0", + "@babel/plugin-transform-parameters": "^7.0.0", + "@babel/plugin-transform-property-literals": "^7.0.0", + "@babel/plugin-transform-react-display-name": "^7.0.0", + "@babel/plugin-transform-react-jsx": "^7.0.0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0", + "@babel/plugin-transform-spread": "^7.0.0", + "@babel/plugin-transform-template-literals": "^7.0.0", + "babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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==" + }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, + "node_modules/base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + }, + "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" + } + ] + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/better-opn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", + "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", + "optional": true, + "peer": true, + "dependencies": { + "open": "^8.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "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/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/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/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "optional": true, + "peer": true, + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "optional": true, + "peer": true, + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "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==", + "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==", + "devOptional": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "devOptional": true, + "dependencies": { + "node-int64": "^0.4.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" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "optional": true, + "peer": true, + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "optional": true, + "peer": true + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "optional": true, + "peer": true + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "devOptional": true + }, + "node_modules/bufferutil": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", + "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==", + "optional": true, + "peer": true + }, + "node_modules/byte-base64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/byte-base64/-/byte-base64-1.1.0.tgz", + "integrity": "sha512-56cXelkJrVMdCY9V/3RfDxTh4VfMFCQ5km7B7GkIGfo4bcPL9aACyJLB0Ms3Ezu5rsHmLB2suis96z4fLM03DA==" + }, + "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/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/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==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "optional": true, + "peer": true, + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-callsite/node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "optional": true, + "peer": true, + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/centra": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.6.0.tgz", + "integrity": "sha512-dgh+YleemrT8u85QL11Z6tYhegAs3MMxsaWAq/oXeAmYJ7VxL3SI9TZtnfaEvNDMAPolj25FXIb3S+HCI4wQaQ==" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "optional": true, + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "optional": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", + "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "optional": true, + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "optional": true, + "peer": true + }, + "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==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "optional": true, + "peer": true + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "optional": true, + "peer": true + }, + "node_modules/compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "optional": true + }, + "node_modules/component-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/component-type/-/component-type-1.2.2.tgz", + "integrity": "sha512-99VUHREHiN5cLeHm3YLq312p6v+HUEcwtLCAtelvUDI6+SH5g5Cr85oNR2S1o6ywzL0ykMbuwLzM2ANocjEOIA==", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "peer": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "optional": true, + "peer": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true + }, + "node_modules/compromise": { + "version": "13.11.4", + "resolved": "https://registry.npmjs.org/compromise/-/compromise-13.11.4.tgz", + "integrity": "sha512-nBITcNdqIHSVDDluaG6guyFFCSNXN+Hu87fU8VlhkE5Z0PwTZN1nro2O7a8JcUH88nB5EOzrxd9zKfXLSNFqcg==", + "dependencies": { + "efrt-unpack": "2.2.0" + }, + "engines": { + "node": ">=8.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==" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "optional": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "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-disposition/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/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz", + "integrity": "sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==", + "optional": true, + "peer": true, + "dependencies": { + "browserslist": "^4.21.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "optional": true, + "peer": true + }, + "node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "optional": true, + "peer": true, + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "optional": true, + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/cosmiconfig/node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "optional": true, + "peer": true, + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "optional": true, + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/cosmiconfig/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "optional": true, + "peer": true, + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "optional": true, + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dag-map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dag-map/-/dag-map-1.0.2.tgz", + "integrity": "sha512-+LSAiGFwQ9dRnRdOeaj7g47ZFJcOUPukAP8J3A3fuZ1g9Y44BG+P1sgApjLXTQPOzC4+7S9Wr8kXsfpINM4jpw==", + "optional": true, + "peer": true + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", + "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==", + "optional": true, + "peer": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "optional": true, + "peer": true, + "dependencies": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "optional": true, + "peer": true, + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/denodeify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", + "integrity": "sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg==", + "optional": true, + "peer": true + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/deprecated-react-native-prop-types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-4.1.0.tgz", + "integrity": "sha512-WfepZHmRbbdTvhcolb8aOKEvQdcmTMn5tKLbqbXmkBvjFjRVWAYqsXk/DBsV8TZxws8SdGHLuHaJrHSQUPRdfw==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native/normalize-colors": "*", + "invariant": "*", + "prop-types": "*" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "optional": true, + "peer": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/discord-api-types": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.33.5.tgz", + "integrity": "sha512-dvO5M52v7m7Dy96+XUnzXNsQ/0npsYpU6dL205kAtEDueswoz3aU3bh1UMoK4cQmcGtB1YRyLKqp+DXi05lzFg==" + }, + "node_modules/discord-logs": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/discord-logs/-/discord-logs-2.2.1.tgz", + "integrity": "sha512-VTNe/uRcfdLDLBLf1Taaj3OYU1GLWTAVEcCPC/xZqZd1X4D3DXW1qYJWxoyx3yqiJZ4rwQ3A0bPIFryIdniKrQ==", + "dependencies": { + "@types/node": "^18.7.11", + "@types/ws": "^8.5.3" + } + }, + "node_modules/discord-logs/node_modules/@types/node": { + "version": "18.17.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.16.tgz", + "integrity": "sha512-e0zgs7qe1XH/X3KEPnldfkD07LH9O1B9T31U8qoO7lqGSjj3/IrBuvqMeJ1aYejXRK3KOphIUDw6pLIplEW17A==" + }, + "node_modules/discord.js": { + "version": "13.17.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.17.1.tgz", + "integrity": "sha512-h13kUf+7ZaP5ZWggzooCxFutvJJvugcAO54oTEIdVr3zQWi0Sf/61S1kETtuY9nVAyYebXR/Ey4C+oWbsgEkew==", + "dependencies": { + "@discordjs/builders": "^0.16.0", + "@discordjs/collection": "^0.7.0", + "@sapphire/async-queue": "^1.5.0", + "@types/node-fetch": "^2.6.3", + "@types/ws": "^8.5.4", + "discord-api-types": "^0.33.5", + "form-data": "^4.0.0", + "node-fetch": "^2.6.7", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=16.6.0", + "npm": ">=7.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.6.tgz", + "integrity": "sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==", + "optional": true, + "peer": true, + "dependencies": { + "dotenv": "^16.4.4" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==" + }, + "node_modules/efrt-unpack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/efrt-unpack/-/efrt-unpack-2.2.0.tgz", + "integrity": "sha512-9xUSSj7qcUxz+0r4X3+bwUNttEfGfK5AH+LVa1aTpqdAfrN5VhROYCfcF+up4hp5OL7IUKcZJJrzAGipQRDoiQ==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "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==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "peer": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-editor": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", + "integrity": "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "optional": true, + "peer": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "optional": true, + "peer": true + }, + "node_modules/erlpack": { + "version": "0.1.3", + "resolved": "git+ssh://git@github.com/discord/erlpack.git#cbe76be04c2210fc9cb6ff95910f0937c1011d04", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.15.0" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "devOptional": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "optional": true, + "peer": true, + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/errorhandler": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz", + "integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==", + "optional": true, + "peer": true, + "dependencies": { + "accepts": "~1.3.7", + "escape-html": "~1.0.3" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==" + }, + "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==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "devOptional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "optional": true, + "peer": true, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/exec-async": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz", + "integrity": "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==", + "optional": true, + "peer": true + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "optional": true, + "peer": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "optional": true, + "peer": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "optional": true, + "peer": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "optional": true, + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expo": { + "version": "51.0.2", + "resolved": "https://registry.npmjs.org/expo/-/expo-51.0.2.tgz", + "integrity": "sha512-aRKrheMMQBcNDg2SBjW5kcSN5G58bdIpsxeSQ65Bx18DFLXjPv5UaU9kzIWRAcxaPtgictn9ut9IJQVZKChNxQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.20.0", + "@expo/cli": "0.18.10", + "@expo/config": "9.0.1", + "@expo/config-plugins": "8.0.4", + "@expo/metro-config": "0.18.3", + "@expo/vector-icons": "^14.0.0", + "babel-preset-expo": "~11.0.5", + "expo-asset": "~10.0.6", + "expo-file-system": "~17.0.1", + "expo-font": "~12.0.4", + "expo-keep-awake": "~13.0.1", + "expo-modules-autolinking": "1.11.1", + "expo-modules-core": "1.12.10", + "fbemitter": "^3.0.0", + "whatwg-url-without-unicode": "8.0.0-3" + }, + "bin": { + "expo": "bin/cli" + } + }, + "node_modules/expo-asset": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-10.0.6.tgz", + "integrity": "sha512-waP73/ccn/HZNNcGM4/s3X3icKjSSbEQ9mwc6tX34oYNg+XE5WdwOuZ9wgVVFrU7wZMitq22lQXd2/O0db8bxg==", + "optional": true, + "peer": true, + "dependencies": { + "@react-native/assets-registry": "~0.74.83", + "expo-constants": "~16.0.0", + "invariant": "^2.2.4", + "md5-file": "^3.2.3" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-asset/node_modules/@react-native/assets-registry": { + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.74.83.tgz", + "integrity": "sha512-2vkLMVnp+YTZYTNSDIBZojSsjz8sl5PscP3j4GcV6idD8V978SZfwFlk8K0ti0BzRs11mzL0Pj17km597S/eTQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/expo-constants": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-16.0.1.tgz", + "integrity": "sha512-s6aTHtglp926EsugWtxN7KnpSsE9FCEjb7CgEjQQ78Gpu4btj4wB+IXot2tlqNwqv+x7xFe5veoPGfJDGF/kVg==", + "optional": true, + "peer": true, + "dependencies": { + "@expo/config": "~9.0.0-beta.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-file-system": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-17.0.1.tgz", + "integrity": "sha512-dYpnZJqTGj6HCYJyXAgpFkQWsiCH3HY1ek2cFZVHFoEc5tLz9gmdEgTF6nFHurvmvfmXqxi7a5CXyVm0aFYJBw==", + "optional": true, + "peer": true, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-font": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-12.0.4.tgz", + "integrity": "sha512-VtOQB7MEeFMVwo46/9/ntqzrgraTE7gAsnfi2NukFcCpDmyAU3G1R7m287LUXltE46SmGkMgAvM6+fflXFjaJA==", + "optional": true, + "peer": true, + "dependencies": { + "fontfaceobserver": "^2.1.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-keep-awake": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-13.0.1.tgz", + "integrity": "sha512-Kqv8Bf1f5Jp7YMUgTTyKR9GatgHJuAcC8vVWDEkgVhB3O7L3pgBy5MMSMUhkTmRRV6L8TZe/rDmjiBoVS/soFA==", + "optional": true, + "peer": true, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-modules-autolinking": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-0.0.3.tgz", + "integrity": "sha512-azkCRYj/DxbK4udDuDxA9beYzQTwpJ5a9QA0bBgha2jHtWdFGF4ZZWSY+zNA5mtU3KqzYt8jWHfoqgSvKyu1Aw==", + "optional": true, + "dependencies": { + "chalk": "^4.1.0", + "commander": "^7.2.0", + "fast-glob": "^3.2.5", + "find-up": "~5.0.0", + "fs-extra": "^9.1.0" + }, + "bin": { + "expo-modules-autolinking": "bin/expo-modules-autolinking.js" + } + }, + "node_modules/expo-modules-autolinking/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "optional": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/expo-modules-core": { + "version": "1.12.10", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.12.10.tgz", + "integrity": "sha512-aS4imfr7fuUtcx+j/CHuG6ohNSThyCzGRh1kKjQTDcO0/CqDO2cSFnxf7n2vpiRFgyoMFJvFFtW/zIzVXiC2Tw==", + "optional": true, + "peer": true, + "dependencies": { + "invariant": "^2.2.4" + } + }, + "node_modules/expo-random": { + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/expo-random/-/expo-random-13.4.0.tgz", + "integrity": "sha512-Z/Bbd+1MbkK8/4ukspgA3oMlcu0q3YTCu//7q2xHwy35huN6WCv4/Uw2OGyCiOQjAbU02zwq6swA+VgVmJRCEw==", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo/node_modules/expo-modules-autolinking": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-1.11.1.tgz", + "integrity": "sha512-2dy3lTz76adOl7QUvbreMCrXyzUiF8lygI7iFJLjgIQIVH+43KnFWE5zBumpPbkiaq0f0uaFpN9U0RGQbnKiMw==", + "optional": true, + "peer": true, + "dependencies": { + "chalk": "^4.1.0", + "commander": "^7.2.0", + "fast-glob": "^3.2.5", + "find-up": "^5.0.0", + "fs-extra": "^9.1.0" + }, + "bin": { + "expo-modules-autolinking": "bin/expo-modules-autolinking.js" + } + }, + "node_modules/expo/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "optional": true, + "peer": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/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/express/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/express/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/express/node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express/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/express/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "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==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "optional": true, + "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==", + "optional": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.7.tgz", + "integrity": "sha512-J8r6BriSLO1uj2miOk1NW0YVm8AGOOu3Si2HQp/cSmo6EA4m3fcwu2WKjJ4RK9wMLBtg69y1kS8baDiQBR41Ig==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "optional": true, + "peer": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "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==", + "optional": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "devOptional": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fbemitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz", + "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==", + "optional": true, + "peer": true, + "dependencies": { + "fbjs": "^3.0.0" + } + }, + "node_modules/fbjs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", + "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", + "optional": true, + "peer": true, + "dependencies": { + "cross-fetch": "^3.1.5", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^1.0.35" + } + }, + "node_modules/fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", + "optional": true, + "peer": true + }, + "node_modules/fetch-retry": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-4.1.1.tgz", + "integrity": "sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA==", + "optional": true, + "peer": true + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/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==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, + "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==", + "devOptional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "optional": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "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==", + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "optional": true, + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "optional": true, + "peer": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "optional": true, + "peer": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver" + } + }, + "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==", + "optional": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "optional": true, + "peer": true, + "dependencies": { + "micromatch": "^4.0.2" + } + }, + "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==" + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.5.tgz", + "integrity": "sha512-PSZF9ZuaZD03sT9YaIs0FrGJ7lSUw7rHZIex+73UYVXg46eL/wxN5PaVcPJFudE2cJu5f0fezitV5aBkLHPUOQ==", + "optional": true, + "peer": true + }, + "node_modules/flow-parser": { + "version": "0.206.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.206.0.tgz", + "integrity": "sha512-HVzoK3r6Vsg+lKvlIZzaWNBVai+FXTX1wdYhz/wVlH13tb/gOdLXmlTqy6odmTBhT5UoWUbq0k8263Qhr9d88w==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fontfaceobserver": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", + "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", + "optional": true, + "peer": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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/freeport-async": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", + "integrity": "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "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==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "devOptional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "optional": true, + "peer": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getenv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz", + "integrity": "sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "optional": true, + "peer": true, + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphql": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", + "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.12.0.tgz", + "integrity": "sha512-+e8xR6SCen0wyAKrMT3UD0ZCCLymKhRgjEB5sS28rKiFir/fXgLoeRilRUssFCILmGHb+OvHDUlhxs0+IEyvQw==", + "optional": true, + "peer": true + }, + "node_modules/hermes-parser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.12.0.tgz", + "integrity": "sha512-d4PHnwq6SnDLhYl3LHNHvOg7nQ6rcI7QVil418REYksv0Mh3cEkHDcuhGxNQ3vgnLSLl4QSvDrFCwQNYdpWlzw==", + "optional": true, + "peer": true, + "dependencies": { + "hermes-estree": "0.12.0" + } + }, + "node_modules/hermes-profile-transformer": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/hermes-profile-transformer/-/hermes-profile-transformer-0.0.6.tgz", + "integrity": "sha512-cnN7bQUm65UWOy6cbGcCcZ3rpwW8Q/j4OP5aWRhEry4Z2t2aR1cjrbp0BS+KiBN0smvP1caBgAuxutvyvJILzQ==", + "optional": true, + "peer": true, + "dependencies": { + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/hosted-git-info": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", + "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", + "optional": true, + "peer": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/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==", + "optional": true, + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true, + "peer": true + }, + "node_modules/html-entities": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "optional": true + }, + "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==", + "optional": true, + "peer": true, + "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/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "devOptional": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "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==", + "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" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", + "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", + "optional": true, + "peer": true, + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "devOptional": true, + "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==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ] + }, + "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==", + "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==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "optional": true, + "peer": true + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "optional": true, + "peer": true, + "dependencies": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "optional": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", + "optional": true, + "peer": true + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "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==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "devOptional": true + }, + "node_modules/is-arrow-function": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-arrow-function/-/is-arrow-function-2.0.3.tgz", + "integrity": "sha512-iDStzcT1FJMzx+TjCOK//uDugSe/Mif/8a+T0htydQ3qkJGvSweTZpVYz4hpJH0baloSPiAFQdA8WslAgJphvQ==", + "dependencies": { + "is-callable": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true, + "peer": true + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "devOptional": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "optional": true, + "peer": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-equal": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/is-equal/-/is-equal-1.6.4.tgz", + "integrity": "sha512-NiPOTBb5ahmIOYkJ7mVTvvB1bydnTzixvfO+59AjJKBpyjPBIULL3EHGxySyZijlVpewveJyhiLQThcivkkAtw==", + "dependencies": { + "es-get-iterator": "^1.1.2", + "functions-have-names": "^1.2.2", + "has": "^1.0.3", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "is-arrow-function": "^2.0.3", + "is-bigint": "^1.0.4", + "is-boolean-object": "^1.1.2", + "is-callable": "^1.2.4", + "is-date-object": "^1.0.5", + "is-generator-function": "^1.0.10", + "is-number-object": "^1.0.6", + "is-regex": "^1.1.4", + "is-string": "^1.0.7", + "is-symbol": "^1.0.4", + "isarray": "^2.0.5", + "object-inspect": "^1.12.0", + "object.entries": "^1.1.5", + "object.getprototypeof": "^1.0.3", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-invalid-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", + "integrity": "sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==", + "optional": true, + "peer": true, + "dependencies": { + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-invalid-path/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-invalid-path/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "optional": true, + "peer": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "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==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "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==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "optional": true, + "peer": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-valid-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", + "integrity": "sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==", + "optional": true, + "peer": true, + "dependencies": { + "is-invalid-path": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "optional": true, + "peer": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isomorphic-webcrypto": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/isomorphic-webcrypto/-/isomorphic-webcrypto-2.3.8.tgz", + "integrity": "sha512-XddQSI0WYlSCjxtm1AI8kWQOulf7hAN3k3DclF1sxDJZqOe0pcsOt675zvWW91cZH9hYs3nlA3Ev8QK5i80SxQ==", + "dependencies": { + "@peculiar/webcrypto": "^1.0.22", + "asmcrypto.js": "^0.22.0", + "b64-lite": "^1.3.1", + "b64u-lite": "^1.0.1", + "msrcrypto": "^1.5.6", + "str2buf": "^1.3.0", + "webcrypto-shim": "^0.1.4" + }, + "optionalDependencies": { + "@unimodules/core": "*", + "@unimodules/react-native-adapter": "*", + "expo-random": "*", + "react-native-securerandom": "^0.1.1" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterate-iterator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.2.tgz", + "integrity": "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", + "dependencies": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "devOptional": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "devOptional": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "devOptional": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-haste-map/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "devOptional": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "devOptional": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/jest-message-util/node_modules/@babel/code-frame/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-message-util/node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-message-util/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "devOptional": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/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==", + "devOptional": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/jest-message-util/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==", + "devOptional": true + }, + "node_modules/jest-message-util/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==", + "devOptional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "devOptional": true + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "devOptional": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "devOptional": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock/node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "devOptional": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", + "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", + "optional": true, + "peer": true, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-runtime/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "devOptional": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "devOptional": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "devOptional": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "devOptional": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "devOptional": true + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "optional": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jimp-compact": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/jimp-compact/-/jimp-compact-0.16.1.tgz", + "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==", + "optional": true, + "peer": true + }, + "node_modules/joi": { + "version": "17.10.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.10.1.tgz", + "integrity": "sha512-vIiDxQKmRidUVp8KngT8MZSOcmRVm2zV7jbMjNYWuHcJWI0bUck3nRTGQjhpPlQenIQIBC5Vp9AhcnHbWQqafw==", + "optional": true, + "peer": true, + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/join-component": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz", + "integrity": "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==", + "optional": true, + "peer": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "devOptional": true + }, + "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==", + "optional": true, + "peer": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsc-android": { + "version": "250231.0.0", + "resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-250231.0.0.tgz", + "integrity": "sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw==", + "optional": true, + "peer": true + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", + "optional": true, + "peer": true + }, + "node_modules/jscodeshift": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.14.0.tgz", + "integrity": "sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.13.16", + "@babel/parser": "^7.13.16", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-transform-modules-commonjs": "^7.13.8", + "@babel/preset-flow": "^7.13.13", + "@babel/preset-typescript": "^7.13.0", + "@babel/register": "^7.13.16", + "babel-core": "^7.0.0-bridge.0", + "chalk": "^4.1.2", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.4", + "neo-async": "^2.5.0", + "node-dir": "^0.1.17", + "recast": "^0.21.0", + "temp": "^0.8.4", + "write-file-atomic": "^2.3.0" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "devOptional": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "optional": true, + "peer": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-deref-sync": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/json-schema-deref-sync/-/json-schema-deref-sync-0.13.0.tgz", + "integrity": "sha512-YBOEogm5w9Op337yb6pAT6ZXDqlxAsQCanM3grid8lMWNxRJO/zWEJi3ZzqDL8boWfwhTFym5EFrNgWwpqcBRg==", + "optional": true, + "peer": true, + "dependencies": { + "clone": "^2.1.2", + "dag-map": "~1.0.0", + "is-valid-path": "^0.1.1", + "lodash": "^4.17.13", + "md5": "~2.2.0", + "memory-cache": "~0.2.0", + "traverse": "~0.6.6", + "valid-url": "~1.0.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/json-schema-deref-sync/node_modules/md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha512-PlGG4z5mBANDGCKsYQe0CaUYHdZYZt8ZPZLmEt+Urf0W4GlpTX4HescwHU+dc9+Z/G/vZKYZYFrwgm9VxK6QOQ==", + "optional": true, + "peer": true, + "dependencies": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "devOptional": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "optional": true, + "peer": true, + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true + }, + "node_modules/lightningcss": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.19.0.tgz", + "integrity": "sha512-yV5UR7og+Og7lQC+70DA7a8ta1uiOPnWPJfxa0wnxylev5qfo4P+4iMpzWAdYWOca4jdNQZii+bDL/l+4hUXIA==", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.19.0", + "lightningcss-darwin-x64": "1.19.0", + "lightningcss-linux-arm-gnueabihf": "1.19.0", + "lightningcss-linux-arm64-gnu": "1.19.0", + "lightningcss-linux-arm64-musl": "1.19.0", + "lightningcss-linux-x64-gnu": "1.19.0", + "lightningcss-linux-x64-musl": "1.19.0", + "lightningcss-win32-x64-msvc": "1.19.0" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.19.0.tgz", + "integrity": "sha512-wIJmFtYX0rXHsXHSr4+sC5clwblEMji7HHQ4Ub1/CznVRxtCFha6JIt5JZaNf8vQrfdZnBxLLC6R8pC818jXqg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.19.0.tgz", + "integrity": "sha512-Lif1wD6P4poaw9c/4Uh2z+gmrWhw/HtXFoeZ3bEsv6Ia4tt8rOJBdkfVaUJ6VXmpKHALve+iTyP2+50xY1wKPw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.19.0.tgz", + "integrity": "sha512-P15VXY5682mTXaiDtbnLYQflc8BYb774j2R84FgDLJTN6Qp0ZjWEFyN1SPqyfTj2B2TFjRHRUvQSSZ7qN4Weig==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.19.0.tgz", + "integrity": "sha512-zwXRjWqpev8wqO0sv0M1aM1PpjHz6RVIsBcxKszIG83Befuh4yNysjgHVplF9RTU7eozGe3Ts7r6we1+Qkqsww==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.19.0.tgz", + "integrity": "sha512-vSCKO7SDnZaFN9zEloKSZM5/kC5gbzUjoJQ43BvUpyTFUX7ACs/mDfl2Eq6fdz2+uWhUh7vf92c4EaaP4udEtA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.19.0.tgz", + "integrity": "sha512-0AFQKvVzXf9byrXUq9z0anMGLdZJS+XSDqidyijI5njIwj6MdbvX2UZK/c4FfNmeRa2N/8ngTffoIuOUit5eIQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.19.0.tgz", + "integrity": "sha512-SJoM8CLPt6ECCgSuWe+g0qo8dqQYVcPiW2s19dxkmSI5+Uu1GIRzyKA0b7QqmEXolA+oSJhQqCmJpzjY4CuZAg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.19.0.tgz", + "integrity": "sha512-C+VuUTeSUOAaBZZOPT7Etn/agx/MatzJzGRkeV+zEABmPuntv1zihncsi+AyGmjkkzq3wVedEy7h0/4S84mUtg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "optional": true, + "peer": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "devOptional": true + }, + "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==", + "optional": true, + "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==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "optional": true, + "peer": true + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "optional": true, + "peer": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/logkitty": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", + "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-fragments": "^0.2.1", + "dayjs": "^1.8.15", + "yargs": "^15.1.0" + }, + "bin": { + "logkitty": "bin/logkitty.js" + } + }, + "node_modules/logkitty/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/logkitty/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "optional": true, + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/logkitty/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "optional": true, + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "optional": true, + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "optional": true, + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logkitty/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "optional": true, + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "optional": true, + "peer": true + }, + "node_modules/logkitty/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "optional": true, + "peer": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "optional": true, + "peer": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "optional": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "devOptional": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/luxon": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", + "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/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==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "devOptional": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", + "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "optional": true, + "peer": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "optional": true, + "peer": true, + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/md5-file": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-3.2.3.tgz", + "integrity": "sha512-3Tkp1piAHaworfcCgH0jKbTvj1jWWFgbvh2cXaNCgHwyTCBxxvD1Y04rmfpvdPm1P4oXMOpm6+2H7sr7v9v8Fw==", + "optional": true, + "peer": true, + "dependencies": { + "buffer-alloc": "^1.1.0" + }, + "bin": { + "md5-file": "cli.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/md5hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/md5hex/-/md5hex-1.0.0.tgz", + "integrity": "sha512-c2YOUbp33+6thdCUi34xIyOU/a7bvGKj/3DB1iaPMTuPHf/Q2d5s4sn1FaCOO43XkXggnb08y5W2PU8UNYNLKQ==", + "optional": true, + "peer": true + }, + "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/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "optional": true, + "peer": true + }, + "node_modules/memory-cache": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", + "integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==", + "optional": true, + "peer": true + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "devOptional": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "optional": true, + "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/metro": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.76.7.tgz", + "integrity": "sha512-67ZGwDeumEPnrHI+pEDSKH2cx+C81Gx8Mn5qOtmGUPm/Up9Y4I1H2dJZ5n17MWzejNo0XAvPh0QL0CrlJEODVQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.0", + "@babel/parser": "^7.20.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.20.0", + "@babel/types": "^7.20.0", + "accepts": "^1.3.7", + "async": "^3.2.2", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "denodeify": "^1.2.1", + "error-stack-parser": "^2.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.12.0", + "image-size": "^1.0.2", + "invariant": "^2.2.4", + "jest-worker": "^27.2.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.76.7", + "metro-cache": "0.76.7", + "metro-cache-key": "0.76.7", + "metro-config": "0.76.7", + "metro-core": "0.76.7", + "metro-file-map": "0.76.7", + "metro-inspector-proxy": "0.76.7", + "metro-minify-terser": "0.76.7", + "metro-minify-uglify": "0.76.7", + "metro-react-native-babel-preset": "0.76.7", + "metro-resolver": "0.76.7", + "metro-runtime": "0.76.7", + "metro-source-map": "0.76.7", + "metro-symbolicate": "0.76.7", + "metro-transform-plugins": "0.76.7", + "metro-transform-worker": "0.76.7", + "mime-types": "^2.1.27", + "node-fetch": "^2.2.0", + "nullthrows": "^1.1.1", + "rimraf": "^3.0.2", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "strip-ansi": "^6.0.0", + "throat": "^5.0.0", + "ws": "^7.5.1", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-babel-transformer": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.76.7.tgz", + "integrity": "sha512-bgr2OFn0J4r0qoZcHrwEvccF7g9k3wdgTOgk6gmGHrtlZ1Jn3oCpklW/DfZ9PzHfjY2mQammKTc19g/EFGyOJw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "hermes-parser": "0.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-cache": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.76.7.tgz", + "integrity": "sha512-nWBMztrs5RuSxZRI7hgFgob5PhYDmxICh9FF8anm9/ito0u0vpPvRxt7sRu8fyeD2AHdXqE7kX32rWY0LiXgeg==", + "optional": true, + "peer": true, + "dependencies": { + "metro-core": "0.76.7", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-cache-key": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.76.7.tgz", + "integrity": "sha512-0pecoIzwsD/Whn/Qfa+SDMX2YyasV0ndbcgUFx7w1Ct2sLHClujdhQ4ik6mvQmsaOcnGkIyN0zcceMDjC2+BFQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-config": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.76.7.tgz", + "integrity": "sha512-CFDyNb9bqxZemiChC/gNdXZ7OQkIwmXzkrEXivcXGbgzlt/b2juCv555GWJHyZSlorwnwJfY3uzAFu4A9iRVfg==", + "optional": true, + "peer": true, + "dependencies": { + "connect": "^3.6.5", + "cosmiconfig": "^5.0.5", + "jest-validate": "^29.2.1", + "metro": "0.76.7", + "metro-cache": "0.76.7", + "metro-core": "0.76.7", + "metro-runtime": "0.76.7" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-config/node_modules/metro-runtime": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.76.7.tgz", + "integrity": "sha512-MuWHubQHymUWBpZLwuKZQgA/qbb35WnDAKPo83rk7JRLIFPvzXSvFaC18voPuzJBt1V98lKQIonh6MiC9gd8Ug==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "react-refresh": "^0.4.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-core": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.76.7.tgz", + "integrity": "sha512-0b8KfrwPmwCMW+1V7ZQPkTy2tsEKZjYG9Pu1PTsu463Z9fxX7WaR0fcHFshv+J1CnQSUTwIGGjbNvj1teKe+pw==", + "optional": true, + "peer": true, + "dependencies": { + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.76.7" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-file-map": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.76.7.tgz", + "integrity": "sha512-s+zEkTcJ4mOJTgEE2ht4jIo1DZfeWreQR3tpT3gDV/Y/0UQ8aJBTv62dE775z0GLsWZApiblAYZsj7ZE8P06nw==", + "optional": true, + "peer": true, + "dependencies": { + "anymatch": "^3.0.3", + "debug": "^2.2.0", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-regex-util": "^27.0.6", + "jest-util": "^27.2.0", + "jest-worker": "^27.2.0", + "micromatch": "^4.0.4", + "node-abort-controller": "^3.1.1", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/metro-file-map/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "optional": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/metro-file-map/node_modules/@types/yargs": { + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", + "integrity": "sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==", + "optional": true, + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/metro-file-map/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/metro-file-map/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "optional": true, + "peer": true, + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/metro-file-map/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true + }, + "node_modules/metro-inspector-proxy": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-inspector-proxy/-/metro-inspector-proxy-0.76.7.tgz", + "integrity": "sha512-rNZ/6edTl/1qUekAhAbaFjczMphM50/UjtxiKulo6vqvgn/Mjd9hVqDvVYfAMZXqPvlusD88n38UjVYPkruLSg==", + "optional": true, + "peer": true, + "dependencies": { + "connect": "^3.6.5", + "debug": "^2.2.0", + "node-fetch": "^2.2.0", + "ws": "^7.5.1", + "yargs": "^17.6.2" + }, + "bin": { + "metro-inspector-proxy": "src/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-inspector-proxy/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/metro-inspector-proxy/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true + }, + "node_modules/metro-inspector-proxy/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/metro-minify-terser": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.76.7.tgz", + "integrity": "sha512-FQiZGhIxCzhDwK4LxyPMLlq0Tsmla10X7BfNGlYFK0A5IsaVKNJbETyTzhpIwc+YFRT4GkFFwgo0V2N5vxO5HA==", + "optional": true, + "peer": true, + "dependencies": { + "terser": "^5.15.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-minify-uglify": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-minify-uglify/-/metro-minify-uglify-0.76.7.tgz", + "integrity": "sha512-FuXIU3j2uNcSvQtPrAJjYWHruPiQ+EpE++J9Z+VznQKEHcIxMMoQZAfIF2IpZSrZYfLOjVFyGMvj41jQMxV1Vw==", + "optional": true, + "peer": true, + "dependencies": { + "uglify-es": "^3.1.9" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-react-native-babel-transformer": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.76.7.tgz", + "integrity": "sha512-W6lW3J7y/05ph3c2p3KKJNhH0IdyxdOCbQ5it7aM2MAl0SM4wgKjaV6EYv9b3rHklpV6K3qMH37UKVcjMooWiA==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "babel-preset-fbjs": "^3.4.0", + "hermes-parser": "0.12.0", + "metro-react-native-babel-preset": "0.76.7", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/metro-react-native-babel-transformer/node_modules/metro-react-native-babel-preset": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.76.7.tgz", + "integrity": "sha512-R25wq+VOSorAK3hc07NW0SmN8z9S/IR0Us0oGAsBcMZnsgkbOxu77Mduqf+f4is/wnWHc5+9bfiqdLnaMngiVw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/plugin-proposal-async-generator-functions": "^7.0.0", + "@babel/plugin-proposal-class-properties": "^7.18.0", + "@babel/plugin-proposal-export-default-from": "^7.0.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", + "@babel/plugin-proposal-numeric-separator": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.20.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-default-from": "^7.0.0", + "@babel/plugin-syntax-flow": "^7.18.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", + "@babel/plugin-syntax-optional-chaining": "^7.0.0", + "@babel/plugin-transform-arrow-functions": "^7.0.0", + "@babel/plugin-transform-async-to-generator": "^7.20.0", + "@babel/plugin-transform-block-scoping": "^7.0.0", + "@babel/plugin-transform-classes": "^7.0.0", + "@babel/plugin-transform-computed-properties": "^7.0.0", + "@babel/plugin-transform-destructuring": "^7.20.0", + "@babel/plugin-transform-flow-strip-types": "^7.20.0", + "@babel/plugin-transform-function-name": "^7.0.0", + "@babel/plugin-transform-literals": "^7.0.0", + "@babel/plugin-transform-modules-commonjs": "^7.0.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", + "@babel/plugin-transform-parameters": "^7.0.0", + "@babel/plugin-transform-react-display-name": "^7.0.0", + "@babel/plugin-transform-react-jsx": "^7.0.0", + "@babel/plugin-transform-react-jsx-self": "^7.0.0", + "@babel/plugin-transform-react-jsx-source": "^7.0.0", + "@babel/plugin-transform-runtime": "^7.0.0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0", + "@babel/plugin-transform-spread": "^7.0.0", + "@babel/plugin-transform-sticky-regex": "^7.0.0", + "@babel/plugin-transform-typescript": "^7.5.0", + "@babel/plugin-transform-unicode-regex": "^7.0.0", + "@babel/template": "^7.0.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.4.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/metro-resolver": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.76.7.tgz", + "integrity": "sha512-pC0Wgq29HHIHrwz23xxiNgylhI8Rq1V01kQaJ9Kz11zWrIdlrH0ZdnJ7GC6qA0ErROG+cXmJ0rJb8/SW1Zp2IA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-runtime": { + "version": "0.76.8", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.76.8.tgz", + "integrity": "sha512-XKahvB+iuYJSCr3QqCpROli4B4zASAYpkK+j3a0CJmokxCDNbgyI4Fp88uIL6rNaZfN0Mv35S0b99SdFXIfHjg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "react-refresh": "^0.4.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-source-map": { + "version": "0.76.8", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.76.8.tgz", + "integrity": "sha512-Hh0ncPsHPVf6wXQSqJqB3K9Zbudht4aUtNpNXYXSxH+pteWqGAXnjtPsRAnCsCWl38wL0jYF0rJDdMajUI3BDw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.20.0", + "@babel/types": "^7.20.0", + "invariant": "^2.2.4", + "metro-symbolicate": "0.76.8", + "nullthrows": "^1.1.1", + "ob1": "0.76.8", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-source-map/node_modules/metro-symbolicate": { + "version": "0.76.8", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.76.8.tgz", + "integrity": "sha512-LrRL3uy2VkzrIXVlxoPtqb40J6Bf1mlPNmUQewipc3qfKKFgtPHBackqDy1YL0njDsWopCKcfGtFYLn0PTUn3w==", + "optional": true, + "peer": true, + "dependencies": { + "invariant": "^2.2.4", + "metro-source-map": "0.76.8", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "through2": "^2.0.1", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-source-map/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.76.7.tgz", + "integrity": "sha512-p0zWEME5qLSL1bJb93iq+zt5fz3sfVn9xFYzca1TJIpY5MommEaS64Va87lp56O0sfEIvh4307Oaf/ZzRjuLiQ==", + "optional": true, + "peer": true, + "dependencies": { + "invariant": "^2.2.4", + "metro-source-map": "0.76.7", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "through2": "^2.0.1", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-symbolicate/node_modules/metro-source-map": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.76.7.tgz", + "integrity": "sha512-Prhx7PeRV1LuogT0Kn5VjCuFu9fVD68eefntdWabrksmNY6mXK8pRqzvNJOhTojh6nek+RxBzZeD6MIOOyXS6w==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.20.0", + "@babel/types": "^7.20.0", + "invariant": "^2.2.4", + "metro-symbolicate": "0.76.7", + "nullthrows": "^1.1.1", + "ob1": "0.76.7", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-symbolicate/node_modules/ob1": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.76.7.tgz", + "integrity": "sha512-BQdRtxxoUNfSoZxqeBGOyuT9nEYSn18xZHwGMb0mMVpn2NBcYbnyKY4BK2LIHRgw33CBGlUmE+KMaNvyTpLLtQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-symbolicate/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.76.7.tgz", + "integrity": "sha512-iSmnjVApbdivjuzb88Orb0JHvcEt5veVyFAzxiS5h0QB+zV79w6JCSqZlHCrbNOkOKBED//LqtKbFVakxllnNg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.20.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.76.7.tgz", + "integrity": "sha512-cGvELqFMVk9XTC15CMVzrCzcO6sO1lURfcbgjuuPdzaWuD11eEyocvkTX0DPiRjsvgAmicz4XYxVzgYl3MykDw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.0", + "@babel/parser": "^7.20.0", + "@babel/types": "^7.20.0", + "babel-preset-fbjs": "^3.4.0", + "metro": "0.76.7", + "metro-babel-transformer": "0.76.7", + "metro-cache": "0.76.7", + "metro-cache-key": "0.76.7", + "metro-source-map": "0.76.7", + "metro-transform-plugins": "0.76.7", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-transform-worker/node_modules/metro-source-map": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.76.7.tgz", + "integrity": "sha512-Prhx7PeRV1LuogT0Kn5VjCuFu9fVD68eefntdWabrksmNY6mXK8pRqzvNJOhTojh6nek+RxBzZeD6MIOOyXS6w==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.20.0", + "@babel/types": "^7.20.0", + "invariant": "^2.2.4", + "metro-symbolicate": "0.76.7", + "nullthrows": "^1.1.1", + "ob1": "0.76.7", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-transform-worker/node_modules/ob1": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.76.7.tgz", + "integrity": "sha512-BQdRtxxoUNfSoZxqeBGOyuT9nEYSn18xZHwGMb0mMVpn2NBcYbnyKY4BK2LIHRgw33CBGlUmE+KMaNvyTpLLtQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro-transform-worker/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "optional": true, + "peer": true + }, + "node_modules/metro/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/metro/node_modules/metro-react-native-babel-preset": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.76.7.tgz", + "integrity": "sha512-R25wq+VOSorAK3hc07NW0SmN8z9S/IR0Us0oGAsBcMZnsgkbOxu77Mduqf+f4is/wnWHc5+9bfiqdLnaMngiVw==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/plugin-proposal-async-generator-functions": "^7.0.0", + "@babel/plugin-proposal-class-properties": "^7.18.0", + "@babel/plugin-proposal-export-default-from": "^7.0.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", + "@babel/plugin-proposal-numeric-separator": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.20.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-default-from": "^7.0.0", + "@babel/plugin-syntax-flow": "^7.18.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", + "@babel/plugin-syntax-optional-chaining": "^7.0.0", + "@babel/plugin-transform-arrow-functions": "^7.0.0", + "@babel/plugin-transform-async-to-generator": "^7.20.0", + "@babel/plugin-transform-block-scoping": "^7.0.0", + "@babel/plugin-transform-classes": "^7.0.0", + "@babel/plugin-transform-computed-properties": "^7.0.0", + "@babel/plugin-transform-destructuring": "^7.20.0", + "@babel/plugin-transform-flow-strip-types": "^7.20.0", + "@babel/plugin-transform-function-name": "^7.0.0", + "@babel/plugin-transform-literals": "^7.0.0", + "@babel/plugin-transform-modules-commonjs": "^7.0.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", + "@babel/plugin-transform-parameters": "^7.0.0", + "@babel/plugin-transform-react-display-name": "^7.0.0", + "@babel/plugin-transform-react-jsx": "^7.0.0", + "@babel/plugin-transform-react-jsx-self": "^7.0.0", + "@babel/plugin-transform-react-jsx-source": "^7.0.0", + "@babel/plugin-transform-runtime": "^7.0.0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0", + "@babel/plugin-transform-spread": "^7.0.0", + "@babel/plugin-transform-sticky-regex": "^7.0.0", + "@babel/plugin-transform-typescript": "^7.5.0", + "@babel/plugin-transform-unicode-regex": "^7.0.0", + "@babel/template": "^7.0.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.4.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/metro/node_modules/metro-runtime": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.76.7.tgz", + "integrity": "sha512-MuWHubQHymUWBpZLwuKZQgA/qbb35WnDAKPo83rk7JRLIFPvzXSvFaC18voPuzJBt1V98lKQIonh6MiC9gd8Ug==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "react-refresh": "^0.4.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro/node_modules/metro-source-map": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.76.7.tgz", + "integrity": "sha512-Prhx7PeRV1LuogT0Kn5VjCuFu9fVD68eefntdWabrksmNY6mXK8pRqzvNJOhTojh6nek+RxBzZeD6MIOOyXS6w==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.20.0", + "@babel/types": "^7.20.0", + "invariant": "^2.2.4", + "metro-symbolicate": "0.76.7", + "nullthrows": "^1.1.1", + "ob1": "0.76.7", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true + }, + "node_modules/metro/node_modules/ob1": { + "version": "0.76.7", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.76.7.tgz", + "integrity": "sha512-BQdRtxxoUNfSoZxqeBGOyuT9nEYSn18xZHwGMb0mMVpn2NBcYbnyKY4BK2LIHRgw33CBGlUmE+KMaNvyTpLLtQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/metro/node_modules/serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "devOptional": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "optional": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=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==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "optional": true, + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.43", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz", + "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "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==" + }, + "node_modules/msrcrypto": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/msrcrypto/-/msrcrypto-1.5.8.tgz", + "integrity": "sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q==" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", + "optional": true, + "peer": true, + "dependencies": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/mv/node_modules/glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", + "optional": true, + "peer": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mv/node_modules/rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", + "optional": true, + "peer": true, + "dependencies": { + "glob": "^6.0.1" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "optional": true, + "peer": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "optional": true, + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "optional": true, + "peer": true, + "bin": { + "ncp": "bin/ncp" + } + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "optional": true, + "peer": true + }, + "node_modules/nested-error-stacks": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", + "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", + "optional": true, + "peer": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "optional": true, + "peer": true + }, + "node_modules/nlp_compromise": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/nlp_compromise/-/nlp_compromise-4.12.1.tgz", + "integrity": "sha512-5emgBgKTEElKewFy2TB9HhBp9QYT2A3PuunB5FBblKYml6kkQ9+j1l3FEKtWN8Epscp/hwt6AycUPnoYVjpkEQ==", + "deprecated": "nlp_compromise has been renamed to 'compromise', please update" + }, + "node_modules/nlp-syllables": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/nlp-syllables/-/nlp-syllables-0.0.5.tgz", + "integrity": "sha512-nMjl3oqdXe4ZrhK6FzuPRPO7zRW704TkbpDj74ufQrCGZVCUed0vzwvhdhfN3V7LJD5e3nsxH52R6uTcoMGryA==", + "dependencies": { + "nlp_compromise": ">=4.12.0" + } + }, + "node_modules/nocache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", + "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "optional": true, + "peer": true + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" + }, + "node_modules/node-dir": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", + "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", + "optional": true, + "peer": true, + "dependencies": { + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.10.5" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", + "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "devOptional": true + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-schedule": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz", + "integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==", + "dependencies": { + "cron-parser": "^4.2.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-package-arg": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-7.0.0.tgz", + "integrity": "sha512-xXxr8y5U0kl8dVkz2oK7yZjPBvqM2fwaO5l3Yg13p03v8+E3qQcD0JNhHzjL1vyGgxcKkD0cco+NLR72iuPk3g==", + "optional": true, + "peer": true, + "dependencies": { + "hosted-git-info": "^3.0.2", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "optional": true, + "peer": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "optional": true, + "peer": true + }, + "node_modules/ob1": { + "version": "0.76.8", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.76.8.tgz", + "integrity": "sha512-dlBkJJV5M/msj9KYA9upc+nUWVwuOFFTbu28X6kZeGwcuW+JxaHSBZ70SYQnk5M+j5JbNLR6yKHmgW4M5E7X5g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.getprototypeof": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/object.getprototypeof/-/object.getprototypeof-1.0.5.tgz", + "integrity": "sha512-4G0QiXpoIppBUz5efmxTm/HTbVN2ioGjk/PbsaNvwISFX+saj8muGp6vNuzIdsosFxM4V/kpUVNvy/+9+DVBZQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "reflect.getprototypeof": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "optional": true, + "peer": true, + "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==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "optional": true, + "peer": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "optional": true, + "peer": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "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==", + "devOptional": true, + "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==", + "optional": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, + "node_modules/parse-duration": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.0.tgz", + "integrity": "sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ==" + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "optional": true, + "peer": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parse-png": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz", + "integrity": "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==", + "optional": true, + "peer": true, + "dependencies": { + "pngjs": "^3.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/password-prompt": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/password-prompt/-/password-prompt-1.1.3.tgz", + "integrity": "sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-escapes": "^4.3.2", + "cross-spawn": "^7.0.3" + } + }, + "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==", + "devOptional": true, + "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==", + "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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "devOptional": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "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==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pjson": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/pjson/-/pjson-1.0.9.tgz", + "integrity": "sha512-4hRJH3YzkUpOlShRzhyxAmThSNnAaIlWZCAb27hd0pVUAXNUAHAO7XZbsPPvsCYwBFEScTmCCL6DGE8NyZ8BdQ==" + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "optional": true, + "peer": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "optional": true, + "peer": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "optional": true, + "peer": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "optional": true, + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "optional": true, + "peer": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "optional": true, + "peer": true, + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/plist/node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/plist/node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "optional": true, + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "optional": true, + "peer": true, + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "optional": true, + "peer": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "optional": true, + "peer": true, + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "devOptional": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "optional": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "optional": true, + "peer": true + }, + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "peer": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/pvtsutils": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", + "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", + "dependencies": { + "tslib": "^2.6.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/qrcode-terminal": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz", + "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==", + "optional": true, + "peer": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "optional": true, + "peer": true, + "dependencies": { + "inherits": "~2.0.3" + } + }, + "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" + } + ], + "optional": true + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "optional": true, + "peer": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "optional": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-devtools-core": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.28.5.tgz", + "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", + "optional": true, + "peer": true, + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "optional": true, + "peer": true + }, + "node_modules/react-native": { + "version": "0.72.4", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.72.4.tgz", + "integrity": "sha512-+vrObi0wZR+NeqL09KihAAdVlQ9IdplwznJWtYrjnQ4UbCW6rkzZJebRsugwUneSOKNFaHFEo1uKU89HsgtYBg==", + "optional": true, + "peer": true, + "dependencies": { + "@jest/create-cache-key-function": "^29.2.1", + "@react-native-community/cli": "11.3.6", + "@react-native-community/cli-platform-android": "11.3.6", + "@react-native-community/cli-platform-ios": "11.3.6", + "@react-native/assets-registry": "^0.72.0", + "@react-native/codegen": "^0.72.6", + "@react-native/gradle-plugin": "^0.72.11", + "@react-native/js-polyfills": "^0.72.1", + "@react-native/normalize-colors": "^0.72.0", + "@react-native/virtualized-lists": "^0.72.8", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "base64-js": "^1.1.2", + "deprecated-react-native-prop-types": "4.1.0", + "event-target-shim": "^5.0.1", + "flow-enums-runtime": "^0.0.5", + "invariant": "^2.2.4", + "jest-environment-node": "^29.2.1", + "jsc-android": "^250231.0.0", + "memoize-one": "^5.0.0", + "metro-runtime": "0.76.8", + "metro-source-map": "0.76.8", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "pretty-format": "^26.5.2", + "promise": "^8.3.0", + "react-devtools-core": "^4.27.2", + "react-refresh": "^0.4.0", + "react-shallow-renderer": "^16.15.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.24.0-canary-efb381bbf-20230505", + "stacktrace-parser": "^0.1.10", + "use-sync-external-store": "^1.0.0", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.2", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "node_modules/react-native-securerandom": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/react-native-securerandom/-/react-native-securerandom-0.1.1.tgz", + "integrity": "sha512-CozcCx0lpBLevxiXEb86kwLRalBCHNjiGPlw3P7Fi27U6ZLdfjOCNRHD1LtBKcvPvI3TvkBXB3GOtLvqaYJLGw==", + "optional": true, + "dependencies": { + "base64-js": "*" + }, + "peerDependencies": { + "react-native": "*" + } + }, + "node_modules/react-native/node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "optional": true, + "peer": true, + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/react-native/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "optional": true, + "peer": true + }, + "node_modules/react-native/node_modules/ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "optional": true, + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", + "integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", + "optional": true, + "peer": true, + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readline": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", + "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==", + "optional": true, + "peer": true + }, + "node_modules/recast": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.21.5.tgz", + "integrity": "sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==", + "optional": true, + "peer": true, + "dependencies": { + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "optional": true, + "peer": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "optional": true, + "peer": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "optional": true, + "peer": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "optional": true, + "peer": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "optional": true, + "peer": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "optional": true, + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/remove-trailing-slash": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz", + "integrity": "sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==", + "optional": true, + "peer": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "optional": true, + "peer": true + }, + "node_modules/requireg": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/requireg/-/requireg-0.2.2.tgz", + "integrity": "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==", + "optional": true, + "peer": true, + "dependencies": { + "nested-error-stacks": "~2.0.1", + "rc": "~1.2.7", + "resolve": "~1.7.1" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/requireg/node_modules/resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "optional": true, + "peer": true, + "dependencies": { + "path-parse": "^1.0.5" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.5.tgz", + "integrity": "sha512-qWhv7PF1V95QPvRoUGHxOtnAlEvlXBylMZcjUR9pAumMmveFtcHJRXGIr+TkjfNJVQypqv2qcDiiars2y1PsSg==", + "devOptional": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "optional": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "engines": { + "node": ">=0.12.0" + } + }, + "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" + } + ], + "optional": true, + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true, + "peer": true + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "optional": true, + "peer": true + }, + "node_modules/scheduler": { + "version": "0.24.0-canary-efb381bbf-20230505", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz", + "integrity": "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==", + "optional": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "optional": true, + "peer": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "optional": true, + "peer": true, + "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==", + "optional": true, + "peer": true, + "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==", + "optional": true, + "peer": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "optional": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "optional": true, + "peer": true + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/sequelize": { + "version": "6.33.0", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.33.0.tgz", + "integrity": "sha512-GkeCbqgaIcpyZ1EyXrDNIwktbfMldHAGOVXHGM4x8bxGSRAOql5htDWofPvwpfL/FoZ59CaFmfO3Mosv1lDbQw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/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/serve-static/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/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static/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/serve-static/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/node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "optional": true, + "peer": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "optional": true, + "peer": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "devOptional": true, + "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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", + "optional": true, + "peer": true, + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, + "node_modules/simple-plist/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "optional": true, + "peer": true, + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "devOptional": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/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==", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/slice-ansi/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==", + "optional": true, + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/slice-ansi/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==", + "optional": true, + "peer": true + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "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==", + "optional": true, + "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==", + "optional": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks/node_modules/ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "optional": true + }, + "node_modules/sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "optional": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "optional": true, + "peer": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "devOptional": true + }, + "node_modules/sqlite3": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", + "integrity": "sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "node-addon-api": "^4.2.0", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "devOptional": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "optional": true, + "peer": true + }, + "node_modules/stacktrace-parser": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", + "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", + "optional": true, + "peer": true, + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stop-discord-phishing": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/stop-discord-phishing/-/stop-discord-phishing-0.3.3.tgz", + "integrity": "sha512-xl0GkusEhg4BDA20SuQiyAiaTnP+BfZVpEuAa211kAhqmmADrB9JSGeX9GNZfJNboI/CPiCSxAMaH+kSVr71Lw==", + "dependencies": { + "axios": "^0.24.0" + } + }, + "node_modules/stop-discord-phishing/node_modules/axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "dependencies": { + "follow-redirects": "^1.14.4" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/str2buf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/str2buf/-/str2buf-1.3.0.tgz", + "integrity": "sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA==" + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.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==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/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" + } + ] + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-similarity": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", + "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "ISC" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "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==", + "optional": true, + "peer": true + }, + "node_modules/structured-headers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", + "integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==", + "optional": true, + "peer": true + }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sudo-prompt": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-8.2.5.tgz", + "integrity": "sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==", + "optional": true, + "peer": true + }, + "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==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "devOptional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/temp": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", + "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", + "optional": true, + "peer": true, + "dependencies": { + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "optional": true, + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tempy": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.7.1.tgz", + "integrity": "sha512-vXPxwOyaNVi9nyczO16mxmHGpl6ASC5/TVhRRHpqeYHvKQm58EaWNvZXxAhR0lYYnBOQFjXjhzeLsaXdjxLjRg==", + "optional": true, + "peer": true, + "dependencies": { + "del": "^6.0.0", + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.19.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz", + "integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "optional": true, + "peer": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "optional": true, + "peer": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "optional": true, + "peer": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "optional": true, + "peer": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "optional": true, + "peer": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "optional": true, + "peer": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "optional": true, + "peer": true + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "optional": true, + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tldjs": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.3.2.tgz", + "integrity": "sha512-EORDwFMSZKrHPUVDhejCMDeAovRS5d8jZKiqALFiPp3cjKjEldPkxBY39ZSx3c45awz3RpKwJD1cCgGxEfy8/A==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "devOptional": true + }, + "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==", + "devOptional": true, + "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==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/traverse": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.9.tgz", + "integrity": "sha512-7bBrcF+/LQzSgFmT0X5YclVqQxtv7TDJ1f8Wj7ibBu/U6BMLeOpUxuZjV7rMc44UtKxlnMFigdhFAIszSX1DMg==", + "optional": true, + "peer": true, + "dependencies": { + "gopd": "^1.0.1", + "typedarray.prototype.slice": "^1.0.3", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "optional": true, + "peer": true + }, + "node_modules/ts-mixer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", + "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "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/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.3.tgz", + "integrity": "sha512-8WbVAQAUlENo1q3c3zZYuy5k9VzBQvp8AX9WOtbvyWlLM1v5JaSRmjubLjzHF4JFtptjH/5c/i95yaElvcjC0A==", + "optional": true, + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-errors": "^1.3.0", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-offset": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz", + "integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "optional": true, + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/uglify-es": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", + "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", + "deprecated": "support for ECMAScript is superseded by `uglify-js` as of v3.13.0", + "optional": true, + "peer": true, + "dependencies": { + "commander": "~2.13.0", + "source-map": "~0.6.1" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uglify-es/node_modules/commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", + "optional": true, + "peer": true + }, + "node_modules/uglify-es/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "optional": true, + "peer": true + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "optional": true, + "peer": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "optional": true, + "peer": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-join": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.0.tgz", + "integrity": "sha512-EGXjXJZhIHiQMK2pQukuFcL303nskqIRzWvPvV5O8miOfwoUb9G+a/Cld60kUyeaybEI94wvVClT10DtfeAExA==", + "optional": true, + "peer": true + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "optional": true, + "peer": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/utf-8-validate": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.3.tgz", + "integrity": "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "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==" + }, + "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==", + "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==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==", + "optional": true, + "peer": true + }, + "node_modules/validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==", + "optional": true, + "peer": true, + "dependencies": { + "builtins": "^1.0.3" + } + }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vlq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", + "optional": true, + "peer": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "devOptional": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webcrypto-core": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.7.tgz", + "integrity": "sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/json-schema": "^1.1.12", + "asn1js": "^3.0.1", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + } + }, + "node_modules/webcrypto-shim": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/webcrypto-shim/-/webcrypto-shim-0.1.7.tgz", + "integrity": "sha512-JAvAQR5mRNRxZW2jKigWMjCMkjSdmP5cColRP1U/pTg69VgHXEi1orv5vVpJ55Zc5MIaPc1aaurzd9pjv2bveg==" + }, + "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==" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.19", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz", + "integrity": "sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw==", + "optional": true, + "peer": true + }, + "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==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/whatwg-url-without-unicode": { + "version": "8.0.0-3", + "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", + "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", + "optional": true, + "peer": true, + "dependencies": { + "buffer": "^5.4.3", + "punycode": "^2.1.1", + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "optional": true, + "peer": true + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/wonka": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/wonka/-/wonka-4.0.15.tgz", + "integrity": "sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg==", + "optional": true, + "peer": true + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==" + }, + "node_modules/write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/ws": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", + "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", + "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/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "optional": true, + "peer": true, + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xcode/node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/xml2js": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", + "integrity": "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==", + "optional": true, + "peer": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz", + "integrity": "sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "devOptional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, + "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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zlib-sync": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/zlib-sync/-/zlib-sync-0.1.8.tgz", + "integrity": "sha512-Xbu4odT5SbLsa1HFz8X/FvMgUbJYWxJYKB2+bqxJ6UOIIPaVGrqHEB3vyXDltSA6tTqBhSGYLgiVpzPQHYi3lA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "nan": "^2.17.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..29ecfb87 --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "customdcbot", + "version": "3.8.0", + "description": "Create your own discord bot - Fully customizable and with a lot of features", + "main": "main.js", + "repository": { + "type": "git", + "url": "https://github.com/SCNetwork/CustomDCBot.git" + }, + "scripts": { + "start": "node main.js", + "generate-config": "node generate-config.js", + "generate-template": "node generate-template.js", + "test": "jest" + }, + "author": "ScootKit Team", + "contributors": [ + "SCDerox " + ], + "funding": "https://github.com/ScootKit/CustomDCBot?sponsor=1", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@androz2091/discord-invites-tracker": "1.1.1", + "@pixelfactory/privatebin": "2.6.1", + "@scderox/ikea-name-generator": "1.0.0", + "@twurple/api": "5.3.4", + "@twurple/auth": "5.3.4", + "age-calculator": "1.0.0", + "bs58": "5.0.0", + "centra": "2.6.0", + "discord-logs": "2.2.1", + "discord.js": "13.17.1", + "dotenv": "16.3.1", + "fs-extra": "11.1.1", + "html-entities": "2.4.0", + "is-equal": "1.6.4", + "isomorphic-webcrypto": "2.3.8", + "jsonfile": "6.1.0", + "log4js": "6.9.1", + "node-schedule": "2.1.1", + "parse-duration": "1.1.0", + "sequelize": "6.33.0", + "sqlite3": "5.1.6", + "stop-discord-phishing": "0.3.3", + "express": "^4.18.2", + "axios": "^1.4.0", + "url-parse": "^1.5.10", + "tldjs": "^2.3.2", + "string-similarity": "^4.0.4" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "optionalDependencies": { + "erlpack": "github:discord/erlpack", + "bufferutil": "4.0.7", + "zlib-sync": "0.1.8", + "utf-8-validate": "6.0.3" + } +} \ No newline at end of file diff --git a/quick-test.js b/quick-test.js new file mode 100644 index 00000000..709b9386 --- /dev/null +++ b/quick-test.js @@ -0,0 +1,14 @@ +const { checkPhishing, setCustomPatterns } = require('./modules/moderation/phishingService'); + +(async () => { + const urls = [ + 'http://paypal.com.example.tk/login', + 'https://example.com', + 'http://192.168.0.1', + ]; + + for (const u of urls) { + const res = await checkPhishing({ url: u }); + console.log(u, '=>', res); + } +})(); diff --git a/simulate-phish.js b/simulate-phish.js new file mode 100644 index 00000000..1b4ac712 --- /dev/null +++ b/simulate-phish.js @@ -0,0 +1,228 @@ +// Standalone simulation of checkPhishing logic with minimal helpers + +const { URL } = require('url'); + +// minimal string similarity (normalized Levenshtein, but simpler: ratio of matches) +function compareTwoStrings(a, b) { + if (a === b) return 1; + const len = Math.max(a.length, b.length); + let same = 0; + for (let i = 0; i < Math.min(a.length, b.length); i++) { + if (a[i] === b[i]) same++; + } + return same / len; +} + +const stringSimilarity = { compareTwoStrings }; + +// import real helpers from the module so simulation stays aligned +const heuristics = require('./modules/moderation/phishingHeuristics'); +const tld = { + getDomain: heuristics.getDomain, + getSubdomain: heuristics.getSubdomain, + getTld: heuristics.getTld +}; + +const punycode = { + toASCII: (str) => str // no IDN handling +}; + +// stub axios head to just return object with same URL +const axios = { + head: async (url, opts) => ({ request: { res: { responseUrl: url } } }) +}; + +const dns = { + resolve: async (host) => ['1.2.3.4'] +}; + +async function loadModel() { + return { + predict: (features) => { + const sum = features.reduce((a, b) => a + b, 0); + return 1 / (1 + Math.exp(-sum / features.length)); + } + }; +} + +// copy of checkPhishing but with above helpers +async function checkPhishing(input) { + let url = input.url || input.message; + let isMessage = !!input.message; + let riskScore = 0; + let reasons = []; + let features = []; + + if (isMessage) { + const urlRegex = /(https?:\/\/[^\s]+)/g; + const matches = url.match(urlRegex); + if (matches) url = matches[0]; + else return { isPhishing: false, riskScore: 0, reasons: ['No URL found'] }; + } + + let parsed; + try { + parsed = new URL(url); + } catch (e) { + return { isPhishing: true, riskScore: 100, reasons: ['Invalid URL format'] }; + } + + const hostname = parsed.hostname; + const domain = tld.getDomain(hostname); + const subdomain = tld.getSubdomain(hostname); + const path = parsed.pathname; + const query = parsed.search; + + const legitDomains = [ + 'paypal.com', 'google.com', 'bankofamerica.com', 'apple.com', 'amazon.com', + 'microsoft.com', 'facebook.com', 'twitter.com', 'instagram.com', 'netflix.com', + 'chase.com', 'wellsfargo.com', 'citibank.com', 'usbank.com', 'capitalone.com' + ]; + + const phishingKeywords = [ + 'login', 'secure', 'account', 'verify', 'update', 'banking', 'password', + 'signin', 'auth', 'recovery', 'billing', 'payment', 'support', 'helpdesk' + ]; + const suspiciousTLDs = ['tk', 'ml', 'ga', 'cf', 'gq', 'xyz', 'top', 'club', 'site', 'online']; + + // heuristics + if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname) || /^[\da-f:]+$/i.test(hostname)) { + riskScore += 30; + reasons.push('URL uses IP address (potential obfuscation)'); + features.push(1); + } + + if (shorteners.includes(domain)) { + riskScore += 20; + reasons.push('URL shortener detected (often hides phishing)'); + features.push(1); + } + + legitDomains.forEach(legit => { + const similarity = stringSimilarity.compareTwoStrings(domain, legit); + if (similarity > 0.7 && domain !== legit) { + riskScore += 40; + reasons.push(`Domain similar to ${legit} (typosquatting score: ${similarity})`); + features.push(similarity); + } + }); + + const punyHostname = punycode.toASCII(hostname); + if (punyHostname !== hostname) { + riskScore += 35; + reasons.push('Internationalized Domain Name (IDN) detected - potential homograph attack'); + features.push(1); + legitDomains.forEach(legit => { + const sim = stringSimilarity.compareTwoStrings(punyHostname.replace('xn--', ''), legit); + if (sim > 0.8) { + riskScore += 20; + reasons.push(`Punycode similar to ${legit}`); + } + }); + } + + const parsedTld = tld.getTld(hostname); + if (subdomain.split('.').length > 2 || suspiciousTLDs.includes(parsedTld)) { + riskScore += 15; + reasons.push('Suspicious TLD or deep subdomain (common in free domain phishing)'); + features.push(1); + } + + phishingKeywords.forEach(kw => { + if (path.toLowerCase().includes(kw) || query.toLowerCase().includes(kw)) { + riskScore += 10; + reasons.push(`Phishing keyword '${kw}' in URL path/query`); + features.push(1); + } + }); + + if (url.length > 100) { + riskScore += 10; + reasons.push('Excessively long URL (potential obfuscation)'); + features.push(url.length / 100); + } + + if (parsed.protocol !== 'https:') { + riskScore += 25; + reasons.push('Non-secure HTTP (phishers avoid cert costs)'); + features.push(1); + } + + try { + const dnsInfo = await dns.resolve(hostname); + if (dnsInfo.length === 0) { + riskScore += 30; + reasons.push('No DNS resolution (possible dead or fake domain)'); + } + } catch (err) { + riskScore += 20; + reasons.push('DNS resolution failed'); + features.push(1); + } + + if (url.includes('@')) { + riskScore += 30; + reasons.push('URL contains @ (potential credential phishing via basic auth)'); + features.push(1); + } + + if (/%[0-9A-Fa-f]{2}/.test(url) || /\\x[0-9A-Fa-f]{2}/.test(url)) { + riskScore += 15; + reasons.push('Encoded characters in URL (obfuscation technique)'); + features.push(1); + } + + customPatterns.forEach(p => { + try { + const re = new RegExp(p, 'i'); + if (re.test(url) || (isMessage && re.test(input.message))) { + riskScore += 20; + reasons.push(`Custom pattern '${p}' matched`); + features.push(1); + } + } catch (e) {} + }); + try { + const response = await axios.head(url, { maxRedirects: 5 }); + if (response.request.res.responseUrl !== url) { + riskScore += 20; + reasons.push('URL redirects (check target separately)'); + } + } catch (err) {} + + const model = await loadModel(); + const mlProb = model.predict(features); + riskScore += mlProb * 50; + + riskScore = Math.min(Math.max(riskScore, 0), 100); + + if (isMessage) { + const urgencyWords = ['urgent', 'immediate', 'action required', 'suspended', 'verify now', 'click here']; + urgencyWords.forEach(word => { + if (input.message.toLowerCase().includes(word)) { + riskScore += 10; + reasons.push(`Urgency keyword '${word}' in message`); + } + }); + } + + return { + isPhishing: riskScore > 50, + riskScore, + reasons + }; +} + +// helper variables +let customPatterns = []; +const shorteners = ['bit.ly', 'tinyurl.com', 'goo.gl', 't.co', 'ow.ly', 'is.gd', 'buff.ly']; + +// run some tests +(async () => { + const testUrls = ['http://paypal.com.example.tk/login', 'http://example.com']; + for (const u of testUrls) { + const res = await checkPhishing({ url: u }); + console.log(`\n${u}`); + console.log(res); + } +})(); diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 00000000..fd6a2ecb --- /dev/null +++ b/src/cli.js @@ -0,0 +1,54 @@ +const fs = require('fs'); +const {reloadConfig} = require('./functions/configuration'); +const {syncCommandsIfNeeded} = require('../main'); + +module.exports.commands = [ + { + command: 'help', + description: 'Shows this help message', + run: function (inputElement) { + let allCommandString = `Welcome! Currently ${inputElement.cliCommands.length} commands are loaded.\n\n`; + for (const command of inputElement.cliCommands) { + if (command.module) allCommandString = allCommandString + `[${command.module}] ${command.originalName || command.command}: ${command.description}\n`; + else allCommandString = allCommandString + `${command.originalName || command.command}: ${command.description}\n`; + } + console.log(allCommandString); + } + }, + { + command: 'license', + description: 'Shows the license', + run: function () { + const license = fs.readFileSync(`${__dirname}/../LICENSE`); + console.log(license.toString()); + } + }, + { + command: 'reload', + description: 'Reloads the configuration of the bot', + run: async function (inputElement) { + if (inputElement.client.logChannel) await inputElement.client.logChannel.send('🔄 Reloading configuration because CLI said so'); + reloadConfig(inputElement.client).then(async () => { + if (inputElement.client.logChannel) await inputElement.client.logChannel.send('✅ Configuration reloaded successfully.'); + console.log('Reloaded successfully, syncing commands...'); + await syncCommandsIfNeeded(); + console.log('Synced commands, configuration reloaded.'); + }).catch(async () => { + if (inputElement.client.logChannel) await inputElement.client.logChannel.send('⚠️️ Configuration reloaded failed. Bot shutting down'); + console.log('Reload failed. Exiting'); + process.exit(1); + }); + } + }, + { + command: 'modules', + description: 'Shows all modules of the bot', + run: async function (inputElement) { + let message = '=== MODULES ==='; + for (const moduleName in inputElement.client.modules) { + message = message + `\n• ${moduleName}: ${inputElement.client.modules[moduleName].enabled ? 'Enabled' : 'Disabled'}`; + } + console.log(message); + } + } +]; \ No newline at end of file diff --git a/src/commands/help.js b/src/commands/help.js new file mode 100644 index 00000000..bee8af8a --- /dev/null +++ b/src/commands/help.js @@ -0,0 +1,142 @@ +const {truncate, formatDate, sendMultipleSiteButtonMessage, formatDiscordUserName} = require('../functions/helpers'); +const {MessageEmbed} = require('discord.js'); +const {localize} = require('../functions/localize'); + +module.exports.run = async function (interaction) { + const modules = {}; + for (const command of interaction.client.commands) { + if (command.module && !interaction.client.modules[command.module].enabled) continue; + if (!modules[command.module || 'none']) modules[command.module || 'none'] = []; + modules[command.module || 'none'].push(command); + } + const sites = []; + let siteCount = 0; + + const embedFields = []; + for (const module in modules) { + let content = ''; + if (module !== 'none') content = `*${(interaction.client.modules[module]['config']['description'][interaction.client.locale] || interaction.client.modules[module]['config']['description']['en'])}*\n`; + for (let d of modules[module]) { + content = content + `\n* \`/${d.name}\`: ${d.description}`; + d = {...d}; + if (typeof d.options === 'function') d.options = await d.options(interaction.client); + if ((d.options || []).filter(o => o.type === 'SUB_COMMAND' || o.type === 'SUB_COMMANDS_GROUP').length !== 0) { + for (const c of d.options) { + addSubCommand(c); + } + } + + /** + * Add a bullet-point for a subcommand + * @private + * @param {Object} command Command to add + * @param {String} bulletPointStyle Style of bullet-points to use + * @param {String} tab Tabs to use to make the message look good + */ + function addSubCommand(command, tab = ' ') { + content = content + `\n${tab}* \`${command.name}\`: ${command.description}`; + if (command.type === 'SUB_COMMAND_GROUP' && (command.options || []).filter(o => o.type === 'SUB_COMMAND').length !== 0) { + for (const c of command.options) { + addSubCommand(c, ' '); + } + } + } + } + embedFields.push({ + name: `**${module === 'none' ? interaction.client.strings.helpembed.build_in : (interaction.client.modules[module]['config']['humanReadableName'][interaction.client.locale] || interaction.client.modules[module]['config']['humanReadableName']['en'] || module)}**`, + value: truncate(content, 1024) + }); + } + + embedFields.filter(f => f.name === '**' + interaction.client.strings.helpembed.build_in + '**').forEach(f => { + const fields = [ + f + ]; + if (!interaction.client.strings['putBotInfoOnLastSite']) { + fields.push({ + name: '\u200b', + value: '\u200b' + }); + fields.push({ + name: localize('help', 'bot-info-titel'), + + /* + *IMPORTANT WARNING: + *Changing or removing the license notice might be a violation of the Business Source License the bot was licensed under. + *Violating the license might lead to deactivation of your bot on Discord and legal action being taken against you. + *Please read the license carefully: https://github.com/ScootKit/CustomDCBot/blob/main/LICENSE + */ + value: localize('help', 'bot-info-description', {g: interaction.guild.name}) + }); + } + if (!interaction.client.strings['disableHelpEmbedStats']) fields.push({ + name: localize('help', 'stats-title'), + value: localize('help', 'stats-content', { + am: Object.keys(interaction.client.modules).length, + rc: interaction.client.commands.length, + v: interaction.client.scnxSetup ? interaction.client.scnxData.bot.version : null, + si: interaction.client.scnxSetup ? interaction.client.scnxData.bot.instanceID : null, + pl: interaction.client.scnxSetup ? localize('scnx', 'plan-' + interaction.client.scnxData.plan) : null, + lr: formatDate(interaction.client.readyAt), + lR: formatDate(interaction.client.botReadyAt) + }) + }); + addSite( + fields, + true); + }); + + + let fieldCount = 0; + let fieldCache = []; + for (const field of embedFields.filter(f => f.name !== '**' + interaction.client.strings.helpembed.build_in + '**')) { + fieldCount++; + fieldCache.push(field); + if (fieldCount % 3 === 0) { + addSite(fieldCache); + fieldCache = []; + } + } + if (fieldCache.length !== 0) addSite(fieldCache); + + /** + * Adds a site to the embed + * @param {Array} fields Fields to add + * @param atBeginning If this site needs to go at the beginning of the array + * @private + */ + function addSite(fields, atBeginning = false) { + siteCount++; + const embed = new MessageEmbed().setColor('RANDOM') + .setDescription(interaction.client.strings.helpembed.description) + .setThumbnail(interaction.client.user.avatarURL()) + .setAuthor({name: formatDiscordUserName(interaction.user), iconURL: interaction.user.avatarURL()}) + .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) + .setTitle(interaction.client.strings.helpembed.title.replaceAll('%site%', siteCount)) + .addFields(fields); + if (atBeginning) sites.unshift(embed); + else sites.push(embed); + } + + if (interaction.client.strings['putBotInfoOnLastSite']) sites[sites.length - 1].setFields(...sites[sites.length - 1].fields, { + name: '\u200b', + value: '\u200b' + }, { + name: localize('help', 'bot-info-titel'), + + /* + *IMPORTANT WARNING: + *Changing or removing the license notice might be a violation of the Business Source License the bot was licensed under. + *Violating the license might lead to deactivation of your bot on Discord and legal action being taken against you. + *Please read the license carefully: https://github.com/ScootKit/CustomDCBot/blob/main/LICENSE + */ + value: localize('help', 'bot-info-description', {g: interaction.guild.name}) + }); + + sendMultipleSiteButtonMessage(interaction.channel, sites, [interaction.user.id], interaction); +}; + +module.exports.config = { + name: 'help', + description: localize('help', 'command-description') +}; \ No newline at end of file diff --git a/src/commands/reload.js b/src/commands/reload.js new file mode 100644 index 00000000..cd6598e5 --- /dev/null +++ b/src/commands/reload.js @@ -0,0 +1,31 @@ +const {reloadConfig} = require('../functions/configuration'); +const {syncCommandsIfNeeded} = require('../../main'); +const {localize} = require('../functions/localize'); +const {formatDiscordUserName} = require('../functions/helpers'); + +module.exports.run = async function (interaction) { + await interaction.reply({ + ephemeral: true, + content: localize('reload', 'reloading-config') + }); + if (interaction.client.logChannel) interaction.client.logChannel.send('🔄 ' + localize('reload', 'reloading-config-with-name', {tag: formatDiscordUserName(interaction.user)})).then(() => { + }); + await reloadConfig(interaction.client).catch((async reason => { + if (interaction.client.logChannel) interaction.client.logChannel.send('⚠️️ ' + localize('reload', 'reload-failed')).then(() => { + }); + await interaction.editReply({content: localize('reload', 'reload-failed-message', {reason})}); + process.exit(1); + })).then(async (res) => { + if (interaction.client.logChannel) interaction.client.logChannel.send('✅ ' + localize('reload', 'reloaded-config', res)).then(() => { + }); + await interaction.editReply(localize('reload', 'reload-successful-syncing-commands')); + await syncCommandsIfNeeded(); + await interaction.editReply(localize('reload', 'reloaded-config', res)); + }); +}; + +module.exports.config = { + name: 'reload', + description: localize('reload', 'command-description'), + restricted: true +}; \ No newline at end of file diff --git a/src/events/botReady.js b/src/events/botReady.js new file mode 100644 index 00000000..987d3ca8 --- /dev/null +++ b/src/events/botReady.js @@ -0,0 +1,4 @@ +module.exports.run = async (client) => { + if (client.config.disableStatus) client.user.setActivity(null); + else await client.user.setActivity(client.config.user_presence); +}; \ No newline at end of file diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js new file mode 100644 index 00000000..631d7def --- /dev/null +++ b/src/events/interactionCreate.js @@ -0,0 +1,109 @@ +const {embedType, formatDiscordUserName} = require('../functions/helpers'); +const {localize} = require('../functions/localize'); + +module.exports.run = async (client, interaction) => { + if (!client.botReadyAt) { + if (interaction.isAutocomplete()) return interaction.respond({}); + return interaction.reply({ + content: '⚠️ ' + localize('command', 'startup'), + ephemeral: true + }); + } + if (client.guild.id !== interaction.guild.id) { + if (interaction.isAutocomplete()) return interaction.respond({}); + return interaction.reply({ + content: '⚠️ ' + localize('command', 'wrong-guild', {g: client.guild.name}), + ephemeral: true + }); + } + if ((interaction.customId || '').startsWith('cc-') && client.scnxSetup) return require('../functions/scnx-integration').customCommandInteractionClick(interaction); + if (interaction.isSelectMenu() && interaction.customId === 'select-roles' && client.scnxSetup) return require('../functions/scnx-integration').handleSelectRoles(client, interaction); + if (interaction.isButton() && interaction.customId.startsWith('srb-') && client.scnxSetup) return require('../functions/scnx-integration').handleRoleButton(client, interaction); + if (!interaction.commandName) return; + const command = client.commands.find(c => c.name.toLowerCase() === interaction.commandName.toLowerCase()); + if (!command) { + if (client.scnxSetup) return require('./../functions/scnx-integration').customCommandSlashInteraction(interaction); + else return interaction.reply({content: '⚠️ ' + localize('command', 'not-found'), ephemeral: true}); + } + if (command.module && !client.modules[command.module].enabled) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('command', 'module-disabled', {m: command.module}) + }); + if (command && typeof (command || {}).options === 'function') command.options = await command.options(interaction.client); + const group = interaction.options['_group']; + const subCommand = interaction.options['_subcommand']; + if (interaction.isAutocomplete()) { + let focusedOption = interaction.options['_hoistedOptions'].find(h => h.focused); + interaction.value = (focusedOption || {}).value; + focusedOption = (focusedOption || {}).name; + if (!focusedOption) return interaction.respond({}); + try { + if (!command) return interaction.respond({}); + if (command.options.filter(c => c.type === 'SUB_COMMAND').length === 0) return await command.autoComplete[focusedOption](interaction); + if (group) return await command.autoComplete[group][subCommand][focusedOption](interaction); + else return await command.autoComplete[subCommand][focusedOption](interaction); + } catch (e) { + if (client.captureException) client.captureException(e, { + command: command.name, + module: command.module, + group, + subCommand, + focusedOption, + userID: interaction.user.id + }); + interaction.client.logger.error(localize('command', 'autcomplete-execution-failed', { + e, + f: focusedOption, + c: command.name, + g: group || '', + s: subCommand || '' + })); + interaction.respond([]); + } + } + if (!interaction.isCommand()) return; + if (command.restricted === true && !client.config.botOperators.includes(interaction.user.id)) return interaction.reply(embedType(client.strings.not_enough_permissions)); + client.logger.debug(localize('command', 'used', { + tag: formatDiscordUserName(interaction.user), + id: interaction.user.id, + c: command.name + `${group ? ' ' + group : ''}${subCommand ? ' ' + subCommand : ''}` + })); + + try { + if (command.options.filter(c => c.type === 'SUB_COMMAND').length === 0) return await command.run(interaction); + if (!command.subcommands) { + interaction.client.logger.error(`Command ${interaction.commandName} has subcommands but does not use the subcommands handler (required).`); + return interaction.reply({ + content: '⚠️ This command is not configured correctly and can not be executed, please contact the developer.', + ephemeral: true + }); + } + if (command.beforeSubcommand) await command.beforeSubcommand(interaction); + if (group) await command.subcommands[group][subCommand](interaction); + else await command.subcommands[subCommand](interaction); + if (command.run) await command.run(interaction); + } catch (e) { + if (client.captureException) client.captureException(e, { + command: command.name, + module: command.module, + group, + subCommand, + userID: interaction.user.id + }); + interaction.client.logger.error(localize('command', 'execution-failed', { + e, + c: command.name, + g: group || '', + s: subCommand || '' + })); + if (!interaction.deferred) { + interaction.reply({ + content: localize('command', 'execution-failed-message', {e}), + ephemeral: true + }).catch(() => { + }); + } else await interaction.editReply(localize('command', 'execution-failed-message')).catch(() => { + }); + } +}; +module.exports.ignoreBotReadyCheck = true; \ No newline at end of file diff --git a/src/functions/configuration.js b/src/functions/configuration.js new file mode 100644 index 00000000..0b585c89 --- /dev/null +++ b/src/functions/configuration.js @@ -0,0 +1,362 @@ +/** + * Handels configuration loading and reloading + * @module Configuration + * @author Simon Csaba + */ +const jsonfile = require('jsonfile'); +const fs = require('fs'); +const {logger, client} = require('../../main'); +const {localize} = require('./localize'); +const isEqual = require('is-equal'); + +/** + * Check every (including module) configuration and load them + * @author Simon Csaba + * @param {Client} client The client + * @param {Object} moduleConf Configuration of modules.json + * @return {Promise} + */ +async function loadAllConfigs(client) { + logger.info(localize('config', 'checking-config')); + return new Promise(async (resolve, reject) => { + await fs.readdir(`${__dirname}/../../config-generator`, async (err, files) => { + for (const f of files) { + await checkConfigFile(f).catch((reason) => { + logger.error(reason); + reject(reason); + }); + } + }); + + for (const moduleName in client.modules) { + if (!client.modules[moduleName].userEnabled) continue; + await checkModuleConfig(moduleName, client.modules[moduleName]['config']['on-checked-config-event'] ? require(`./modules/${moduleName}/${client.modules[moduleName]['config']['on-checked-config-event']}`) : null) + .catch(async (e) => { + client.modules[moduleName].enabled = false; + client.logger.error(`[CONFIGURATION] ERROR CHECKING ${moduleName}. Module disabled internally. Error: ${e}`); + if (client.scnxSetup) await require('./scnx-integration').reportIssue(client, { + type: 'MODULE_FAILURE', + errorDescription: 'module_disabled', + module: moduleName, + errorData: {reason: 'Invalid configuration: ' + e} + }); + }); + } + const data = { + totalModules: Object.keys(client.modules).length, + enabled: Object.values(client.modules).filter(m => m.enabled).length, + configDisabled: Object.values(client.modules).filter(m => m.userEnabled && !m.enabled).length, + userEnabled: Object.values(client.modules).filter(m => m.userEnabled && !m.enabled).length + }; + logger.info(localize('config', 'done-with-checking', data)); + resolve(data); + }); +} + +/** + * + */ +async function checkConfigFile(file, moduleName) { + const {client} = require('../../main'); + return new Promise(async (resolve, reject) => { + const builtIn = !moduleName; + let exampleFile; + try { + exampleFile = require(builtIn ? `${__dirname}/../../config-generator/${file}` : `${__dirname}/../../modules/${moduleName}/${file}`); + } catch (e) { + logger.error(`Not found config example file: ${file}`); + return reject(`Not found config example file: ${file}`); + } + if (!exampleFile) return; + let forceOverwrite = false; + let configData = exampleFile.configElements ? [] : {}; + try { + configData = jsonfile.readFileSync(`${client.configDir}${builtIn ? '' : '/' + moduleName}/${exampleFile.filename}`); + } catch (e) { + forceOverwrite = true; + logger.info(localize('config', 'creating-file', { + m: builtIn ? 'bot' : moduleName, + f: exampleFile.filename + })); + } + let newConfig = exampleFile.configElements ? [] : {}; + if (exampleFile.elementLimits) configData = require('./scnx-integration').verifyLimitedConfigElementFile(client, exampleFile, configData); + + let skipOverwrite = false; + if (exampleFile.skipContentCheck) newConfig = configData; + else if (exampleFile.configElements) { + if (!Array.isArray(configData)) { + client.logger.warn(`${builtIn ? '' : '/' + moduleName}/${exampleFile.filename}: This file should be a config-element, but is not. Converting to config-element.`); + if (typeof configData === 'object') configData = [configData]; + else configData = []; + } + for (const object of configData) { + const objectData = {}; + for (const field of exampleFile.content) { + const dependsOnField = field.dependsOn ? exampleFile.content.find(f => f.name === field.dependsOn) : null; + if (field.dependsOn && !dependsOnField) return reject(`Depends-On-Field ${field.dependsOn} does not exist.`); + if (dependsOnField && !(typeof object[dependsOnField.name] === 'undefined' ? (dependsOnField.default[client.locale] || dependsOnField.default['en']) : object[dependsOnField.name])) { + objectData[field.name] = configData[field.name] || (field.default[client.locale] || field.default['en']); // Otherwise disabled fields may be overwritten + continue; + } + try { + objectData[field.name] = await checkField(field, object[field.name]); + } catch (e) { + return reject(e); + } + } + newConfig.push(objectData); + } + } else { + for (const field of exampleFile.content) { + if (exampleFile.content.find(f => f.elementToggle) && !configData[exampleFile.content.find(f => f.elementToggle).name]) { + skipOverwrite = true; + continue; + } + const dependsOnField = field.dependsOn ? exampleFile.content.find(f => f.name === field.dependsOn) : null; + if (field.dependsOn && !dependsOnField) return reject(`Depends-On-Field ${field.dependsOn} does not exist.`); + if (dependsOnField && !(typeof configData[dependsOnField.name] === 'undefined' ? (dependsOnField.default[client.locale] || dependsOnField.default['en']) : configData[dependsOnField.name])) { + newConfig[field.name] = configData[field.name] || (field.default[client.locale] || field.default['en']); // Otherwise disabled fields may be overwritten + continue; + } + try { + newConfig[field.name] = await checkField(field, configData[field.name]); + } catch (e) { + return reject(e); + } + } + } + + /** + * Checks the content of a field + * @param {Field} field Field-Object + * @param {*} fieldValue Current config element + * @returns {Promise} + */ + function checkField(fieldData, fieldValue) { + const field = {...fieldData}; + return new Promise(async (res, rej) => { + if (!field.name) return rej('missing fieldname.'); + if (typeof field.default === 'undefined' || typeof field.default.en === 'undefined') { + console.log(field.default); + return rej('Missing default value on ' + field.name); + } + if (typeof field.default !== 'object') return rej(`${field.name} has an invalid default value. The default value needs to be localized. A possible fix could be: default = "${JSON.stringify({en: field.default})}". If you want a default value for all languages, only set the "en" key.`); + field.default = field.default[client.locale] || field.default['en']; + if (typeof fieldValue === 'undefined') { + fieldValue = field.default; + return res(fieldValue); + } else if (field.type === 'keyed' && field.disableKeyEdits) for (const key in field.default) if (typeof fieldValue[key] === 'undefined') fieldValue[key] = field.default[key]; + if (field.allowNull && field.type !== 'boolean' && !fieldValue) return res(fieldValue); + if (!await checkType(field.type, fieldValue, field.content, field.allowEmbed)) { + if (client.scnxSetup) await require('./scnx-integration').reportIssue(client, { + type: 'CONFIGURATION_ISSUE', + module: moduleName, + field: field.name, + configFile: exampleFile.filename.replaceAll('.json', ''), + errorDescription: 'field_check_failed' + }); + logger.error(localize('config', 'checking-of-field-failed', { + fieldName: field.name, + m: moduleName, + f: exampleFile.filename + })); + rej(localize('config', 'checking-of-field-failed', { + fieldName: field.name, + m: moduleName, + f: exampleFile.filename + })); + } + if (field.disableKeyEdits && field.type === 'keyed') { + for (const key in fieldValue) { + if (typeof field.default[key] === 'undefined') delete fieldValue[key]; + } + for (const key in field.default) { + if (typeof fieldValue[key] === 'undefined') fieldValue[key] = field.default[key]; + } + } + if (client.scnxSetup) fieldValue = require('./scnx-integration').setFieldValue(client, field, fieldValue); + res(fieldValue); + }); + } + + if (forceOverwrite || (!skipOverwrite && !isEqual(configData, newConfig))) { + if (!fs.existsSync(`${client.configDir}/${moduleName}`) && moduleName) fs.mkdirSync(`${client.configDir}/${moduleName}`); + jsonfile.writeFileSync(`${client.configDir}${builtIn ? '' : '/' + moduleName}/${exampleFile.filename}`, newConfig, {spaces: 2}); + logger.info(localize('config', 'saved-file', { + f: file, + m: moduleName + })); + } + if (!builtIn) client.configurations[moduleName][exampleFile.filename.split('.json').join('')] = newConfig; + resolve(); + }); +} + +/** + * Checks the build-in-configuration (not modules) + * @private + * @param {String} moduleName Name of the module to check + * @param {FileName} afterCheckEventFile File to execute after config got checked + * @returns {Promise} + */ +async function checkModuleConfig(moduleName, afterCheckEventFile = null) { + return new Promise(async (resolve, reject) => { + const moduleConf = require(`../../modules/${moduleName}/module.json`); + if ((moduleConf['config-example-files'] || []).length === 0) return resolve(); + try { + for (const v of moduleConf['config-example-files']) await checkConfigFile(v, moduleName); + resolve(); + } catch (r) { + reject(r); + } + if (afterCheckEventFile) require(`../../modules/${moduleName}/${afterCheckEventFile}`).afterCheckEvent(config); + } + ); +} + +module.exports.loadAllConfigs = loadAllConfigs; + +/** + * Check type of one field + * @param {FieldType} type Type of the field + * @param {String} value Value in the configuration file + * @param {ConfigFormat} contentFormat Format of the content + * @param {Boolean} allowEmbed If embeds are allowed + * @returns {Promise} + * @private + */ +async function checkType(type, value, contentFormat = null, allowEmbed = false) { + const {client} = require('../../main'); + switch (type) { + case 'integer': + if (parseInt(value) === 0) return true; + return !!parseInt(value); + case 'float': + if (parseFloat(value) === 0) return true; + return !!parseFloat(value); + case 'string': + case 'emoji': + case 'imgURL': + case 'timezone': // Timezones can not be checked correctly for their type currently. + if (allowEmbed && typeof value === 'object') return true; + return typeof value === 'string'; + case 'array': + if (!Array.isArray(value)) return false; + let errored = false; + for (const v of value) { + if (!errored) errored = !(await checkType(contentFormat, v, null, allowEmbed)); + } + return !errored; + case 'userID': + const user = await client.users.fetch(value).catch(() => { + }); + if (!user) { + logger.error(localize('config', 'user-not-found', {id: value})); + return false; + } + return true; + case 'channelID': + const channel = await client.channels.fetch(value).catch(() => { + }); + if (!channel) { + logger.error(localize('config', 'channel-not-found', {id: value})); + return false; + } + if (channel.guild.id !== client.guildID) { + logger.error(localize('config', 'channel-not-on-guild', {id: value})); + return false; + } + if (!(contentFormat || ['GUILD_TEXT', 'GUILD_CATEGORY', 'GUILD_NEWS', 'GUILD_VOICE', 'GUILD_STAGE_VOICE']).includes(channel.type)) { + logger.error(localize('config', 'channel-invalid-type', {id: value})); + return false; + } + return true; + case 'roleID': + if (await (await client.guilds.fetch(client.guildID)).roles.fetch(value)) { + return true; + } else { + logger.error(localize('config', 'role-not-found', {id: value})); + return false; + } + case 'guildID': + if (client.guilds.cache.find(g => g.id === client.guildID)) { + return true; + } else { + logger.error(`Guild with ID "${value}" could not be found - have you invited the bot?`); + return false; + } + case 'keyed': + if (typeof value !== 'object') return false; + let returnValue = true; + for (const v in value) { + if (returnValue) { + returnValue = await checkType(contentFormat.key, v); + returnValue = await checkType(contentFormat.value, value[v]); + } + } + return returnValue; + case 'select': + return contentFormat.includes(value); + case 'boolean': + return typeof value === 'boolean'; + default: + logger.error(`Unknown type: ${type}`); + process.exit(1); + } +} + +/** + * Check every (including module) configuration and load them + * @param {Client} client The client + * @fires Client#configReload + * @fires Client#botReady when loaded successfully + * @since v2 + * @author Simon Csaba + * @return {Promise} + */ +module.exports.reloadConfig = async function (client) { + client.logger.info(localize('config', 'config-reload')); + if (client.scnxSetup) await require('./scnx-integration').beforeInit(client); + client.botReadyAt = null; + + /** + * Emitted when the configuration gets reloaded, used to disable intervals + * @event Client#configReload + */ + client.emit('configReload'); + + for (const interval of client.intervals) { + clearInterval(interval); + } + client.intervals = []; + for (const job of client.jobs.filter(f => f !== null)) { + job.cancel(); + } + client.jobs = []; + + // Reload module configuration + const moduleConf = jsonfile.readFileSync(`${client.configDir}/modules.json`); + for (const moduleName in client.modules) { + client.modules[moduleName].enabled = !!moduleConf[moduleName]; + client.modules[moduleName].userEnabled = !!moduleConf[moduleName]; + } + + const res = await loadAllConfigs(client); + client.botReadyAt = new Date(); + + if (client.scnxSetup) await require('./scnx-integration').init(client, true); + + /** + * Emitted when the configuration got loaded successfully + * @event Client#botReady + */ + client.emit('botReady'); + + if (client.scnxSetup) { + client.config.customCommands = jsonfile.readFileSync(`${client.configDir}/custom-commands.json`); + await require('./scnx-integration').verifyCustomCommands(client); + } + + return res; +}; \ No newline at end of file diff --git a/src/functions/helpers.js b/src/functions/helpers.js new file mode 100644 index 00000000..f42f2fd4 --- /dev/null +++ b/src/functions/helpers.js @@ -0,0 +1,562 @@ +/** + * Functions to make your live easier + * @module Helpers + */ + +const {MessageEmbed, MessageAttachment} = require('discord.js'); +const {localize} = require('./localize'); +const {PrivatebinClient} = require('@pixelfactory/privatebin'); +const privatebin = new PrivatebinClient('https://paste.scootkit.net'); +const isoCrypto = require('isomorphic-webcrypto'); +const {encode} = require('bs58'); +const crypto = require('crypto'); +const {client} = require('../../main'); + +/** + * Will loop asynchrony through every object in the array + * @deprecated Since version v3.0.0. Will be deleted in v3.1.0. Use for(const value of array) instead. + * @param {Array} array Array of objects + * @param {function(object, number, array)} callback Function that gets executed on every array (object, index in the array, array) + * @return {Promise} + */ +module.exports.asyncForEach = async function (array, callback) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array); + } +}; + +/** + * Formates a Discord username (either #tag or username) + * @param {User} userData User to format + * @returns {string} + */ +function formatDiscordUserName(userData) { + if (userData.discriminator === '0') return ((client.strings || {addAtToUsernames: false}).addAtToUsernames ? '@' : '') + userData.username; + return userData.tag || (userData.username + '#' + userData.discriminator); +} + +module.exports.formatDiscordUserName = formatDiscordUserName; + +/** + * Replaces every argument with a string + * @param {Object} args Arguments to replace + * @param {String} input Input + * @param {Boolean} returnNull Allows returning null if input is null + * @returns {String} + * @private + */ +function inputReplacer(args, input, returnNull = false) { + if (returnNull && !input) return null; + else if (!input) input = ''; + if (typeof args !== 'object') return input; + for (const arg in args) { + if (typeof args[arg] !== 'string' && typeof args[arg] !== 'number') args[arg] = ''; + input = (input || '').replaceAll(arg, args[arg]); + } + if (returnNull && !input) return null; + return input; +} + +module.exports.inputReplacer = inputReplacer; + +/** + * Will turn an object or string into embeds + * @param {string|array} input Input in the configuration file + * @param {Object} args Object of variables to replace + * @param {Object} optionsToKeep [BaseMessageOptions](https://discord.js.org/#/docs/main/stable/typedef/BaseMessageOptions) to keep + * @param {Array} mergeComponentsRows ActionRows to be merged with custom rows + * @author Simon Csaba + * @return {object} Returns [MessageOptions](https://discord.js.org/#/docs/main/stable/typedef/MessageOptions) + */ +function embedType(input, args = {}, optionsToKeep = {}, mergeComponentsRows = []) { + if (!optionsToKeep.allowedMentions) { + optionsToKeep.allowedMentions = {parse: ['users', 'roles']}; + if (client.config.disableEveryoneProtection) optionsToKeep.allowedMentions.parse.push('everyone'); + } + if (typeof input === 'string') { + optionsToKeep.content = inputReplacer(args, input); + return optionsToKeep; + } + const schemaVersion = input['_schema'] || 'v2'; + if (schemaVersion === 'v2') return embedTypeSchemaV2(input, args, optionsToKeep, mergeComponentsRows); + + optionsToKeep.embeds = []; + for (const embedData of input.embeds || []) { + if (client.scnxSetup) embedData.footer = require('./scnx-integration').verifySchemaV3Embed(client, embedData.footer); + let footer = null; + if (!embedData.footer?.disabled) footer = { + text: inputReplacer(args, embedData.footer?.text, true) || client.strings.footer, + iconURL: embedData.footer?.iconURL || client.strings.footerImgUrl + }; + const fields = []; + + for (const fieldData of embedData.fields || []) fields.push({ + name: inputReplacer(args, fieldData.name, true) || '\u200B', + value: inputReplacer(args, fieldData.value, true) || '\u200B', + inline: fieldData.inline + }); + + const embed = new MessageEmbed({ + title: inputReplacer(args, embedData.title, true), + description: inputReplacer(args, embedData.description, true), + color: embedData.color, + thumbnail: embedData.thumbnailURL ? {url: inputReplacer(args, embedData.thumbnailURL)} : null, + image: embedData.imageURL ? {url: inputReplacer(args, embedData.imageURL)} : null, + timestamp: (embedData.footer?.hideTime || embedData.footer?.disabled) ? null : new Date(), + author: embedData.author?.name ? { + name: inputReplacer(args, embedData.author.name), + iconURL: inputReplacer(args, embedData.author.imageURL, null), + url: inputReplacer(args, embedData.author.url, null) + } : null, + footer, + fields + }); + optionsToKeep.embeds.push(embed); + } + + optionsToKeep.files = [...(optionsToKeep.files || [])]; + for (const url of input.attachmentURLs || []) { + optionsToKeep.files.push({attachment: url}); + } + + if (!optionsToKeep.components && client.scnxSetup) optionsToKeep.components = require('./scnx-integration').returnSCNXComponents(input, mergeComponentsRows, args); + if (!optionsToKeep.content) optionsToKeep.content = inputReplacer(args, input['content'], true); + + return optionsToKeep; +} + +function embedTypeSchemaV2(input, args = {}, optionsToKeep = {}, mergeComponentsRows = []) { + if (!optionsToKeep.allowedMentions) { + optionsToKeep.allowedMentions = {parse: ['users', 'roles']}; + if (client.config.disableEveryoneProtection) optionsToKeep.allowedMentions.parse.push('everyone'); + } + if (client.scnxSetup) input = require('./scnx-integration').verifyEmbedType(client, input); + if (input.title || input.description || (input.author || {}).name || input.image) { + const emb = new MessageEmbed(); + if (input['title']) emb.setTitle(inputReplacer(args, input['title'])); + if (input['description']) emb.setDescription(inputReplacer(args, input['description'])); + if (input['color']) emb.setColor(input['color']); + if (input['url']) emb.setURL(input['url']); + if ((input['image'] || '').replaceAll(' ', '')) emb.setImage(inputReplacer(args, input['image'])); + if ((input['thumbnail'] || '').replaceAll(' ', '')) emb.setThumbnail(inputReplacer(args, input['thumbnail'])); + if (input['author'] && typeof input['author'] === 'object' && (input['author'] || {}).name) emb.setAuthor({ + name: inputReplacer(args, input['author']['name']), + iconURL: (input['author']['img'] || '').replaceAll(' ', '') ? inputReplacer(args, input['author']['img']) : null + }); + if (typeof input['fields'] === 'object') { + input.fields.forEach(f => { + emb.addField(inputReplacer(args, f['name']), inputReplacer(args, f['value']), f['inline']); + }); + } + if (!client.strings.disableFooterTimestamp && !input.embedTimestamp) emb.setTimestamp(); + if (input.embedTimestamp) emb.setTimestamp(input.embedTimestamp); + emb.setFooter({ + text: input.footer ? inputReplacer(args, input.footer) : client.strings.footer, + iconURL: (input.footerImgUrl || client.strings.footerImgUrl) + }); + optionsToKeep.embeds = [emb]; + } else optionsToKeep.embeds = []; + if (!optionsToKeep.components && client.scnxSetup) optionsToKeep.components = require('./scnx-integration').returnSCNXComponents(input, mergeComponentsRows, args); + optionsToKeep.content = input['message'] ? inputReplacer(args, input['message']) : null; + return optionsToKeep; +} + +module.exports.embedType = embedType; + +module.exports.embedTypeV2 = async function (input, args, otP, mergeComponentsRows) { + let optionsToKeep = embedType(input, args, otP, mergeComponentsRows); + if (!optionsToKeep.attachments && client.scnxSetup && (input.dynamicImage || {}).enabled) optionsToKeep = await require('./scnx-integration').returnDynamicImages(input, optionsToKeep, args); + return optionsToKeep; +}; + +/** + * Makes a Date humanly readable + * @param {Date} date Date to format + * @param {Boolean} skipDiscordFormat If enabled, the time will be returned in a real string, not using discord's message attachments + * @return {string} Returns humanly readable string + * @author Simon Csaba + */ +function formatDate(date, skipDiscordFormat = false) { + if (!skipDiscordFormat) return `${dateToDiscordTimestamp(date)} (${dateToDiscordTimestamp(date, 'R')})`; + const yyyy = date.getFullYear().toString(), mm = (date.getMonth() + 1).toString(), dd = date.getDate().toString(), + hh = date.getHours().toString(), min = date.getMinutes().toString(); + return localize('helpers', 'timestamp', { + dd: dd[1] ? dd : '0' + dd[0], + mm: mm[1] ? mm : '0' + mm[0], + yyyy, + hh: hh[1] ? hh : '0' + hh[0], + min: min[1] ? min : '0' + min[0] + }); +} + +module.exports.formatDate = formatDate; + +/** + * Posts (encrypted) content to SC Network Paste + * @param {String} content Content to post + * @param {Object} opts Configuration of upload entry + * @return {Promise} URL to document + */ +async function postToSCNetworkPaste(content, opts = { + expire: '1month', + burnafterreading: 0, + opendiscussion: 1, + textformat: 'plaintext', + output: 'text', + compression: 'zlib' +}) { + const key = isoCrypto.getRandomValues(new Uint8Array(32)); + const res = await privatebin.sendText(content, key, opts); + return `https://paste.scootkit.net${res.url}#${encode(key)}`; +} + +module.exports.postToSCNetworkPaste = postToSCNetworkPaste; + +/** + * Genrate a random string (cryptographically unsafe) + * @param {Number} length Length of the generated string + * @param {String} characters String of characters to choose from + * @returns {string} Random string + */ +module.exports.randomString = function (length, characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') { + let result = ''; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result = result + characters.charAt(Math.floor(Math.random() * + charactersLength)); + } + return result; +}; + +/** + * Creates a paste from the messages in a channel. + * @param {Channel} channel Channel to create log from + * @param {Number} limit Number of messages to include + * @param {String} expire Time after with paste expires + * @return {Promise} + */ +async function messageLogToStringToPaste(channel, limit = 100, expire = '1month') { + let messages = ''; + (await channel.messages.fetch({limit: limit > 100 ? 100 : limit})).forEach(m => { + messages = `[${m.id}] ${m.author.bot ? '[BOT] ' : ''}${formatDiscordUserName(m.author)} (${m.author.id}): ${m.content}\n` + messages; + }); + messages = `=== CHANNEL-LOG OF ${channel.name} (${channel.id}): Last messages before report ${formatDate(new Date())} ===\n` + messages; + return await postToSCNetworkPaste(messages, + { + expire, + burnafterreading: 0, + opendiscussion: 0, + textformat: 'plaintext', + output: 'text', + compression: 'zlib' + }); +} + +module.exports.messageLogToStringToPaste = messageLogToStringToPaste; + +/** + * Truncates a string to a specific length + * @param {string} string String to truncate + * @param {number} length Length to truncate to + * @return {string} Truncated string + */ +function truncate(string, length) { + return (string.length > length) ? string.substr(0, length - 3).trim() + '...' : string; +} + +module.exports.truncate = truncate; + +/** + * Puffers (add empty spaces to center text) a string to a specific size + * @param {string} string String to puffer + * @param {number} size Length to puffer to + * @return {string} + * @author Simon Csaba + */ +function pufferStringToSize(string, size) { + if (typeof string !== 'string') string = string.toString(); + const pufferNeeded = size - string.length; + for (let i = 0; i < pufferNeeded; i++) { + if (i % 2 === 0) string = '\xa0' + string; + else string = string + '\xa0'; + } + return string; +} + +module.exports.pufferStringToSize = pufferStringToSize; + +/** + * Sends a multiple-site-embed-message + * @param {Object} channel Channel in which to send the message + * @param {Array} sites Array of MessageEmbeds (https://discord.js.org/#/docs/main/stable/class/MessageEmbed) + * @param {Array} allowedUserIDs Array of User-IDs of users allowed to use the pagination + * @param {Object} messageOrInteraction Message or [CommandInteraction](https://discord.js.org/#/docs/main/stable/class/CommandInteraction) to respond to + * @return {string} + * @author Simon Csaba + */ +async function sendMultipleSiteButtonMessage(channel, sites = [], allowedUserIDs = [], messageOrInteraction = null) { + if (sites.length === 1) { + if (messageOrInteraction) return messageOrInteraction.reply({embeds: [sites[0]]}); + return await channel.send({embeds: [sites[0]]}); + } + let m; + if (messageOrInteraction) m = await messageOrInteraction.reply({ + components: [{type: 'ACTION_ROW', components: getButtons(1)}], + embeds: [sites[0]], + fetchReply: true + }); + else m = await channel.send({components: [{type: 'ACTION_ROW', components: getButtons(1)}], embeds: [sites[0]]}); + const c = m.createMessageComponentCollector({componentType: 'BUTTON', time: 60000}); + let currentSite = 1; + c.on('collect', async (interaction) => { + if (!allowedUserIDs.includes(interaction.user.id)) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('helpers', 'you-did-not-run-this-command') + }); + let nextSite = currentSite + 1; + if (interaction.customId === 'back') nextSite = currentSite - 1; + currentSite = nextSite; + await interaction.update({ + components: [{type: 'ACTION_ROW', components: getButtons(nextSite)}], + embeds: [sites[nextSite - 1]] + }); + }); + c.on('end', () => { + m.edit({ + components: [{type: 'ACTION_ROW', components: getButtons(currentSite, true)}], + embeds: [sites[currentSite - 1]] + }); + }); + + /** + * Generate the buttons for a specified site + * @param {Number} site Site-Number + * @param {Boolean} disabled If the buttons should be disabled + * @returns {Array} + * @private + */ + function getButtons(site, disabled = false) { + const btns = []; + if (site !== 1) btns.push({ + type: 'BUTTON', + label: '◀ ' + localize('helpers', 'back'), + customId: 'back', + style: 'PRIMARY', + disabled + }); + if (site !== sites.length) btns.push({ + type: 'BUTTON', + label: localize('helpers', 'next') + ' ▶', + customId: 'next', + style: 'PRIMARY', + disabled + }); + return btns; + } +} + +module.exports.sendMultipleSiteButtonMessage = sendMultipleSiteButtonMessage; + +/** + * Compares two arrays + * @param {Array} array1 First array + * @param {Array} array2 Second array + * @returns {boolean} Wherever the arrays are the same + */ +function compareArrays(array1, array2) { + if (array1.length !== array2.length) return false; + + for (let i = 0, l = array1.length; i < l; i++) { + if (array1[i] instanceof Object) { + for (const key in array1[i]) { + if (array2[key] !== array1[key]) return false; + } + continue; + } + if (!array2.includes(array1[i])) return false; + } + return true; +} + +module.exports.compareArrays = compareArrays; + +/** + * Check if a new version of CustomDCBot is available in the main branch on github + * @returns {Promise} + */ +async function checkForUpdates() { +} + +module.exports.checkForUpdates = checkForUpdates; + +/** + * Randomly selects a number between min and max + * @param {Number} min + * @param {Number} max + * @returns {number} Random integer + */ +function randomIntFromInterval(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +module.exports.randomIntFromInterval = randomIntFromInterval; + +/** + * Returns a random element from an array + * @param {Array} array Array of values + * @returns {*} + */ +function randomElementFromArray(array) { + if (array.length === 0) return null; + if (array.length === 1) return array[0]; + return array[Math.floor(Math.random() * array.length)]; +} + +module.exports.randomElementFromArray = randomElementFromArray; + +/** + * Returns a string (progressbar) to visualize a progress in percentage + * @param {Number} percentage Percentage of progress + * @param {Number} length Length of the whole progressbar + * @return {string} Progressbar + */ +function renderProgressbar(percentage, length = 20) { + let s = ''; + for (let i = 1; i <= length; i++) { + if (percentage >= 5 * i) s = s + '█'; + else s = s + '░'; + } + return s; +} + +module.exports.renderProgressbar = renderProgressbar; + +/** + * Formats a Date to a discord timestamp + * @param {Date} date Date to convert + * @param {String} timeStampStyle [Timestamp Style](https://discord.com/developers/docs/reference#message-formatting-timestamp-styles) in which this timeStamp should be + * @return {string} Discord-Timestamp + */ +function dateToDiscordTimestamp(date, timeStampStyle = null) { + return ``; +} + +module.exports.dateToDiscordTimestamp = dateToDiscordTimestamp; + +/** + * Locks a Guild-Channel for everyone except roles specified in allowedRoles + * @param {GuildChannel} channel Channel to lock + * @param {Array} allowedRoles Array of roles who can talk in the channel + * @param {String} reason Reason for the channel lock + * @return {Promise} + */ +async function lockChannel(channel, allowedRoles = [], reason = localize('main', 'channel-lock')) { + const dup = await channel.client.models['ChannelLock'].findOne({where: {id: channel.id}}); + if (dup) await dup.destroy(); + await channel.client.models['ChannelLock'].create({ + id: channel.id, + lockReason: reason, + permissions: Array.from(channel.permissionOverwrites.cache.values()) + }); + + for (const overwrite of channel.permissionOverwrites.cache.filter(e => e.allow.has('SEND_MESSAGES')).values()) { + await overwrite.edit({ + SEND_MESSAGES: false, + SEND_MESSAGES_IN_THREADS: false + }, reason); + } + + const everyoneRole = await channel.guild.roles.cache.find(r => r.name === '@everyone'); + if (channel.permissionsFor(everyoneRole).has('VIEW_CHANNEL')) await channel.permissionOverwrites.create(everyoneRole, { + SEND_MESSAGES: false, + SEND_MESSAGES_IN_THREADS: false + }, {reason}); + + for (const roleID of allowedRoles) { + await channel.permissionOverwrites.create(roleID, { + SEND_MESSAGES: true + }, {reason}); + } +} + +/** + * Unlocks a previously locked channel + * @param {GuildChannel} channel Channel to unlock + * @param {String} reason Reason for this unlock + * @return {Promise} + */ +async function unlockChannel(channel, reason = localize('main', 'channel-unlock')) { + const item = await channel.client.models['ChannelLock'].findOne({where: {id: channel.id}}); + if (item && (item || {}).permissions) await channel.permissionOverwrites.set(item.permissions, reason); + else channel.client.logger.error(localize('main', 'channel-unlock-data-not-found', {c: channel.id})); +} + +module.exports.lockChannel = lockChannel; +module.exports.unlockChannel = unlockChannel; + +/** + * Function to migrate Database models + * @param {string} module Name of the Module + * @param {string} oldModel Name of the old Model + * @param {string} newModel Name of the new Model + * @returns {Promise} + * @author jateute + */ +async function migrate(module, oldModel, newModel) { + const old = await client.models[module][oldModel].findAll(); + if (old.length === 0) return; + client.logger.info(localize('main', 'migrate-start', {o: oldModel, m: newModel})); + for (const model of old) { + delete model.dataValues.updatedAt; + delete model.dataValues.createdAt; + await client.models[module][newModel].create(model.dataValues); + await model.destroy(); + } + client.logger.info(localize('main', 'migrate-success', {o: oldModel, m: newModel})); +} + +module.exports.migrate = migrate; + +/** + * Disables a module. NOTE: This can't and won't clear any set intervals or jobs + * @param {String} module Name of the module to disable + * @param {String} reason Reason why module should gets disabled. + */ +function disableModule(module, reason = null) { + if (!client.modules[module]) throw new Error(`${module} got never loaded`); + client.modules[module].enabled = false; + client.logger.error(localize('main', 'module-disable', {r: reason, m: module})); + if (client.logChannel) client.logChannel.send(localize('main', 'module-disable', { + m: module, + r: reason + })).then(() => { + }); + if (client.scnxSetup) require('./scnx-integration').reportIssue(client, { + type: 'MODULE_FAILURE', + errorDescription: 'module_disabled', + errorData: {reason}, + module + }).then(() => { + }); +} + +module.exports.disableModule = disableModule; + +/** + * Formates a number to make it human-readable + * @param {Number|string} number + * @returns {string} + */ +module.exports.formatNumber = function (number) { + if (typeof number === 'string') number = parseInt(number); + return new Intl.NumberFormat(client.locale, {}).format(number); +}; + +/** + * Creates a MD5 Hash String from a string + * @param {String} string String to hash + * @return {string} MD5 Hash String + */ +module.exports.hashMD5 = function (string) { + return crypto.createHash('md5').update(string).digest('hex'); +}; \ No newline at end of file diff --git a/src/functions/localize.js b/src/functions/localize.js new file mode 100644 index 00000000..b241f742 --- /dev/null +++ b/src/functions/localize.js @@ -0,0 +1,43 @@ +/** + * This module can fetch, update and get translations of strings + * @module Locales + */ +const {client} = require('../../main'); +const jsonfile = require('jsonfile'); +const fs = require('fs'); + +const locals = {}; +loadLocale('en'); + +/** + * Loads a locale file + * @private + * @param {String} locale Locale to load + */ +function loadLocale(locale) { + if (locals[locale]) return; + if (!fs.existsSync(`${__dirname}/../../locales/${locale}.json`)) locale = 'en'; + locals[locale] = jsonfile.readFileSync(`${__dirname}/../../locales/${locale}.json`); +} + +/** + * Gets the translation for a string + * @param {String} file File-Name + * @param {String} string Localization-String-Name + * @param {Object} replace Object of parameters to replace + * @return {String} Translation in the user's language + */ +function localize(file, string, replace = {}) { + loadLocale(client.locale); + if (!locals[client.locale]) client.locale = 'en'; + if (!locals[client.locale][file]) locals[client.locale][file] = {}; + let rs = locals[client.locale][file][string]; + if (!rs) rs = locals['en'][file][string]; + if (!rs) throw new Error(`String ${file}/${string} not found`); + for (const key in replace) { + rs = rs.replaceAll(`%${key}`, replace[key]); + } + return rs; +} + +module.exports.localize = localize; \ No newline at end of file diff --git a/src/gen-doc/Client.js b/src/gen-doc/Client.js new file mode 100644 index 00000000..930effbb --- /dev/null +++ b/src/gen-doc/Client.js @@ -0,0 +1,97 @@ +/** + * The bot client. Extends [discord.js's Client](https://discord.js.org/#/docs/main/stable/class/Client). This file only exists for documentation-purposes and is intended to be used in any other way. + */ +class Client { + constructor() { + /** + * Timestamp on which the bot is ready + * @type {Date} + */ + this.botReadyAt = null; + /** + * [TextChannel](https://discord.js.org/#/docs/main/stable/class/TextChannel) which should be used as default log-channel and in which some basic information gets send. ⚠️️ In some cases this value is `null` so always catch or check the value before any calls on this property. + * @type {TextChannel} + */ + this.logChannel = null; + /** + * Object of all models, mapped by module + * @type {Object} + */ + this.models = null; + /** + * Content of the `strings.json` file + * @type {Object} + */ + this.strings = null; + /** + * Content of the `modules.json` file. + * @type {Object} + */ + this.moduleConf = null; + /** + * Object of every module + * @type {Object} + */ + this.modules = null; + /** + * [Collection](https://discord.js.org/#/docs/collection/main/class/Collection) of every registered command + * @type {Collection} + */ + this.commands = null; + /** + * [Collection](https://discord.js.org/#/docs/collection/main/class/Collection) of every registered command alias + * @type {Collection} + */ + this.aliases = null; + /** + * [Collection](https://discord.js.org/#/docs/collection/main/class/Collection) of every registered events + * @type {Collection} + */ + this.events = null; + /** + * Array of [Intervals](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) which get cleared on config-reload to make the live of module-developers easier + * @type {Array} + */ + this.intervals = []; + /** + * Array of [Jobs](https://github.com/node-schedule/node-schedule#handle-jobs-and-job-invocations) which get canceled on config-reload to make the live of module-developers easier + * @type {Array} + */ + this.jobs = []; + /** + * ID of the guild the bot should run on + * @type {String} + */ + this.guildID = null; + /** + * The [guild](https://discord.js.org/#/docs/main/stable/class/Guild) the bot should run on + * @type {Guild} + */ + this.guild = null; + /** + * Content of `config.json` + * @type {Object} + */ + this.config = null; + /** + * Path to the configuration-directory + * @type {Path} + */ + this.configDir = null; + /** + * Path to the data-directory + * @type {Path} + */ + this.dataDir = null; + /** + * Object containing every configuration, mapped by module + * @type {Object} + */ + this.configurations = null; + /** + * Logger + * @type {Logger} + */ + this.logger = null; + } +} \ No newline at end of file diff --git a/src/models/ChannelLock.js b/src/models/ChannelLock.js new file mode 100644 index 00000000..5c8d29a5 --- /dev/null +++ b/src/models/ChannelLock.js @@ -0,0 +1,22 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class ChannelLock extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.STRING, + primaryKey: true + }, + permissions: DataTypes.JSON, + lockReason: DataTypes.STRING + }, { + tableName: 'system_ChannelLock', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'ChannelLock' +}; \ No newline at end of file diff --git a/src/models/DatabaseSchemeVersion.js b/src/models/DatabaseSchemeVersion.js new file mode 100644 index 00000000..49613764 --- /dev/null +++ b/src/models/DatabaseSchemeVersion.js @@ -0,0 +1,21 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class DatabaseSchemeVersion extends Model { + static init(sequelize) { + return super.init({ + model: { + type: DataTypes.STRING, + primaryKey: true + }, + version: DataTypes.STRING + }, { + tableName: 'system_DatabaseSchemeVersion', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'DatabaseSchemeVersion' +}; \ No newline at end of file diff --git a/test-phishing.js b/test-phishing.js new file mode 100644 index 00000000..e468252b --- /dev/null +++ b/test-phishing.js @@ -0,0 +1,21 @@ +const { checkPhishing } = require('./modules/moderation/phishingService'); + +async function runTest() { + const testUrls = [ + 'http://paypal.com.example.tk/login', + 'http://xn--pypal-4ve.com', + 'http://example.com', + ]; + + for (const url of testUrls) { + console.log(`\nTesting URL: ${url}`); + try { + const result = await checkPhishing({ url }); + console.log('Result:', result); + } catch (err) { + console.error('Error during checkPhishing:', err); + } + } +} + +runTest();