diff --git a/src/interactions/client.ts b/src/interactions/client.ts index 2ee6753e..b6ed5055 100644 --- a/src/interactions/client.ts +++ b/src/interactions/client.ts @@ -32,6 +32,7 @@ import { Message } from '../structures/message.ts' import { MessageComponentInteraction } from '../structures/messageComponents.ts' import { AutocompleteInteraction } from '../structures/autocompleteInteraction.ts' import { ModalSubmitInteraction } from '../structures/modalSubmitInteraction.ts' +import { MessageComponentType } from '../types/messageComponents.ts' export type ApplicationCommandHandlerCallback = ( interaction: ApplicationCommandInteraction @@ -51,6 +52,7 @@ export type { ApplicationCommandHandlerCallback as SlashCommandHandlerCallback } export type { ApplicationCommandHandler as SlashCommandHandler } export type AutocompleteHandlerCallback = (d: AutocompleteInteraction) => any +export type ComponentInteractionCallback = (d: T) => any export interface AutocompleteHandler { cmd: string @@ -60,6 +62,13 @@ export interface AutocompleteHandler { handler: AutocompleteHandlerCallback } +// deno-lint-ignore no-explicit-any +export interface ComponentInteractionHandler { + customID: string + handler: ComponentInteractionCallback + type: 'button' | 'modal' +} + /** Options for InteractionsClient */ export interface SlashOptions { id?: string | (() => string) @@ -96,6 +105,7 @@ export class InteractionsClient extends HarmonyEventEmitter { @@ -140,9 +151,17 @@ export class InteractionsClient extends HarmonyEventEmitter { + e.handler = e.handler.bind(this.client) + this.componentHandlers.push(e) + }) + } + const self = this as unknown as InteractionsClient & { _decoratedAppCmd: ApplicationCommandHandler[] _decoratedAutocomplete: AutocompleteHandler[] + _decoratedComponents: ComponentInteractionHandler[] } if (self._decoratedAppCmd !== undefined) { @@ -159,6 +178,13 @@ export class InteractionsClient extends HarmonyEventEmitter { + e.handler = e.handler.bind(this.client) + self.componentHandlers.push(e) + }) + } + Object.defineProperty(this, 'rest', { value: options.client === undefined @@ -385,6 +411,45 @@ export class InteractionsClient extends HarmonyEventEmitter e.components).flat() + ].find((e) => { + if (i.customID !== e.customID) return false + + if (i.isMessageComponent() === true) { + return ( + e.type === 'button' && + i.data.component_type === MessageComponentType.BUTTON + ) + } + + return false + }) + } + + /** Get Handler for an modal submit Interaction. */ + private _getModalSubmitHandler( + i: ModalSubmitInteraction + ): ComponentInteractionHandler | undefined { + return [ + ...this.componentHandlers, + ...this.modules.map((e) => e.components).flat() + ].find((e) => { + if (i.customID !== e.customID) return false + + if (e.type === 'modal' && i.isModalSubmit() === true) { + return true + } + + return false + }) + } + /** Process an incoming Interaction */ async _process( interaction: Interaction | ApplicationCommandInteraction @@ -409,6 +474,40 @@ export class InteractionsClient extends HarmonyEventEmitter e.components).flat() + ].find((e) => e.customID === '*' && e.type === 'button') + + try { + await handle?.handler(interaction) + } catch (e) { + await this.emit('interactionError', e as Error) + } + return + } + + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (interaction.isModalSubmit()) { + const handle = + this._getModalSubmitHandler(interaction) ?? + [ + ...this.componentHandlers, + ...this.modules.map((e) => e.components).flat() + ].find((e) => e.customID === '*' && e.type === 'modal') + + try { + await handle?.handler(interaction) + } catch (e) { + await this.emit('interactionError', e as Error) + } + return + } + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!interaction.isApplicationCommand()) return diff --git a/src/interactions/commandModule.ts b/src/interactions/commandModule.ts index 350ed008..d2e2c11e 100644 --- a/src/interactions/commandModule.ts +++ b/src/interactions/commandModule.ts @@ -1,12 +1,14 @@ import type { ApplicationCommandHandler, - AutocompleteHandler + AutocompleteHandler, + ComponentInteractionHandler } from './client.ts' export class ApplicationCommandsModule { name: string = '' commands: ApplicationCommandHandler[] = [] autocomplete: AutocompleteHandler[] = [] + components: ComponentInteractionHandler[] = [] constructor() { if ((this as any)._decoratedAppCmd !== undefined) { @@ -16,6 +18,10 @@ export class ApplicationCommandsModule { if ((this as any)._decoratedAutocomplete !== undefined) { this.autocomplete = (this as any)._decoratedAutocomplete } + + if ((this as any)._decoratedComponents !== undefined) { + this.components = (this as any)._decoratedComponents + } } add(handler: ApplicationCommandHandler): this { diff --git a/src/interactions/decorators.ts b/src/interactions/decorators.ts index 6f305701..5cbad128 100644 --- a/src/interactions/decorators.ts +++ b/src/interactions/decorators.ts @@ -3,18 +3,23 @@ import { ApplicationCommandHandlerCallback, AutocompleteHandler, AutocompleteHandlerCallback, - InteractionsClient + ComponentInteractionCallback, + InteractionsClient, + ComponentInteractionHandler } from './client.ts' import type { Client } from '../client/mod.ts' import { ApplicationCommandsModule } from './commandModule.ts' import { ApplicationCommandInteraction } from '../structures/applicationCommand.ts' import { GatewayIntents } from '../types/gateway.ts' import { ApplicationCommandType } from '../types/applicationCommand.ts' +import { MessageComponentInteraction } from '../structures/messageComponents.ts' +import { ModalSubmitInteraction } from '../structures/modalSubmitInteraction.ts' /** Type extension that adds the `_decoratedAppCmd` list. */ interface DecoratedAppExt { _decoratedAppCmd?: ApplicationCommandHandler[] _decoratedAutocomplete?: AutocompleteHandler[] + _decoratedComponents?: ComponentInteractionHandler[] } // Maybe a better name for this would be `ApplicationCommandBase` or `ApplicationCommandObject` or something else @@ -47,6 +52,12 @@ type AutocompleteDecorator = ( desc: TypedPropertyDescriptor ) => void +type MessageComponentDecorator = ( + client: ApplicationCommandClientExt, + prop: string, + desc: TypedPropertyDescriptor> +) => void + /** * Wraps the command handler with a validation function. * @param desc property descriptor @@ -369,6 +380,88 @@ export function userContextMenu(name?: string): ApplicationCommandDecorator { } } +/** + * Decorator to create a Button message component interaction handler. + * + * Example: + * ```ts + * class MyClient extends Client { + * // ... + * + * @messageComponent("custom_id") + * buttonHandler(i: MessageComponentInteraction) { + * // ... + * } + * } + * ``` + * + * First argument that is `name` is optional and can be + * inferred from method name. + */ +export function messageComponent( + customID?: string +): MessageComponentDecorator { + return function ( + client: ApplicationCommandClientExt, + prop: string, + desc: TypedPropertyDescriptor< + ComponentInteractionCallback + > + ) { + if (client._decoratedComponents === undefined) + client._decoratedComponents = [] + if (typeof desc.value !== 'function') { + throw new Error('@messageComponent decorator requires a function') + } else + client._decoratedComponents.push({ + customID: customID ?? prop, + handler: desc.value, + type: 'button' + }) + } +} + +/** + * Decorator to create a Modal submit interaction handler. + * + * Example: + * ```ts + * class MyClient extends Client { + * // ... + * + * @modalHandler("custom_id") + * modalSubmit(i: ModalSubmitInteraction) { + * // ... + * } + * } + * ``` + * + * First argument that is `name` is optional and can be + * inferred from method name. + */ +export function modalHandler( + customID?: string +): MessageComponentDecorator { + return function ( + client: ApplicationCommandClientExt, + prop: string, + desc: TypedPropertyDescriptor< + ComponentInteractionCallback + > + ) { + if (client._decoratedComponents === undefined) + client._decoratedComponents = [] + if (typeof desc.value !== 'function') { + throw new Error('@modalHandler decorator requires a function') + } else + client._decoratedComponents.push({ + customID: customID ?? prop, + handler: desc.value, + type: 'modal' + }) + } +} + /** * The command can only be called from a guild. * @param action message or function called when the condition is not met diff --git a/test/slash.ts b/test/slash.ts index ad0da4a4..1ee23949 100644 --- a/test/slash.ts +++ b/test/slash.ts @@ -1,4 +1,14 @@ -import { Client, Intents, event, slash } from '../mod.ts' +import { + Client, + Intents, + event, + slash, + messageComponent, + MessageComponentInteraction, + SlashCommandInteraction, + modalHandler, + ModalSubmitInteraction +} from '../mod.ts' import { ApplicationCommandInteraction } from '../src/structures/applicationCommand.ts' import { ApplicationCommandOptionType as Type } from '../src/types/applicationCommand.ts' import { TOKEN, GUILD } from './config.ts' @@ -68,11 +78,62 @@ export class MyClient extends Client { ] } ] + }, + { + name: 'test3', + description: 'Test command with a message component decorators.' + }, + { + name: 'test4', + description: 'Test command with a modal decorators.' } ], GUILD ) - this.slash.commands.bulkEdit([]) + this.interactions.commands.bulkEdit([]) + } + + @slash() test4(d: SlashCommandInteraction): void { + d.showModal({ + title: 'Test', + customID: 'modal_id', + components: [ + { + type: 1, + components: [ + { + type: 4, + customID: 'text_field_id', + placeholder: 'Test', + label: 'Test', + style: 1 + } + ] + } + ] + }) + } + + @modalHandler('modal_id') modal(d: ModalSubmitInteraction): void { + d.reply(JSON.stringify(d.data.components)) + } + + @slash() test3(d: SlashCommandInteraction): void { + d.reply({ + components: [ + { + type: 1, + components: [ + { + type: 2, + customID: 'button_id', + label: 'Test', + style: 1 + } + ] + } + ] + }) } @slash() test(d: ApplicationCommandInteraction): void { @@ -80,6 +141,10 @@ export class MyClient extends Client { console.log(d.options) } + @messageComponent('button_id') cid(d: MessageComponentInteraction): void { + d.reply('Working as intented') + } + @event() raw(evt: string, d: any): void { if (evt === 'INTERACTION_CREATE') console.log(evt, d?.data?.resolved) } @@ -92,4 +157,8 @@ const client = new MyClient({ } }) +client.interactions.on('interactionError', (d) => { + console.log(d) +}) + client.connect(TOKEN, Intents.None)