feat: optionsParser option

This commit is contained in:
MARCROCK22 2024-06-02 20:21:18 +00:00
parent 5f61e8e66b
commit 2737b8f426
4 changed files with 604 additions and 525 deletions

View File

@ -1,6 +1,29 @@
import { GatewayIntentBits, type GatewayDispatchPayload, type GatewayPresenceUpdateData } from 'discord-api-types/v10'; import {
import type { Command, CommandContext, Message, SubCommand } from '..'; type APIApplicationCommandInteractionDataOption,
import { lazyLoadPackage, type DeepPartial, type If, type WatcherPayload, type WatcherSendToShard } from '../common'; 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 { EventHandler } from '../events';
import { ClientUser } from '../structures'; import { ClientUser } from '../structures';
import { ShardManager, properties, type ShardManagerOptions } from '../websocket'; 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 type { BaseClientOptions, InternalRuntimeConfig, ServicesOptions, StartOptions } from './base';
import { BaseClient } from './base'; import { BaseClient } from './base';
import { onInteractionCreate } from './oninteractioncreate'; import { onInteractionCreate } from './oninteractioncreate';
import { onMessageCreate } from './onmessagecreate'; import { defaultArgsParser, defaultParseOptions, onMessageCreate } from './onmessagecreate';
import { Collectors } from './collectors'; import { Collectors } from './collectors';
let parentPort: import('node:worker_threads').MessagePort; let parentPort: import('node:worker_threads').MessagePort;
@ -18,7 +41,9 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
private __handleGuilds?: Set<string> = new Set(); private __handleGuilds?: Set<string> = new Set();
gateway!: ShardManager; gateway!: ShardManager;
me!: If<Ready, ClientUser>; me!: If<Ready, ClientUser>;
declare options: ClientOptions; declare options: Omit<ClientOptions, 'commands'> & {
commands: MakeRequired<NonNullable<ClientOptions['commands']>, 'argsParser' | 'optionsParser'>;
};
memberUpdateHandler = new MemberUpdateHandler(); memberUpdateHandler = new MemberUpdateHandler();
presenceUpdateHandler = new PresenceUpdateHandler(); presenceUpdateHandler = new PresenceUpdateHandler();
collectors = new Collectors(); collectors = new Collectors();
@ -26,6 +51,12 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
constructor(options?: ClientOptions) { constructor(options?: ClientOptions) {
super(options); super(options);
this.options = MergeOptions(this.options, {
commands: {
argsParser: defaultArgsParser,
optionsParser: defaultParseOptions,
},
} satisfies ClientOptions);
} }
setServices({ setServices({
@ -212,6 +243,20 @@ export interface ClientOptions extends BaseClientOptions {
deferReplyResponse?: (ctx: CommandContext) => Parameters<Message['write']>[0]; deferReplyResponse?: (ctx: CommandContext) => Parameters<Message['write']>[0];
reply?: (ctx: CommandContext) => boolean; reply?: (ctx: CommandContext) => boolean;
argsParser?: (content: string, command: SubCommand | Command, message: Message) => Record<string, string>; argsParser?: (content: string, command: SubCommand | Command, message: Message) => Record<string, string>;
optionsParser?: (
self: UsingClient,
command: Command | SubCommand,
message: GatewayMessageCreateDispatchData,
args: Partial<Record<string, string>>,
resolved: MakeRequired<ContextOptionsResolved>,
) => Awaitable<{
errors: {
name: string;
error: string;
fullError: MessageCommandOptionErrors;
}[];
options: APIApplicationCommandInteractionDataOption[];
}>;
}; };
handlePayload?: ShardManagerOptions['handlePayload']; handlePayload?: ShardManagerOptions['handlePayload'];
} }

View File

@ -9,9 +9,11 @@ import {
Command, Command,
CommandContext, CommandContext,
IgnoreCommand, IgnoreCommand,
type MessageCommandOptionErrors,
OptionResolver, OptionResolver,
SubCommand, SubCommand,
User, User,
type UsingClient,
type Client, type Client,
type CommandOption, type CommandOption,
type ContextOptionsResolved, type ContextOptionsResolved,
@ -119,14 +121,30 @@ export async function onMessageCreate(
newContent = newContent.slice(newContent.indexOf(i) + i.length); newContent = newContent.slice(newContent.indexOf(i) + i.length);
} }
const args = (self.options?.commands?.argsParser ?? defaultArgsParser)(newContent.slice(1), command, message); const args = self.options.commands.argsParser(newContent.slice(1), command, message);
const { options, errors } = await parseOptions(self, command, rawMessage, args, resolved); 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 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 //@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 {
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) { if (command.defaultMemberPermissions && message.guildId) {
const memberPermissions = await self.members.permissions(message.guildId, message.author.id); const memberPermissions = await self.members.permissions(message.guildId, message.author.id);
const permissions = memberPermissions.missings(...memberPermissions.values([command.defaultMemberPermissions])); const permissions = memberPermissions.missings(...memberPermissions.values([command.defaultMemberPermissions]));
@ -147,22 +165,6 @@ export async function onMessageCreate(
return command.onBotPermissionsFail?.(context, appPermissions.keys(permissions)); 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); const [erroredOptions, result] = await command.__runOptions(context, optionsResolver);
if (erroredOptions) { if (erroredOptions) {
return command.onOptionsError?.(context, result); return command.onOptionsError?.(context, result);
@ -198,15 +200,15 @@ export async function onMessageCreate(
} }
} }
async function parseOptions( export async function defaultParseOptions(
self: Client | WorkerClient, self: UsingClient,
command: Command | SubCommand, command: Command | SubCommand,
message: GatewayMessageCreateDispatchData, message: GatewayMessageCreateDispatchData,
args: Partial<Record<string, string>>, args: Partial<Record<string, string>>,
resolved: MakeRequired<ContextOptionsResolved>, resolved: MakeRequired<ContextOptionsResolved>,
) { ) {
const options: APIApplicationCommandInteractionDataOption[] = []; const options: APIApplicationCommandInteractionDataOption[] = [];
const errors: { name: string; error: string }[] = []; const errors: { name: string; error: string; fullError: MessageCommandOptionErrors }[] = [];
for (const i of (command.options ?? []) as (CommandOption & { type: ApplicationCommandOptionType })[]) { for (const i of (command.options ?? []) as (CommandOption & { type: ApplicationCommandOptionType })[]) {
try { try {
let value: string | boolean | number | undefined; let value: string | boolean | number | undefined;
@ -239,6 +241,7 @@ async function parseOptions(
error: `The entered channel type is not one of ${(i as SeyfertChannelOption) error: `The entered channel type is not one of ${(i as SeyfertChannelOption)
.channel_types!.map(t => ChannelType[t]) .channel_types!.map(t => ChannelType[t])
.join(', ')}`, .join(', ')}`,
fullError: ['CHANNEL_TYPES', (i as SeyfertChannelOption).channel_types!],
}); });
break; break;
} }
@ -335,6 +338,7 @@ async function parseOptions(
errors.push({ errors.push({
name: i.name, name: i.name,
error: `The entered string has less than ${option.min_length} characters. The minimum required is ${option.min_length} characters.`, 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; break;
} }
@ -345,6 +349,7 @@ async function parseOptions(
errors.push({ errors.push({
name: i.name, name: i.name,
error: `The entered string has more than ${option.max_length} characters. The maximum required is ${option.max_length} characters.`, 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; break;
} }
@ -358,6 +363,7 @@ async function parseOptions(
error: `The entered choice is invalid. Please choose one of the following options: ${option.choices error: `The entered choice is invalid. Please choose one of the following options: ${option.choices
.map(x => x.name) .map(x => x.name)
.join(', ')}.`, .join(', ')}.`,
fullError: ['STRING_INVALID_CHOICE', option.choices],
}); });
break; break;
} }
@ -380,6 +386,7 @@ async function parseOptions(
errors.push({ errors.push({
name: i.name, name: i.name,
error: 'The entered choice is an invalid number.', error: 'The entered choice is an invalid number.',
fullError: ['NUMBER_NAN', args[i.name]],
}); });
break; break;
} }
@ -389,6 +396,7 @@ async function parseOptions(
errors.push({ errors.push({
name: i.name, name: i.name,
error: `The entered number is less than ${option.min_value}. The minimum allowed is ${option.min_value}`, 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; break;
} }
@ -399,6 +407,7 @@ async function parseOptions(
errors.push({ errors.push({
name: i.name, name: i.name,
error: `The entered number is greater than ${option.max_value}. The maximum allowed is ${option.max_value}`, 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;
} }
@ -413,6 +422,7 @@ async function parseOptions(
error: `The entered choice is invalid. Please choose one of the following options: ${option.choices error: `The entered choice is invalid. Please choose one of the following options: ${option.choices
.map(x => x.name) .map(x => x.name)
.join(', ')}.`, .join(', ')}.`,
fullError: ['NUMBER_INVALID_CHOICE', option.choices],
}); });
break; break;
} }
@ -433,11 +443,13 @@ async function parseOptions(
errors.push({ errors.push({
error: 'Option is required but returned undefined', error: 'Option is required but returned undefined',
name: i.name, name: i.name,
fullError: ['OPTION_REQUIRED'],
}); });
} catch (e) { } catch (e) {
errors.push({ errors.push({
error: e && typeof e === 'object' && 'message' in e ? (e.message as string) : `${e}`, error: e && typeof e === 'object' && 'message' in e ? (e.message as string) : `${e}`,
name: i.name, name: i.name,
fullError: ['UNKNOWN', e],
}); });
} }
} }
@ -445,7 +457,7 @@ async function parseOptions(
return { errors, options }; return { errors, options };
} }
function defaultArgsParser(content: string) { export function defaultArgsParser(content: string) {
const args: Record<string, string> = {}; const args: Record<string, string> = {};
for (const i of content.match(/-(.*?)(?=\s-|$)/gs) ?? []) { for (const i of content.match(/-(.*?)(?=\s-|$)/gs) ?? []) {
args[i.slice(1).split(' ')[0]] = i.split(' ').slice(1).join(' '); args[i.slice(1).split(' ')[0]] = i.split(' ').slice(1).join(' ');

View File

@ -31,6 +31,7 @@ import type {
StopFunction, StopFunction,
UsingClient, UsingClient,
} from './shared'; } from './shared';
import { inspect } from 'node:util';
export interface ReturnOptionsTypes { export interface ReturnOptionsTypes {
1: never; // subcommand 1: never; // subcommand
@ -153,9 +154,15 @@ export class BaseCommand {
const value = const value =
resolver.getHoisted(i.name)?.value !== undefined resolver.getHoisted(i.name)?.value !== undefined
? await new Promise( ? await new Promise(
(res, rej) => // biome-ignore lint/suspicious/noAsyncPromiseExecutor: yes
option.value?.({ context: ctx, value: resolver.getValue(i.name) } as never, res, rej) || async (res, rej) => {
res(resolver.getValue(i.name)), 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; : undefined;
if (value === undefined) { if (value === undefined) {
@ -178,7 +185,7 @@ export class BaseCommand {
errored = true; errored = true;
data[i.name] = { data[i.name] = {
failed: true, failed: true,
value: e instanceof Error ? e.message : `${e}`, value: e instanceof Error ? e.message : typeof e === 'string' ? e : inspect(e),
}; };
} }
} }

View File

@ -1,3 +1,4 @@
import type { ChannelType } from 'discord-api-types/v10';
import type { BaseClient } from '../../client/base'; import type { BaseClient } from '../../client/base';
import type { IsStrictlyUndefined } from '../../common'; import type { IsStrictlyUndefined } from '../../common';
import type { RegisteredMiddlewares } from '../decorators'; import type { RegisteredMiddlewares } from '../decorators';
@ -41,6 +42,18 @@ export type CommandMetadata<T extends readonly (keyof RegisteredMiddlewares)[]>
: {} : {}
: {}; : {};
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< export type OnOptionsReturnObject = Record<
string, string,
| { | {
@ -50,6 +63,8 @@ export type OnOptionsReturnObject = Record<
| { | {
failed: true; failed: true;
value: string; value: string;
parseError?: //only for text command
MessageCommandOptionErrors;
} }
>; >;