diff --git a/example/modules/Commands/Flags.js b/example/modules/Commands/Flags.js new file mode 100644 index 00000000..1d78446e --- /dev/null +++ b/example/modules/Commands/Flags.js @@ -0,0 +1,29 @@ +// @ts-check +import { MessageCommandBuilder } from "reciple"; +import { createMessageCommandUsage } from '@reciple/message-command-utils'; + +export class Message { + commands = [ + new MessageCommandBuilder() + .setName('flag') + .setDescription('Sends a message') + .addFlag(flag => flag + .setName('flag') + .setDescription('A flag') + .setValueType('string') + .setRequired(true) + .setMandatory(true) + ) + .setExecute(async ({ message, flags }) => { + await message.reply(flags.getFlagValues('flag', { required: true, type: 'string' })[0]); + }) + ]; + + onStart() { + logger.log(this.commands[0]) + logger.log(createMessageCommandUsage(this.commands[0])) + return true; + } +} + +export default new Message() diff --git a/example/modules/Commands/Say.js b/example/modules/Commands/Say.js index da3675f0..04ea5bfd 100644 --- a/example/modules/Commands/Say.js +++ b/example/modules/Commands/Say.js @@ -1,5 +1,4 @@ // @ts-check - import { SlashCommandBuilder } from 'reciple'; /** diff --git a/example/modules/Halts/MessageCommandArguments.js b/example/modules/Halts/MessageCommandArguments.js index 76738ad2..b309a25e 100644 --- a/example/modules/Halts/MessageCommandArguments.js +++ b/example/modules/Halts/MessageCommandArguments.js @@ -13,9 +13,12 @@ export class MessageCommandArguments { * @param {import('reciple').MessageCommandHaltTriggerData} data */ async messageCommandHalt(data) { - if (data.reason !== CommandHaltReason.InvalidArguments && data.reason !== CommandHaltReason.MissingArguments) return; - console.log(data.executeData.options.invalidOptions); - console.log(data.executeData.options.missingOptions); + if ( + data.reason !== CommandHaltReason.InvalidArguments && + data.reason !== CommandHaltReason.MissingArguments && + data.reason !== CommandHaltReason.InvalidFlags && + data.reason !== CommandHaltReason.MissingFlags + ) return; switch (data.reason) { case CommandHaltReason.InvalidArguments: @@ -24,6 +27,12 @@ export class MessageCommandArguments { case CommandHaltReason.MissingArguments: await data.executeData.message.reply(`## Missing arguments\n${data.executeData.options.missingOptions.map(o => `- ${inlineCode(o.name)}`).join('\n')}`); break; + case CommandHaltReason.InvalidFlags: + await data.executeData.message.reply(`## Invalid flags\n${data.executeData.flags.invalidFlags.map(o => `- ${inlineCode(o.name)} ${o.error?.message ?? 'Invalid value'}`).join('\n')}`); + break; + case CommandHaltReason.MissingFlags: + await data.executeData.message.reply(`## Missing flags\n${data.executeData.flags.missingFlags.map(o => `- ${inlineCode(o.name)}`).join('\n')}`); + break; } return true; diff --git a/example/modules/Preconditions/MyPrecondition.js b/example/modules/Preconditions/MyPrecondition.js index 05c8de1e..1e74c5f5 100644 --- a/example/modules/Preconditions/MyPrecondition.js +++ b/example/modules/Preconditions/MyPrecondition.js @@ -1,5 +1,4 @@ // @ts-check - /** * @satisfies {import("reciple").CommandPreconditionData} */ diff --git a/example/nodemon.json b/example/nodemon.json index 465767fb..7c52f1dd 100644 --- a/example/nodemon.json +++ b/example/nodemon.json @@ -8,7 +8,7 @@ "node_modules/**" ], "watch": [ - "src", + "modules", "reciple.mjs", ".env" ], diff --git a/packages/core/src/classes/builders/ContextMenuCommandBuilder.ts b/packages/core/src/classes/builders/ContextMenuCommandBuilder.ts index 33f6f148..61fb6a13 100644 --- a/packages/core/src/classes/builders/ContextMenuCommandBuilder.ts +++ b/packages/core/src/classes/builders/ContextMenuCommandBuilder.ts @@ -93,7 +93,7 @@ export class ContextMenuCommandBuilder extends Mixin(DiscordJsContextMenuCommand } public static resolve(data: ContextMenuCommandResolvable): ContextMenuCommandBuilder { - return data instanceof ContextMenuCommandBuilder ? data : this.from(data); + return data instanceof ContextMenuCommandBuilder ? data : ContextMenuCommandBuilder.from(data); } public static async execute({ client, interaction, command }: ContextMenuCommandExecuteOptions): Promise { diff --git a/packages/core/src/classes/builders/MessageCommandBuilder.ts b/packages/core/src/classes/builders/MessageCommandBuilder.ts index d9458034..2e8a2e3c 100644 --- a/packages/core/src/classes/builders/MessageCommandBuilder.ts +++ b/packages/core/src/classes/builders/MessageCommandBuilder.ts @@ -11,6 +11,10 @@ import type { RecipleClient } from '../structures/RecipleClient.js'; import { RecipleError } from '../structures/RecipleError.js'; import type { CooldownData } from '../structures/Cooldown.js'; import { getCommand } from 'fallout-utility/commands'; +import { parseArgs } from 'util'; +import { MessageCommandFlagBuilder, type MessageCommandFlagResolvable } from './MessageCommandFlagBuilder.js'; +import { MessageCommandFlagValidators } from '../validators/MessageCommandFlagValidator.js'; +import { MessageCommandFlagManager } from '../managers/MessageCommandFlagManager.js'; export interface MessageCommandExecuteData { type: CommandType.MessageCommand; @@ -18,6 +22,7 @@ export interface MessageCommandExecuteData { message: Message; parserData: CommandData; options: MessageCommandOptionManager; + flags: MessageCommandFlagManager; builder: MessageCommandBuilder; } @@ -47,6 +52,11 @@ export interface MessageCommandBuilderData extends BaseCommandBuilderData { * @default true */ validate_options?: boolean; + /** + * Whether to validate flags or not. + * @default true + */ + validate_flags?: boolean; /** * Allows commands to be executed in DMs. * @default false @@ -61,6 +71,10 @@ export interface MessageCommandBuilderData extends BaseCommandBuilderData { * The options of the command. */ options?: MessageCommandOptionResolvable[]; + /** + * The flags of the command. + */ + flags?: MessageCommandFlagResolvable[]; } export interface MessageCommandBuilder extends BaseCommandBuilder { @@ -77,9 +91,11 @@ export class MessageCommandBuilder extends BaseCommandBuilder implements Message public description: string = ''; public aliases: string[] = []; public validate_options: boolean = true; + public validate_flags: boolean = true; public dm_permission: boolean = false; public allow_bot: boolean = false; public options: MessageCommandOptionBuilder[] = []; + public flags: MessageCommandFlagBuilder[] = []; constructor(data?: Omit, 'command_type'>) { super(data); @@ -88,9 +104,11 @@ export class MessageCommandBuilder extends BaseCommandBuilder implements Message if (data?.description) this.setDescription(data.description); if (data?.aliases) this.setAliases(data.aliases); if (data?.validate_options) this.setValidateOptions(data.validate_options); + if (data?.validate_flags) this.setValidateFlags(data.validate_flags); if (data?.dm_permission) this.setDMPermission(data.dm_permission); if (data?.allow_bot) this.setAllowBot(data.allow_bot); if (data?.options) this.setOptions(data.options); + if (data?.flags) this.setFlags(data.flags); } /** @@ -145,6 +163,16 @@ export class MessageCommandBuilder extends BaseCommandBuilder implements Message return this; } + /** + * Set whether to validate flags or not. + * @param enabled Enable flag validation. + */ + public setValidateFlags(enabled: boolean): this { + MessageCommandValidators.isValidValidateFlags(enabled); + this.validate_flags = enabled; + return this; + } + /** * Sets whether the command is available in DMs or not. * @param DMPermission Enable command in Dms. @@ -170,7 +198,7 @@ export class MessageCommandBuilder extends BaseCommandBuilder implements Message * @param option Option data or builder. */ public addOption(option: MessageCommandOptionResolvable|((builder: MessageCommandOptionBuilder) => MessageCommandOptionBuilder)): this { - const opt = typeof option === 'function' ? option(new MessageCommandOptionBuilder()) : MessageCommandOptionBuilder.from(option); + const opt = typeof option === 'function' ? option(new MessageCommandOptionBuilder()) : MessageCommandOptionBuilder.resolve(option); MessageCommandOptionValidators.isValidMessageCommandOptionResolvable(opt); if (this.options.find(o => o.name === opt.name)) throw new RecipleError('An option with name "' + opt.name + '" already exists.'); @@ -196,6 +224,36 @@ export class MessageCommandBuilder extends BaseCommandBuilder implements Message return this; } + /** + * Adds new flag to the command. + * @param option Flag data or builder. + */ + public addFlag(option: MessageCommandFlagResolvable|((builder: MessageCommandFlagBuilder) => MessageCommandFlagBuilder)): this { + const opt = typeof option === 'function' ? option(new MessageCommandFlagBuilder()) : MessageCommandFlagBuilder.resolve(option); + MessageCommandFlagValidators.isValidMessageCommandFlagResolvable(opt); + + if (this.flags.find(o => o.name === opt.name)) throw new RecipleError('A flag with name "' + opt.name + '" already exists.'); + + this.flags.push(MessageCommandFlagBuilder.resolve(opt)); + return this; + } + + /** + * Sets the flags of the command. + * @param flags Flags data or builders. + */ + public setFlags(...flags: RestOrArray MessageCommandFlagBuilder)>): this { + flags = normalizeArray(flags); + MessageCommandValidators.isValidFlags(flags); + this.flags = []; + + for (const flag of flags) { + this.addFlag(flag); + } + + return this; + } + public toJSON(): MessageCommandBuilderData { return { name: this.name, @@ -204,7 +262,8 @@ export class MessageCommandBuilder extends BaseCommandBuilder implements Message validate_options: this.validate_options, dm_permission: this.dm_permission, allow_bot: this.allow_bot, - options: this.options, + options: this.options.map(b => b.toJSON()), + flags: this.flags.map(b => b.toJSON()), ...super._toJSON() }; } @@ -214,7 +273,7 @@ export class MessageCommandBuilder extends BaseCommandBuilder implements Message } public static resolve(data: MessageCommandResolvable): MessageCommandBuilder { - return data instanceof MessageCommandBuilder ? data : this.from(data); + return data instanceof MessageCommandBuilder ? data : MessageCommandBuilder.from(data); } public static async execute({ client, message, command }: MessageCommandExecuteOptions): Promise { @@ -222,12 +281,45 @@ export class MessageCommandBuilder extends BaseCommandBuilder implements Message const prefix = typeof client.config.commands?.messageCommand?.prefix === 'function' ? await Promise.resolve(client.config.commands.messageCommand.prefix({ client, message, guild: message.guild, command })) : client.config.commands?.messageCommand?.prefix; const separator = typeof client.config.commands?.messageCommand?.commandArgumentSeparator === 'function' ? await Promise.resolve(client.config.commands.messageCommand.commandArgumentSeparator({ client, message, guild: message.guild, command })) : client.config.commands?.messageCommand?.commandArgumentSeparator; - const parserData = getCommand(message.content, prefix, separator); - if (!parserData || !parserData.name) return null; + const commandData = getCommand(message.content, prefix, separator); + if (!commandData || !commandData.name) return null; - const builder = command ? this.resolve(command) : client.commands.get(parserData.name, CommandType.MessageCommand); + const builder = command ? this.resolve(command) : client.commands.get(commandData.name, CommandType.MessageCommand); if (!builder) return null; + const { positionals: args, values: flags } = parseArgs({ + args: commandData.args, + allowPositionals: true, + strict: false, + options: Object.fromEntries( + builder.flags + .map((o) => [ + o.name, + Object.fromEntries( + Object.entries({ + type: o.value_type ?? 'string', + multiple: o.multiple, + short: o.short, + default: o.multiple ? o.default_values : o.default_values?.[0], + }) + .filter(([key, value]) => value !== undefined) + ) as any + ]) + ), + }); + + const parserData = { + ...commandData as CommandData & { name: string; }, + args, + flags: Object + .entries(flags) + .filter(([key, value]) => value !== undefined) + .map(([key, value]) => ({ + name: key, + value: Array.isArray(value) ? value : [value] as (string|boolean)[], + })) + }; + const executeData: MessageCommandExecuteData = { type: builder.command_type, client, @@ -239,6 +331,12 @@ export class MessageCommandBuilder extends BaseCommandBuilder implements Message message, parserData, client + }), + flags: await MessageCommandFlagManager.parseFlags({ + command: builder, + message, + parserData, + client }) }; @@ -295,6 +393,28 @@ export class MessageCommandBuilder extends BaseCommandBuilder implements Message } } + if (builder.validate_flags) { + if (executeData.flags.hasInvalidFlags) { + await client.commands.executeHalts({ + commandType: builder.command_type, + reason: CommandHaltReason.InvalidFlags, + executeData, + invalidFlags: executeData.flags.invalidFlags + }); + return null; + } + + if (executeData.flags.hasMissingFlags) { + await client.commands.executeHalts({ + commandType: builder.command_type, + reason: CommandHaltReason.MissingFlags, + executeData, + missingFlags: executeData.flags.missingFlags + }); + return null; + } + } + return (await client.commands.executeCommandBuilderExecute(executeData)) ? executeData : null; } } diff --git a/packages/core/src/classes/builders/MessageCommandFlagBuilder.ts b/packages/core/src/classes/builders/MessageCommandFlagBuilder.ts new file mode 100644 index 00000000..5290afc7 --- /dev/null +++ b/packages/core/src/classes/builders/MessageCommandFlagBuilder.ts @@ -0,0 +1,163 @@ +import { isJSONEncodable, normalizeArray, type Awaitable, type JSONEncodable, type Message, type RestOrArray } from 'discord.js'; +import type { CommandData } from '../../types/structures.js'; +import type { MessageCommandBuilder } from './MessageCommandBuilder.js'; +import type { RecipleClient } from '../structures/RecipleClient.js'; +import { MessageCommandFlagValidators } from '../validators/MessageCommandFlagValidator.js'; + +export interface MessageCommandFlagBuilderResolveValueOptions { + /** + * The values of the given flag + */ + values: string[]|boolean[]; + /** + * The parser data when parsing this command. + */ + parserData: CommandData; + /** + * The flag builder used to build this option. + */ + flag: MessageCommandFlagBuilder; + /** + * The command builder used to build this command. + */ + command: MessageCommandBuilder; + /** + * The message that triggered this command. + */ + message: Message; + /** + * The client instance + */ + client: RecipleClient; +} + +export interface MessageCommandFlagBuilderData { + name: string; + short?: string; + description: string; + default_values?: string[]|boolean[]; + required?: boolean; + mandatory?: boolean; + multiple?: boolean; + value_type?: 'string'|'boolean'; + /** + * The function that validates the option value. + * @param options The option value and message. + */ + validate?: (options: MessageCommandFlagBuilderResolveValueOptions) => Awaitable; + /** + * Resolves the option value. + * @param options The option value and message. + */ + resolve_value?: (options: MessageCommandFlagBuilderResolveValueOptions) => Awaitable; +} + +export class MessageCommandFlagBuilder implements MessageCommandFlagBuilderData { + public name: string = ''; + public short?: string; + public description: string = ''; + public default_values?: string[]|boolean[]; + public required: boolean = false; + public mandatory?: boolean = false; + public multiple?: boolean = false; + public value_type?: 'string'|'boolean' = 'string'; + public validate?: (options: MessageCommandFlagBuilderResolveValueOptions) => Awaitable; + public resolve_value?: (options: MessageCommandFlagBuilderResolveValueOptions) => Awaitable; + + constructor(data?: Partial>) { + if (data?.name) this.setName(data.name); + if (data?.short) this.setShort(data.short); + if (data?.description) this.setDescription(data.description); + if (data?.default_values) this.setDefaultValues(data.default_values); + if (data?.required) this.setRequired(data.required); + if (data?.mandatory) this.setMandatory(data.mandatory); + if (data?.multiple) this.setMultiple(data.multiple); + if (data?.value_type) this.setValueType(data.value_type); + if (data?.validate) this.setValidate(data.validate); + if (data?.resolve_value) this.setResolveValue(data.resolve_value); + } + + public setName(name: string): this { + MessageCommandFlagValidators.isValidName(name); + this.name = name; + return this; + } + + public setShort(short: string): this { + MessageCommandFlagValidators.isValidShort(short); + this.short = short; + return this; + } + + public setDescription(description: string): this { + MessageCommandFlagValidators.isValidDescription(description); + this.description = description; + return this; + } + + public setDefaultValues(...defaultValues: RestOrArray): this { + defaultValues = normalizeArray(defaultValues) as string[]|boolean[]; + MessageCommandFlagValidators.isValidDefaultValues(defaultValues); + this.default_values = defaultValues; + return this; + } + + public setRequired(required: boolean): this { + MessageCommandFlagValidators.isValidRequired(required); + this.required = required; + return this; + } + + public setMandatory(mandatory: boolean): this { + MessageCommandFlagValidators.isValidMandatory(mandatory); + this.mandatory = mandatory; + return this; + } + + public setMultiple(multiple: boolean): this { + MessageCommandFlagValidators.isValidMultiple(multiple); + this.multiple = multiple; + return this; + } + + public setValueType(valueType: 'string'|'boolean'): this { + MessageCommandFlagValidators.isValidValueType(valueType); + this.value_type = valueType as any; + return this as any; + } + + public setValidate(validate: MessageCommandFlagBuilderData['validate']): this { + MessageCommandFlagValidators.isValidValidate(validate); + this.validate = validate; + return this; + } + + public setResolveValue(resolve_value: MessageCommandFlagBuilderData['resolve_value']): this { + MessageCommandFlagValidators.isValidResolveValue(resolve_value); + this.resolve_value = resolve_value; + return this; + } + + public toJSON(): MessageCommandFlagBuilderData { + return { + name: this.name, + short: this.short, + description: this.description, + default_values: this.default_values, + required: this.required, + multiple: this.multiple, + validate: this.validate, + resolve_value: this.resolve_value + }; + } + + public static from(data: MessageCommandFlagResolvable): MessageCommandFlagBuilder { + return new MessageCommandFlagBuilder(isJSONEncodable(data) ? data.toJSON() : data); + } + + public static resolve(data: MessageCommandFlagResolvable): MessageCommandFlagBuilder { + return data instanceof MessageCommandFlagBuilder ? data : MessageCommandFlagBuilder.from(data); + } +} + +export type MessageCommandFlagResolvable = JSONEncodable>|MessageCommandFlagBuilderData; diff --git a/packages/core/src/classes/builders/MessageCommandOptionBuilder.ts b/packages/core/src/classes/builders/MessageCommandOptionBuilder.ts index 26d884a2..5f305b49 100644 --- a/packages/core/src/classes/builders/MessageCommandOptionBuilder.ts +++ b/packages/core/src/classes/builders/MessageCommandOptionBuilder.ts @@ -137,7 +137,7 @@ export class MessageCommandOptionBuilder implements Message } public static resolve(data: MessageCommandOptionResolvable): MessageCommandOptionBuilder { - return data instanceof MessageCommandOptionBuilder ? data : this.from(data); + return data instanceof MessageCommandOptionBuilder ? data : MessageCommandOptionBuilder.from(data); } } diff --git a/packages/core/src/classes/builders/SlashCommandBuilder.ts b/packages/core/src/classes/builders/SlashCommandBuilder.ts index 61167bef..60eda726 100644 --- a/packages/core/src/classes/builders/SlashCommandBuilder.ts +++ b/packages/core/src/classes/builders/SlashCommandBuilder.ts @@ -213,7 +213,7 @@ export class SlashCommandBuilder extends Mixin(DiscordJsSlashCommandBuilder, Bas } public static resolve(data: SlashCommandResolvable): AnySlashCommandBuilder { - return data instanceof SlashCommandBuilder ? data : this.from(data); + return data instanceof SlashCommandBuilder ? data : SlashCommandBuilder.from(data); } public static async execute({ client, interaction, command }: SlashCommandExecuteOptions): Promise { diff --git a/packages/core/src/classes/managers/MessageCommandFlagManager.ts b/packages/core/src/classes/managers/MessageCommandFlagManager.ts new file mode 100644 index 00000000..dddc6cac --- /dev/null +++ b/packages/core/src/classes/managers/MessageCommandFlagManager.ts @@ -0,0 +1,107 @@ +import type { Message } from 'discord.js'; +import type { MessageCommandBuilder } from '../builders/MessageCommandBuilder.js'; +import { MessageCommandFlagValue, type MessageCommandFlagParseOptionValueOptions } from '../structures/MessageCommandFlagValue.js'; +import type { RecipleClient } from '../structures/RecipleClient.js'; +import { DataManager } from './DataManager.js'; +import type { MessageCommandOptionManagerOptions } from './MessageCommandOptionManager.js'; +import type { CommandData } from '../../types/structures.js'; +import { RecipleError } from '../structures/RecipleError.js'; + +export interface MessageCommandFlagManagerParseOptionsData extends Omit { + command: MessageCommandBuilder; + parserData: CommandData; +} + +export class MessageCommandFlagManager extends DataManager implements MessageCommandOptionManagerOptions { + readonly command: MessageCommandBuilder; + readonly client: RecipleClient; + readonly message: Message; + readonly parserData: CommandData; + + get rawFlags() { return this.parserData.flags; } + + constructor(data: MessageCommandOptionManagerOptions) { + super(); + + this.command = data.command; + this.client = data.client; + this.message = data.message; + this.parserData = data.parserData; + } + + get hasMissingFlags() { + return this.cache.some(o => o.missing); + } + + get hasInvalidFlags() { + return this.cache.some(o => o.invalid); + } + + get hasValidationErrors() { + return this.cache.some(o => o.error); + } + + get missingFlags() { + return this.cache.filter(o => o.missing); + } + + get invalidFlags() { + return this.cache.filter(o => o.invalid); + } + + /** + * Retrieves the value of a message command flag. + * + * @param {string} name - The name of the flag. + * @param {boolean} required - Whether the flag is required or not. + * @return {MessageCommandOptionValue|null} The value of the message command flag. + */ + public getFlag(name: string, required: true): MessageCommandFlagValue; + public getFlag(name: string, required?: boolean): MessageCommandFlagValue|null; + public getFlag(name: string, required: boolean = false): MessageCommandFlagValue|null { + const flag = this.cache.get(name) as MessageCommandFlagValue|undefined; + if (required && !flag) throw new RecipleError(`Unable to find required message flag '${name}'`); + + return flag ?? null; + } + + /** + * Obtains the value of a message command flag. + * @param name The name of the flag. + * @param options The flags to use when parsing the flag value. + * @param options.required Whether the flag is required or not. + * @param options.resolveValue Whether to resolve the value or not. + */ + public getFlagValues(name: string, options?: { required?: boolean; resolveValue?: false; type?: V }): V extends 'string' ? string[] : V extends 'boolean' ? boolean[] : string[]|boolean[]; + public getFlagValues(name: string, options?: { required?: boolean; resolveValue?: true; }): Promise; + public getFlagValues(name: string, options?: { required?: boolean; resolveValue?: boolean; type?: V }): Promise|(V extends 'string' ? string[] : V extends 'boolean' ? boolean[] : string[]|boolean[]); + public getFlagValues(name: string, options: { required?: boolean; resolveValue?: boolean; type?: 'string'|'boolean' } = { required: false, resolveValue: false }): Promise|string[]|boolean[] { + const value = this.getFlag(name, options.required); + return options.resolveValue + ? Promise.resolve(value?.resolveValues()).then(v => v ?? []) + : value?.values ?? []; + } + + private async _parseFlags(): Promise { + if (!this.command.flags?.length || !this.client.isReady()) return; + + for (const data of this.command.flags) { + const flag = this.rawFlags.find(f => f.name === data.name); + this._cache.set(data.name, await MessageCommandFlagValue.parseFlagValue({ + client: this.client, + message: this.message, + command: this.command, + parserData: this.parserData, + values: flag?.value, + flag: data + })); + } + } + + public static async parseFlags(options: MessageCommandFlagManagerParseOptionsData): Promise { + const manager = new MessageCommandFlagManager(options); + + await manager._parseFlags(); + return manager; + } +} diff --git a/packages/core/src/classes/managers/MessageCommandOptionManager.ts b/packages/core/src/classes/managers/MessageCommandOptionManager.ts index 2d9832aa..1f5b81f7 100644 --- a/packages/core/src/classes/managers/MessageCommandOptionManager.ts +++ b/packages/core/src/classes/managers/MessageCommandOptionManager.ts @@ -95,8 +95,8 @@ export class MessageCommandOptionManager extends DataManager|null} The value of the message command option. */ - public getOption(name: string, required?: false): MessageCommandOptionValue; - public getOption(name: string, required?: true): MessageCommandOptionValue; + public getOption(name: string, required: true): MessageCommandOptionValue; + public getOption(name: string, required?: boolean): MessageCommandOptionValue|null; public getOption(name: string, required: boolean = false): MessageCommandOptionValue|null { const option = this.cache.get(name); if (required && !option) throw new RecipleError(`Unable to find required message option '${name}'`); @@ -113,11 +113,21 @@ export class MessageCommandOptionManager extends DataManager(name: string, options?: { required?: false; resolveValue?: false; }): string|null; public getOptionValue(name: string, options?: { required?: true; resolveValue?: false; }): string; - public getOptionValue(name: string, options?: { required?: true; resolveValue?: true; }): Promise; + public getOptionValue(name: string, options?: { required?: boolean; resolveValue?: false; }): string|null; public getOptionValue(name: string, options?: { required?: false; resolveValue?: true; }): Promise; + public getOptionValue(name: string, options?: { required?: true; resolveValue?: true; }): Promise; + public getOptionValue(name: string, options?: { required?: boolean; resolveValue?: true; }): Promise; + public getOptionValue(name: string, options?: { required?: boolean; resolveValue?: boolean; }): string|null|Promise; public getOptionValue(name: string, options: { required?: boolean; resolveValue?: boolean; } = { required: false, resolveValue: false }): string|null|Promise { - const value = this.getOption(name); - return options.resolveValue ? Promise.resolve(value.resolveValue(options.required) ?? value.value) : value.value; + const value = this.getOption(name, options.required); + + if (options.resolveValue) { + if (!value) return Promise.resolve(null); + + return value.resolveValue(options.required) ?? null; + } + + return value?.value ?? null; } public toJSON() { diff --git a/packages/core/src/classes/structures/CommandHalt.ts b/packages/core/src/classes/structures/CommandHalt.ts index 7d3824e3..56ade0ab 100644 --- a/packages/core/src/classes/structures/CommandHalt.ts +++ b/packages/core/src/classes/structures/CommandHalt.ts @@ -126,7 +126,7 @@ export class CommandHalt implements CommandHaltData { } public static resolve(data: CommandHaltResolvable): CommandHalt { - return data instanceof CommandHalt ? data : this.from(data); + return data instanceof CommandHalt ? data : CommandHalt.from(data); } } diff --git a/packages/core/src/classes/structures/CommandPrecondition.ts b/packages/core/src/classes/structures/CommandPrecondition.ts index c50ae104..5cddc6dd 100644 --- a/packages/core/src/classes/structures/CommandPrecondition.ts +++ b/packages/core/src/classes/structures/CommandPrecondition.ts @@ -111,7 +111,7 @@ export class CommandPrecondition implements CommandPreconditionData { } public static resolve(data: CommandPreconditionResolvable): CommandPrecondition { - return data instanceof CommandPrecondition ? data : this.from(data); + return data instanceof CommandPrecondition ? data : CommandPrecondition.from(data); } } diff --git a/packages/core/src/classes/structures/MessageCommandFlagValue.ts b/packages/core/src/classes/structures/MessageCommandFlagValue.ts new file mode 100644 index 00000000..d0369b55 --- /dev/null +++ b/packages/core/src/classes/structures/MessageCommandFlagValue.ts @@ -0,0 +1,138 @@ +import type { Message } from 'discord.js'; +import type { CommandData } from '../../types/structures.js'; +import type { MessageCommandBuilder } from '../builders/MessageCommandBuilder.js'; +import type { MessageCommandFlagBuilder, MessageCommandFlagBuilderResolveValueOptions } from '../builders/MessageCommandFlagBuilder.js'; +import type { RecipleClient } from './RecipleClient.js'; +import { RecipleError } from './RecipleError.js'; + +export interface MessageCommandFlagValueData { + /** + * The name of the option. + */ + name: string; + /** + * The builder that is used to create the option. + */ + flag: MessageCommandFlagBuilder; + /** + * The raw value of the option. + */ + values: V extends 'boolean' ? boolean[] : V extends 'string' ? string[] : string[]|boolean[]; + /** + * Whether the option is missing. + */ + missing: boolean; + /** + * Whether the option is invalid. + */ + invalid: boolean; + /** + * The error that occurred while parsing the option. + */ + error?: string|Error; +} + +export interface MessageCommandFlagParseOptionValueOptions extends Omit, 'values'> { + values?: T[]|null; +} + +export interface MessageCommandFlagValueOptions extends MessageCommandFlagValueData, Pick, 'parserData'|'command'> { + client: RecipleClient; + message: Message; +} + +export class MessageCommandFlagValue implements MessageCommandFlagValueData { + readonly name: string; + readonly flag: MessageCommandFlagBuilder; + readonly values: V extends 'boolean' ? boolean[] : V extends 'string' ? string[] : string[]|boolean[]; + readonly missing: boolean; + readonly invalid: boolean; + readonly message: Message; + readonly error?: Error; + + readonly parserData: CommandData; + readonly command: MessageCommandBuilder; + readonly client: RecipleClient; + + constructor(options: MessageCommandFlagValueOptions) { + this.name = options.name; + this.flag = options.flag; + this.values = options.values; + this.missing = options.missing; + this.invalid = options.invalid; + this.message = options.message; + this.error = typeof options.error === 'string' ? new Error(options.error) : options.error; + this.parserData = options.parserData; + this.command = options.command; + this.client = options.client; + } + + /** + * Resolves the raw value of the option. + * @param required Whether the option is required. + */ + public async resolveValues(): Promise { + if (this.values.length) return []; + + return this.flag.resolve_value + ? Promise.resolve(this.flag.resolve_value({ + values: this.values, + flag: this.flag, + parserData: this.parserData, + command: this.command, + message: this.message, + client: this.client, + })) + : []; + } + + public toJSON(): MessageCommandFlagValueData { + return { + name: this.name, + flag: this.flag, + values: this.values, + missing: this.missing, + invalid: this.invalid, + error: this.error, + } + } + + public static async parseFlagValue(options: MessageCommandFlagParseOptionValueOptions): Promise> { + const filteredValues = options.values?.filter(value => typeof value == options.flag.value_type); + const missing = filteredValues + ? !!options.flag.required && !filteredValues?.length + : options.flag.mandatory ?? false; + + const validateData = !missing + ? options.flag.validate && filteredValues?.length + ? await Promise.resolve(options.flag.validate({ + values: filteredValues as string[]|boolean[], + flag: options.flag, + parserData: options.parserData, + command: options.command, + message: options.message, + client: options.client, + })) + : true + : new RecipleError( + filteredValues + ? RecipleError.createCommandRequiredFlagNotFoundErrorOptions(options.flag.name, filteredValues.join(', ')) + : RecipleError.createCommandMandatoryFlagNotFoundErrorOptions(options.flag.name, 'undefined') + ); + + return new MessageCommandFlagValue({ + name: options.flag.name, + flag: options.flag, + values: (filteredValues ?? []) as string[]|boolean[], + missing, + invalid: !validateData, + message: options.message, + error: typeof validateData !== 'boolean' + ? typeof validateData === 'string' ? new Error(validateData) : validateData + : undefined, + parserData: options.parserData, + command: options.command, + client: options.client, + }) + } +} diff --git a/packages/core/src/classes/structures/MessageCommandOptionValue.ts b/packages/core/src/classes/structures/MessageCommandOptionValue.ts index 4561e55a..38b461ea 100644 --- a/packages/core/src/classes/structures/MessageCommandOptionValue.ts +++ b/packages/core/src/classes/structures/MessageCommandOptionValue.ts @@ -3,6 +3,7 @@ import { MessageCommandBuilder } from '../builders/MessageCommandBuilder.js'; import type { CommandData } from '../../types/structures.js'; import { RecipleClient } from './RecipleClient.js'; import { Message } from 'discord.js'; +import { RecipleError } from './RecipleError.js'; export interface MessageCommandOptionValueData { /** @@ -107,7 +108,7 @@ export class MessageCommandOptionValue implements MessageCo client: options.client, })) : true - : false; + : new RecipleError(RecipleError.createCommandRequiredOptionNotFoundErrorOptions(options.option.name, options.value)); return new MessageCommandOptionValue({ name: options.option.name, diff --git a/packages/core/src/classes/structures/RecipleError.ts b/packages/core/src/classes/structures/RecipleError.ts index e3096f93..00362199 100644 --- a/packages/core/src/classes/structures/RecipleError.ts +++ b/packages/core/src/classes/structures/RecipleError.ts @@ -60,12 +60,28 @@ export class RecipleError extends Error { public static createCommandRequiredOptionNotFoundErrorOptions(optionName: string, value: unknown): RecipleErrorOptions { return { - message: `No value given from required option ${kleur.cyan("'" + optionName + "'")}`, + message: `No value given for required option ${kleur.cyan("'" + optionName + "'")}`, cause: { value }, name: 'RequiredOptionNotFound' }; } + public static createCommandRequiredFlagNotFoundErrorOptions(flagName: string, value: unknown): RecipleErrorOptions { + return { + message: `No value given for required flag ${kleur.cyan("'" + flagName + "'")}`, + cause: { value }, + name: 'RequiredFlagNotFound' + }; + } + + public static createCommandMandatoryFlagNotFoundErrorOptions(flagName: string, value: unknown): RecipleErrorOptions { + return { + message: `No value given for mandatory flag ${kleur.cyan("'" + flagName + "'")}`, + cause: { value }, + name: 'MandatoryFlagNotFound' + }; + } + public static createStartModuleErrorOptions(moduleName: string, cause: unknown): RecipleErrorOptions { return { message: `Failed to start module ${kleur.red("'" + moduleName + "'")}`, diff --git a/packages/core/src/classes/validators/MessageCommandFlagValidator.ts b/packages/core/src/classes/validators/MessageCommandFlagValidator.ts new file mode 100644 index 00000000..6d92463b --- /dev/null +++ b/packages/core/src/classes/validators/MessageCommandFlagValidator.ts @@ -0,0 +1,142 @@ +import type { MessageCommandFlagBuilderData, MessageCommandFlagResolvable } from '../builders/MessageCommandFlagBuilder.js'; +import { BaseCommandValidators } from './BaseCommandValidators.js'; +import { isJSONEncodable } from 'discord.js'; + +export class MessageCommandFlagValidators extends BaseCommandValidators { + public static name = MessageCommandFlagValidators.s + .string({ message: 'Expected string as message command flag name' }) + .lengthGreaterThanOrEqual(1, { message: 'Message command flag name needs to have at least single character' }) + .lengthLessThanOrEqual(32, { message: 'Message command flag name cannot exceed 32 characters' }) + .regex(/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u, { message: 'Message command flag name can only be alphanumeric without spaces' }); + + public static short = MessageCommandFlagValidators.s + .string({ message: 'Expected string as message command flag short' }) + .lengthEqual(1, { message: 'Message command flag short needs to have at least single character' }) + .optional(); + + public static description = MessageCommandFlagValidators.s + .string({ message: 'Expected string as message command flag description' }) + .lengthGreaterThanOrEqual(1, { message: 'Message command flag description needs to have at least single character' }) + .lengthLessThanOrEqual(100, { message: 'Message command flag description cannot exceed 100 characters' }); + + public static default_values = MessageCommandFlagValidators.s + .union([ + MessageCommandFlagValidators.s + .string({ message: 'Expected string as message command flag default value' }) + .array({ message: 'Expected array as message command flag default values' }), + MessageCommandFlagValidators.s + .boolean({ message: 'Expected boolean as message command flag default value' }) + .array({ message: 'Expected array as message command flag default values' }), + ]) + .optional(); + + public static required = MessageCommandFlagValidators.s + .boolean({ message: 'Expected boolean for .required' }) + .optional(); + + public static mandatory = MessageCommandFlagValidators.s + .boolean({ message: 'Expected boolean for .mandatory' }) + .optional(); + + public static multiple = MessageCommandFlagValidators.s + .boolean({ message: 'Expected boolean for .multiple' }) + .optional(); + + public static value_type = MessageCommandFlagValidators.s + .union([ + MessageCommandFlagValidators.s + .literal('string', { equalsOptions: { message: 'Expected "string" for .value_type' } }) + .optional(), + MessageCommandFlagValidators.s + .literal('boolean', { equalsOptions: { message: 'Expected "boolean" for .value_type' } }) + .optional(), + ]) + .optional(); + + public static validate = MessageCommandFlagValidators.s + .instance(Function, { message: 'Expected a function for .validate' }) + .optional(); + + public static resolve_value = MessageCommandFlagValidators.s + .instance(Function, { message: 'Expected a function for .resolve_value' }) + .optional(); + + public static MessageCommandFlagBuilderData = MessageCommandFlagValidators.s.object({ + name: MessageCommandFlagValidators.name, + short: MessageCommandFlagValidators.short, + description: MessageCommandFlagValidators.description, + default_values: MessageCommandFlagValidators.default_values, + required: MessageCommandFlagValidators.required, + multiple: MessageCommandFlagValidators.multiple, + validate: MessageCommandFlagValidators.validate, + resolve_value: MessageCommandFlagValidators.resolve_value + }); + + public static MessageCommandFlagResolvable = MessageCommandFlagValidators.s.union([MessageCommandFlagValidators.MessageCommandFlagBuilderData, MessageCommandFlagValidators.jsonEncodable]); + + public static isValidName(name: unknown): asserts name is MessageCommandFlagBuilderData['name'] { + MessageCommandFlagValidators.name.setValidationEnabled(MessageCommandFlagValidators.isValidationEnabled).parse(name); + } + + public static isValidShort(short: unknown): asserts short is MessageCommandFlagBuilderData['short'] { + MessageCommandFlagValidators.short.setValidationEnabled(MessageCommandFlagValidators.isValidationEnabled).parse(short); + } + + public static isValidDescription(description: unknown): asserts description is MessageCommandFlagBuilderData['description'] { + MessageCommandFlagValidators.description.setValidationEnabled(MessageCommandFlagValidators.isValidationEnabled).parse(description); + } + + public static isValidDefaultValues(defaultValues: unknown): asserts defaultValues is MessageCommandFlagBuilderData['default_values'] { + MessageCommandFlagValidators.default_values.setValidationEnabled(MessageCommandFlagValidators.isValidationEnabled).parse(defaultValues); + } + + public static isValidRequired(required: unknown): asserts required is MessageCommandFlagBuilderData['required'] { + MessageCommandFlagValidators.required.setValidationEnabled(MessageCommandFlagValidators.isValidationEnabled).parse(required); + } + + public static isValidMandatory(mandatory: unknown): asserts mandatory is MessageCommandFlagBuilderData['mandatory'] { + MessageCommandFlagValidators.mandatory.setValidationEnabled(MessageCommandFlagValidators.isValidationEnabled).parse(mandatory); + } + + public static isValidMultiple(multiple: unknown): asserts multiple is MessageCommandFlagBuilderData['multiple'] { + MessageCommandFlagValidators.multiple.setValidationEnabled(MessageCommandFlagValidators.isValidationEnabled).parse(multiple); + } + + public static isValidValueType(valueType: unknown): asserts valueType is MessageCommandFlagBuilderData['value_type'] { + MessageCommandFlagValidators.value_type.setValidationEnabled(MessageCommandFlagValidators.isValidationEnabled).parse(valueType); + } + + public static isValidValidate(validate: unknown): asserts validate is MessageCommandFlagBuilderData['validate'] { + MessageCommandFlagValidators.validate.setValidationEnabled(MessageCommandFlagValidators.isValidationEnabled).parse(validate); + } + + public static isValidResolveValue(resolveValue: unknown): asserts resolveValue is MessageCommandFlagBuilderData['resolve_value'] { + MessageCommandFlagValidators.resolve_value.setValidationEnabled(MessageCommandFlagValidators.isValidationEnabled).parse(resolveValue); + } + + public static isValidMessageCommandFlagBuilderData(data: unknown): asserts data is MessageCommandFlagBuilderData { + const opt = data as MessageCommandFlagBuilderData; + + MessageCommandFlagValidators.isValidName(opt.name); + MessageCommandFlagValidators.isValidShort(opt.short); + MessageCommandFlagValidators.isValidDescription(opt.description); + MessageCommandFlagValidators.isValidDefaultValues(opt.default_values); + MessageCommandFlagValidators.isValidRequired(opt.required); + MessageCommandFlagValidators.isValidMandatory(opt.mandatory); + MessageCommandFlagValidators.isValidMultiple(opt.multiple); + MessageCommandFlagValidators.isValidValueType(opt.value_type); + MessageCommandFlagValidators.isValidValidate(opt.validate); + MessageCommandFlagValidators.isValidResolveValue(opt.resolve_value); + } + + public static isValidMessageCommandFlagResolvable(data: unknown): asserts data is MessageCommandFlagResolvable { + const opt = data as MessageCommandFlagResolvable; + + if (isJSONEncodable(opt)) { + const i = opt.toJSON(); + MessageCommandFlagValidators.isValidMessageCommandFlagBuilderData(i); + } else { + MessageCommandFlagValidators.isValidMessageCommandFlagBuilderData(opt); + } + } +} diff --git a/packages/core/src/classes/validators/MessageCommandValidators.ts b/packages/core/src/classes/validators/MessageCommandValidators.ts index f4fa8645..7516bf64 100644 --- a/packages/core/src/classes/validators/MessageCommandValidators.ts +++ b/packages/core/src/classes/validators/MessageCommandValidators.ts @@ -1,6 +1,7 @@ import { MessageCommandOptionValidators } from './MessageCommandOptionValidators.js'; import type { MessageCommandBuilderData } from '../builders/MessageCommandBuilder.js'; import { BaseCommandValidators } from './BaseCommandValidators.js'; +import { MessageCommandFlagValidators } from './MessageCommandFlagValidator.js'; export class MessageCommandValidators extends BaseCommandValidators { public static name = MessageCommandValidators.s @@ -22,6 +23,10 @@ export class MessageCommandValidators extends BaseCommandValidators { .boolean({ message: 'Expected boolean for .validate_options' }) .optional(); + public static validate_flags = MessageCommandValidators.s + .boolean({ message: 'Expected boolean for .validate_flags' }) + .optional(); + public static dm_permission = MessageCommandValidators.s .boolean({ message: 'Expected boolean for .dm_permission' }) .optional(); @@ -34,6 +39,10 @@ export class MessageCommandValidators extends BaseCommandValidators { .array({ message: 'Expected an array for message command options' }) .optional(); + public static flags = MessageCommandFlagValidators.MessageCommandFlagResolvable + .array({ message: 'Expected an array for message command flags' }) + .optional(); + public static isValidName(name: unknown): asserts name is MessageCommandBuilderData['name'] { MessageCommandValidators.name.setValidationEnabled(MessageCommandValidators.isValidationEnabled).parse(name); } @@ -50,6 +59,10 @@ export class MessageCommandValidators extends BaseCommandValidators { MessageCommandValidators.validate_options.setValidationEnabled(MessageCommandValidators.isValidationEnabled).parse(parseOptions); } + public static isValidValidateFlags(parseFlags: unknown): asserts parseFlags is MessageCommandBuilderData['validate_flags'] { + MessageCommandValidators.validate_flags.setValidationEnabled(MessageCommandValidators.isValidationEnabled).parse(parseFlags); + } + public static isValidDMPermission(DMPermission: unknown): asserts DMPermission is MessageCommandBuilderData['dm_permission'] { MessageCommandValidators.dm_permission.setValidationEnabled(MessageCommandValidators.isValidationEnabled).parse(DMPermission); } @@ -61,4 +74,8 @@ export class MessageCommandValidators extends BaseCommandValidators { public static isValidOptions(options: unknown): asserts options is MessageCommandBuilderData['options'] { MessageCommandValidators.options.setValidationEnabled(MessageCommandValidators.isValidationEnabled).parse(options); } + + public static isValidFlags(flags: unknown): asserts flags is MessageCommandBuilderData['flags'] { + MessageCommandValidators.flags.setValidationEnabled(MessageCommandValidators.isValidationEnabled).parse(flags); + } } diff --git a/packages/core/src/types/constants.ts b/packages/core/src/types/constants.ts index 2febfede..573f857a 100644 --- a/packages/core/src/types/constants.ts +++ b/packages/core/src/types/constants.ts @@ -14,7 +14,9 @@ export enum CommandHaltReason { Cooldown, InvalidArguments, MissingArguments, - PreconditionTrigger + PreconditionTrigger, + InvalidFlags, + MissingFlags } export enum RecipleModuleStatus { diff --git a/packages/core/src/types/structures.ts b/packages/core/src/types/structures.ts index 3fcdf245..45e34a82 100644 --- a/packages/core/src/types/structures.ts +++ b/packages/core/src/types/structures.ts @@ -8,6 +8,7 @@ import type { MessageCommandOptionValue } from '../classes/structures/MessageCom import type { CooldownSweeperOptions } from '../classes/managers/CooldownManager.js'; import type { CommandHaltReason, CommandType } from './constants.js'; import type { Cooldown } from '../classes/structures/Cooldown.js'; +import type { MessageCommandFlagValue } from '../classes/structures/MessageCommandFlagValue.js'; // Config export interface RecipleClientConfig { @@ -66,7 +67,10 @@ export type AnySlashCommandOptionData = AnyNonSubcommandSlashCommandOptionData|A export type CommandHaltTriggerData = | CommandErrorHaltTriggerData | CommandCooldownHaltTriggerData - | (T extends CommandType.MessageCommand ? CommandInvalidArgumentsHaltTriggerData | CommandMissingArgumentsHaltTriggerData : never) + | (T extends CommandType.MessageCommand + ? CommandInvalidArgumentsHaltTriggerData | CommandMissingArgumentsHaltTriggerData | CommandInvalidFlagsHaltTriggerData | CommandMissingFlagsHaltTriggerData + : never + ) | CommandPreconditionResultHaltTriggerData; export interface BaseCommandHaltTriggerData { @@ -101,6 +105,16 @@ export interface CommandMissingArgumentsHaltTriggerData e missingOptions: Collection; } +export interface CommandInvalidFlagsHaltTriggerData extends BaseCommandHaltTriggerData { + reason: CommandHaltReason.InvalidFlags; + invalidFlags: Collection; +} + +export interface CommandMissingFlagsHaltTriggerData extends BaseCommandHaltTriggerData { + reason: CommandHaltReason.MissingFlags; + missingFlags: Collection; +} + export interface CommandPreconditionResultHaltTriggerData extends BaseCommandHaltTriggerData, Omit { reason: CommandHaltReason.PreconditionTrigger; } @@ -109,6 +123,7 @@ export interface CommandPreconditionResultHaltTriggerData export interface CommandData { name?: string; prefix?: string; + flags: { name: string; value: (string|boolean)[]; }[]; args: string[]; raw: string; rawArgs: string; diff --git a/packages/message-command-utils/src/helpers/createMessageCommandUsage.ts b/packages/message-command-utils/src/helpers/createMessageCommandUsage.ts index 04d728a7..d86fddeb 100644 --- a/packages/message-command-utils/src/helpers/createMessageCommandUsage.ts +++ b/packages/message-command-utils/src/helpers/createMessageCommandUsage.ts @@ -3,6 +3,16 @@ import { isJSONEncodable } from 'discord.js'; export interface CreateMessageCommandUsageOptions { prefix?: string; + flags?: { + include?: boolean; + useShort?: boolean; + showValueType?: boolean; + flagBrackets?: { + required?: [string, string]; + mandatory?: [string, string]; + optional?: [string, string]; + } + }; optionBrackets?: { required?: [string, string]; optional?: [string, string]; @@ -23,5 +33,19 @@ export function createMessageCommandUsage(data: MessageCommandResolvable, option usage += ` ${brackets[0]}${option.name}${brackets[1]}`; } + if (options?.flags?.include !== false && command.flags) for (const flagData of command.flags) { + const flag = isJSONEncodable(flagData) ? flagData.toJSON() : flagData; + const brackets = flag.required + ? options?.flags?.flagBrackets?.required ?? ['<', '>'] + : options?.flags?.flagBrackets?.optional ?? ['[', ']']; + const mandatory = options?.flags?.flagBrackets?.mandatory ?? ['', ''] + + let value = `${options?.flags?.useShort ? '-' + flag.short : '--' + flag.name}`; + + if (options?.flags?.showValueType) value += `=${brackets[0]}${flag.value_type === 'string' ? 'string' : 'boolean'}${flag.multiple ? '...' : ''}${brackets[1]}`; + + usage += ` ${mandatory[0]}${value}${mandatory[1]}`; + } + return usage; }