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/extensions/discord-akairo/BotCommand.ts | 597 ++++++++++++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 lib/extensions/discord-akairo/BotCommand.ts (limited to 'lib/extensions/discord-akairo/BotCommand.ts') 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; +}; -- cgit