From e6c2c8db0782f7218839a73cbeec0b31305feae3 Mon Sep 17 00:00:00 2001 From: ZiomaleQ Date: Mon, 4 Sep 2023 17:39:49 +0200 Subject: [PATCH 1/4] Add component interaction decorator --- src/interactions/client.ts | 54 +++++++++++++++++++++++++++++++ src/interactions/commandModule.ts | 8 ++++- src/interactions/decorators.ts | 29 ++++++++++++++++- test/slash.ts | 42 ++++++++++++++++++++++-- 4 files changed, 129 insertions(+), 4 deletions(-) diff --git a/src/interactions/client.ts b/src/interactions/client.ts index 2ee6753e..36fae6bf 100644 --- a/src/interactions/client.ts +++ b/src/interactions/client.ts @@ -51,6 +51,9 @@ export type { ApplicationCommandHandlerCallback as SlashCommandHandlerCallback } export type { ApplicationCommandHandler as SlashCommandHandler } export type AutocompleteHandlerCallback = (d: AutocompleteInteraction) => any +export type ComponentInteractionCallback = ( + d: MessageComponentInteraction +) => any export interface AutocompleteHandler { cmd: string @@ -60,6 +63,11 @@ export interface AutocompleteHandler { handler: AutocompleteHandlerCallback } +export interface ComponentInteractionHandler { + customID: string + handler: ComponentInteractionCallback +} + /** Options for InteractionsClient */ export interface SlashOptions { id?: string | (() => string) @@ -96,6 +104,7 @@ export class InteractionsClient extends HarmonyEventEmitter { @@ -140,9 +150,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 +177,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 +410,18 @@ export class InteractionsClient extends HarmonyEventEmitter e.components).flat() + ].find((e) => { + return i.customID === e.customID + }) + } + /** Process an incoming Interaction */ async _process( interaction: Interaction | ApplicationCommandInteraction @@ -409,6 +446,23 @@ export class InteractionsClient extends HarmonyEventEmitter e.components).flat() + ].find((e) => e.customID === '*') + + 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..5d1b2380 100644 --- a/src/interactions/decorators.ts +++ b/src/interactions/decorators.ts @@ -3,7 +3,9 @@ import { ApplicationCommandHandlerCallback, AutocompleteHandler, AutocompleteHandlerCallback, - InteractionsClient + ComponentInteractionCallback, + InteractionsClient, + ComponentInteractionHandler } from './client.ts' import type { Client } from '../client/mod.ts' import { ApplicationCommandsModule } from './commandModule.ts' @@ -15,6 +17,7 @@ import { ApplicationCommandType } from '../types/applicationCommand.ts' interface DecoratedAppExt { _decoratedAppCmd?: ApplicationCommandHandler[] _decoratedAutocomplete?: AutocompleteHandler[] + _decoratedComponents?: ComponentInteractionHandler[] } // Maybe a better name for this would be `ApplicationCommandBase` or `ApplicationCommandObject` or something else @@ -47,6 +50,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 +378,24 @@ export function userContextMenu(name?: string): ApplicationCommandDecorator { } } +export function messageComponent(customID?: string): MessageComponentDecorator { + return function ( + client: ApplicationCommandClientExt, + prop: string, + desc: TypedPropertyDescriptor + ) { + if (client._decoratedComponents === undefined) + client._decoratedComponents = [] + if (typeof desc.value !== 'function') { + throw new Error('@userContextMenu decorator requires a function') + } else + client._decoratedComponents.push({ + customID: customID ?? prop, + handler: desc.value + }) + } +} + /** * 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..a22450fe 100644 --- a/test/slash.ts +++ b/test/slash.ts @@ -1,4 +1,12 @@ -import { Client, Intents, event, slash } from '../mod.ts' +import { + Client, + Intents, + event, + slash, + messageComponent, + MessageComponentInteraction, + SlashCommandInteraction +} 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 +76,33 @@ export class MyClient extends Client { ] } ] + }, + { + name: 'test3', + description: 'Test command with a message component decorators.' } ], GUILD ) - this.slash.commands.bulkEdit([]) + this.interactions.commands.bulkEdit([]) + } + + @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 +110,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 +126,8 @@ const client = new MyClient({ } }) +client.interactions.on('interactionError', (d) => { + console.log(d) +}) + client.connect(TOKEN, Intents.None) From 714f976546565b7074803af63ea9e25ac9a7c6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw?= Date: Mon, 4 Sep 2023 20:11:22 +0200 Subject: [PATCH 2/4] Fix error message for invalid receiver decorator Co-authored-by: Bloxs <51055767+Blocksnmore@users.noreply.github.com> --- src/interactions/decorators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interactions/decorators.ts b/src/interactions/decorators.ts index 5d1b2380..1fb9dc4a 100644 --- a/src/interactions/decorators.ts +++ b/src/interactions/decorators.ts @@ -387,7 +387,7 @@ export function messageComponent(customID?: string): MessageComponentDecorator { if (client._decoratedComponents === undefined) client._decoratedComponents = [] if (typeof desc.value !== 'function') { - throw new Error('@userContextMenu decorator requires a function') + throw new Error('@messageComponent decorator requires a function') } else client._decoratedComponents.push({ customID: customID ?? prop, From 5134c91c44797205262f6e2d0134fbc48b571d73 Mon Sep 17 00:00:00 2001 From: ZiomaleQ Date: Tue, 5 Sep 2023 12:34:49 +0200 Subject: [PATCH 3/4] Add modal handler decorator --- src/interactions/client.ts | 61 ++++++++++++++++++++++++++---- src/interactions/decorators.ts | 68 +++++++++++++++++++++++++++++++--- test/slash.ts | 33 ++++++++++++++++- 3 files changed, 148 insertions(+), 14 deletions(-) diff --git a/src/interactions/client.ts b/src/interactions/client.ts index 36fae6bf..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,9 +52,7 @@ export type { ApplicationCommandHandlerCallback as SlashCommandHandlerCallback } export type { ApplicationCommandHandler as SlashCommandHandler } export type AutocompleteHandlerCallback = (d: AutocompleteInteraction) => any -export type ComponentInteractionCallback = ( - d: MessageComponentInteraction -) => any +export type ComponentInteractionCallback = (d: T) => any export interface AutocompleteHandler { cmd: string @@ -63,9 +62,11 @@ export interface AutocompleteHandler { handler: AutocompleteHandlerCallback } -export interface ComponentInteractionHandler { +// deno-lint-ignore no-explicit-any +export interface ComponentInteractionHandler { customID: string - handler: ComponentInteractionCallback + handler: ComponentInteractionCallback + type: 'button' | 'modal' } /** Options for InteractionsClient */ @@ -410,7 +411,7 @@ export class InteractionsClient extends HarmonyEventEmitter e.components).flat() ].find((e) => { - return i.customID === e.customID + 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 }) } @@ -453,7 +481,24 @@ export class InteractionsClient extends HarmonyEventEmitter e.components).flat() - ].find((e) => e.customID === '*') + ].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) diff --git a/src/interactions/decorators.ts b/src/interactions/decorators.ts index 1fb9dc4a..0274203d 100644 --- a/src/interactions/decorators.ts +++ b/src/interactions/decorators.ts @@ -12,6 +12,8 @@ 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 { @@ -50,10 +52,10 @@ type AutocompleteDecorator = ( desc: TypedPropertyDescriptor ) => void -type MessageComponentDecorator = ( +type MessageComponentDecorator = ( client: ApplicationCommandClientExt, prop: string, - desc: TypedPropertyDescriptor + desc: TypedPropertyDescriptor> ) => void /** @@ -378,11 +380,29 @@ export function userContextMenu(name?: string): ApplicationCommandDecorator { } } -export function messageComponent(customID?: string): MessageComponentDecorator { +/** + * 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 + desc: TypedPropertyDescriptor> ) { if (client._decoratedComponents === undefined) client._decoratedComponents = [] @@ -391,7 +411,45 @@ export function messageComponent(customID?: string): MessageComponentDecorator { } else client._decoratedComponents.push({ customID: customID ?? prop, - handler: desc.value + 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> + ) { + 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' }) } } diff --git a/test/slash.ts b/test/slash.ts index a22450fe..1ee23949 100644 --- a/test/slash.ts +++ b/test/slash.ts @@ -5,7 +5,9 @@ import { slash, messageComponent, MessageComponentInteraction, - SlashCommandInteraction + SlashCommandInteraction, + modalHandler, + ModalSubmitInteraction } from '../mod.ts' import { ApplicationCommandInteraction } from '../src/structures/applicationCommand.ts' import { ApplicationCommandOptionType as Type } from '../src/types/applicationCommand.ts' @@ -80,6 +82,10 @@ 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 @@ -87,6 +93,31 @@ export class MyClient extends Client { 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: [ From f57524b3b6b4d1e7466d48ebb1271e60300836a7 Mon Sep 17 00:00:00 2001 From: ZiomaleQ Date: Tue, 5 Sep 2023 12:35:22 +0200 Subject: [PATCH 4/4] Format ./src/interactions/decorators.ts --- src/interactions/decorators.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/interactions/decorators.ts b/src/interactions/decorators.ts index 0274203d..5cbad128 100644 --- a/src/interactions/decorators.ts +++ b/src/interactions/decorators.ts @@ -12,8 +12,8 @@ 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'; +import { MessageComponentInteraction } from '../structures/messageComponents.ts' +import { ModalSubmitInteraction } from '../structures/modalSubmitInteraction.ts' /** Type extension that adds the `_decoratedAppCmd` list. */ interface DecoratedAppExt { @@ -398,11 +398,15 @@ export function userContextMenu(name?: string): ApplicationCommandDecorator { * First argument that is `name` is optional and can be * inferred from method name. */ -export function messageComponent(customID?: string): MessageComponentDecorator { +export function messageComponent( + customID?: string +): MessageComponentDecorator { return function ( client: ApplicationCommandClientExt, prop: string, - desc: TypedPropertyDescriptor> + desc: TypedPropertyDescriptor< + ComponentInteractionCallback + > ) { if (client._decoratedComponents === undefined) client._decoratedComponents = [] @@ -435,11 +439,15 @@ export function messageComponent(customID?: string): MessageComponentDecorator { +export function modalHandler( + customID?: string +): MessageComponentDecorator { return function ( client: ApplicationCommandClientExt, prop: string, - desc: TypedPropertyDescriptor> + desc: TypedPropertyDescriptor< + ComponentInteractionCallback + > ) { if (client._decoratedComponents === undefined) client._decoratedComponents = []