prefixed commands options

This commit is contained in:
MARCROCK22 2024-03-20 17:29:02 -04:00
parent 647f7d11d0
commit 8914b8c24c
6 changed files with 252 additions and 177 deletions

View File

@ -3,7 +3,7 @@ import { ApiHandler, Router } from '../api';
import type { Adapter } from '../cache'; import type { Adapter } from '../cache';
import { Cache, MemoryAdapter } from '../cache'; import { Cache, MemoryAdapter } from '../cache';
import type { RegisteredMiddlewares } from '../commands'; import type { RegisteredMiddlewares } from '../commands';
import type { MiddlewareContext } from '../commands/applications/shared'; import type { InferWithPrefix, MiddlewareContext } from '../commands/applications/shared';
import { CommandHandler } from '../commands/handler'; import { CommandHandler } from '../commands/handler';
import { import {
ChannelShorter, ChannelShorter,
@ -24,10 +24,15 @@ import {
type MakeRequired, type MakeRequired,
} from '../common'; } from '../common';
import type { DeepPartial, IntentStrings, OmitInsert } from '../common/types/util'; import type { DeepPartial, IntentStrings, OmitInsert, When } from '../common/types/util';
import { ComponentHandler } from '../components/handler'; import { ComponentHandler } from '../components/handler';
import { LangsHandler } from '../langs/handler'; import { LangsHandler } from '../langs/handler';
import type { ChatInputCommandInteraction, Message, MessageCommandInteraction, UserCommandInteraction } from '../structures'; import type {
ChatInputCommandInteraction,
Message,
MessageCommandInteraction,
UserCommandInteraction,
} from '../structures';
export class BaseClient { export class BaseClient {
rest!: ApiHandler; rest!: ApiHandler;
@ -246,7 +251,7 @@ export interface BaseClientOptions {
| ChatInputCommandInteraction<boolean> | ChatInputCommandInteraction<boolean>
| UserCommandInteraction<boolean> | UserCommandInteraction<boolean>
| MessageCommandInteraction<boolean> | MessageCommandInteraction<boolean>
| Message | When<InferWithPrefix, Message, never>,
) => {}; ) => {};
globalMiddlewares?: readonly (keyof RegisteredMiddlewares)[]; globalMiddlewares?: readonly (keyof RegisteredMiddlewares)[];
} }

View File

@ -1,5 +1,12 @@
import { ApplicationCommandType, InteractionType, type APIInteraction } from 'discord-api-types/v10'; import { ApplicationCommandType, InteractionType, type APIInteraction } from 'discord-api-types/v10';
import { CommandContext, MenuCommandContext, OptionResolver, type Command, type ContextMenuCommand } from '../commands'; import {
CommandContext,
type ContextOptionsResolved,
MenuCommandContext,
OptionResolver,
type Command,
type ContextMenuCommand,
} from '../commands';
import type { import type {
ChatInputCommandInteraction, ChatInputCommandInteraction,
ComponentInteraction, ComponentInteraction,
@ -32,7 +39,7 @@ export async function onInteractionCreate(
body.data.options ?? [], body.data.options ?? [],
parentCommand as Command, parentCommand as Command,
body.data.guild_id, body.data.guild_id,
body.data.resolved, body.data.resolved as ContextOptionsResolved,
); );
const interaction = new AutocompleteInteraction(self, body, __reply); const interaction = new AutocompleteInteraction(self, body, __reply);
const command = optionsResolver.getAutocomplete(); const command = optionsResolver.getAutocomplete();
@ -139,7 +146,7 @@ export async function onInteractionCreate(
packetData.options ?? [], packetData.options ?? [],
parentCommand as Command, parentCommand as Command,
packetData.guild_id, packetData.guild_id,
packetData.resolved, packetData.resolved as ContextOptionsResolved,
); );
const interaction = BaseInteraction.from(self, body, __reply) as ChatInputCommandInteraction; const interaction = BaseInteraction.from(self, body, __reply) as ChatInputCommandInteraction;
const command = optionsResolver.getCommand(); const command = optionsResolver.getCommand();

View File

