structures/Message.js

import User from "./User.js";
import Member from "./Member.js";
import Attachment from "./Attachment.js";
import {
  PERMISSIONS,
  BASE_URL,
  TO_JSON_TYPES_ENUM,
  LIMITS,
  MESSAGE_FLAGS,
} from "../constants.js";
import checkPermission from "../util/discord/checkPermission.js";
import Sticker from "./Sticker.js";
import getTimestamp from "../util/discord/getTimestampFromSnowflake.js";
import MessageReactionManager from "../managers/MessageReactionManager.js";
import Poll from "./Poll.js";
import Embed from "../util/builder/embedBuilder.js";
import GluonCacheOptions from "../managers/GluonCacheOptions.js";
import GuildCacheOptions from "../managers/GuildCacheOptions.js";
import ChannelCacheOptions from "../managers/ChannelCacheOptions.js";
import util from "util";
import MessageComponents from "../util/builder/messageComponents.js";
import encryptStructure from "../util/gluon/encryptStructure.js";
import structureHashName from "../util/general/structureHashName.js";
import decryptStructure from "../util/gluon/decryptStructure.js";
import Client from "../Client.js";
import FileUpload from "../util/builder/fileUpload.js";
import GuildChannelsManager from "../managers/GuildChannelsManager.js";
import GuildManager from "../managers/GuildManager.js";

/**
 * A message belonging to a channel within a guild.
 */
