all repos — kitten @ ec4ee6e742f7af41cc1d4ec1a7d2b057dcfbf1a0

pkgs/core/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
import type {
  ButtonInteraction,
  AnySelectMenuInteraction,
  ModalSubmitInteraction,
} from "discord.js";
import type { Option, InferOptions } from "./options";
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>>,
> {
  type = "component" as const;

  constructor(
    public name: string,
    public config: {
      options?: Record<string, Option<any, any>>;
      run: (interaction: I, args: any) => Promise<void> | void;
    },
  ) {}

  /**
   * @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): Record<string, any> {
    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;
  }
}