import { type ButtonInteraction, type AnySelectMenuInteraction, type ModalSubmitInteraction, ButtonBuilder, StringSelectMenuBuilder, UserSelectMenuBuilder, RoleSelectMenuBuilder, ChannelSelectMenuBuilder, MentionableSelectMenuBuilder, ModalBuilder, } from "discord.js"; import type { Option, InferOptions } from "./options"; import type { Middleware, ErrorHandler, Afterware } from "./commands"; import { CustomIdTooLong } from "./errors"; export type KittenComponentInteraction = | ButtonInteraction | AnySelectMenuInteraction | ModalSubmitInteraction; export class KittenComponent< I extends KittenComponentInteraction = KittenComponentInteraction, O extends Record> = Record>, Ctx = any, Res = unknown, > { type = "component" as const; constructor( public name: string, public config: { options?: Record>; onError?: ErrorHandler; run: (interaction: I, args: InferOptions, ctx: Ctx) => Promise | Res; }, public middlewares: Middleware[] = [], public errorHandlers: ErrorHandler[] = [], public afterwares: Afterware[] = [], ) {} /** * @throws {CustomIdTooLong} if the generated custom ID exceeds Discord's 100 character limit. * @returns a custom ID string that encodes the component name and its options' values. */ id( ...[args]: {} extends InferOptions ? [args?: InferOptions] : [args: InferOptions] ): string { const keys = Object.keys(this.config.options || {}); const values = keys.map((key) => encodeURIComponent(String(args?.[key] ?? ""))); const customId = [this.name, ...values].join(":"); if (customId.length > 100) throw new CustomIdTooLong(this.name, customId.length); return customId; } parseArgs(customId: string): InferOptions { const [, ...parts] = customId.split(":"); const keys = Object.keys(this.config.options || {}); const parsed: Record = {}; keys.forEach((key, index) => { const rawVal = parts[index]; if (rawVal === undefined) return; const val = decodeURIComponent(rawVal); const opt = this.config.options?.[key]; if (opt?.type === "boolean") parsed[key] = val === "true"; else if (opt?.type === "integer" || opt?.type === "number") parsed[key] = Number(val); else parsed[key] = val === "" ? undefined : val; }); return parsed as InferOptions; } button( this: KittenComponent, ...[args]: {} extends InferOptions ? [args?: InferOptions] : [args: InferOptions] ): ButtonBuilder { return new ButtonBuilder().setCustomId(this.id(args as any)); } stringSelectMenu( this: KittenComponent, ...[args]: {} extends InferOptions ? [args?: InferOptions] : [args: InferOptions] ): StringSelectMenuBuilder { return new StringSelectMenuBuilder().setCustomId(this.id(args as any)); } userSelectMenu( this: KittenComponent, ...[args]: {} extends InferOptions ? [args?: InferOptions] : [args: InferOptions] ): UserSelectMenuBuilder { return new UserSelectMenuBuilder().setCustomId(this.id(args as any)); } roleSelectMenu( this: KittenComponent, ...[args]: {} extends InferOptions ? [args?: InferOptions] : [args: InferOptions] ): RoleSelectMenuBuilder { return new RoleSelectMenuBuilder().setCustomId(this.id(args as any)); } channelSelectMenu( this: KittenComponent, ...[args]: {} extends InferOptions ? [args?: InferOptions] : [args: InferOptions] ): ChannelSelectMenuBuilder { return new ChannelSelectMenuBuilder().setCustomId(this.id(args as any)); } mentionableSelectMenu( this: KittenComponent, ...[args]: {} extends InferOptions ? [args?: InferOptions] : [args: InferOptions] ): MentionableSelectMenuBuilder { return new MentionableSelectMenuBuilder().setCustomId(this.id(args as any)); } modal( this: KittenComponent, title: string, ...[args]: {} extends InferOptions ? [args?: InferOptions] : [args: InferOptions] ): ModalBuilder { return new ModalBuilder().setTitle(title).setCustomId(this.id(args as any)); } }