aboutsummaryrefslogtreecommitdiff
path: root/lib/extensions/discord-akairo/BotCommand.ts
diff options
context:
space:
mode:
authorIRONM00N <64110067+IRONM00N@users.noreply.github.com>2022-08-28 21:51:17 -0400
committerIRONM00N <64110067+IRONM00N@users.noreply.github.com>2022-08-28 21:51:17 -0400
commit14eb0e617b084080c4cffc5b781b311c65c5f928 (patch)
tree9aaf1734c1e739814a913afeda40c56b0f84df61 /lib/extensions/discord-akairo/BotCommand.ts
parent03b26d5f00422f3aaddce3db2186765863b1eca0 (diff)
downloadtanzanite-14eb0e617b084080c4cffc5b781b311c65c5f928.tar.gz
tanzanite-14eb0e617b084080c4cffc5b781b311c65c5f928.tar.bz2
tanzanite-14eb0e617b084080c4cffc5b781b311c65c5f928.zip
rebrand v3
Diffstat (limited to 'lib/extensions/discord-akairo/BotCommand.ts')
-rw-r--r--lib/extensions/discord-akairo/BotCommand.ts597
1 files changed, 597 insertions, 0 deletions
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<ArgumentOptions, 'type' | 'prompt'>, 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> | 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<CommandOptions, 'userPermissions' | 'clientPermissions' | 'args'>,
+ 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<BaseBotCommandOptions, 'helpArgs'> | Omit<BaseBotCommandOptions, 'args'>;
+
+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<CommandOptions & ExtendedCommandOptions> = {};
+ 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<T extends keyof PseudoArguments> = NonNullable<PseudoArguments[T]>;
+export type OptArgType<T extends keyof PseudoArguments> = 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<Message>;
+};