From 23422cadfad93436db13770bf0a9db0022ce2bca Mon Sep 17 00:00:00 2001 From: MARCROCK22 <57925328+MARCROCK22@users.noreply.github.com> Date: Fri, 10 May 2024 17:26:16 -0400 Subject: [PATCH] feat: componentcommand and modalcommand now can use middlewares --- src/client/oninteractioncreate.ts | 7 +- src/commands/applications/menucontext.ts | 2 +- src/commands/applications/options.ts | 12 +- src/commands/basecontext.ts | 8 + src/components/command.ts | 33 ----- src/components/componentcommand.ts | 95 ++++++++++++ src/components/componentcontext.ts | 18 +-- src/components/handler.ts | 72 ++++++++- src/components/index.ts | 6 +- src/components/modalcommand.ts | 86 +++++++++++ src/components/modalcontext.ts | 178 +++++++++++++++++++++++ 11 files changed, 462 insertions(+), 55 deletions(-) delete mode 100644 src/components/command.ts create mode 100644 src/components/componentcommand.ts create mode 100644 src/components/modalcommand.ts create mode 100644 src/components/modalcontext.ts diff --git a/src/client/oninteractioncreate.ts b/src/client/oninteractioncreate.ts index 6aded2f..11bd03d 100644 --- a/src/client/oninteractioncreate.ts +++ b/src/client/oninteractioncreate.ts @@ -17,6 +17,7 @@ import type { } from '../structures'; import { AutocompleteInteraction, BaseInteraction } from '../structures'; import type { BaseClient } from './base'; +import { ModalContext } from '../components'; export async function onInteractionCreate( self: BaseClient, @@ -97,7 +98,7 @@ export async function onInteractionCreate( ...interaction.appPermissions.values([command.botPermissions]), ); if (!interaction.appPermissions.has('Administrator') && permissions.length) { - return command.onBotPermissionsFail?.(context, interaction.appPermissions.keys(permissions)); + return command.onBotPermissionsFail(context, interaction.appPermissions.keys(permissions)); } } const resultRunGlobalMiddlewares = await command.__runGlobalMiddlewares(context); @@ -120,7 +121,7 @@ export async function onInteractionCreate( await command.run(context); await command.onAfterRun?.(context, undefined); } catch (error) { - await command.onRunError?.(context, error); + await command.onRunError(context, error); await command.onAfterRun?.(context, error); } } catch (error) { @@ -208,7 +209,7 @@ export async function onInteractionCreate( if (self.components?.hasModal(interaction)) { await self.components.onModalSubmit(interaction); } else { - await self.components?.executeModal(interaction); + await self.components?.executeModal(new ModalContext(self, interaction)); } } break; diff --git a/src/commands/applications/menucontext.ts b/src/commands/applications/menucontext.ts index f508265..293ba9d 100644 --- a/src/commands/applications/menucontext.ts +++ b/src/commands/applications/menucontext.ts @@ -60,7 +60,7 @@ export class MenuCommandContext< } get t() { - return this.client.langs!.get(this.interaction.locale ?? this.client.langs!.defaultLang ?? 'en-US'); + return this.client.t(this.interaction.locale ?? this.client.langs!.defaultLang ?? 'en-US'); } get fullCommandName() { diff --git a/src/commands/applications/options.ts b/src/commands/applications/options.ts index 6809f24..c12b035 100644 --- a/src/commands/applications/options.ts +++ b/src/commands/applications/options.ts @@ -13,6 +13,8 @@ import type { import type { MessageCommandInteraction, UserCommandInteraction } from '../../structures'; import type { CommandContext } from './chatcontext'; import type { MiddlewareContext } from './shared'; +import type { ModalContext } from '../../components'; +import type { ComponentContext } from '../../components/componentcontext'; export type SeyfertBasicOption = __TypesWrapper[T] & D; @@ -86,9 +88,15 @@ export function createAttachmentOption | UserCommandInteraction> = + C extends | CommandContext - | MenuCommandContext | UserCommandInteraction>, + | MenuCommandContext | UserCommandInteraction> + | ComponentContext + | ModalContext = + | CommandContext + | MenuCommandContext | UserCommandInteraction> + | ComponentContext + | ModalContext, >(data: MiddlewareContext) { return data; } diff --git a/src/commands/basecontext.ts b/src/commands/basecontext.ts index cac0eef..b92e8bc 100644 --- a/src/commands/basecontext.ts +++ b/src/commands/basecontext.ts @@ -1,3 +1,4 @@ +import type { ModalContext } from '../components'; import type { ContextComponentCommandInteractionMap, ComponentContext } from '../components/componentcontext'; import type { MessageCommandInteraction, UserCommandInteraction } from '../structures'; import type { CommandContext } from './applications/chatcontext'; @@ -7,6 +8,9 @@ import type { UsingClient } from './applications/shared'; export class BaseContext { constructor(readonly client: UsingClient) {} + /** + * Gets the proxy object. + */ get proxy() { return this.client.proxy; } @@ -30,4 +34,8 @@ export class BaseContext { isComponent(): this is ComponentContext { return false; } + + isModal(): this is ModalContext { + return false; + } } diff --git a/src/components/command.ts b/src/components/command.ts deleted file mode 100644 index 87d5751..0000000 --- a/src/components/command.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ComponentType } from 'discord-api-types/v10'; -import type { ModalSubmitInteraction } from '../structures'; -import type { ContextComponentCommandInteractionMap, ComponentContext } from './componentcontext'; - -export const InteractionCommandType = { - COMPONENT: 0, - MODAL: 1, -} as const; - -export interface ComponentCommand { - __filePath?: string; -} - -export abstract class ComponentCommand { - type = InteractionCommandType.COMPONENT; - abstract componentType: keyof ContextComponentCommandInteractionMap; - abstract filter(interaction: ComponentContext): Promise | boolean; - abstract run(interaction: ComponentContext): any; - - get cType(): number { - return ComponentType[this.componentType]; - } -} - -export interface ModalCommand { - __filePath?: string; -} - -export abstract class ModalCommand { - type = InteractionCommandType.MODAL; - abstract filter(interaction: ModalSubmitInteraction): Promise | boolean; - abstract run(interaction: ModalSubmitInteraction): any; -} diff --git a/src/components/componentcommand.ts b/src/components/componentcommand.ts new file mode 100644 index 0000000..cdf744e --- /dev/null +++ b/src/components/componentcommand.ts @@ -0,0 +1,95 @@ +import { ComponentType } from 'discord-api-types/v10'; +import type { ContextComponentCommandInteractionMap, ComponentContext } from './componentcontext'; +import type { PassFunction, RegisteredMiddlewares, StopFunction, UsingClient } from '../commands'; + +export const InteractionCommandType = { + COMPONENT: 0, + MODAL: 1, +} as const; + +export interface ComponentCommand { + __filePath?: string; +} + +export abstract class ComponentCommand { + type = InteractionCommandType.COMPONENT; + abstract componentType: keyof ContextComponentCommandInteractionMap; + abstract filter(context: ComponentContext): Promise | boolean; + abstract run(context: ComponentContext): any; + + get cType(): number { + return ComponentType[this.componentType]; + } + + onAfterRun?(context: ComponentContext, error: unknown | undefined): any; + onRunError(context: ComponentContext, error: unknown): any { + context.client.logger.fatal('ComponentCommand.', context.author.id, error); + } + onMiddlewaresError(context: ComponentContext, error: string): any { + context.client.logger.fatal('ComponentCommand.', context.author.id, error); + } + onInternalError(client: UsingClient, error?: unknown): any { + client.logger.fatal(error); + } + + middlewares: (keyof RegisteredMiddlewares)[] = []; + /** @internal */ + static __runMiddlewares( + context: ComponentContext, + middlewares: (keyof RegisteredMiddlewares)[], + global: boolean, + ): Promise<{ error?: string; pass?: boolean }> { + if (!middlewares.length) { + return Promise.resolve({}); + } + let index = 0; + + return new Promise(res => { + let running = true; + const pass: PassFunction = () => { + if (!running) { + return; + } + running = false; + return res({ pass: true }); + }; + function next(obj: any) { + if (!running) { + return; + } + // biome-ignore lint/style/noArguments: yes + if (arguments.length) { + // @ts-expect-error + context[global ? 'globalMetadata' : 'metadata'][middlewares[index]] = obj; + } + if (++index >= middlewares.length) { + running = false; + return res({}); + } + context.client.middlewares![middlewares[index]]({ context, next, stop, pass }); + } + const stop: StopFunction = err => { + if (!running) { + return; + } + running = false; + return res({ error: err }); + }; + context.client.middlewares![middlewares[0]]({ context, next, stop, pass }); + }); + } + + /** @internal */ + __runMiddlewares(context: ComponentContext) { + return ComponentCommand.__runMiddlewares(context, this.middlewares as (keyof RegisteredMiddlewares)[], false); + } + + /** @internal */ + __runGlobalMiddlewares(context: ComponentContext) { + return ComponentCommand.__runMiddlewares( + context, + (context.client.options?.globalMiddlewares ?? []) as (keyof RegisteredMiddlewares)[], + true, + ); + } +} diff --git a/src/components/componentcontext.ts b/src/components/componentcontext.ts index 8cdb314..053e3e0 100644 --- a/src/components/componentcontext.ts +++ b/src/components/componentcontext.ts @@ -13,13 +13,14 @@ import type { UserSelectMenuInteraction, WebhookMessage, } from '..'; -import type { ExtendContext, UsingClient } from '../commands'; +import type { CommandMetadata, ExtendContext, GlobalMetadata, RegisteredMiddlewares, UsingClient } from '../commands'; import { BaseContext } from '../commands/basecontext'; import type { ComponentInteractionMessageUpdate, InteractionCreateBodyRequest, InteractionMessageUpdateBodyRequest, ModalCreateBodyRequest, + UnionToTuple, When, } from '../common'; @@ -32,7 +33,10 @@ export interface ComponentContext< * Represents a context for interacting with components in a Discord bot. * @template Type - The type of component interaction. */ -export class ComponentContext extends BaseContext { +export class ComponentContext< + Type extends keyof ContextComponentCommandInteractionMap, + M extends keyof RegisteredMiddlewares = never, +> extends BaseContext { /** * Creates a new instance of the ComponentContext class. * @param client - The UsingClient instance. @@ -45,18 +49,14 @@ export class ComponentContext> = {} as never; + globalMetadata: GlobalMetadata = {}; /** * Gets the language object for the interaction's locale. */ get t() { - return this.client.langs!.get(this.interaction?.locale ?? this.client.langs?.defaultLang ?? 'en-US'); + return this.client.t(this.interaction?.locale ?? this.client.langs?.defaultLang ?? 'en-US'); } /** diff --git a/src/components/handler.ts b/src/components/handler.ts index 15d6a7a..3d729f3 100644 --- a/src/components/handler.ts +++ b/src/components/handler.ts @@ -3,8 +3,10 @@ import { LimitedCollection } from '../collection'; import type { UsingClient } from '../commands'; import { BaseHandler, magicImport, type Logger, type OnFailCallback } from '../common'; import type { ComponentInteraction, ModalSubmitInteraction, StringSelectMenuInteraction } from '../structures'; -import { ComponentCommand, InteractionCommandType, ModalCommand } from './command'; +import { ComponentCommand, InteractionCommandType } from './componentcommand'; import { ComponentContext } from './componentcontext'; +import { ModalCommand } from './modalcommand'; +import type { ModalContext } from './modalcontext'; type COMPONENTS = { components: { match: string | string[] | RegExp; callback: ComponentCallback }[]; @@ -212,7 +214,37 @@ export class ComponentHandler extends BaseHandler { const extended = this.client.options?.context?.(interaction) ?? {}; Object.assign(context, extended); if (!(await i.filter(context))) continue; - await i.run(context); + try { + const resultRunGlobalMiddlewares = await i.__runGlobalMiddlewares(context); + if (resultRunGlobalMiddlewares.pass) { + return; + } + if ('error' in resultRunGlobalMiddlewares) { + return i.onMiddlewaresError(context, resultRunGlobalMiddlewares.error ?? 'Unknown error'); + } + + const resultRunMiddlewares = await i.__runMiddlewares(context); + if (resultRunMiddlewares.pass) { + return; + } + if ('error' in resultRunMiddlewares) { + return i.onMiddlewaresError(context, resultRunMiddlewares.error ?? 'Unknown error'); + } + + try { + await i.run(context); + await i.onAfterRun?.(context, undefined); + } catch (error) { + await i.onRunError(context, error); + await i.onAfterRun?.(context, error); + } + } catch (error) { + try { + await i.onInternalError(this.client, error); + } catch { + // supress error + } + } break; } } catch (e) { @@ -221,11 +253,41 @@ export class ComponentHandler extends BaseHandler { } } - async executeModal(interaction: ModalSubmitInteraction) { + async executeModal(context: ModalContext) { for (const i of this.commands) { try { - if (i.type === InteractionCommandType.MODAL && (await i.filter(interaction))) { - await i.run(interaction); + if (i.type === InteractionCommandType.MODAL && (await i.filter(context))) { + try { + const resultRunGlobalMiddlewares = await i.__runGlobalMiddlewares(context); + if (resultRunGlobalMiddlewares.pass) { + return; + } + if ('error' in resultRunGlobalMiddlewares) { + return i.onMiddlewaresError(context, resultRunGlobalMiddlewares.error ?? 'Unknown error'); + } + + const resultRunMiddlewares = await i.__runMiddlewares(context); + if (resultRunMiddlewares.pass) { + return; + } + if ('error' in resultRunMiddlewares) { + return i.onMiddlewaresError(context, resultRunMiddlewares.error ?? 'Unknown error'); + } + + try { + await i.run(context); + await i.onAfterRun?.(context, undefined); + } catch (error) { + await i.onRunError(context, error); + await i.onAfterRun?.(context, error); + } + } catch (error) { + try { + await i.onInternalError(this.client, error); + } catch { + // supress error + } + } break; } } catch (e) { diff --git a/src/components/index.ts b/src/components/index.ts index b5fc5ee..87f8c84 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -20,8 +20,10 @@ export type MessageComponents = export type ActionRowMessageComponents = Exclude; -export * from './command'; -export * from './componentcontext'; +export * from './componentcommand'; +export * from './componentcommand'; +export * from './modalcommand'; +export * from './modalcontext'; /** * Return a new component instance based on the component type. diff --git a/src/components/modalcommand.ts b/src/components/modalcommand.ts new file mode 100644 index 0000000..6c5647e --- /dev/null +++ b/src/components/modalcommand.ts @@ -0,0 +1,86 @@ +import type { RegisteredMiddlewares, PassFunction, StopFunction, UsingClient } from '../commands'; +import { InteractionCommandType } from './componentcommand'; +import type { ModalContext } from './modalcontext'; + +export interface ModalCommand { + __filePath?: string; +} + +export abstract class ModalCommand { + type = InteractionCommandType.MODAL; + abstract filter(context: ModalContext): Promise | boolean; + abstract run(context: ModalContext): any; + + middlewares: (keyof RegisteredMiddlewares)[] = []; + + onAfterRun?(context: ModalContext, error: unknown | undefined): any; + onRunError(context: ModalContext, error: unknown): any { + context.client.logger.fatal('ComponentCommand.', context.author.id, error); + } + onMiddlewaresError(context: ModalContext, error: string): any { + context.client.logger.fatal('ComponentCommand.', context.author.id, error); + } + onInternalError(client: UsingClient, error?: unknown): any { + client.logger.fatal(error); + } + + /** @internal */ + static __runMiddlewares( + context: ModalContext, + middlewares: (keyof RegisteredMiddlewares)[], + global: boolean, + ): Promise<{ error?: string; pass?: boolean }> { + if (!middlewares.length) { + return Promise.resolve({}); + } + let index = 0; + + return new Promise(res => { + let running = true; + const pass: PassFunction = () => { + if (!running) { + return; + } + running = false; + return res({ pass: true }); + }; + function next(obj: any) { + if (!running) { + return; + } + // biome-ignore lint/style/noArguments: yes + if (arguments.length) { + // @ts-expect-error + context[global ? 'globalMetadata' : 'metadata'][middlewares[index]] = obj; + } + if (++index >= middlewares.length) { + running = false; + return res({}); + } + context.client.middlewares![middlewares[index]]({ context, next, stop, pass }); + } + const stop: StopFunction = err => { + if (!running) { + return; + } + running = false; + return res({ error: err }); + }; + context.client.middlewares![middlewares[0]]({ context, next, stop, pass }); + }); + } + + /** @internal */ + __runMiddlewares(context: ModalContext) { + return ModalCommand.__runMiddlewares(context, this.middlewares as (keyof RegisteredMiddlewares)[], false); + } + + /** @internal */ + __runGlobalMiddlewares(context: ModalContext) { + return ModalCommand.__runMiddlewares( + context, + (context.client.options?.globalMiddlewares ?? []) as (keyof RegisteredMiddlewares)[], + true, + ); + } +} diff --git a/src/components/modalcontext.ts b/src/components/modalcontext.ts new file mode 100644 index 0000000..270705b --- /dev/null +++ b/src/components/modalcontext.ts @@ -0,0 +1,178 @@ +import { MessageFlags } from 'discord-api-types/v10'; +import type { AllChannels, Guild, GuildMember, Message, ModalSubmitInteraction, ReturnCache, WebhookMessage } from '..'; +import type { CommandMetadata, ExtendContext, GlobalMetadata, RegisteredMiddlewares, UsingClient } from '../commands'; +import { BaseContext } from '../commands/basecontext'; +import type { + InteractionCreateBodyRequest, + InteractionMessageUpdateBodyRequest, + ModalCreateBodyRequest, + UnionToTuple, + When, +} from '../common'; + +export interface ModalContext extends BaseContext, ExtendContext {} + +/** + * Represents a context for interacting with components in a Discord bot. + * @template Type - The type of component interaction. + */ +export class ModalContext extends BaseContext { + /** + * Creates a new instance of the ComponentContext class. + * @param client - The UsingClient instance. + * @param interaction - The component interaction object. + */ + constructor( + readonly client: UsingClient, + public interaction: ModalSubmitInteraction, + ) { + super(client); + } + + metadata: CommandMetadata> = {} as never; + globalMetadata: GlobalMetadata = {}; + + get components() { + return this.interaction.components; + } + + /** + * Gets the language object for the interaction's locale. + */ + get t() { + return this.client.t(this.interaction?.locale ?? this.client.langs?.defaultLang ?? 'en-US'); + } + + /** + * Writes a response to the interaction. + * @param body - The body of the response. + * @param fetchReply - Whether to fetch the reply or not. + */ + write(body: InteractionCreateBodyRequest, fetchReply?: FR) { + return this.interaction.write(body, fetchReply); + } + + /** + * Defers the reply to the interaction. + * @param ephemeral - Whether the reply should be ephemeral or not. + */ + deferReply(ephemeral = false) { + return this.interaction.deferReply(ephemeral ? MessageFlags.Ephemeral : undefined); + } + + /** + * Edits the response of the interaction. + * @param body - The updated body of the response. + */ + editResponse(body: InteractionMessageUpdateBodyRequest) { + return this.interaction.editResponse(body); + } + + /** + * Edits the response or replies to the interaction. + * @param body - The body of the response or updated body of the interaction. + * @param fetchReply - Whether to fetch the reply or not. + */ + editOrReply( + body: InteractionCreateBodyRequest | InteractionMessageUpdateBodyRequest, + fetchReply?: FR, + ): Promise> { + return this.interaction.editOrReply(body as InteractionCreateBodyRequest, fetchReply); + } + + /** + * Deletes the response of the interaction. + * @returns A promise that resolves when the response is deleted. + */ + deleteResponse() { + return this.interaction.deleteResponse(); + } + + modal(body: ModalCreateBodyRequest) { + //@ts-expect-error + return this.interaction.modal(body); + } + + /** + * Gets the channel of the interaction. + * @param mode - The mode to fetch the channel. + * @returns A promise that resolves to the channel. + */ + channel(mode?: 'rest' | 'flow'): Promise; + channel(mode?: 'cache'): ReturnCache; + channel(mode: 'cache' | 'rest' | 'flow' = 'cache') { + if (this.interaction?.channel && mode === 'cache') + return this.client.cache.adapter.isAsync ? Promise.resolve(this.interaction.channel) : this.interaction.channel; + return this.client.channels.fetch(this.channelId, mode === 'rest'); + } + + /** + * Gets the bot member in the guild of the interaction. + * @param mode - The mode to fetch the member. + * @returns A promise that resolves to the bot member. + */ + me(mode?: 'rest' | 'flow'): Promise; + me(mode?: 'cache'): ReturnCache; + me(mode: 'cache' | 'rest' | 'flow' = 'cache') { + if (!this.guildId) + return mode === 'cache' ? (this.client.cache.adapter.isAsync ? Promise.resolve() : undefined) : Promise.resolve(); + switch (mode) { + case 'cache': + return this.client.cache.members?.get(this.client.botId, this.guildId); + default: + return this.client.members.fetch(this.guildId, this.client.botId, mode === 'rest'); + } + } + + /** + * Gets the guild of the interaction. + * @param mode - The mode to fetch the guild. + * @returns A promise that resolves to the guild. + */ + guild(mode?: 'rest' | 'flow'): Promise | undefined>; + guild(mode?: 'cache'): ReturnCache | undefined>; + guild(mode: 'cache' | 'rest' | 'flow' = 'cache') { + if (!this.guildId) + return ( + mode === 'cache' ? (this.client.cache.adapter.isAsync ? Promise.resolve() : undefined) : Promise.resolve() + ) as any; + switch (mode) { + case 'cache': + return this.client.cache.guilds?.get(this.guildId); + default: + return this.client.guilds.fetch(this.guildId, mode === 'rest'); + } + } + + /** + * Gets the ID of the guild of the interaction. + */ + get guildId() { + return this.interaction.guildId; + } + + /** + * Gets the ID of the channel of the interaction. + */ + get channelId() { + return this.interaction.channelId!; + } + + /** + * Gets the author of the interaction. + */ + get author() { + return this.interaction.user; + } + + /** + * Gets the member of the interaction. + */ + get member() { + return this.interaction.member; + } + + isModal(): this is ModalContext { + return true; + } +}