From 2737b8f426a8050affd433c7abf6cba415e0b850 Mon Sep 17 00:00:00 2001 From: MARCROCK22 Date: Sun, 2 Jun 2024 20:21:18 +0000 Subject: [PATCH] feat: optionsParser option --- src/client/client.ts | 55 +- src/client/onmessagecreate.ts | 926 ++++++++++++++-------------- src/commands/applications/chat.ts | 15 +- src/commands/applications/shared.ts | 133 ++-- 4 files changed, 604 insertions(+), 525 deletions(-) diff --git a/src/client/client.ts b/src/client/client.ts index 73aa51f..e79ce32 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,6 +1,29 @@ -import { GatewayIntentBits, type GatewayDispatchPayload, type GatewayPresenceUpdateData } from 'discord-api-types/v10'; -import type { Command, CommandContext, Message, SubCommand } from '..'; -import { lazyLoadPackage, type DeepPartial, type If, type WatcherPayload, type WatcherSendToShard } from '../common'; +import { + type APIApplicationCommandInteractionDataOption, + GatewayIntentBits, + type GatewayMessageCreateDispatchData, + type GatewayDispatchPayload, + type GatewayPresenceUpdateData, +} from 'discord-api-types/v10'; +import type { + Command, + CommandContext, + ContextOptionsResolved, + Message, + MessageCommandOptionErrors, + SubCommand, + UsingClient, +} from '..'; +import { + type Awaitable, + type MakeRequired, + MergeOptions, + lazyLoadPackage, + type DeepPartial, + type If, + type WatcherPayload, + type WatcherSendToShard, +} from '../common'; import { EventHandler } from '../events'; import { ClientUser } from '../structures'; import { ShardManager, properties, type ShardManagerOptions } from '../websocket'; @@ -9,7 +32,7 @@ import { PresenceUpdateHandler } from '../websocket/discord/events/presenceUpdat import type { BaseClientOptions, InternalRuntimeConfig, ServicesOptions, StartOptions } from './base'; import { BaseClient } from './base'; import { onInteractionCreate } from './oninteractioncreate'; -import { onMessageCreate } from './onmessagecreate'; +import { defaultArgsParser, defaultParseOptions, onMessageCreate } from './onmessagecreate'; import { Collectors } from './collectors'; let parentPort: import('node:worker_threads').MessagePort; @@ -18,7 +41,9 @@ export class Client extends BaseClient { private __handleGuilds?: Set = new Set(); gateway!: ShardManager; me!: If; - declare options: ClientOptions; + declare options: Omit & { + commands: MakeRequired, 'argsParser' | 'optionsParser'>; + }; memberUpdateHandler = new MemberUpdateHandler(); presenceUpdateHandler = new PresenceUpdateHandler(); collectors = new Collectors(); @@ -26,6 +51,12 @@ export class Client extends BaseClient { constructor(options?: ClientOptions) { super(options); + this.options = MergeOptions(this.options, { + commands: { + argsParser: defaultArgsParser, + optionsParser: defaultParseOptions, + }, + } satisfies ClientOptions); } setServices({ @@ -212,6 +243,20 @@ export interface ClientOptions extends BaseClientOptions { deferReplyResponse?: (ctx: CommandContext) => Parameters[0]; reply?: (ctx: CommandContext) => boolean; argsParser?: (content: string, command: SubCommand | Command, message: Message) => Record; + optionsParser?: ( + self: UsingClient, + command: Command | SubCommand, + message: GatewayMessageCreateDispatchData, + args: Partial>, + resolved: MakeRequired, + ) => Awaitable<{ + errors: { + name: string; + error: string; + fullError: MessageCommandOptionErrors; + }[]; + options: APIApplicationCommandInteractionDataOption[]; + }>; }; handlePayload?: ShardManagerOptions['handlePayload']; } diff --git a/src/client/onmessagecreate.ts b/src/client/onmessagecreate.ts index a14465e..7d40eb4 100644 --- a/src/client/onmessagecreate.ts +++ b/src/client/onmessagecreate.ts @@ -1,457 +1,469 @@ -import { - ApplicationCommandOptionType, - ChannelType, - InteractionContextType, - type APIApplicationCommandInteractionDataOption, - type GatewayMessageCreateDispatchData, -} from 'discord-api-types/v10'; -import { - Command, - CommandContext, - IgnoreCommand, - OptionResolver, - SubCommand, - User, - type Client, - type CommandOption, - type ContextOptionsResolved, - type SeyfertChannelOption, - type SeyfertIntegerOption, - type SeyfertNumberOption, - type SeyfertStringOption, - type WorkerClient, -} from '..'; -import type { MakeRequired } from '../common'; -import { Message } from '../structures'; - -function getCommandFromContent( - commandRaw: string[], - self: Client | WorkerClient, -): { - command?: Command | SubCommand; - parent?: Command; - fullCommandName: string; -} { - const rawParentName = commandRaw[0]; - const rawGroupName = commandRaw.length === 3 ? commandRaw[1] : undefined; - const rawSubcommandName = rawGroupName ? commandRaw[2] : commandRaw[1]; - const parent = self.commands!.values.find( - x => - (!('ignore' in x) || x.ignore !== IgnoreCommand.Message) && - (x.name === rawParentName || ('aliases' in x ? x.aliases?.includes(rawParentName) : false)), - ); - const fullCommandName = `${rawParentName}${ - rawGroupName ? ` ${rawGroupName} ${rawSubcommandName}` : `${rawSubcommandName ? ` ${rawSubcommandName}` : ''}` - }`; - - if (!(parent instanceof Command)) return { fullCommandName }; - - if (rawGroupName && !parent.groups?.[rawGroupName] && !parent.groupsAliases?.[rawGroupName]) - return getCommandFromContent([rawParentName, rawGroupName], self); - if ( - rawSubcommandName && - !parent.options?.some( - x => x instanceof SubCommand && (x.name === rawSubcommandName || x.aliases?.includes(rawSubcommandName)), - ) - ) - return getCommandFromContent([rawParentName], self); - - const groupName = rawGroupName ? parent.groupsAliases?.[rawGroupName] || rawGroupName : undefined; - - const command = - groupName || rawSubcommandName - ? (parent.options?.find(opt => { - if (opt instanceof SubCommand) { - if (groupName) { - if (opt.group !== groupName) return false; - } - if (opt.group && !groupName) return false; - return rawSubcommandName === opt.name || opt.aliases?.includes(rawSubcommandName); - } - return false; - }) as SubCommand) - : parent; - - return { - command, - fullCommandName, - parent, - }; -} - -export async function onMessageCreate( - self: Client | WorkerClient, - rawMessage: GatewayMessageCreateDispatchData, - shardId: number, -) { - if (!self.options?.commands?.prefix) return; - const message = new Message(self, rawMessage); - const prefixes = (await self.options.commands.prefix(message)).sort((a, b) => b.length - a.length); - const prefix = prefixes.find(x => message.content.startsWith(x)); - - if (!(prefix && message.content.startsWith(prefix))) return; - - const content = message.content.slice(prefix.length).trimStart(); - const { fullCommandName, command, parent } = getCommandFromContent( - content - .split(' ') - .filter(x => x) - .slice(0, 3), - self, - ); - - if (!command) return; - if (!command.run) return self.logger.warn(`${fullCommandName} command does not have 'run' callback`); - - if (!(command.contexts?.includes(InteractionContextType.BotDM) || message.guildId)) return; - if (command.guildId && !command.guildId?.includes(message.guildId!)) return; - - const resolved: MakeRequired = { - channels: {}, - roles: {}, - users: {}, - members: {}, - attachments: {}, - }; - - let newContent = content; - for (const i of fullCommandName.split(' ')) { - newContent = newContent.slice(newContent.indexOf(i) + i.length); - } - - const args = (self.options?.commands?.argsParser ?? defaultArgsParser)(newContent.slice(1), command, message); - const { options, errors } = await parseOptions(self, command, rawMessage, args, resolved); - const optionsResolver = new OptionResolver(self, options, parent as Command, message.guildId, resolved); - const context = new CommandContext(self, message, optionsResolver, shardId, command); - //@ts-expect-error - const extendContext = self.options?.context?.(message) ?? {}; - Object.assign(context, extendContext); - try { - if (command.defaultMemberPermissions && message.guildId) { - const memberPermissions = await self.members.permissions(message.guildId, message.author.id); - const permissions = memberPermissions.missings(...memberPermissions.values([command.defaultMemberPermissions])); - if ( - !memberPermissions.has('Administrator') && - permissions.length && - (await message.guild())!.ownerId !== message.author.id - ) { - return command.onPermissionsFail?.(context, memberPermissions.keys(permissions)); - } - } - if (command.botPermissions && message.guildId) { - const meMember = await self.cache.members?.get(self.botId, message.guildId); - if (!meMember) return; //enable member cache and "Guilds" intent, lol - const appPermissions = await meMember.fetchPermissions(); - const permissions = appPermissions.missings(...appPermissions.values([command.botPermissions])); - if (!appPermissions.has('Administrator') && permissions.length) { - return command.onBotPermissionsFail?.(context, appPermissions.keys(permissions)); - } - } - if (errors.length) { - return command.onOptionsError?.( - context, - Object.fromEntries( - errors.map(x => { - return [ - x.name, - { - failed: true, - value: x.error, - }, - ]; - }), - ), - ); - } - const [erroredOptions, result] = await command.__runOptions(context, optionsResolver); - if (erroredOptions) { - return command.onOptionsError?.(context, result); - } - const resultRunGlobalMiddlewares = await command.__runGlobalMiddlewares(context); - if (resultRunGlobalMiddlewares.pass) { - return; - } - if ('error' in resultRunGlobalMiddlewares) { - return command.onMiddlewaresError?.(context, resultRunGlobalMiddlewares.error ?? 'Unknown error'); - } - const resultRunMiddlewares = await command.__runMiddlewares(context); - if (resultRunMiddlewares.pass) { - return; - } - if ('error' in resultRunMiddlewares) { - return command.onMiddlewaresError?.(context, resultRunMiddlewares.error ?? 'Unknown error'); - } - - try { - await command.run?.(context); - await command.onAfterRun?.(context, undefined); - } catch (error) { - await command.onRunError?.(context, error); - await command.onAfterRun?.(context, error); - } - } catch (error) { - try { - await command.onInternalError?.(self, context.command, error); - } catch { - // supress error - } - } -} - -async function parseOptions( - self: Client | WorkerClient, - command: Command | SubCommand, - message: GatewayMessageCreateDispatchData, - args: Partial>, - resolved: MakeRequired, -) { - const options: APIApplicationCommandInteractionDataOption[] = []; - const errors: { name: string; error: string }[] = []; - for (const i of (command.options ?? []) as (CommandOption & { type: ApplicationCommandOptionType })[]) { - try { - let value: string | boolean | number | undefined; - let indexAttachment = -1; - switch (i.type) { - case ApplicationCommandOptionType.Attachment: - if (message.attachments[++indexAttachment]) { - value = message.attachments[indexAttachment].id; - resolved.attachments[value] = message.attachments[indexAttachment]; - } - break; - case ApplicationCommandOptionType.Boolean: - if (args[i.name]) { - value = ['yes', 'y', 'true', 'treu'].includes(args[i.name]!.toLowerCase()); - } - break; - case ApplicationCommandOptionType.Channel: - { - 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) { - const channel = - (await self.cache.channels?.get(rawId)) ?? (i.required ? await self.channels.fetch(rawId) : undefined); - if (channel) { - if ('channel_types' in i) { - if (!(i as SeyfertChannelOption).channel_types!.includes(channel.type)) { - errors.push({ - name: i.name, - error: `The entered channel type is not one of ${(i as SeyfertChannelOption) - .channel_types!.map(t => ChannelType[t]) - .join(', ')}`, - }); - break; - } - } - value = rawId; - resolved.channels[rawId] = channel; - } - } - } - break; - case ApplicationCommandOptionType.Mentionable: - { - const matches = message.content.match(/<@[0-9]{17,19}(?=>)|<@&[0-9]{17,19}(?=>)/g) ?? []; - for (const match of matches) { - if (match.includes('&')) { - const rawId = match.slice(3); - if (rawId) { - const role = - (await self.cache.roles?.get(rawId)) ?? - (i.required ? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) : undefined); - if (role) { - value = rawId; - resolved.roles[rawId] = role; - break; - } - } - } else { - const rawId = match.slice(2); - const raw = message.mentions.find(x => rawId === x.id); - if (raw) { - const { member, ...user } = raw; - value = raw.id; - resolved.users[raw.id] = user; - if (member) resolved.members[raw.id] = member; - break; - } - } - } - } - break; - case ApplicationCommandOptionType.Role: - { - const rawId = - message.mention_roles.find(x => args[i.name]?.includes(x)) || args[i.name]?.match(/[0-9]{17,19}/g)?.[0]; - if (rawId) { - const role = - (await self.cache.roles?.get(rawId)) ?? - (i.required ? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) : undefined); - - if (role) { - value = rawId; - resolved.roles[rawId] = role; - } - } - } - break; - case ApplicationCommandOptionType.User: - { - 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) { - value = raw.id; - if (raw instanceof User) { - 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; - case ApplicationCommandOptionType.String: - { - value = args[i.name]; - const option = i as SeyfertStringOption; - if (!value) break; - if (option.min_length) { - if (value.length < option.min_length) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered string has less than ${option.min_length} characters. The minimum required is ${option.min_length} characters.`, - }); - break; - } - } - if (option.max_length) { - if (value.length > option.max_length) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered string has more than ${option.max_length} characters. The maximum required is ${option.max_length} characters.`, - }); - break; - } - } - if (option.choices?.length) { - const choice = option.choices.find(x => x.name === value); - if (!choice) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered choice is invalid. Please choose one of the following options: ${option.choices - .map(x => x.name) - .join(', ')}.`, - }); - break; - } - value = choice.value; - } - } - break; - case ApplicationCommandOptionType.Number: - case ApplicationCommandOptionType.Integer: - { - const option = i as SeyfertNumberOption | SeyfertIntegerOption; - if (!option.choices?.length) { - value = Number(args[i.name]); - if (args[i.name] === undefined) { - value = undefined; - break; - } - if (Number.isNaN(value)) { - value = undefined; - errors.push({ - name: i.name, - error: 'The entered choice is an invalid number.', - }); - break; - } - if (option.min_value) { - if (value < option.min_value) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered number is less than ${option.min_value}. The minimum allowed is ${option.min_value}`, - }); - break; - } - } - if (option.max_value) { - if (value > option.max_value) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered number is greater than ${option.max_value}. The maximum allowed is ${option.max_value}`, - }); - break; - } - } - break; - } - const choice = option.choices.find(x => x.name === args[i.name]); - if (!choice) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered choice is invalid. Please choose one of the following options: ${option.choices - .map(x => x.name) - .join(', ')}.`, - }); - break; - } - value = choice.value; - } - break; - default: - break; - } - if (value !== undefined) { - options.push({ - name: i.name, - type: i.type, - value, - } as APIApplicationCommandInteractionDataOption); - } else if (i.required) - if (!errors.some(x => x.name === i.name)) - errors.push({ - error: 'Option is required but returned undefined', - 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 }; -} - -function defaultArgsParser(content: string) { - const args: Record = {}; - for (const i of content.match(/-(.*?)(?=\s-|$)/gs) ?? []) { - args[i.slice(1).split(' ')[0]] = i.split(' ').slice(1).join(' '); - } - return args; -} - -//-(.*?)(?=\s-|$)/gs -//-(?[^-]*)/gm +import { + ApplicationCommandOptionType, + ChannelType, + InteractionContextType, + type APIApplicationCommandInteractionDataOption, + type GatewayMessageCreateDispatchData, +} from 'discord-api-types/v10'; +import { + Command, + CommandContext, + IgnoreCommand, + type MessageCommandOptionErrors, + OptionResolver, + SubCommand, + User, + type UsingClient, + type Client, + type CommandOption, + type ContextOptionsResolved, + type SeyfertChannelOption, + type SeyfertIntegerOption, + type SeyfertNumberOption, + type SeyfertStringOption, + type WorkerClient, +} from '..'; +import type { MakeRequired } from '../common'; +import { Message } from '../structures'; + +function getCommandFromContent( + commandRaw: string[], + self: Client | WorkerClient, +): { + command?: Command | SubCommand; + parent?: Command; + fullCommandName: string; +} { + const rawParentName = commandRaw[0]; + const rawGroupName = commandRaw.length === 3 ? commandRaw[1] : undefined; + const rawSubcommandName = rawGroupName ? commandRaw[2] : commandRaw[1]; + const parent = self.commands!.values.find( + x => + (!('ignore' in x) || x.ignore !== IgnoreCommand.Message) && + (x.name === rawParentName || ('aliases' in x ? x.aliases?.includes(rawParentName) : false)), + ); + const fullCommandName = `${rawParentName}${ + rawGroupName ? ` ${rawGroupName} ${rawSubcommandName}` : `${rawSubcommandName ? ` ${rawSubcommandName}` : ''}` + }`; + + if (!(parent instanceof Command)) return { fullCommandName }; + + if (rawGroupName && !parent.groups?.[rawGroupName] && !parent.groupsAliases?.[rawGroupName]) + return getCommandFromContent([rawParentName, rawGroupName], self); + if ( + rawSubcommandName && + !parent.options?.some( + x => x instanceof SubCommand && (x.name === rawSubcommandName || x.aliases?.includes(rawSubcommandName)), + ) + ) + return getCommandFromContent([rawParentName], self); + + const groupName = rawGroupName ? parent.groupsAliases?.[rawGroupName] || rawGroupName : undefined; + + const command = + groupName || rawSubcommandName + ? (parent.options?.find(opt => { + if (opt instanceof SubCommand) { + if (groupName) { + if (opt.group !== groupName) return false; + } + if (opt.group && !groupName) return false; + return rawSubcommandName === opt.name || opt.aliases?.includes(rawSubcommandName); + } + return false; + }) as SubCommand) + : parent; + + return { + command, + fullCommandName, + parent, + }; +} + +export async function onMessageCreate( + self: Client | WorkerClient, + rawMessage: GatewayMessageCreateDispatchData, + shardId: number, +) { + if (!self.options?.commands?.prefix) return; + const message = new Message(self, rawMessage); + const prefixes = (await self.options.commands.prefix(message)).sort((a, b) => b.length - a.length); + const prefix = prefixes.find(x => message.content.startsWith(x)); + + if (!(prefix && message.content.startsWith(prefix))) return; + + const content = message.content.slice(prefix.length).trimStart(); + const { fullCommandName, command, parent } = getCommandFromContent( + content + .split(' ') + .filter(x => x) + .slice(0, 3), + self, + ); + + if (!command) return; + if (!command.run) return self.logger.warn(`${fullCommandName} command does not have 'run' callback`); + + if (!(command.contexts?.includes(InteractionContextType.BotDM) || message.guildId)) return; + if (command.guildId && !command.guildId?.includes(message.guildId!)) return; + + const resolved: MakeRequired = { + channels: {}, + roles: {}, + users: {}, + members: {}, + attachments: {}, + }; + + let newContent = content; + for (const i of fullCommandName.split(' ')) { + newContent = newContent.slice(newContent.indexOf(i) + i.length); + } + + const args = self.options.commands.argsParser(newContent.slice(1), command, message); + const { options, errors } = await self.options.commands.optionsParser(self, command, rawMessage, args, resolved); + const optionsResolver = new OptionResolver(self, options, parent as Command, message.guildId, resolved); + const context = new CommandContext(self, message, optionsResolver, shardId, command); + //@ts-expect-error + const extendContext = self.options?.context?.(message) ?? {}; + Object.assign(context, extendContext); + try { + if (errors.length) { + return command.onOptionsError?.( + context, + Object.fromEntries( + errors.map(x => { + return [ + x.name, + { + failed: true, + value: x.error, + }, + ]; + }), + ), + ); + } + if (command.defaultMemberPermissions && message.guildId) { + const memberPermissions = await self.members.permissions(message.guildId, message.author.id); + const permissions = memberPermissions.missings(...memberPermissions.values([command.defaultMemberPermissions])); + if ( + !memberPermissions.has('Administrator') && + permissions.length && + (await message.guild())!.ownerId !== message.author.id + ) { + return command.onPermissionsFail?.(context, memberPermissions.keys(permissions)); + } + } + if (command.botPermissions && message.guildId) { + const meMember = await self.cache.members?.get(self.botId, message.guildId); + if (!meMember) return; //enable member cache and "Guilds" intent, lol + const appPermissions = await meMember.fetchPermissions(); + const permissions = appPermissions.missings(...appPermissions.values([command.botPermissions])); + if (!appPermissions.has('Administrator') && permissions.length) { + return command.onBotPermissionsFail?.(context, appPermissions.keys(permissions)); + } + } + const [erroredOptions, result] = await command.__runOptions(context, optionsResolver); + if (erroredOptions) { + return command.onOptionsError?.(context, result); + } + const resultRunGlobalMiddlewares = await command.__runGlobalMiddlewares(context); + if (resultRunGlobalMiddlewares.pass) { + return; + } + if ('error' in resultRunGlobalMiddlewares) { + return command.onMiddlewaresError?.(context, resultRunGlobalMiddlewares.error ?? 'Unknown error'); + } + const resultRunMiddlewares = await command.__runMiddlewares(context); + if (resultRunMiddlewares.pass) { + return; + } + if ('error' in resultRunMiddlewares) { + return command.onMiddlewaresError?.(context, resultRunMiddlewares.error ?? 'Unknown error'); + } + + try { + await command.run?.(context); + await command.onAfterRun?.(context, undefined); + } catch (error) { + await command.onRunError?.(context, error); + await command.onAfterRun?.(context, error); + } + } catch (error) { + try { + await command.onInternalError?.(self, context.command, error); + } catch { + // supress error + } + } +} + +export async function defaultParseOptions( + self: UsingClient, + command: Command | SubCommand, + message: GatewayMessageCreateDispatchData, + args: Partial>, + resolved: MakeRequired, +) { + const options: APIApplicationCommandInteractionDataOption[] = []; + const errors: { name: string; error: string; fullError: MessageCommandOptionErrors }[] = []; + for (const i of (command.options ?? []) as (CommandOption & { type: ApplicationCommandOptionType })[]) { + try { + let value: string | boolean | number | undefined; + let indexAttachment = -1; + switch (i.type) { + case ApplicationCommandOptionType.Attachment: + if (message.attachments[++indexAttachment]) { + value = message.attachments[indexAttachment].id; + resolved.attachments[value] = message.attachments[indexAttachment]; + } + break; + case ApplicationCommandOptionType.Boolean: + if (args[i.name]) { + value = ['yes', 'y', 'true', 'treu'].includes(args[i.name]!.toLowerCase()); + } + break; + case ApplicationCommandOptionType.Channel: + { + 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) { + const channel = + (await self.cache.channels?.get(rawId)) ?? (i.required ? await self.channels.fetch(rawId) : undefined); + if (channel) { + if ('channel_types' in i) { + if (!(i as SeyfertChannelOption).channel_types!.includes(channel.type)) { + errors.push({ + name: i.name, + error: `The entered channel type is not one of ${(i as SeyfertChannelOption) + .channel_types!.map(t => ChannelType[t]) + .join(', ')}`, + fullError: ['CHANNEL_TYPES', (i as SeyfertChannelOption).channel_types!], + }); + break; + } + } + value = rawId; + resolved.channels[rawId] = channel; + } + } + } + break; + case ApplicationCommandOptionType.Mentionable: + { + const matches = message.content.match(/<@[0-9]{17,19}(?=>)|<@&[0-9]{17,19}(?=>)/g) ?? []; + for (const match of matches) { + if (match.includes('&')) { + const rawId = match.slice(3); + if (rawId) { + const role = + (await self.cache.roles?.get(rawId)) ?? + (i.required ? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) : undefined); + if (role) { + value = rawId; + resolved.roles[rawId] = role; + break; + } + } + } else { + const rawId = match.slice(2); + const raw = message.mentions.find(x => rawId === x.id); + if (raw) { + const { member, ...user } = raw; + value = raw.id; + resolved.users[raw.id] = user; + if (member) resolved.members[raw.id] = member; + break; + } + } + } + } + break; + case ApplicationCommandOptionType.Role: + { + const rawId = + message.mention_roles.find(x => args[i.name]?.includes(x)) || args[i.name]?.match(/[0-9]{17,19}/g)?.[0]; + if (rawId) { + const role = + (await self.cache.roles?.get(rawId)) ?? + (i.required ? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) : undefined); + + if (role) { + value = rawId; + resolved.roles[rawId] = role; + } + } + } + break; + case ApplicationCommandOptionType.User: + { + 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) { + value = raw.id; + if (raw instanceof User) { + 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; + case ApplicationCommandOptionType.String: + { + value = args[i.name]; + const option = i as SeyfertStringOption; + if (!value) break; + if (option.min_length) { + if (value.length < option.min_length) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered string has less than ${option.min_length} characters. The minimum required is ${option.min_length} characters.`, + fullError: ['STRING_MIN_LENGTH', option.min_length], + }); + break; + } + } + if (option.max_length) { + if (value.length > option.max_length) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered string has more than ${option.max_length} characters. The maximum required is ${option.max_length} characters.`, + fullError: ['STRING_MAX_LENGTH', option.max_length], + }); + break; + } + } + if (option.choices?.length) { + const choice = option.choices.find(x => x.name === value); + if (!choice) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered choice is invalid. Please choose one of the following options: ${option.choices + .map(x => x.name) + .join(', ')}.`, + fullError: ['STRING_INVALID_CHOICE', option.choices], + }); + break; + } + value = choice.value; + } + } + break; + case ApplicationCommandOptionType.Number: + case ApplicationCommandOptionType.Integer: + { + const option = i as SeyfertNumberOption | SeyfertIntegerOption; + if (!option.choices?.length) { + value = Number(args[i.name]); + if (args[i.name] === undefined) { + value = undefined; + break; + } + if (Number.isNaN(value)) { + value = undefined; + errors.push({ + name: i.name, + error: 'The entered choice is an invalid number.', + fullError: ['NUMBER_NAN', args[i.name]], + }); + break; + } + if (option.min_value) { + if (value < option.min_value) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered number is less than ${option.min_value}. The minimum allowed is ${option.min_value}`, + fullError: ['NUMBER_MIN_VALUE', option.min_value], + }); + break; + } + } + if (option.max_value) { + if (value > option.max_value) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered number is greater than ${option.max_value}. The maximum allowed is ${option.max_value}`, + fullError: ['NUMBER_MAX_VALUE', option.max_value], + }); + break; + } + } + break; + } + const choice = option.choices.find(x => x.name === args[i.name]); + if (!choice) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered choice is invalid. Please choose one of the following options: ${option.choices + .map(x => x.name) + .join(', ')}.`, + fullError: ['NUMBER_INVALID_CHOICE', option.choices], + }); + break; + } + value = choice.value; + } + break; + default: + break; + } + if (value !== undefined) { + options.push({ + name: i.name, + type: i.type, + value, + } as APIApplicationCommandInteractionDataOption); + } else if (i.required) + if (!errors.some(x => x.name === i.name)) + errors.push({ + error: 'Option is required but returned undefined', + name: i.name, + fullError: ['OPTION_REQUIRED'], + }); + } catch (e) { + errors.push({ + error: e && typeof e === 'object' && 'message' in e ? (e.message as string) : `${e}`, + name: i.name, + fullError: ['UNKNOWN', e], + }); + } + } + + return { errors, options }; +} + +export function defaultArgsParser(content: string) { + const args: Record = {}; + for (const i of content.match(/-(.*?)(?=\s-|$)/gs) ?? []) { + args[i.slice(1).split(' ')[0]] = i.split(' ').slice(1).join(' '); + } + return args; +} + +//-(.*?)(?=\s-|$)/gs +//-(?[^-]*)/gm diff --git a/src/commands/applications/chat.ts b/src/commands/applications/chat.ts index c877d51..2eb6e8f 100644 --- a/src/commands/applications/chat.ts +++ b/src/commands/applications/chat.ts @@ -31,6 +31,7 @@ import type { StopFunction, UsingClient, } from './shared'; +import { inspect } from 'node:util'; export interface ReturnOptionsTypes { 1: never; // subcommand @@ -153,9 +154,15 @@ export class BaseCommand { const value = resolver.getHoisted(i.name)?.value !== undefined ? await new Promise( - (res, rej) => - option.value?.({ context: ctx, value: resolver.getValue(i.name) } as never, res, rej) || - res(resolver.getValue(i.name)), + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: yes + async (res, rej) => { + try { + (await option.value?.({ context: ctx, value: resolver.getValue(i.name) } as never, res, rej)) || + res(resolver.getValue(i.name)); + } catch (e) { + rej(e); + } + }, ) : undefined; if (value === undefined) { @@ -178,7 +185,7 @@ export class BaseCommand { errored = true; data[i.name] = { failed: true, - value: e instanceof Error ? e.message : `${e}`, + value: e instanceof Error ? e.message : typeof e === 'string' ? e : inspect(e), }; } } diff --git a/src/commands/applications/shared.ts b/src/commands/applications/shared.ts index 4ebb1a8..56d00c2 100644 --- a/src/commands/applications/shared.ts +++ b/src/commands/applications/shared.ts @@ -1,59 +1,74 @@ -import type { BaseClient } from '../../client/base'; -import type { IsStrictlyUndefined } from '../../common'; -import type { RegisteredMiddlewares } from '../decorators'; - -export type OKFunction = (value: T) => void; -export type StopFunction = (error: string) => void; -export type NextFunction = IsStrictlyUndefined extends true ? () => void : (data: T) => void; -export type PassFunction = () => void; - -export type InferWithPrefix = InternalOptions extends { withPrefix: infer P } ? P : false; - -export interface GlobalMetadata {} -export interface DefaultLocale {} -export interface ExtendContext {} -export interface UsingClient extends BaseClient {} -export type ParseClient = T; -export interface InternalOptions {} - -export type MiddlewareContext = (context: { - context: C; - next: NextFunction; - stop: StopFunction; - pass: PassFunction; -}) => any; -export type MetadataMiddleware = IsStrictlyUndefined< - Parameters[0]['next']>[0] -> extends true - ? never - : Parameters[0]['next']>[0]; -export type CommandMetadata = T extends readonly [ - infer first, - ...infer rest, -] - ? first extends keyof RegisteredMiddlewares - ? (MetadataMiddleware extends never - ? {} - : { - [key in first]: MetadataMiddleware; - }) & - (rest extends readonly (keyof RegisteredMiddlewares)[] ? CommandMetadata : {}) - : {} - : {}; - -export type OnOptionsReturnObject = Record< - string, - | { - failed: false; - value: unknown; - } - | { - failed: true; - value: string; - } ->; - -export enum IgnoreCommand { - Slash = 0, - Message = 1, -} +import type { ChannelType } from 'discord-api-types/v10'; +import type { BaseClient } from '../../client/base'; +import type { IsStrictlyUndefined } from '../../common'; +import type { RegisteredMiddlewares } from '../decorators'; + +export type OKFunction = (value: T) => void; +export type StopFunction = (error: string) => void; +export type NextFunction = IsStrictlyUndefined extends true ? () => void : (data: T) => void; +export type PassFunction = () => void; + +export type InferWithPrefix = InternalOptions extends { withPrefix: infer P } ? P : false; + +export interface GlobalMetadata {} +export interface DefaultLocale {} +export interface ExtendContext {} +export interface UsingClient extends BaseClient {} +export type ParseClient = T; +export interface InternalOptions {} + +export type MiddlewareContext = (context: { + context: C; + next: NextFunction; + stop: StopFunction; + pass: PassFunction; +}) => any; +export type MetadataMiddleware = IsStrictlyUndefined< + Parameters[0]['next']>[0] +> extends true + ? never + : Parameters[0]['next']>[0]; +export type CommandMetadata = T extends readonly [ + infer first, + ...infer rest, +] + ? first extends keyof RegisteredMiddlewares + ? (MetadataMiddleware extends never + ? {} + : { + [key in first]: MetadataMiddleware; + }) & + (rest extends readonly (keyof RegisteredMiddlewares)[] ? CommandMetadata : {}) + : {} + : {}; + +export type MessageCommandOptionErrors = + | ['CHANNEL_TYPES', type: ChannelType[]] + | ['STRING_MIN_LENGTH', min: number] + | ['STRING_MAX_LENGTH', max: number] + | ['STRING_INVALID_CHOICE', choices: readonly { name: string; value: string }[]] + | ['NUMBER_NAN', value: string | undefined] + | ['NUMBER_MIN_VALUE', min: number] + | ['NUMBER_MAX_VALUE', max: number] + | ['NUMBER_INVALID_CHOICE', choices: readonly { name: string; value: number }[]] + | ['OPTION_REQUIRED'] + | ['UNKNOWN', error: unknown]; + +export type OnOptionsReturnObject = Record< + string, + | { + failed: false; + value: unknown; + } + | { + failed: true; + value: string; + parseError?: //only for text command + MessageCommandOptionErrors; + } +>; + +export enum IgnoreCommand { + Slash = 0, + Message = 1, +}