structures/Reaction.js

import Client from "../Client.js";
import { TO_JSON_TYPES_ENUM } from "../constants.js";
import Emoji from "./Emoji.js";
import util from "util";

/**
 * Represents a reaction belonging to a message.
 */
class Reaction {
  #_client;
  #_guild_id;
  #emoji;
  #_reacted;
  #initial_reactor;
  #count;
  /**
   * Creates the structure for a reaction.
   * @param {Client} client The client instance.
   * @param {Object} data The raw reaction data from Discord.
   * @param {Object} options Additional options for this structure.
   * @param {String} options.guildId The id of the guild that the reaction belongs to.
   * @see {@link https://discord.com/developers/docs/resources/channel#reaction-object-reaction-structure}
   */
  constructor(client, data, { guildId } = {}) {
    if (!(client instanceof Client))
      throw new TypeError("GLUON: Client must be an instance of Client");
    if (typeof data !== "object")
      throw new TypeError("GLUON: Data must be an object");
    if (typeof guildId !== "string")
      throw new TypeError("GLUON: Guild ID must be a string");

    /**
     * The client instance.
     * @type {Client}
     * @private
     */
    this.#_client = client;

    /**
     * The id of the guild that this reaction belongs to.
     * @type {BigInt}
     * @private
     */
    this.#_guild_id = BigInt(guildId);

    if (data.emoji.mention)
      /**
       * The emoji used for the reaction.
       * @type {Emoji}
       * @private
       */
      this.#emoji = data.emoji;
    else
      this.#emoji = new Emoji(client, data.emoji, { guildId, nocache: true });

    /**
     * Users who reacted with this emoji.
     * @type {Array<BigInt>}
     * @private
     */
    this.#_reacted = data._reacted?.map((r) => BigInt(r)) || [];
    if (!Array.isArray(data._reacted) && data.count) this.#count = data.count;

    /**
     * The user who added the first reaction.
     * @type {BigInt?}
     * @private
     */
    if (data.initial_reactor)
      this.#initial_reactor = BigInt(data.initial_reactor);
  }

  /**
   * The number of reactions to this message.
   * @readonly
   * @type {Number}
   * @public
   */
  get count() {
    return this.#count ?? this.#_reacted.length;
  }

  /**
   * The member objects of the members who reacted. Returns the user id of the member cannot be found.
   * @readonly
   * @type {Array<Member | String>}
   * @public
   */
  get reacted() {
    return this.#_reacted.map((userId) => {
      const member = this.guild.members.get(String(userId));

      if (member) return member;
      else return String(userId);
    });
  }

  /**
   * The user ids of the users who reacted.
   * @readonly
   * @type {Array<String>}
   * @public
   */
  get reactedIds() {
    return this.#_reacted.map((r) => String(r));
  }

  /**
   * The id of the guild that this reaction belongs to.
   * @type {String}
   * @readonly
   * @public
   */
  get guildId() {
    return String(this.#_guild_id);
  }

  /**
   * The guild that this reaction belongs to.
   * @type {Guild?}
   * @readonly
   * @public
   */
  get guild() {
    return this.#_client.guilds.get(this.guildId) || null;
  }

  /**
   * The emoji used for the reaction.
   * @type {Emoji}
   * @readonly
   * @public
   */
  get emoji() {
    return this.#emoji;
  }

  /**
   * The user who added the first reaction.
   * @type {String?}
   * @readonly
   * @public
   */
  get initialReactor() {
    return this.#initial_reactor ? String(this.#initial_reactor) : null;
  }

  /**
   * Adds a user to the list of reactors.
   * @param {String} userId The id of the user to add as a reactor.
   * @throws {TypeError}
   * @public
   * @method
   */
  _addReactor(userId) {
    if (typeof userId !== "string")
      throw new TypeError("GLUON: User ID must be a string.");

    if (this.#_reacted.length === 0 && !this.#initial_reactor)
      this.#initial_reactor = BigInt(userId);

    this.#_reacted.push(BigInt(userId));

    if (this.#count) this.#count++;
  }

  /**
   * Removes a user from the list of reactors.
   * @param {String} userId The id of the user to add as a reactor.
   * @throws {TypeError}
   * @public
   * @method
   */
  _removeReactor(userId) {
    if (typeof userId !== "string")
      throw new TypeError("GLUON: User ID must be a string.");

    this.#_reacted = this.#_reacted.filter((r) => r !== BigInt(userId));

    if (this.#count) this.#count--;
  }

  /**
   * @method
   * @public
   */
  toString() {
    return `<Reaction: ${this.emoji}>`;
  }

  /**
   * @method
   * @public
   */
  [util.inspect.custom]() {
    return this.toString();
  }

  /**
   * Returns the JSON representation of this structure.
   * @param {Number} format The format to return the data in.
   * @returns {Object}
   * @public
   * @method
   */
  toJSON(format) {
    switch (format) {
      case TO_JSON_TYPES_ENUM.CACHE_FORMAT:
      case TO_JSON_TYPES_ENUM.STORAGE_FORMAT: {
        return {
          emoji: this.emoji.toJSON(format),
          _reacted: this.reactedIds,
          initial_reactor: this.initialReactor ?? undefined,
        };
      }
      case TO_JSON_TYPES_ENUM.DISCORD_FORMAT:
      default: {
        return {
          emoji: this.emoji.toJSON(format),
          count: this.count,
        };
      }
    }
  }
}

export default Reaction;