Skip to content

Commit

Permalink
Merge pull request #367 from ZiomaleQ/main
Browse files Browse the repository at this point in the history
Add component interaction decorators
  • Loading branch information
Helloyunho authored Sep 25, 2023
2 parents 80b3317 + f57524b commit 1f731a3
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 4 deletions.
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

0 comments on commit 1f731a3

Please sign in to comment.