diff --git a/src/index.ts b/src/index.ts index bbb5765d5..fac95546f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,6 +67,8 @@ export * from './lib/errors/ArgumentError'; export * from './lib/errors/Identifiers'; export * from './lib/errors/PreconditionError'; export * from './lib/errors/UserError'; +export * from './lib/parsers/Args'; +export * from './lib/parsers/ChatInputCommandArgs'; export * from './lib/parsers/MessageArgs'; export * from './lib/plugins/Plugin'; export * from './lib/plugins/PluginManager'; diff --git a/src/lib/parsers/Args.ts b/src/lib/parsers/Args.ts index 4f85d919e..f1fa2a3f8 100644 --- a/src/lib/parsers/Args.ts +++ b/src/lib/parsers/Args.ts @@ -1,5 +1,5 @@ -import type { ChannelTypes, GuildBasedChannelTypes } from '@sapphire/discord.js-utilities'; -import { Result, type Option } from '@sapphire/result'; +import type { AnyInteraction, ChannelTypes, GuildBasedChannelTypes } from '@sapphire/discord.js-utilities'; +import { Result } from '@sapphire/result'; import type { CategoryChannel, ChannelType, @@ -15,10 +15,13 @@ import type { VoiceChannel } from 'discord.js'; import { ArgumentError } from '../errors/ArgumentError'; -import type { UserError } from '../errors/UserError'; +import { UserError } from '../errors/UserError'; import type { EmojiObject } from '../resolvers/emoji'; import type { Argument, IArgument } from '../structures/Argument'; import type { Awaitable } from '@sapphire/utilities'; +import { Identifiers } from '../errors/Identifiers'; +import { container } from '@sapphire/pieces'; +import type { Command } from '../structures/Command'; export abstract class Args { public abstract start(): this; @@ -28,9 +31,32 @@ export abstract class Args { public abstract rest(options: T): Promise>; public abstract repeatResult(options: T): Promise>>; public abstract repeat(options: T): Promise[]>; - public abstract peekResult(options: T): Promise>>; - public abstract peek(options: T): Promise>; - // nextMaybe, next, getFlags, getOptionResult, getOption, getOptionsResult, getOptions should only go on message args + public abstract peekResult(options: T): Promise>>; + public abstract peek(options: T): Promise>; + + protected unavailableArgument(type: string | IArgument): Result.Err { + const name = typeof type === 'string' ? type : type.name; + return Result.err( + new UserError({ + identifier: Identifiers.ArgsUnavailable, + message: `The argument "${name}" was not found.`, + context: { name, ...this.toJSON() } + }) + ); + } + + protected missingArguments(): Result.Err { + return Result.err(new UserError({ identifier: Identifiers.ArgsMissing, message: 'There are no more arguments.', context: this.toJSON() })); + } + + /** + * Resolves an argument. + * @param arg The argument name or {@link IArgument} instance. + */ + protected resolveArgument(arg: keyof ArgType | IArgument): IArgument | undefined { + if (typeof arg === 'object') return arg; + return container.stores.get('arguments').get(arg as string) as IArgument | undefined; + } /** * Converts a callback into a usable argument. @@ -56,6 +82,46 @@ export abstract class Args { public static error(options: ArgumentError.Options): Result.Err> { return Result.err(new ArgumentError(options)); } + + /** + * Defines the `JSON.stringify` override. + */ + public abstract toJSON(): ArgsJson; +} + +export interface ArgsJson { + message: Message | AnyInteraction; + command: Command; + commandContext: Record; +} + +export interface ArgType { + boolean: boolean; + channel: ChannelTypes; + date: Date; + dmChannel: DMChannel; + emoji: EmojiObject; + float: number; + guildCategoryChannel: CategoryChannel; + guildChannel: GuildBasedChannelTypes; + guildNewsChannel: NewsChannel; + guildNewsThreadChannel: ThreadChannel & { type: ChannelType.AnnouncementThread; parent: NewsChannel | null }; + guildPrivateThreadChannel: ThreadChannel & { type: ChannelType.PrivateThread; parent: TextChannel | null }; + guildPublicThreadChannel: ThreadChannel & { type: ChannelType.PublicThread; parent: TextChannel | null }; + guildStageVoiceChannel: StageChannel; + guildTextChannel: TextChannel; + guildThreadChannel: ThreadChannel; + guildVoiceChannel: VoiceChannel; + hyperlink: URL; + integer: number; + member: GuildMember; + message: Message; + number: number; + role: Role; + string: string; + url: URL; + user: User; + enum: string; } export interface ArgsOptions @@ -125,13 +191,3 @@ export interface ArgType { export type ResultType = Result>; export type ArrayResultType = Result>; - -/** - * The callback used for {@link Args.nextMaybe} and {@link Args.next}. - */ -export interface ArgsNextCallback { - /** - * The value to be mapped. - */ - (value: string): Option; -} diff --git a/src/lib/parsers/ChatInputCommandArgs.ts b/src/lib/parsers/ChatInputCommandArgs.ts index 777efcbf9..b4dc59b03 100644 --- a/src/lib/parsers/ChatInputCommandArgs.ts +++ b/src/lib/parsers/ChatInputCommandArgs.ts @@ -1,32 +1,21 @@ -import type { AnyInteraction, ChannelTypes, GuildBasedChannelTypes } from '@sapphire/discord.js-utilities'; import { join, type Parameter } from '@sapphire/lexure'; -import { container } from '@sapphire/pieces'; -import { Option, Result } from '@sapphire/result'; -import type { - CategoryChannel, - ChannelType, - ChatInputCommandInteraction, - CommandInteraction, - DMChannel, - GuildMember, - Message, - NewsChannel, - Role, - StageChannel, - TextChannel, - ThreadChannel, - User, - VoiceChannel -} from 'discord.js'; -import type { URL } from 'node:url'; +import { Result } from '@sapphire/result'; +import type { ChatInputCommandInteraction, CommandInteractionOption } from 'discord.js'; import { ArgumentError } from '../errors/ArgumentError'; -import { Identifiers } from '../errors/Identifiers'; import { UserError } from '../errors/UserError'; -import type { EmojiObject } from '../resolvers/emoji'; -import type { IArgument } from '../structures/Argument'; import { Command } from '../structures/Command'; -import { Args, type ArgsOptions, type InferArgReturnType, type PeekArgsOptions, type RepeatArgsOptions } from './Args'; +import { + Args, + type ArgsJson, + type ArgsOptions, + type ArrayResultType, + type InferArgReturnType, + type PeekArgsOptions, + type RepeatArgsOptions, + type ResultType +} from './Args'; import type { ChatInputParser } from './ChatInputParser'; +import type { ChatInputCommand } from '../types/CommandTypes'; /** * The argument parser to be used in {@link Command}. @@ -40,7 +29,7 @@ export class ChatInputCommandArgs extends Args { /** * The command that is being run. */ - public readonly command: Command; + public readonly command: ChatInputCommand; /** * The context of the command being run. @@ -57,9 +46,14 @@ export class ChatInputCommandArgs extends Args { * @see Args#save * @see Args#restore */ - private readonly states: number[] = []; - - public constructor(interaction: ChatInputCommandInteraction, command: Command, parser: ChatInputParser, context: Record) { + private readonly states: Set[] = []; + + public constructor( + interaction: ChatInputCommandInteraction, + command: ChatInputCommand, + parser: ChatInputParser, + context: Record + ) { super(); this.interaction = interaction; this.command = command; @@ -126,7 +120,7 @@ export class ChatInputCommandArgs extends Args { const argument = this.resolveArgument(options.type); if (!argument) return this.unavailableArgument(options.type); - const result = await this.parser.singleParseAsync(async (arg) => + const result = await this.parser.singleParseAsync(options.name, async (arg) => argument.run(arg, { args: this, argument, @@ -317,7 +311,7 @@ export class ChatInputCommandArgs extends Args { const output: InferArgReturnType[] = []; for (let i = 0, times = options.times ?? Infinity; i < times; i++) { - const result = await this.parser.singleParseAsync(async (arg) => + const result = await this.parser.singleParseAsync(options.name, async (arg) => argument.run(arg, { args: this, argument, @@ -551,76 +545,4 @@ export class ChatInputCommandArgs extends Args { public toJSON(): ArgsJson { return { message: this.interaction, command: this.command, commandContext: this.commandContext }; } - - protected unavailableArgument(type: string | IArgument): Result.Err { - const name = typeof type === 'string' ? type : type.name; - return Result.err( - new UserError({ - identifier: Identifiers.ArgsUnavailable, - message: `The argument "${name}" was not found.`, - context: { name, ...this.toJSON() } - }) - ); - } - - protected missingArguments(): Result.Err { - return Result.err(new UserError({ identifier: Identifiers.ArgsMissing, message: 'There are no more arguments.', context: this.toJSON() })); - } - - /** - * Resolves an argument. - * @param arg The argument name or {@link IArgument} instance. - */ - private resolveArgument(arg: keyof ArgType | IArgument): IArgument | undefined { - if (typeof arg === 'object') return arg; - return container.stores.get('arguments').get(arg as string) as IArgument | undefined; - } } - -export interface ArgsJson { - message: Message | AnyInteraction; - command: Command; - commandContext: Record; -} - -export interface ArgType { - boolean: boolean; - channel: ChannelTypes; - date: Date; - dmChannel: DMChannel; - emoji: EmojiObject; - float: number; - guildCategoryChannel: CategoryChannel; - guildChannel: GuildBasedChannelTypes; - guildNewsChannel: NewsChannel; - guildNewsThreadChannel: ThreadChannel & { type: ChannelType.AnnouncementThread; parent: NewsChannel | null }; - guildPrivateThreadChannel: ThreadChannel & { type: ChannelType.PrivateThread; parent: TextChannel | null }; - guildPublicThreadChannel: ThreadChannel & { type: ChannelType.PublicThread; parent: TextChannel | null }; - guildStageVoiceChannel: StageChannel; - guildTextChannel: TextChannel; - guildThreadChannel: ThreadChannel; - guildVoiceChannel: VoiceChannel; - hyperlink: URL; - integer: number; - member: GuildMember; - message: Message; - number: number; - role: Role; - string: string; - url: URL; - user: User; - enum: string; -} - -/** - * The callback used for {@link Args.nextMaybe} and {@link Args.next}. - */ -export interface ArgsNextCallback { - /** - * The value to be mapped. - */ - (value: CommandInteraction): Option; -} - -export type ResultType = Result>; -export type ArrayResultType = Result>; diff --git a/src/lib/parsers/ChatInputParser.ts b/src/lib/parsers/ChatInputParser.ts index f6c20e6ad..3d83f5e1e 100644 --- a/src/lib/parsers/ChatInputParser.ts +++ b/src/lib/parsers/ChatInputParser.ts @@ -3,35 +3,39 @@ import { Option, Result } from '@sapphire/result'; import { type Parameter } from '@sapphire/lexure'; export class ChatInputParser { - public position: number = 0; + public used: Set = new Set(); public constructor(public interaction: CommandInteraction) {} public get finished(): boolean { - return this.position === this.interaction.options.data.length; + return this.used.size === this.interaction.options.data.length; } public reset(): void { - this.position = 0; + this.used.clear(); } - public save(): number { - return this.position; + public save(): Set { + return new Set(this.used); } - public restore(state: number): void { - this.position = state; + public restore(state: Set): void { + this.used = state; } public async singleParseAsync( + name: string, predicate: (arg: CommandInteractionOption) => Promise>, useAnyways?: boolean ): Promise> { if (this.finished) return Result.err(null); - const result = await predicate(this.interaction.options.data[this.position]); + const option = this.interaction.options.data.find((option) => option.name === name); + if (!option) return Result.err(null); + + const result = await predicate(option); if (result.isOk() || useAnyways) { - this.position++; + this.used.add(option); } return result; } diff --git a/src/lib/parsers/MessageArgs.ts b/src/lib/parsers/MessageArgs.ts index fc0bf68ec..e963606f4 100644 --- a/src/lib/parsers/MessageArgs.ts +++ b/src/lib/parsers/MessageArgs.ts @@ -1,29 +1,20 @@ -import type { AnyInteraction, ChannelTypes, GuildBasedChannelTypes } from '@sapphire/discord.js-utilities'; import { type ArgumentStream, join, type Parameter } from '@sapphire/lexure'; -import { container } from '@sapphire/pieces'; import { Option, Result } from '@sapphire/result'; -import type { - CategoryChannel, - ChannelType, - DMChannel, - GuildMember, - Message, - NewsChannel, - Role, - StageChannel, - TextChannel, - ThreadChannel, - User, - VoiceChannel -} from 'discord.js'; -import type { URL } from 'node:url'; +import type { Message } from 'discord.js'; import { ArgumentError } from '../errors/ArgumentError'; -import { Identifiers } from '../errors/Identifiers'; import { UserError } from '../errors/UserError'; -import type { EmojiObject } from '../resolvers/emoji'; -import type { IArgument } from '../structures/Argument'; import { Command } from '../structures/Command'; -import { Args, type ArgsOptions, type InferArgReturnType, type PeekArgsOptions, type RepeatArgsOptions } from './Args'; +import { + Args, + type ArgsJson, + type ArgsOptions, + type ArrayResultType, + type InferArgReturnType, + type PeekArgsOptions, + type RepeatArgsOptions, + type ResultType +} from './Args'; +import type { MessageCommand } from '../types/CommandTypes'; /** * The argument parser to be used in {@link Command}. @@ -37,7 +28,7 @@ export class MessageArgs extends Args { /** * The command that is being run. */ - public readonly command: Command; + public readonly command: MessageCommand; /** * The context of the command being run. @@ -56,7 +47,7 @@ export class MessageArgs extends Args { */ private readonly states: ArgumentStream.State[] = []; - public constructor(message: Message, command: Command, parser: ArgumentStream, context: Record) { + public constructor(message: Message, command: MessageCommand, parser: ArgumentStream, context: Record) { super(); this.message = message; this.command = command; @@ -721,65 +712,6 @@ export class MessageArgs extends Args { public toJSON(): ArgsJson { return { message: this.message, command: this.command, commandContext: this.commandContext }; } - - protected unavailableArgument(type: string | IArgument): Result.Err { - const name = typeof type === 'string' ? type : type.name; - return Result.err( - new UserError({ - identifier: Identifiers.ArgsUnavailable, - message: `The argument "${name}" was not found.`, - context: { name, ...this.toJSON() } - }) - ); - } - - protected missingArguments(): Result.Err { - return Result.err(new UserError({ identifier: Identifiers.ArgsMissing, message: 'There are no more arguments.', context: this.toJSON() })); - } - - /** - * Resolves an argument. - * @param arg The argument name or {@link IArgument} instance. - */ - private resolveArgument(arg: keyof ArgType | IArgument): IArgument | undefined { - if (typeof arg === 'object') return arg; - return container.stores.get('arguments').get(arg as string) as IArgument | undefined; - } -} - -export interface ArgsJson { - message: Message | AnyInteraction; - command: Command; - commandContext: Record; -} - -export interface ArgType { - boolean: boolean; - channel: ChannelTypes; - date: Date; - dmChannel: DMChannel; - emoji: EmojiObject; - float: number; - guildCategoryChannel: CategoryChannel; - guildChannel: GuildBasedChannelTypes; - guildNewsChannel: NewsChannel; - guildNewsThreadChannel: ThreadChannel & { type: ChannelType.AnnouncementThread; parent: NewsChannel | null }; - guildPrivateThreadChannel: ThreadChannel & { type: ChannelType.PrivateThread; parent: TextChannel | null }; - guildPublicThreadChannel: ThreadChannel & { type: ChannelType.PublicThread; parent: TextChannel | null }; - guildStageVoiceChannel: StageChannel; - guildTextChannel: TextChannel; - guildThreadChannel: ThreadChannel; - guildVoiceChannel: VoiceChannel; - hyperlink: URL; - integer: number; - member: GuildMember; - message: Message; - number: number; - role: Role; - string: string; - url: URL; - user: User; - enum: string; } /** @@ -791,6 +723,3 @@ export interface ArgsNextCallback { */ (value: string): Option; } - -export type ResultType = Result>; -export type ArrayResultType = Result>; diff --git a/src/lib/structures/Argument.ts b/src/lib/structures/Argument.ts index 9c53a48e3..d76351c17 100644 --- a/src/lib/structures/Argument.ts +++ b/src/lib/structures/Argument.ts @@ -6,6 +6,7 @@ import type { ArgumentError } from '../errors/ArgumentError'; import { Args } from '../parsers/Args'; import { Command } from './Command'; import type { AnyInteraction } from '@sapphire/discord.js-utilities'; +import type { ChatInputCommand, MessageCommand } from '../types/CommandTypes'; /** * Defines a synchronous result of an {@link Argument}, check {@link Argument.AsyncResult} for the asynchronous version. @@ -135,7 +136,7 @@ export interface ArgumentContext extends Record; args: Args; messageOrInteraction: Message | AnyInteraction; - command: Command; + command: MessageCommand | ChatInputCommand; commandContext: Record; minimum?: number; maximum?: number; diff --git a/src/lib/structures/Command.ts b/src/lib/structures/Command.ts index b92ab3070..f936824d6 100644 --- a/src/lib/structures/Command.ts +++ b/src/lib/structures/Command.ts @@ -30,7 +30,7 @@ import { emitPerRegistryError } from '../utils/application-commands/registriesEr import { PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray'; import { FlagUnorderedStrategy } from '../utils/strategies/FlagUnorderedStrategy'; import { ChatInputParser } from '../parsers/ChatInputParser'; -import { MessageArgs } from '../..'; +import { MessageArgs } from '../parsers/MessageArgs'; import { ChatInputCommandArgs } from '../parsers/ChatInputCommandArgs'; const ChannelTypes = Object.values(ChannelType).filter((type) => typeof type === 'number') as readonly ChannelType[]; @@ -145,12 +145,17 @@ export class Command { const parser = new Parser(this.strategy); const args = new ArgumentStream(parser.run(this.lexer.run(parameters))); - return new MessageArgs(message, this as Command, args, context) as PreParseReturn; + return new MessageArgs(message, this as MessageCommand, args, context) as PreParseReturn; } + /** + * The chat input pre-parse method. This method can be overridden by plugins to define their own argument parser. + * @param interaction The interaction that triggered the command. + * @param context The command-context used in this execution. + */ public chatInputPreParse(interaction: ChatInputCommandInteraction, context: ChatInputCommand.RunContext): Awaitable { const parser = new ChatInputParser(interaction); - return new ChatInputCommandArgs(interaction, this as Command, parser, context) as PreParseReturn; + return new ChatInputCommandArgs(interaction, this as ChatInputCommand, parser, context) as PreParseReturn; } /**