util/builder/buttonBuilder.js

import {
  COMPONENT_TYPES,
  BUTTON_STYLES,
  LIMITS,
  TO_JSON_TYPES_ENUM,
} from "../../constants.js";
import resolveEmoji from "../discord/resolveEmoji.js";
import isValidUrl from "../general/isValidUrl.js";

/**
 * Helps to construct a button for a message.
 * @see {@link https://discord.com/developers/docs/interactions/message-components#button-object-button-structure}
 */
class Button {
  /**
   * Creates a button.
   */
  constructor() {
    this.type = COMPONENT_TYPES.BUTTON;
  }

  /**
   * Sets the text on the button.
   * @param {String} label The text to display on the button.
   * @returns {Button}
   */
  setLabel(label) {
    if (!label) throw new TypeError("GLUON: Button label must be provided.");

    this.label =
      label && label.length > LIMITS.MAX_BUTTON_LABEL
        ? `${label.substring(0, LIMITS.MAX_BUTTON_LABEL - 3)}...`
        : label;

    return this;
  }

  /**
   * Sets the emoji to be displayed on the button.
   * @param {String} emoji The emoji to display on the button. For a custom emoji, it should be in the format "<:bitcoin:844240546246950922>".
   * @returns {Button}
   */
  setEmoji(emoji) {
    this.emoji = resolveEmoji(emoji);

    if (!this.emoji)
      throw new TypeError("GLUON: Button emoji must be provided.");

    return this;
  }

  /**
   * Sets the style of the button.
   * @param {Number} style The button style.
   * @returns {Button}
   * @see {@link https://discord.com/developers/docs/interactions/message-components#button-object-button-styles}
   */
  setStyle(style) {
    if (!style) throw new TypeError("GLUON: Button style must be provided.");

    this.style = style;

    return this;
  }

  /**
   * Set the custom id of the button.
   * @param {String} id The custom id of the button.
   * @returns {Button}
   * @see {@link https://discord.com/developers/docs/interactions/message-components#custom-id}
   */
  setCustomID(id) {
    if (!id)
      throw new TypeError(
        "GLUON: Button custom id must be provided for non-link buttons.",
      );

    if (id.length > LIMITS.MAX_BUTTON_CUSTOM_ID)
      throw new RangeError(
        `GLUON: Button custom id must be under ${LIMITS.MAX_BUTTON_CUSTOM_ID} characters.`,
      );

    this.custom_id = id;

    return this;
  }

  /**
   * Sets the url of the button.
   * @param {String} url The url for a link button.
   * @returns {Button}
   */
  setURL(url) {
    this.url = url;

    return this;
  }

  /**
   * Disables the button from being clickable.
   * @param {Boolean} disabled Whether this button should be displayed as disabled.
   * @returns {Button}
   */
  setDisabled(disabled) {
    this.disabled = disabled;

    return this;
  }

  /**
   * Returns the correct Discord format for a button.
   * @returns {Object}
   */
  toJSON(
    format,
    { suppressValidation = false } = { suppressValidation: false },
  ) {
    if (suppressValidation !== true) {
      if (!this.label)
        throw new TypeError("GLUON: Button label must be provided.");
      if (typeof this.label !== "string")
        throw new TypeError("GLUON: Button label must be a string.");
      if (this.label.length > LIMITS.MAX_BUTTON_LABEL)
        throw new RangeError(
          `GLUON: Button label must be less than ${LIMITS.MAX_BUTTON_LABEL} characters.`,
        );
      if (typeof this.style !== "number")
        throw new TypeError("GLUON: Button style must be a number.");
      if (this.style === BUTTON_STYLES.LINK && !this.url)
        throw new TypeError(
          "GLUON: Button url must be provided for link buttons.",
        );
      if (this.style !== BUTTON_STYLES.LINK && !this.custom_id)
        throw new TypeError(
          "GLUON: Button custom id must be provided for non-link buttons.",
        );
      if (this.style === BUTTON_STYLES.LINK && this.custom_id)
        throw new TypeError(
          "GLUON: Button custom id must not be provided for link buttons.",
        );
      if (this.style !== BUTTON_STYLES.LINK && this.url)
        throw new TypeError(
          "GLUON: Button url must not be provided for non-link buttons.",
        );
      if (this.style === BUTTON_STYLES.LINK && this.emoji)
        throw new TypeError(
          "GLUON: Button emoji must not be provided for link buttons.",
        );
      if (this.type !== COMPONENT_TYPES.BUTTON)
        throw new TypeError("GLUON: Button type must be set to 'BUTTON'.");
      if (this.emoji && typeof this.emoji !== "object")
        throw new TypeError("GLUON: Button emoji must be an object.");
      if (this.custom_id && typeof this.custom_id !== "string")
        throw new TypeError("GLUON: Button custom id must be a string.");
      if (this.custom_id && this.custom_id.length > LIMITS.MAX_BUTTON_CUSTOM_ID)
        throw new RangeError(
          `GLUON: Button custom id must be less than ${LIMITS.MAX_BUTTON_CUSTOM_ID} characters.`,
        );
      if (this.url && typeof this.url !== "string")
        throw new TypeError("GLUON: Button url must be a string.");
      if (this.url && !isValidUrl(this.url))
        throw new TypeError("GLUON: Button url must be a valid url.");
      if (
        typeof this.disabled !== "undefined" &&
        typeof this.disabled !== "boolean"
      )
        throw new TypeError("GLUON: Button disabled must be a boolean.");
    }
    switch (format) {
      case TO_JSON_TYPES_ENUM.CACHE_FORMAT:
      case TO_JSON_TYPES_ENUM.DISCORD_FORMAT:
      case TO_JSON_TYPES_ENUM.STORAGE_FORMAT:
      default: {
        return {
          type: this.type,
          label: this.label,
          emoji: this.emoji,
          style: this.style,
          custom_id: this.custom_id,
          url: this.url,
          disabled: this.disabled,
        };
      }
    }
  }
}

export default Button;