diff --git a/src/index.ts b/src/index.ts index 295c2c031c..bbb5765d5a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,7 +67,7 @@ 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/MessageArgs'; export * from './lib/plugins/Plugin'; export * from './lib/plugins/PluginManager'; export * from './lib/plugins/symbols'; diff --git a/src/lib/parsers/Args.ts b/src/lib/parsers/Args.ts index 91ef4dc13e..d9987820b2 100644 --- a/src/lib/parsers/Args.ts +++ b/src/lib/parsers/Args.ts @@ -1,12 +1,8 @@ -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 { Awaitable } from '@sapphire/utilities'; +import type { ChannelTypes, GuildBasedChannelTypes } from '@sapphire/discord.js-utilities'; +import { Result, type Option } from '@sapphire/result'; import type { CategoryChannel, ChannelType, - ChatInputCommandInteraction, DMChannel, GuildMember, Message, @@ -18,751 +14,23 @@ import type { User, VoiceChannel } from 'discord.js'; -import type { URL } from 'node:url'; import { ArgumentError } from '../errors/ArgumentError'; -import { Identifiers } from '../errors/Identifiers'; -import { UserError } from '../errors/UserError'; +import type { UserError } from '../errors/UserError'; import type { EmojiObject } from '../resolvers/emoji'; import type { Argument, IArgument } from '../structures/Argument'; -import { Command } from '../structures/Command'; -import type { Parser, Arg } from './Parser'; - -/** - * The argument parser to be used in {@link Command}. - */ -export class Args { - /** - * The original message or interaction that triggered the command. - */ - public readonly messageOrInteraction: Message | ChatInputCommandInteraction; - - /** - * The command that is being run. - */ - public readonly command: Command; - - /** - * The context of the command being run. - */ - public readonly commandContext: Record; - - /** - * The internal Lexure parser. - */ - protected readonly parser: Parser; - - /** - * The states stored in the args. - * @see Args#save - * @see Args#restore - */ - private readonly states: unknown[] = []; - - public constructor( - messageOrInteraction: Message | ChatInputCommandInteraction, - command: Command, - parser: Parser, - context: Record - ) { - this.messageOrInteraction = messageOrInteraction; - this.command = command; - this.parser = parser; - this.commandContext = context; - } - - /** - * Sets the parser to the first token. - */ - public start(): Args { - this.parser.reset(); - return this; - } - - /** - * Retrieves the next parameter and parses it. Advances index on success. - * @param type The type of the argument. - * @param options The pickResult options. - * @example - * ```typescript - * // !square 5 - * const resolver = Args.make((parameter, { argument }) => { - * const parsed = Number(parameter); - * if (Number.isNaN(parsed)) { - * return Args.error({ argument, parameter, identifier: 'ArgumentNumberNaN', message: 'You must write a valid number.' }); - * } - * - * return Args.ok(parsed); - * }); - * - * const a = await args.pickResult(resolver); - * if (!a.success) { - * throw new UserError({ identifier: 'ArgumentNumberNaN', message: 'You must write a valid number.' }); - * } - * - * await message.channel.send(`The result is: ${a.value ** 2}!`); - * // Sends "The result is: 25" - * ``` - */ - public async pickResult(type: IArgument, options?: ArgOptions): Promise>; - /** - * Retrieves the next parameter and parses it. Advances index on success. - * @param type The type of the argument. - * @param options The pickResult options. - * @example - * ```typescript - * // !add 1 2 - * const a = await args.pickResult('integer'); - * if (!a.success) { - * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write two numbers, but the first one did not match.' }); - * } - * - * const b = await args.pickResult('integer'); - * if (!b.success) { - * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write two numbers, but the second one did not match.' }); - * } - * - * await message.channel.send(`The result is: ${a.value + b.value}!`); - * // Sends "The result is: 3" - * ``` - */ - public async pickResult(type: K, options?: ArgOptions): Promise>; - public async pickResult(type: K, options: ArgOptions = {}): Promise> { - const argument = this.resolveArgument(type); - if (!argument) return this.unavailableArgument(type); - - const result = await this.parser.singleParseAsync(async (arg) => - argument.run(arg, { - args: this, - argument, - messageOrInteraction: this.messageOrInteraction, - command: this.command, - commandContext: this.commandContext, - ...options - }) - ); - if (result.isErrAnd((value) => value === null)) { - return this.missingArguments(); - } - - return result as ResultType; - } - - /** - * Similar to {@link Args.pickResult} but returns the value on success, throwing otherwise. - * @param type The type of the argument. - * @param options The pick options. - * @example - * ```typescript - * // !square 5 - * const resolver = Args.make((parameter, { argument }) => { - * const parsed = Number(parameter); - * if (Number.isNaN(parsed)) { - * return Args.error({ argument, parameter, identifier: 'ArgumentNumberNaN', message: 'You must write a valid number.' }); - * } - * - * return Args.ok(parsed); - * }); - * - * const a = await args.pick(resolver); - * - * await message.channel.send(`The result is: ${a ** 2}!`); - * // Sends "The result is: 25" - * ``` - */ - public async pick(type: IArgument, options?: ArgOptions): Promise; - /** - * Similar to {@link Args.pickResult} but returns the value on success, throwing otherwise. - * @param type The type of the argument. - * @param options The pick options. - * @example - * ```typescript - * // !add 1 2 - * const a = await args.pick('integer'); - * const b = await args.pick('integer'); - * await message.channel.send(`The result is: ${a + b}!`); - * // Sends "The result is: 3" - * ``` - */ - public async pick(type: K, options?: ArgOptions): Promise; - public async pick(type: K, options?: ArgOptions): Promise { - const result = await this.pickResult(type, options); - return result.unwrapRaw(); - } - - /** - * Retrieves all the following arguments. - * @param type The type of the argument. - * @param options The restResult options. - * @example - * ```typescript - * // !reverse Hello world! - * const resolver = Args.make((parameter) => Args.ok(parameter.split('').reverse())); - * - * const a = await args.restResult(resolver); - * if (!a.success) { - * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write some text.' }); - * } - * - * await message.channel.send(`The reversed value is... ${a.value}`); - * // Sends "The reversed value is... !dlrow olleH" - * ``` - */ - public async restResult(type: IArgument, options?: ArgOptions): Promise>; - /** - * Retrieves all the following arguments. - * @param type The type of the argument. - * @param options The restResult options. - * @example - * ```typescript - * // !add 2 Hello World! - * const a = await args.pickResult('integer'); - * if (!a.success) { - * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write a number and a text, but the former did not match.' }); - * } - * - * const b = await args.restResult('string', { minimum: 1 }); - * if (!b.success) { - * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write a number and a text, but the latter did not match.' }); - * } - * - * await message.channel.send(`The repeated value is... ${b.value.repeat(a.value)}!`); - * // Sends "The repeated value is... Hello World!Hello World!" - * ``` - */ - public async restResult(type: K, options?: ArgOptions): Promise>; - public async restResult(type: keyof ArgType | IArgument, options: ArgOptions = {}): Promise> { - const argument = this.resolveArgument(type); - if (!argument) return this.unavailableArgument(type); - if (this.parser.finished) return this.missingArguments(); - - const state = this.parser.save(); - const data = join(this.parser.many().unwrapOr([])); - const result = await argument.run(data, { - args: this, - argument, - messageOrInteraction: this.messageOrInteraction, - command: this.command, - commandContext: this.commandContext, - ...options - }); - - return result.inspectErr(() => this.parser.restore(state)); - } - - /** - * Similar to {@link Args.restResult} but returns the value on success, throwing otherwise. - * @param type The type of the argument. - * @param options The rest options. - * @example - * ```typescript - * // !reverse Hello world! - * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse())); - * const a = await args.rest(resolver); - * await message.channel.send(`The reversed value is... ${a}`); - * // Sends "The reversed value is... !dlrow olleH" - * ``` - */ - public async rest(type: IArgument, options?: ArgOptions): Promise; - /** - * Similar to {@link Args.restResult} but returns the value on success, throwing otherwise. - * @param type The type of the argument. - * @param options The rest options. - * @example - * ```typescript - * // !add 2 Hello World! - * const a = await args.pick('integer'); - * const b = await args.rest('string', { minimum: 1 }); - * await message.channel.send(`The repeated value is... ${b.repeat(a)}!`); - * // Sends "The repeated value is... Hello World!Hello World!" - * ``` - */ - public async rest(type: K, options?: ArgOptions): Promise; - public async rest(type: K, options?: ArgOptions): Promise { - const result = await this.restResult(type, options); - return result.unwrapRaw(); - } - - /** - * Retrieves all the following arguments. - * @param type The type of the argument. - * @param options The repeatResult options. - * @example - * ```typescript - * // !add 2 Hello World! - * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse())); - * const result = await args.repeatResult(resolver, { times: 5 }); - * if (!result.success) { - * throw new UserError({ identifier: 'CountArgumentError', message: 'You must write up to 5 words.' }); - * } - * - * await message.channel.send(`You have written ${result.value.length} word(s): ${result.value.join(' ')}`); - * // Sends "You have written 2 word(s): olleH !dlroW" - * ``` - */ - public async repeatResult(type: IArgument, options?: RepeatArgOptions): Promise>; - /** - * Retrieves all the following arguments. - * @param type The type of the argument. - * @param options The repeatResult options. - * @example - * ```typescript - * // !reverse-each 2 Hello World! - * const result = await args.repeatResult('string', { times: 5 }); - * if (!result.success) { - * throw new UserError({ identifier: 'CountArgumentError', message: 'You must write up to 5 words.' }); - * } - * - * await message.channel.send(`You have written ${result.value.length} word(s): ${result.value.join(' ')}`); - * // Sends "You have written 2 word(s): Hello World!" - * ``` - */ - public async repeatResult(type: K, options?: RepeatArgOptions): Promise>; - public async repeatResult(type: K, options: RepeatArgOptions = {}): Promise> { - const argument = this.resolveArgument(type); - if (!argument) return this.unavailableArgument(type); - if (this.parser.finished) return this.missingArguments(); - - const output: ArgType[K][] = []; - - for (let i = 0, times = options.times ?? Infinity; i < times; i++) { - const result = await this.parser.singleParseAsync(async (arg) => - argument.run(arg, { - args: this, - argument, - messageOrInteraction: this.messageOrInteraction, - command: this.command, - commandContext: this.commandContext, - ...options - }) - ); - - if (result.isErr()) { - const error = result.unwrapErr(); - if (error === null) break; - - if (output.length === 0) { - return result as Result.Err>; - } - - break; - } - - output.push(result.unwrap() as ArgType[K]); - } - - return Result.ok(output); - } - - /** - * Similar to {@link Args.repeatResult} but returns the value on success, throwing otherwise. - * @param type The type of the argument. - * @param options The repeat options. - * @example - * ```typescript - * // !reverse-each 2 Hello World! - * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse())); - * const result = await args.repeat(resolver, { times: 5 }); - * await message.channel.send(`You have written ${result.length} word(s): ${result.join(' ')}`); - * // Sends "You have written 2 word(s): Hello World!" - * ``` - */ - public async repeat(type: IArgument, options?: RepeatArgOptions): Promise; - /** - * Similar to {@link Args.repeatResult} but returns the value on success, throwing otherwise. - * @param type The type of the argument. - * @param options The repeat options. - * @example - * ```typescript - * // !add 2 Hello World! - * const words = await args.repeat('string', { times: 5 }); - * await message.channel.send(`You have written ${words.length} word(s): ${words.join(' ')}`); - * // Sends "You have written 2 word(s): Hello World!" - * ``` - */ - public async repeat(type: K, options?: RepeatArgOptions): Promise; - public async repeat(type: K, options?: RepeatArgOptions): Promise { - const result = await this.repeatResult(type, options); - return result.unwrapRaw(); - } - - /** - * Peeks the following parameter(s) without advancing the parser's state. - * Passing a function as a parameter allows for returning {@link Args.pickResult}, {@link Args.repeatResult}, - * or {@link Args.restResult}; otherwise, passing the custom argument or the argument type with options - * will use {@link Args.pickResult} and only peek a single argument. - * @param type The function, custom argument, or argument name. - * @example - * ```typescript - * // !reversedandscreamfirst hello world - * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse().join(''))); - * - * const result = await args.repeatResult(resolver); - * await result.inspectAsync((value) => - * message.channel.send(`Reversed ${value.length} word(s): ${value.join(' ')}`) - * ); // Reversed 2 word(s): olleh dlrow - * - * const firstWord = await args.pickResult('string'); - * await firstWord.inspectAsync((value) => - * message.channel.send(firstWord.value.toUpperCase()) - * ); // HELLO - * ``` - */ - public async peekResult(type: () => Argument.Result): Promise>; - /** - * Peeks the following parameter(s) without advancing the parser's state. - * Passing a function as a parameter allows for returning {@link Args.pickResult}, {@link Args.repeatResult}, - * or {@link Args.restResult}; otherwise, passing the custom argument or the argument type with options - * will use {@link Args.pickResult} and only peek a single argument. - * @param type The function, custom argument, or argument name. - * @param options The peekResult options. - * @example - * ```typescript - * // !reverseandscreamfirst sapphire community - * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse().join(''))); - * - * const peekedWord = await args.peekResult(resolver); - * await peekedWord.inspectAsync((value) => message.channel.send(value)); // erihppas - * - * const firstWord = await args.pickResult('string'); - * await firstWord.inspectAsync((value) => message.channel.send(value.toUpperCase())); // SAPPHIRE - * ``` - */ - public async peekResult(type: IArgument, options?: ArgOptions): Promise>; - /** - * Peeks the following parameter(s) without advancing the parser's state. - * Passing a function as a parameter allows for returning {@link Args.pickResult}, {@link Args.repeatResult}, - * or {@link Args.restResult}; otherwise, passing the custom argument or the argument type with options - * will use {@link Args.pickResult} and only peek a single argument. - * @param type The function, custom argument, or argument name. - * @param options The peekResult options. - * @example - * ```typescript - * // !datethenaddtwo 1608867472611 - * const date = await args.peekResult('date'); - * await date.inspectAsync((value) => - * message.channel.send(`Your date (in UTC): ${value.toUTCString()}`) - * ); // Your date (in UTC): Fri, 25 Dec 2020 03:37:52 GMT - * - * const result = await args.pickResult('number', { maximum: Number.MAX_SAFE_INTEGER - 2 }); - * await result.inspectAsync((value) => - * message.channel.send(`Your number plus two: ${value + 2}`) - * ); // Your number plus two: 1608867472613 - * ``` - */ - public async peekResult( - type: (() => Awaitable>) | K, - options?: ArgOptions - ): Promise>; - - public async peekResult( - type: (() => Awaitable>) | K, - options: ArgOptions = {} - ): Promise> { - this.save(); - const result = typeof type === 'function' ? await type() : await this.pickResult(type, options); - this.restore(); - return result; - } - - /** - * Similar to {@link Args.peekResult} but returns the value on success, throwing otherwise. - * @param type The function, custom argument, or argument name. - * @example - * ```typescript - * // !bigintsumthensquarefirst 25 50 75 - * const resolver = Args.make((arg, { argument }) => { - * try { - * return Args.ok(BigInt(arg)); - * } catch { - * return Args.error({ parameter: arg, argument, identifier: 'InvalidBigInt', message: 'You must specify a valid number for a bigint.' }) - * } - * }); - * - * const peeked = await args.repeatResult(resolver); - * await peeked.inspectAsync((value) => message.channel.send(`Sum: **${value.reduce((x, y) => x + y, 0n)}**`)); // Sum: 150n - * - * const first = await args.pick(resolver); - * await message.channel.send(`First bigint squared: ${first**2n}`); // First bigint squared: 625 - * ``` - */ - public async peek(type: () => Argument.Result): Promise; - /** - * Similar to {@link Args.peekResult} but returns the value on success, throwing otherwise. - * @param type The function, custom argument, or argument name. - * @param options The peek options. - * @example - * ```typescript - * import { SnowflakeRegex } from '@sapphire/discord.js-utilities'; - * import { DiscordSnowflake } from '@sapphire/snowflake'; - * - * // !createdat 730159185517477900 - * const snowflakeResolver = Args.make((arg, { argument }) => { - * return SnowflakeRegex.test(arg) - * ? Args.ok(BigInt(arg)) - * : Args.error({ parameter: arg, argument, identifier: 'InvalidSnowflake', message: 'You must specify a valid snowflake.' }); - * }); - * - * const snowflake = await args.peek(snowflakeResolver); - * const timestamp = Number((snowflake >> 22n) + DiscordSnowflake.epoch); - * const createdAt = new Date(timestamp); - * - * await message.channel.send( - * `The snowflake ${snowflake} was registered on ${createdAt.toUTCString()}.` - * ); // The snowflake 730159185517477900 was registered on Tue, 07 Jul 2020 20:31:55 GMT. - * - * const id = await args.pick('string'); - * await message.channel.send(`Your ID, reversed: ${id.split('').reverse().join('')}`); // Your ID, reversed: 009774715581951037 - * ``` - */ - public async peek(type: IArgument, options?: ArgOptions): Promise; - /** - * Similar to {@link Args.peekResult} but returns the value on success, throwing otherwise. - * @param type The function, custom argument, or argument name. - * @param options The peek options. - * @example - * ```typescript - * // !messagelink https://discord.com/channels/737141877803057244/737142209639350343/791843123898089483 - * const remoteMessage = await args.peek('message'); - * await message.channel.send( - * `${remoteMessage.author.tag}: ${remoteMessage.content}` - * ); // RealShadowNova#7462: Yeah, Sapphire has been a great experience so far, especially being able to help and contribute. - * - * const url = await args.pick('hyperlink'); - * await message.channel.send(`Hostname: ${url.hostname}`); // Hostname: discord.com - * ``` - */ - public async peek(type: (() => Argument.Result) | K, options?: ArgOptions): Promise; - public async peek(type: (() => Argument.Result) | K, options?: ArgOptions): Promise { - const result = await this.peekResult(type, options); - return result.unwrapRaw(); - } - - /** - * Retrieves the next raw argument from the parser. - * @example - * ```typescript - * // !numbers 1 2 3 - * - * console.log(args.nextMaybe()); - * // -> { exists: true, value: '1' } - * ``` - */ - public nextMaybe(): Option; - /** - * Retrieves the value of the next unused ordered token, but only if it could be transformed. - * That token will now be used if the transformation succeeds. - * @typeparam T Output type of the {@link ArgsNextCallback callback}. - * @param cb Gives an option of either the resulting value, or nothing if failed. - * @example - * ```typescript - * // !numbers 1 2 3 - * const parse = (x: string) => { - * const n = Number(x); - * return Number.isNaN(n) ? none() : some(n); - * }; - * - * console.log(args.nextMaybe(parse)); - * // -> { exists: true, value: 1 } - * ``` - */ - public nextMaybe(cb: ArgsNextCallback): Option; - public nextMaybe(cb?: ArgsNextCallback): Option { - return Option.from(typeof cb === 'function' ? this.parser.singleMap(cb) : this.parser.single()); - } - - /** - * Similar to {@link Args.nextMaybe} but returns the value on success, null otherwise. - * @example - * ```typescript - * // !numbers 1 2 3 - * - * console.log(args.next()); - * // -> '1' - * ``` - */ - public next(): string; - /** - * Similar to {@link Args.nextMaybe} but returns the value on success, null otherwise. - * @typeparam T Output type of the {@link ArgsNextCallback callback}. - * @param cb Gives an option of either the resulting value, or nothing if failed. - * @example - * ```typescript - * // !numbers 1 2 3 - * const parse = (x: string) => { - * const n = Number(x); - * return Number.isNaN(n) ? none() : some(n); - * }; - * - * console.log(args.nextMaybe(parse)); - * // -> 1 - * ``` - */ - public next(cb: ArgsNextCallback): T; - public next(cb?: ArgsNextCallback): T | Arg | null { - const value = cb ? this.nextMaybe(cb) : this.nextMaybe(); - return value.unwrapOr(null); - } - - /** - * Checks if one or more flag were given. - * @param keys The name(s) of the flag. - * @example - * ```typescript - * // Suppose args are from '--f --g'. - * console.log(args.getFlags('f')); - * // >>> true - * - * console.log(args.getFlags('g', 'h')); - * // >>> true - * - * console.log(args.getFlags('h')); - * // >>> false - * ``` - */ - public getFlags(...keys: readonly string[]): boolean { - return this.parser.flag(...keys); - } - - /** - * Gets the last value of one or more options as an {@link Option}. - * If you do not care about safely handling non-existing values - * you can use {@link Args.getOption} to get `string | null` as return type - * @param keys The name(s) of the option. - * @example - * ```typescript - * // Suppose args are from '--a=1 --b=2 --c=3'. - * console.log(args.getOptionResult('a')); - * // >>> Some { value: '1' } - * - * console.log(args.getOptionResult('b', 'c')); - * // >>> Some { value: '2' } - * - * console.log(args.getOptionResult('d')); - * // >>> None {} - * ``` - */ - public getOptionResult(...keys: readonly string[]): Option { - return this.parser.option(...keys); - } - - /** - * Gets the last value of one or more options. - * Similar to {@link Args.getOptionResult} but returns the value on success, or `null` if not. - * @param keys The name(s) of the option. - * @example - * ```typescript - * // Suppose args are from '--a=1 --b=2 --c=3'. - * console.log(args.getOption('a')); - * // >>> '1' - * - * console.log(args.getOption('b', 'c')); - * // >>> '2' - * - * console.log(args.getOption('d')); - * // >>> null - * ``` - */ - public getOption(...keys: readonly string[]): string | null { - return this.parser.option(...keys).unwrapOr(null); - } - - /** - * Gets all the values of one or more option. - * @param keys The name(s) of the option. - * @example - * ```typescript - * // Suppose args are from '--a=1 --a=1 --b=2 --c=3'. - * console.log(args.getOptionsResult('a')); - * // >>> Some { value: [ '1' ] } - * - * console.log(args.getOptionsResult('a', 'd')); - * // >>> Some { value: [ '1' ] } - * - * console.log(args.getOptionsResult('b', 'c')); - * // >>> Some { value: [ '2', '3' ] } - * - * console.log(args.getOptionsResult('d')); - * // >>> None {} - * ``` - */ - public getOptionsResult(...keys: readonly string[]): Option { - return this.parser.options(...keys); - } - - /** - * Gets all the values of one or more option. - * Similar to {@link Args.getOptionsResult} but returns the value on success, or `null` if not. - * @param keys The name(s) of the option. - * @example - * ```typescript - * // Suppose args are from '--a=1 --a=1 --b=2 --c=3'. - * console.log(args.getOptions('a')); - * // >>> ['1', '1'] - * - * console.log(args.getOptions('b', 'c')); - * // >>> ['2', '3'] - * - * console.log(args.getOptions('d')); - * // >>> null - * ``` - */ - public getOptions(...keys: readonly string[]): readonly string[] | null { - return this.parser.options(...keys).unwrapOr(null); - } - - /** - * Saves the current state into the stack following a FILO strategy (first-in, last-out). - * @see Args#restore - */ - public save(): void { - this.states.push(this.parser.save()); - } - - /** - * Restores the previously saved state from the stack. - * @see Args#save - */ - public restore(): void { - if (this.states.length !== 0) this.parser.restore(this.states.pop()!); - } - - /** - * Whether all arguments have been consumed. - */ - public get finished(): boolean { - return this.parser.finished; - } - - /** - * Defines the `JSON.stringify` override. - */ - public toJSON(): ArgsJson { - return { message: this.messageOrInteraction, 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() })); - } +import type { Awaitable } from '@sapphire/utilities'; - /** - * 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 abstract class Args { + public abstract start(): this; + public abstract pickResult(options: T): Promise>>; + public abstract pick(options: T): Promise>; + public abstract restResult(options: T): Promise>>; + 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 /** * Converts a callback into a usable argument. @@ -790,12 +58,41 @@ export class Args { } } -export interface ArgsJson { - message: Message | AnyInteraction; - command: Command; - commandContext: Record; +export interface ArgsOptions + extends Omit { + // Up to the person implementing if this should always be required or only required for chat commands, but this should + // always be required for chat commands, and be used to find the starting point for our parsing + name: string; + type: IArgument | K; + minimum?: number; + maximum?: number; + inclusive?: boolean; } +export interface RepeatArgsOptions extends ArgsOptions { + /** + * The maximum amount of times the argument can be repeated. + * @default Infinity + */ + times?: number; +} + +export interface PeekArgsOptions extends Omit { + type: () => Awaitable> | K; +} + +export type InferArgReturnType = T extends ArgsOptions + ? T['type'] extends IArgument + ? R + : T['type'] extends keyof ArgType + ? ArgType[T['type']] + : never + : T['type'] extends Awaitable> + ? R + : T['type'] extends keyof ArgType + ? ArgType[T['type']] + : never; + export interface ArgType { boolean: boolean; channel: ChannelTypes; @@ -825,15 +122,8 @@ export interface ArgType { enum: string; } -export interface ArgOptions extends Omit {} - -export interface RepeatArgOptions extends ArgOptions { - /** - * The maximum amount of times the argument can be repeated. - * @default Infinity - */ - times?: number; -} +export type ResultType = Result>; +export type ArrayResultType = Result>; /** * The callback used for {@link Args.nextMaybe} and {@link Args.next}. @@ -842,8 +132,5 @@ export interface ArgsNextCallback { /** * The value to be mapped. */ - (value: Arg): Option; + (value: string): Option; } - -export type ResultType = Result>; -export type ArrayResultType = Result>; diff --git a/src/lib/parsers/ChatInputCommandArgs.ts b/src/lib/parsers/ChatInputCommandArgs.ts new file mode 100644 index 0000000000..cbd92028aa --- /dev/null +++ b/src/lib/parsers/ChatInputCommandArgs.ts @@ -0,0 +1,637 @@ +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 { Awaitable } from '@sapphire/utilities'; +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 { ArgumentError } from '../errors/ArgumentError'; +import { Identifiers } from '../errors/Identifiers'; +import { UserError } from '../errors/UserError'; +import type { EmojiObject } from '../resolvers/emoji'; +import type { Argument, IArgument } from '../structures/Argument'; +import { Command } from '../structures/Command'; +import { Args, type ArgsOptions, type InferArgReturnType, type RepeatArgsOptions } from './Args'; +import type { ChatInputParser } from './ChatInputParser'; + +/** + * The argument parser to be used in {@link Command}. + */ +export class ChatInputCommandArgs extends Args { + /** + * The original interaction that triggered the command. + */ + public readonly interaction: ChatInputCommandInteraction; + + /** + * The command that is being run. + */ + public readonly command: Command; + + /** + * The context of the command being run. + */ + public readonly commandContext: Record; + + /** + * The internal parser. + */ + protected readonly parser: ChatInputParser; + + /** + * The states stored in the args. + * @see Args#save + * @see Args#restore + */ + private readonly states: number[] = []; + + public constructor(interaction: ChatInputCommandInteraction, command: Command, parser: ChatInputParser, context: Record) { + super(); + this.interaction = interaction; + this.command = command; + this.parser = parser; + this.commandContext = context; + } + + /** + * Sets the parser to the first token. + */ + public start(): this { + this.parser.reset(); + return this; + } + + /** + * Retrieves the next parameter and parses it. Advances index on success. + * @param type The type of the argument. + * @param options The pickResult options. + * @example + * ```typescript + * // !square 5 + * const resolver = Args.make((parameter, { argument }) => { + * const parsed = Number(parameter); + * if (Number.isNaN(parsed)) { + * return Args.error({ argument, parameter, identifier: 'ArgumentNumberNaN', message: 'You must write a valid number.' }); + * } + * + * return Args.ok(parsed); + * }); + * + * const a = await args.pickResult(resolver); + * if (!a.success) { + * throw new UserError({ identifier: 'ArgumentNumberNaN', message: 'You must write a valid number.' }); + * } + * + * await message.channel.send(`The result is: ${a.value ** 2}!`); + * // Sends "The result is: 25" + * ``` + */ + public async pickResult(options: T): Promise>>; + /** + * Retrieves the next parameter and parses it. Advances index on success. + * @param type The type of the argument. + * @param options The pickResult options. + * @example + * ```typescript + * // !add 1 2 + * const a = await args.pickResult('integer'); + * if (!a.success) { + * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write two numbers, but the first one did not match.' }); + * } + * + * const b = await args.pickResult('integer'); + * if (!b.success) { + * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write two numbers, but the second one did not match.' }); + * } + * + * await message.channel.send(`The result is: ${a.value + b.value}!`); + * // Sends "The result is: 3" + * ``` + */ + public async pickResult(options: T): Promise>> { + const argument = this.resolveArgument(options.type); + if (!argument) return this.unavailableArgument(options.type); + + const result = await this.parser.singleParseAsync(async (arg) => + argument.run(arg, { + args: this, + argument, + messageOrInteraction: this.interaction, + command: this.command, + commandContext: this.commandContext, + ...options + }) + ); + if (result.isErrAnd((value) => value === null)) { + return this.missingArguments(); + } + + return result as ResultType>; + } + + /** + * Similar to {@link Args.pickResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The pick options. + * @example + * ```typescript + * // !square 5 + * const resolver = Args.make((parameter, { argument }) => { + * const parsed = Number(parameter); + * if (Number.isNaN(parsed)) { + * return Args.error({ argument, parameter, identifier: 'ArgumentNumberNaN', message: 'You must write a valid number.' }); + * } + * + * return Args.ok(parsed); + * }); + * + * const a = await args.pick(resolver); + * + * await message.channel.send(`The result is: ${a ** 2}!`); + * // Sends "The result is: 25" + * ``` + */ + public async pick(options: T): Promise>; + /** + * Similar to {@link Args.pickResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The pick options. + * @example + * ```typescript + * // !add 1 2 + * const a = await args.pick('integer'); + * const b = await args.pick('integer'); + * await message.channel.send(`The result is: ${a + b}!`); + * // Sends "The result is: 3" + * ``` + */ + public async pick(options: T): Promise> { + const result = await this.pickResult(options); + return result.unwrapRaw(); + } + + /** + * Retrieves all the following arguments. + * @param type The type of the argument. + * @param options The restResult options. + * @example + * ```typescript + * // !reverse Hello world! + * const resolver = Args.make((parameter) => Args.ok(parameter.split('').reverse())); + * + * const a = await args.restResult(resolver); + * if (!a.success) { + * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write some text.' }); + * } + * + * await message.channel.send(`The reversed value is... ${a.value}`); + * // Sends "The reversed value is... !dlrow olleH" + * ``` + */ + public async restResult(options: T): Promise>>; + /** + * Retrieves all the following arguments. + * @param type The type of the argument. + * @param options The restResult options. + * @example + * ```typescript + * // !add 2 Hello World! + * const a = await args.pickResult('integer'); + * if (!a.success) { + * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write a number and a text, but the former did not match.' }); + * } + * + * const b = await args.restResult('string', { minimum: 1 }); + * if (!b.success) { + * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write a number and a text, but the latter did not match.' }); + * } + * + * await message.channel.send(`The repeated value is... ${b.value.repeat(a.value)}!`); + * // Sends "The repeated value is... Hello World!Hello World!" + * ``` + */ + public async restResult(options: T): Promise>>; + public async restResult(options: T): Promise>> { + const argument = this.resolveArgument(options.type); + if (!argument) return this.unavailableArgument(options.type); + if (this.parser.finished) return this.missingArguments(); + + const state = this.parser.save(); + const data = join(this.parser.many().unwrapOr([])); + const result = await argument.run(data, { + args: this, + argument, + messageOrInteraction: this.interaction, + command: this.command, + commandContext: this.commandContext, + ...options + }); + + return result.inspectErr(() => this.parser.restore(state)) as ResultType>; + } + + /** + * Similar to {@link Args.restResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The rest options. + * @example + * ```typescript + * // !reverse Hello world! + * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse())); + * const a = await args.rest(resolver); + * await message.channel.send(`The reversed value is... ${a}`); + * // Sends "The reversed value is... !dlrow olleH" + * ``` + */ + public async rest(options: T): Promise>; + /** + * Similar to {@link Args.restResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The rest options. + * @example + * ```typescript + * // !add 2 Hello World! + * const a = await args.pick('integer'); + * const b = await args.rest('string', { minimum: 1 }); + * await message.channel.send(`The repeated value is... ${b.repeat(a)}!`); + * // Sends "The repeated value is... Hello World!Hello World!" + * ``` + */ + public async rest(options: T): Promise>; + public async rest(options: T): Promise> { + const result = await this.restResult(options); + return result.unwrapRaw(); + } + + /** + * Retrieves all the following arguments. + * @param type The type of the argument. + * @param options The repeatResult options. + * @example + * ```typescript + * // !add 2 Hello World! + * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse())); + * const result = await args.repeatResult(resolver, { times: 5 }); + * if (!result.success) { + * throw new UserError({ identifier: 'CountArgumentError', message: 'You must write up to 5 words.' }); + * } + * + * await message.channel.send(`You have written ${result.value.length} word(s): ${result.value.join(' ')}`); + * // Sends "You have written 2 word(s): olleH !dlroW" + * ``` + */ + public async repeatResult(options: T): Promise>>; + /** + * Retrieves all the following arguments. + * @param type The type of the argument. + * @param options The repeatResult options. + * @example + * ```typescript + * // !reverse-each 2 Hello World! + * const result = await args.repeatResult('string', { times: 5 }); + * if (!result.success) { + * throw new UserError({ identifier: 'CountArgumentError', message: 'You must write up to 5 words.' }); + * } + * + * await message.channel.send(`You have written ${result.value.length} word(s): ${result.value.join(' ')}`); + * // Sends "You have written 2 word(s): Hello World!" + * ``` + */ + public async repeatResult(options: T): Promise>>; + public async repeatResult(options: T): Promise>> { + const argument = this.resolveArgument(options.type); + if (!argument) return this.unavailableArgument(options.type); + if (this.parser.finished) return this.missingArguments(); + + const output: InferArgReturnType[] = []; + + for (let i = 0, times = options.times ?? Infinity; i < times; i++) { + const result = await this.parser.singleParseAsync(async (arg) => + argument.run(arg, { + args: this, + argument, + messageOrInteraction: this.interaction, + command: this.command, + commandContext: this.commandContext, + ...options + }) + ); + + if (result.isErr()) { + const error = result.unwrapErr(); + if (error === null) break; + + if (output.length === 0) { + return result as Result.Err>>; + } + + break; + } + + output.push(result.unwrap() as InferArgReturnType); + } + + return Result.ok(output); + } + + /** + * Similar to {@link Args.repeatResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The repeat options. + * @example + * ```typescript + * // !reverse-each 2 Hello World! + * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse())); + * const result = await args.repeat(resolver, { times: 5 }); + * await message.channel.send(`You have written ${result.length} word(s): ${result.join(' ')}`); + * // Sends "You have written 2 word(s): Hello World!" + * ``` + */ + public async repeat(options: T): Promise[]>; + /** + * Similar to {@link Args.repeatResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The repeat options. + * @example + * ```typescript + * // !add 2 Hello World! + * const words = await args.repeat('string', { times: 5 }); + * await message.channel.send(`You have written ${words.length} word(s): ${words.join(' ')}`); + * // Sends "You have written 2 word(s): Hello World!" + * ``` + */ + public async repeat(options: T): Promise[]>; + public async repeat(options: T): Promise[]> { + const result = await this.repeatResult(options); + return result.unwrapRaw(); + } + + /** + * Peeks the following parameter(s) without advancing the parser's state. + * Passing a function as a parameter allows for returning {@link Args.pickResult}, {@link Args.repeatResult}, + * or {@link Args.restResult}; otherwise, passing the custom argument or the argument type with options + * will use {@link Args.pickResult} and only peek a single argument. + * @param type The function, custom argument, or argument name. + * @example + * ```typescript + * // !reversedandscreamfirst hello world + * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse().join(''))); + * + * const result = await args.repeatResult(resolver); + * await result.inspectAsync((value) => + * message.channel.send(`Reversed ${value.length} word(s): ${value.join(' ')}`) + * ); // Reversed 2 word(s): olleh dlrow + * + * const firstWord = await args.pickResult('string'); + * await firstWord.inspectAsync((value) => + * message.channel.send(firstWord.value.toUpperCase()) + * ); // HELLO + * ``` + */ + public async peekResult(type: () => Argument.Result): Promise>; + /** + * Peeks the following parameter(s) without advancing the parser's state. + * Passing a function as a parameter allows for returning {@link Args.pickResult}, {@link Args.repeatResult}, + * or {@link Args.restResult}; otherwise, passing the custom argument or the argument type with options + * will use {@link Args.pickResult} and only peek a single argument. + * @param type The function, custom argument, or argument name. + * @param options The peekResult options. + * @example + * ```typescript + * // !reverseandscreamfirst sapphire community + * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse().join(''))); + * + * const peekedWord = await args.peekResult(resolver); + * await peekedWord.inspectAsync((value) => message.channel.send(value)); // erihppas + * + * const firstWord = await args.pickResult('string'); + * await firstWord.inspectAsync((value) => message.channel.send(value.toUpperCase())); // SAPPHIRE + * ``` + */ + public async peekResult(options: T): Promise>; + /** + * Peeks the following parameter(s) without advancing the parser's state. + * Passing a function as a parameter allows for returning {@link Args.pickResult}, {@link Args.repeatResult}, + * or {@link Args.restResult}; otherwise, passing the custom argument or the argument type with options + * will use {@link Args.pickResult} and only peek a single argument. + * @param type The function, custom argument, or argument name. + * @param options The peekResult options. + * @example + * ```typescript + * // !datethenaddtwo 1608867472611 + * const date = await args.peekResult('date'); + * await date.inspectAsync((value) => + * message.channel.send(`Your date (in UTC): ${value.toUTCString()}`) + * ); // Your date (in UTC): Fri, 25 Dec 2020 03:37:52 GMT + * + * const result = await args.pickResult('number', { maximum: Number.MAX_SAFE_INTEGER - 2 }); + * await result.inspectAsync((value) => + * message.channel.send(`Your number plus two: ${value + 2}`) + * ); // Your number plus two: 1608867472613 + * ``` + */ + public async peekResult( + type: (() => Awaitable>) | K, + options?: ArgOptions + ): Promise>; + + public async peekResult( + type: (() => Awaitable>) | K, + options: ArgsOptions + ): Promise> { + this.save(); + const result = typeof type === 'function' ? await type() : await this.pickResult(options); + this.restore(); + return result; + } + + /** + * Similar to {@link Args.peekResult} but returns the value on success, throwing otherwise. + * @param type The function, custom argument, or argument name. + * @example + * ```typescript + * // !bigintsumthensquarefirst 25 50 75 + * const resolver = Args.make((arg, { argument }) => { + * try { + * return Args.ok(BigInt(arg)); + * } catch { + * return Args.error({ parameter: arg, argument, identifier: 'InvalidBigInt', message: 'You must specify a valid number for a bigint.' }) + * } + * }); + * + * const peeked = await args.repeatResult(resolver); + * await peeked.inspectAsync((value) => message.channel.send(`Sum: **${value.reduce((x, y) => x + y, 0n)}**`)); // Sum: 150n + * + * const first = await args.pick(resolver); + * await message.channel.send(`First bigint squared: ${first**2n}`); // First bigint squared: 625 + * ``` + */ + public async peek(type: () => Argument.Result): Promise; + /** + * Similar to {@link Args.peekResult} but returns the value on success, throwing otherwise. + * @param type The function, custom argument, or argument name. + * @param options The peek options. + * @example + * ```typescript + * import { SnowflakeRegex } from '@sapphire/discord.js-utilities'; + * import { DiscordSnowflake } from '@sapphire/snowflake'; + * + * // !createdat 730159185517477900 + * const snowflakeResolver = Args.make((arg, { argument }) => { + * return SnowflakeRegex.test(arg) + * ? Args.ok(BigInt(arg)) + * : Args.error({ parameter: arg, argument, identifier: 'InvalidSnowflake', message: 'You must specify a valid snowflake.' }); + * }); + * + * const snowflake = await args.peek(snowflakeResolver); + * const timestamp = Number((snowflake >> 22n) + DiscordSnowflake.epoch); + * const createdAt = new Date(timestamp); + * + * await message.channel.send( + * `The snowflake ${snowflake} was registered on ${createdAt.toUTCString()}.` + * ); // The snowflake 730159185517477900 was registered on Tue, 07 Jul 2020 20:31:55 GMT. + * + * const id = await args.pick('string'); + * await message.channel.send(`Your ID, reversed: ${id.split('').reverse().join('')}`); // Your ID, reversed: 009774715581951037 + * ``` + */ + public async peek(type: IArgument, options?: ArgOptions): Promise; + /** + * Similar to {@link Args.peekResult} but returns the value on success, throwing otherwise. + * @param type The function, custom argument, or argument name. + * @param options The peek options. + * @example + * ```typescript + * // !messagelink https://discord.com/channels/737141877803057244/737142209639350343/791843123898089483 + * const remoteMessage = await args.peek('message'); + * await message.channel.send( + * `${remoteMessage.author.tag}: ${remoteMessage.content}` + * ); // RealShadowNova#7462: Yeah, Sapphire has been a great experience so far, especially being able to help and contribute. + * + * const url = await args.pick('hyperlink'); + * await message.channel.send(`Hostname: ${url.hostname}`); // Hostname: discord.com + * ``` + */ + public async peek(type: (() => Argument.Result) | K, options?: ArgOptions): Promise; + public async peek(type: (() => Argument.Result) | K, options?: ArgOptions): Promise { + const result = await this.peekResult(type, options); + return result.unwrapRaw(); + } + + /** + * Saves the current state into the stack following a FILO strategy (first-in, last-out). + * @see Args#restore + */ + public save(): void { + this.states.push(this.parser.save()); + } + + /** + * Restores the previously saved state from the stack. + * @see Args#save + */ + public restore(): void { + if (this.states.length !== 0) this.parser.restore(this.states.pop()!); + } + + /** + * Whether all arguments have been consumed. + */ + public get finished(): boolean { + return this.parser.finished; + } + + /** + * Defines the `JSON.stringify` override. + */ + 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 17447c3a64..f6c20e6ad4 100644 --- a/src/lib/parsers/ChatInputParser.ts +++ b/src/lib/parsers/ChatInputParser.ts @@ -1,9 +1,8 @@ -import { type CommandInteraction } from 'discord.js'; -import type { Arg, Parser } from './Parser'; +import { type CommandInteraction, type CommandInteractionOption } from 'discord.js'; import { Option, Result } from '@sapphire/result'; import { type Parameter } from '@sapphire/lexure'; -export class ChatInputParser implements Parser { +export class ChatInputParser { public position: number = 0; public constructor(public interaction: CommandInteraction) {} @@ -24,22 +23,10 @@ export class ChatInputParser implements Parser { this.position = state; } - public single(): Option { - if (this.finished) return Option.none; - return Option.some(this.interaction.options.data[this.position++]); - } - - public singleMap(predicate: (value: Arg) => Option, useAnyways?: boolean): Option { - if (this.finished) return Option.none; - - const result = predicate(this.interaction.options.data[this.position]); - if (result.isSome() || useAnyways) { - this.position++; - } - return result; - } - - public async singleParseAsync(predicate: (arg: Arg) => Promise>, useAnyways?: boolean): Promise> { + public async singleParseAsync( + predicate: (arg: CommandInteractionOption) => Promise>, + useAnyways?: boolean + ): Promise> { if (this.finished) return Result.err(null); const result = await predicate(this.interaction.options.data[this.position]); @@ -69,19 +56,4 @@ export class ChatInputParser implements Parser { return parameters.length === 0 ? Option.none : Option.some(parameters); } - - public flag(..._names: string[]): boolean { - // TODO: Figure out what to do for slash commands - return false; - } - - public option(..._names: string[]): Option { - // TODO: Figure out what to do for slash commands - return Option.none; - } - - public options(..._names: string[]): Option { - // TODO: Figure out what to do for slash commands - return Option.none; - } } diff --git a/src/lib/parsers/MessageArgs.ts b/src/lib/parsers/MessageArgs.ts new file mode 100644 index 0000000000..547fccc3e8 --- /dev/null +++ b/src/lib/parsers/MessageArgs.ts @@ -0,0 +1,794 @@ +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 { ArgumentError } from '../errors/ArgumentError'; +import { Identifiers } from '../errors/Identifiers'; +import { UserError } from '../errors/UserError'; +import type { EmojiObject } from '../resolvers/emoji'; +import type { Argument, IArgument } from '../structures/Argument'; +import { Command } from '../structures/Command'; +import { Args, type ArgsOptions, type InferArgReturnType, type PeekArgsOptions, type RepeatArgsOptions } from './Args'; + +/** + * The argument parser to be used in {@link Command}. + */ +export class MessageArgs extends Args { + /** + * The original message that triggered the command. + */ + public readonly message: Message; + + /** + * The command that is being run. + */ + public readonly command: Command; + + /** + * The context of the command being run. + */ + public readonly commandContext: Record; + + /** + * The internal Lexure parser. + */ + protected readonly parser: ArgumentStream; + + /** + * The states stored in the args. + * @see Args#save + * @see Args#restore + */ + private readonly states: ArgumentStream.State[] = []; + + public constructor(message: Message, command: Command, parser: ArgumentStream, context: Record) { + super(); + this.message = message; + this.command = command; + this.parser = parser; + this.commandContext = context; + } + + /** + * Sets the parser to the first token. + */ + public start(): this { + this.parser.reset(); + return this; + } + + /** + * Retrieves the next parameter and parses it. Advances index on success. + * @param type The type of the argument. + * @param options The pickResult options. + * @example + * ```typescript + * // !square 5 + * const resolver = Args.make((parameter, { argument }) => { + * const parsed = Number(parameter); + * if (Number.isNaN(parsed)) { + * return Args.error({ argument, parameter, identifier: 'ArgumentNumberNaN', message: 'You must write a valid number.' }); + * } + * + * return Args.ok(parsed); + * }); + * + * const a = await args.pickResult(resolver); + * if (!a.success) { + * throw new UserError({ identifier: 'ArgumentNumberNaN', message: 'You must write a valid number.' }); + * } + * + * await message.channel.send(`The result is: ${a.value ** 2}!`); + * // Sends "The result is: 25" + * ``` + */ + public async pickResult(options: T): Promise>>; + /** + * Retrieves the next parameter and parses it. Advances index on success. + * @param type The type of the argument. + * @param options The pickResult options. + * @example + * ```typescript + * // !add 1 2 + * const a = await args.pickResult('integer'); + * if (!a.success) { + * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write two numbers, but the first one did not match.' }); + * } + * + * const b = await args.pickResult('integer'); + * if (!b.success) { + * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write two numbers, but the second one did not match.' }); + * } + * + * await message.channel.send(`The result is: ${a.value + b.value}!`); + * // Sends "The result is: 3" + * ``` + */ + public async pickResult(options: T): Promise>> { + const argument = this.resolveArgument(options.type); + if (!argument) return this.unavailableArgument(options.type); + + const result = await this.parser.singleParseAsync(async (arg) => + argument.run(arg, { + args: this, + argument, + messageOrInteraction: this.message, + command: this.command, + commandContext: this.commandContext, + ...options + }) + ); + if (result.isErrAnd((value) => value === null)) { + return this.missingArguments(); + } + + return result as ResultType>; + } + + /** + * Similar to {@link Args.pickResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The pick options. + * @example + * ```typescript + * // !square 5 + * const resolver = Args.make((parameter, { argument }) => { + * const parsed = Number(parameter); + * if (Number.isNaN(parsed)) { + * return Args.error({ argument, parameter, identifier: 'ArgumentNumberNaN', message: 'You must write a valid number.' }); + * } + * + * return Args.ok(parsed); + * }); + * + * const a = await args.pick(resolver); + * + * await message.channel.send(`The result is: ${a ** 2}!`); + * // Sends "The result is: 25" + * ``` + */ + public async pick(options: T): Promise>; + /** + * Similar to {@link Args.pickResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The pick options. + * @example + * ```typescript + * // !add 1 2 + * const a = await args.pick('integer'); + * const b = await args.pick('integer'); + * await message.channel.send(`The result is: ${a + b}!`); + * // Sends "The result is: 3" + * ``` + */ + public async pick(options: T): Promise> { + const result = await this.pickResult(options); + return result.unwrapRaw(); + } + + /** + * Retrieves all the following arguments. + * @param type The type of the argument. + * @param options The restResult options. + * @example + * ```typescript + * // !reverse Hello world! + * const resolver = Args.make((parameter) => Args.ok(parameter.split('').reverse())); + * + * const a = await args.restResult(resolver); + * if (!a.success) { + * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write some text.' }); + * } + * + * await message.channel.send(`The reversed value is... ${a.value}`); + * // Sends "The reversed value is... !dlrow olleH" + * ``` + */ + public async restResult(options: T): Promise>>; + /** + * Retrieves all the following arguments. + * @param type The type of the argument. + * @param options The restResult options. + * @example + * ```typescript + * // !add 2 Hello World! + * const a = await args.pickResult('integer'); + * if (!a.success) { + * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write a number and a text, but the former did not match.' }); + * } + * + * const b = await args.restResult('string', { minimum: 1 }); + * if (!b.success) { + * throw new UserError({ identifier: 'AddArgumentError', message: 'You must write a number and a text, but the latter did not match.' }); + * } + * + * await message.channel.send(`The repeated value is... ${b.value.repeat(a.value)}!`); + * // Sends "The repeated value is... Hello World!Hello World!" + * ``` + */ + public async restResult(options: T): Promise>> { + const argument = this.resolveArgument(options.type); + if (!argument) return this.unavailableArgument(options.type); + if (this.parser.finished) return this.missingArguments(); + + const state = this.parser.save(); + const data = join(this.parser.many().unwrapOr([])); + const result = await argument.run(data, { + args: this, + argument, + messageOrInteraction: this.message, + command: this.command, + commandContext: this.commandContext, + ...options + }); + + return result.inspectErr(() => this.parser.restore(state)) as ResultType>; + } + + /** + * Similar to {@link Args.restResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The rest options. + * @example + * ```typescript + * // !reverse Hello world! + * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse())); + * const a = await args.rest(resolver); + * await message.channel.send(`The reversed value is... ${a}`); + * // Sends "The reversed value is... !dlrow olleH" + * ``` + */ + public async rest(options: T): Promise>; + /** + * Similar to {@link Args.restResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The rest options. + * @example + * ```typescript + * // !add 2 Hello World! + * const a = await args.pick('integer'); + * const b = await args.rest('string', { minimum: 1 }); + * await message.channel.send(`The repeated value is... ${b.repeat(a)}!`); + * // Sends "The repeated value is... Hello World!Hello World!" + * ``` + */ + public async rest(options: T): Promise> { + const result = await this.restResult(options); + return result.unwrapRaw(); + } + + /** + * Retrieves all the following arguments. + * @param type The type of the argument. + * @param options The repeatResult options. + * @example + * ```typescript + * // !add 2 Hello World! + * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse())); + * const result = await args.repeatResult(resolver, { times: 5 }); + * if (!result.success) { + * throw new UserError({ identifier: 'CountArgumentError', message: 'You must write up to 5 words.' }); + * } + * + * await message.channel.send(`You have written ${result.value.length} word(s): ${result.value.join(' ')}`); + * // Sends "You have written 2 word(s): olleH !dlroW" + * ``` + */ + public async repeatResult(options: T): Promise>>; + /** + * Retrieves all the following arguments. + * @param type The type of the argument. + * @param options The repeatResult options. + * @example + * ```typescript + * // !reverse-each 2 Hello World! + * const result = await args.repeatResult('string', { times: 5 }); + * if (!result.success) { + * throw new UserError({ identifier: 'CountArgumentError', message: 'You must write up to 5 words.' }); + * } + * + * await message.channel.send(`You have written ${result.value.length} word(s): ${result.value.join(' ')}`); + * // Sends "You have written 2 word(s): Hello World!" + * ``` + */ + public async repeatResult(options: T): Promise>> { + const argument = this.resolveArgument(options.type); + if (!argument) return this.unavailableArgument(options.type); + if (this.parser.finished) return this.missingArguments(); + + const output: InferArgReturnType[] = []; + + for (let i = 0, times = options.times ?? Infinity; i < times; i++) { + const result = await this.parser.singleParseAsync(async (arg) => + argument.run(arg, { + args: this, + argument, + messageOrInteraction: this.message, + command: this.command, + commandContext: this.commandContext, + ...options + }) + ); + + if (result.isErr()) { + const error = result.unwrapErr(); + if (error === null) break; + + if (output.length === 0) { + return result as Result.Err>>; + } + + break; + } + + output.push(result.unwrap() as InferArgReturnType); + } + + return Result.ok(output); + } + + /** + * Similar to {@link Args.repeatResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The repeat options. + * @example + * ```typescript + * // !reverse-each 2 Hello World! + * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse())); + * const result = await args.repeat(resolver, { times: 5 }); + * await message.channel.send(`You have written ${result.length} word(s): ${result.join(' ')}`); + * // Sends "You have written 2 word(s): Hello World!" + * ``` + */ + public async repeat(options: RepeatArgsOptions): Promise; + /** + * Similar to {@link Args.repeatResult} but returns the value on success, throwing otherwise. + * @param type The type of the argument. + * @param options The repeat options. + * @example + * ```typescript + * // !add 2 Hello World! + * const words = await args.repeat('string', { times: 5 }); + * await message.channel.send(`You have written ${words.length} word(s): ${words.join(' ')}`); + * // Sends "You have written 2 word(s): Hello World!" + * ``` + */ + public async repeat(options: RepeatArgsOptions): Promise[]> { + const result = await this.repeatResult(options); + return result.unwrapRaw(); + } + + /** + * Peeks the following parameter(s) without advancing the parser's state. + * Passing a function as a parameter allows for returning {@link Args.pickResult}, {@link Args.repeatResult}, + * or {@link Args.restResult}; otherwise, passing the custom argument or the argument type with options + * will use {@link Args.pickResult} and only peek a single argument. + * @param type The function, custom argument, or argument name. + * @example + * ```typescript + * // !reversedandscreamfirst hello world + * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse().join(''))); + * + * const result = await args.repeatResult(resolver); + * await result.inspectAsync((value) => + * message.channel.send(`Reversed ${value.length} word(s): ${value.join(' ')}`) + * ); // Reversed 2 word(s): olleh dlrow + * + * const firstWord = await args.pickResult('string'); + * await firstWord.inspectAsync((value) => + * message.channel.send(firstWord.value.toUpperCase()) + * ); // HELLO + * ``` + */ + public async peekResult(options: T): Promise>>; + /** + * Peeks the following parameter(s) without advancing the parser's state. + * Passing a function as a parameter allows for returning {@link Args.pickResult}, {@link Args.repeatResult}, + * or {@link Args.restResult}; otherwise, passing the custom argument or the argument type with options + * will use {@link Args.pickResult} and only peek a single argument. + * @param type The function, custom argument, or argument name. + * @param options The peekResult options. + * @example + * ```typescript + * // !reverseandscreamfirst sapphire community + * const resolver = Args.make((arg) => Args.ok(arg.split('').reverse().join(''))); + * + * const peekedWord = await args.peekResult(resolver); + * await peekedWord.inspectAsync((value) => message.channel.send(value)); // erihppas + * + * const firstWord = await args.pickResult('string'); + * await firstWord.inspectAsync((value) => message.channel.send(value.toUpperCase())); // SAPPHIRE + * ``` + */ + public async peekResult(options: T): Promise>>; + /** + * Peeks the following parameter(s) without advancing the parser's state. + * Passing a function as a parameter allows for returning {@link Args.pickResult}, {@link Args.repeatResult}, + * or {@link Args.restResult}; otherwise, passing the custom argument or the argument type with options + * will use {@link Args.pickResult} and only peek a single argument. + * @param type The function, custom argument, or argument name. + * @param options The peekResult options. + * @example + * ```typescript + * // !datethenaddtwo 1608867472611 + * const date = await args.peekResult('date'); + * await date.inspectAsync((value) => + * message.channel.send(`Your date (in UTC): ${value.toUTCString()}`) + * ); // Your date (in UTC): Fri, 25 Dec 2020 03:37:52 GMT + * + * const result = await args.pickResult('number', { maximum: Number.MAX_SAFE_INTEGER - 2 }); + * await result.inspectAsync((value) => + * message.channel.send(`Your number plus two: ${value + 2}`) + * ); // Your number plus two: 1608867472613 + * ``` + */ + public async peekResult(options: T): Promise>> { + this.save(); + const result = typeof options.type === 'function' ? await options.type() : await this.pickResult(options); + this.restore(); + return result; + } + + /** + * Similar to {@link Args.peekResult} but returns the value on success, throwing otherwise. + * @param type The function, custom argument, or argument name. + * @example + * ```typescript + * // !bigintsumthensquarefirst 25 50 75 + * const resolver = Args.make((arg, { argument }) => { + * try { + * return Args.ok(BigInt(arg)); + * } catch { + * return Args.error({ parameter: arg, argument, identifier: 'InvalidBigInt', message: 'You must specify a valid number for a bigint.' }) + * } + * }); + * + * const peeked = await args.repeatResult(resolver); + * await peeked.inspectAsync((value) => message.channel.send(`Sum: **${value.reduce((x, y) => x + y, 0n)}**`)); // Sum: 150n + * + * const first = await args.pick(resolver); + * await message.channel.send(`First bigint squared: ${first**2n}`); // First bigint squared: 625 + * ``` + */ + public async peek(type: () => Argument.Result): Promise; + /** + * Similar to {@link Args.peekResult} but returns the value on success, throwing otherwise. + * @param type The function, custom argument, or argument name. + * @param options The peek options. + * @example + * ```typescript + * import { SnowflakeRegex } from '@sapphire/discord.js-utilities'; + * import { DiscordSnowflake } from '@sapphire/snowflake'; + * + * // !createdat 730159185517477900 + * const snowflakeResolver = Args.make((arg, { argument }) => { + * return SnowflakeRegex.test(arg) + * ? Args.ok(BigInt(arg)) + * : Args.error({ parameter: arg, argument, identifier: 'InvalidSnowflake', message: 'You must specify a valid snowflake.' }); + * }); + * + * const snowflake = await args.peek(snowflakeResolver); + * const timestamp = Number((snowflake >> 22n) + DiscordSnowflake.epoch); + * const createdAt = new Date(timestamp); + * + * await message.channel.send( + * `The snowflake ${snowflake} was registered on ${createdAt.toUTCString()}.` + * ); // The snowflake 730159185517477900 was registered on Tue, 07 Jul 2020 20:31:55 GMT. + * + * const id = await args.pick('string'); + * await message.channel.send(`Your ID, reversed: ${id.split('').reverse().join('')}`); // Your ID, reversed: 009774715581951037 + * ``` + */ + public async peek(options?: T): Promise>; + /** + * Similar to {@link Args.peekResult} but returns the value on success, throwing otherwise. + * @param type The function, custom argument, or argument name. + * @param options The peek options. + * @example + * ```typescript + * // !messagelink https://discord.com/channels/737141877803057244/737142209639350343/791843123898089483 + * const remoteMessage = await args.peek('message'); + * await message.channel.send( + * `${remoteMessage.author.tag}: ${remoteMessage.content}` + * ); // RealShadowNova#7462: Yeah, Sapphire has been a great experience so far, especially being able to help and contribute. + * + * const url = await args.pick('hyperlink'); + * await message.channel.send(`Hostname: ${url.hostname}`); // Hostname: discord.com + * ``` + */ + public async peek(options: T): Promise>; + public async peek(options: T): Promise> { + const result = await this.peekResult(options); + return result.unwrapRaw(); + } + + /** + * Retrieves the next raw argument from the parser. + * @example + * ```typescript + * // !numbers 1 2 3 + * + * console.log(args.nextMaybe()); + * // -> { exists: true, value: '1' } + * ``` + */ + public nextMaybe(): Option; + /** + * Retrieves the value of the next unused ordered token, but only if it could be transformed. + * That token will now be used if the transformation succeeds. + * @typeparam T Output type of the {@link ArgsNextCallback callback}. + * @param cb Gives an option of either the resulting value, or nothing if failed. + * @example + * ```typescript + * // !numbers 1 2 3 + * const parse = (x: string) => { + * const n = Number(x); + * return Number.isNaN(n) ? none() : some(n); + * }; + * + * console.log(args.nextMaybe(parse)); + * // -> { exists: true, value: 1 } + * ``` + */ + public nextMaybe(cb: ArgsNextCallback): Option; + public nextMaybe(cb?: ArgsNextCallback): Option { + return Option.from(typeof cb === 'function' ? this.parser.singleMap(cb) : this.parser.single()); + } + + /** + * Similar to {@link Args.nextMaybe} but returns the value on success, null otherwise. + * @example + * ```typescript + * // !numbers 1 2 3 + * + * console.log(args.next()); + * // -> '1' + * ``` + */ + public next(): string; + /** + * Similar to {@link Args.nextMaybe} but returns the value on success, null otherwise. + * @typeparam T Output type of the {@link ArgsNextCallback callback}. + * @param cb Gives an option of either the resulting value, or nothing if failed. + * @example + * ```typescript + * // !numbers 1 2 3 + * const parse = (x: string) => { + * const n = Number(x); + * return Number.isNaN(n) ? none() : some(n); + * }; + * + * console.log(args.nextMaybe(parse)); + * // -> 1 + * ``` + */ + public next(cb: ArgsNextCallback): T; + public next(cb?: ArgsNextCallback): T | string | null { + const value = cb ? this.nextMaybe(cb) : this.nextMaybe(); + return value.unwrapOr(null); + } + + /** + * Checks if one or more flag were given. + * @param keys The name(s) of the flag. + * @example + * ```typescript + * // Suppose args are from '--f --g'. + * console.log(args.getFlags('f')); + * // >>> true + * + * console.log(args.getFlags('g', 'h')); + * // >>> true + * + * console.log(args.getFlags('h')); + * // >>> false + * ``` + */ + public getFlags(...keys: readonly string[]): boolean { + return this.parser.flag(...keys); + } + + /** + * Gets the last value of one or more options as an {@link Option}. + * If you do not care about safely handling non-existing values + * you can use {@link Args.getOption} to get `string | null` as return type + * @param keys The name(s) of the option. + * @example + * ```typescript + * // Suppose args are from '--a=1 --b=2 --c=3'. + * console.log(args.getOptionResult('a')); + * // >>> Some { value: '1' } + * + * console.log(args.getOptionResult('b', 'c')); + * // >>> Some { value: '2' } + * + * console.log(args.getOptionResult('d')); + * // >>> None {} + * ``` + */ + public getOptionResult(...keys: readonly string[]): Option { + return this.parser.option(...keys); + } + + /** + * Gets the last value of one or more options. + * Similar to {@link Args.getOptionResult} but returns the value on success, or `null` if not. + * @param keys The name(s) of the option. + * @example + * ```typescript + * // Suppose args are from '--a=1 --b=2 --c=3'. + * console.log(args.getOption('a')); + * // >>> '1' + * + * console.log(args.getOption('b', 'c')); + * // >>> '2' + * + * console.log(args.getOption('d')); + * // >>> null + * ``` + */ + public getOption(...keys: readonly string[]): string | null { + return this.parser.option(...keys).unwrapOr(null); + } + + /** + * Gets all the values of one or more option. + * @param keys The name(s) of the option. + * @example + * ```typescript + * // Suppose args are from '--a=1 --a=1 --b=2 --c=3'. + * console.log(args.getOptionsResult('a')); + * // >>> Some { value: [ '1' ] } + * + * console.log(args.getOptionsResult('a', 'd')); + * // >>> Some { value: [ '1' ] } + * + * console.log(args.getOptionsResult('b', 'c')); + * // >>> Some { value: [ '2', '3' ] } + * + * console.log(args.getOptionsResult('d')); + * // >>> None {} + * ``` + */ + public getOptionsResult(...keys: readonly string[]): Option { + return this.parser.options(...keys); + } + + /** + * Gets all the values of one or more option. + * Similar to {@link Args.getOptionsResult} but returns the value on success, or `null` if not. + * @param keys The name(s) of the option. + * @example + * ```typescript + * // Suppose args are from '--a=1 --a=1 --b=2 --c=3'. + * console.log(args.getOptions('a')); + * // >>> ['1', '1'] + * + * console.log(args.getOptions('b', 'c')); + * // >>> ['2', '3'] + * + * console.log(args.getOptions('d')); + * // >>> null + * ``` + */ + public getOptions(...keys: readonly string[]): readonly string[] | null { + return this.parser.options(...keys).unwrapOr(null); + } + + /** + * Saves the current state into the stack following a FILO strategy (first-in, last-out). + * @see Args#restore + */ + public save(): void { + this.states.push(this.parser.save()); + } + + /** + * Restores the previously saved state from the stack. + * @see Args#save + */ + public restore(): void { + if (this.states.length !== 0) this.parser.restore(this.states.pop()!); + } + + /** + * Whether all arguments have been consumed. + */ + public get finished(): boolean { + return this.parser.finished; + } + + /** + * Defines the `JSON.stringify` override. + */ + 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; +} + +/** + * The callback used for {@link Args.nextMaybe} and {@link Args.next}. + */ +export interface ArgsNextCallback { + /** + * The value to be mapped. + */ + (value: string): Option; +} + +export type ResultType = Result>; +export type ArrayResultType = Result>; diff --git a/src/lib/parsers/Parser.ts b/src/lib/parsers/Parser.ts deleted file mode 100644 index 94e98621a9..0000000000 --- a/src/lib/parsers/Parser.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Parameter } from '@sapphire/lexure'; -import type { Option, Result } from '@sapphire/result'; -import type { CommandInteractionOption } from 'discord.js'; - -export type Arg = string | CommandInteractionOption; - -export interface Parser { - reset(): void; - - save(): unknown; - - restore(state: unknown): void; - - single(): Option; - - singleMap(predicate: (value: Arg) => Option, useAnyways?: boolean): Option; - - singleParseAsync(predicate: (arg: Arg) => Promise>, useAnyways?: boolean): Promise>; - - many(): Option; - - flag(...names: string[]): boolean; - - option(...names: string[]): Option; - - options(...names: string[]): Option; - - finished: boolean; -} diff --git a/src/lib/structures/Command.ts b/src/lib/structures/Command.ts index 95aa203bd8..b92ab30707 100644 --- a/src/lib/structures/Command.ts +++ b/src/lib/structures/Command.ts @@ -30,6 +30,8 @@ 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 { ChatInputCommandArgs } from '../parsers/ChatInputCommandArgs'; const ChannelTypes = Object.values(ChannelType).filter((type) => typeof type === 'number') as readonly ChannelType[]; const GuildChannelTypes = ChannelTypes.filter((type) => type !== ChannelType.DM && type !== ChannelType.GroupDM) as readonly ChannelType[]; @@ -143,12 +145,12 @@ export class Command { const parser = new Parser(this.strategy); const args = new ArgumentStream(parser.run(this.lexer.run(parameters))); - return new Args(message, this as Command, args, context) as PreParseReturn; + return new MessageArgs(message, this as Command, args, context) as PreParseReturn; } public chatInputPreParse(interaction: ChatInputCommandInteraction, context: ChatInputCommand.RunContext): Awaitable { - const args = new ChatInputParser(interaction); - return new Args(interaction, this as Command, args, context) as PreParseReturn; + const parser = new ChatInputParser(interaction); + return new ChatInputCommandArgs(interaction, this as Command, parser, context) as PreParseReturn; } /**