From 14eb0e617b084080c4cffc5b781b311c65c5f928 Mon Sep 17 00:00:00 2001
From: IRONM00N <64110067+IRONM00N@users.noreply.github.com>
Date: Sun, 28 Aug 2022 21:51:17 -0400
Subject: rebrand v3
---
.eslintrc.cjs | 2 +-
.prettierrc.json | 2 +-
.vscode/launch.json | 2 +-
.vscode/settings.json | 1 +
.vscode/typescript.code-snippets | 4 +-
README.md | 31 +-
ecosystem.config.cjs | 6 +-
lib/arguments/abbreviatedNumber.ts | 4 +-
lib/arguments/contentWithDuration.ts | 4 +-
lib/arguments/discordEmoji.ts | 4 +-
lib/arguments/duration.ts | 4 +-
lib/arguments/durationSeconds.ts | 4 +-
lib/arguments/globalUser.ts | 4 +-
lib/arguments/messageLink.ts | 4 +-
lib/arguments/permission.ts | 4 +-
lib/arguments/roleWithDuration.ts | 4 +-
lib/arguments/snowflake.ts | 4 +-
lib/arguments/tinyColor.ts | 4 +-
lib/automod/AutomodShared.ts | 6 +-
lib/automod/MessageAutomod.ts | 10 +-
lib/common/BotCache.ts | 26 +
lib/common/BushCache.ts | 26 -
lib/common/HighlightManager.ts | 2 +-
.../discord-akairo/BotArgumentTypeCaster.ts | 3 +
lib/extensions/discord-akairo/BotCommand.ts | 597 ++++++++++++++++++++
lib/extensions/discord-akairo/BotCommandHandler.ts | 37 ++
lib/extensions/discord-akairo/BotInhibitor.ts | 18 +
.../discord-akairo/BotInhibitorHandler.ts | 3 +
lib/extensions/discord-akairo/BotListener.ts | 3 +
.../discord-akairo/BotListenerHandler.ts | 3 +
lib/extensions/discord-akairo/BotTask.ts | 3 +
lib/extensions/discord-akairo/BotTaskHandler.ts | 3 +
.../discord-akairo/BushArgumentTypeCaster.ts | 3 -
lib/extensions/discord-akairo/BushClient.ts | 601 --------------------
lib/extensions/discord-akairo/BushCommand.ts | 597 --------------------
.../discord-akairo/BushCommandHandler.ts | 37 --
lib/extensions/discord-akairo/BushInhibitor.ts | 19 -
.../discord-akairo/BushInhibitorHandler.ts | 3 -
lib/extensions/discord-akairo/BushListener.ts | 3 -
.../discord-akairo/BushListenerHandler.ts | 3 -
lib/extensions/discord-akairo/BushTask.ts | 3 -
lib/extensions/discord-akairo/BushTaskHandler.ts | 3 -
lib/extensions/discord-akairo/TanzaniteClient.ts | 591 ++++++++++++++++++++
lib/extensions/discord.js/BotClientEvents.ts | 211 +++++++
lib/extensions/discord.js/BushClientEvents.ts | 200 -------
lib/extensions/discord.js/ExtendedGuild.ts | 46 +-
lib/extensions/discord.js/ExtendedGuildMember.ts | 148 ++---
lib/extensions/discord.js/ExtendedUser.ts | 4 +-
lib/index.ts | 47 +-
lib/models/instance/Guild.ts | 4 +-
lib/types/BushInspectOptions.ts | 123 -----
lib/types/InspectOptions.ts | 127 +++++
lib/types/misc.ts | 14 +
lib/utils/Arg.ts | 36 +-
lib/utils/BotClientUtils.ts | 496 +++++++++++++++++
lib/utils/BushClientUtils.ts | 496 -----------------
lib/utils/BushConstants.ts | 554 -------------------
lib/utils/BushLogger.ts | 315 -----------
lib/utils/BushUtils.ts | 615 ---------------------
lib/utils/Constants.ts | 554 +++++++++++++++++++
lib/utils/ErrorHandler.ts | 8 +-
lib/utils/FormatResponse.ts | 4 +-
lib/utils/Logger.ts | 314 +++++++++++
lib/utils/Minecraft.ts | 4 -
lib/utils/Minecraft_Test.ts | 10 +-
lib/utils/Utils.ts | 615 +++++++++++++++++++++
package.json | 6 +-
src/bot.ts | 8 +-
src/commands/_fake-command/ironmoon.ts | 4 +-
src/commands/admin/channelPermissions.ts | 4 +-
src/commands/admin/roleAll.ts | 4 +-
src/commands/config/blacklist.ts | 4 +-
src/commands/config/config.ts | 4 +-
src/commands/config/disable.ts | 6 +-
src/commands/config/features.ts | 4 +-
src/commands/config/log.ts | 4 +-
src/commands/dev/__template.ts | 4 +-
src/commands/dev/dm.ts | 4 +-
src/commands/dev/eval.ts | 10 +-
src/commands/dev/javascript.ts | 4 +-
src/commands/dev/reload.ts | 4 +-
src/commands/dev/say.ts | 4 +-
src/commands/dev/servers.ts | 4 +-
src/commands/dev/sh.ts | 4 +-
src/commands/dev/superUser.ts | 4 +-
src/commands/dev/syncAutomod.ts | 4 +-
src/commands/dev/test.ts | 4 +-
src/commands/fun/coinFlip.ts | 4 +-
src/commands/fun/dice.ts | 4 +-
src/commands/fun/eightBall.ts | 4 +-
src/commands/fun/minesweeper.ts | 4 +-
src/commands/info/avatar.ts | 4 +-
src/commands/info/botInfo.ts | 4 +-
src/commands/info/color.ts | 4 +-
src/commands/info/guildInfo.ts | 16 +-
src/commands/info/help.ts | 16 +-
src/commands/info/icon.ts | 4 +-
src/commands/info/inviteInfo.ts | 4 +-
src/commands/info/links.ts | 4 +-
src/commands/info/ping.ts | 4 +-
src/commands/info/pronouns.ts | 4 +-
src/commands/info/snowflake.ts | 8 +-
src/commands/info/userInfo.ts | 4 +-
src/commands/leveling/leaderboard.ts | 4 +-
src/commands/leveling/level.ts | 4 +-
src/commands/leveling/levelRoles.ts | 4 +-
src/commands/leveling/setLevel.ts | 4 +-
src/commands/leveling/setXp.ts | 4 +-
src/commands/moderation/_activePunishments.ts | 4 +-
src/commands/moderation/ban.ts | 6 +-
src/commands/moderation/block.ts | 6 +-
src/commands/moderation/evidence.ts | 7 +-
src/commands/moderation/hideCase.ts | 7 +-
src/commands/moderation/kick.ts | 6 +-
src/commands/moderation/lockdown.ts | 4 +-
src/commands/moderation/massBan.ts | 13 +-
src/commands/moderation/massEvidence.ts | 7 +-
src/commands/moderation/modlog.ts | 4 +-
src/commands/moderation/mute.ts | 6 +-
src/commands/moderation/myLogs.ts | 4 +-
src/commands/moderation/purge.ts | 7 +-
src/commands/moderation/removeReactionEmoji.ts | 4 +-
src/commands/moderation/role.ts | 6 +-
src/commands/moderation/slowmode.ts | 4 +-
src/commands/moderation/timeout.ts | 6 +-
src/commands/moderation/unban.ts | 6 +-
src/commands/moderation/unblock.ts | 6 +-
src/commands/moderation/unlockdown.ts | 4 +-
src/commands/moderation/unmute.ts | 6 +-
src/commands/moderation/untimeout.ts | 6 +-
src/commands/moderation/warn.ts | 6 +-
src/commands/moulberry-bush/capePermissions.ts | 4 +-
src/commands/moulberry-bush/capes.ts | 4 +-
src/commands/moulberry-bush/giveawayPing.ts | 4 +-
src/commands/moulberry-bush/moulHammer.ts | 12 +-
src/commands/moulberry-bush/neuRepo.ts | 4 +-
src/commands/moulberry-bush/report.ts | 4 +-
src/commands/moulberry-bush/rule.ts | 4 +-
src/commands/moulberry-bush/serverStatus.ts | 4 +-
src/commands/moulberry-bush/solved.ts | 4 +-
src/commands/tickets/ticket-!.ts | 4 +-
src/commands/utilities/_poll.ts | 4 +-
src/commands/utilities/activity.ts | 8 +-
src/commands/utilities/calculator.ts | 4 +-
src/commands/utilities/decode.ts | 4 +-
src/commands/utilities/hash.ts | 4 +-
src/commands/utilities/highlight-!.ts | 4 +-
src/commands/utilities/highlight-add.ts | 4 +-
src/commands/utilities/highlight-block.ts | 4 +-
src/commands/utilities/highlight-clear.ts | 4 +-
src/commands/utilities/highlight-matches.ts | 4 +-
src/commands/utilities/highlight-remove.ts | 4 +-
src/commands/utilities/highlight-show.ts | 4 +-
src/commands/utilities/highlight-unblock.ts | 4 +-
src/commands/utilities/price.ts | 4 +-
src/commands/utilities/remind.ts | 4 +-
src/commands/utilities/reminders.ts | 4 +-
src/commands/utilities/steal.ts | 4 +-
src/commands/utilities/suicide.ts | 4 +-
src/commands/utilities/uuid.ts | 4 +-
src/commands/utilities/viewRaw.ts | 4 +-
src/commands/utilities/whoHasRole.ts | 4 +-
src/commands/utilities/wolframAlpha.ts | 4 +-
src/inhibitors/blacklist/channelGlobalBlacklist.ts | 7 +-
src/inhibitors/blacklist/channelGuildBlacklist.ts | 7 +-
src/inhibitors/blacklist/guildBlacklist.ts | 5 +-
src/inhibitors/blacklist/userGlobalBlacklist.ts | 5 +-
src/inhibitors/blacklist/userGuildBlacklist.ts | 5 +-
src/inhibitors/checks/fatal.ts | 5 +-
src/inhibitors/checks/guildUnavailable.ts | 5 +-
src/inhibitors/command/dm.ts | 7 +-
src/inhibitors/command/globalDisabledCommand.ts | 7 +-
src/inhibitors/command/guild.ts | 7 +-
src/inhibitors/command/guildDisabledCommand.ts | 7 +-
src/inhibitors/command/nsfw.ts | 7 +-
src/inhibitors/command/owner.ts | 7 +-
src/inhibitors/command/restrictedChannel.ts | 7 +-
src/inhibitors/command/restrictedGuild.ts | 7 +-
src/inhibitors/command/superUser.ts | 7 +-
src/listeners/automod/automodCreate.ts | 9 +-
src/listeners/automod/automodUpdate.ts | 9 +-
src/listeners/automod/memberAutomod.ts | 6 +-
src/listeners/automod/presenceAutomod.ts | 6 +-
src/listeners/bush/appealListener.ts | 9 +-
src/listeners/bush/joinAutoBan.ts | 11 +-
src/listeners/bush/supportThread.ts | 9 +-
src/listeners/bush/userUpdateAutoBan.ts | 11 +-
src/listeners/client/akairoDebug.ts | 9 +-
src/listeners/client/dcjsDebug.ts | 15 -
src/listeners/client/dcjsError.ts | 15 -
src/listeners/client/dcjsWarn.ts | 15 -
src/listeners/client/djsDebug.ts | 14 +
src/listeners/client/djsError.ts | 14 +
src/listeners/client/djsWarn.ts | 14 +
src/listeners/client/ready.ts | 6 +-
src/listeners/commands/commandBlocked.ts | 15 +-
src/listeners/commands/commandCooldown.ts | 9 +-
src/listeners/commands/commandError.ts | 9 +-
src/listeners/commands/commandLocked.ts | 9 +-
.../commands/commandMissingPermissions.ts | 13 +-
src/listeners/commands/commandStarted.ts | 9 +-
src/listeners/commands/messageBlocked.ts | 9 +-
src/listeners/commands/slashBlocked.ts | 9 +-
src/listeners/commands/slashCommandError.ts | 9 +-
src/listeners/commands/slashMissingPermissions.ts | 9 +-
src/listeners/commands/slashNotFound.ts | 9 +-
src/listeners/commands/slashStarted.ts | 9 +-
.../contextCommands/contextCommandBlocked.ts | 7 +-
.../contextCommands/contextCommandError.ts | 7 +-
.../contextCommands/contextCommandNotFound.ts | 7 +-
.../contextCommands/contextCommandStarted.ts | 7 +-
src/listeners/guild-custom/bushLockdown.ts | 33 --
src/listeners/guild-custom/bushUnlockdown.ts | 33 --
src/listeners/guild-custom/lockdown.ts | 32 ++
src/listeners/guild-custom/unlockdown.ts | 32 ++
src/listeners/guild/guildCreate.ts | 9 +-
src/listeners/guild/guildDelete.ts | 9 +-
src/listeners/guild/guildMemberAdd.ts | 9 +-
src/listeners/guild/guildMemberRemove.ts | 9 +-
src/listeners/guild/joinRoles.ts | 9 +-
src/listeners/guild/syncUnbanPunishmentModel.ts | 9 +-
src/listeners/interaction/interactionCreate.ts | 9 +-
src/listeners/member-custom/bushBan.ts | 33 --
src/listeners/member-custom/bushBlock.ts | 35 --
src/listeners/member-custom/bushKick.ts | 32 --
src/listeners/member-custom/bushLevelUpdate.ts | 64 ---
src/listeners/member-custom/bushMute.ts | 33 --
src/listeners/member-custom/bushPunishRole.ts | 32 --
.../member-custom/bushPunishRoleRemove.ts | 33 --
src/listeners/member-custom/bushPurge.ts | 44 --
src/listeners/member-custom/bushRemoveTimeout.ts | 32 --
src/listeners/member-custom/bushTimeout.ts | 33 --
src/listeners/member-custom/bushUnban.ts | 32 --
src/listeners/member-custom/bushUnblock.ts | 33 --
src/listeners/member-custom/bushUnmute.ts | 32 --
src/listeners/member-custom/bushUpdateModlog.ts | 41 --
src/listeners/member-custom/bushUpdateSettings.ts | 34 --
src/listeners/member-custom/bushWarn.ts | 32 --
src/listeners/member-custom/customBan.ts | 32 ++
src/listeners/member-custom/customBlock.ts | 36 ++
src/listeners/member-custom/customKick.ts | 31 ++
src/listeners/member-custom/customMute.ts | 32 ++
src/listeners/member-custom/customPurge.ts | 43 ++
src/listeners/member-custom/customRemoveTimeout.ts | 31 ++
src/listeners/member-custom/customTimeout.ts | 32 ++
src/listeners/member-custom/customUnban.ts | 31 ++
src/listeners/member-custom/customUnblock.ts | 32 ++
src/listeners/member-custom/customUnmute.ts | 31 ++
src/listeners/member-custom/customWarnMember.ts | 31 ++
src/listeners/member-custom/levelUpdate.ts | 63 +++
src/listeners/member-custom/massBan.ts | 11 +-
src/listeners/member-custom/massEvidence.ts | 11 +-
src/listeners/member-custom/punishRole.ts | 31 ++
src/listeners/member-custom/punishRoleRemove.ts | 32 ++
src/listeners/member-custom/updateModlog.ts | 40 ++
src/listeners/member-custom/updateSettings.ts | 33 ++
src/listeners/message/autoPublisher.ts | 9 +-
src/listeners/message/blacklistedFile.ts | 7 +-
src/listeners/message/boosterMessage.ts | 9 +-
src/listeners/message/directMessage.ts | 9 +-
src/listeners/message/highlight.ts | 9 +-
src/listeners/message/level.ts | 11 +-
src/listeners/message/quoteCreate.ts | 9 +-
src/listeners/message/quoteEdit.ts | 7 +-
src/listeners/message/verbose.ts | 9 +-
src/listeners/other/consoleListener.ts | 116 ++--
src/listeners/other/exit.ts | 4 +-
src/listeners/other/promiseRejection.ts | 4 +-
src/listeners/other/uncaughtException.ts | 4 +-
src/listeners/other/warning.ts | 4 +-
src/listeners/rest/rateLimit.ts | 7 +-
.../track-manual-punishments/modlogSyncBan.ts | 9 +-
.../track-manual-punishments/modlogSyncKick.ts | 9 +-
.../track-manual-punishments/modlogSyncTimeout.ts | 9 +-
.../track-manual-punishments/modlogSyncUnban.ts | 9 +-
src/listeners/ws/INTERACTION_CREATE.ts | 7 +-
src/tasks/cache/cpuUsage.ts | 4 +-
src/tasks/cache/updateCache.ts | 4 +-
src/tasks/cache/updateHighlightCache.ts | 5 +-
src/tasks/cache/updateNeuItemCache.ts | 4 +-
src/tasks/cache/updatePriceItemCache.ts | 4 +-
src/tasks/feature/handleReminders.ts | 4 +-
src/tasks/feature/removeExpiredPunishements.ts | 12 +-
src/tasks/feature/updateStats.ts | 4 +-
src/tasks/stats/guildCount.ts | 5 +-
src/tasks/stats/memberCount.ts | 4 +-
yarn.lock | 118 ++--
287 files changed, 5134 insertions(+), 5191 deletions(-)
create mode 100644 lib/common/BotCache.ts
delete mode 100644 lib/common/BushCache.ts
create mode 100644 lib/extensions/discord-akairo/BotArgumentTypeCaster.ts
create mode 100644 lib/extensions/discord-akairo/BotCommand.ts
create mode 100644 lib/extensions/discord-akairo/BotCommandHandler.ts
create mode 100644 lib/extensions/discord-akairo/BotInhibitor.ts
create mode 100644 lib/extensions/discord-akairo/BotInhibitorHandler.ts
create mode 100644 lib/extensions/discord-akairo/BotListener.ts
create mode 100644 lib/extensions/discord-akairo/BotListenerHandler.ts
create mode 100644 lib/extensions/discord-akairo/BotTask.ts
create mode 100644 lib/extensions/discord-akairo/BotTaskHandler.ts
delete mode 100644 lib/extensions/discord-akairo/BushArgumentTypeCaster.ts
delete mode 100644 lib/extensions/discord-akairo/BushClient.ts
delete mode 100644 lib/extensions/discord-akairo/BushCommand.ts
delete mode 100644 lib/extensions/discord-akairo/BushCommandHandler.ts
delete mode 100644 lib/extensions/discord-akairo/BushInhibitor.ts
delete mode 100644 lib/extensions/discord-akairo/BushInhibitorHandler.ts
delete mode 100644 lib/extensions/discord-akairo/BushListener.ts
delete mode 100644 lib/extensions/discord-akairo/BushListenerHandler.ts
delete mode 100644 lib/extensions/discord-akairo/BushTask.ts
delete mode 100644 lib/extensions/discord-akairo/BushTaskHandler.ts
create mode 100644 lib/extensions/discord-akairo/TanzaniteClient.ts
create mode 100644 lib/extensions/discord.js/BotClientEvents.ts
delete mode 100644 lib/extensions/discord.js/BushClientEvents.ts
delete mode 100644 lib/types/BushInspectOptions.ts
create mode 100644 lib/types/InspectOptions.ts
create mode 100644 lib/types/misc.ts
create mode 100644 lib/utils/BotClientUtils.ts
delete mode 100644 lib/utils/BushClientUtils.ts
delete mode 100644 lib/utils/BushConstants.ts
delete mode 100644 lib/utils/BushLogger.ts
delete mode 100644 lib/utils/BushUtils.ts
create mode 100644 lib/utils/Constants.ts
create mode 100644 lib/utils/Logger.ts
create mode 100644 lib/utils/Utils.ts
delete mode 100644 src/listeners/client/dcjsDebug.ts
delete mode 100644 src/listeners/client/dcjsError.ts
delete mode 100644 src/listeners/client/dcjsWarn.ts
create mode 100644 src/listeners/client/djsDebug.ts
create mode 100644 src/listeners/client/djsError.ts
create mode 100644 src/listeners/client/djsWarn.ts
delete mode 100644 src/listeners/guild-custom/bushLockdown.ts
delete mode 100644 src/listeners/guild-custom/bushUnlockdown.ts
create mode 100644 src/listeners/guild-custom/lockdown.ts
create mode 100644 src/listeners/guild-custom/unlockdown.ts
delete mode 100644 src/listeners/member-custom/bushBan.ts
delete mode 100644 src/listeners/member-custom/bushBlock.ts
delete mode 100644 src/listeners/member-custom/bushKick.ts
delete mode 100644 src/listeners/member-custom/bushLevelUpdate.ts
delete mode 100644 src/listeners/member-custom/bushMute.ts
delete mode 100644 src/listeners/member-custom/bushPunishRole.ts
delete mode 100644 src/listeners/member-custom/bushPunishRoleRemove.ts
delete mode 100644 src/listeners/member-custom/bushPurge.ts
delete mode 100644 src/listeners/member-custom/bushRemoveTimeout.ts
delete mode 100644 src/listeners/member-custom/bushTimeout.ts
delete mode 100644 src/listeners/member-custom/bushUnban.ts
delete mode 100644 src/listeners/member-custom/bushUnblock.ts
delete mode 100644 src/listeners/member-custom/bushUnmute.ts
delete mode 100644 src/listeners/member-custom/bushUpdateModlog.ts
delete mode 100644 src/listeners/member-custom/bushUpdateSettings.ts
delete mode 100644 src/listeners/member-custom/bushWarn.ts
create mode 100644 src/listeners/member-custom/customBan.ts
create mode 100644 src/listeners/member-custom/customBlock.ts
create mode 100644 src/listeners/member-custom/customKick.ts
create mode 100644 src/listeners/member-custom/customMute.ts
create mode 100644 src/listeners/member-custom/customPurge.ts
create mode 100644 src/listeners/member-custom/customRemoveTimeout.ts
create mode 100644 src/listeners/member-custom/customTimeout.ts
create mode 100644 src/listeners/member-custom/customUnban.ts
create mode 100644 src/listeners/member-custom/customUnblock.ts
create mode 100644 src/listeners/member-custom/customUnmute.ts
create mode 100644 src/listeners/member-custom/customWarnMember.ts
create mode 100644 src/listeners/member-custom/levelUpdate.ts
create mode 100644 src/listeners/member-custom/punishRole.ts
create mode 100644 src/listeners/member-custom/punishRoleRemove.ts
create mode 100644 src/listeners/member-custom/updateModlog.ts
create mode 100644 src/listeners/member-custom/updateSettings.ts
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index b8ef98d..3666e7a 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -161,7 +161,7 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'prefer-template': 'warn',
'@typescript-eslint/no-this-alias': ['error', { allowDestructuring: true, allowedNames: ['that'] }],
- '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
+ '@typescript-eslint/no-unused-vars': 'off' /* ['warn', { argsIgnorePattern: '^_' }] */,
'no-implied-eval': 'off',
'@typescript-eslint/no-implied-eval': ['error'],
'deprecation/deprecation': 'warn',
diff --git a/.prettierrc.json b/.prettierrc.json
index d704546..5c947a7 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -7,7 +7,7 @@
"endOfLine": "lf",
"overrides": [
{
- "files": ["*BushClientEvents.ts"],
+ "files": ["*CustomClientEvents.ts"],
"options": {
"printWidth": 80
}
diff --git a/.vscode/launch.json b/.vscode/launch.json
index d6eb609..cce0ba4 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -3,7 +3,7 @@
{
"type": "node-terminal",
"command": "yarn start",
- "name": "BushBot",
+ "name": "Tanzanite",
"request": "launch",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/dist/src/bot.js",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 651c2c2..600c85e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -20,6 +20,7 @@
"source.organizeImports": true,
"source.format": true
},
+ "editor.formatOnSave": true,
"diffEditor.wordWrap": "on",
"editor.insertSpaces": false,
"editor.wordWrap": "on",
diff --git a/.vscode/typescript.code-snippets b/.vscode/typescript.code-snippets
index 77b2f7f..4e2bec4 100644
--- a/.vscode/typescript.code-snippets
+++ b/.vscode/typescript.code-snippets
@@ -10,8 +10,8 @@
"description": "A bot command template",
"body": [
"import {",
- "\tBushCommand,",
"\tclientSendAndPermCheck,",
+ "\tCustomCommand,",
"\temojis,",
"\ttype ArgType,",
"\ttype CommandMessage,",
@@ -20,7 +20,7 @@
"} from '#lib';",
"",
"import { ApplicationCommandOptionType } from 'discord.js';",
- "export default class ${1:CommandName} extends BushCommand {",
+ "export default class ${1:CommandName} extends CustomCommand {",
"\tpublic constructor() {",
"\t\tsuper('${2:commandId}', {",
"\t\t\taliases: ['${3:alias}'],",
diff --git a/README.md b/README.md
index 714965d..3cc7955 100644
--- a/README.md
+++ b/README.md
@@ -1,41 +1,38 @@
- BushBot
+ Tanzanite
-BushBot is an open-sourced multi-purpose moderation, and leveling bot.
+Tanzanite is a multipurpose moderation and utility bot. It contains an extensive auto-moderation system, leveling features, message highlighting, and information commands.
Set Up
@@ -61,4 +58,4 @@ If you would like to contribute to the bot feel free to open a pull request and
Credits
- discord.js - The main library used to interface with discord
-- discord-akairo - The framework the bot is built on
+- discord-akairo - The framework the bot is built on
diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs
index 3bd532f..32971cf 100644
--- a/ecosystem.config.cjs
+++ b/ecosystem.config.cjs
@@ -1,7 +1,7 @@
module.exports = {
apps: [
...['', '-beta'].map((e) => ({
- name: `bush-bot${e}`,
+ name: `tanzanite${e}`,
script: 'yarn',
args: 'start:raw',
out_file: `../bushbot${e}.log`,
@@ -22,9 +22,9 @@ module.exports = {
'user': 'pi',
'host': '192.168.1.210',
'ref': `origin/${e === 'production' ? 'master' : 'beta'}`,
- 'repo': 'https://github.com/NotEnoughUpdates/bush-bot.git',
+ 'repo': 'https://github.com/TanzaniteBot/tanzanite.git',
'path': `/code/bush-bot${e === 'beta' ? '-beta' : ''}`,
- 'post-deploy': `yarn install && yarn build && pm2 start ecosystem.config.cjs --only bush-bot${
+ 'post-deploy': `yarn install && yarn build && pm2 start ecosystem.config.cjs --only tanzanite${
e === 'beta' ? '-beta' : ''
}`
}
diff --git a/lib/arguments/abbreviatedNumber.ts b/lib/arguments/abbreviatedNumber.ts
index a7d8ce5..5fe39b5 100644
--- a/lib/arguments/abbreviatedNumber.ts
+++ b/lib/arguments/abbreviatedNumber.ts
@@ -1,9 +1,9 @@
-import type { BushArgumentTypeCaster } from '#lib';
+import type { BotArgumentTypeCaster } from '#lib';
import assert from 'assert/strict';
import numeral from 'numeral';
assert(typeof numeral === 'function');
-export const abbreviatedNumber: BushArgumentTypeCaster = (_, phrase) => {
+export const abbreviatedNumber: BotArgumentTypeCaster = (_, phrase) => {
if (!phrase) return null;
const num = numeral(phrase?.toLowerCase()).value();
diff --git a/lib/arguments/contentWithDuration.ts b/lib/arguments/contentWithDuration.ts
index 0efba39..ec015dc 100644
--- a/lib/arguments/contentWithDuration.ts
+++ b/lib/arguments/contentWithDuration.ts
@@ -1,5 +1,5 @@
-import { parseDuration, type BushArgumentTypeCaster, type ParsedDuration } from '#lib';
+import { parseDuration, type BotArgumentTypeCaster, type ParsedDuration } from '#lib';
-export const contentWithDuration: BushArgumentTypeCaster> = async (_, phrase) => {
+export const contentWithDuration: BotArgumentTypeCaster> = async (_, phrase) => {
return parseDuration(phrase);
};
diff --git a/lib/arguments/discordEmoji.ts b/lib/arguments/discordEmoji.ts
index 92d6502..0a0d168 100644
--- a/lib/arguments/discordEmoji.ts
+++ b/lib/arguments/discordEmoji.ts
@@ -1,7 +1,7 @@
-import { regex, type BushArgumentTypeCaster } from '#lib';
+import { regex, type BotArgumentTypeCaster } from '#lib';
import type { Snowflake } from 'discord.js';
-export const discordEmoji: BushArgumentTypeCaster = (_, phrase) => {
+export const discordEmoji: BotArgumentTypeCaster = (_, phrase) => {
if (!phrase) return null;
const validEmoji: RegExpExecArray | null = regex.discordEmoji.exec(phrase);
if (!validEmoji || !validEmoji.groups) return null;
diff --git a/lib/arguments/duration.ts b/lib/arguments/duration.ts
index 09dd3d5..4952dc4 100644
--- a/lib/arguments/duration.ts
+++ b/lib/arguments/duration.ts
@@ -1,5 +1,5 @@
-import { parseDuration, type BushArgumentTypeCaster } from '#lib';
+import { parseDuration, type BotArgumentTypeCaster } from '#lib';
-export const duration: BushArgumentTypeCaster = (_, phrase) => {
+export const duration: BotArgumentTypeCaster = (_, phrase) => {
return parseDuration(phrase).duration;
};
diff --git a/lib/arguments/durationSeconds.ts b/lib/arguments/durationSeconds.ts
index d8d6749..8deee5b 100644
--- a/lib/arguments/durationSeconds.ts
+++ b/lib/arguments/durationSeconds.ts
@@ -1,6 +1,6 @@
-import { parseDuration, type BushArgumentTypeCaster } from '#lib';
+import { parseDuration, type BotArgumentTypeCaster } from '#lib';
-export const durationSeconds: BushArgumentTypeCaster = (_, phrase) => {
+export const durationSeconds: BotArgumentTypeCaster = (_, phrase) => {
phrase += 's';
return parseDuration(phrase).duration;
};
diff --git a/lib/arguments/globalUser.ts b/lib/arguments/globalUser.ts
index 4324aa9..4198e3c 100644
--- a/lib/arguments/globalUser.ts
+++ b/lib/arguments/globalUser.ts
@@ -1,7 +1,7 @@
-import type { BushArgumentTypeCaster } from '#lib';
+import type { BotArgumentTypeCaster } from '#lib';
import type { User } from 'discord.js';
// resolve non-cached users
-export const globalUser: BushArgumentTypeCaster> = async (message, phrase) => {
+export const globalUser: BotArgumentTypeCaster> = async (message, phrase) => {
return message.client.users.resolve(phrase) ?? (await message.client.users.fetch(`${phrase}`).catch(() => null));
};
diff --git a/lib/arguments/messageLink.ts b/lib/arguments/messageLink.ts
index c95e42d..ffb48a0 100644
--- a/lib/arguments/messageLink.ts
+++ b/lib/arguments/messageLink.ts
@@ -1,7 +1,7 @@
-import { BushArgumentTypeCaster, regex } from '#lib';
+import { BotArgumentTypeCaster, regex } from '#lib';
import type { Message } from 'discord.js';
-export const messageLink: BushArgumentTypeCaster> = async (message, phrase) => {
+export const messageLink: BotArgumentTypeCaster> = async (message, phrase) => {
const match = new RegExp(regex.messageLink).exec(phrase);
if (!match || !match.groups) return null;
diff --git a/lib/arguments/permission.ts b/lib/arguments/permission.ts
index 98bfe74..4d09e9c 100644
--- a/lib/arguments/permission.ts
+++ b/lib/arguments/permission.ts
@@ -1,7 +1,7 @@
-import type { BushArgumentTypeCaster } from '#lib';
+import type { BotArgumentTypeCaster } from '#lib';
import { PermissionFlagsBits, type PermissionsString } from 'discord.js';
-export const permission: BushArgumentTypeCaster = (_, phrase) => {
+export const permission: BotArgumentTypeCaster = (_, phrase) => {
if (!phrase) return null;
phrase = phrase.toUpperCase().replace(/ /g, '_');
if (!(phrase in PermissionFlagsBits)) {
diff --git a/lib/arguments/roleWithDuration.ts b/lib/arguments/roleWithDuration.ts
index b97f205..9391c75 100644
--- a/lib/arguments/roleWithDuration.ts
+++ b/lib/arguments/roleWithDuration.ts
@@ -1,7 +1,7 @@
-import { Arg, BushArgumentTypeCaster, parseDuration } from '#lib';
+import { Arg, BotArgumentTypeCaster, parseDuration } from '#lib';
import type { Role } from 'discord.js';
-export const roleWithDuration: BushArgumentTypeCaster> = async (message, phrase) => {
+export const roleWithDuration: BotArgumentTypeCaster> = async (message, phrase) => {
// eslint-disable-next-line prefer-const
let { duration, content } = parseDuration(phrase);
if (content === null || content === undefined) return null;
diff --git a/lib/arguments/snowflake.ts b/lib/arguments/snowflake.ts
index b98a20f..ab0c7fc 100644
--- a/lib/arguments/snowflake.ts
+++ b/lib/arguments/snowflake.ts
@@ -1,7 +1,7 @@
-import { BushArgumentTypeCaster, regex } from '#lib';
+import { BotArgumentTypeCaster, regex } from '#lib';
import type { Snowflake } from 'discord.js';
-export const snowflake: BushArgumentTypeCaster = (_, phrase) => {
+export const snowflake: BotArgumentTypeCaster = (_, phrase) => {
if (!phrase) return null;
if (regex.snowflake.test(phrase)) return phrase;
return null;
diff --git a/lib/arguments/tinyColor.ts b/lib/arguments/tinyColor.ts
index 148c078..2eb6ab2 100644
--- a/lib/arguments/tinyColor.ts
+++ b/lib/arguments/tinyColor.ts
@@ -1,9 +1,9 @@
-import type { BushArgumentTypeCaster } from '#lib';
+import type { BotArgumentTypeCaster } from '#lib';
import assert from 'assert/strict';
import tinycolorModule from 'tinycolor2';
assert(tinycolorModule);
-export const tinyColor: BushArgumentTypeCaster = (_message, phrase) => {
+export const tinyColor: BotArgumentTypeCaster = (_message, phrase) => {
// if the phase is a number it converts it to hex incase it could be representing a color in decimal
const newPhase = isNaN(phrase as any) ? phrase : `#${Number(phrase).toString(16)}`;
return tinycolorModule(newPhase).isValid() ? newPhase : null;
diff --git a/lib/automod/AutomodShared.ts b/lib/automod/AutomodShared.ts
index 9cdb020..29b0536 100644
--- a/lib/automod/AutomodShared.ts
+++ b/lib/automod/AutomodShared.ts
@@ -1,6 +1,6 @@
import * as Moderation from '#lib/common/Moderation.js';
import { unmuteResponse } from '#lib/extensions/discord.js/ExtendedGuildMember.js';
-import { colors, emojis } from '#lib/utils/BushConstants.js';
+import { colors, emojis } from '#lib/utils/Constants.js';
import * as Format from '#lib/utils/Format.js';
import { formatUnmuteResponse } from '#lib/utils/FormatResponse.js';
import {
@@ -165,7 +165,7 @@ export async function handleAutomodInteraction(interaction: ButtonInteraction) {
const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true;
if (check !== true) return interaction.reply({ content: check, ephemeral: true });
- const result = await interaction.guild?.bushBan({
+ const result = await interaction.guild?.customBan({
user: userId,
reason,
moderator: interaction.user.id,
@@ -209,7 +209,7 @@ export async function handleAutomodInteraction(interaction: ButtonInteraction) {
const check2 = await Moderation.checkMutePermissions(interaction.guild);
if (check2 !== true) return interaction.reply({ content: formatUnmuteResponse('/', victim!, check2), ephemeral: true });
- const result = await victim.bushUnmute({
+ const result = await victim.customUnmute({
reason,
moderator: interaction.member as GuildMember,
evidence: (interaction.message as Message).url ?? undefined
diff --git a/lib/automod/MessageAutomod.ts b/lib/automod/MessageAutomod.ts
index 0abd34c..0b6ebba 100644
--- a/lib/automod/MessageAutomod.ts
+++ b/lib/automod/MessageAutomod.ts
@@ -2,8 +2,8 @@ import { stripIndent } from '#tags';
import assert from 'assert/strict';
import chalk from 'chalk';
import { EmbedBuilder, GuildTextBasedChannel, PermissionFlagsBits, type Message } from 'discord.js';
-import { colors } from '../utils/BushConstants.js';
-import { format, formatError } from '../utils/BushUtils.js';
+import { colors } from '../utils/Constants.js';
+import { format, formatError } from '../utils/Utils.js';
import { Automod, BadWordDetails, Severity } from './AutomodShared.js';
/**
@@ -202,7 +202,7 @@ export class MessageAutomod extends Automod {
}
case Severity.WARN: {
void this.message.delete().catch((e) => deleteError.bind(this, e));
- void this.member.bushWarn({
+ void this.member.customWarn({
moderator: this.guild!.members.me!,
reason: `[Automod] ${highestOffense.reason}`
});
@@ -211,7 +211,7 @@ export class MessageAutomod extends Automod {
}
case Severity.TEMP_MUTE: {
void this.message.delete().catch((e) => deleteError.bind(this, e));
- void this.member.bushMute({
+ void this.member.customMute({
moderator: this.guild!.members.me!,
reason: `[Automod] ${highestOffense.reason}`,
duration: 900_000 // 15 minutes
@@ -221,7 +221,7 @@ export class MessageAutomod extends Automod {
}
case Severity.PERM_MUTE: {
void this.message.delete().catch((e) => deleteError.bind(this, e));
- void this.member.bushMute({
+ void this.member.customMute({
moderator: this.guild!.members.me!,
reason: `[Automod] ${highestOffense.reason}`,
duration: 0 // permanent
diff --git a/lib/common/BotCache.ts b/lib/common/BotCache.ts
new file mode 100644
index 0000000..e91d9e5
--- /dev/null
+++ b/lib/common/BotCache.ts
@@ -0,0 +1,26 @@
+import { BadWords, GlobalModel, SharedModel, type Guild } from '#lib';
+import { Collection, type Snowflake } from 'discord.js';
+
+export class BotCache {
+ public global = new GlobalCache();
+ public shared = new SharedCache();
+ public guilds = new GuildCache();
+}
+
+export class GlobalCache implements Omit {
+ public disabledCommands: string[] = [];
+ public blacklistedChannels: Snowflake[] = [];
+ public blacklistedGuilds: Snowflake[] = [];
+ public blacklistedUsers: Snowflake[] = [];
+}
+
+export class SharedCache implements Omit {
+ public superUsers: Snowflake[] = [];
+ public privilegedUsers: Snowflake[] = [];
+ public badLinksSecret: string[] = [];
+ public badLinks: string[] = [];
+ public badWords: BadWords = {};
+ public autoBanCode: string | null = null;
+}
+
+export class GuildCache extends Collection {}
diff --git a/lib/common/BushCache.ts b/lib/common/BushCache.ts
deleted file mode 100644
index 22a13ef..0000000
--- a/lib/common/BushCache.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { BadWords, GlobalModel, SharedModel, type Guild } from '#lib';
-import { Collection, type Snowflake } from 'discord.js';
-
-export class BushCache {
- public global = new GlobalCache();
- public shared = new SharedCache();
- public guilds = new GuildCache();
-}
-
-export class GlobalCache implements Omit {
- public disabledCommands: string[] = [];
- public blacklistedChannels: Snowflake[] = [];
- public blacklistedGuilds: Snowflake[] = [];
- public blacklistedUsers: Snowflake[] = [];
-}
-
-export class SharedCache implements Omit {
- public superUsers: Snowflake[] = [];
- public privilegedUsers: Snowflake[] = [];
- public badLinksSecret: string[] = [];
- public badLinks: string[] = [];
- public badWords: BadWords = {};
- public autoBanCode: string | null = null;
-}
-
-export class GuildCache extends Collection {}
diff --git a/lib/common/HighlightManager.ts b/lib/common/HighlightManager.ts
index cc31413..ca71a83 100644
--- a/lib/common/HighlightManager.ts
+++ b/lib/common/HighlightManager.ts
@@ -10,7 +10,7 @@ import {
type Snowflake,
type TextBasedChannel
} from 'discord.js';
-import { colors, Time } from '../utils/BushConstants.js';
+import { colors, Time } from '../utils/Constants.js';
import { sanitizeInputForDiscord } from '../utils/Format.js';
const NOTIFY_COOLDOWN = 5 * Time.Minute;
diff --git a/lib/extensions/discord-akairo/BotArgumentTypeCaster.ts b/lib/extensions/discord-akairo/BotArgumentTypeCaster.ts
new file mode 100644
index 0000000..5f4f32f
--- /dev/null
+++ b/lib/extensions/discord-akairo/BotArgumentTypeCaster.ts
@@ -0,0 +1,3 @@
+import { type CommandMessage } from '#lib';
+
+export type BotArgumentTypeCaster = (message: CommandMessage, phrase: string) => R;
diff --git a/lib/extensions/discord-akairo/BotCommand.ts b/lib/extensions/discord-akairo/BotCommand.ts
new file mode 100644
index 0000000..abd945e
--- /dev/null
+++ b/lib/extensions/discord-akairo/BotCommand.ts
@@ -0,0 +1,597 @@
+import { type DiscordEmojiInfo, type RoleWithDuration } from '#args';
+import {
+ type BotArgumentTypeCaster,
+ type BotCommandHandler,
+ type BotInhibitor,
+ type BotListener,
+ type BotTask,
+ type ParsedDuration,
+ type TanzaniteClient
+} from '#lib';
+import {
+ ArgumentMatch,
+ Command,
+ CommandUtil,
+ type AkairoApplicationCommandAutocompleteOption,
+ type AkairoApplicationCommandChannelOptionData,
+ type AkairoApplicationCommandChoicesData,
+ type AkairoApplicationCommandNonOptionsData,
+ type AkairoApplicationCommandNumericOptionData,
+ type AkairoApplicationCommandOptionData,
+ type AkairoApplicationCommandSubCommandData,
+ type AkairoApplicationCommandSubGroupData,
+ type ArgumentOptions,
+ type ArgumentType,
+ type ArgumentTypeCaster,
+ type BaseArgumentType,
+ type CommandOptions,
+ type ContextMenuCommand,
+ type MissingPermissionSupplier,
+ type SlashOption,
+ type SlashResolveType
+} from 'discord-akairo';
+import {
+ Message,
+ PermissionsBitField,
+ User,
+ type ApplicationCommandOptionChoiceData,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ type ApplicationCommandOptionType,
+ type PermissionResolvable,
+ type PermissionsString,
+ type Snowflake
+} from 'discord.js';
+import _ from 'lodash';
+import { SlashMessage } from './SlashMessage.js';
+
+export interface OverriddenBaseArgumentType extends BaseArgumentType {
+ commandAlias: BotCommand | null;
+ command: BotCommand | null;
+ inhibitor: BotInhibitor | null;
+ listener: BotListener | null;
+ task: BotTask | null;
+ contextMenuCommand: ContextMenuCommand | null;
+}
+
+export interface BaseBotArgumentType extends OverriddenBaseArgumentType {
+ duration: number | null;
+ contentWithDuration: ParsedDuration;
+ permission: PermissionsString | null;
+ snowflake: Snowflake | null;
+ discordEmoji: DiscordEmojiInfo | null;
+ roleWithDuration: RoleWithDuration | null;
+ abbreviatedNumber: number | null;
+ globalUser: User | null;
+ messageLink: Message | null;
+ durationSeconds: number | null;
+ tinyColor: string | null;
+}
+
+export type BotArgumentType = keyof BaseBotArgumentType | RegExp;
+
+interface BaseBotArgumentOptions extends Omit, ExtraArgumentOptions {
+ id: string;
+ description: string;
+
+ /**
+ * The message sent for the prompt and the slash command description.
+ */
+ prompt?: string;
+
+ /**
+ * The message set for the retry prompt.
+ */
+ retry?: string;
+
+ /**
+ * Whether or not the argument is optional.
+ */
+ optional?: boolean;
+
+ /**
+ * The type used for slash commands. Set to false to disable this argument for slash commands.
+ */
+ slashType: AkairoApplicationCommandOptionData['type'] | false;
+
+ /**
+ * Allows you to get a discord resolved object
+ *
+ * ex. get the resolved member object when the type is {@link ApplicationCommandOptionType.User User}
+ */
+ slashResolve?: SlashResolveType;
+
+ /**
+ * The choices of the option for the user to pick from
+ */
+ choices?: ApplicationCommandOptionChoiceData[];
+
+ /**
+ * Whether the option is an autocomplete option
+ */
+ autocomplete?: boolean;
+
+ /**
+ * When the option type is channel, the allowed types of channels that can be selected
+ */
+ channelTypes?: AkairoApplicationCommandChannelOptionData['channelTypes'];
+
+ /**
+ * The minimum value for an {@link ApplicationCommandOptionType.Integer Integer} or {@link ApplicationCommandOptionType.Number Number} option
+ */
+ minValue?: number;
+
+ /**
+ * The maximum value for an {@link ApplicationCommandOptionType.Integer Integer} or {@link ApplicationCommandOptionType.Number Number} option
+ */
+ maxValue?: number;
+}
+
+interface ExtraArgumentOptions {
+ /**
+ * Restrict this argument to only slash or only text commands.
+ */
+ only?: 'slash' | 'text';
+
+ /**
+ * Readable type for the help command.
+ */
+ readableType?: string;
+
+ /**
+ * Whether the argument is only accessible to the owners.
+ * @default false
+ */
+ ownerOnly?: boolean;
+
+ /**
+ * Whether the argument is only accessible to the super users.
+ * @default false
+ */
+ superUserOnly?: boolean;
+}
+
+export interface BotArgumentOptions extends BaseBotArgumentOptions {
+ /**
+ * The type that the argument should be cast to.
+ * - `string` does not cast to any type.
+ * - `lowercase` makes the input lowercase.
+ * - `uppercase` makes the input uppercase.
+ * - `charCodes` transforms the input to an array of char codes.
+ * - `number` casts to a number.
+ * - `integer` casts to an integer.
+ * - `bigint` casts to a big integer.
+ * - `url` casts to an `URL` object.
+ * - `date` casts to a `Date` object.
+ * - `color` casts a hex code to an integer.
+ * - `commandAlias` tries to resolve to a command from an alias.
+ * - `command` matches the ID of a command.
+ * - `inhibitor` matches the ID of an inhibitor.
+ * - `listener` matches the ID of a listener.
+ *
+ * Possible Discord-related types.
+ * These types can be plural (add an 's' to the end) and a collection of matching objects will be used.
+ * - `user` tries to resolve to a user.
+ * - `member` tries to resolve to a member.
+ * - `relevant` tries to resolve to a relevant user, works in both guilds and DMs.
+ * - `channel` tries to resolve to a channel.
+ * - `textChannel` tries to resolve to a text channel.
+ * - `voiceChannel` tries to resolve to a voice channel.
+ * - `stageChannel` tries to resolve to a stage channel.
+ * - `threadChannel` tries to resolve a thread channel.
+ * - `role` tries to resolve to a role.
+ * - `emoji` tries to resolve to a custom emoji.
+ * - `guild` tries to resolve to a guild.
+ * - `permission` tries to resolve to a permissions.
+ *
+ * Other Discord-related types:
+ * - `message` tries to fetch a message from an ID within the channel.
+ * - `guildMessage` tries to fetch a message from an ID within the guild.
+ * - `relevantMessage` is a combination of the above, works in both guilds and DMs.
+ * - `invite` tries to fetch an invite object from a link.
+ * - `userMention` matches a mention of a user.
+ * - `memberMention` matches a mention of a guild member.
+ * - `channelMention` matches a mention of a channel.
+ * - `roleMention` matches a mention of a role.
+ * - `emojiMention` matches a mention of an emoji.
+ *
+ * Misc:
+ * - `duration` tries to parse duration in milliseconds
+ * - `contentWithDuration` tries to parse duration in milliseconds and returns the remaining content with the duration
+ * removed
+ */
+ type?: BotArgumentType | (keyof BaseBotArgumentType)[] | BotArgumentTypeCaster;
+}
+
+export interface CustomBotArgumentOptions extends BaseBotArgumentOptions {
+ /**
+ * An array of strings can be used to restrict input to only those strings, case insensitive.
+ * The array can also contain an inner array of strings, for aliases.
+ * If so, the first entry of the array will be used as the final argument.
+ *
+ * A regular expression can also be used.
+ * The evaluated argument will be an object containing the `match` and `matches` if global.
+ */
+ customType?: (string | string[])[] | RegExp | string | null;
+}
+
+export type CustomMissingPermissionSupplier = (message: CommandMessage | SlashMessage) => Promise | any;
+
+interface ExtendedCommandOptions {
+ /**
+ * Whether the command is hidden from the help command.
+ */
+ hidden?: boolean;
+
+ /**
+ * The channels the command is limited to run in.
+ */
+ restrictedChannels?: Snowflake[];
+
+ /**
+ * The guilds the command is limited to run in.
+ */
+ restrictedGuilds?: Snowflake[];
+
+ /**
+ * Show how to use the command.
+ */
+ usage: string[];
+
+ /**
+ * Examples for how to use the command.
+ */
+ examples: string[];
+
+ /**
+ * A fake command, completely hidden from the help command.
+ */
+ pseudo?: boolean;
+
+ /**
+ * Allow this command to be run in channels that are blacklisted.
+ */
+ bypassChannelBlacklist?: boolean;
+
+ /**
+ * Use instead of {@link BaseBotCommandOptions.args} when using argument generators or custom slashOptions
+ */
+ helpArgs?: ArgsInfo[];
+
+ /**
+ * Extra information about the command, displayed in the help command.
+ */
+ note?: string;
+}
+
+export interface BaseBotCommandOptions
+ extends Omit,
+ ExtendedCommandOptions {
+ /**
+ * The description of the command.
+ */
+ description: string;
+
+ /**
+ * The arguments for the command.
+ */
+ args?: BotArgumentOptions[] & CustomBotArgumentOptions[];
+
+ category: string;
+
+ /**
+ * Permissions required by the client to run this command.
+ */
+ clientPermissions: bigint | bigint[] | CustomMissingPermissionSupplier;
+
+ /**
+ * Permissions required by the user to run this command.
+ */
+ userPermissions: bigint | bigint[] | CustomMissingPermissionSupplier;
+
+ /**
+ * Whether the argument is only accessible to the owners.
+ */
+ ownerOnly?: boolean;
+
+ /**
+ * Whether the argument is only accessible to the super users.
+ */
+ superUserOnly?: boolean;
+}
+
+export type CustomCommandOptions = Omit | Omit;
+
+export interface ArgsInfo {
+ /**
+ * The name of the argument.
+ */
+ name: string;
+
+ /**
+ * The description of the argument.
+ */
+ description: string;
+
+ /**
+ * Whether the argument is optional.
+ * @default false
+ */
+ optional?: boolean;
+
+ /**
+ * Whether or not the argument has autocomplete enabled.
+ * @default false
+ */
+ autocomplete?: boolean;
+
+ /**
+ * Whether the argument is restricted a certain command.
+ * @default 'slash & text'
+ */
+ only?: 'slash & text' | 'slash' | 'text';
+
+ /**
+ * The method that arguments are matched for text commands.
+ * @default 'phrase'
+ */
+ match?: ArgumentMatch;
+
+ /**
+ * The readable type of the argument.
+ */
+ type: string;
+
+ /**
+ * If {@link match} is 'flag' or 'option', these are the flags that are matched
+ * @default []
+ */
+ flag?: string[];
+
+ /**
+ * Whether the argument is only accessible to the owners.
+ * @default false
+ */
+ ownerOnly?: boolean;
+
+ /**
+ * Whether the argument is only accessible to the super users.
+ * @default false
+ */
+ superUserOnly?: boolean;
+}
+
+export abstract class BotCommand extends Command {
+ public declare client: TanzaniteClient;
+ public declare handler: BotCommandHandler;
+ public declare description: string;
+
+ /**
+ * Show how to use the command.
+ */
+ public usage: string[];
+
+ /**
+ * Examples for how to use the command.
+ */
+ public examples: string[];
+
+ /**
+ * The options sent to the constructor
+ */
+ public options: CustomCommandOptions;
+
+ /**
+ * The options sent to the super call
+ */
+ public parsedOptions: CommandOptions;
+
+ /**
+ * The channels the command is limited to run in.
+ */
+ public restrictedChannels: Snowflake[] | undefined;
+
+ /**
+ * The guilds the command is limited to run in.
+ */
+ public restrictedGuilds: Snowflake[] | undefined;
+
+ /**
+ * Whether the command is hidden from the help command.
+ */
+ public hidden: boolean;
+
+ /**
+ * A fake command, completely hidden from the help command.
+ */
+ public pseudo: boolean;
+
+ /**
+ * Allow this command to be run in channels that are blacklisted.
+ */
+ public bypassChannelBlacklist: boolean;
+
+ /**
+ * Info about the arguments for the help command.
+ */
+ public argsInfo?: ArgsInfo[];
+
+ /**
+ * Extra information about the command, displayed in the help command.
+ */
+ public note?: string;
+
+ public constructor(id: string, options: CustomCommandOptions) {
+ const options_ = options as BaseBotCommandOptions;
+
+ if (options_.args && typeof options_.args !== 'function') {
+ options_.args.forEach((_, index: number) => {
+ if ('customType' in (options_.args?.[index] ?? {})) {
+ if (!options_.args![index]['type']) options_.args![index]['type'] = options_.args![index]['customType']! as any;
+ delete options_.args![index]['customType'];
+ }
+ });
+ }
+
+ const newOptions: Partial = {};
+ for (const _key in options_) {
+ const key = _key as keyof typeof options_; // you got to love typescript
+ if (key === 'args' && 'args' in options_ && typeof options_.args === 'object') {
+ const newTextArgs: (ArgumentOptions & ExtraArgumentOptions)[] = [];
+ const newSlashArgs: SlashOption[] = [];
+ for (const arg of options_.args) {
+ if (arg.only !== 'slash' && !options_.slashOnly) {
+ const newArg: ArgumentOptions & ExtraArgumentOptions = {};
+ if ('default' in arg) newArg.default = arg.default;
+ if ('description' in arg) newArg.description = arg.description;
+ if ('flag' in arg) newArg.flag = arg.flag;
+ if ('id' in arg) newArg.id = arg.id;
+ if ('index' in arg) newArg.index = arg.index;
+ if ('limit' in arg) newArg.limit = arg.limit;
+ if ('match' in arg) newArg.match = arg.match;
+ if ('modifyOtherwise' in arg) newArg.modifyOtherwise = arg.modifyOtherwise;
+ if ('multipleFlags' in arg) newArg.multipleFlags = arg.multipleFlags;
+ if ('otherwise' in arg) newArg.otherwise = arg.otherwise;
+ if ('prompt' in arg || 'retry' in arg || 'optional' in arg) {
+ newArg.prompt = {};
+ if ('prompt' in arg) newArg.prompt.start = arg.prompt;
+ if ('retry' in arg) newArg.prompt.retry = arg.retry;
+ if ('optional' in arg) newArg.prompt.optional = arg.optional;
+ }
+ if ('type' in arg) newArg.type = arg.type as ArgumentType | ArgumentTypeCaster;
+ if ('unordered' in arg) newArg.unordered = arg.unordered;
+ if ('ownerOnly' in arg) newArg.ownerOnly = arg.ownerOnly;
+ if ('superUserOnly' in arg) newArg.superUserOnly = arg.superUserOnly;
+ newTextArgs.push(newArg);
+ }
+ if (
+ arg.only !== 'text' &&
+ !('slashOptions' in options_) &&
+ (options_.slash || options_.slashOnly) &&
+ arg.slashType !== false
+ ) {
+ const newArg: {
+ [key in SlashOptionKeys]?: any;
+ } = {
+ name: arg.id,
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ description: arg.prompt || arg.description || 'No description provided.',
+ type: arg.slashType
+ };
+ if ('slashResolve' in arg) newArg.resolve = arg.slashResolve;
+ if ('autocomplete' in arg) newArg.autocomplete = arg.autocomplete;
+ if ('channelTypes' in arg) newArg.channelTypes = arg.channelTypes;
+ if ('choices' in arg) newArg.choices = arg.choices;
+ if ('minValue' in arg) newArg.minValue = arg.minValue;
+ if ('maxValue' in arg) newArg.maxValue = arg.maxValue;
+ newArg.required = 'optional' in arg ? !arg.optional : true;
+ newSlashArgs.push(newArg as SlashOption);
+ }
+ }
+ if (newTextArgs.length > 0) newOptions.args = newTextArgs;
+ if (newSlashArgs.length > 0) newOptions.slashOptions = options_.slashOptions ?? newSlashArgs;
+ } else if (key === 'clientPermissions' || key === 'userPermissions') {
+ newOptions[key] = options_[key] as PermissionResolvable | PermissionResolvable[] | MissingPermissionSupplier;
+ } else {
+ newOptions[key] = options_[key];
+ }
+ }
+
+ if (
+ 'userPermissions' in newOptions &&
+ !('slashDefaultMemberPermissions' in newOptions) &&
+ typeof newOptions.userPermissions !== 'function'
+ ) {
+ const perms = new PermissionsBitField(newOptions.userPermissions);
+
+ newOptions.slashDefaultMemberPermissions = perms.toArray().length === 0 ? null : perms;
+ }
+
+ super(id, newOptions);
+
+ if (options_.args ?? options_.helpArgs) {
+ const argsInfo: ArgsInfo[] = [];
+ const combined = (options_.args ?? options_.helpArgs)!.map((arg) => {
+ const norm = options_.args
+ ? options_.args.find((_arg) => _arg.id === ('id' in arg ? arg.id : arg.name)) ?? ({} as BotArgumentOptions)
+ : ({} as BotArgumentOptions);
+ const help = options_.helpArgs
+ ? options_.helpArgs.find((_arg) => _arg.name === ('id' in arg ? arg.id : arg.name)) ?? ({} as ArgsInfo)
+ : ({} as ArgsInfo);
+ return { ...norm, ...help };
+ });
+
+ for (const arg of combined) {
+ const name = _.camelCase('id' in arg ? arg.id : arg.name),
+ description = arg.description || '*No description provided.*',
+ optional = arg.optional ?? false,
+ autocomplete = arg.autocomplete ?? false,
+ only = arg.only ?? 'slash & text',
+ match = arg.match ?? 'phrase',
+ type = match === 'flag' ? 'flag' : arg.readableType ?? arg.type ?? 'string',
+ flag = arg.flag ? (Array.isArray(arg.flag) ? arg.flag : [arg.flag]) : [],
+ ownerOnly = arg.ownerOnly ?? false,
+ superUserOnly = arg.superUserOnly ?? false;
+
+ argsInfo.push({ name, description, optional, autocomplete, only, match, type, flag, ownerOnly, superUserOnly });
+ }
+
+ this.argsInfo = argsInfo;
+ }
+
+ this.description = options_.description;
+ this.usage = options_.usage;
+ this.examples = options_.examples;
+ this.options = options_;
+ this.parsedOptions = newOptions;
+ this.hidden = !!options_.hidden;
+ this.restrictedChannels = options_.restrictedChannels;
+ this.restrictedGuilds = options_.restrictedGuilds;
+ this.pseudo = !!options_.pseudo;
+ this.bypassChannelBlacklist = !!options_.bypassChannelBlacklist;
+ this.note = options_.note;
+ }
+
+ /**
+ * Executes the command.
+ * @param message - Message that triggered the command.
+ * @param args - Evaluated arguments.
+ */
+ public abstract override exec(message: CommandMessage, args: any): any;
+ /**
+ * Executes the command.
+ * @param message - Message that triggered the command.
+ * @param args - Evaluated arguments.
+ */
+ public abstract override exec(message: CommandMessage | SlashMessage, args: any): any;
+}
+
+type SlashOptionKeys =
+ | keyof AkairoApplicationCommandSubGroupData
+ | keyof AkairoApplicationCommandNonOptionsData
+ | keyof AkairoApplicationCommandChannelOptionData
+ | keyof AkairoApplicationCommandChoicesData
+ | keyof AkairoApplicationCommandAutocompleteOption
+ | keyof AkairoApplicationCommandNumericOptionData
+ | keyof AkairoApplicationCommandSubCommandData;
+
+interface PseudoArguments extends BaseBotArgumentType {
+ boolean: boolean;
+ flag: boolean;
+ regex: { match: RegExpMatchArray; matches: RegExpExecArray[] };
+}
+
+export type ArgType = NonNullable;
+export type OptArgType = PseudoArguments[T];
+
+/**
+ * `util` is always defined for messages after `'all'` inhibitors
+ */
+export type CommandMessage = Message & {
+ /**
+ * Extra properties applied to the Discord.js message object.
+ * Utilities for command responding.
+ * Available on all messages after 'all' inhibitors and built-in inhibitors (bot, client).
+ * Not all properties of the util are available, depending on the input.
+ * */
+ util: CommandUtil;
+};
diff --git a/lib/extensions/discord-akairo/BotCommandHandler.ts b/lib/extensions/discord-akairo/BotCommandHandler.ts
new file mode 100644
index 0000000..8a4fe60
--- /dev/null
+++ b/lib/extensions/discord-akairo/BotCommandHandler.ts
@@ -0,0 +1,37 @@
+import { type BotCommand, type CommandMessage, type SlashMessage } from '#lib';
+import { CommandHandler, type Category, type CommandHandlerEvents, type CommandHandlerOptions } from 'discord-akairo';
+import { type Collection, type Message, type PermissionsString } from 'discord.js';
+
+export type CustomCommandHandlerOptions = CommandHandlerOptions;
+
+export interface BotCommandHandlerEvents extends CommandHandlerEvents {
+ commandBlocked: [message: CommandMessage, command: BotCommand, reason: string];
+ commandBreakout: [message: CommandMessage, command: BotCommand, /* no util */ breakMessage: Message];
+ commandCancelled: [message: CommandMessage, command: BotCommand, /* no util */ retryMessage?: Message];
+ commandFinished: [message: CommandMessage, command: BotCommand, args: any, returnValue: any];
+ commandInvalid: [message: CommandMessage, command: BotCommand];
+ commandLocked: [message: CommandMessage, command: BotCommand];
+ commandStarted: [message: CommandMessage, command: BotCommand, args: any];
+ cooldown: [message: CommandMessage | SlashMessage, command: BotCommand, remaining: number];
+ error: [error: Error, message: /* no util */ Message, command?: BotCommand];
+ inPrompt: [message: /* no util */ Message];
+ load: [command: BotCommand, isReload: boolean];
+ messageBlocked: [message: /* no util */ Message | CommandMessage | SlashMessage, reason: string];
+ messageInvalid: [message: CommandMessage];
+ missingPermissions: [message: CommandMessage, command: BotCommand, type: 'client' | 'user', missing: PermissionsString[]];
+ remove: [command: BotCommand];
+ slashBlocked: [message: SlashMessage, command: BotCommand, reason: string];
+ slashError: [error: Error, message: SlashMessage, command: BotCommand];
+ slashFinished: [message: SlashMessage, command: BotCommand, args: any, returnValue: any];
+ slashMissingPermissions: [message: SlashMessage, command: BotCommand, type: 'client' | 'user', missing: PermissionsString[]];
+ slashStarted: [message: SlashMessage, command: BotCommand, args: any];
+}
+
+export class BotCommandHandler extends CommandHandler {
+ public declare modules: Collection;
+ public declare categories: Collection>;
+}
+
+export interface BotCommandHandler extends CommandHandler {
+ findCommand(name: string): BotCommand;
+}
diff --git a/lib/extensions/discord-akairo/BotInhibitor.ts b/lib/extensions/discord-akairo/BotInhibitor.ts
new file mode 100644
index 0000000..d134eab
--- /dev/null
+++ b/lib/extensions/discord-akairo/BotInhibitor.ts
@@ -0,0 +1,18 @@
+import { type BotCommand, type CommandMessage, type SlashMessage } from '#lib';
+import { Inhibitor } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export abstract class BotInhibitor extends Inhibitor {
+ /**
+ * Checks if message should be blocked.
+ * A return value of true will block the message.
+ * If returning a Promise, a resolved value of true will block the message.
+ *
+ * **Note:** `'all'` type inhibitors do not have {@link Message.util} defined.
+ *
+ * @param message - Message being handled.
+ * @param command - Command to check.
+ */
+ public abstract override exec(message: CommandMessage, command: BotCommand): any;
+ public abstract override exec(message: CommandMessage | SlashMessage, command: BotCommand): any;
+}
diff --git a/lib/extensions/discord-akairo/BotInhibitorHandler.ts b/lib/extensions/discord-akairo/BotInhibitorHandler.ts
new file mode 100644
index 0000000..c6f318d
--- /dev/null
+++ b/lib/extensions/discord-akairo/BotInhibitorHandler.ts
@@ -0,0 +1,3 @@
+import { InhibitorHandler } from 'discord-akairo';
+
+export class BotInhibitorHandler extends InhibitorHandler {}
diff --git a/lib/extensions/discord-akairo/BotListener.ts b/lib/extensions/discord-akairo/BotListener.ts
new file mode 100644
index 0000000..f4bfd6c
--- /dev/null
+++ b/lib/extensions/discord-akairo/BotListener.ts
@@ -0,0 +1,3 @@
+import { Listener } from 'discord-akairo';
+
+export abstract class BotListener extends Listener {}
diff --git a/lib/extensions/discord-akairo/BotListenerHandler.ts b/lib/extensions/discord-akairo/BotListenerHandler.ts
new file mode 100644
index 0000000..9b3b525
--- /dev/null
+++ b/lib/extensions/discord-akairo/BotListenerHandler.ts
@@ -0,0 +1,3 @@
+import { ListenerHandler } from 'discord-akairo';
+
+export class BotListenerHandler extends ListenerHandler {}
diff --git a/lib/extensions/discord-akairo/BotTask.ts b/lib/extensions/discord-akairo/BotTask.ts
new file mode 100644
index 0000000..09b30ed
--- /dev/null
+++ b/lib/extensions/discord-akairo/BotTask.ts
@@ -0,0 +1,3 @@
+import { Task } from 'discord-akairo';
+
+export abstract class BotTask extends Task {}
diff --git a/lib/extensions/discord-akairo/BotTaskHandler.ts b/lib/extensions/discord-akairo/BotTaskHandler.ts
new file mode 100644
index 0000000..b522f2c
--- /dev/null
+++ b/lib/extensions/discord-akairo/BotTaskHandler.ts
@@ -0,0 +1,3 @@
+import { TaskHandler } from 'discord-akairo';
+
+export class BotTaskHandler extends TaskHandler {}
diff --git a/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts b/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts
deleted file mode 100644
index def7ad6..0000000
--- a/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { type CommandMessage } from '#lib';
-
-export type BushArgumentTypeCaster = (message: CommandMessage, phrase: string) => R;
diff --git a/lib/extensions/discord-akairo/BushClient.ts b/lib/extensions/discord-akairo/BushClient.ts
deleted file mode 100644
index b311ffd..0000000
--- a/lib/extensions/discord-akairo/BushClient.ts
+++ /dev/null
@@ -1,601 +0,0 @@
-import {
- abbreviatedNumber,
- contentWithDuration,
- discordEmoji,
- duration,
- durationSeconds,
- globalUser,
- messageLink,
- permission,
- roleWithDuration,
- snowflake
-} from '#args';
-import type { Config } from '#config';
-import { BushClientEvents, emojis, formatError, inspect, updateEveryCache } from '#lib';
-import { patch, type PatchedElements } from '@notenoughupdates/events-intercept';
-import * as Sentry from '@sentry/node';
-import {
- AkairoClient,
- ArgumentTypeCaster,
- ContextMenuCommandHandler,
- version as akairoVersion,
- type ArgumentPromptData,
- type OtherwiseContentSupplier
-} from 'discord-akairo';
-import {
- ActivityType,
- GatewayIntentBits,
- MessagePayload,
- Options,
- Partials,
- Structures,
- version as discordJsVersion,
- type Awaitable,
- type If,
- type InteractionReplyOptions,
- type Message,
- type MessageEditOptions,
- type MessageOptions,
- type ReplyMessageOptions,
- type Snowflake,
- type UserResolvable,
- type WebhookEditMessageOptions
-} from 'discord.js';
-import type EventEmitter from 'events';
-import { google } from 'googleapis';
-import path from 'path';
-import readline from 'readline';
-import { Options as SequelizeOptions, Sequelize, Sequelize as SequelizeType } from 'sequelize';
-import { fileURLToPath } from 'url';
-import { tinyColor } from '../../arguments/tinyColor.js';
-import { BushCache } from '../../common/BushCache.js';
-import { HighlightManager } from '../../common/HighlightManager.js';
-import {
- ActivePunishment,
- Global,
- Guild as GuildModel,
- GuildCount,
- Highlight,
- Level,
- MemberCount,
- ModLog,
- Reminder,
- Shared,
- Stat,
- StickyRole
-} from '../../models/index.js';
-import { AllowedMentions } from '../../utils/AllowedMentions.js';
-import { BushClientUtils } from '../../utils/BushClientUtils.js';
-import { BushLogger } from '../../utils/BushLogger.js';
-import { ExtendedGuild } from '../discord.js/ExtendedGuild.js';
-import { ExtendedGuildMember } from '../discord.js/ExtendedGuildMember.js';
-import { ExtendedMessage } from '../discord.js/ExtendedMessage.js';
-import { ExtendedUser } from '../discord.js/ExtendedUser.js';
-import { BushCommandHandler } from './BushCommandHandler.js';
-import { BushInhibitorHandler } from './BushInhibitorHandler.js';
-import { BushListenerHandler } from './BushListenerHandler.js';
-import { BushTaskHandler } from './BushTaskHandler.js';
-
-declare module 'discord.js' {
- export interface Client extends EventEmitter {
- /** The ID of the owner(s). */
- ownerID: Snowflake | Snowflake[];
- /** The ID of the superUser(s). */
- superUserID: Snowflake | Snowflake[];
- /** Whether or not the client is ready. */
- customReady: boolean;
- /** The configuration for the client. */
- readonly config: Config;
- /** Stats for the client. */
- readonly stats: BushStats;
- /** The handler for the bot's listeners. */
- readonly listenerHandler: BushListenerHandler;
- /** The handler for the bot's command inhibitors. */
- readonly inhibitorHandler: BushInhibitorHandler;
- /** The handler for the bot's commands. */
- readonly commandHandler: BushCommandHandler;
- /** The handler for the bot's tasks. */
- readonly taskHandler: BushTaskHandler;
- /** The handler for the bot's context menu commands. */
- readonly contextMenuCommandHandler: ContextMenuCommandHandler;
- /** The database connection for this instance of the bot (production, beta, or development). */
- readonly instanceDB: SequelizeType;
- /** The database connection that is shared between all instances of the bot. */
- readonly sharedDB: SequelizeType;
- /** A custom logging system for the bot. */
- readonly logger: BushLogger;
- /** Cached global and guild database data. */
- readonly cache: BushCache;
- /** Sentry error reporting for the bot. */
- readonly sentry: typeof Sentry;
- /** Manages most aspects of the highlight command */
- readonly highlightManager: HighlightManager;
- /** The perspective api */
- perspective: any;
- /** Client utilities. */
- readonly utils: BushClientUtils;
- /** A custom logging system for the bot. */
- get console(): BushLogger;
- on(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this;
- once(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this;
- emit(event: K, ...args: BushClientEvents[K]): boolean;
- off(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this;
- removeAllListeners(event?: K): this;
- /**
- * Checks if a user is the owner of this bot.
- * @param user - User to check.
- */
- isOwner(user: UserResolvable): boolean;
- /**
- * Checks if a user is a super user of this bot.
- * @param user - User to check.
- */
- isSuperUser(user: UserResolvable): boolean;
- }
-}
-
-export type ReplyMessageType = string | MessagePayload | ReplyMessageOptions;
-export type EditMessageType = string | MessageEditOptions | MessagePayload;
-export type SlashSendMessageType = string | MessagePayload | InteractionReplyOptions;
-export type SlashEditMessageType = string | MessagePayload | WebhookEditMessageOptions;
-export type SendMessageType = string | MessagePayload | MessageOptions;
-
-const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout,
- terminal: false
-});
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-
-/**
- * The main hub for interacting with the Discord API.
- */
-export class BushClient extends AkairoClient {
- public declare ownerID: Snowflake[];
- public declare superUserID: Snowflake[];
-
- /**
- * Whether or not the client is ready.
- */
- public override customReady = false;
-
- /**
- * Stats for the client.
- */
- public override readonly stats: BushStats = { cpu: undefined, commandsUsed: 0n, slashCommandsUsed: 0n };
-
- /**
- * The handler for the bot's listeners.
- */
- public override readonly listenerHandler: BushListenerHandler;
-
- /**
- * The handler for the bot's command inhibitors.
- */
- public override readonly inhibitorHandler: BushInhibitorHandler;
-
- /**
- * The handler for the bot's commands.
- */
- public override readonly commandHandler: BushCommandHandler;
-
- /**
- * The handler for the bot's tasks.
- */
- public override readonly taskHandler: BushTaskHandler;
-
- /**
- * The handler for the bot's context menu commands.
- */
- public override readonly contextMenuCommandHandler: ContextMenuCommandHandler;
-
- /**
- * The database connection for this instance of the bot (production, beta, or development).
- */
- public override readonly instanceDB: SequelizeType;
-
- /**
- * The database connection that is shared between all instances of the bot.
- */
- public override readonly sharedDB: SequelizeType;
-
- /**
- * A custom logging system for the bot.
- */
- public override readonly logger: BushLogger = new BushLogger(this);
-
- /**
- * Cached global and guild database data.
- */
- public override readonly cache = new BushCache();
-
- /**
- * Sentry error reporting for the bot.
- */
- public override readonly sentry!: typeof Sentry;
-
- /**
- * Manages most aspects of the highlight command
- */
- public override readonly highlightManager: HighlightManager = new HighlightManager(this);
-
- /**
- * The perspective api
- */
- public override perspective: any;
-
- /**
- * Client utilities.
- */
- public override readonly utils: BushClientUtils = new BushClientUtils(this);
-
- /**
- * @param config The configuration for the client.
- */
- public constructor(
- /**
- * The configuration for the client.
- */
- public override readonly config: Config
- ) {
- super({
- ownerID: config.owners,
- intents: Object.keys(GatewayIntentBits)
- .map((i) => (typeof i === 'string' ? GatewayIntentBits[i as keyof typeof GatewayIntentBits] : i))
- .reduce((acc, p) => acc | p, 0),
- partials: Object.keys(Partials).map((p) => Partials[p as keyof typeof Partials]),
- presence: {
- activities: [{ name: 'Beep Boop', type: ActivityType.Watching }],
- status: 'online'
- },
- allowedMentions: AllowedMentions.none(), // no mentions by default
- makeCache: Options.cacheWithLimits({
- PresenceManager: {
- maxSize: 0,
- keepOverLimit: (_, key) => {
- if (config.owners.includes(key)) return true;
-
- return HighlightManager.keep.has(key);
- }
- }
- }),
- failIfNotExists: false,
- rest: { api: 'https://canary.discord.com/api' }
- });
- patch(this);
-
- this.token = config.token as If;
-
- /* =-=-= handlers =-=-= */
- this.listenerHandler = new BushListenerHandler(this, {
- directory: path.join(__dirname, '..', '..', '..', 'src', 'listeners'),
- extensions: ['.js'],
- automateCategories: true
- });
- this.inhibitorHandler = new BushInhibitorHandler(this, {
- directory: path.join(__dirname, '..', '..', '..', 'src', 'inhibitors'),
- extensions: ['.js'],
- automateCategories: true
- });
- this.taskHandler = new BushTaskHandler(this, {
- directory: path.join(__dirname, '..', '..', '..', 'src', 'tasks'),
- extensions: ['.js'],
- automateCategories: true
- });
-
- const modify = async (
- message: Message,
- text: string | MessagePayload | MessageOptions | OtherwiseContentSupplier,
- data: ArgumentPromptData,
- replaceError: boolean
- ) => {
- const ending = '\n\n Type **cancel** to cancel the command';
- const options = typeof text === 'function' ? await text(message, data) : text;
- const search = '{error}',
- replace = emojis.error;
-
- if (typeof options === 'string') return (replaceError ? options.replace(search, replace) : options) + ending;
-
- if (options instanceof MessagePayload) {
- if (options.options.content) {
- if (replaceError) options.options.content = options.options.content.replace(search, replace);
- options.options.content += ending;
- }
- } else if (options.content) {
- if (replaceError) options.content = options.content.replace(search, replace);
- options.content += ending;
- }
- return options;
- };
-
- this.commandHandler = new BushCommandHandler(this, {
- directory: path.join(__dirname, '..', '..', '..', 'src', 'commands'),
- extensions: ['.js'],
- prefix: async ({ guild }: Message) => {
- if (this.config.isDevelopment) return 'dev ';
- if (!guild) return this.config.prefix;
- const prefix = await guild.getSetting('prefix');
- return (prefix ?? this.config.prefix) as string;
- },
- allowMention: true,
- handleEdits: true,
- commandUtil: true,
- commandUtilLifetime: 300_000, // 5 minutes
- argumentDefaults: {
- prompt: {
- start: 'Placeholder argument prompt. **If you see this please tell my developers**.',
- retry: 'Placeholder failed argument prompt. **If you see this please tell my developers**.',
- modifyStart: (message, text, data) => modify(message, text, data, false),
- modifyRetry: (message, text, data) => modify(message, text, data, true),
- timeout: ':hourglass: You took too long the command has been cancelled.',
- ended: 'You exceeded the maximum amount of tries the command has been cancelled',
- cancel: 'The command has been cancelled',
- retries: 3,
- time: 3e4
- },
- otherwise: ''
- },
- automateCategories: false,
- autoRegisterSlashCommands: true,
- skipBuiltInPostInhibitors: true,
- aliasReplacement: /-/g
- });
- this.contextMenuCommandHandler = new ContextMenuCommandHandler(this, {
- directory: path.join(__dirname, '..', '..', '..', 'src', 'context-menu-commands'),
- extensions: ['.js'],
- automateCategories: true
- });
-
- /* =-=-= databases =-=-= */
- const sharedDBOptions: SequelizeOptions = {
- username: this.config.db.username,
- password: this.config.db.password,
- dialect: 'postgres',
- host: this.config.db.host,
- port: this.config.db.port,
- logging: this.config.logging.db ? (sql) => this.logger.debug(sql) : false,
- timezone: 'America/New_York'
- };
- this.instanceDB = new Sequelize({
- ...sharedDBOptions,
- database: this.config.isDevelopment ? 'bushbot-dev' : this.config.isBeta ? 'bushbot-beta' : 'bushbot'
- });
- this.sharedDB = new Sequelize({
- ...sharedDBOptions,
- database: 'bushbot-shared'
- });
-
- this.sentry = Sentry;
- }
-
- /**
- * A custom logging system for the bot.
- */
- public override get console(): BushLogger {
- return this.logger;
- }
-
- /**
- * Extends discord.js structures before the client is instantiated.
- */
- public static extendStructures(): void {
- Structures.extend('GuildMember', () => ExtendedGuildMember);
- Structures.extend('Guild', () => ExtendedGuild);
- Structures.extend('Message', () => ExtendedMessage);
- Structures.extend('User', () => ExtendedUser);
- }
-
- /**
- * Initializes the bot.
- */
- public async init() {
- if (parseInt(process.versions.node.split('.')[0]) < 18) {
- void (await this.console.error('version', `Please use node <>, not <<${process.version}>>.`, false));
- process.exit(2);
- }
-
- this.setMaxListeners(20);
-
- this.perspective = await google.discoverAPI('https://commentanalyzer.googleapis.com/$discovery/rest?version=v1alpha1');
-
- this.commandHandler.useInhibitorHandler(this.inhibitorHandler);
- this.commandHandler.useListenerHandler(this.listenerHandler);
- this.commandHandler.useTaskHandler(this.taskHandler);
- this.commandHandler.useContextMenuCommandHandler(this.contextMenuCommandHandler);
- this.commandHandler.ignorePermissions = this.config.owners;
- this.commandHandler.ignoreCooldown = [...new Set([...this.config.owners, ...this.cache.shared.superUsers])];
- const emitters: Emitters = {
- client: this,
- commandHandler: this.commandHandler,
- inhibitorHandler: this.inhibitorHandler,
- listenerHandler: this.listenerHandler,
- taskHandler: this.taskHandler,
- contextMenuCommandHandler: this.contextMenuCommandHandler,
- process,
- stdin: rl,
- gateway: this.ws,
- rest: this.rest,
- ws: this.ws
- };
- this.listenerHandler.setEmitters(emitters);
- this.commandHandler.resolver.addTypes({
- duration: duration,
- contentWithDuration: contentWithDuration,
- permission: permission,
- snowflake: snowflake,
- discordEmoji: discordEmoji,
- roleWithDuration: roleWithDuration,
- abbreviatedNumber: abbreviatedNumber,
- durationSeconds: durationSeconds,
- globalUser: globalUser,
- messageLink: messageLink,
- tinyColor: tinyColor
- });
-
- this.sentry.setTag('process', process.pid.toString());
- this.sentry.setTag('discord.js', discordJsVersion);
- this.sentry.setTag('discord-akairo', akairoVersion);
- void this.logger.success('startup', `Successfully connected to <>.`, false);
-
- // loads all the handlers
- const handlers = {
- commands: this.commandHandler,
- contextMenuCommands: this.contextMenuCommandHandler,
- listeners: this.listenerHandler,
- inhibitors: this.inhibitorHandler,
- tasks: this.taskHandler
- };
- const handlerPromises = Object.entries(handlers).map(([handlerName, handler]) =>
- handler
- .loadAll()
- .then(() => {
- void this.logger.success('startup', `Successfully loaded <<${handlerName}>>.`, false);
- })
- .catch((e) => {
- void this.logger.error('startup', `Unable to load loader <<${handlerName}>> with error:\n${formatError(e)}`, false);
- if (process.argv.includes('dry')) process.exit(1);
- })
- );
- await Promise.allSettled(handlerPromises);
- }
-
- /**
- * Connects to the database, initializes models, and creates tables if they do not exist.
- */
- public async dbPreInit() {
- try {
- await this.instanceDB.authenticate();
- GuildModel.initModel(this.instanceDB, this);
- ModLog.initModel(this.instanceDB);
- ActivePunishment.initModel(this.instanceDB);
- Level.initModel(this.instanceDB);
- StickyRole.initModel(this.instanceDB);
- Reminder.initModel(this.instanceDB);
- Highlight.initModel(this.instanceDB);
- await this.instanceDB.sync({ alter: true }); // Sync all tables to fix everything if updated
- await this.console.success('startup', `Successfully connected to <>.`, false);
- } catch (e) {
- await this.console.error(
- 'startup',
- `Failed to connect to <> with error:\n${inspect(e, { colors: true, depth: 1 })}`,
- false
- );
- process.exit(2);
- }
- try {
- await this.sharedDB.authenticate();
- Stat.initModel(this.sharedDB);
- Global.initModel(this.sharedDB);
- Shared.initModel(this.sharedDB);
- MemberCount.initModel(this.sharedDB);
- GuildCount.initModel(this.sharedDB);
- await this.sharedDB.sync({
- // Sync all tables to fix everything if updated
- // if another instance restarts we don't want to overwrite new changes made in development
- alter: this.config.isDevelopment
- });
- await this.console.success('startup', `Successfully connected to <>.`, false);
- } catch (e) {
- await this.console.error(
- 'startup',
- `Failed to connect to <> with error:\n${inspect(e, { colors: true, depth: 1 })}`,
- false
- );
- process.exit(2);
- }
- }
-
- /**
- * Starts the bot
- */
- public async start() {
- this.intercept('ready', async (arg, done) => {
- const promises = this.guilds.cache
- .filter((g) => g.large)
- .map((guild) => {
- return guild.members.fetch();
- });
- await Promise.all(promises);
- this.customReady = true;
- this.taskHandler.startAll();
- return done(null, `intercepted ${arg}`);
- });
-
- try {
- await this.highlightManager.syncCache();
- await updateEveryCache(this);
- void this.console.success('startup', `Successfully created <>.`, false);
-
- const stats =
- (await Stat.findByPk(this.config.environment)) ?? (await Stat.create({ environment: this.config.environment }));
- this.stats.commandsUsed = stats.commandsUsed;
- this.stats.slashCommandsUsed = stats.slashCommandsUsed;
- await this.login(this.token!);
- } catch (e) {
- await this.console.error('start', inspect(e, { colors: true, depth: 1 }), false);
- process.exit(1);
- }
- }
-
- /**
- * Logs out, terminates the connection to Discord, and destroys the client.
- */
- public override destroy(relogin = false): void | Promise {
- super.destroy();
- if (relogin) {
- return this.login(this.token!);
- }
- }
-
- public override isOwner(user: UserResolvable): boolean {
- return this.config.owners.includes(this.users.resolveId(user!)!);
- }
-
- public override isSuperUser(user: UserResolvable): boolean {
- const userID = this.users.resolveId(user)!;
- return this.cache.shared.superUsers.includes(userID) || this.config.owners.includes(userID);
- }
-}
-
-export interface BushClient extends EventEmitter, PatchedElements, AkairoClient {
- on(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this;
- once(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this;
- emit(event: K, ...args: BushClientEvents[K]): boolean;
- off(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this;
- removeAllListeners(event?: K): this;
-}
-
-/**
- * Various statistics
- */
-export interface BushStats {
- /**
- * The average cpu usage of the bot from the past 60 seconds.
- */
- cpu: number | undefined;
-
- /**
- * The total number of times any command has been used.
- */
- commandsUsed: bigint;
-
- /**
- * The total number of times any slash command has been used.
- */
- slashCommandsUsed: bigint;
-}
-
-export interface Emitters {
- client: BushClient;
- commandHandler: BushClient['commandHandler'];
- inhibitorHandler: BushClient['inhibitorHandler'];
- listenerHandler: BushClient['listenerHandler'];
- taskHandler: BushClient['taskHandler'];
- contextMenuCommandHandler: BushClient['contextMenuCommandHandler'];
- process: NodeJS.Process;
- stdin: readline.Interface;
- gateway: BushClient['ws'];
- rest: BushClient['rest'];
- ws: BushClient['ws'];
-}
diff --git a/lib/extensions/discord-akairo/BushCommand.ts b/lib/extensions/discord-akairo/BushCommand.ts
deleted file mode 100644
index 7201248..0000000
--- a/lib/extensions/discord-akairo/BushCommand.ts
+++ /dev/null
@@ -1,597 +0,0 @@
-import { type DiscordEmojiInfo, type RoleWithDuration } from '#args';
-import {
- type BushArgumentTypeCaster,
- type BushClient,
- type BushCommandHandler,
- type BushInhibitor,
- type BushListener,
- type BushTask,
- type ParsedDuration
-} from '#lib';
-import {
- ArgumentMatch,
- Command,
- CommandUtil,
- type AkairoApplicationCommandAutocompleteOption,
- type AkairoApplicationCommandChannelOptionData,
- type AkairoApplicationCommandChoicesData,
- type AkairoApplicationCommandNonOptionsData,
- type AkairoApplicationCommandNumericOptionData,
- type AkairoApplicationCommandOptionData,
- type AkairoApplicationCommandSubCommandData,
- type AkairoApplicationCommandSubGroupData,
- type ArgumentOptions,
- type ArgumentType,
- type ArgumentTypeCaster,
- type BaseArgumentType,
- type CommandOptions,
- type ContextMenuCommand,
- type MissingPermissionSupplier,
- type SlashOption,
- type SlashResolveType
-} from 'discord-akairo';
-import {
- Message,
- PermissionsBitField,
- User,
- type ApplicationCommandOptionChoiceData,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- type ApplicationCommandOptionType,
- type PermissionResolvable,
- type PermissionsString,
- type Snowflake
-} from 'discord.js';
-import _ from 'lodash';
-import { SlashMessage } from './SlashMessage.js';
-
-export interface OverriddenBaseArgumentType extends BaseArgumentType {
- commandAlias: BushCommand | null;
- command: BushCommand | null;
- inhibitor: BushInhibitor | null;
- listener: BushListener | null;
- task: BushTask | null;
- contextMenuCommand: ContextMenuCommand | null;
-}
-
-export interface BaseBushArgumentType extends OverriddenBaseArgumentType {
- duration: number | null;
- contentWithDuration: ParsedDuration;
- permission: PermissionsString | null;
- snowflake: Snowflake | null;
- discordEmoji: DiscordEmojiInfo | null;
- roleWithDuration: RoleWithDuration | null;
- abbreviatedNumber: number | null;
- globalUser: User | null;
- messageLink: Message | null;
- durationSeconds: number | null;
- tinyColor: string | null;
-}
-
-export type BushArgumentType = keyof BaseBushArgumentType | RegExp;
-
-interface BaseBushArgumentOptions extends Omit, ExtraArgumentOptions {
- id: string;
- description: string;
-
- /**
- * The message sent for the prompt and the slash command description.
- */
- prompt?: string;
-
- /**
- * The message set for the retry prompt.
- */
- retry?: string;
-
- /**
- * Whether or not the argument is optional.
- */
- optional?: boolean;
-
- /**
- * The type used for slash commands. Set to false to disable this argument for slash commands.
- */
- slashType: AkairoApplicationCommandOptionData['type'] | false;
-
- /**
- * Allows you to get a discord resolved object
- *
- * ex. get the resolved member object when the type is {@link ApplicationCommandOptionType.User User}
- */
- slashResolve?: SlashResolveType;
-
- /**
- * The choices of the option for the user to pick from
- */
- choices?: ApplicationCommandOptionChoiceData[];
-
- /**
- * Whether the option is an autocomplete option
- */
- autocomplete?: boolean;
-
- /**
- * When the option type is channel, the allowed types of channels that can be selected
- */
- channelTypes?: AkairoApplicationCommandChannelOptionData['channelTypes'];
-
- /**
- * The minimum value for an {@link ApplicationCommandOptionType.Integer Integer} or {@link ApplicationCommandOptionType.Number Number} option
- */
- minValue?: number;
-
- /**
- * The maximum value for an {@link ApplicationCommandOptionType.Integer Integer} or {@link ApplicationCommandOptionType.Number Number} option
- */
- maxValue?: number;
-}
-
-interface ExtraArgumentOptions {
- /**
- * Restrict this argument to only slash or only text commands.
- */
- only?: 'slash' | 'text';
-
- /**
- * Readable type for the help command.
- */
- readableType?: string;
-
- /**
- * Whether the argument is only accessible to the owners.
- * @default false
- */
- ownerOnly?: boolean;
-
- /**
- * Whether the argument is only accessible to the super users.
- * @default false
- */
- superUserOnly?: boolean;
-}
-
-export interface BushArgumentOptions extends BaseBushArgumentOptions {
- /**
- * The type that the argument should be cast to.
- * - `string` does not cast to any type.
- * - `lowercase` makes the input lowercase.
- * - `uppercase` makes the input uppercase.
- * - `charCodes` transforms the input to an array of char codes.
- * - `number` casts to a number.
- * - `integer` casts to an integer.
- * - `bigint` casts to a big integer.
- * - `url` casts to an `URL` object.
- * - `date` casts to a `Date` object.
- * - `color` casts a hex code to an integer.
- * - `commandAlias` tries to resolve to a command from an alias.
- * - `command` matches the ID of a command.
- * - `inhibitor` matches the ID of an inhibitor.
- * - `listener` matches the ID of a listener.
- *
- * Possible Discord-related types.
- * These types can be plural (add an 's' to the end) and a collection of matching objects will be used.
- * - `user` tries to resolve to a user.
- * - `member` tries to resolve to a member.
- * - `relevant` tries to resolve to a relevant user, works in both guilds and DMs.
- * - `channel` tries to resolve to a channel.
- * - `textChannel` tries to resolve to a text channel.
- * - `voiceChannel` tries to resolve to a voice channel.
- * - `stageChannel` tries to resolve to a stage channel.
- * - `threadChannel` tries to resolve a thread channel.
- * - `role` tries to resolve to a role.
- * - `emoji` tries to resolve to a custom emoji.
- * - `guild` tries to resolve to a guild.
- * - `permission` tries to resolve to a permissions.
- *
- * Other Discord-related types:
- * - `message` tries to fetch a message from an ID within the channel.
- * - `guildMessage` tries to fetch a message from an ID within the guild.
- * - `relevantMessage` is a combination of the above, works in both guilds and DMs.
- * - `invite` tries to fetch an invite object from a link.
- * - `userMention` matches a mention of a user.
- * - `memberMention` matches a mention of a guild member.
- * - `channelMention` matches a mention of a channel.
- * - `roleMention` matches a mention of a role.
- * - `emojiMention` matches a mention of an emoji.
- *
- * Misc:
- * - `duration` tries to parse duration in milliseconds
- * - `contentWithDuration` tries to parse duration in milliseconds and returns the remaining content with the duration
- * removed
- */
- type?: BushArgumentType | (keyof BaseBushArgumentType)[] | BushArgumentTypeCaster;
-}
-
-export interface CustomBushArgumentOptions extends BaseBushArgumentOptions {
- /**
- * An array of strings can be used to restrict input to only those strings, case insensitive.
- * The array can also contain an inner array of strings, for aliases.
- * If so, the first entry of the array will be used as the final argument.
- *
- * A regular expression can also be used.
- * The evaluated argument will be an object containing the `match` and `matches` if global.
- */
- customType?: (string | string[])[] | RegExp | string | null;
-}
-
-export type BushMissingPermissionSupplier = (message: CommandMessage | SlashMessage) => Promise | any;
-
-interface ExtendedCommandOptions {
- /**
- * Whether the command is hidden from the help command.
- */
- hidden?: boolean;
-
- /**
- * The channels the command is limited to run in.
- */
- restrictedChannels?: Snowflake[];
-
- /**
- * The guilds the command is limited to run in.
- */
- restrictedGuilds?: Snowflake[];
-
- /**
- * Show how to use the command.
- */
- usage: string[];
-
- /**
- * Examples for how to use the command.
- */
- examples: string[];
-
- /**
- * A fake command, completely hidden from the help command.
- */
- pseudo?: boolean;
-
- /**
- * Allow this command to be run in channels that are blacklisted.
- */
- bypassChannelBlacklist?: boolean;
-
- /**
- * Use instead of {@link BaseBushCommandOptions.args} when using argument generators or custom slashOptions
- */
- helpArgs?: ArgsInfo[];
-
- /**
- * Extra information about the command, displayed in the help command.
- */
- note?: string;
-}
-
-export interface BaseBushCommandOptions
- extends Omit,
- ExtendedCommandOptions {
- /**
- * The description of the command.
- */
- description: string;
-
- /**
- * The arguments for the command.
- */
- args?: BushArgumentOptions[] & CustomBushArgumentOptions[];
-
- category: string;
-
- /**
- * Permissions required by the client to run this command.
- */
- clientPermissions: bigint | bigint[] | BushMissingPermissionSupplier;
-
- /**
- * Permissions required by the user to run this command.
- */
- userPermissions: bigint | bigint[] | BushMissingPermissionSupplier;
-
- /**
- * Whether the argument is only accessible to the owners.
- */
- ownerOnly?: boolean;
-
- /**
- * Whether the argument is only accessible to the super users.
- */
- superUserOnly?: boolean;
-}
-
-export type BushCommandOptions = Omit | Omit;
-
-export interface ArgsInfo {
- /**
- * The name of the argument.
- */
- name: string;
-
- /**
- * The description of the argument.
- */
- description: string;
-
- /**
- * Whether the argument is optional.
- * @default false
- */
- optional?: boolean;
-
- /**
- * Whether or not the argument has autocomplete enabled.
- * @default false
- */
- autocomplete?: boolean;
-
- /**
- * Whether the argument is restricted a certain command.
- * @default 'slash & text'
- */
- only?: 'slash & text' | 'slash' | 'text';
-
- /**
- * The method that arguments are matched for text commands.
- * @default 'phrase'
- */
- match?: ArgumentMatch;
-
- /**
- * The readable type of the argument.
- */
- type: string;
-
- /**
- * If {@link match} is 'flag' or 'option', these are the flags that are matched
- * @default []
- */
- flag?: string[];
-
- /**
- * Whether the argument is only accessible to the owners.
- * @default false
- */
- ownerOnly?: boolean;
-
- /**
- * Whether the argument is only accessible to the super users.
- * @default false
- */
- superUserOnly?: boolean;
-}
-
-export abstract class BushCommand extends Command {
- public declare client: BushClient;
- public declare handler: BushCommandHandler;
- public declare description: string;
-
- /**
- * Show how to use the command.
- */
- public usage: string[];
-
- /**
- * Examples for how to use the command.
- */
- public examples: string[];
-
- /**
- * The options sent to the constructor
- */
- public options: BushCommandOptions;
-
- /**
- * The options sent to the super call
- */
- public parsedOptions: CommandOptions;
-
- /**
- * The channels the command is limited to run in.
- */
- public restrictedChannels: Snowflake[] | undefined;
-
- /**
- * The guilds the command is limited to run in.
- */
- public restrictedGuilds: Snowflake[] | undefined;
-
- /**
- * Whether the command is hidden from the help command.
- */
- public hidden: boolean;
-
- /**
- * A fake command, completely hidden from the help command.
- */
- public pseudo: boolean;
-
- /**
- * Allow this command to be run in channels that are blacklisted.
- */
- public bypassChannelBlacklist: boolean;
-
- /**
- * Info about the arguments for the help command.
- */
- public argsInfo?: ArgsInfo[];
-
- /**
- * Extra information about the command, displayed in the help command.
- */
- public note?: string;
-
- public constructor(id: string, options: BushCommandOptions) {
- const options_ = options as BaseBushCommandOptions;
-
- if (options_.args && typeof options_.args !== 'function') {
- options_.args.forEach((_, index: number) => {
- if ('customType' in (options_.args?.[index] ?? {})) {
- if (!options_.args![index]['type']) options_.args![index]['type'] = options_.args![index]['customType']! as any;
- delete options_.args![index]['customType'];
- }
- });
- }
-
- const newOptions: Partial = {};
- for (const _key in options_) {
- const key = _key as keyof typeof options_; // you got to love typescript
- if (key === 'args' && 'args' in options_ && typeof options_.args === 'object') {
- const newTextArgs: (ArgumentOptions & ExtraArgumentOptions)[] = [];
- const newSlashArgs: SlashOption[] = [];
- for (const arg of options_.args) {
- if (arg.only !== 'slash' && !options_.slashOnly) {
- const newArg: ArgumentOptions & ExtraArgumentOptions = {};
- if ('default' in arg) newArg.default = arg.default;
- if ('description' in arg) newArg.description = arg.description;
- if ('flag' in arg) newArg.flag = arg.flag;
- if ('id' in arg) newArg.id = arg.id;
- if ('index' in arg) newArg.index = arg.index;
- if ('limit' in arg) newArg.limit = arg.limit;
- if ('match' in arg) newArg.match = arg.match;
- if ('modifyOtherwise' in arg) newArg.modifyOtherwise = arg.modifyOtherwise;
- if ('multipleFlags' in arg) newArg.multipleFlags = arg.multipleFlags;
- if ('otherwise' in arg) newArg.otherwise = arg.otherwise;
- if ('prompt' in arg || 'retry' in arg || 'optional' in arg) {
- newArg.prompt = {};
- if ('prompt' in arg) newArg.prompt.start = arg.prompt;
- if ('retry' in arg) newArg.prompt.retry = arg.retry;
- if ('optional' in arg) newArg.prompt.optional = arg.optional;
- }
- if ('type' in arg) newArg.type = arg.type as ArgumentType | ArgumentTypeCaster;
- if ('unordered' in arg) newArg.unordered = arg.unordered;
- if ('ownerOnly' in arg) newArg.ownerOnly = arg.ownerOnly;
- if ('superUserOnly' in arg) newArg.superUserOnly = arg.superUserOnly;
- newTextArgs.push(newArg);
- }
- if (
- arg.only !== 'text' &&
- !('slashOptions' in options_) &&
- (options_.slash || options_.slashOnly) &&
- arg.slashType !== false
- ) {
- const newArg: {
- [key in SlashOptionKeys]?: any;
- } = {
- name: arg.id,
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- description: arg.prompt || arg.description || 'No description provided.',
- type: arg.slashType
- };
- if ('slashResolve' in arg) newArg.resolve = arg.slashResolve;
- if ('autocomplete' in arg) newArg.autocomplete = arg.autocomplete;
- if ('channelTypes' in arg) newArg.channelTypes = arg.channelTypes;
- if ('choices' in arg) newArg.choices = arg.choices;
- if ('minValue' in arg) newArg.minValue = arg.minValue;
- if ('maxValue' in arg) newArg.maxValue = arg.maxValue;
- newArg.required = 'optional' in arg ? !arg.optional : true;
- newSlashArgs.push(newArg as SlashOption);
- }
- }
- if (newTextArgs.length > 0) newOptions.args = newTextArgs;
- if (newSlashArgs.length > 0) newOptions.slashOptions = options_.slashOptions ?? newSlashArgs;
- } else if (key === 'clientPermissions' || key === 'userPermissions') {
- newOptions[key] = options_[key] as PermissionResolvable | PermissionResolvable[] | MissingPermissionSupplier;
- } else {
- newOptions[key] = options_[key];
- }
- }
-
- if (
- 'userPermissions' in newOptions &&
- !('slashDefaultMemberPermissions' in newOptions) &&
- typeof newOptions.userPermissions !== 'function'
- ) {
- const perms = new PermissionsBitField(newOptions.userPermissions);
-
- newOptions.slashDefaultMemberPermissions = perms.toArray().length === 0 ? null : perms;
- }
-
- super(id, newOptions);
-
- if (options_.args ?? options_.helpArgs) {
- const argsInfo: ArgsInfo[] = [];
- const combined = (options_.args ?? options_.helpArgs)!.map((arg) => {
- const norm = options_.args
- ? options_.args.find((_arg) => _arg.id === ('id' in arg ? arg.id : arg.name)) ?? ({} as BushArgumentOptions)
- : ({} as BushArgumentOptions);
- const help = options_.helpArgs
- ? options_.helpArgs.find((_arg) => _arg.name === ('id' in arg ? arg.id : arg.name)) ?? ({} as ArgsInfo)
- : ({} as ArgsInfo);
- return { ...norm, ...help };
- });
-
- for (const arg of combined) {
- const name = _.camelCase('id' in arg ? arg.id : arg.name),
- description = arg.description || '*No description provided.*',
- optional = arg.optional ?? false,
- autocomplete = arg.autocomplete ?? false,
- only = arg.only ?? 'slash & text',
- match = arg.match ?? 'phrase',
- type = match === 'flag' ? 'flag' : arg.readableType ?? arg.type ?? 'string',
- flag = arg.flag ? (Array.isArray(arg.flag) ? arg.flag : [arg.flag]) : [],
- ownerOnly = arg.ownerOnly ?? false,
- superUserOnly = arg.superUserOnly ?? false;
-
- argsInfo.push({ name, description, optional, autocomplete, only, match, type, flag, ownerOnly, superUserOnly });
- }
-
- this.argsInfo = argsInfo;
- }
-
- this.description = options_.description;
- this.usage = options_.usage;
- this.examples = options_.examples;
- this.options = options_;
- this.parsedOptions = newOptions;
- this.hidden = !!options_.hidden;
- this.restrictedChannels = options_.restrictedChannels;
- this.restrictedGuilds = options_.restrictedGuilds;
- this.pseudo = !!options_.pseudo;
- this.bypassChannelBlacklist = !!options_.bypassChannelBlacklist;
- this.note = options_.note;
- }
-
- /**
- * Executes the command.
- * @param message - Message that triggered the command.
- * @param args - Evaluated arguments.
- */
- public abstract override exec(message: CommandMessage, args: any): any;
- /**
- * Executes the command.
- * @param message - Message that triggered the command.
- * @param args - Evaluated arguments.
- */
- public abstract override exec(message: CommandMessage | SlashMessage, args: any): any;
-}
-
-type SlashOptionKeys =
- | keyof AkairoApplicationCommandSubGroupData
- | keyof AkairoApplicationCommandNonOptionsData
- | keyof AkairoApplicationCommandChannelOptionData
- | keyof AkairoApplicationCommandChoicesData
- | keyof AkairoApplicationCommandAutocompleteOption
- | keyof AkairoApplicationCommandNumericOptionData
- | keyof AkairoApplicationCommandSubCommandData;
-
-interface PseudoArguments extends BaseBushArgumentType {
- boolean: boolean;
- flag: boolean;
- regex: { match: RegExpMatchArray; matches: RegExpExecArray[] };
-}
-
-export type ArgType = NonNullable;
-export type OptArgType = PseudoArguments[T];
-
-/**
- * `util` is always defined for messages after `'all'` inhibitors
- */
-export type CommandMessage = Message & {
- /**
- * Extra properties applied to the Discord.js message object.
- * Utilities for command responding.
- * Available on all messages after 'all' inhibitors and built-in inhibitors (bot, client).
- * Not all properties of the util are available, depending on the input.
- * */
- util: CommandUtil;
-};
diff --git a/lib/extensions/discord-akairo/BushCommandHandler.ts b/lib/extensions/discord-akairo/BushCommandHandler.ts
deleted file mode 100644
index da49af9..0000000
--- a/lib/extensions/discord-akairo/BushCommandHandler.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { type BushCommand, type CommandMessage, type SlashMessage } from '#lib';
-import { CommandHandler, type Category, type CommandHandlerEvents, type CommandHandlerOptions } from 'discord-akairo';
-import { type Collection, type Message, type PermissionsString } from 'discord.js';
-
-export type BushCommandHandlerOptions = CommandHandlerOptions;
-
-export interface BushCommandHandlerEvents extends CommandHandlerEvents {
- commandBlocked: [message: CommandMessage, command: BushCommand, reason: string];
- commandBreakout: [message: CommandMessage, command: BushCommand, /* no util */ breakMessage: Message];
- commandCancelled: [message: CommandMessage, command: BushCommand, /* no util */ retryMessage?: Message];
- commandFinished: [message: CommandMessage, command: BushCommand, args: any, returnValue: any];
- commandInvalid: [message: CommandMessage, command: BushCommand];
- commandLocked: [message: CommandMessage, command: BushCommand];
- commandStarted: [message: CommandMessage, command: BushCommand, args: any];
- cooldown: [message: CommandMessage | SlashMessage, command: BushCommand, remaining: number];
- error: [error: Error, message: /* no util */ Message, command?: BushCommand];
- inPrompt: [message: /* no util */ Message];
- load: [command: BushCommand, isReload: boolean];
- messageBlocked: [message: /* no util */ Message | CommandMessage | SlashMessage, reason: string];
- messageInvalid: [message: CommandMessage];
- missingPermissions: [message: CommandMessage, command: BushCommand, type: 'client' | 'user', missing: PermissionsString[]];
- remove: [command: BushCommand];
- slashBlocked: [message: SlashMessage, command: BushCommand, reason: string];
- slashError: [error: Error, message: SlashMessage, command: BushCommand];
- slashFinished: [message: SlashMessage, command: BushCommand, args: any, returnValue: any];
- slashMissingPermissions: [message: SlashMessage, command: BushCommand, type: 'client' | 'user', missing: PermissionsString[]];
- slashStarted: [message: SlashMessage, command: BushCommand, args: any];
-}
-
-export class BushCommandHandler extends CommandHandler {
- public declare modules: Collection;
- public declare categories: Collection>;
-}
-
-export interface BushCommandHandler extends CommandHandler {
- findCommand(name: string): BushCommand;
-}
diff --git a/lib/extensions/discord-akairo/BushInhibitor.ts b/lib/extensions/discord-akairo/BushInhibitor.ts
deleted file mode 100644
index be396cf..0000000
--- a/lib/extensions/discord-akairo/BushInhibitor.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { type BushCommand, type CommandMessage, type SlashMessage } from '#lib';
-import { Inhibitor } from 'discord-akairo';
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import { Message } from 'discord.js';
-
-export abstract class BushInhibitor extends Inhibitor {
- /**
- * Checks if message should be blocked.
- * A return value of true will block the message.
- * If returning a Promise, a resolved value of true will block the message.
- *
- * **Note:** `'all'` type inhibitors do not have {@link Message.util} defined.
- *
- * @param message - Message being handled.
- * @param command - Command to check.
- */
- public abstract override exec(message: CommandMessage, command: BushCommand): any;
- public abstract override exec(message: CommandMessage | SlashMessage, command: BushCommand): any;
-}
diff --git a/lib/extensions/discord-akairo/BushInhibitorHandler.ts b/lib/extensions/discord-akairo/BushInhibitorHandler.ts
deleted file mode 100644
index 5e4fb6c..0000000
--- a/lib/extensions/discord-akairo/BushInhibitorHandler.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { InhibitorHandler } from 'discord-akairo';
-
-export class BushInhibitorHandler extends InhibitorHandler {}
diff --git a/lib/extensions/discord-akairo/BushListener.ts b/lib/extensions/discord-akairo/BushListener.ts
deleted file mode 100644
index 6917641..0000000
--- a/lib/extensions/discord-akairo/BushListener.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { Listener } from 'discord-akairo';
-
-export abstract class BushListener extends Listener {}
diff --git a/lib/extensions/discord-akairo/BushListenerHandler.ts b/lib/extensions/discord-akairo/BushListenerHandler.ts
deleted file mode 100644
index 9c3e4af..0000000
--- a/lib/extensions/discord-akairo/BushListenerHandler.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { ListenerHandler } from 'discord-akairo';
-
-export class BushListenerHandler extends ListenerHandler {}
diff --git a/lib/extensions/discord-akairo/BushTask.ts b/lib/extensions/discord-akairo/BushTask.ts
deleted file mode 100644
index 1b70c88..0000000
--- a/lib/extensions/discord-akairo/BushTask.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { Task } from 'discord-akairo';
-
-export abstract class BushTask extends Task {}
diff --git a/lib/extensions/discord-akairo/BushTaskHandler.ts b/lib/extensions/discord-akairo/BushTaskHandler.ts
deleted file mode 100644
index 6535abb..0000000
--- a/lib/extensions/discord-akairo/BushTaskHandler.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { TaskHandler } from 'discord-akairo';
-
-export class BushTaskHandler extends TaskHandler {}
diff --git a/lib/extensions/discord-akairo/TanzaniteClient.ts b/lib/extensions/discord-akairo/TanzaniteClient.ts
new file mode 100644
index 0000000..24ce962
--- /dev/null
+++ b/lib/extensions/discord-akairo/TanzaniteClient.ts
@@ -0,0 +1,591 @@
+import {
+ abbreviatedNumber,
+ contentWithDuration,
+ discordEmoji,
+ duration,
+ durationSeconds,
+ globalUser,
+ messageLink,
+ permission,
+ roleWithDuration,
+ snowflake
+} from '#args';
+import type { Config } from '#config';
+import { BotClientEvents, emojis, formatError, inspect, updateEveryCache } from '#lib';
+import { patch, type PatchedElements } from '@notenoughupdates/events-intercept';
+import * as Sentry from '@sentry/node';
+import {
+ AkairoClient,
+ ArgumentTypeCaster,
+ ContextMenuCommandHandler,
+ version as akairoVersion,
+ type ArgumentPromptData,
+ type OtherwiseContentSupplier
+} from 'discord-akairo';
+import {
+ ActivityType,
+ GatewayIntentBits,
+ MessagePayload,
+ Options,
+ Partials,
+ Structures,
+ version as discordJsVersion,
+ type Awaitable,
+ type If,
+ type Message,
+ type MessageOptions,
+ type Snowflake,
+ type UserResolvable
+} from 'discord.js';
+import type EventEmitter from 'events';
+import { google } from 'googleapis';
+import path from 'path';
+import readline from 'readline';
+import { Options as SequelizeOptions, Sequelize, Sequelize as SequelizeType } from 'sequelize';
+import { fileURLToPath } from 'url';
+import { tinyColor } from '../../arguments/tinyColor.js';
+import { BotCache } from '../../common/BotCache.js';
+import { HighlightManager } from '../../common/HighlightManager.js';
+import {
+ ActivePunishment,
+ Global,
+ Guild as GuildModel,
+ GuildCount,
+ Highlight,
+ Level,
+ MemberCount,
+ ModLog,
+ Reminder,
+ Shared,
+ Stat,
+ StickyRole
+} from '../../models/index.js';
+import { AllowedMentions } from '../../utils/AllowedMentions.js';
+import { BotClientUtils } from '../../utils/BotClientUtils.js';
+import { Logger } from '../../utils/Logger.js';
+import { ExtendedGuild } from '../discord.js/ExtendedGuild.js';
+import { ExtendedGuildMember } from '../discord.js/ExtendedGuildMember.js';
+import { ExtendedMessage } from '../discord.js/ExtendedMessage.js';
+import { ExtendedUser } from '../discord.js/ExtendedUser.js';
+import { BotCommandHandler } from './BotCommandHandler.js';
+import { BotInhibitorHandler } from './BotInhibitorHandler.js';
+import { BotListenerHandler } from './BotListenerHandler.js';
+import { BotTaskHandler } from './BotTaskHandler.js';
+
+declare module 'discord.js' {
+ export interface Client extends EventEmitter {
+ /** The ID of the owner(s). */
+ ownerID: Snowflake | Snowflake[];
+ /** The ID of the superUser(s). */
+ superUserID: Snowflake | Snowflake[];
+ /** Whether or not the client is ready. */
+ customReady: boolean;
+ /** The configuration for the client. */
+ readonly config: Config;
+ /** Stats for the client. */
+ readonly stats: BotStats;
+ /** The handler for the bot's listeners. */
+ readonly listenerHandler: BotListenerHandler;
+ /** The handler for the bot's command inhibitors. */
+ readonly inhibitorHandler: BotInhibitorHandler;
+ /** The handler for the bot's commands. */
+ readonly commandHandler: BotCommandHandler;
+ /** The handler for the bot's tasks. */
+ readonly taskHandler: BotTaskHandler;
+ /** The handler for the bot's context menu commands. */
+ readonly contextMenuCommandHandler: ContextMenuCommandHandler;
+ /** The database connection for this instance of the bot (production, beta, or development). */
+ readonly instanceDB: SequelizeType;
+ /** The database connection that is shared between all instances of the bot. */
+ readonly sharedDB: SequelizeType;
+ /** A custom logging system for the bot. */
+ readonly logger: Logger;
+ /** Cached global and guild database data. */
+ readonly cache: BotCache;
+ /** Sentry error reporting for the bot. */
+ readonly sentry: typeof Sentry;
+ /** Manages most aspects of the highlight command */
+ readonly highlightManager: HighlightManager;
+ /** The perspective api */
+ perspective: any;
+ /** Client utilities. */
+ readonly utils: BotClientUtils;
+ /** A custom logging system for the bot. */
+ get console(): Logger;
+ on(event: K, listener: (...args: BotClientEvents[K]) => Awaitable): this;
+ once(event: K, listener: (...args: BotClientEvents[K]) => Awaitable): this;
+ emit(event: K, ...args: BotClientEvents[K]): boolean;
+ off(event: K, listener: (...args: BotClientEvents[K]) => Awaitable): this;
+ removeAllListeners(event?: K): this;
+ /**
+ * Checks if a user is the owner of this bot.
+ * @param user - User to check.
+ */
+ isOwner(user: UserResolvable): boolean;
+ /**
+ * Checks if a user is a super user of this bot.
+ * @param user - User to check.
+ */
+ isSuperUser(user: UserResolvable): boolean;
+ }
+}
+
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ terminal: false
+});
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+/**
+ * The main hub for interacting with the Discord API.
+ */
+export class TanzaniteClient extends AkairoClient {
+ public declare ownerID: Snowflake[];
+ public declare superUserID: Snowflake[];
+
+ /**
+ * Whether or not the client is ready.
+ */
+ public override customReady = false;
+
+ /**
+ * Stats for the client.
+ */
+ public override readonly stats: BotStats = { cpu: undefined, commandsUsed: 0n, slashCommandsUsed: 0n };
+
+ /**
+ * The handler for the bot's listeners.
+ */
+ public override readonly listenerHandler: BotListenerHandler;
+
+ /**
+ * The handler for the bot's command inhibitors.
+ */
+ public override readonly inhibitorHandler: BotInhibitorHandler;
+
+ /**
+ * The handler for the bot's commands.
+ */
+ public override readonly commandHandler: BotCommandHandler;
+
+ /**
+ * The handler for the bot's tasks.
+ */
+ public override readonly taskHandler: BotTaskHandler;
+
+ /**
+ * The handler for the bot's context menu commands.
+ */
+ public override readonly contextMenuCommandHandler: ContextMenuCommandHandler;
+
+ /**
+ * The database connection for this instance of the bot (production, beta, or development).
+ */
+ public override readonly instanceDB: SequelizeType;
+
+ /**
+ * The database connection that is shared between all instances of the bot.
+ */
+ public override readonly sharedDB: SequelizeType;
+
+ /**
+ * A custom logging system for the bot.
+ */
+ public override readonly logger: Logger = new Logger(this);
+
+ /**
+ * Cached global and guild database data.
+ */
+ public override readonly cache = new BotCache();
+
+ /**
+ * Sentry error reporting for the bot.
+ */
+ public override readonly sentry!: typeof Sentry;
+
+ /**
+ * Manages most aspects of the highlight command
+ */
+ public override readonly highlightManager: HighlightManager = new HighlightManager(this);
+
+ /**
+ * The perspective api
+ */
+ public override perspective: any;
+
+ /**
+ * Client utilities.
+ */
+ public override readonly utils: BotClientUtils = new BotClientUtils(this);
+
+ /**
+ * @param config The configuration for the client.
+ */
+ public constructor(
+ /**
+ * The configuration for the client.
+ */
+ public override readonly config: Config
+ ) {
+ super({
+ ownerID: config.owners,
+ intents: Object.keys(GatewayIntentBits)
+ .map((i) => (typeof i === 'string' ? GatewayIntentBits[i as keyof typeof GatewayIntentBits] : i))
+ .reduce((acc, p) => acc | p, 0),
+ partials: Object.keys(Partials).map((p) => Partials[p as keyof typeof Partials]),
+ presence: {
+ activities: [{ name: 'Beep Boop', type: ActivityType.Watching }],
+ status: 'online'
+ },
+ allowedMentions: AllowedMentions.none(), // no mentions by default
+ makeCache: Options.cacheWithLimits({
+ PresenceManager: {
+ maxSize: 0,
+ keepOverLimit: (_, key) => {
+ if (config.owners.includes(key)) return true;
+
+ return HighlightManager.keep.has(key);
+ }
+ }
+ }),
+ failIfNotExists: false,
+ rest: { api: 'https://canary.discord.com/api' }
+ });
+ patch(this);
+
+ this.token = config.token as If;
+
+ /* =-=-= handlers =-=-= */
+ this.listenerHandler = new BotListenerHandler(this, {
+ directory: path.join(__dirname, '..', '..', '..', 'src', 'listeners'),
+ extensions: ['.js'],
+ automateCategories: true
+ });
+ this.inhibitorHandler = new BotInhibitorHandler(this, {
+ directory: path.join(__dirname, '..', '..', '..', 'src', 'inhibitors'),
+ extensions: ['.js'],
+ automateCategories: true
+ });
+ this.taskHandler = new BotTaskHandler(this, {
+ directory: path.join(__dirname, '..', '..', '..', 'src', 'tasks'),
+ extensions: ['.js'],
+ automateCategories: true
+ });
+
+ const modify = async (
+ message: Message,
+ text: string | MessagePayload | MessageOptions | OtherwiseContentSupplier,
+ data: ArgumentPromptData,
+ replaceError: boolean
+ ) => {
+ const ending = '\n\n Type **cancel** to cancel the command';
+ const options = typeof text === 'function' ? await text(message, data) : text;
+ const search = '{error}',
+ replace = emojis.error;
+
+ if (typeof options === 'string') return (replaceError ? options.replace(search, replace) : options) + ending;
+
+ if (options instanceof MessagePayload) {
+ if (options.options.content) {
+ if (replaceError) options.options.content = options.options.content.replace(search, replace);
+ options.options.content += ending;
+ }
+ } else if (options.content) {
+ if (replaceError) options.content = options.content.replace(search, replace);
+ options.content += ending;
+ }
+ return options;
+ };
+
+ this.commandHandler = new BotCommandHandler(this, {
+ directory: path.join(__dirname, '..', '..', '..', 'src', 'commands'),
+ extensions: ['.js'],
+ prefix: async ({ guild }: Message) => {
+ if (this.config.isDevelopment) return 'dev ';
+ if (!guild) return this.config.prefix;
+ const prefix = await guild.getSetting('prefix');
+ return (prefix ?? this.config.prefix) as string;
+ },
+ allowMention: true,
+ handleEdits: true,
+ commandUtil: true,
+ commandUtilLifetime: 300_000, // 5 minutes
+ argumentDefaults: {
+ prompt: {
+ start: 'Placeholder argument prompt. **If you see this please tell my developers**.',
+ retry: 'Placeholder failed argument prompt. **If you see this please tell my developers**.',
+ modifyStart: (message, text, data) => modify(message, text, data, false),
+ modifyRetry: (message, text, data) => modify(message, text, data, true),
+ timeout: ':hourglass: You took too long the command has been cancelled.',
+ ended: 'You exceeded the maximum amount of tries the command has been cancelled',
+ cancel: 'The command has been cancelled',
+ retries: 3,
+ time: 3e4
+ },
+ otherwise: ''
+ },
+ automateCategories: false,
+ autoRegisterSlashCommands: true,
+ skipBuiltInPostInhibitors: true,
+ aliasReplacement: /-/g
+ });
+ this.contextMenuCommandHandler = new ContextMenuCommandHandler(this, {
+ directory: path.join(__dirname, '..', '..', '..', 'src', 'context-menu-commands'),
+ extensions: ['.js'],
+ automateCategories: true
+ });
+
+ /* =-=-= databases =-=-= */
+ const sharedDBOptions: SequelizeOptions = {
+ username: this.config.db.username,
+ password: this.config.db.password,
+ dialect: 'postgres',
+ host: this.config.db.host,
+ port: this.config.db.port,
+ logging: this.config.logging.db ? (sql) => this.logger.debug(sql) : false,
+ timezone: 'America/New_York'
+ };
+ this.instanceDB = new Sequelize({
+ ...sharedDBOptions,
+ database: this.config.isDevelopment ? 'bushbot-dev' : this.config.isBeta ? 'bushbot-beta' : 'bushbot'
+ });
+ this.sharedDB = new Sequelize({
+ ...sharedDBOptions,
+ database: 'bushbot-shared'
+ });
+
+ this.sentry = Sentry;
+ }
+
+ /**
+ * A custom logging system for the bot.
+ */
+ public override get console(): Logger {
+ return this.logger;
+ }
+
+ /**
+ * Extends discord.js structures before the client is instantiated.
+ */
+ public static extendStructures(): void {
+ Structures.extend('GuildMember', () => ExtendedGuildMember);
+ Structures.extend('Guild', () => ExtendedGuild);
+ Structures.extend('Message', () => ExtendedMessage);
+ Structures.extend('User', () => ExtendedUser);
+ }
+
+ /**
+ * Initializes the bot.
+ */
+ public async init() {
+ if (parseInt(process.versions.node.split('.')[0]) < 18) {
+ void (await this.console.error('version', `Please use node <>, not <<${process.version}>>.`, false));
+ process.exit(2);
+ }
+
+ this.setMaxListeners(20);
+
+ this.perspective = await google.discoverAPI('https://commentanalyzer.googleapis.com/$discovery/rest?version=v1alpha1');
+
+ this.commandHandler.useInhibitorHandler(this.inhibitorHandler);
+ this.commandHandler.useListenerHandler(this.listenerHandler);
+ this.commandHandler.useTaskHandler(this.taskHandler);
+ this.commandHandler.useContextMenuCommandHandler(this.contextMenuCommandHandler);
+ this.commandHandler.ignorePermissions = this.config.owners;
+ this.commandHandler.ignoreCooldown = [...new Set([...this.config.owners, ...this.cache.shared.superUsers])];
+ const emitters: Emitters = {
+ client: this,
+ commandHandler: this.commandHandler,
+ inhibitorHandler: this.inhibitorHandler,
+ listenerHandler: this.listenerHandler,
+ taskHandler: this.taskHandler,
+ contextMenuCommandHandler: this.contextMenuCommandHandler,
+ process,
+ stdin: rl,
+ gateway: this.ws,
+ rest: this.rest,
+ ws: this.ws
+ };
+ this.listenerHandler.setEmitters(emitters);
+ this.commandHandler.resolver.addTypes({
+ duration: duration,
+ contentWithDuration: contentWithDuration,
+ permission: permission,
+ snowflake: snowflake,
+ discordEmoji: discordEmoji,
+ roleWithDuration: roleWithDuration,
+ abbreviatedNumber: abbreviatedNumber,
+ durationSeconds: durationSeconds,
+ globalUser: globalUser,
+ messageLink: messageLink,
+ tinyColor: tinyColor
+ });
+
+ this.sentry.setTag('process', process.pid.toString());
+ this.sentry.setTag('discord.js', discordJsVersion);
+ this.sentry.setTag('discord-akairo', akairoVersion);
+ void this.logger.success('startup', `Successfully connected to <>.`, false);
+
+ // loads all the handlers
+ const handlers = {
+ commands: this.commandHandler,
+ contextMenuCommands: this.contextMenuCommandHandler,
+ listeners: this.listenerHandler,
+ inhibitors: this.inhibitorHandler,
+ tasks: this.taskHandler
+ };
+ const handlerPromises = Object.entries(handlers).map(([handlerName, handler]) =>
+ handler
+ .loadAll()
+ .then(() => {
+ void this.logger.success('startup', `Successfully loaded <<${handlerName}>>.`, false);
+ })
+ .catch((e) => {
+ void this.logger.error('startup', `Unable to load loader <<${handlerName}>> with error:\n${formatError(e)}`, false);
+ if (process.argv.includes('dry')) process.exit(1);
+ })
+ );
+ await Promise.allSettled(handlerPromises);
+ }
+
+ /**
+ * Connects to the database, initializes models, and creates tables if they do not exist.
+ */
+ public async dbPreInit() {
+ try {
+ await this.instanceDB.authenticate();
+ GuildModel.initModel(this.instanceDB, this);
+ ModLog.initModel(this.instanceDB);
+ ActivePunishment.initModel(this.instanceDB);
+ Level.initModel(this.instanceDB);
+ StickyRole.initModel(this.instanceDB);
+ Reminder.initModel(this.instanceDB);
+ Highlight.initModel(this.instanceDB);
+ await this.instanceDB.sync({ alter: true }); // Sync all tables to fix everything if updated
+ await this.console.success('startup', `Successfully connected to <>.`, false);
+ } catch (e) {
+ await this.console.error(
+ 'startup',
+ `Failed to connect to <> with error:\n${inspect(e, { colors: true, depth: 1 })}`,
+ false
+ );
+ process.exit(2);
+ }
+ try {
+ await this.sharedDB.authenticate();
+ Stat.initModel(this.sharedDB);
+ Global.initModel(this.sharedDB);
+ Shared.initModel(this.sharedDB);
+ MemberCount.initModel(this.sharedDB);
+ GuildCount.initModel(this.sharedDB);
+ await this.sharedDB.sync({
+ // Sync all tables to fix everything if updated
+ // if another instance restarts we don't want to overwrite new changes made in development
+ alter: this.config.isDevelopment
+ });
+ await this.console.success('startup', `Successfully connected to <>.`, false);
+ } catch (e) {
+ await this.console.error(
+ 'startup',
+ `Failed to connect to <> with error:\n${inspect(e, { colors: true, depth: 1 })}`,
+ false
+ );
+ process.exit(2);
+ }
+ }
+
+ /**
+ * Starts the bot
+ */
+ public async start() {
+ this.intercept('ready', async (arg, done) => {
+ const promises = this.guilds.cache
+ .filter((g) => g.large)
+ .map((guild) => {
+ return guild.members.fetch();
+ });
+ await Promise.all(promises);
+ this.customReady = true;
+ this.taskHandler.startAll();
+ return done(null, `intercepted ${arg}`);
+ });
+
+ try {
+ await this.highlightManager.syncCache();
+ await updateEveryCache(this);
+ void this.console.success('startup', `Successfully created <>.`, false);
+
+ const stats =
+ (await Stat.findByPk(this.config.environment)) ?? (await Stat.create({ environment: this.config.environment }));
+ this.stats.commandsUsed = stats.commandsUsed;
+ this.stats.slashCommandsUsed = stats.slashCommandsUsed;
+ await this.login(this.token!);
+ } catch (e) {
+ await this.console.error('start', inspect(e, { colors: true, depth: 1 }), false);
+ process.exit(1);
+ }
+ }
+
+ /**
+ * Logs out, terminates the connection to Discord, and destroys the client.
+ */
+ public override destroy(relogin = false): void | Promise {
+ super.destroy();
+ if (relogin) {
+ return this.login(this.token!);
+ }
+ }
+
+ public override isOwner(user: UserResolvable): boolean {
+ return this.config.owners.includes(this.users.resolveId(user!)!);
+ }
+
+ public override isSuperUser(user: UserResolvable): boolean {
+ const userID = this.users.resolveId(user)!;
+ return this.cache.shared.superUsers.includes(userID) || this.config.owners.includes(userID);
+ }
+}
+
+export interface TanzaniteClient extends EventEmitter, PatchedElements, AkairoClient {
+ on(event: K, listener: (...args: BotClientEvents[K]) => Awaitable): this;
+ once(event: K, listener: (...args: BotClientEvents[K]) => Awaitable): this;
+ emit(event: K, ...args: BotClientEvents[K]): boolean;
+ off(event: K, listener: (...args: BotClientEvents[K]) => Awaitable): this;
+ removeAllListeners(event?: K): this;
+}
+
+/**
+ * Various statistics
+ */
+export interface BotStats {
+ /**
+ * The average cpu usage of the bot from the past 60 seconds.
+ */
+ cpu: number | undefined;
+
+ /**
+ * The total number of times any command has been used.
+ */
+ commandsUsed: bigint;
+
+ /**
+ * The total number of times any slash command has been used.
+ */
+ slashCommandsUsed: bigint;
+}
+
+export interface Emitters {
+ client: TanzaniteClient;
+ commandHandler: TanzaniteClient['commandHandler'];
+ inhibitorHandler: TanzaniteClient['inhibitorHandler'];
+ listenerHandler: TanzaniteClient['listenerHandler'];
+ taskHandler: TanzaniteClient['taskHandler'];
+ contextMenuCommandHandler: TanzaniteClient['contextMenuCommandHandler'];
+ process: NodeJS.Process;
+ stdin: readline.Interface;
+ gateway: TanzaniteClient['ws'];
+ rest: TanzaniteClient['rest'];
+ ws: TanzaniteClient['ws'];
+}
diff --git a/lib/extensions/discord.js/BotClientEvents.ts b/lib/extensions/discord.js/BotClientEvents.ts
new file mode 100644
index 0000000..284ea32
--- /dev/null
+++ b/lib/extensions/discord.js/BotClientEvents.ts
@@ -0,0 +1,211 @@
+import type { BanResponse, CommandMessage, Guild as GuildDB, GuildSettings } from '#lib';
+import type { AkairoClientEvents } from 'discord-akairo';
+import type {
+ ButtonInteraction,
+ Collection,
+ Guild,
+ GuildMember,
+ GuildTextBasedChannel,
+ Message,
+ ModalSubmitInteraction,
+ Role,
+ SelectMenuInteraction,
+ Snowflake,
+ User
+} from 'discord.js';
+
+export enum TanzaniteEvent {
+ Ban = 'customBan',
+ Block = 'customBlock',
+ Kick = 'customKick',
+ Mute = 'customMute',
+ PunishRoleAdd = 'punishRoleAdd',
+ PunishRoleRemove = 'punishRoleRemove',
+ Purge = 'customPurge',
+ RemoveTimeout = 'customRemoveTimeout',
+ Timeout = 'customTimeout',
+ Unban = 'customUnban',
+ Unblock = 'customUnblock',
+ Unmute = 'customUnmute',
+ UpdateModlog = 'updateModlog',
+ UpdateSettings = 'updateSettings',
+ Warn = 'customWarn',
+ LevelUpdate = 'levelUpdate',
+ Lockdown = 'lockdown',
+ Unlockdown = 'unlockdown',
+ MassBan = 'massBan',
+ MassEvidence = 'massEvidence',
+ Button = 'button',
+ SelectMenu = 'selectMenu',
+ ModalSubmit = 'modal'
+}
+
+export interface BotClientEvents extends AkairoClientEvents {
+ [TanzaniteEvent.Ban]: [
+ victim: GuildMember | User,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ duration: number,
+ dmSuccess?: boolean,
+ evidence?: string
+ ];
+ [TanzaniteEvent.Block]: [
+ victim: GuildMember,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ duration: number,
+ dmSuccess: boolean,
+ channel: GuildTextBasedChannel,
+ evidence?: string
+ ];
+ [TanzaniteEvent.Kick]: [
+ victim: GuildMember,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ dmSuccess: boolean,
+ evidence?: string
+ ];
+ [TanzaniteEvent.Mute]: [
+ victim: GuildMember,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ duration: number,
+ dmSuccess: boolean,
+ evidence?: string
+ ];
+ [TanzaniteEvent.PunishRoleAdd]: [
+ victim: GuildMember,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ duration: number,
+ role: Role,
+ evidence?: string
+ ];
+ [TanzaniteEvent.PunishRoleRemove]: [
+ victim: GuildMember,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ role: Role,
+ evidence?: string
+ ];
+ [TanzaniteEvent.Purge]: [
+ moderator: User,
+ guild: Guild,
+ channel: GuildTextBasedChannel,
+ messages: Collection
+ ];
+ [TanzaniteEvent.RemoveTimeout]: [
+ victim: GuildMember,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ dmSuccess: boolean,
+ evidence?: string
+ ];
+ [TanzaniteEvent.Timeout]: [
+ victim: GuildMember,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ duration: number,
+ dmSuccess: boolean,
+ evidence?: string
+ ];
+ [TanzaniteEvent.Unban]: [
+ victim: User,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ dmSuccess: boolean,
+ evidence?: string
+ ];
+ [TanzaniteEvent.Unblock]: [
+ victim: GuildMember | User,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ dmSuccess: boolean,
+ channel: GuildTextBasedChannel,
+ evidence?: string
+ ];
+ [TanzaniteEvent.Unmute]: [
+ victim: GuildMember,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ dmSuccess: boolean,
+ evidence?: string
+ ];
+ [TanzaniteEvent.UpdateModlog]: [
+ moderator: GuildMember,
+ modlogID: string,
+ key: 'evidence' | 'hidden',
+ oldModlog: string | boolean,
+ newModlog: string | boolean
+ ];
+ [TanzaniteEvent.UpdateSettings]: [
+ setting: Setting,
+ guild: Guild,
+ oldValue: GuildDB[Setting],
+ newValue: GuildDB[Setting],
+ moderator?: GuildMember
+ ];
+ [TanzaniteEvent.Warn]: [
+ victim: GuildMember,
+ moderator: User,
+ guild: Guild,
+ reason: string | undefined,
+ caseID: string,
+ dmSuccess: boolean,
+ evidence?: string
+ ];
+ [TanzaniteEvent.LevelUpdate]: [
+ member: GuildMember,
+ oldLevel: number,
+ newLevel: number,
+ currentXp: number,
+ message: CommandMessage
+ ];
+ [TanzaniteEvent.Lockdown]: [
+ moderator: GuildMember,
+ reason: string | undefined,
+ channelsSuccessMap: Collection,
+ all?: boolean
+ ];
+ [TanzaniteEvent.Unlockdown]: [
+ moderator: GuildMember,
+ reason: string | undefined,
+ channelsSuccessMap: Collection,
+ all?: boolean
+ ];
+ [TanzaniteEvent.MassBan]: [
+ moderator: GuildMember,
+ guild: Guild,
+ reason: string | undefined,
+ results: Collection
+ ];
+ [TanzaniteEvent.MassEvidence]: [moderator: GuildMember, guild: Guild, evidence: string, lines: string[]];
+ /* components */
+ [TanzaniteEvent.Button]: [button: ButtonInteraction];
+ [TanzaniteEvent.SelectMenu]: [selectMenu: SelectMenuInteraction];
+ [TanzaniteEvent.ModalSubmit]: [modal: ModalSubmitInteraction];
+}
+
+type Setting = GuildSettings | 'enabledFeatures' | 'blacklistedChannels' | 'blacklistedUsers' | 'disabledCommands';
diff --git a/lib/extensions/discord.js/BushClientEvents.ts b/lib/extensions/discord.js/BushClientEvents.ts
deleted file mode 100644
index 22bae65..0000000
--- a/lib/extensions/discord.js/BushClientEvents.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import type {
- BanResponse,
- CommandMessage,
- Guild as GuildDB,
- GuildSettings
-} from '#lib';
-import type { AkairoClientEvents } from 'discord-akairo';
-import type {
- ButtonInteraction,
- Collection,
- Guild,
- GuildMember,
- GuildTextBasedChannel,
- Message,
- ModalSubmitInteraction,
- Role,
- SelectMenuInteraction,
- Snowflake,
- User
-} from 'discord.js';
-
-export interface BushClientEvents extends AkairoClientEvents {
- bushBan: [
- victim: GuildMember | User,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- duration: number,
- dmSuccess?: boolean,
- evidence?: string
- ];
- bushBlock: [
- victim: GuildMember,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- duration: number,
- dmSuccess: boolean,
- channel: GuildTextBasedChannel,
- evidence?: string
- ];
- bushKick: [
- victim: GuildMember,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- dmSuccess: boolean,
- evidence?: string
- ];
- bushMute: [
- victim: GuildMember,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- duration: number,
- dmSuccess: boolean,
- evidence?: string
- ];
- bushPunishRole: [
- victim: GuildMember,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- duration: number,
- role: Role,
- evidence?: string
- ];
- bushPunishRoleRemove: [
- victim: GuildMember,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- role: Role,
- evidence?: string
- ];
- bushPurge: [
- moderator: User,
- guild: Guild,
- channel: GuildTextBasedChannel,
- messages: Collection
- ];
- bushRemoveTimeout: [
- victim: GuildMember,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- dmSuccess: boolean,
- evidence?: string
- ];
- bushTimeout: [
- victim: GuildMember,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- duration: number,
- dmSuccess: boolean,
- evidence?: string
- ];
- bushUnban: [
- victim: User,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- dmSuccess: boolean,
- evidence?: string
- ];
- bushUnblock: [
- victim: GuildMember | User,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- dmSuccess: boolean,
- channel: GuildTextBasedChannel,
- evidence?: string
- ];
- bushUnmute: [
- victim: GuildMember,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- dmSuccess: boolean,
- evidence?: string
- ];
- bushUpdateModlog: [
- moderator: GuildMember,
- modlogID: string,
- key: 'evidence' | 'hidden',
- oldModlog: string | boolean,
- newModlog: string | boolean
- ];
- bushUpdateSettings: [
- setting: Setting,
- guild: Guild,
- oldValue: GuildDB[Setting],
- newValue: GuildDB[Setting],
- moderator?: GuildMember
- ];
- bushWarn: [
- victim: GuildMember,
- moderator: User,
- guild: Guild,
- reason: string | undefined,
- caseID: string,
- dmSuccess: boolean,
- evidence?: string
- ];
- bushLevelUpdate: [
- member: GuildMember,
- oldLevel: number,
- newLevel: number,
- currentXp: number,
- message: CommandMessage
- ];
- bushLockdown: [
- moderator: GuildMember,
- reason: string | undefined,
- channelsSuccessMap: Collection,
- all?: boolean
- ];
- bushUnlockdown: [
- moderator: GuildMember,
- reason: string | undefined,
- channelsSuccessMap: Collection,
- all?: boolean
- ];
- massBan: [
- moderator: GuildMember,
- guild: Guild,
- reason: string | undefined,
- results: Collection
- ];
- massEvidence: [
- moderator: GuildMember,
- guild: Guild,
- evidence: string,
- lines: string[]
- ];
- /* components */
- button: [button: ButtonInteraction];
- selectMenu: [selectMenu: SelectMenuInteraction];
- modal: [modal: ModalSubmitInteraction];
-}
-
-type Setting =
- | GuildSettings
- | 'enabledFeatures'
- | 'blacklistedChannels'
- | 'blacklistedUsers'
- | 'disabledCommands';
diff --git a/lib/extensions/discord.js/ExtendedGuild.ts b/lib/extensions/discord.js/ExtendedGuild.ts
index 20c3d29..67de5cf 100644
--- a/lib/extensions/discord.js/ExtendedGuild.ts
+++ b/lib/extensions/discord.js/ExtendedGuild.ts
@@ -6,6 +6,7 @@ import {
emojis,
permissionsResponse,
punishmentEntryRemove,
+ TanzaniteClient,
type BanResponse,
type GuildFeatures,
type GuildLogType,
@@ -13,7 +14,7 @@ import {
} from '#lib';
import * as Moderation from '#lib/common/Moderation.js';
import { Guild as GuildDB, ModLogType } from '#lib/models/index.js';
-import { addOrRemoveFromArray } from '#lib/utils/BushUtils.js';
+import { addOrRemoveFromArray } from '#lib/utils/Utils.js';
import assert from 'assert/strict';
import {
AttachmentBuilder,
@@ -42,8 +43,13 @@ import {
type WebhookMessageOptions
} from 'discord.js';
import _ from 'lodash';
+import { TanzaniteEvent } from './BotClientEvents.js';
declare module 'discord.js' {
+ export interface BaseGuild {
+ client: TanzaniteClient;
+ }
+
export interface Guild {
/**
* Checks if the guild has a certain custom feature.
@@ -93,7 +99,7 @@ declare module 'discord.js' {
/**
* Sends a message to the guild's specified logging channel
* @param logType The corresponding channel that the message will be sent to
- * @param message The parameters for {@link BushTextChannel.send}
+ * @param message The parameters for {@link TextChannel.send}
*/
sendLogChannel(logType: GuildLogType, message: string | MessagePayload | MessageOptions): Promise;
/**
@@ -107,15 +113,15 @@ declare module 'discord.js' {
* @param options Options for banning the user.
* @returns A string status message of the ban.
*/
- bushBan(options: GuildBushBanOptions): Promise;
+ customBan(options: GuildCustomBanOptions): Promise;
/**
- * {@link bushBan} with less resolving and checks
+ * {@link customBan} with less resolving and checks
* @param options Options for banning the user.
* @returns A string status message of the ban.
* **Preconditions:**
* - {@link me} has the `BanMembers` permission
* **Warning:**
- * - Doesn't emit bushBan Event
+ * - Doesn't emit customBan Event
*/
massBanOne(options: GuildMassBanOneOptions): Promise;
/**
@@ -123,7 +129,7 @@ declare module 'discord.js' {
* @param options Options for unbanning the user.
* @returns A status message of the unban.
*/
- bushUnban(options: GuildBushUnbanOptions): Promise;
+ customUnban(options: GuildCustomUnbanOptions): Promise;
/**
* Denies send permissions in specified channels
* @param options The options for locking down the guild
@@ -207,7 +213,7 @@ export class ExtendedGuild extends Guild {
const oldValue = row[setting] as GuildDB[K];
row[setting] = value;
this.client.cache.guilds.set(this.id, row.toJSON() as GuildDB);
- this.client.emit('bushUpdateSettings', setting, this, oldValue, row[setting], moderator);
+ this.client.emit(TanzaniteEvent.UpdateSettings, setting, this, oldValue, row[setting], moderator);
return await row.save();
}
@@ -229,7 +235,7 @@ export class ExtendedGuild extends Guild {
/**
* Sends a message to the guild's specified logging channel
* @param logType The corresponding channel that the message will be sent to
- * @param message The parameters for {@link BushTextChannel.send}
+ * @param message The parameters for {@link TextChannel.send}
*/
public override async sendLogChannel(
logType: GuildLogType,
@@ -265,7 +271,7 @@ export class ExtendedGuild extends Guild {
* @param options Options for banning the user.
* @returns A string status message of the ban.
*/
- public override async bushBan(options: GuildBushBanOptions): Promise {
+ public override async customBan(options: GuildCustomBanOptions): Promise {
// checks
if (!this.members.me!.permissions.has(PermissionFlagsBits.BanMembers)) return banResponse.MISSING_PERMISSIONS;
@@ -330,7 +336,7 @@ export class ExtendedGuild extends Guild {
if (!([banResponse.ACTION_ERROR, banResponse.MODLOG_ERROR, banResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret))
this.client.emit(
- 'bushBan',
+ TanzaniteEvent.Ban,
user,
moderator,
this,
@@ -344,13 +350,13 @@ export class ExtendedGuild extends Guild {
}
/**
- * {@link bushBan} with less resolving and checks
+ * {@link customBan} with less resolving and checks
* @param options Options for banning the user.
* @returns A string status message of the ban.
* **Preconditions:**
* - {@link me} has the `BanMembers` permission
* **Warning:**
- * - Doesn't emit bushBan Event
+ * - Doesn't emit customBan Event
*/
public override async massBanOne(options: GuildMassBanOneOptions): Promise {
if (this.bans.cache.has(options.user)) return banResponse.ALREADY_BANNED;
@@ -414,7 +420,7 @@ export class ExtendedGuild extends Guild {
* @param options Options for unbanning the user.
* @returns A status message of the unban.
*/
- public override async bushUnban(options: GuildBushUnbanOptions): Promise {
+ public override async customUnban(options: GuildCustomUnbanOptions): Promise {
// checks
if (!this.members.me!.permissions.has(PermissionFlagsBits.BanMembers)) return unbanResponse.MISSING_PERMISSIONS;
@@ -483,7 +489,7 @@ export class ExtendedGuild extends Guild {
)
)
this.client.emit(
- 'bushUnban',
+ TanzaniteEvent.Unban,
user,
moderator,
this,
@@ -570,7 +576,13 @@ export class ExtendedGuild extends Guild {
else return `success: ${success.filter((c) => c === true).size}`;
})();
- this.client.emit(options.unlock ? 'bushUnlockdown' : 'bushLockdown', moderator, options.reason, success, options.all);
+ this.client.emit(
+ options.unlock ? TanzaniteEvent.Unlockdown : TanzaniteEvent.Lockdown,
+ moderator,
+ options.reason,
+ success,
+ options.all
+ );
return ret;
}
@@ -783,7 +795,7 @@ export class ExtendedGuild extends Guild {
/**
* Options for unbanning a user
*/
-export interface GuildBushUnbanOptions {
+export interface GuildCustomUnbanOptions {
/**
* The user to unban
*/
@@ -830,7 +842,7 @@ export interface GuildMassBanOneOptions {
/**
* Options for banning a user
*/
-export interface GuildBushBanOptions {
+export interface GuildCustomBanOptions {
/**
* The user to ban
*/
diff --git a/lib/extensions/discord.js/ExtendedGuildMember.ts b/lib/extensions/discord.js/ExtendedGuildMember.ts
index f8add83..172f6df 100644
--- a/lib/extensions/discord.js/ExtendedGuildMember.ts
+++ b/lib/extensions/discord.js/ExtendedGuildMember.ts
@@ -1,5 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
-import { formatError, Moderation, ModLogType, Time, type BushClientEvents, type PunishmentTypeDM, type ValueOf } from '#lib';
+import {
+ formatError,
+ Moderation,
+ ModLogType,
+ TanzaniteClient,
+ Time,
+ type BotClientEvents,
+ type PunishmentTypeDM,
+ type ValueOf
+} from '#lib';
import {
ChannelType,
GuildMember,
@@ -8,10 +17,13 @@ import {
type GuildTextBasedChannel,
type Role
} from 'discord.js';
+import { TanzaniteEvent } from './BotClientEvents.js';
/* eslint-enable @typescript-eslint/no-unused-vars */
declare module 'discord.js' {
export interface GuildMember {
+ client: TanzaniteClient;
+
/**
* Send a punishment dm to the user.
* @param punishment The punishment that the user has received.
@@ -21,7 +33,7 @@ declare module 'discord.js' {
* @param sendFooter Whether or not to send the guild's punishment footer with the dm.
* @returns Whether or not the dm was sent successfully.
*/
- bushPunishDM(
+ customPunishDM(
punishment: PunishmentTypeDM,
reason?: string | null,
duration?: number,
@@ -32,71 +44,71 @@ declare module 'discord.js' {
* Warn the user, create a modlog entry, and send a dm to the user.
* @param options Options for warning the user.
* @returns An object with the result of the warning, and the case number of the warn.
- * @emits {@link BushClientEvents.bushWarn}
+ * @emits {@link BotClientEvents.warnMember}
*/
- bushWarn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }>;
+ customWarn(options: CustomPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }>;
/**
* Add a role to the user, if it is a punishment create a modlog entry, and create a punishment entry if it is temporary or a punishment.
* @param options Options for adding a role to the user.
* @returns A status message for adding the add.
- * @emits {@link BushClientEvents.bushPunishRole}
+ * @emits {@link BotClientEvents.punishRole}
*/
- bushAddRole(options: AddRoleOptions): Promise;
+ customAddRole(options: AddRoleOptions): Promise;
/**
* Remove a role from the user, if it is a punishment create a modlog entry, and destroy a punishment entry if it was temporary or a punishment.
* @param options Options for removing a role from the user.
* @returns A status message for removing the role.
- * @emits {@link BushClientEvents.bushPunishRoleRemove}
+ * @emits {@link BotClientEvents.punishRoleRemove}
*/
- bushRemoveRole(options: RemoveRoleOptions): Promise;
+ customRemoveRole(options: RemoveRoleOptions): Promise;
/**
* Mute the user, create a modlog entry, creates a punishment entry, and dms the user.
* @param options Options for muting the user.
* @returns A status message for muting the user.
- * @emits {@link BushClientEvents.bushMute}
+ * @emits {@link BotClientEvents.customMute}
*/
- bushMute(options: BushTimedPunishmentOptions): Promise;
+ customMute(options: CustomTimedPunishmentOptions): Promise;
/**
* Unmute the user, create a modlog entry, remove the punishment entry, and dm the user.
* @param options Options for unmuting the user.
* @returns A status message for unmuting the user.
- * @emits {@link BushClientEvents.bushUnmute}
+ * @emits {@link BotClientEvents.customUnmute}
*/
- bushUnmute(options: BushPunishmentOptions): Promise;
+ customUnmute(options: CustomPunishmentOptions): Promise;
/**
* Kick the user, create a modlog entry, and dm the user.
* @param options Options for kicking the user.
* @returns A status message for kicking the user.
- * @emits {@link BushClientEvents.bushKick}
+ * @emits {@link BotClientEvents.customKick}
*/
- bushKick(options: BushPunishmentOptions): Promise;
+ customKick(options: CustomPunishmentOptions): Promise;
/**
* Ban the user, create a modlog entry, create a punishment entry, and dm the user.
* @param options Options for banning the user.
* @returns A status message for banning the user.
- * @emits {@link BushClientEvents.bushBan}
+ * @emits {@link BotClientEvents.customBan}
*/
- bushBan(options: BushBanOptions): Promise>;
+ customBan(options: CustomBanOptions): Promise>;
/**
* Prevents a user from speaking in a channel.
* @param options Options for blocking the user.
*/
- bushBlock(options: BlockOptions): Promise;
+ customBlock(options: BlockOptions): Promise;
/**
* Allows a user to speak in a channel.
* @param options Options for unblocking the user.
*/
- bushUnblock(options: UnblockOptions): Promise;
+ customUnblock(options: UnblockOptions): Promise;
/**
* Mutes a user using discord's timeout feature.
* @param options Options for timing out the user.
*/
- bushTimeout(options: BushTimeoutOptions): Promise;
+ customTimeout(options: CustomTimeoutOptions): Promise;
/**
* Removes a timeout from a user.
* @param options Options for removing the timeout.
*/
- bushRemoveTimeout(options: BushPunishmentOptions): Promise;
+ customRemoveTimeout(options: CustomPunishmentOptions): Promise;
/**
* Whether or not the user is an owner of the bot.
*/
@@ -121,7 +133,7 @@ export class ExtendedGuildMember extends GuildMember {
* @param sendFooter Whether or not to send the guild's punishment footer with the dm.
* @returns Whether or not the dm was sent successfully.
*/
- public override async bushPunishDM(
+ public override async customPunishDM(
punishment: PunishmentTypeDM,
reason?: string | null,
duration?: number,
@@ -144,9 +156,9 @@ export class ExtendedGuildMember extends GuildMember {
* Warn the user, create a modlog entry, and send a dm to the user.
* @param options Options for warning the user.
* @returns An object with the result of the warning, and the case number of the warn.
- * @emits {@link BushClientEvents.bushWarn}
+ * @emits {@link BotClientEvents.warnMember}
*/
- public override async bushWarn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }> {
+ public override async customWarn(options: CustomPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }> {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
@@ -172,7 +184,7 @@ export class ExtendedGuildMember extends GuildMember {
if (!options.silent) {
// dm user
- const dmSuccess = await this.bushPunishDM('warned', options.reason);
+ const dmSuccess = await this.customPunishDM('warned', options.reason);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return { result: warnResponse.DM_ERROR, caseNum: result.caseNum };
}
@@ -180,7 +192,7 @@ export class ExtendedGuildMember extends GuildMember {
return { result: warnResponse.SUCCESS, caseNum: result.caseNum };
})();
if (!([warnResponse.MODLOG_ERROR] as const).includes(ret.result) && !options.silent)
- this.client.emit('bushWarn', this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!);
+ this.client.emit(TanzaniteEvent.Warn, this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!);
return ret;
}
@@ -188,9 +200,9 @@ export class ExtendedGuildMember extends GuildMember {
* Add a role to the user, if it is a punishment create a modlog entry, and create a punishment entry if it is temporary or a punishment.
* @param options Options for adding a role to the user.
* @returns A status message for adding the add.
- * @emits {@link BushClientEvents.bushPunishRole}
+ * @emits {@link BotClientEvents.punishRole}
*/
- public override async bushAddRole(options: AddRoleOptions): Promise {
+ public override async customAddRole(options: AddRoleOptions): Promise {
// checks
if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) return addRoleResponse.MISSING_PERMISSIONS;
const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator);
@@ -244,7 +256,7 @@ export class ExtendedGuildMember extends GuildMember {
!options.silent
)
this.client.emit(
- 'bushPunishRole',
+ TanzaniteEvent.PunishRoleAdd,
this,
moderator,
this.guild,
@@ -261,9 +273,9 @@ export class ExtendedGuildMember extends GuildMember {
* Remove a role from the user, if it is a punishment create a modlog entry, and destroy a punishment entry if it was temporary or a punishment.
* @param options Options for removing a role from the user.
* @returns A status message for removing the role.
- * @emits {@link BushClientEvents.bushPunishRoleRemove}
+ * @emits {@link BotClientEvents.punishRoleRemove}
*/
- public override async bushRemoveRole(options: RemoveRoleOptions): Promise {
+ public override async customRemoveRole(options: RemoveRoleOptions): Promise {
// checks
if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) return removeRoleResponse.MISSING_PERMISSIONS;
const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator);
@@ -318,7 +330,7 @@ export class ExtendedGuildMember extends GuildMember {
!options.silent
)
this.client.emit(
- 'bushPunishRoleRemove',
+ TanzaniteEvent.PunishRoleRemove,
this,
moderator,
this.guild,
@@ -354,9 +366,9 @@ export class ExtendedGuildMember extends GuildMember {
* Mute the user, create a modlog entry, creates a punishment entry, and dms the user.
* @param options Options for muting the user.
* @returns A status message for muting the user.
- * @emits {@link BushClientEvents.bushMute}
+ * @emits {@link BotClientEvents.customMute}
*/
- public override async bushMute(options: BushTimedPunishmentOptions): Promise {
+ public override async customMute(options: CustomTimedPunishmentOptions): Promise {
// checks
const checks = await Moderation.checkMutePermissions(this.guild);
if (checks !== true) return checks;
@@ -410,7 +422,7 @@ export class ExtendedGuildMember extends GuildMember {
if (!options.silent) {
// dm user
- const dmSuccess = await this.bushPunishDM('muted', options.reason, options.duration ?? 0, modlog.id);
+ const dmSuccess = await this.customPunishDM('muted', options.reason, options.duration ?? 0, modlog.id);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return muteResponse.DM_ERROR;
}
@@ -423,7 +435,7 @@ export class ExtendedGuildMember extends GuildMember {
!options.silent
)
this.client.emit(
- 'bushMute',
+ TanzaniteEvent.Mute,
this,
moderator,
this.guild,
@@ -440,9 +452,9 @@ export class ExtendedGuildMember extends GuildMember {
* Unmute the user, create a modlog entry, remove the punishment entry, and dm the user.
* @param options Options for unmuting the user.
* @returns A status message for unmuting the user.
- * @emits {@link BushClientEvents.bushUnmute}
+ * @emits {@link BotClientEvents.customUnmute}
*/
- public override async bushUnmute(options: BushPunishmentOptions): Promise {
+ public override async customUnmute(options: CustomPunishmentOptions): Promise {
// checks
const checks = await Moderation.checkMutePermissions(this.guild);
if (checks !== true) return checks;
@@ -492,7 +504,7 @@ export class ExtendedGuildMember extends GuildMember {
if (!options.silent) {
// dm user
- const dmSuccess = await this.bushPunishDM('unmuted', options.reason, undefined, '', false);
+ const dmSuccess = await this.customPunishDM('unmuted', options.reason, undefined, '', false);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return unmuteResponse.DM_ERROR;
}
@@ -507,7 +519,7 @@ export class ExtendedGuildMember extends GuildMember {
!options.silent
)
this.client.emit(
- 'bushUnmute',
+ TanzaniteEvent.Unmute,
this,
moderator,
this.guild,
@@ -523,9 +535,9 @@ export class ExtendedGuildMember extends GuildMember {
* Kick the user, create a modlog entry, and dm the user.
* @param options Options for kicking the user.
* @returns A status message for kicking the user.
- * @emits {@link BushClientEvents.bushKick}
+ * @emits {@link BotClientEvents.customKick}
*/
- public override async bushKick(options: BushPunishmentOptions): Promise {
+ public override async customKick(options: CustomPunishmentOptions): Promise {
// checks
if (!this.guild.members.me?.permissions.has(PermissionFlagsBits.KickMembers) || !this.kickable)
return kickResponse.MISSING_PERMISSIONS;
@@ -550,7 +562,7 @@ export class ExtendedGuildMember extends GuildMember {
caseID = modlog.id;
// dm user
- const dmSuccess = options.silent ? null : await this.bushPunishDM('kicked', options.reason, undefined, modlog.id);
+ const dmSuccess = options.silent ? null : await this.customPunishDM('kicked', options.reason, undefined, modlog.id);
dmSuccessEvent = dmSuccess ?? undefined;
// kick
@@ -562,7 +574,7 @@ export class ExtendedGuildMember extends GuildMember {
})();
if (!([kickResponse.ACTION_ERROR, kickResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent)
this.client.emit(
- 'bushKick',
+ TanzaniteEvent.Kick,
this,
moderator,
this.guild,
@@ -578,9 +590,11 @@ export class ExtendedGuildMember extends GuildMember {
* Ban the user, create a modlog entry, create a punishment entry, and dm the user.
* @param options Options for banning the user.
* @returns A status message for banning the user.
- * @emits {@link BushClientEvents.bushBan}
+ * @emits {@link BotClientEvents.customBan}
*/
- public override async bushBan(options: BushBanOptions): Promise> {
+ public override async customBan(
+ options: CustomBanOptions
+ ): Promise> {
// checks
if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.BanMembers) || !this.bannable)
return banResponse.MISSING_PERMISSIONS;
@@ -591,7 +605,7 @@ export class ExtendedGuildMember extends GuildMember {
if (!moderator) return banResponse.CANNOT_RESOLVE_USER;
// ignore result, they should still be banned even if their mute cannot be removed
- await this.bushUnmute({
+ await this.customUnmute({
reason: 'User is about to be banned, a mute is no longer necessary.',
moderator: this.guild.members.me!,
silent: true
@@ -616,7 +630,7 @@ export class ExtendedGuildMember extends GuildMember {
// dm user
const dmSuccess = options.silent
? null
- : await this.bushPunishDM('banned', options.reason, options.duration ?? 0, modlog.id);
+ : await this.customPunishDM('banned', options.reason, options.duration ?? 0, modlog.id);
dmSuccessEvent = dmSuccess ?? undefined;
// ban
@@ -645,7 +659,7 @@ export class ExtendedGuildMember extends GuildMember {
!options.silent
)
this.client.emit(
- 'bushBan',
+ TanzaniteEvent.Ban,
this,
moderator,
this.guild,
@@ -662,7 +676,7 @@ export class ExtendedGuildMember extends GuildMember {
* Prevents a user from speaking in a channel.
* @param options Options for blocking the user.
*/
- public override async bushBlock(options: BlockOptions): Promise {
+ public override async customBlock(options: BlockOptions): Promise {
const channel = this.guild.channels.resolve(options.channel);
if (!channel || (!channel.isTextBased() && !channel.isThread())) return blockResponse.INVALID_CHANNEL;
@@ -737,7 +751,7 @@ export class ExtendedGuildMember extends GuildMember {
!options.silent
)
this.client.emit(
- 'bushBlock',
+ TanzaniteEvent.Block,
this,
moderator,
this.guild,
@@ -755,7 +769,7 @@ export class ExtendedGuildMember extends GuildMember {
* Allows a user to speak in a channel.
* @param options Options for unblocking the user.
*/
- public override async bushUnblock(options: UnblockOptions): Promise {
+ public override async customUnblock(options: UnblockOptions): Promise {
const _channel = this.guild.channels.resolve(options.channel);
if (!_channel || (_channel.type !== ChannelType.GuildText && !_channel.isThread())) return unblockResponse.INVALID_CHANNEL;
const channel = _channel as GuildTextBasedChannel;
@@ -828,7 +842,7 @@ export class ExtendedGuildMember extends GuildMember {
!options.silent
)
this.client.emit(
- 'bushUnblock',
+ TanzaniteEvent.Unblock,
this,
moderator,
this.guild,
@@ -845,7 +859,7 @@ export class ExtendedGuildMember extends GuildMember {
* Mutes a user using discord's timeout feature.
* @param options Options for timing out the user.
*/
- public override async bushTimeout(options: BushTimeoutOptions): Promise {
+ public override async customTimeout(options: CustomTimeoutOptions): Promise {
// checks
if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ModerateMembers)) return timeoutResponse.MISSING_PERMISSIONS;
@@ -883,7 +897,7 @@ export class ExtendedGuildMember extends GuildMember {
if (!options.silent) {
// dm user
- const dmSuccess = await this.bushPunishDM('timedout', options.reason, options.duration, modlog.id);
+ const dmSuccess = await this.customPunishDM('timedout', options.reason, options.duration, modlog.id);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return timeoutResponse.DM_ERROR;
}
@@ -893,7 +907,7 @@ export class ExtendedGuildMember extends GuildMember {
if (!([timeoutResponse.ACTION_ERROR, timeoutResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent)
this.client.emit(
- 'bushTimeout',
+ TanzaniteEvent.Timeout,
this,
moderator,
this.guild,
@@ -910,7 +924,7 @@ export class ExtendedGuildMember extends GuildMember {
* Removes a timeout from a user.
* @param options Options for removing the timeout.
*/
- public override async bushRemoveTimeout(options: BushPunishmentOptions): Promise {
+ public override async customRemoveTimeout(options: CustomPunishmentOptions): Promise {
// checks
if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ModerateMembers))
return removeTimeoutResponse.MISSING_PERMISSIONS;
@@ -944,7 +958,7 @@ export class ExtendedGuildMember extends GuildMember {
if (!options.silent) {
// dm user
- const dmSuccess = await this.bushPunishDM('untimedout', options.reason, undefined, '', false);
+ const dmSuccess = await this.customPunishDM('untimedout', options.reason, undefined, '', false);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return removeTimeoutResponse.DM_ERROR;
}
@@ -954,7 +968,7 @@ export class ExtendedGuildMember extends GuildMember {
if (!([removeTimeoutResponse.ACTION_ERROR, removeTimeoutResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent)
this.client.emit(
- 'bushRemoveTimeout',
+ TanzaniteEvent.RemoveTimeout,
this,
moderator,
this.guild,
@@ -984,7 +998,7 @@ export class ExtendedGuildMember extends GuildMember {
/**
* Options for punishing a user.
*/
-export interface BushPunishmentOptions {
+export interface CustomPunishmentOptions {
/**
* The reason for the punishment.
*/
@@ -1009,7 +1023,7 @@ export interface BushPunishmentOptions {
/**
* Punishment options for punishments that can be temporary.
*/
-export interface BushTimedPunishmentOptions extends BushPunishmentOptions {
+export interface CustomTimedPunishmentOptions extends CustomPunishmentOptions {
/**
* The duration of the punishment.
*/
@@ -1019,7 +1033,7 @@ export interface BushTimedPunishmentOptions extends BushPunishmentOptions {
/**
* Options for a role add punishment.
*/
-export interface AddRoleOptions extends BushTimedPunishmentOptions {
+export interface AddRoleOptions extends CustomTimedPunishmentOptions {
/**
* The role to add to the user.
*/
@@ -1034,7 +1048,7 @@ export interface AddRoleOptions extends BushTimedPunishmentOptions {
/**
* Options for a role remove punishment.
*/
-export interface RemoveRoleOptions extends BushTimedPunishmentOptions {
+export interface RemoveRoleOptions extends CustomTimedPunishmentOptions {
/**
* The role to remove from the user.
*/
@@ -1049,7 +1063,7 @@ export interface RemoveRoleOptions extends BushTimedPunishmentOptions {
/**
* Options for banning a user.
*/
-export interface BushBanOptions extends BushTimedPunishmentOptions {
+export interface CustomBanOptions extends CustomTimedPunishmentOptions {
/**
* The number of days to delete the user's messages for.
*/
@@ -1059,7 +1073,7 @@ export interface BushBanOptions extends BushTimedPunishmentOptions {
/**
* Options for blocking a user from a channel.
*/
-export interface BlockOptions extends BushTimedPunishmentOptions {
+export interface BlockOptions extends CustomTimedPunishmentOptions {
/**
* The channel to block the user from.
*/
@@ -1069,7 +1083,7 @@ export interface BlockOptions extends BushTimedPunishmentOptions {
/**
* Options for unblocking a user from a channel.
*/
-export interface UnblockOptions extends BushPunishmentOptions {
+export interface UnblockOptions extends CustomPunishmentOptions {
/**
* The channel to unblock the user from.
*/
@@ -1079,7 +1093,7 @@ export interface UnblockOptions extends BushPunishmentOptions {
/**
* Punishment options for punishments that can be temporary.
*/
-export interface BushTimeoutOptions extends BushPunishmentOptions {
+export interface CustomTimeoutOptions extends CustomPunishmentOptions {
/**
* The duration of the punishment.
*/
@@ -1251,5 +1265,5 @@ export type TimeoutResponse = ValueOf;
export type RemoveTimeoutResponse = ValueOf;
/**
- * @typedef {BushClientEvents} VSCodePleaseDontRemove
+ * @typedef {BotClientEvents} VSCodePleaseDontRemove
*/
diff --git a/lib/extensions/discord.js/ExtendedUser.ts b/lib/extensions/discord.js/ExtendedUser.ts
index 23de523..65b14c7 100644
--- a/lib/extensions/discord.js/ExtendedUser.ts
+++ b/lib/extensions/discord.js/ExtendedUser.ts
@@ -1,4 +1,4 @@
-import { User, type Partialize } from 'discord.js';
+import { User } from 'discord.js';
declare module 'discord.js' {
export interface User {
@@ -13,8 +13,6 @@ declare module 'discord.js' {
}
}
-export type PartialBushUser = Partialize;
-
/**
* Represents a user on Discord.
*/
diff --git a/lib/index.ts b/lib/index.ts
index ca23177..fc7bb4c 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -1,10 +1,9 @@
import './global.js';
-
export * from './automod/AutomodShared.js';
export * from './automod/MemberAutomod.js';
export * from './automod/MessageAutomod.js';
export * from './automod/PresenceAutomod.js';
-export * from './common/BushCache.js';
+export * from './common/BotCache.js';
export * from './common/ButtonPaginator.js';
export * from './common/CanvasProgressBar.js';
export * from './common/ConfirmationPrompt.js';
@@ -20,42 +19,32 @@ export type {
RemovePunishmentEntryOptions,
SimpleCreateModLogEntryOptions
} from './common/Moderation.js';
-export * from './extensions/discord-akairo/BushArgumentTypeCaster.js';
-export * from './extensions/discord-akairo/BushClient.js';
-export * from './extensions/discord-akairo/BushCommand.js';
-export * from './extensions/discord-akairo/BushCommandHandler.js';
-export * from './extensions/discord-akairo/BushInhibitor.js';
-export * from './extensions/discord-akairo/BushInhibitorHandler.js';
-export * from './extensions/discord-akairo/BushListener.js';
-export * from './extensions/discord-akairo/BushListenerHandler.js';
-export * from './extensions/discord-akairo/BushTask.js';
-export * from './extensions/discord-akairo/BushTaskHandler.js';
+export * from './extensions/discord-akairo/BotArgumentTypeCaster.js';
+export * from './extensions/discord-akairo/BotCommand.js';
+export * from './extensions/discord-akairo/BotCommandHandler.js';
+export * from './extensions/discord-akairo/BotInhibitor.js';
+export * from './extensions/discord-akairo/BotInhibitorHandler.js';
+export * from './extensions/discord-akairo/BotListener.js';
+export * from './extensions/discord-akairo/BotListenerHandler.js';
+export * from './extensions/discord-akairo/BotTask.js';
+export * from './extensions/discord-akairo/BotTaskHandler.js';
export * from './extensions/discord-akairo/SlashMessage.js';
-export type { BushClientEvents } from './extensions/discord.js/BushClientEvents.js';
+export * from './extensions/discord-akairo/TanzaniteClient.js';
+export * from './extensions/discord.js/BotClientEvents.js';
export * from './extensions/discord.js/ExtendedGuild.js';
export * from './extensions/discord.js/ExtendedGuildMember.js';
export * from './extensions/discord.js/ExtendedMessage.js';
export * from './extensions/discord.js/ExtendedUser.js';
-export * from './models/BaseModel.js';
-export * from './models/instance/ActivePunishment.js';
-export * from './models/instance/Guild.js';
-export * from './models/instance/Highlight.js';
-export * from './models/instance/Level.js';
-export * from './models/instance/ModLog.js';
-export * from './models/instance/Reminder.js';
-export * from './models/instance/StickyRole.js';
-export * from './models/shared/Global.js';
-export * from './models/shared/MemberCount.js';
-export * from './models/shared/Shared.js';
-export * from './models/shared/Stat.js';
-export type { BushInspectOptions } from './types/BushInspectOptions.js';
+export * from './models/index.js';
export type { CodeBlockLang } from './types/CodeBlockLang.js';
+export type { CustomInspectOptions } from './types/InspectOptions.js';
+export * from './types/misc.js';
export * from './utils/AllowedMentions.js';
export * as Arg from './utils/Arg.js';
-export * from './utils/BushConstants.js';
-export * from './utils/BushLogger.js';
-export * from './utils/BushUtils.js';
+export * from './utils/Constants.js';
export * from './utils/ErrorHandler.js';
export * as Format from './utils/Format.js';
export * from './utils/FormatResponse.js';
+export * from './utils/Logger.js';
export * from './utils/UpdateCache.js';
+export * from './utils/Utils.js';
diff --git a/lib/models/instance/Guild.ts b/lib/models/instance/Guild.ts
index 763485f..72091ca 100644
--- a/lib/models/instance/Guild.ts
+++ b/lib/models/instance/Guild.ts
@@ -1,6 +1,6 @@
import config from '#config';
import { BadWordDetails } from '#lib/automod/AutomodShared.js';
-import { type BushClient } from '#lib/extensions/discord-akairo/BushClient.js';
+import { type TanzaniteClient } from '#lib/extensions/discord-akairo/TanzaniteClient.js';
import { ChannelType, Constants, type Snowflake } from 'discord.js';
import { DataTypes, type Sequelize } from 'sequelize';
import { BaseModel } from '../BaseModel.js';
@@ -145,7 +145,7 @@ export class Guild extends BaseModel i
* Initializes the model.
* @param sequelize The sequelize instance.
*/
- public static initModel(sequelize: Sequelize, client: BushClient): void {
+ public static initModel(sequelize: Sequelize, client: TanzaniteClient): void {
Guild.init(
{
id: { type: DataTypes.STRING, primaryKey: true },
diff --git a/lib/types/BushInspectOptions.ts b/lib/types/BushInspectOptions.ts
deleted file mode 100644
index 30ed01a..0000000
--- a/lib/types/BushInspectOptions.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { type InspectOptions } from 'util';
-
-/**
- * {@link https://nodejs.org/api/util.html#utilinspectobject-showhidden-depth-colors util.inspect Options Documentation}
- */
-export interface BushInspectOptions extends InspectOptions {
- /**
- * If `true`, object's non-enumerable symbols and properties are included in the
- * formatted result. [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)
- * and [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) entries
- * are also included as well as user defined prototype properties (excluding method properties).
- *
- * @default false
- */
- showHidden?: boolean | undefined;
-
- /**
- * Specifies the number of times to recurse while formatting `object`. This is useful
- * for inspecting large objects. To recurse up to the maximum call stack size pass
- * `Infinity` or `null`.
- *
- * @default 2
- */
- depth?: number | null | undefined;
-
- /**
- * If `true`, the output is styled with ANSI color codes. Colors are customizable. See
- * [Customizing util.inspect colors](https://nodejs.org/api/util.html#util_customizing_util_inspect_colors).
- *
- * @default false
- */
- colors?: boolean | undefined;
-
- /**
- * If `false`, `[util.inspect.custom](depth, opts)` functions are not invoked.
- *
- * @default true
- */
- customInspect?: boolean | undefined;
-
- /**
- * If `true`, `Proxy` inspection includes the
- * [`target` and `handler`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology)
- * objects.
- *
- * @default false
- */
- showProxy?: boolean | undefined;
-
- /**
- * Specifies the maximum number of `Array`, [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray),
- * [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) and
- * [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) elements to
- * include when formatting. Set to `null` or `Infinity` to show all elements.
- * Set to `0` or negative to show no elements.
- *
- * @default 100
- */
- maxArrayLength?: number | null | undefined;
-
- /**
- * Specifies the maximum number of characters to include when formatting. Set to
- * `null` or `Infinity` to show all elements. Set to `0` or negative to show no
- * characters.
- *
- * @default 10000
- */
- maxStringLength?: number | null | undefined;
-
- /**
- * The length at which input values are split across multiple lines. Set to
- * `Infinity` to format the input as a single line (in combination with compact set
- * to `true` or any number >= `1`).
- *
- * @default 80
- */
- breakLength?: number | undefined;
-
- /**
- * Setting this to `false` causes each object key to be displayed on a new line. It
- * will break on new lines in text that is longer than `breakLength`. If set to a
- * number, the most `n` inner elements are united on a single line as long as all
- * properties fit into `breakLength`. Short array elements are also grouped together.
- *
- * @default 3
- */
- compact?: boolean | number | undefined;
-
- /**
- * If set to `true` or a function, all properties of an object, and `Set` and `Map`
- * entries are sorted in the resulting string. If set to `true` the
- * [default sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) is used.
- * If set to a function, it is used as a
- * [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters).
- *
- * @default false
- */
- sorted?: boolean | ((a: string, b: string) => number) | undefined;
-
- /**
- * If set to `true`, getters are inspected. If set to `'get'`, only getters without a
- * corresponding setter are inspected. If set to `'set'`, only getters with a
- * corresponding setter are inspected. This might cause side effects depending on
- * the getter function.
- *
- * @default false
- */
- getters?: 'get' | 'set' | boolean | undefined;
-
- /**
- * If set to `true`, an underscore is used to separate every three digits in all bigints and numbers.
- *
- * @default false
- */
- numericSeparator?: boolean;
-
- /**
- * Whether or not to inspect strings.
- *
- * @default false
- */
- inspectStrings?: boolean;
-}
diff --git a/lib/types/InspectOptions.ts b/lib/types/InspectOptions.ts
new file mode 100644
index 0000000..1113f2b
--- /dev/null
+++ b/lib/types/InspectOptions.ts
@@ -0,0 +1,127 @@
+import { InspectOptions } from 'node:util';
+
+declare module 'util' {
+ /**
+ * {@link https://nodejs.org/api/util.html#utilinspectobject-showhidden-depth-colors util.inspect Options Documentation}
+ */
+ export interface InspectOptions {
+ /**
+ * If `true`, object's non-enumerable symbols and properties are included in the
+ * formatted result. [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)
+ * and [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) entries
+ * are also included as well as user defined prototype properties (excluding method properties).
+ *
+ * @default false
+ */
+ showHidden?: boolean | undefined;
+
+ /**
+ * Specifies the number of times to recurse while formatting `object`. This is useful
+ * for inspecting large objects. To recurse up to the maximum call stack size pass
+ * `Infinity` or `null`.
+ *
+ * @default 2
+ */
+ depth?: number | null | undefined;
+
+ /**
+ * If `true`, the output is styled with ANSI color codes. Colors are customizable. See
+ * [Customizing util.inspect colors](https://nodejs.org/api/util.html#util_customizing_util_inspect_colors).
+ *
+ * @default false
+ */
+ colors?: boolean | undefined;
+
+ /**
+ * If `false`, `[util.inspect.custom](depth, opts)` functions are not invoked.
+ *
+ * @default true
+ */
+ customInspect?: boolean | undefined;
+
+ /**
+ * If `true`, `Proxy` inspection includes the
+ * [`target` and `handler`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology)
+ * objects.
+ *
+ * @default false
+ */
+ showProxy?: boolean | undefined;
+
+ /**
+ * Specifies the maximum number of `Array`, [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray),
+ * [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) and
+ * [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) elements to
+ * include when formatting. Set to `null` or `Infinity` to show all elements.
+ * Set to `0` or negative to show no elements.
+ *
+ * @default 100
+ */
+ maxArrayLength?: number | null | undefined;
+
+ /**
+ * Specifies the maximum number of characters to include when formatting. Set to
+ * `null` or `Infinity` to show all elements. Set to `0` or negative to show no
+ * characters.
+ *
+ * @default 10000
+ */
+ maxStringLength?: number | null | undefined;
+
+ /**
+ * The length at which input values are split across multiple lines. Set to
+ * `Infinity` to format the input as a single line (in combination with compact set
+ * to `true` or any number >= `1`).
+ *
+ * @default 80
+ */
+ breakLength?: number | undefined;
+
+ /**
+ * Setting this to `false` causes each object key to be displayed on a new line. It
+ * will break on new lines in text that is longer than `breakLength`. If set to a
+ * number, the most `n` inner elements are united on a single line as long as all
+ * properties fit into `breakLength`. Short array elements are also grouped together.
+ *
+ * @default 3
+ */
+ compact?: boolean | number | undefined;
+
+ /**
+ * If set to `true` or a function, all properties of an object, and `Set` and `Map`
+ * entries are sorted in the resulting string. If set to `true` the
+ * [default sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) is used.
+ * If set to a function, it is used as a
+ * [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters).
+ *
+ * @default false
+ */
+ sorted?: boolean | ((a: string, b: string) => number) | undefined;
+
+ /**
+ * If set to `true`, getters are inspected. If set to `'get'`, only getters without a
+ * corresponding setter are inspected. If set to `'set'`, only getters with a
+ * corresponding setter are inspected. This might cause side effects depending on
+ * the getter function.
+ *
+ * @default false
+ */
+ getters?: 'get' | 'set' | boolean | undefined;
+
+ /**
+ * If set to `true`, an underscore is used to separate every three digits in all bigints and numbers.
+ *
+ * @default false
+ */
+ numericSeparator?: boolean;
+ }
+}
+
+export interface CustomInspectOptions extends InspectOptions {
+ /**
+ * Whether or not to inspect strings.
+ *
+ * @default false
+ */
+ inspectStrings?: boolean;
+}
diff --git a/lib/types/misc.ts b/lib/types/misc.ts
new file mode 100644
index 0000000..5bf760c
--- /dev/null
+++ b/lib/types/misc.ts
@@ -0,0 +1,14 @@
+import type {
+ InteractionReplyOptions,
+ MessageEditOptions,
+ MessageOptions,
+ MessagePayload,
+ ReplyMessageOptions,
+ WebhookEditMessageOptions
+} from 'discord.js';
+
+export type ReplyMessageType = string | MessagePayload | ReplyMessageOptions;
+export type EditMessageType = string | MessageEditOptions | MessagePayload;
+export type SlashSendMessageType = string | MessagePayload | InteractionReplyOptions;
+export type SlashEditMessageType = string | MessagePayload | WebhookEditMessageOptions;
+export type SendMessageType = string | MessagePayload | MessageOptions;
diff --git a/lib/utils/Arg.ts b/lib/utils/Arg.ts
index d362225..99060fb 100644
--- a/lib/utils/Arg.ts
+++ b/lib/utils/Arg.ts
@@ -1,7 +1,7 @@
import {
- type BaseBushArgumentType,
- type BushArgumentType,
- type BushArgumentTypeCaster,
+ type BaseBotArgumentType,
+ type BotArgumentType,
+ type BotArgumentTypeCaster,
type CommandMessage,
type SlashMessage
} from '#lib';
@@ -160,33 +160,33 @@ export function withInput(type: AT | ATC): ATC {
return Argument.withInput(type as any);
}
-type BushArgumentTypeCasterReturn = R extends BushArgumentTypeCaster ? S : R;
+type CustomArgumentTypeCasterReturn = R extends BotArgumentTypeCaster ? S : R;
/** ```ts
- * = BushArgumentTypeCaster
+ * = CustomArgumentTypeCaster
* ``` */
-type ATC = BushArgumentTypeCaster;
+type ATC = BotArgumentTypeCaster;
/** ```ts
- * keyof BaseBushArgumentType
+ * keyof BaseCustomArgumentType
* ``` */
-type KBAT = keyof BaseBushArgumentType;
+type KBAT = keyof BaseBotArgumentType;
/** ```ts
- * = BushArgumentTypeCasterReturn
+ * = CustomArgumentTypeCasterReturn
* ``` */
-type ATCR = BushArgumentTypeCasterReturn;
+type ATCR = CustomArgumentTypeCasterReturn;
/** ```ts
- * BushArgumentType
+ * CustomArgumentType
* ``` */
-type AT = BushArgumentType;
+type AT = BotArgumentType;
/** ```ts
- * BaseBushArgumentType
+ * BaseCustomArgumentType
* ``` */
-type BAT = BaseBushArgumentType;
+type BAT = BaseBotArgumentType;
/** ```ts
- * = BushArgumentTypeCaster>
+ * = CustomArgumentTypeCaster>
* ``` */
-type ATCATCR = BushArgumentTypeCaster>;
+type ATCATCR = BotArgumentTypeCaster>;
/** ```ts
- * = BushArgumentTypeCaster
+ * = CustomArgumentTypeCaster
* ``` */
-type ATCBAT = BushArgumentTypeCaster;
+type ATCBAT = BotArgumentTypeCaster;
diff --git a/lib/utils/BotClientUtils.ts b/lib/utils/BotClientUtils.ts
new file mode 100644
index 0000000..a251bdf
--- /dev/null
+++ b/lib/utils/BotClientUtils.ts
@@ -0,0 +1,496 @@
+import { ConfigChannelKey } from '#config';
+import type { CodeBlockLang, CustomInspectOptions } from '#lib';
+import { GlobalCache, SharedCache } from '#lib/common/BotCache.js';
+import { CommandMessage } from '#lib/extensions/discord-akairo/BotCommand.js';
+import { SlashMessage } from '#lib/extensions/discord-akairo/SlashMessage.js';
+import { Global, Shared } from '#lib/models/index.js';
+import assert from 'assert/strict';
+import {
+ cleanCodeBlockContent,
+ DMChannel,
+ escapeCodeBlock,
+ GuildMember,
+ Message,
+ PartialDMChannel,
+ Routes,
+ TextBasedChannel,
+ ThreadMember,
+ User,
+ type APIMessage,
+ type Client,
+ type Snowflake,
+ type UserResolvable
+} from 'discord.js';
+import _ from 'lodash';
+import { emojis, Pronoun, PronounCode, pronounMapping, regex } from './Constants.js';
+import { generateErrorEmbed } from './ErrorHandler.js';
+import { addOrRemoveFromArray, formatError, inspect } from './Utils.js';
+
+/**
+ * Utilities that require access to the client.
+ */
+export class BotClientUtils {
+ /**
+ * The hastebin urls used to post to hastebin, attempts to post in order
+ */
+ #hasteURLs: string[] = [
+ 'https://hst.sh',
+ // 'https://hasteb.in',
+ 'https://hastebin.com',
+ 'https://mystb.in',
+ 'https://haste.clicksminuteper.net',
+ 'https://paste.pythondiscord.com',
+ 'https://haste.unbelievaboat.com'
+ // 'https://haste.tyman.tech'
+ ];
+
+ public constructor(private readonly client: Client) {}
+
+ /**
+ * Maps an array of user ids to user objects.
+ * @param ids The list of IDs to map
+ * @returns The list of users mapped
+ */
+ public async mapIDs(ids: Snowflake[]): Promise {
+ return await Promise.all(ids.map((id) => this.client.users.fetch(id)));
+ }
+
+ /**
+ * Posts text to hastebin
+ * @param content The text to post
+ * @returns The url of the posted text
+ */
+ public async haste(content: string, substr = false): Promise {
+ let isSubstr = false;
+ if (content.length > 400_000 && !substr) {
+ void this.handleError('haste', new Error(`content over 400,000 characters (${content.length.toLocaleString()})`));
+ return { error: 'content too long' };
+ } else if (content.length > 400_000) {
+ content = content.substring(0, 400_000);
+ isSubstr = true;
+ }
+ for (const url of this.#hasteURLs) {
+ try {
+ const res = (await (await fetch(`${url}/documents`, { method: 'POST', body: content })).json()) as HastebinRes;
+ return { url: `${url}/${res.key}`, error: isSubstr ? 'substr' : undefined };
+ } catch {
+ void this.client.console.error('haste', `Unable to upload haste to ${url}`);
+ }
+ }
+ return { error: 'unable to post' };
+ }
+
+ /**
+ * Resolves a user-provided string into a user object, if possible
+ * @param text The text to try and resolve
+ * @returns The user resolved or null
+ */
+ public async resolveUserAsync(text: string): Promise {
+ const idReg = /\d{17,19}/;
+ const idMatch = text.match(idReg);
+ if (idMatch) {
+ try {
+ return await this.client.users.fetch(text as Snowflake);
+ } catch {}
+ }
+ const mentionReg = /<@!?(?\d{17,19})>/;
+ const mentionMatch = text.match(mentionReg);
+ if (mentionMatch) {
+ try {
+ return await this.client.users.fetch(mentionMatch.groups!.id as Snowflake);
+ } catch {}
+ }
+ const user = this.client.users.cache.find((u) => u.username === text);
+ if (user) return user;
+ return null;
+ }
+
+ /**
+ * Surrounds text in a code block with the specified language and puts it in a hastebin if its too long.
+ * * Embed Description Limit = 4096 characters
+ * * Embed Field Limit = 1024 characters
+ * @param code The content of the code block.
+ * @param length The maximum length of the code block.
+ * @param language The language of the code.
+ * @param substr Whether or not to substring the code if it is too long.
+ * @returns The generated code block
+ */
+ public async codeblock(code: string, length: number, language: CodeBlockLang | '' = '', substr = false): Promise {
+ let hasteOut = '';
+ code = escapeCodeBlock(code);
+ const prefix = `\`\`\`${language}\n`;
+ const suffix = '\n```';
+ if (code.length + (prefix + suffix).length >= length) {
+ const haste_ = await this.haste(code, substr);
+ hasteOut = `Too large to display. ${
+ haste_.url
+ ? `Hastebin: ${haste_.url}${language ? `.${language}` : ''}${haste_.error ? ` - ${haste_.error}` : ''}`
+ : `${emojis.error} Hastebin: ${haste_.error}`
+ }`;
+ }
+
+ const FormattedHaste = hasteOut.length ? `\n${hasteOut}` : '';
+ const shortenedCode = hasteOut ? code.substring(0, length - (prefix + FormattedHaste + suffix).length) : code;
+ const code3 = code.length ? prefix + shortenedCode + suffix + FormattedHaste : prefix + suffix;
+ if (code3.length > length) {
+ void this.client.console.warn(`codeblockError`, `Required Length: ${length}. Actual Length: ${code3.length}`, true);
+ void this.client.console.warn(`codeblockError`, code3, true);
+ throw new Error('code too long');
+ }
+ return code3;
+ }
+
+ /**
+ * Maps the key of a credential with a readable version when redacting.
+ * @param key The key of the credential.
+ * @returns The readable version of the key or the original key if there isn't a mapping.
+ */
+ #mapCredential(key: string): string {
+ return (
+ {
+ token: 'Main Token',
+ devToken: 'Dev Token',
+ betaToken: 'Beta Token',
+ hypixelApiKey: 'Hypixel Api Key',
+ wolframAlphaAppId: 'Wolfram|Alpha App ID',
+ dbPassword: 'Database Password'
+ }[key] ?? key
+ );
+ }
+
+ /**
+ * Redacts credentials from a string.
+ * @param text The text to redact credentials from.
+ * @returns The redacted text.
+ */
+ public redact(text: string) {
+ for (const credentialName in { ...this.client.config.credentials, dbPassword: this.client.config.db.password }) {
+ const credential = { ...this.client.config.credentials, dbPassword: this.client.config.db.password }[
+ credentialName as keyof typeof this.client.config.credentials
+ ];
+ if (credential === null || credential === '') continue;
+ const replacement = this.#mapCredential(credentialName);
+ const escapeRegex = /[.*+?^${}()|[\]\\]/g;
+ text = text.replace(new RegExp(credential.toString().replace(escapeRegex, '\\$&'), 'g'), `[${replacement} Omitted]`);
+ text = text.replace(
+ new RegExp([...credential.toString()].reverse().join('').replace(escapeRegex, '\\$&'), 'g'),
+ `[${replacement} Omitted]`
+ );
+ }
+ return text;
+ }
+
+ /**
+ * Takes an any value, inspects it, redacts credentials, and puts it in a codeblock
+ * (and uploads to hast if the content is too long).
+ * @param input The object to be inspect, redacted, and put into a codeblock.
+ * @param language The language to make the codeblock.
+ * @param inspectOptions The options for {@link CustomClientUtil.inspect}.
+ * @param length The maximum length that the codeblock can be.
+ * @returns The generated codeblock.
+ */
+ public async inspectCleanRedactCodeblock(
+ input: any,
+ language?: CodeBlockLang | '',
+ inspectOptions?: CustomInspectOptions,
+ length = 1024
+ ) {
+ input = inspect(input, inspectOptions ?? undefined);
+ if (inspectOptions) inspectOptions.inspectStrings = undefined;
+ input = cleanCodeBlockContent(input);
+ input = this.redact(input);
+ return this.codeblock(input, length, language, true);
+ }
+
+ /**
+ * Takes an any value, inspects it, redacts credentials, and uploads it to haste.
+ * @param input The object to be inspect, redacted, and upload.
+ * @param inspectOptions The options for {@link BotClientUtils.inspect}.
+ * @returns The {@link HasteResults}.
+ */
+ public async inspectCleanRedactHaste(input: any, inspectOptions?: CustomInspectOptions): Promise {
+ input = inspect(input, inspectOptions ?? undefined);
+ input = this.redact(input);
+ return this.haste(input, true);
+ }
+
+ /**
+ * Takes an any value, inspects it and redacts credentials.
+ * @param input The object to be inspect and redacted.
+ * @param inspectOptions The options for {@link BotClientUtils.inspect}.
+ * @returns The redacted and inspected object.
+ */
+ public inspectAndRedact(input: any, inspectOptions?: CustomInspectOptions): string {
+ input = inspect(input, inspectOptions ?? undefined);
+ return this.redact(input);
+ }
+
+ /**
+ * Get the global cache.
+ */
+ public getGlobal(): GlobalCache;
+ /**
+ * Get a key from the global cache.
+ * @param key The key to get in the global cache.
+ */
+ public getGlobal(key: K): GlobalCache[K];
+ public getGlobal(key?: keyof GlobalCache) {
+ return key ? this.client.cache.global[key] : this.client.cache.global;
+ }
+
+ /**
+ * Get the shared cache.
+ */
+ public getShared(): SharedCache;
+ /**
+ * Get a key from the shared cache.
+ * @param key The key to get in the shared cache.
+ */
+ public getShared(key: K): SharedCache[K];
+ public getShared(key?: keyof SharedCache) {
+ return key ? this.client.cache.shared[key] : this.client.cache.shared;
+ }
+
+ /**
+ * Add or remove an element from an array stored in the Globals database.
+ * @param action Either `add` or `remove` an element.
+ * @param key The key of the element in the global cache to update.
+ * @param value The value to add/remove from the array.
+ */
+ public async insertOrRemoveFromGlobal(
+ action: 'add' | 'remove',
+ key: K,
+ value: Client['cache']['global'][K][0]
+ ): Promise {
+ const row =
+ (await Global.findByPk(this.client.config.environment)) ??
+ (await Global.create({ environment: this.client.config.environment }));
+ const oldValue: any[] = row[key];
+ const newValue = addOrRemoveFromArray(action, oldValue, value);
+ row[key] = newValue;
+ this.client.cache.global[key] = newValue;
+ return await row.save().catch((e) => this.handleError('insertOrRemoveFromGlobal', e));
+ }
+
+ /**
+ * Add or remove an element from an array stored in the Shared database.
+ * @param action Either `add` or `remove` an element.
+ * @param key The key of the element in the shared cache to update.
+ * @param value The value to add/remove from the array.
+ */
+ public async insertOrRemoveFromShared>(
+ action: 'add' | 'remove',
+ key: K,
+ value: Client['cache']['shared'][K][0]
+ ): Promise {
+ const row = (await Shared.findByPk(0)) ?? (await Shared.create());
+ const oldValue: any[] = row[key];
+ const newValue = addOrRemoveFromArray(action, oldValue, value);
+ row[key] = newValue;
+ this.client.cache.shared[key] = newValue;
+ return await row.save().catch((e) => this.handleError('insertOrRemoveFromShared', e));
+ }
+
+ /**
+ * Updates an element in the Globals database.
+ * @param key The key in the global cache to update.
+ * @param value The value to set the key to.
+ */
+ public async setGlobal(
+ key: K,
+ value: Client['cache']['global'][K]
+ ): Promise {
+ const row =
+ (await Global.findByPk(this.client.config.environment)) ??
+ (await Global.create({ environment: this.client.config.environment }));
+ row[key] = value;
+ this.client.cache.global[key] = value;
+ return await row.save().catch((e) => this.handleError('setGlobal', e));
+ }
+
+ /**
+ * Updates an element in the Shared database.
+ * @param key The key in the shared cache to update.
+ * @param value The value to set the key to.
+ */
+ public async setShared>(
+ key: K,
+ value: Client['cache']['shared'][K]
+ ): Promise {
+ const row = (await Shared.findByPk(0)) ?? (await Shared.create());
+ row[key] = value;
+ this.client.cache.shared[key] = value;
+ return await row.save().catch((e) => this.handleError('setShared', e));
+ }
+
+ /**
+ * Send a message in the error logging channel and console for an error.
+ * @param context
+ * @param error
+ */
+ public async handleError(context: string, error: Error) {
+ await this.client.console.error(_.camelCase(context), `An error occurred:\n${formatError(error, false)}`, false);
+ await this.client.console.channelError({
+ embeds: await generateErrorEmbed(this.client, { type: 'unhandledRejection', error: error, context })
+ });
+ }
+
+ /**
+ * Fetches a user from discord.
+ * @param user The user to fetch
+ * @returns Undefined if the user is not found, otherwise the user.
+ */
+ public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise {
+ if (user == null) return undefined;
+ const resolvedUser =
+ user instanceof User
+ ? user
+ : user instanceof GuildMember
+ ? user.user
+ : user instanceof ThreadMember
+ ? user.user
+ : user instanceof Message
+ ? user.author
+ : undefined;
+
+ return resolvedUser ?? (await this.client.users.fetch(user as Snowflake).catch(() => undefined));
+ }
+
+ /**
+ * Get the pronouns of a discord user from pronoundb.org
+ * @param user The user to retrieve the promises of.
+ * @returns The human readable pronouns of the user, or undefined if they do not have any.
+ */
+ public async getPronounsOf(user: User | Snowflake): Promise {
+ const _user = await this.resolveNonCachedUser(user);
+ if (!_user) throw new Error(`Cannot find user ${user}`);
+ const apiRes = (await fetch(`https://pronoundb.org/api/v1/lookup?platform=discord&id=${_user.id}`)
+ .then((p) => (p.ok ? p.json() : undefined))
+ .catch(() => undefined)) as { pronouns: PronounCode } | undefined;
+
+ if (!apiRes) return undefined;
+ assert(apiRes.pronouns);
+
+ return pronounMapping[apiRes.pronouns!]!;
+ }
+
+ /**
+ * Uploads an image to imgur.
+ * @param image The image to upload.
+ * @returns The url of the imgur.
+ */
+ public async uploadImageToImgur(image: string) {
+ const clientId = this.client.config.credentials.imgurClientId;
+
+ const formData = new FormData();
+ formData.append('type', 'base64');
+ formData.append('image', image);
+
+ const resp = (await fetch('https://api.imgur.com/3/upload', {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ Authorization: `Client-ID ${clientId}`
+ },
+ body: formData,
+ redirect: 'follow'
+ })
+ .then((p) => (p.ok ? p.json() : null))
+ .catch(() => null)) as { data: { link: string } } | null;
+
+ return resp?.data?.link ?? null;
+ }
+
+ /**
+ * Gets the prefix based off of the message.
+ * @param message The message to get the prefix from.
+ * @returns The prefix.
+ */
+ public prefix(message: CommandMessage | SlashMessage): string {
+ return message.util.isSlash
+ ? '/'
+ : this.client.config.isDevelopment
+ ? 'dev '
+ : message.util.parsed?.prefix ?? this.client.config.prefix;
+ }
+
+ public async resolveMessageLinks(content: string | null): Promise