diff --git a/src/arguments/CoreBoolean.ts b/src/arguments/CoreBoolean.ts index d9d782795..fd7da69f5 100644 --- a/src/arguments/CoreBoolean.ts +++ b/src/arguments/CoreBoolean.ts @@ -2,13 +2,15 @@ import { container } from '@sapphire/pieces'; import { resolveBoolean } from '../lib/resolvers/boolean'; import { Argument } from '../lib/structures/Argument'; import type { BooleanArgumentContext } from '../lib/types/ArgumentContexts'; +import { ApplicationCommandOptionType, type CommandInteractionOption } from 'discord.js'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'boolean' }); + super(context, { name: 'boolean', optionType: ApplicationCommandOptionType.Boolean }); } - public run(parameter: string, context: BooleanArgumentContext): Argument.Result { + public run(parameter: string | CommandInteractionOption, context: BooleanArgumentContext): Argument.Result { + if (typeof parameter !== 'string') return this.ok(parameter.value as boolean); const resolved = resolveBoolean(parameter, { truths: context.truths, falses: context.falses }); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/arguments/CoreChannel.ts b/src/arguments/CoreChannel.ts index 5ba6c836a..f7ca328b3 100644 --- a/src/arguments/CoreChannel.ts +++ b/src/arguments/CoreChannel.ts @@ -2,14 +2,16 @@ import type { ChannelTypes } from '@sapphire/discord.js-utilities'; import { container } from '@sapphire/pieces'; import { resolveChannel } from '../lib/resolvers/channel'; import { Argument } from '../lib/structures/Argument'; +import { ApplicationCommandOptionType, type CommandInteractionOption } from 'discord.js'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'channel' }); + super(context, { name: 'channel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const resolved = resolveChannel(parameter, context.message); + public override run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + 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 acb28af09..fc5f0f081 100644 --- a/src/arguments/CoreDMChannel.ts +++ b/src/arguments/CoreDMChannel.ts @@ -1,15 +1,16 @@ import { container } from '@sapphire/pieces'; -import type { DMChannel } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type DMChannel } from 'discord.js'; import { resolveDMChannel } from '../lib/resolvers/dmChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'dmChannel' }); + super(context, { name: 'dmChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const resolved = resolveDMChannel(parameter, context.message); + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + const resolved = resolveDMChannel(parameter, context.messageOrInteraction); return resolved.mapErrInto((identifier) => this.error({ parameter, diff --git a/src/arguments/CoreDate.ts b/src/arguments/CoreDate.ts index 73a570646..8cf2ca860 100644 --- a/src/arguments/CoreDate.ts +++ b/src/arguments/CoreDate.ts @@ -1,4 +1,5 @@ import { container } from '@sapphire/pieces'; +import { ApplicationCommandOptionType, type CommandInteractionOption } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveDate } from '../lib/resolvers/date'; import { Argument } from '../lib/structures/Argument'; @@ -11,10 +12,11 @@ export class CoreArgument extends Argument { } as const; public constructor(context: Argument.LoaderContext) { - super(context, { name: 'date' }); + super(context, { name: 'date', optionType: ApplicationCommandOptionType.String }); } - public run(parameter: string, context: Argument.Context): Argument.Result { + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.value as string; const resolved = resolveDate(parameter, { minimum: context.minimum, maximum: context.maximum }); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/arguments/CoreEmoji.ts b/src/arguments/CoreEmoji.ts index feac0c863..8b04e6e21 100644 --- a/src/arguments/CoreEmoji.ts +++ b/src/arguments/CoreEmoji.ts @@ -1,13 +1,15 @@ import { container } from '@sapphire/pieces'; +import { ApplicationCommandOptionType, type CommandInteractionOption } from 'discord.js'; import { resolveEmoji, type EmojiObject } from '../lib/resolvers/emoji'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'emoji' }); + super(context, { name: 'emoji', optionType: ApplicationCommandOptionType.String }); } - public run(parameter: string, context: Argument.Context): Argument.Result { + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.value as string; const resolved = resolveEmoji(parameter); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/arguments/CoreEnum.ts b/src/arguments/CoreEnum.ts index ec6ac9b00..aaacfe232 100644 --- a/src/arguments/CoreEnum.ts +++ b/src/arguments/CoreEnum.ts @@ -1,14 +1,16 @@ import { container } from '@sapphire/pieces'; +import { ApplicationCommandOptionType, type CommandInteractionOption } from 'discord.js'; import { resolveEnum } from '../lib/resolvers/enum'; import { Argument } from '../lib/structures/Argument'; import type { EnumArgumentContext } from '../lib/types/ArgumentContexts'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'enum' }); + super(context, { name: 'enum', optionType: ApplicationCommandOptionType.String }); } - public run(parameter: string, context: EnumArgumentContext): Argument.Result { + public run(parameter: string | CommandInteractionOption, context: EnumArgumentContext): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.value as string; const resolved = resolveEnum(parameter, { enum: context.enum, caseInsensitive: context.caseInsensitive }); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/arguments/CoreFloat.ts b/src/arguments/CoreFloat.ts index 9c793fe25..0e86342b7 100644 --- a/src/arguments/CoreFloat.ts +++ b/src/arguments/CoreFloat.ts @@ -1,4 +1,5 @@ import { container } from '@sapphire/pieces'; +import { ApplicationCommandOptionType, type CommandInteractionOption } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveFloat } from '../lib/resolvers/float'; import { Argument } from '../lib/structures/Argument'; @@ -11,10 +12,11 @@ export class CoreArgument extends Argument { } as const; public constructor(context: Argument.LoaderContext) { - super(context, { name: 'float' }); + super(context, { name: 'float', optionType: ApplicationCommandOptionType.Number }); } - public run(parameter: string, context: Argument.Context): Argument.Result { + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') return this.ok(parameter.value as number); const resolved = resolveFloat(parameter, { minimum: context.minimum, maximum: context.maximum }); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/arguments/CoreGuild.ts b/src/arguments/CoreGuild.ts index 05dd40d35..32ea30e47 100644 --- a/src/arguments/CoreGuild.ts +++ b/src/arguments/CoreGuild.ts @@ -1,14 +1,15 @@ import { container } from '@sapphire/pieces'; -import type { Guild } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type Guild } from 'discord.js'; import { resolveGuild } from '../lib/resolvers/guild'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'guild' }); + super(context, { name: 'guild', optionType: ApplicationCommandOptionType.String }); } - public async run(parameter: string, context: Argument.Context): Argument.AsyncResult { + public async run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.AsyncResult { + if (typeof parameter !== 'string') parameter = parameter.value as string; const resolved = await resolveGuild(parameter); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/arguments/CoreGuildCategoryChannel.ts b/src/arguments/CoreGuildCategoryChannel.ts index ac9766cd0..375ca9487 100644 --- a/src/arguments/CoreGuildCategoryChannel.ts +++ b/src/arguments/CoreGuildCategoryChannel.ts @@ -1,16 +1,17 @@ import { container } from '@sapphire/pieces'; -import type { CategoryChannel } from 'discord.js'; +import { ApplicationCommandOptionType, type CategoryChannel, type CommandInteractionOption } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveGuildCategoryChannel } from '../lib/resolvers/guildCategoryChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'guildCategoryChannel' }); + super(context, { name: 'guildCategoryChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildChannel.ts b/src/arguments/CoreGuildChannel.ts index 67e9c8f4f..9a3a1484f 100644 --- a/src/arguments/CoreGuildChannel.ts +++ b/src/arguments/CoreGuildChannel.ts @@ -1,16 +1,18 @@ import type { GuildBasedChannelTypes } from '@sapphire/discord.js-utilities'; import { container } from '@sapphire/pieces'; +import { ApplicationCommandOptionType, type CommandInteractionOption } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveGuildChannel } from '../lib/resolvers/guildChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'guildChannel' }); + super(context, { name: 'guildChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildNewsChannel.ts b/src/arguments/CoreGuildNewsChannel.ts index e852ea014..73cd07e1d 100644 --- a/src/arguments/CoreGuildNewsChannel.ts +++ b/src/arguments/CoreGuildNewsChannel.ts @@ -1,16 +1,17 @@ import { container } from '@sapphire/pieces'; -import type { NewsChannel } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type NewsChannel } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveGuildNewsChannel } from '../lib/resolvers/guildNewsChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'guildNewsChannel' }); + super(context, { name: 'guildNewsChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildNewsThreadChannel.ts b/src/arguments/CoreGuildNewsThreadChannel.ts index bd9337ded..1fd8e181e 100644 --- a/src/arguments/CoreGuildNewsThreadChannel.ts +++ b/src/arguments/CoreGuildNewsThreadChannel.ts @@ -1,16 +1,17 @@ import { container } from '@sapphire/pieces'; -import type { ThreadChannel } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type ThreadChannel } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveGuildNewsThreadChannel } from '../lib/resolvers/guildNewsThreadChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'guildNewsThreadChannel' }); + super(context, { name: 'guildNewsThreadChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildPrivateThreadChannel.ts b/src/arguments/CoreGuildPrivateThreadChannel.ts index 3e467741b..beed11f3d 100644 --- a/src/arguments/CoreGuildPrivateThreadChannel.ts +++ b/src/arguments/CoreGuildPrivateThreadChannel.ts @@ -1,16 +1,17 @@ import { container } from '@sapphire/pieces'; -import type { ThreadChannel } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type ThreadChannel } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveGuildPrivateThreadChannel } from '../lib/resolvers/guildPrivateThreadChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'guildPrivateThreadChannel' }); + super(context, { name: 'guildPrivateThreadChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildPublicThreadChannel.ts b/src/arguments/CoreGuildPublicThreadChannel.ts index e00898783..422a35c55 100644 --- a/src/arguments/CoreGuildPublicThreadChannel.ts +++ b/src/arguments/CoreGuildPublicThreadChannel.ts @@ -1,16 +1,17 @@ import { container } from '@sapphire/pieces'; -import type { ThreadChannel } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type ThreadChannel } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveGuildPublicThreadChannel } from '../lib/resolvers/guildPublicThreadChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'guildPublicThreadChannel' }); + super(context, { name: 'guildPublicThreadChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildStageVoiceChannel.ts b/src/arguments/CoreGuildStageVoiceChannel.ts index 1beb6aa77..dd302a92c 100644 --- a/src/arguments/CoreGuildStageVoiceChannel.ts +++ b/src/arguments/CoreGuildStageVoiceChannel.ts @@ -1,16 +1,17 @@ import { container } from '@sapphire/pieces'; -import type { StageChannel } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type StageChannel } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveGuildStageVoiceChannel } from '../lib/resolvers/guildStageVoiceChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'guildStageVoiceChannel' }); + super(context, { name: 'guildStageVoiceChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildTextChannel.ts b/src/arguments/CoreGuildTextChannel.ts index f1c9d7ff8..30c3a9904 100644 --- a/src/arguments/CoreGuildTextChannel.ts +++ b/src/arguments/CoreGuildTextChannel.ts @@ -1,16 +1,17 @@ import { container } from '@sapphire/pieces'; -import type { TextChannel } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type TextChannel } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveGuildTextChannel } from '../lib/resolvers/guildTextChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'guildTextChannel' }); + super(context, { name: 'guildTextChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreGuildThreadChannel.ts b/src/arguments/CoreGuildThreadChannel.ts index 639f197b2..3a2267ae9 100644 --- a/src/arguments/CoreGuildThreadChannel.ts +++ b/src/arguments/CoreGuildThreadChannel.ts @@ -1,16 +1,16 @@ import { container } from '@sapphire/pieces'; -import type { ThreadChannel } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type ThreadChannel } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveGuildThreadChannel } from '../lib/resolvers/guildThreadChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'guildThreadChannel' }); + super(context, { name: 'guildThreadChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, @@ -20,6 +20,7 @@ export class CoreArgument extends Argument { }); } + if (typeof parameter !== 'string') parameter = parameter.channel!.id; const resolved = resolveGuildThreadChannel(parameter, guild); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/arguments/CoreGuildVoiceChannel.ts b/src/arguments/CoreGuildVoiceChannel.ts index fdbbd3323..305604b8a 100644 --- a/src/arguments/CoreGuildVoiceChannel.ts +++ b/src/arguments/CoreGuildVoiceChannel.ts @@ -1,16 +1,17 @@ import { container } from '@sapphire/pieces'; -import type { VoiceChannel } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type VoiceChannel } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveGuildVoiceChannel } from '../lib/resolvers/guildVoiceChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'guildVoiceChannel' }); + super(context, { name: 'guildVoiceChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const { guild } = context.message; + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreHyperlink.ts b/src/arguments/CoreHyperlink.ts index 3af40b7f0..5b52ca94a 100644 --- a/src/arguments/CoreHyperlink.ts +++ b/src/arguments/CoreHyperlink.ts @@ -1,14 +1,16 @@ import { container } from '@sapphire/pieces'; +import { ApplicationCommandOptionType, type CommandInteractionOption } from 'discord.js'; import type { URL } from 'node:url'; import { resolveHyperlink } from '../lib/resolvers/hyperlink'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'hyperlink', aliases: ['url'] }); + super(context, { name: 'hyperlink', aliases: ['url'], optionType: ApplicationCommandOptionType.String }); } - public run(parameter: string, context: Argument.Context): Argument.Result { + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.value as string; const resolved = resolveHyperlink(parameter); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/arguments/CoreInteger.ts b/src/arguments/CoreInteger.ts index 17b2c56a3..54551c22a 100644 --- a/src/arguments/CoreInteger.ts +++ b/src/arguments/CoreInteger.ts @@ -1,4 +1,5 @@ import { container } from '@sapphire/pieces'; +import { ApplicationCommandOptionType, type CommandInteractionOption } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveInteger } from '../lib/resolvers/integer'; import { Argument } from '../lib/structures/Argument'; @@ -11,10 +12,11 @@ export class CoreArgument extends Argument { } as const; public constructor(context: Argument.LoaderContext) { - super(context, { name: 'integer' }); + super(context, { name: 'integer', optionType: ApplicationCommandOptionType.Integer }); } - public run(parameter: string, context: Argument.Context): Argument.Result { + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') return this.ok(parameter.value as number); const resolved = resolveInteger(parameter, { minimum: context.minimum, maximum: context.maximum }); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/arguments/CoreMember.ts b/src/arguments/CoreMember.ts index af78c9c90..3e8153755 100644 --- a/src/arguments/CoreMember.ts +++ b/src/arguments/CoreMember.ts @@ -1,5 +1,5 @@ import { container } from '@sapphire/pieces'; -import type { GuildMember } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type GuildMember } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveMember } from '../lib/resolvers/member'; import { Argument } from '../lib/structures/Argument'; @@ -7,11 +7,12 @@ import type { MemberArgumentContext } from '../lib/types/ArgumentContexts'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'member' }); + super(context, { name: 'member', optionType: ApplicationCommandOptionType.User }); } - public async run(parameter: string, context: MemberArgumentContext): Argument.AsyncResult { - const { guild } = context.message; + public async run(parameter: string | CommandInteractionOption, context: MemberArgumentContext): Argument.AsyncResult { + if (typeof parameter !== 'string') parameter = parameter.user!.id; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ diff --git a/src/arguments/CoreMessage.ts b/src/arguments/CoreMessage.ts index a6e62fbb5..66f6c8e63 100644 --- a/src/arguments/CoreMessage.ts +++ b/src/arguments/CoreMessage.ts @@ -1,18 +1,19 @@ import { container } from '@sapphire/pieces'; -import type { Message } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type Message } from 'discord.js'; import { resolveMessage } from '../lib/resolvers/message'; import { Argument } from '../lib/structures/Argument'; import type { MessageArgumentContext } from '../lib/types/ArgumentContexts'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'message' }); + super(context, { name: 'message', optionType: ApplicationCommandOptionType.String }); } - public async run(parameter: string, context: MessageArgumentContext): Argument.AsyncResult { - const channel = context.channel ?? context.message.channel; + public async run(parameter: string | CommandInteractionOption, context: MessageArgumentContext): Argument.AsyncResult { + if (typeof parameter !== 'string') parameter = parameter.value as string; + 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/CoreNumber.ts b/src/arguments/CoreNumber.ts index 6563ccf37..0a36ba06f 100644 --- a/src/arguments/CoreNumber.ts +++ b/src/arguments/CoreNumber.ts @@ -1,4 +1,5 @@ import { container } from '@sapphire/pieces'; +import { ApplicationCommandOptionType, type CommandInteractionOption } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveNumber } from '../lib/resolvers/number'; import { Argument } from '../lib/structures/Argument'; @@ -11,10 +12,11 @@ export class CoreArgument extends Argument { } as const; public constructor(context: Argument.LoaderContext) { - super(context, { name: 'number' }); + super(context, { name: 'number', optionType: ApplicationCommandOptionType.Number }); } - public run(parameter: string, context: Argument.Context): Argument.Result { + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') return this.ok(parameter.value as number); const resolved = resolveNumber(parameter, { minimum: context.minimum, maximum: context.maximum }); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/arguments/CorePartialDMChannel.ts b/src/arguments/CorePartialDMChannel.ts index b9c135cf0..77f437088 100644 --- a/src/arguments/CorePartialDMChannel.ts +++ b/src/arguments/CorePartialDMChannel.ts @@ -1,15 +1,16 @@ import { container } from '@sapphire/pieces'; -import type { DMChannel, PartialDMChannel } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type DMChannel, type PartialDMChannel } from 'discord.js'; import { resolvePartialDMChannel } from '../lib/resolvers/partialDMChannel'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'partialDMChannel' }); + super(context, { name: 'partialDMChannel', optionType: ApplicationCommandOptionType.Channel }); } - public run(parameter: string, context: Argument.Context): Argument.Result { - const resolved = resolvePartialDMChannel(parameter, context.message); + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') parameter = parameter.channel!.id; + 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 90756b0f4..63d638659 100644 --- a/src/arguments/CoreRole.ts +++ b/src/arguments/CoreRole.ts @@ -1,16 +1,17 @@ import { container } from '@sapphire/pieces'; -import type { Role } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type Role } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveRole } from '../lib/resolvers/role'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'role' }); + super(context, { name: 'role', optionType: ApplicationCommandOptionType.Role }); } - public async run(parameter: string, context: Argument.Context): Argument.AsyncResult { - const { guild } = context.message; + public async run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.AsyncResult { + if (typeof parameter !== 'string') parameter = parameter.role!.id; + const { guild } = context.messageOrInteraction; if (!guild) { return this.error({ parameter, diff --git a/src/arguments/CoreString.ts b/src/arguments/CoreString.ts index c11a90151..776cd1cc9 100644 --- a/src/arguments/CoreString.ts +++ b/src/arguments/CoreString.ts @@ -1,4 +1,5 @@ import { container } from '@sapphire/pieces'; +import { ApplicationCommandOptionType, type CommandInteractionOption } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import { resolveString } from '../lib/resolvers/string'; import { Argument } from '../lib/structures/Argument'; @@ -10,10 +11,11 @@ export class CoreArgument extends Argument { } as const; public constructor(context: Argument.LoaderContext) { - super(context, { name: 'string' }); + super(context, { name: 'string', optionType: ApplicationCommandOptionType.String }); } - public run(parameter: string, context: Argument.Context): Argument.Result { + public run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.Result { + if (typeof parameter !== 'string') return this.ok(parameter.value as string); const resolved = resolveString(parameter, { minimum: context?.minimum, maximum: context?.maximum }); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/arguments/CoreUser.ts b/src/arguments/CoreUser.ts index 61c3a31b8..64f9bcdf2 100644 --- a/src/arguments/CoreUser.ts +++ b/src/arguments/CoreUser.ts @@ -1,14 +1,15 @@ import { container } from '@sapphire/pieces'; -import type { User } from 'discord.js'; +import { ApplicationCommandOptionType, type CommandInteractionOption, type User } from 'discord.js'; import { resolveUser } from '../lib/resolvers/user'; import { Argument } from '../lib/structures/Argument'; export class CoreArgument extends Argument { public constructor(context: Argument.LoaderContext) { - super(context, { name: 'user' }); + super(context, { name: 'user', optionType: ApplicationCommandOptionType.User }); } - public async run(parameter: string, context: Argument.Context): Argument.AsyncResult { + public async run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.AsyncResult { + if (typeof parameter !== 'string') return this.ok(parameter.user!); const resolved = await resolveUser(parameter); return resolved.mapErrInto((identifier) => this.error({ diff --git a/src/index.ts b/src/index.ts index f2efa90d6..cdd657dde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,6 +71,8 @@ export * from './lib/errors/Identifiers'; export * from './lib/errors/PreconditionError'; export * from './lib/errors/UserError'; export * from './lib/parsers/Args'; +export * from './lib/parsers/ChatInputCommandArgs'; +export * from './lib/parsers/MessageArgs'; export * from './lib/plugins/Plugin'; export * from './lib/plugins/PluginManager'; export * from './lib/plugins/symbols'; diff --git a/src/lib/errors/ArgumentError.ts b/src/lib/errors/ArgumentError.ts index 066c23e39..399f581d5 100644 --- a/src/lib/errors/ArgumentError.ts +++ b/src/lib/errors/ArgumentError.ts @@ -1,3 +1,4 @@ +import type { CommandInteractionOption } from 'discord.js'; import type { IArgument } from '../structures/Argument'; import { UserError } from './UserError'; @@ -8,7 +9,7 @@ import { UserError } from './UserError'; */ export class ArgumentError extends UserError { public readonly argument: IArgument; - public readonly parameter: string; + public readonly parameter: string | CommandInteractionOption; public constructor(options: ArgumentError.Options) { super({ ...options, identifier: options.identifier ?? options.argument.name }); @@ -38,7 +39,7 @@ export namespace ArgumentError { * The parameter that failed to be parsed. * @since 1.0.0 */ - parameter: string; + parameter: string | CommandInteractionOption; /** * The identifier. diff --git a/src/lib/parsers/Args.ts b/src/lib/parsers/Args.ts index 4eac58ee2..321fb42ed 100644 --- a/src/lib/parsers/Args.ts +++ b/src/lib/parsers/Args.ts @@ -1,9 +1,9 @@ -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 { container } from '@sapphire/pieces'; -import { Option, Result } from '@sapphire/result'; +import { Result } from '@sapphire/result'; import type { Awaitable } from '@sapphire/utilities'; import type { + ApplicationCommandOptionType, CategoryChannel, ChannelType, DMChannel, @@ -17,721 +17,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 { EmojiObject } from '../resolvers/emoji'; import type { Argument, IArgument } from '../structures/Argument'; -import type { MessageCommand } from '../types/CommandTypes'; - -/** - * The argument parser to be used in {@link Command}. - */ -export class Args { - /** - * The original message that triggered the command. - */ - public readonly message: Message; - - /** - * The command that is being run. - */ - public readonly command: MessageCommand; - - /** - * The context of the command being run. - */ - public readonly commandContext: MessageCommand.RunContext; - - /** - * 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: MessageCommand, parser: ArgumentStream, context: MessageCommand.RunContext) { - this.message = message; - 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, - message: 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(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, - message: this.message, - 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, - message: 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 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 | 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 }; - } +import type { Command } from '../structures/Command'; + +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>; protected unavailableArgument(type: string | IArgument): Result.Err { const name = typeof type === 'string' ? type : type.name; @@ -752,7 +54,7 @@ export class Args { * Resolves an argument. * @param arg The argument name or {@link IArgument} instance. */ - private resolveArgument(arg: keyof ArgType | IArgument): IArgument | undefined { + protected resolveArgument(arg: keyof ArgType | IArgument): IArgument | undefined { if (typeof arg === 'object') return arg; return container.stores.get('arguments').get(arg as string) as IArgument | undefined; } @@ -762,8 +64,8 @@ export class Args { * @param cb The callback to convert into an {@link IArgument}. * @param name The name of the argument. */ - public static make(cb: IArgument['run'], name = ''): IArgument { - return { run: cb, name }; + public static make(cb: IArgument['run'], optionType: ApplicationCommandOptionType, name = ''): IArgument { + return { run: cb, optionType, name }; } /** @@ -781,12 +83,17 @@ export class Args { public static error(options: ArgumentError.Options): Result.Err> { return Result.err(new ArgumentError(options)); } + + /** + * Defines the `JSON.stringify` override. + */ + public abstract toJSON(): ArgsJson; } export interface ArgsJson { - message: Message; - command: MessageCommand; - commandContext: MessageCommand.RunContext; + messageOrInteraction: Message | AnyInteraction; + command: Command; + commandContext: Record; } export interface ArgType { @@ -818,9 +125,18 @@ export interface ArgType { enum: string; } -export interface ArgOptions extends Omit {} +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 RepeatArgOptions extends ArgOptions { +export interface RepeatArgsOptions extends ArgsOptions { /** * The maximum amount of times the argument can be repeated. * @default Infinity @@ -828,14 +144,50 @@ export interface RepeatArgOptions extends ArgOptions { times?: number; } -/** - * The callback used for {@link Args.nextMaybe} and {@link Args.next}. - */ -export interface ArgsNextCallback { - /** - * The value to be mapped. - */ - (value: string): Option; +export interface PeekArgsOptions extends Omit { + name: string; + 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; + date: Date; + dmChannel: DMChannel; + emoji: EmojiObject; + float: number; + guildCategoryChannel: CategoryChannel; + guildChannel: GuildBasedChannelTypes; + guildNewsChannel: NewsChannel; + guildNewsThreadChannel: ThreadChannel & { type: ChannelType.AnnouncementThread; parent: NewsChannel | null }; + guildPrivateThreadChannel: ThreadChannel & { type: ChannelType.PrivateThread; parent: TextChannel | null }; + guildPublicThreadChannel: ThreadChannel & { type: ChannelType.PublicThread; parent: TextChannel | null }; + guildStageVoiceChannel: StageChannel; + guildTextChannel: TextChannel; + guildThreadChannel: ThreadChannel; + guildVoiceChannel: VoiceChannel; + hyperlink: URL; + integer: number; + member: GuildMember; + message: Message; + number: number; + role: Role; + string: string; + url: URL; + user: User; + enum: string; } export type ResultType = Result>; diff --git a/src/lib/parsers/ChatInputCommandArgs.ts b/src/lib/parsers/ChatInputCommandArgs.ts new file mode 100644 index 000000000..20ce37694 --- /dev/null +++ b/src/lib/parsers/ChatInputCommandArgs.ts @@ -0,0 +1,552 @@ +import { join, type Parameter } from '@sapphire/lexure'; +import { Result } from '@sapphire/result'; +import type { ChatInputCommandInteraction, CommandInteractionOption } from 'discord.js'; +import { ArgumentError } from '../errors/ArgumentError'; +import { UserError } from '../errors/UserError'; +import { Command } from '../structures/Command'; +import type { ChatInputCommand } from '../types/CommandTypes'; +import { + Args, + type ArgsJson, + type ArgsOptions, + type ArrayResultType, + type InferArgReturnType, + type PeekArgsOptions, + type RepeatArgsOptions, + type ResultType +} 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: ChatInputCommand; + + /** + * The context of the command being run. + */ + public readonly commandContext: ChatInputCommand.RunContext; + + /** + * The internal parser. + */ + protected readonly parser: ChatInputParser; + + /** + * The states stored in the args. + * @see Args#save + * @see Args#restore + */ + private readonly states: Set[] = []; + + public constructor( + interaction: ChatInputCommandInteraction, + command: ChatInputCommand, + parser: ChatInputParser, + context: ChatInputCommand.RunContext + ) { + 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(options.name, async (arg) => { + if (arg.type !== argument.optionType) return Args.error({ argument, identifier: argument.name, parameter: arg }); + + return 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>> { + 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> { + 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(options.name, async (arg) => { + if (arg.type !== argument.optionType) return Args.error({ argument, identifier: argument.name, parameter: arg }); + + return 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[]> { + 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()) as ResultType>) + : await this.pickResult(options as unknown as ArgsOptions); + 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(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 + * 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> { + const result = await this.peekResult(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 { messageOrInteraction: this.interaction, command: this.command, commandContext: this.commandContext }; + } +} diff --git a/src/lib/parsers/ChatInputParser.ts b/src/lib/parsers/ChatInputParser.ts new file mode 100644 index 000000000..bcb3db8d6 --- /dev/null +++ b/src/lib/parsers/ChatInputParser.ts @@ -0,0 +1,63 @@ +import { type ChatInputCommandInteraction, type CommandInteractionOption } from 'discord.js'; +import { Option, Result } from '@sapphire/result'; +import { type Parameter } from '@sapphire/lexure'; + +export class ChatInputParser { + public used: Set = new Set(); + + public constructor(public interaction: ChatInputCommandInteraction) {} + + public get finished(): boolean { + return this.used.size === this.interaction.options.data.length; + } + + public reset(): void { + this.used.clear(); + } + + public save(): Set { + return new Set(this.used); + } + + public restore(state: Set): void { + this.used = state; + } + + public async singleParseAsync( + name: string, + predicate: (arg: CommandInteractionOption) => Promise>, + useAnyways?: boolean + ): Promise> { + if (this.finished) return Result.err(null); + + const option = this.interaction.options.data.find((option) => option.name === name); + if (!option) return Result.err(null); + + const result = await predicate(option); + if (result.isOk() || useAnyways) { + this.used.add(option); + } + 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); + } +} diff --git a/src/lib/parsers/MessageArgs.ts b/src/lib/parsers/MessageArgs.ts new file mode 100644 index 000000000..ba35e24b3 --- /dev/null +++ b/src/lib/parsers/MessageArgs.ts @@ -0,0 +1,725 @@ +import { type ArgumentStream, join, type Parameter } from '@sapphire/lexure'; +import { Option, Result } from '@sapphire/result'; +import type { Message } from 'discord.js'; +import { ArgumentError } from '../errors/ArgumentError'; +import { UserError } from '../errors/UserError'; +import { Command } from '../structures/Command'; +import { + Args, + type ArgsJson, + type ArgsOptions, + type ArrayResultType, + type InferArgReturnType, + type PeekArgsOptions, + type RepeatArgsOptions, + type ResultType +} from './Args'; +import type { MessageCommand } from '../types/CommandTypes'; + +/** + * The argument parser to be used in {@link Command}. + */ +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: MessageCommand; + + /** + * The context of the command being run. + */ + public readonly commandContext: MessageCommand.RunContext; + + /** + * 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: MessageCommand, parser: ArgumentStream, context: MessageCommand.RunContext) { + 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()) as ResultType>) + : await this.pickResult(options as unknown as ArgsOptions); + 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(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 + * 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> { + 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 { messageOrInteraction: this.message, command: this.command, commandContext: this.commandContext }; + } +} + +/** + * The callback used for {@link Args.nextMaybe} and {@link Args.next}. + */ +export interface ArgsNextCallback { + /** + * The value to be mapped. + */ + (value: string): Option; +} diff --git a/src/lib/resolvers/channel.ts b/src/lib/resolvers/channel.ts index c2ded6023..8af9f260f 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 0a372bd36..cc937bb1d 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 8283a40da..b8e97521c 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 e8db6e106..06211ac9b 100644 --- a/src/lib/structures/Argument.ts +++ b/src/lib/structures/Argument.ts @@ -1,10 +1,13 @@ +import type { AnyInteraction } from '@sapphire/discord.js-utilities'; 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 { ApplicationCommandOptionType } from 'discord.js'; import type { ArgumentError } from '../errors/ArgumentError'; import { Args } from '../parsers/Args'; -import type { MessageCommand } from '../types/CommandTypes'; +import type { ChatInputCommand, MessageCommand } from '../types/CommandTypes'; +import { Command } from './Command'; /** * Defines a synchronous result of an {@link Argument}, check {@link Argument.AsyncResult} for the asynchronous version. @@ -27,12 +30,17 @@ export interface IArgument { */ readonly name: string; + /** + * The required chat input command option type, this will be checked before {@link Argument.run} is called. + */ + readonly optionType: ApplicationCommandOptionType; + /** * The method which is called when invoking the argument. - * @param parameter The string parameter to parse. + * @param parameter The string or {@link CommandInteractionOption} to be parsed. * @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; } /** @@ -105,11 +113,14 @@ export abstract class Argument implements IArgument { + public optionType: ApplicationCommandOptionType; + public constructor(context: Argument.LoaderContext, options: Options = {} as Options) { super(context, options); + this.optionType = options.optionType; } - public abstract run(parameter: string, context: Argument.Context): Argument.AwaitableResult; + public abstract run(parameter: string | CommandInteractionOption, context: Argument.Context): Argument.AwaitableResult; /** * Wraps a value into a successful value. @@ -128,14 +139,16 @@ export abstract class Argument extends Record { argument: IArgument; args: Args; - message: Message; - command: MessageCommand; - commandContext: MessageCommand.RunContext; + messageOrInteraction: Message | AnyInteraction; + command: MessageCommand | ChatInputCommand; + commandContext: MessageCommand.RunContext | ChatInputCommand.RunContext; minimum?: number; maximum?: number; inclusive?: boolean; diff --git a/src/lib/structures/Command.ts b/src/lib/structures/Command.ts index 8cdf63d66..d34573be3 100644 --- a/src/lib/structures/Command.ts +++ b/src/lib/structures/Command.ts @@ -29,6 +29,9 @@ 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'; +import { MessageArgs } from '../parsers/MessageArgs'; +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[]; @@ -153,7 +156,17 @@ 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 MessageArgs(message, this as MessageCommand, args, context) as PreParseReturn; + } + + /** + * The chat input pre-parse method. This method can be overridden by plugins to define their own argument parser. + * @param interaction The interaction that triggered the command. + * @param context The command-context used in this execution. + */ + public chatInputPreParse(interaction: ChatInputCommandInteraction, context: ChatInputCommand.RunContext): Awaitable { + const parser = new ChatInputParser(interaction); + return new ChatInputCommandArgs(interaction, this as ChatInputCommand, parser, context) as PreParseReturn; } /** @@ -205,7 +218,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 24f53e31f..40c7d158f 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 });