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 --- 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 +++++++++++++++++++++ 59 files changed, 3809 insertions(+), 3783 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 (limited to 'lib') 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: bi