all repos — kitten @ cc55514091d8d0ea86ffe7cf25645fee1768a697

pkgs/router/src/components.ts (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
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<string, Option<any, any>> = Record<string, Option<any, any>>,
	Ctx = any,
> {
	type = "component" as const;

	constructor(
		public name: string,
		public config: {
			options?: Record<string, Option<any, any>>;
			onError?: ErrorHandler<I, Ctx>;
			run: (interaction: I, args: InferOptions<O>, ctx: Ctx) => Promise<unknown> | unknown;
		},
		public middlewares: Middleware<any, any, any>[] = [],
		public errorHandlers: ErrorHandler<any, any>[] = [],
	) {}

	/**
	 * @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<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>]
	): 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<O> {
		const [, ...parts] = customId.split(":");
		const keys = Object.keys(this.config.options || {});
		const parsed: Record<string, any> = {};

		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<O>;
	}
}