Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add component interaction decorators #367

Merged
merged 4 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/interactions/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -51,6 +52,7 @@ export type { ApplicationCommandHandlerCallback as SlashCommandHandlerCallback }
export type { ApplicationCommandHandler as SlashCommandHandler }

export type AutocompleteHandlerCallback = (d: AutocompleteInteraction) => any
export type ComponentInteractionCallback<T> = (d: T) => any

export interface AutocompleteHandler {
cmd: string
Expand All @@ -60,6 +62,13 @@ export interface AutocompleteHandler {
handler: AutocompleteHandlerCallback
}

// deno-lint-ignore no-explicit-any
export interface ComponentInteractionHandler<T = any> {
customID: string
handler: ComponentInteractionCallback<T>
type: 'button' | 'modal'
}

/** Options for InteractionsClient */
export interface SlashOptions {
id?: string | (() => string)
Expand Down Expand Up @@ -96,6 +105,7 @@ export class InteractionsClient extends HarmonyEventEmitter<InteractionsClientEv
commands: ApplicationCommandsManager
handlers: ApplicationCommandHandler[] = []
autocompleteHandlers: AutocompleteHandler[] = []
componentHandlers: ComponentInteractionHandler[] = []
readonly rest!: RESTManager
modules: ApplicationCommandsModule[] = []
publicKey?: string
Expand Down Expand Up @@ -125,6 +135,7 @@ export class InteractionsClient extends HarmonyEventEmitter<InteractionsClientEv
const client = this.client as unknown as {
_decoratedAppCmd: ApplicationCommandHandler[]
_decoratedAutocomplete: AutocompleteHandler[]
_decoratedComponents: ComponentInteractionHandler[]
}
if (client?._decoratedAppCmd !== undefined) {
client._decoratedAppCmd.forEach((e) => {
Expand All @@ -140,9 +151,17 @@ export class InteractionsClient extends HarmonyEventEmitter<InteractionsClientEv
})
}

if (client?._decoratedComponents !== undefined) {
client._decoratedComponents.forEach((e) => {
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) {
Expand All @@ -159,6 +178,13 @@ export class InteractionsClient extends HarmonyEventEmitter<InteractionsClientEv
})
}

if (self._decoratedComponents !== undefined) {
self._decoratedComponents.forEach((e) => {
e.handler = e.handler.bind(this.client)
self.componentHandlers.push(e)
})
}

Object.defineProperty(this, 'rest', {
value:
options.client === undefined
Expand Down Expand Up @@ -385,6 +411,45 @@ export class InteractionsClient extends HarmonyEventEmitter<InteractionsClientEv
})
}

/** Get Handler for an component Interaction. */
private _getComponentHandler(
i: MessageComponentInteraction
): ComponentInteractionHandler | undefined {
return [
...this.componentHandlers,
...this.modules.map((e) => 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
Expand All @@ -409,6 +474,40 @@ export class InteractionsClient extends HarmonyEventEmitter<InteractionsClientEv
return
}

// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (interaction.isMessageComponent()) {
const handle =
this._getComponentHandler(interaction) ??
[
...this.componentHandlers,
...this.modules.map((e) => 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

Expand Down
8 changes: 7 additions & 1 deletion src/interactions/commandModule.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 {
Expand Down
95 changes: 94 additions & 1 deletion src/interactions/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,6 +52,12 @@ type AutocompleteDecorator = (
desc: TypedPropertyDescriptor<AutocompleteHandlerCallback>
) => void

type MessageComponentDecorator<T = any> = (
client: ApplicationCommandClientExt,
prop: string,
desc: TypedPropertyDescriptor<ComponentInteractionCallback<T>>
) => void

/**
* Wraps the command handler with a validation function.
* @param desc property descriptor
Expand Down Expand Up @@ -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<MessageComponentInteraction> {
return function (
client: ApplicationCommandClientExt,
prop: string,
desc: TypedPropertyDescriptor<
ComponentInteractionCallback<MessageComponentInteraction>
>
) {
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<ModalSubmitInteraction> {
return function (
client: ApplicationCommandClientExt,
prop: string,
desc: TypedPropertyDescriptor<
ComponentInteractionCallback<ModalSubmitInteraction>
>
) {
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
Expand Down
Loading