diff --git a/src/arguments/CoreChannel.ts b/src/arguments/CoreChannel.ts index 5ba6c836a0..2a6b4c9279 100644 --- a/src/arguments/CoreChannel.ts +++ b/src/arguments/CoreChannel.ts @@ -9,7 +9,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const resolved = resolveChannel(parameter, context.message); + const resolved = resolveChannel(parameter, context.messageOrInteraction); return resolved.mapErrInto((identifier) => this.error({ parameter, diff --git a/src/arguments/CoreDMChannel.ts b/src/arguments/CoreDMChannel.ts index acb28af09f..9b0d784c20 100644 --- a/src/arguments/CoreDMChannel.ts +++ b/src/arguments/CoreDMChannel.ts @@ -9,7 +9,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const resolved = resolveDMChannel(parameter, context.message); + const resolved = resolveDMChannel(parameter, context.messageOrInteraction); return resolved.mapErrInto((identifier) => this.error({ parameter, diff --git a/src/arguments/CoreGuildCategoryChannel.ts b/src/arguments/CoreGuildCategoryChannel.ts index ac9766cd00..18245dfbac 100644 --- a/src/arguments/CoreGuildCategoryChannel.ts +++ b/src/arguments/CoreGuildCategoryChannel.ts @@ -10,7 +10,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildChannel.ts b/src/arguments/CoreGuildChannel.ts index 67e9c8f4f3..420eb55a41 100644 --- a/src/arguments/CoreGuildChannel.ts +++ b/src/arguments/CoreGuildChannel.ts @@ -10,7 +10,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildNewsChannel.ts b/src/arguments/CoreGuildNewsChannel.ts index e852ea014c..428046050f 100644 --- a/src/arguments/CoreGuildNewsChannel.ts +++ b/src/arguments/CoreGuildNewsChannel.ts @@ -10,7 +10,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildNewsThreadChannel.ts b/src/arguments/CoreGuildNewsThreadChannel.ts index bd9337ded9..469ec89802 100644 --- a/src/arguments/CoreGuildNewsThreadChannel.ts +++ b/src/arguments/CoreGuildNewsThreadChannel.ts @@ -10,7 +10,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildPrivateThreadChannel.ts b/src/arguments/CoreGuildPrivateThreadChannel.ts index 3e467741be..8e994868c3 100644 --- a/src/arguments/CoreGuildPrivateThreadChannel.ts +++ b/src/arguments/CoreGuildPrivateThreadChannel.ts @@ -10,7 +10,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildPublicThreadChannel.ts b/src/arguments/CoreGuildPublicThreadChannel.ts index e008987833..524674ac40 100644 --- a/src/arguments/CoreGuildPublicThreadChannel.ts +++ b/src/arguments/CoreGuildPublicThreadChannel.ts @@ -10,7 +10,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildStageVoiceChannel.ts b/src/arguments/CoreGuildStageVoiceChannel.ts index 1beb6aa77d..a1fccaf6ce 100644 --- a/src/arguments/CoreGuildStageVoiceChannel.ts +++ b/src/arguments/CoreGuildStageVoiceChannel.ts @@ -10,7 +10,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildTextChannel.ts b/src/arguments/CoreGuildTextChannel.ts index f1c9d7ff84..79ad0b105c 100644 --- a/src/arguments/CoreGuildTextChannel.ts +++ b/src/arguments/CoreGuildTextChannel.ts @@ -10,7 +10,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildThreadChannel.ts b/src/arguments/CoreGuildThreadChannel.ts index 639f197b2a..2c8cf5b047 100644 --- a/src/arguments/CoreGuildThreadChannel.ts +++ b/src/arguments/CoreGuildThreadChannel.ts @@ -10,7 +10,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildVoiceChannel.ts b/src/arguments/CoreGuildVoiceChannel.ts index fdbbd3323b..d85dc84da5 100644 --- a/src/arguments/CoreGuildVoiceChannel.ts +++ b/src/arguments/CoreGuildVoiceChannel.ts @@ -10,7 +10,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreMember.ts b/src/arguments/CoreMember.ts index af78c9c905..2770ec6ec1 100644 --- a/src/arguments/CoreMember.ts +++ b/src/arguments/CoreMember.ts @@ -11,7 +11,7 @@ export class CoreArgument extends Argument { } public async run(parameter: string, context: MemberArgumentContext): Argument.AsyncResult { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ diff --git a/src/arguments/CoreMessage.ts b/src/arguments/CoreMessage.ts index a6e62fbb51..f13dc32566 100644 --- a/src/arguments/CoreMessage.ts +++ b/src/arguments/CoreMessage.ts @@ -10,9 +10,9 @@ export class CoreArgument extends Argument { } public async run(parameter: string, context: MessageArgumentContext): Argument.AsyncResult { - const channel = context.channel ?? context.message.channel; + const channel = context.channel ?? context.messageOrInteraction.channel; const resolved = await resolveMessage(parameter, { - messageOrInteraction: context.message, + messageOrInteraction: context.messageOrInteraction, channel: context.channel, scan: context.scan ?? false }); diff --git a/src/arguments/CorePartialDMChannel.ts b/src/arguments/CorePartialDMChannel.ts index b9c135cf0f..31c0bda33e 100644 --- a/src/arguments/CorePartialDMChannel.ts +++ b/src/arguments/CorePartialDMChannel.ts @@ -9,7 +9,7 @@ export class CoreArgument extends Argument { } public run(parameter: string, context: Argument.Context): Argument.Result { - const resolved = resolvePartialDMChannel(parameter, context.message); + const resolved = resolvePartialDMChannel(parameter, context.messageOrInteraction); return resolved.mapErrInto((identifier) => this.error({ parameter, diff --git a/src/arguments/CoreRole.ts b/src/arguments/CoreRole.ts index 90756b0f42..e32d6c51f8 100644 --- a/src/arguments/CoreRole.ts +++ b/src/arguments/CoreRole.ts @@ -10,7 +10,7 @@ export class CoreArgument extends Argument { } public async run(parameter: string, context: Argument.Context): Argument.AsyncResult { - const { guild } = context.message; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/lib/parsers/Args.ts b/src/lib/parsers/Args.ts index 4eac58ee22..91ef4dc13e 100644 --- a/src/lib/parsers/Args.ts +++ b/src/lib/parsers/Args.ts @@ -1,11 +1,12 @@ -import type { ChannelTypes, GuildBasedChannelTypes } from '@sapphire/discord.js-utilities'; -import { join, type ArgumentStream, type Parameter } from '@sapphire/lexure'; +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, DMChannel, GuildMember, Message, @@ -23,41 +24,47 @@ import { Identifiers } from '../errors/Identifiers'; import { UserError } from '../errors/UserError'; import type { EmojiObject } from '../resolvers/emoji'; import type { Argument, IArgument } from '../structures/Argument'; -import type { MessageCommand } from '../types/CommandTypes'; +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 that triggered the command. + * The original message or interaction that triggered the command. */ - public readonly message: Message; + public readonly messageOrInteraction: Message | ChatInputCommandInteraction; /** * The command that is being run. */ - public readonly command: MessageCommand; + public readonly command: Command; /** * The context of the command being run. */ - public readonly commandContext: MessageCommand.RunContext; + public readonly commandContext: Record; /** * The internal Lexure parser. */ - protected readonly parser: ArgumentStream; + protected readonly parser: Parser; /** * The states stored in the args. * @see Args#save * @see Args#restore */ - private readonly states: ArgumentStream.State[] = []; + private readonly states: unknown[] = []; - public constructor(message: Message, command: MessageCommand, parser: ArgumentStream, context: MessageCommand.RunContext) { - this.message = message; + public constructor( + messageOrInteraction: Message | ChatInputCommandInteraction, + command: Command, + parser: Parser, + context: Record + ) { + this.messageOrInteraction = messageOrInteraction; this.command = command; this.parser = parser; this.commandContext = context; @@ -127,7 +134,7 @@ export class Args { argument.run(arg, { args: this, argument, - message: this.message, + messageOrInteraction: this.messageOrInteraction, command: this.command, commandContext: this.commandContext, ...options @@ -233,7 +240,7 @@ export class Args { const result = await argument.run(data, { args: this, argument, - message: this.message, + messageOrInteraction: this.messageOrInteraction, command: this.command, commandContext: this.commandContext, ...options @@ -322,7 +329,7 @@ export class Args { argument.run(arg, { args: this, argument, - message: this.message, + messageOrInteraction: this.messageOrInteraction, command: this.command, commandContext: this.commandContext, ...options @@ -540,7 +547,7 @@ export class Args { * // -> { exists: true, value: '1' } * ``` */ - public nextMaybe(): Option; + 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. @@ -559,8 +566,8 @@ export class Args { * ``` */ public nextMaybe(cb: ArgsNextCallback): Option; - public nextMaybe(cb?: ArgsNextCallback): Option { - return Option.from(typeof cb === 'function' ? this.parser.singleMap(cb) : this.parser.single()); + public nextMaybe(cb?: ArgsNextCallback): Option { + return Option.from(typeof cb === 'function' ? this.parser.singleMap(cb) : this.parser.single()); } /** @@ -591,8 +598,8 @@ export class Args { * ``` */ public next(cb: ArgsNextCallback): T; - public next(cb?: ArgsNextCallback): T | string | null { - const value = cb ? this.nextMaybe(cb) : this.nextMaybe(); + public next(cb?: ArgsNextCallback): T | Arg | null { + const value = cb ? this.nextMaybe(cb) : this.nextMaybe(); return value.unwrapOr(null); } @@ -730,7 +737,7 @@ export class Args { * Defines the `JSON.stringify` override. */ public toJSON(): ArgsJson { - return { message: this.message, command: this.command, commandContext: this.commandContext }; + return { message: this.messageOrInteraction, command: this.command, commandContext: this.commandContext }; } protected unavailableArgument(type: string | IArgument): Result.Err { @@ -784,9 +791,9 @@ export class Args { } export interface ArgsJson { - message: Message; - command: MessageCommand; - commandContext: MessageCommand.RunContext; + message: Message | AnyInteraction; + command: Command; + commandContext: Record; } export interface ArgType { @@ -835,7 +842,7 @@ export interface ArgsNextCallback { /** * The value to be mapped. */ - (value: string): Option; + (value: Arg): Option; } export type ResultType = Result>; diff --git a/src/lib/parsers/ChatInputParser.ts b/src/lib/parsers/ChatInputParser.ts new file mode 100644 index 0000000000..17447c3a64 --- /dev/null +++ b/src/lib/parsers/ChatInputParser.ts @@ -0,0 +1,87 @@ +import { type CommandInteraction } from 'discord.js'; +import type { Arg, Parser } from './Parser'; +import { Option, Result } from '@sapphire/result'; +import { type Parameter } from '@sapphire/lexure'; + +export class ChatInputParser implements Parser { + public position: number = 0; + + public constructor(public interaction: CommandInteraction) {} + + public get finished(): boolean { + return this.position === this.interaction.options.data.length; + } + + public reset(): void { + this.position = 0; + } + + public save(): number { + return this.position; + } + + public restore(state: number): void { + 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> { + if (this.finished) return Result.err(null); + + const result = await predicate(this.interaction.options.data[this.position]); + if (result.isOk() || useAnyways) { + this.position++; + } + return result; + } + + // TODO: This method doesn't really make sense for slash commands. Currently tries to convert CommandInteractionOptions back to strings. Any suggestions? + public many(): Option { + const parameters: Parameter[] = []; + for (const option of this.interaction.options.data) { + const keys = ['value', 'user', 'member', 'channel', 'role', 'attachment', 'message'] as const; + let value = ''; + for (const key of keys) { + const optionValue = option[key]; + if (optionValue) { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + value = optionValue.toString(); + break; + } + } + + parameters.push({ value, raw: value, separators: [], leading: '' }); + } + + 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/Parser.ts b/src/lib/parsers/Parser.ts new file mode 100644 index 0000000000..94e98621a9 --- /dev/null +++ b/src/lib/parsers/Parser.ts @@ -0,0 +1,29 @@ +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/resolvers/channel.ts b/src/lib/resolvers/channel.ts index c2ded60233..8af9f260fc 100644 --- a/src/lib/resolvers/channel.ts +++ b/src/lib/resolvers/channel.ts @@ -1,12 +1,12 @@ -import { ChannelMentionRegex, type ChannelTypes } from '@sapphire/discord.js-utilities'; +import { ChannelMentionRegex, type AnyInteraction, type ChannelTypes } from '@sapphire/discord.js-utilities'; import { container } from '@sapphire/pieces'; import { Result } from '@sapphire/result'; -import type { CommandInteraction, Message, Snowflake } from 'discord.js'; +import type { Message, Snowflake } from 'discord.js'; import { Identifiers } from '../errors/Identifiers'; export function resolveChannel( parameter: string, - messageOrInteraction: Message | CommandInteraction + messageOrInteraction: Message | AnyInteraction ): Result { const channelId = (ChannelMentionRegex.exec(parameter)?.[1] ?? parameter) as Snowflake; const channel = (messageOrInteraction.guild ? messageOrInteraction.guild.channels : container.client.channels).cache.get(channelId); diff --git a/src/lib/resolvers/dmChannel.ts b/src/lib/resolvers/dmChannel.ts index 0a372bd366..cc937bb1dc 100644 --- a/src/lib/resolvers/dmChannel.ts +++ b/src/lib/resolvers/dmChannel.ts @@ -1,12 +1,12 @@ -import { isDMChannel } from '@sapphire/discord.js-utilities'; +import { isDMChannel, type AnyInteraction } from '@sapphire/discord.js-utilities'; import { Result } from '@sapphire/result'; -import type { CommandInteraction, DMChannel, Message } from 'discord.js'; +import type { DMChannel, Message } from 'discord.js'; import { Identifiers } from '../errors/Identifiers'; import { resolveChannel } from './channel'; export function resolveDMChannel( parameter: string, - messageOrInteraction: Message | CommandInteraction + messageOrInteraction: Message | AnyInteraction ): Result { const result = resolveChannel(parameter, messageOrInteraction); return result.mapInto((value) => { diff --git a/src/lib/resolvers/partialDMChannel.ts b/src/lib/resolvers/partialDMChannel.ts index 8283a40daf..b8e97521c4 100644 --- a/src/lib/resolvers/partialDMChannel.ts +++ b/src/lib/resolvers/partialDMChannel.ts @@ -1,4 +1,4 @@ -import { isDMChannel } from '@sapphire/discord.js-utilities'; +import { isDMChannel, type AnyInteraction } from '@sapphire/discord.js-utilities'; import { Result } from '@sapphire/result'; import type { DMChannel, Message, PartialDMChannel } from 'discord.js'; import { Identifiers } from '../errors/Identifiers'; @@ -6,7 +6,7 @@ import { resolveChannel } from './channel'; export function resolvePartialDMChannel( parameter: string, - message: Message + message: Message | AnyInteraction ): Result { const result = resolveChannel(parameter, message); return result.mapInto((channel) => { diff --git a/src/lib/structures/Argument.ts b/src/lib/structures/Argument.ts index e8db6e1062..9c53a48e3b 100644 --- a/src/lib/structures/Argument.ts +++ b/src/lib/structures/Argument.ts @@ -1,10 +1,11 @@ import { AliasPiece } from '@sapphire/pieces'; import type { Result } from '@sapphire/result'; import type { Awaitable } from '@sapphire/utilities'; -import type { Message } from 'discord.js'; +import type { CommandInteractionOption, Message } from 'discord.js'; import type { ArgumentError } from '../errors/ArgumentError'; import { Args } from '../parsers/Args'; -import type { MessageCommand } from '../types/CommandTypes'; +import { Command } from './Command'; +import type { AnyInteraction } from '@sapphire/discord.js-utilities'; /** * Defines a synchronous result of an {@link Argument}, check {@link Argument.AsyncResult} for the asynchronous version. @@ -32,7 +33,7 @@ export interface IArgument { * @param parameter The string parameter to parse. * @param context The context for the method call, contains the message, command, and other options. */ - run(parameter: string, context: Argument.Context): Argument.AwaitableResult; + run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.AwaitableResult; } /** @@ -133,9 +134,9 @@ export interface ArgumentOptions extends AliasPiece.Options {} export interface ArgumentContext extends Record { argument: IArgument; args: Args; - message: Message; - command: MessageCommand; - commandContext: MessageCommand.RunContext; + messageOrInteraction: Message | AnyInteraction; + command: Command; + commandContext: Record; minimum?: number; maximum?: number; inclusive?: boolean; diff --git a/src/lib/structures/Command.ts b/src/lib/structures/Command.ts index 235604a127..95aa203bd8 100644 --- a/src/lib/structures/Command.ts +++ b/src/lib/structures/Command.ts @@ -29,6 +29,7 @@ import { getNeededRegistryParameters } from '../utils/application-commands/getNe import { emitPerRegistryError } from '../utils/application-commands/registriesErrors'; import { PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray'; import { FlagUnorderedStrategy } from '../utils/strategies/FlagUnorderedStrategy'; +import { ChatInputParser } from '../parsers/ChatInputParser'; 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[]; @@ -142,7 +143,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 MessageCommand, args, context) as PreParseReturn; + return new Args(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; } /** @@ -194,7 +200,7 @@ export class Command; + public chatInputRun?(interaction: ChatInputCommandInteraction, args: PreParseReturn, context: ChatInputCommand.RunContext): Awaitable; /** * Executes the context menu's logic. diff --git a/src/listeners/application-commands/chat-input/CoreChatInputCommandAccepted.ts b/src/listeners/application-commands/chat-input/CoreChatInputCommandAccepted.ts index 24f53e31f3..40c7d158ff 100644 --- a/src/listeners/application-commands/chat-input/CoreChatInputCommandAccepted.ts +++ b/src/listeners/application-commands/chat-input/CoreChatInputCommandAccepted.ts @@ -11,12 +11,13 @@ export class CoreListener extends Listener { this.container.client.emit(Events.ChatInputCommandRun, interaction, command, { ...payload }); const stopwatch = new Stopwatch(); - const result = await command.chatInputRun(interaction, context); + const result = await command.chatInputRun(interaction, args, context); const { duration } = stopwatch.stop(); this.container.client.emit(Events.ChatInputCommandSuccess, { ...payload, result, duration });