class Message {
  #_client;
  #_guild_id;
  #_channel_id;
  #_id;
  #author;
  #member;
  #attachments;
  #content;
  #poll;
  #reactions;
  #embeds;
  #_attributes;
  #reference;
  #type;
  #webhook_id;
  #sticker_items;
  #message_snapshots;
  #edited_timestamp;
  #flags;
  /**
   * Creates the structure for a message.
   * @param {Client} client The client instance.
   * @param {Object} data Message data returned from Discord.
   * @param {Object} options Additional options for this structure.
   * @param {String} options.channelId The id of the channel that the message belongs to.
   * @param {String} options.guildId The id of the guild that the channel belongs to.
   * @param {Boolean?} options.nocache Whether this message should be cached or not.
   * @param {Boolean?} options.ignoreExisting Whether to ignore existing messages in the cache.
   * @see {@link https://discord.com/developers/docs/resources/channel#message-object}
   */
  constructor(
    client,
    data,
    { channelId, guildId, nocache = false, ignoreExisting = false } = {
      nocache: false,
      ignoreExisting: 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 channelId !== "string")
      throw new TypeError("GLUON: Channel ID must be a string");
    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");
    if (typeof ignoreExisting !== "boolean")
      throw new TypeError("GLUON: Ignore existing must be a boolean");

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

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

    /**
     * The id of the channel that this message belongs to.
     * @type {BigInt}
     * @private
     */
    this.#_channel_id = BigInt(channelId);

    const existing =
      ignoreExisting != true
        ? this.channel?.messages.get(data.id) || null
        : null;

    /**
     * The id of the message.
     * @type {BigInt}
     * @private
     */
    this.#_id = BigInt(data.id);

    /**
     * The timestamp for when this message was last edited.
     * @type {Number?}
     * @private
     */
    if (data.edited_timestamp)
      this.#edited_timestamp =
        (new Date(data.edited_timestamp).getTime() / 1000) | 0;
    else if (existing?.editedTimestamp)
      this.#edited_timestamp = existing.editedTimestamp;

    // messages only ever need to be cached if logging is enabled
    // but this should always return a "refined" message, so commands can be handled
    if (data.author)
      /**
       * The message author.
       * @type {User}
       * @private
       */
      this.#author = new User(this.#_client, data.author, {
        nocache: !data.webhook_id || nocache,
        noDbStore: true,
      });
    else if (existing?.author) this.#author = existing.author;

    if (data.member)
      /**
       * The member who sent the message.
       * @type {Member?}
       * @private
       */
      this.#member = new Member(this.#_client, data.member, {
        userId: data.author.id,
        guildId,
        user: new User(this.#_client, data.author),
      });
    else if (data.author)
      this.#member = this.guild?.members.get(data.author.id) || null;
    else if (existing?.member) this.#member = existing.member;

    // should only be stored if file logging is enabled
    /**
     * The message attachments.
     * @type {Attachment[]?}
     * @private
     */
    this.#attachments = [];
    if (data.attachments != undefined)
      for (let i = 0; i < data.attachments.length; i++)
        this.#attachments.push(
          new Attachment(this.#_client, data.attachments[i], {
            channelId: this.channelId,
          }),
        );
    else if (existing?.attachments) this.#attachments = existing.attachments;

    /**
     * The message content.
     * @type {String?}
     * @private
     */
    if (this.channel._cacheOptions.contentCaching === true) {
      this.#content = data.content;
      if (!this.#content && existing && existing.content)
        this.#content = existing.content;
      else if (!this.#content) this.#content = null;
    }

    if (this.channel._cacheOptions.pollCaching === true) {
      /**
       * The message poll.
       * @type {Object?}
       * @private
       */
      if (data.poll)
        this.#poll = new Poll(this.#_client, data.poll, { guildId });
      else if (
        this.#poll == undefined &&
        existing &&
        existing.poll != undefined
      )
        this.#poll = existing.poll;
      else if (this.#poll == undefined) this.#poll = undefined;
    }

    if (this.channel._cacheOptions.reactionCaching === true) {
      if (existing?.reactions)
        /**
         * The message reactions.
         * @type {MessageReactionManager}
         * @private
         */
        this.#reactions = existing.reactions;
      else
        this.#reactions = new MessageReactionManager(
          this.#_client,
          this.guild,
          data.messageReactions,
        );
    }

    if (this.channel._cacheOptions.embedCaching === true) {
      /**
       * The message embeds.
       * @type {Embed[]}
       * @private
       */
      if (data.embeds) this.#embeds = data.embeds.map((e) => new Embed(e));
      else if (existing && existing.embeds != undefined)
        this.#embeds = existing.embeds;
      else if (this.#embeds == undefined) this.#embeds = [];
    }

    if (this.channel._cacheOptions.attributeCaching === true) {
      /**
       * The message attributes.
       * @type {Number}
       * @private
       */
      this.#_attributes = data._attributes || 0;

      if (data.mentions && data.mentions.length != 0)
        this.#_attributes |= 0b1 << 0;
      else if (
        data.mentions == undefined &&
        existing &&
        existing.mentions == true
      )
        this.#_attributes |= 0b1 << 0;

      if (data.mention_roles && data.mention_roles.length != 0)
        this.#_attributes |= 0b1 << 1;
      else if (
        data.mention_roles == undefined &&
        existing &&
        existing.mentionRoles == true
      )
        this.#_attributes |= 0b1 << 1;

      if (data.mention_everyone != undefined && data.mention_everyone == true)
        this.#_attributes |= 0b1 << 2;
      else if (
        data.mention_everyone == undefined &&
        existing &&
        existing.mentionEveryone == true
      )
        this.#_attributes |= 0b1 << 2;

      if (data.pinned != undefined && data.pinned == true)
        this.#_attributes |= 0b1 << 3;
      else if (data.pinned == undefined && existing && existing.pinned == true)
        this.#_attributes |= 0b1 << 3;

      if (data.mirrored != undefined && data.mirrored == true)
        this.#_attributes |= 0b1 << 4;
      else if (
        data.mirrored == undefined &&
        existing &&
        existing.mirrored == true
      )
        this.#_attributes |= 0b1 << 4;
    }

    if (this.channel._cacheOptions.referenceCaching === true) {
      /**
       * The message that this message references.
       * @type {Object}
       * @private
       */
      this.#reference = {};
      if (data.referenced_message)
        this.#reference.message_id = BigInt(data.referenced_message.id);
      else if (existing && existing.reference?.messageId)
        this.#reference.message_id = existing.reference.messageId;
    }

    /**
     * The flags of the message.
     * @type {Number}
     * @private
     */
    this.#flags = data.flags;

    /**
     * The type of message.
     * @type {Number}
     * @private
     */
    this.#type = data.type;
    if (
      typeof this.#type != "number" &&
      existing &&
      typeof existing.type == "number"
    )
      this.#type = existing.type;

    if (this.channel._cacheOptions.webhookCaching === true) {
      /**
       * The id of the webhook this message is from.
       * @type {BigInt?}
       * @private
       */
      if (data.webhook_id) this.#webhook_id = BigInt(data.webhook_id);
      else if (existing?.webhookId) this.#webhook_id = existing.webhookId;
    }

    if (this.channel._cacheOptions.stickerCaching === true) {
      /**
       * Stickers sent with this message.
       * @type {Sticker[]}
       * @private
       */
      this.#sticker_items = [];
      if (data.sticker_items != undefined)
        for (let i = 0; i < data.sticker_items.length; i++)
          this.#sticker_items.push(
            new Sticker(this.#_client, data.sticker_items[i]),
          );
      else if (existing && existing.stickerItems != undefined)
        this.#sticker_items = existing.stickerItems;
    }

    if (this.channel._cacheOptions.referenceCaching === true) {
      /**
       * The snapshot data about the message.
       * @type {Object?}
       * @private
       */
      if (data.message_snapshots)
        this.#message_snapshots = data.message_snapshots;
      else if (existing && existing.messageSnapshots != undefined)
        this.#message_snapshots = existing.messageSnapshots;
    }

    if (
      nocache === false &&
      Message.shouldCache(
        this.#_client._cacheOptions,
        this.guild._cacheOptions,
        this.channel._cacheOptions,
      ) &&
      ((this.#attachments.length !== 0 &&
        this.channel._cacheOptions.fileCaching === true) ||
        (this.#content && this.channel._cacheOptions.contentCaching === true) ||
        (this.#poll && this.channel._cacheOptions.pollCaching === true) ||
        (this.#reactions &&
          this.channel._cacheOptions.reactionCaching === true) ||
        (this.#embeds.length !== 0 &&
          this.channel._cacheOptions.embedCaching === true) ||
        (this.#_attributes !== 0 &&
          this.channel._cacheOptions.attributeCaching === true) ||
        (this.#reference.message_id &&
          this.channel._cacheOptions.referenceCaching === true) ||
        (this.#webhook_id &&
          this.channel._cacheOptions.webhookCaching === true) ||
        (this.#sticker_items.length !== 0 &&
          this.channel._cacheOptions.stickerCaching === true) ||
        (this.#message_snapshots &&
          this.channel._cacheOptions.referenceCaching === true))
    )
      this.channel?.messages.set(data.id, this);
  }

  /**
   * The timestamp for when this message was last edited.
   * @type {Number?}
   * @readonly
   * @public
   */
  get editedTimestamp() {
    return this.#edited_timestamp;
  }

  /**
   * The user who sent the message.
   * @type {User}
   * @readonly
   * @public
   */
  get author() {
    return this.#author;
  }

  /**
   * The id of the user who sent the message.
   * @type {String}
   * @readonly
   * @public
   */
  get authorId() {
    return this.#author.id;
  }

  /**
   * The member who sent the message.
   * @type {Member?}
   * @readonly
   * @public
   */
  get member() {
    return this.#member;
  }

  /**
   * Whether this message includes user mentions.
   * @readonly
   * @type {Boolean}
   * @public
   */
  get mentions() {
    return (this.#_attributes & (0b1 << 0)) == 0b1 << 0;
  }

  /**
   * Whether this message includes role mentions.
   * @readonly
   * @type {Boolean}
   * @public
   */
  get mentionRoles() {
    return (this.#_attributes & (0b1 << 1)) == 0b1 << 1;
  }

  /**
   * Whether this message mentions everyone.
   * @readonly
   * @type {Boolean}
   * @public
   */
  get mentionEveryone() {
    return (this.#_attributes & (0b1 << 2)) == 0b1 << 2;
  }

  /**
   * Whether this message has been pinned.
   * @readonly
   * @type {Boolean}
   * @public
   */
  get pinned() {
    return (this.#_attributes & (0b1 << 3)) == 0b1 << 3;
  }

  /**
   * Whether another message has replaced this original message.
   * @readonly
   * @type {Boolean}
   * @public
   */
  get mirrored() {
    return (this.#_attributes & (0b1 << 4)) == 0b1 << 4;
  }

  /**
   * The UNIX (seconds) timestamp for when this message was created.
   * @readonly
   * @type {Number}
   * @public
   */
  get timestamp() {
    return getTimestamp(this.id);
  }

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

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

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

  /**
   * The channel that this message belongs to.
   * @type {String}
   * @readonly
   * @public
   */
  get channelId() {
    return String(this.#_channel_id);
  }

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

  /**
   * The message attachments.
   * @type {Attachment[]}
   * @readonly
   * @public
   */
  get attachments() {
    return this.#attachments;
  }

  /**
   * The message content.
   * @type {String?}
   * @readonly
   * @public
   */
  get content() {
    return this.#content;
  }

  /**
   * The message poll.
   * @type {Poll?}
   * @readonly
   * @public
   */
  get poll() {
    return this.#poll;
  }

  /**
   * The message reactions.
   * @type {MessageReactionManager}
   * @readonly
   * @public
   */
  get reactions() {
    return this.#reactions;
  }

  /**
   * The message embeds.
   * @type {Array<Embed>}
   * @readonly
   * @public
   */
  get embeds() {
    return this.#embeds;
  }

  /**
   * The message that this message references.
   * @type {Object}
   * @readonly
   * @public
   */
  get reference() {
    return {
      messageId: this.#reference.message_id
        ? String(this.#reference.message_id)
        : undefined,
    };
  }

  /**
   * The flags of the message.
   * @type {String[]}
   * @readonly
   * @public
   * @see {@link https://discord.com/developers/docs/resources/message#message-object-message-flags}
   */
  get flags() {
    const flags = [];
    if ((this.#flags & MESSAGE_FLAGS.CROSSPOSTED) === MESSAGE_FLAGS.CROSSPOSTED)
      flags.push("CROSSPOSTED");
    if (
      (this.#flags & MESSAGE_FLAGS.IS_CROSSPOST) ===
      MESSAGE_FLAGS.IS_CROSSPOST
    )
      flags.push("IS_CROSSPOST");
    if (
      (this.#flags & MESSAGE_FLAGS.SUPPRESS_EMBEDS) ===
      MESSAGE_FLAGS.SUPPRESS_EMBEDS
    )
      flags.push("SUPPRESS_EMBEDS");
    if (
      (this.#flags & MESSAGE_FLAGS.SOURCE_MESSAGE_DELETED) ===
      MESSAGE_FLAGS.SOURCE_MESSAGE_DELETED
    )
      flags.push("SOURCE_MESSAGE_DELETED");
    if ((this.#flags & MESSAGE_FLAGS.URGENT) === MESSAGE_FLAGS.URGENT)
      flags.push("URGENT");
    if ((this.#flags & MESSAGE_FLAGS.HAS_THREAD) === MESSAGE_FLAGS.HAS_THREAD)
      flags.push("HAS_THREAD");
    if ((this.#flags & MESSAGE_FLAGS.EPHEMERAL) === MESSAGE_FLAGS.EPHEMERAL)
      flags.push("EPHEMERAL");
    if ((this.#flags & MESSAGE_FLAGS.LOADING) === MESSAGE_FLAGS.LOADING)
      flags.push("LOADING");
    if (
      (this.#flags & MESSAGE_FLAGS.FAILED_TO_MENTION_SOME_ROLES_IN_THREAD) ===
      MESSAGE_FLAGS.FAILED_TO_MENTION_SOME_ROLES_IN_THREAD
    )
      flags.push("FAILED_TO_MENTION_SOME_ROLES_IN_THREAD");
    if (
      (this.#flags & MESSAGE_FLAGS.SUPPRESS_NOTIFICATIONS) ===
      MESSAGE_FLAGS.SUPPRESS_NOTIFICATIONS
    )
      flags.push("SUPPRESS_NOTIFICATIONS");
    if (
      (this.#flags & MESSAGE_FLAGS.IS_VOICE_MESSAGE) ===
      MESSAGE_FLAGS.IS_VOICE_MESSAGE
    )
      flags.push("IS_VOICE_MESSAGE");
    return flags;
  }

  /**
   * The raw flags of the message.
   * @type {Number}
   * @readonly
   * @public
   */
  get flagsRaw() {
    return this.#flags;
  }

  /**
   * The type of message.
   * @type {Number}
   * @readonly
   * @public
   */
  get type() {
    return this.#type;
  }

  /**
   * The id of the webhook this message is from.
   * @type {String?}
   * @readonly
   * @public
   */
  get webhookId() {
    return this.#webhook_id ? String(this.#webhook_id) : null;
  }

  /**
   * Stickers sent with this message.
   * @type {Sticker[]}
   * @readonly
   * @public
   */
  get stickerItems() {
    return this.#sticker_items;
  }

  /**
   * The snapshot data about the message.
   * @type {Object?}
   * @readonly
   * @public
   */
  get messageSnapshots() {
    return this.#message_snapshots;
  }

  /**
   * The URL of the message.
   * @type {String}
   * @readonly
   * @public
   */
  get url() {
    return Message.getUrl(this.guildId, this.channelId, this.id);
  }

  /**
   * The hash name for the message.
   * @type {String}
   * @readonly
   * @public
   */
  get hashName() {
    return Message.getHashName(this.guildId, this.channelId, this.id);
  }

  /**
   * The URL of the message.
   * @param {String} guildId The id of the guild that the message belongs to.
   * @param {String} channelId The id of the channel that the message belongs to.
   * @param {String} messageId The id of the message.
   * @returns {String}
   * @public
   * @static
   * @method
   */
  static getUrl(guildId, channelId, messageId) {
    if (typeof guildId !== "string")
      throw new TypeError("GLUON: Guild ID must be a string.");
    if (typeof channelId !== "string")
      throw new TypeError("GLUON: Channel ID must be a string.");
    if (typeof messageId !== "string")
      throw new TypeError("GLUON: Message ID must be a string.");
    return `${BASE_URL}/channels/${guildId}/${channelId}/${messageId}`;
  }

  /**
   * Replies to the message.
   * @param {Object?} options Embeds, components and files to attach to the message.
   * @param {String?} options.content The message content.
   * @param {Embed?} options.embed Embed to send with the message.
   * @param {MessageComponents?} options.components Message components to send with the message.
   * @param {Array<FileUpload>?} options.files Array of file objects for files to send with the message.
   * @returns {Promise<Message>}
   * @see {@link https://discord.com/developers/docs/resources/channel#create-message}
   * @method
   * @public
   * @async
   * @throws {Error | TypeError}
   */
  reply({ content, embeds, components, files, suppressMentions = false } = {}) {
    return Message.send(this.#_client, this.channelId, this.guildId, {
      content,
      reference: {
        message_id: this.id,
        channel_id: this.channelId,
        guild_id: this.guildId,
      },
      embeds,
      components,
      files,
      suppressMentions,
    });
  }

  /**
   * Edits the message, assuming it is sent by the client user.
   * @param {Object?} options Content, embeds and components to attach to the message.
   * @param {String?} options.content The message content.
   * @param {Embed?} options.embed Embed to send with the message.
   * @param {MessageComponents?} options.components Message components to send with the message.
   * @param {Array<Attachment>?} options.attachments Array of attachment objects for files to send with the message.
   * @param {Number?} options.flags The message flags.
   * @param {Object?} options.reference The message reference.
   * @param {String?} options.reference.message_id The id of the message to reference.
   * @param {String?} options.reference.channel_id The id of the channel to reference.
   * @param {String?} options.reference.guild_id The id of the guild to reference.
   * @param {FileUpload[]?} options.files Array of file objects for files to send with the message.
   * @returns {Promise<Message>}
   * @see {@link https://discord.com/developers/docs/resources/channel#edit-message}
   * @method
   * @public
   * @async
   * @throws {Error | TypeError}
   */
  edit(
    {
      components,
      files,
      content = this.content,
      embeds = this.embeds,
      attachments = this.attachments,
      flags = this.flagsRaw,
      reference = {
        message_id: this.reference.messageId,
        channel_id: this.channelId,
        guild_id: this.guildId,
      },
    } = {
      components: null,
      files: null,
      content: null,
      embeds: null,
      attachments: null,
      flags: null,
      reference: null,
    },
  ) {
    return Message.edit(this.#_client, this.channelId, this.id, this.guildId, {
      content,
      components,
      files,
      embeds,
      attachments,
      flags,
      reference,
    });
  }

  /**
   * Deletes the message.
   * @param {Object?} options The options for deleting the message.
   * @param {String?} options.reason The reason for deleting the message
   * @returns {Promise<void>}
   * @method
   * @public
   * @async
   */
  delete({ reason } = {}) {
    return Message.delete(this.#_client, this.channelId, this.id, { reason });
  }

  /**
   * Determines whether the message should be cached.
   * @param {GluonCacheOptions} gluonCacheOptions The cache options for the client.
   * @param {GuildCacheOptions} guildCacheOptions The cache options for the guild.
   * @param {ChannelCacheOptions} channelCacheOptions The cache options for the channel.
   * @returns {Boolean}
   * @public
   * @static
   * @method
   */
  static shouldCache(
    gluonCacheOptions,
    guildCacheOptions,
    channelCacheOptions,
  ) {
    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 (!(channelCacheOptions instanceof ChannelCacheOptions))
      throw new TypeError(
        "GLUON: Channel cache options must be a ChannelCacheOptions.",
      );
    if (gluonCacheOptions.cacheMessages === false) return false;
    if (guildCacheOptions.messageCaching === false) return false;
    if (channelCacheOptions.messageCaching === false) return false;
    return true;
  }

  /**
   * Posts a message to the specified channel.
   * @param {Client} client The client instance.
   * @param {String} channelId The id of the channel to send the message to.
   * @param {String} guildId The id of the guild which the channel belongs to.
   * @param {Object?} options Content, embeds, components and files to attach to the message.
   * @param {String?} options.content The message content.
   * @param {Embed[]} options.embeds Array of embeds to send with the message.
   * @param {MessageComponents?} options.components Message components to send with the message.
   * @param {Array<FileUpload>?} options.files Array of file objects for files to send with the message.
   * @param {Boolean?} options.suppressMentions Whether to suppress mentions in the message.
   * @returns {Promise<Message>}
   * @public
   * @method
   * @async
   * @throws {TypeError}
   */
  static async send(
    client,
    channelId,
    guildId,
    {
      content,
      embeds,
      components,
      files,
      reference,
      suppressMentions = false,
    } = {
      suppressMentions: false,
    },
  ) {
    if (!(client instanceof Client))
      throw new TypeError("GLUON: Client must be a Client instance.");
    if (typeof channelId !== "string")
      throw new TypeError("GLUON: Channel ID is not a string.");
    if (typeof guildId !== "string")
      throw new TypeError("GLUON: Guild ID is not a string.");

    Message.sendValidation({ content, embeds, components, files, reference });

    if (typeof suppressMentions !== "boolean")
      throw new TypeError("GLUON: Suppress mentions is not a boolean.");

    if (
      !checkPermission(
        GuildChannelsManager.getChannel(
          client,
          guildId,
          channelId,
        ).checkPermission(await GuildManager.getGuild(client, guildId).me()),
        PERMISSIONS.SEND_MESSAGES,
      )
    )
      throw new Error("MISSING PERMISSIONS: SEND_MESSAGES");

    const body = {};

    if (content) body.content = content;

    if (embeds && embeds.length !== 0) body.embeds = embeds;
    if (components) body.components = components;
    if (files) body.files = files;
    if (suppressMentions === true) {
      body.allowed_mentions = {};
      body.allowed_mentions.parse = [];
    }
    if (reference) body.message_reference = reference;

    const data = await client.request.makeRequest(
      "postCreateMessage",
      [channelId],
      body,
    );

    return new Message(client, data, {
      channelId,
      guildId,
    });
  }

  /**
   * Edits a message.
   * @param {Client} client The client instance.
   * @param {String} channelId The id of the channel the message belongs to.
   * @param {String} messageId The id of the message.
   * @param {String} guildId The id of the guild the message belongs to.
   * @param {Object?} options The message options.
   * @param {String?} options.content The message content.
   * @param {Embed[]?} options.embeds Array of embeds to send with the message.
   * @param {MessageComponents?} options.components Message components to send with the message.
   * @param {Array<FileUpload>?} options.files Array of file objects for files to send with the message.
   * @param {Array<Attachment>?} options.attachments Array of attachment objects for existing attachments sent with the message.
   * @returns {Promise<Message>}
   */
  static async edit(
    client,
    channelId,
    messageId,
    guildId,
    { content, embeds, components, attachments, files } = {},
  ) {
    if (!(client instanceof Client))
      throw new TypeError("GLUON: Client must be a Client instance.");
    if (typeof channelId !== "string")
      throw new TypeError("GLUON: Channel ID is not a string.");
    if (typeof guildId !== "string")
      throw new TypeError("GLUON: Guild ID is not a string.");
    if (typeof messageId !== "string")
      throw new TypeError("GLUON: Message ID is not a string.");

    Message.sendValidation({ content, embeds, components, attachments, files });

    if (
      !checkPermission(
        GuildChannelsManager.getChannel(
          client,
          guildId,
          channelId,
        ).checkPermission(await GuildManager.getGuild(client, guildId).me()),
        PERMISSIONS.SEND_MESSAGES,
      )
    )
      throw new Error("MISSING PERMISSIONS: SEND_MESSAGES");

    const body = {};

    body.content = content;
    body.embeds = embeds;
    body.components = components;
    body.attachments = attachments;
    body.files = files;

    const data = await client.request.makeRequest(
      "patchEditMessage",
      [channelId, messageId],
      body,
    );

    return new Message(client, data, {
      channelId,
      guildId,
    });
  }

  /**
   * Returns the hash name for the message.
   * @param {String} guildId The id of the guild that the message belongs to.
   * @param {String} channelId The id of the channel that the message belongs to.
   * @param {String} messageId The id of the message.
   * @returns {String}
   * @public
   * @static
   * @method
   * @throws {TypeError}
   */
  static getHashName(guildId, channelId, messageId) {
    if (typeof guildId !== "string")
      throw new TypeError("GLUON: Guild ID must be a string.");
    if (typeof channelId !== "string")
      throw new TypeError("GLUON: Channel ID must be a string.");
    if (typeof messageId !== "string")
      throw new TypeError("GLUON: Message ID must be a string.");
    return structureHashName(guildId, channelId, messageId);
  }

  /**
   * Decrypts a message.
   * @param {Client} client The client instance.
   * @param {String} data The encrypted message data.
   * @param {String} guildId The id of the guild that the message belongs to.
   * @param {String} channelId The id of the channel that the message belongs to.
   * @param {String} messageId The id of the message.
   * @returns {Message}
   * @public
   * @static
   * @method
   * @throws {TypeError}
   */
  static decrypt(client, data, guildId, channelId, messageId) {
    if (!(client instanceof Client))
      throw new TypeError("GLUON: Client must be a Client instance.");
    if (typeof data !== "string")
      throw new TypeError("GLUON: Data must be a string.");
    if (typeof guildId !== "string")
      throw new TypeError("GLUON: Guild ID must be a string.");
    if (typeof channelId !== "string")
      throw new TypeError("GLUON: Channel ID must be a string.");
    if (typeof messageId !== "string")
      throw new TypeError("GLUON: Message ID must be a string.");
    return new Message(
      client,
      decryptStructure(data, messageId, channelId, guildId),
      { channelId, guildId },
    );
  }

  /**
   * Validates the message content, embeds, components and files.
   * @param {Object} options The message options.
   * @param {String} options.content The message content.
   * @param {Embed[]} options.embeds Array of embeds to send with the message.
   * @param {MessageComponents} options.components Message components to send with the message.
   * @param {Array<FileUpload>} options.files Array of file objects for files to send with the message.
   * @param {Array<Attachment>} options.attachments Array of attachment objects for existing attachments sent with the message.
   * @param {Number} options.flags The message flags.
   * @param {Object} options.reference The message reference.
   * @param {String} options.reference.message_id The id of the message to reference.
   * @param {String} options.reference.channel_id The id of the channel to reference.
   * @param {String} options.reference.guild_id The id of the guild to reference.
   * @returns {void}
   * @throws {Error | TypeError | RangeError}
   * @public
   * @static
   * @method
   */
  static sendValidation({
    content,
    embeds,
    components,
    files,
    attachments,
    flags,
    reference,
  } = {}) {
    if (!content && !embeds && !components && !files)
      throw new Error(
        "GLUON: Must provide content, embeds, components or files",
      );

    if (typeof content !== "undefined" && typeof content !== "string")
      throw new TypeError("GLUON: Content must be a string.");

    if (content && content.length > LIMITS.MAX_MESSAGE_CONTENT)
      throw new RangeError(
        `GLUON: Content exceeds ${LIMITS.MAX_MESSAGE_CONTENT} characters.`,
      );

    if (
      typeof embeds !== "undefined" &&
      (!Array.isArray(embeds) || !embeds.every((e) => e instanceof Embed))
    )
      throw new TypeError("GLUON: Embeds must be an array of embeds.");

    if (embeds && embeds.length > LIMITS.MAX_MESSAGE_EMBEDS)
      throw new RangeError(
        `GLUON: Embeds exceeds ${LIMITS.MAX_MESSAGE_EMBEDS}.`,
      );

    if (
      typeof components !== "undefined" &&
      !(components instanceof MessageComponents)
    )
      throw new TypeError("GLUON: Components must be an array of components.");

    if (
      typeof files !== "undefined" &&
      (!Array.isArray(files) || !files.every((f) => f instanceof FileUpload))
    )
      throw new TypeError("GLUON: Files must be an array of files.");

    if (files && files.length > LIMITS.MAX_MESSAGE_FILES)
      throw new RangeError(`GLUON: Files exceeds ${LIMITS.MAX_MESSAGE_FILES}.`);

    if (
      typeof attachments !== "undefined" &&
      (!Array.isArray(attachments) ||
        !attachments.every((a) => a instanceof Attachment))
    )
      throw new TypeError(
        "GLUON: Attachments must be an array of attachments.",
      );

    if (attachments && attachments.length > LIMITS.MAX_MESSAGE_FILES)
      throw new RangeError(
        `GLUON: Attachments exceeds ${LIMITS.MAX_MESSAGE_FILES}.`,
      );

    if (typeof flags !== "undefined" && typeof flags !== "number")
      throw new TypeError("GLUON: Flags must be a number.");

    if (typeof reference !== "undefined" && typeof reference !== "object")
      throw new TypeError("GLUON: Reference must be an object.");
    if (reference && typeof reference.message_id !== "string")
      throw new TypeError("GLUON: Reference message id must be a string.");
    if (reference && typeof reference.channel_id !== "string")
      throw new TypeError("GLUON: Reference channel id must be a string.");
    if (reference && typeof reference.guild_id !== "string")
      throw new TypeError("GLUON: Reference guild id must be a string.");
  }

  /**
   * Deletes one message.
   * @param {Client} client The client instance.
   * @param {String} channelId The id of the channel that the message belongs to.
   * @param {String} messageId The id of the message to delete.
   * @param {Object?} options
   * @param {String?} options.reason The reason for deleting the message.
   * @returns {Promise<void>}
   * @public
   * @method
   * @async
   * @throws {TypeError}
   */
  static async delete(client, channelId, messageId, { reason } = {}) {
    if (!(client instanceof Client))
      throw new TypeError("GLUON: Client must be a Client instance.");
    if (typeof channelId !== "string")
      throw new TypeError("GLUON: Channel ID is not a string.");
    if (typeof messageId !== "string")
      throw new TypeError("GLUON: Message ID is not a string.");
    if (typeof reason !== "undefined" && typeof reason !== "string")
      throw new TypeError("GLUON: Reason is not a string.");

    const body = {};

    if (reason) body["X-Audit-Log-Reason"] = reason;

    await client.request.makeRequest(
      "deleteChannelMessage",
      [channelId, messageId],
      body,
    );
  }

  /**
   * Encrypts the message.
   * @returns {String}
   * @public
   * @method
   */
  encrypt() {
    return encryptStructure(this, this.id, this.channelId, this.guildId);
  }

  /**
   * @method
   * @public
   */
  toString() {
    return `<Message: ${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:
      case TO_JSON_TYPES_ENUM.STORAGE_FORMAT: {
        return {
          id: this.id,
          author: this.author.toJSON(format),
          member: this.member?.toJSON(format),
          content: this.content,
          _attributes: this.#_attributes,
          attachments: this.attachments.map((a) => a.toJSON(format)),
          embeds: this.embeds.map((e) => e.toJSON(format)),
          edited_timestamp: this.editedTimestamp
            ? this.editedTimestamp * 1000
            : null,
          poll: this.poll?.toJSON(format),
          message_snapshots: this.messageSnapshots,
          type: this.type,
          referenced_message: this.reference?.messageId
            ? {
                id: this.reference.messageId
                  ? this.reference.messageId
                  : undefined,
              }
            : undefined,
          sticker_items: this.stickerItems.map((s) => s.toJSON(format)),
          messageReactions: this.reactions.toJSON(format),
        };
      }
      case TO_JSON_TYPES_ENUM.DISCORD_FORMAT:
      default: {
        return {
          id: this.id,
          channel_id: this.channelId,
          author: this.author.toJSON(format),
          member: this.member?.toJSON(format),
          content: this.content,
          pinned: this.pinned,
          attachments: this.attachments.map((a) => a.toJSON(format)),
          embeds: this.embeds.map((e) => e.toJSON(format)),
          edited_timestamp: this.editedTimestamp
            ? this.editedTimestamp * 1000
            : null,
          poll: this.poll?.toJSON(format),
          message_snapshots: this.messageSnapshots,
          type: this.type,
          referenced_message: this.reference?.messageId
            ? {
                id: this.reference.messageId
                  ? this.reference.messageId
                  : undefined,
              }
            : undefined,
          sticker_items: this.stickerItems?.map((s) => s.toJSON(format)),
          reactions: this.reactions?.toJSON(format),
          mention_everyone: this.mentionEveryone,
          mention_roles: this.mentionRoles ? [""] : [],
          mentions: this.mentions ? [""] : [],
        };
      }
    }
  }
}

export default Message;