structures/VoiceState.js

import Client from "../Client.js";
import { GLUON_CACHING_OPTIONS, TO_JSON_TYPES_ENUM } from "../constants.js";
import GluonCacheOptions from "../managers/GluonCacheOptions.js";
import GuildCacheOptions from "../managers/GuildCacheOptions.js";
import Member from "./Member.js";
import util from "util";

/**
 * Represents a voice state.
 */
class VoiceState {
  #_client;
  #_guild_id;
  #_channel_id;
  #_attributes;
  #member;
  #_user_id;
  #joined;
  #request_to_speak_timestamp;
  /**
   * Creates the structure for a voice state.
   * @param {Client} client The client instance.
   * @param {Object} data The raw voice state data from Discord.
   * @param {Object} options The additional options for this structure.
   * @param {String} options.guildId The id of the guild that the voice state belongs to.
   * @param {Boolean?} options.nocache Whether this voice state should be cached.
   */
  constructor(client, data, { guildId, nocache = false } = { nocache: false }) {
    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");
    if (typeof nocache !== "boolean")
      throw new TypeError("GLUON: No cache must be a boolean");

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

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

    if (this.guild)
      nocache =
        (this.guild._cache_options & GLUON_CACHING_OPTIONS.NO_VOICE_STATE) ==
        GLUON_CACHING_OPTIONS.NO_VOICE_STATE;

    const existing = this.guild?.voiceStates.get(data.user_id) || null;

    /**
     * The id of the channel involved.
     * @type {BigInt}
     * @private
     */
    this.#_channel_id = BigInt(data.channel_id);

    /**
     * The attributes of the voice state.
     * @type {Number}
     * @private
     */
    this.#_attributes = data._attributes ?? 0;

    if (data.deaf == true) this.#_attributes |= 0b1 << 0;

    if (data.mute == true) this.#_attributes |= 0b1 << 1;

    if (data.self_deaf == true) this.#_attributes |= 0b1 << 2;

    if (data.self_mute == true) this.#_attributes |= 0b1 << 3;

    if (data.self_stream == true) this.#_attributes |= 0b1 << 4;

    if (data.self_video == true) this.#_attributes |= 0b1 << 5;

    if (data.suppress == true) this.#_attributes |= 0b1 << 6;

    if (data.member)
      /**
       * The member the voice state is about.
       * @type {Member?}
       * @private
       */
      this.#member = new Member(this.#_client, data.member, {
        userId: data.user_id,
        guildId: data.guild_id,
        user: data.member.user,
        nocache,
      });
    else this.#member = this.guild?.members.get(data.user_id) || null;

    /**
     * The id of the user the voice state is about.
     * @type {BigInt}
     * @private
     */
    this.#_user_id = BigInt(data.user_id);

    /**
     * The UNIX time the user joined the voice channel.
     * @type {Number}
     * @private
     */
    if (typeof data.joined == "number") this.#joined = data.joined;
    else if (existing && typeof existing.joined == "number")
      this.#joined = existing.joined;
    else this.#joined = (Date.now() / 1000) | 0;

    /**
     * The UNIX timestamp of when the user requested to speak.
     * @type {Number?}
     * @private
     */
    if (data.request_to_speak_timestamp)
      this.#request_to_speak_timestamp =
        (new Date(data.request_to_speak_timestamp).getTime() / 1000) | 0;

    if (
      nocache === false &&
      VoiceState.shouldCache(
        this.#_client._cacheOptions,
        this.guild._cacheOptions,
      )
    )
      this.guild.voiceStates.set(data.user_id, this);
  }

  /**
   * Is server deafened?
   * @readonly
   * @type {Boolean}
   * @public
   */
  get deaf() {
    return (this.#_attributes & (0b1 << 0)) == 0b1 << 0;
  }

  /**
   * Is server muted?
   * @readonly
   * @type {Boolean}
   * @public
   */
  get mute() {
    return (this.#_attributes & (0b1 << 1)) == 0b1 << 1;
  }

  /**
   * Is self defeaned?
   * @readonly
   * @type {Boolean}
   * @public
   */
  get selfDeaf() {
    return (this.#_attributes & (0b1 << 2)) == 0b1 << 2;
  }

  /**
   * Is self muted?
   * @readonly
   * @type {Boolean}
   * @public
   */
  get selfMute() {
    return (this.#_attributes & (0b1 << 3)) == 0b1 << 3;
  }

  /**
   * Is streaming?
   * @readonly
   * @type {Boolean}
   * @public
   */
  get selfStream() {
    return (this.#_attributes & (0b1 << 4)) == 0b1 << 4;
  }

  /**
   * Is sharing video?
   * @readonly
   * @type {Boolean}
   * @public
   */
  get selfVideo() {
    return (this.#_attributes & (0b1 << 5)) == 0b1 << 5;
  }

  /**
   * Is suppressed (for stage channels)?
   * @readonly
   * @type {Boolean}
   * @public
   */
  get suppress() {
    return (this.#_attributes & (0b1 << 6)) == 0b1 << 6;
  }

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

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

  /**
   * The channel involved.
   * @type {Channel?}
   * @readonly
   * @public
   */
  get channel() {
    return this.guild?.channels.get(this.channelId) || null;
  }

  /**
   * The id of the channel involved.
   * @type {String}
   * @readonly
   * @public
   */
  get channelId() {
    return String(this.#_channel_id);
  }

  /**
   * The member the voice state is about.
   * @type {Member?}
   * @readonly
   * @public
   */
  get member() {
    return this.#member;
  }

  /**
   * The id of the user the voice state is about.
   * @type {String}
   * @readonly
   * @public
   */
  get memberId() {
    return String(this.#_user_id);
  }

  /**
   * The UNIX time the user joined the voice channel.
   * @type {Number}
   * @readonly
   * @public
   */
  get joined() {
    return this.#joined;
  }

  /**
   * The UNIX timestamp of when the user requested to speak.
   * @type {Number?}
   * @readonly
   * @public
   */
  get requestToSpeakTimestamp() {
    return this.#request_to_speak_timestamp;
  }

  /**
   * Determines whether the voice state should be cached.
   * @param {GluonCacheOptions} gluonCacheOptions The cache options for the client.
   * @param {GuildCacheOptions} guildCacheOptions The cache options for the guild.
   * @returns {Boolean}
   * @public
   * @static
   * @method
   */
  static shouldCache(gluonCacheOptions, guildCacheOptions) {
    if (!(gluonCacheOptions instanceof GluonCacheOptions))
      throw new TypeError(
        "GLUON: Gluon cache options must be a GluonCacheOptions.",
      );
    if (!(guildCacheOptions instanceof GuildCacheOptions))
      throw new TypeError(
        "GLUON: Guild cache options must be a GuildCacheOptions.",
      );
    if (gluonCacheOptions.cacheVoiceStates === false) return false;
    if (guildCacheOptions.voiceStateCaching === false) return false;
    return true;
  }

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

  /**
   * @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.STORAGE_FORMAT:
      case TO_JSON_TYPES_ENUM.CACHE_FORMAT: {
        return {
          guild_id: this.guildId,
          channel_id: this.channelId,
          _attributes: this.#_attributes,
          member: this.member.toJSON(format),
          user_id: this.memberId,
          joined: this.joined,
          request_to_speak_timestamp: this.requestToSpeakTimestamp * 1000,
        };
      }
      case TO_JSON_TYPES_ENUM.DISCORD_FORMAT:
      default: {
        return {
          guild_id: this.guildId,
          channel_id: this.channelId,
          deaf: this.deaf,
          mute: this.mute,
          self_deaf: this.selfDeaf,
          self_mute: this.selfMute,
          self_stream: this.selfStream,
          self_video: this.selfVideo,
          suppress: this.suppress,
          member: this.member,
          user_id: this.memberId,
          joined: this.joined,
          request_to_speak_timestamp: this.requestToSpeakTimestamp * 1000,
        };
      }
    }
  }
}

export default VoiceState;