all repos — kitten @ eeea7493aaf03e48a3673dd02e78c109ef7be762

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
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
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<string, Option<any, any>> = Record<string, Option<any, any>>,
	Ctx = any,
	Res = unknown,
> {
	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<Res> | Res;
		},
		public middlewares: Middleware<any, any, any>[] = [],
		public errorHandlers: ErrorHandler<any, any>[] = [],
		public afterwares: Afterware<any, 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>;
	}

	button(
		this: KittenComponent<ButtonInteraction, O, Ctx, Res>,
		...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>]
	): ButtonBuilder {
		return new ButtonBuilder().setCustomId(this.id(args as any));
	}

	stringSelectMenu(
		this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>,
		...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>]
	): StringSelectMenuBuilder {
		return new StringSelectMenuBuilder().setCustomId(this.id(args as any));
	}

	userSelectMenu(
		this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>,
		...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>]
	): UserSelectMenuBuilder {
		return new UserSelectMenuBuilder().setCustomId(this.id(args as any));
	}

	roleSelectMenu(
		this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>,
		...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>]
	): RoleSelectMenuBuilder {
		return new RoleSelectMenuBuilder().setCustomId(this.id(args as any));
	}

	channelSelectMenu(
		this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>,
		...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>]
	): ChannelSelectMenuBuilder {
		return new ChannelSelectMenuBuilder().setCustomId(this.id(args as any));
	}

	mentionableSelectMenu(
		this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>,
		...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>]
	): MentionableSelectMenuBuilder {
		return new MentionableSelectMenuBuilder().setCustomId(this.id(args as any));
	}

	modal(
		this: KittenComponent<ModalSubmitInteraction, O, Ctx, Res>,
		title: string,
		...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>]
	): ModalBuilder {
		return new ModalBuilder().setTitle(title).setCustomId(this.id(args as any));
	}
}