@ -2,14 +2,15 @@ import {
ApplicationCommandOptionType, ApplicationCommandOptionType,
ChannelType, ChannelType,
type APIApplicationCommandInteractionDataOption, type APIApplicationCommandInteractionDataOption,
type APIInteractionDataResolved,
type GatewayMessageCreateDispatchData, type GatewayMessageCreateDispatchData,
} from 'discord-api-types/v10'; } from 'discord-api-types/v10';
import { import {
Command, Command,
CommandContext, CommandContext,
type ContextOptionsResolved,
OptionResolver, OptionResolver,
SubCommand, SubCommand,
User,
type Client, type Client,
type CommandOption, type CommandOption,
type SeyfertChannelOption, type SeyfertChannelOption,
@ -88,7 +89,7 @@ export async function onMessageCreate(
if (command.dm && !message.guildId) return; if (command.dm && !message.guildId) return;
if (command.guild_id && !command.guild_id?.includes(message.guildId!)) return; if (command.guild_id && !command.guild_id?.includes(message.guildId!)) return;
const resolved: MakeRequired<APIInteractionDataResolved> = { const resolved: MakeRequired<ContextOptionsResolved> = {
channels: {}, channels: {},
roles: {}, roles: {},
users: {}, users: {},
@ -99,6 +100,7 @@ export async function onMessageCreate(
const { options, errors } = await parseOptions(self, command, rawMessage, args, resolved); const { options, errors } = await parseOptions(self, command, rawMessage, args, resolved);
const optionsResolver = new OptionResolver(self, options, parent as Command, message.guildId, resolved); const optionsResolver = new OptionResolver(self, options, parent as Command, message.guildId, resolved);
const context = new CommandContext(self, message, optionsResolver, shardId, command); const context = new CommandContext(self, message, optionsResolver, shardId, command);
//@ts-expect-error
const extendContext = self.options?.context?.(message) ?? {}; const extendContext = self.options?.context?.(message) ?? {};
Object.assign(context, extendContext); Object.assign(context, extendContext);
try { try {
@ -167,11 +169,12 @@ async function parseOptions(
command: Command | SubCommand, command: Command | SubCommand,
message: GatewayMessageCreateDispatchData, message: GatewayMessageCreateDispatchData,
args: Partial<Record<string, string>>, args: Partial<Record<string, string>>,
resolved: MakeRequired<APIInteractionDataResolved, keyof APIInteractionDataResolved>, resolved: MakeRequired<ContextOptionsResolved>,
) { ) {
const options: APIApplicationCommandInteractionDataOption[] = []; const options: APIApplicationCommandInteractionDataOption[] = [];
const errors: { name: string; error: string }[] = []; const errors: { name: string; error: string }[] = [];
for (const i of (command.options ?? []) as (CommandOption & { type: ApplicationCommandOptionType })[]) { for (const i of (command.options ?? []) as (CommandOption & { type: ApplicationCommandOptionType })[]) {
try {
let value: string | boolean | number | undefined; let value: string | boolean | number | undefined;
let indexAttachment = -1; let indexAttachment = -1;
switch (i.type) { switch (i.type) {
@ -188,9 +191,12 @@ async function parseOptions(
break; break;
case ApplicationCommandOptionType.Channel: case ApplicationCommandOptionType.Channel:
{ {
const rawId = message.content.match(/(?<=<#)[0-9]{17,19}(?=>)/g)?.find(x => args[i.name]?.includes(x)); const rawId =
message.content.match(/(?<=<#)[0-9]{17,19}(?=>)/g)?.find(x => args[i.name]?.includes(x)) ||
args[i.name]?.match(/[0-9]{17,19}/g)?.[0];
if (rawId) { if (rawId) {
const channel = i.required ? await self.channels.fetch(rawId) : await self.cache.channels?.get(rawId); const channel =
(await self.cache.channels?.get(rawId)) ?? (i.required ? await self.channels.fetch(rawId) : undefined);
if (channel) { if (channel) {
if ('channel_types' in i) { if ('channel_types' in i) {
if (!(i as SeyfertChannelOption).channel_types!.includes(channel.type)) { if (!(i as SeyfertChannelOption).channel_types!.includes(channel.type)) {
@ -204,7 +210,6 @@ async function parseOptions(
} }
} }
value = rawId; value = rawId;
//@ts-expect-error
resolved.channels[rawId] = channel; resolved.channels[rawId] = channel;
} }
} }
@ -217,12 +222,11 @@ async function parseOptions(
if (match.includes('&')) { if (match.includes('&')) {
const rawId = match.slice(3); const rawId = match.slice(3);
if (rawId) { if (rawId) {
const role = i.required const role =
? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) (await self.cache.roles?.get(rawId)) ??
: await self.cache.roles?.get(rawId); (i.required ? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) : undefined);
if (role) { if (role) {
value = rawId; value = rawId;
//@ts-expect-error
resolved.roles[rawId] = role; resolved.roles[rawId] = role;
break; break;
} }
@ -231,8 +235,10 @@ async function parseOptions(
const rawId = match.slice(2); const rawId = match.slice(2);
const raw = message.mentions.find(x => rawId === x.id); const raw = message.mentions.find(x => rawId === x.id);
if (raw) { if (raw) {
const { member, ...user } = raw;
value = raw.id; value = raw.id;
resolved.users[raw.id] = raw; resolved.users[raw.id] = user;
if (member) resolved.members[raw.id] = member;
break; break;
} }
} }
@ -241,14 +247,15 @@ async function parseOptions(
break; break;
case ApplicationCommandOptionType.Role: case ApplicationCommandOptionType.Role:
{ {
const rawId = message.mention_roles.find(x => args[i.name]?.includes(x)); const rawId =
message.mention_roles.find(x => args[i.name]?.includes(x)) || args[i.name]?.match(/[0-9]{17,19}/g)?.[0];
if (rawId) { if (rawId) {
const role = i.required const role =
? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) //why, discord, why (await self.cache.roles?.get(rawId)) ??
: await self.cache.roles?.get(rawId); (i.required ? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) : undefined);
if (role) { if (role) {
value = rawId; value = rawId;
//@ts-expect-error
resolved.roles[rawId] = role; resolved.roles[rawId] = role;
} }
} }
@ -256,10 +263,30 @@ async function parseOptions(
break; break;
case ApplicationCommandOptionType.User: case ApplicationCommandOptionType.User:
{ {
const raw = message.mentions.find(x => args[i.name]?.includes(x.id)); const rawId =
message.mentions.find(x => args[i.name]?.includes(x.id))?.id || args[i.name]?.match(/[0-9]{17,19}/g)?.[0];
if (rawId) {
const raw =
message.mentions.find(x => args[i.name]?.includes(x.id)) ??
(await self.cache.users?.get(rawId)) ??
(i.required ? await self.users.fetch(rawId) : undefined);
if (raw) { if (raw) {
value = raw.id; value = raw.id;
if (raw instanceof User) {
resolved.users[raw.id] = raw; resolved.users[raw.id] = raw;
if (message.guild_id) {
const member =
message.mentions.find(x => args[i.name]?.includes(x.id))?.member ??
(await self.cache.members?.get(rawId, message.guild_id)) ??
(i.required ? await self.members.fetch(rawId, message.guild_id) : undefined);
if (member) resolved.members[raw.id] = member;
}
} else {
const { member, ...user } = raw;
resolved.users[user.id] = user;
if (member) resolved.members[user.id] = member;
}
}
} }
} }
break; break;
@ -365,12 +392,17 @@ async function parseOptions(
type: i.type, type: i.type,
value, value,
} as APIApplicationCommandInteractionDataOption); } as APIApplicationCommandInteractionDataOption);
} } else if (i.required)
if (i.required && value === undefined)
errors.push({ errors.push({
error: 'Option is required but returned undefined', error: 'Option is required but returned undefined',
name: i.name, name: i.name,
}); });
} catch (e) {
errors.push({
error: e && typeof e === 'object' && 'message' in e ? (e.message as string) : `${e}`,
name: i.name,
});
}
} }
return { errors, options }; return { errors, options };

View File

@ -1,5 +1,14 @@
import { Attachment, GuildMember } from '..'; import { Attachment, GuildMember } from '..';
import type { APIApplicationCommandInteractionDataOption, APIInteractionDataResolved, MakeRequired } from '../common'; import type {
APIApplicationCommandInteractionDataOption,
APIAttachment,
APIGuildMember,
APIInteractionDataResolvedChannel,
APIInteractionGuildMember,
APIRole,
APIUser,
MakeRequired,
} from '../common';
import { ApplicationCommandOptionType } from '../common'; import { ApplicationCommandOptionType } from '../common';
import type { AllChannels } from '../structures'; import type { AllChannels } from '../structures';
import { GuildRole, InteractionGuildMember, User } from '../structures'; import { GuildRole, InteractionGuildMember, User } from '../structures';
@ -7,6 +16,14 @@ import channelFrom from '../structures/channels';
import type { Command, CommandAutocompleteOption, CommandOption, SubCommand } from './applications/chat'; import type { Command, CommandAutocompleteOption, CommandOption, SubCommand } from './applications/chat';
import type { UsingClient } from './applications/shared'; import type { UsingClient } from './applications/shared';
export type ContextOptionsResolved = {
members?: Record<string, APIGuildMember | APIInteractionGuildMember | GuildMember | InteractionGuildMember>;
users?: Record<string, APIUser | User>;
roles?: Record<string, APIRole | GuildRole>;
channels?: Record<string, APIInteractionDataResolvedChannel | AllChannels>;
attachments?: Record<string, APIAttachment | Attachment>;
};
export class OptionResolver { export class OptionResolver {
readonly options: OptionResolved[]; readonly options: OptionResolved[];
public hoistedOptions: OptionResolved[]; public hoistedOptions: OptionResolved[];
@ -17,7 +34,7 @@ export class OptionResolver {
options: APIApplicationCommandInteractionDataOption[], options: APIApplicationCommandInteractionDataOption[],
public parent?: Command, public parent?: Command,
public guildId?: string, public guildId?: string,
public resolved?: APIInteractionDataResolved, public resolved?: ContextOptionsResolved,
) { ) {
this.hoistedOptions = this.options = options.map(option => this.transformOption(option, resolved)); this.hoistedOptions = this.options = options.map(option => this.transformOption(option, resolved));
@ -129,7 +146,7 @@ export class OptionResolver {
return option.value as string; return option.value as string;
} }
transformOption(option: APIApplicationCommandInteractionDataOption, resolved?: APIInteractionDataResolved) { transformOption(option: APIApplicationCommandInteractionDataOption, resolved?: ContextOptionsResolved) {
const resolve: OptionResolved = { const resolve: OptionResolved = {
...option, ...option,
}; };
@ -144,16 +161,18 @@ export class OptionResolver {
const value = resolve.value as string; const value = resolve.value as string;
const user = resolved.users?.[value]; const user = resolved.users?.[value];
if (user) { if (user) {
resolve.user = new User(this.client, user); resolve.user = user instanceof User ? user : new User(this.client, user);
} }
const member = resolved.members?.[value]; const member = resolved.members?.[value];
if (member) { if (member) {
resolve.member = resolve.member =
member instanceof GuildMember member instanceof GuildMember || member instanceof InteractionGuildMember
? member ? member
: new InteractionGuildMember(this.client, member, user!, this.guildId!); : 'permissions' in member
? new InteractionGuildMember(this.client, member, user!, this.guildId!)
: new GuildMember(this.client, member, user!, this.guildId!);
} }
const channel = resolved.channels?.[value]; const channel = resolved.channels?.[value];
@ -168,7 +187,7 @@ export class OptionResolver {
const attachment = resolved.attachments?.[value]; const attachment = resolved.attachments?.[value];
if (attachment) { if (attachment) {
resolve.attachment = new Attachment(this.client, attachment); resolve.attachment = attachment instanceof Attachment ? attachment : new Attachment(this.client, attachment);
} }
} }

View File

@ -1,7 +1,13 @@
import type { InternalRuntimeConfig, InternalRuntimeConfigHTTP, RuntimeConfig, RuntimeConfigHTTP } from './client/base'; import type { InternalRuntimeConfig, InternalRuntimeConfigHTTP, RuntimeConfig, RuntimeConfigHTTP } from './client/base';
import { GatewayIntentBits } from './common'; import type { InferWithPrefix } from './commands';
import { GatewayIntentBits, type When } from './common';
import type { ClientNameEvents, EventContext } from './events'; import type { ClientNameEvents, EventContext } from './events';
import type { ChatInputCommandInteraction, Message, MessageCommandInteraction, UserCommandInteraction } from './structures'; import type {
ChatInputCommandInteraction,
Message,
MessageCommandInteraction,
UserCommandInteraction,
} from './structures';
export { Logger, PermissionFlagsBits, PermissionStrings, Watcher } from './common'; export { Logger, PermissionFlagsBits, PermissionStrings, Watcher } from './common';
// //
@ -95,7 +101,13 @@ export const config = {
* }); * });
*/ */
export function extendContext<T extends {}>( export function extendContext<T extends {}>(
cb: (interaction: ChatInputCommandInteraction | UserCommandInteraction | MessageCommandInteraction | Message) => T, cb: (
interaction:
| ChatInputCommandInteraction
| UserCommandInteraction
| MessageCommandInteraction
| When<InferWithPrefix, Message, never>,
) => T,
) { ) {
return cb; return cb;
} }

View File

@ -2,7 +2,7 @@ import { mix } from 'ts-mixer';
import type { RawFile } from '../api'; import type { RawFile } from '../api';
import { ActionRow, Embed, Modal, resolveAttachment, resolveFiles } from '../builders'; import { ActionRow, Embed, Modal, resolveAttachment, resolveFiles } from '../builders';
import type { BaseClient } from '../client/base'; import type { BaseClient } from '../client/base';
import { OptionResolver, type UsingClient } from '../commands'; import { type ContextOptionsResolved, OptionResolver, type UsingClient } from '../commands';
import type { import type {
APIActionRowComponent, APIActionRowComponent,
APIApplicationCommandAutocompleteInteraction, APIApplicationCommandAutocompleteInteraction,
@ -303,7 +303,7 @@ export class AutocompleteInteraction<FromGuild extends boolean = boolean> extend
interaction.data.options, interaction.data.options,
undefined, undefined,
interaction.guild_id, interaction.guild_id,
interaction.data.resolved, interaction.data.resolved as ContextOptionsResolved,
); );
} }