structures/User.js

import getTimestamp from "../util/discord/getTimestampFromSnowflake.js";
import { CDN_BASE_URL, TO_JSON_TYPES_ENUM } from "../constants.js";
import GluonCacheOptions from "../managers/GluonCacheOptions.js";
import util from "util";
import Client from "../Client.js";

/**
 * Represents a Discord user.
 * @see {@link https://discord.com/developers/docs/resources/user}
 */
class User {
  #_client;
  #_id;
  #_attributes;
  #_avatar;
  #username;
  #global_name;
  #discriminator;
  #_cached;
  #overrideAvatar;
  /**
   * Creates a structure for a user.
   * @param {Client} client The client instance.
   * @param {Object} data The raw user data.
   * @param {Object?} options Additional options for this structure.
   * @param {Boolean?} options.nocache Whether the user should be cached or not.
   * @see {@link https://discord.com/developers/docs/resources/user#user-object}
   */
  constructor(
    client,
    data,
    { 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 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 user.
     * @type {BigInt}
     * @private
     */
    this.#_id = BigInt(data.id);

    /**
     * The attributes of the user.
     * @type {Number}
     * @private
     */
    this.#_attributes = 0;

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

    if (data.avatar && data.avatar.startsWith("a_") == true)
      this.#_attributes |= 0b1 << 1;

    /**
     * The avatar of the user.
     * @type {BigInt?}
     * @private
     */
    this.#_avatar = data.avatar
      ? BigInt(`0x${data.avatar.replace("a_", "")}`)
      : null;

    /**
     * The username of the user.
     * @type {String}
     * @private
     */
    this.#username = data.username;

    /**
     * The global name of the user.
     * @type {String}
     * @private
     */
    this.#global_name = data.global_name;

    if (data.discriminator && data.discriminator != 0)
      /**
       * The discriminator of the user (only if user is a bot).
       * @type {Number?}
       * @private
       */
      this.#discriminator = Number(data.discriminator);

    /**
     * The UNIX (seconds) timestamp when this user was last cached.
     * @type {Number}
     * @private
     */
    if (data._cached) this.#_cached = data._cached;
    else this.#_cached = (new Date().getTime() / 1000) | 0;

    if (nocache === false && User.shouldCache(this.#_client._cacheOptions)) {
      this.#_client.users.set(data.id, this);
    }
  }

  /**
   * Overrides the user's avatar with a custom URL.
   * @param {String} url The URL of the avatar to override the user's avatar with.
   * @public
   * @method
   * @returns {void}
   */
  overrideAvatarURL(url) {
    this.#overrideAvatar = url;
  }

  /**
   * The ID of the user.
   * @type {String}
   * @readonly
   * @public
   */
  get id() {
    return String(this.#_id);
  }

  /**
   * The username of the user.
   * @type {String}
   * @readonly
   * @public
   */
  get username() {
    return this.#username;
  }

  /**
   * The global name of the user.
   * @type {String}
   * @readonly
   * @public
   */
  get globalName() {
    return this.#global_name;
  }

  /**
   * The discriminator of the user.
   * @type {String?}
   * @readonly
   * @public
   */
  get discriminator() {
    if (!this.#discriminator) return null;
    let formattedDiscriminator = String(this.#discriminator);
    while (formattedDiscriminator.length != 4)
      // eslint-disable-next-line quotes
      formattedDiscriminator = `0${formattedDiscriminator}`;
    return formattedDiscriminator;
  }

  /**
   * The UNIX (seconds) timestamp when this user was last cached.
   * @type {Number}
   * @readonly
   * @public
   */
  get _cached() {
    return this.#_cached;
  }

  /**
   * The mention string for the user.
   * @type {String}
   * @readonly
   * @public
   */
  get mention() {
    return User.getMention(this.id);
  }

  /**
   * The hash of the users's avatar, as it was received from Discord.
   * @readonly
   * @type {String?}
   * @private
   */
  get #_originalAvatarHash() {
    return this.#_avatar
      ? // eslint-disable-next-line quotes
        `${this.avatarIsAnimated ? "a_" : ""}${this.#_formattedAvatarHash}`
      : null;
  }

  /**
   * The hash of the users's avatar as a string.
   * @readonly
   * @type {String}
   * @private
   */
  get #_formattedAvatarHash() {
    if (!this.#_avatar) return null;

    let formattedHash = this.#_avatar.toString(16);

    while (formattedHash.length != 32)
      // eslint-disable-next-line quotes
      formattedHash = `0${formattedHash}`;

    return formattedHash;
  }

  /**
   * The avatar URL of the user.
   * @readonly
   * @type {String}
   * @public
   */
  get displayAvatarURL() {
    if (this.#overrideAvatar) return this.#overrideAvatar;

    return User.getAvatarUrl(this.id, this.#_originalAvatarHash);
  }

  /**
   * The username of the user, including their discriminator if they are a bot (username#0001).
   * @readonly
   * @type {String}
   * @public
   */
  get tag() {
    return this.#discriminator
      ? `${this.#username}#${this.discriminator}`
      : this.#username;
  }

  /**
   * The UNIX (seconds) timestamp of when this user created their Discord account.
   * @readonly
   * @type {Number}
   * @public
   */
  get createdTimestamp() {
    return getTimestamp(this.id);
  }

  /**
   * Whether the user is a bot or not.
   * @readonly
   * @type {Boolean}
   * @public
   */
  get bot() {
    return (this.#_attributes & (0b1 << 0)) == 0b1 << 0;
  }

  /**
   * Whether the user has an animated avatar or not.
   * @readonly
   * @type {Boolean}
   * @public
   */
  get avatarIsAnimated() {
    return (this.#_attributes & (0b1 << 1)) == 0b1 << 1;
  }

  /**
   * Returns a mention string for the user.
   * @param {String} id The ID of the user to mention.
   * @returns {String}
   * @public
   * @static
   * @method
   */
  static getMention(id) {
    if (typeof id !== "string")
      throw new TypeError("GLUON: User id must be a string.");
    return `<@${id}>`;
  }

  /**
   * Returns the URL to the user's avatar.
   * @param {String} id The ID of the user to get the avatar for.
   * @param {String?} hash The hash of the avatar.
   * @returns {String}
   * @public
   * @static
   * @method
   */
  static getAvatarUrl(id, hash) {
    if (typeof id !== "string")
      throw new TypeError("GLUON: User id must be a string.");
    if (hash && typeof hash !== "string")
      throw new TypeError("GLUON: Avatar hash must be a string.");
    return hash
      ? // eslint-disable-next-line quotes
        `${CDN_BASE_URL}/avatars/${id}/${hash}.${
          hash.startsWith("a_") == true ? "gif" : "png"
        }`
      : `${CDN_BASE_URL}/embed/avatars/${String((BigInt(id) >> 22n) % 6n)}.png`;
  }

  /**
   * Determines whether the user 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) {
    if (!(gluonCacheOptions instanceof GluonCacheOptions))
      throw new TypeError(
        "GLUON: Gluon cache options must be a GluonCacheOptions.",
      );
    if (gluonCacheOptions.cacheUsers === false) return false;
    return true;
  }

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

  /**
   * @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: {
        return {
          id: this.id,
          avatar: this.#_originalAvatarHash,
          _cached: this._cached,
          bot: this.bot,
          username: this.username,
          global_name: this.globalName,
          discriminator: this.discriminator,
        };
      }
      case TO_JSON_TYPES_ENUM.STORAGE_FORMAT:
      case TO_JSON_TYPES_ENUM.DISCORD_FORMAT:
      default: {
        return {
          id: this.id,
          avatar: this.#_originalAvatarHash,
          bot: this.bot,
          username: this.username,
          global_name: this.globalName,
          discriminator: this.discriminator,
        };
      }
    }
  }
}

export default User;