import type { ButtonInteraction, AnySelectMenuInteraction, ModalSubmitInteraction, } from "discord.js"; import type { Option, InferOptions } from "./options"; import type { Middleware, ErrorHandler } 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, > { type = "component" as const; constructor( public name: string, public config: { options?: Record>; onError?: ErrorHandler; run: (interaction: I, args: InferOptions, ctx: Ctx) => Promise | unknown; }, public middlewares: Middleware[] = [], public errorHandlers: ErrorHandler[] = [], ) {} /** * @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; } }