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 --- .../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 +- 25 files changed, 1583 insertions(+), 1559 deletions(-) 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 (limited to 'lib/extensions') 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 && - !('slas