gateway/Shard.js

"use strict";

const util = require("util");
const Base = require("../structures/Base");
const Bucket = require("../util/Bucket");
const Channel = require("../structures/Channel");
const ForumChannel = require("../structures/ForumChannel");
const GuildChannel = require("../structures/GuildChannel");
const Message = require("../structures/Message");
const DMChannel = require("../structures/DMChannel");
const ExtendedUser = require("../structures/ExtendedUser");
const User = require("../structures/User");
const Invite = require("../structures/Invite");
const Interaction = require("../structures/Interaction");
const Constants = require("../Constants");
const ThreadChannel = require("../structures/ThreadChannel");
const StageInstance = require("../structures/StageInstance");
const GuildScheduledEvent = require("../structures/GuildScheduledEvent");
const GuildAuditLogEntry = require("../structures/GuildAuditLogEntry");
const AutoModerationRule = require("../structures/AutoModerationRule");
const SoundboardSound = require("../structures/SoundboardSound");

const WebSocket = typeof window !== "undefined" ? require("../util/BrowserWebSocket") : require("ws");

let EventEmitter;
try {
  EventEmitter = require("eventemitter3");
} catch {
  EventEmitter = require("events").EventEmitter;
}
let Erlpack;
try {
  Erlpack = require("erlpack");
} catch {} // eslint-disable-line no-empty
let ZlibSync;
try {
  ZlibSync = require("zlib-sync");
} catch {
  try {
    ZlibSync = require("pako");
  } catch {} // eslint-disable-line no-empty
}

/**
 * Represents a shard
 * @extends EventEmitter
 * @prop {Number} id The ID of the shard
 * @prop {Boolean} connecting Whether the shard is connecting
 * @prop {Array<String>?} discordServerTrace Debug trace of Discord servers
 * @prop {Number} lastHeartbeatReceived Last time Discord acknowledged a heartbeat, null if shard has not sent heartbeat yet
 * @prop {Number} lastHeartbeatSent Last time shard sent a heartbeat, null if shard has not sent heartbeat yet
 * @prop {Number} latency The current latency between the shard and Discord, in milliseconds
 * @prop {Boolean} ready Whether the shard is ready
 * @prop {String} status The status of the shard. "disconnected"/"connecting"/"handshaking"/"ready"/"identifying"/"resuming"
 */
class Shard extends EventEmitter {
  constructor(id, client) {
    super();

    this.id = id;
    this.client = client;

    this.onPacket = this.onPacket.bind(this);
    this._onWSOpen = this._onWSOpen.bind(this);
    this._onWSMessage = this._onWSMessage.bind(this);
    this._onWSError = this._onWSError.bind(this);
    this._onWSClose = this._onWSClose.bind(this);

    this.hardReset();
  }

  checkReady() {
    if (!this.ready) {
      if (this.getAllUsersQueue.length > 0) {
        this.requestGuildMembers(this.getAllUsersQueue);
        this.getAllUsersQueue = [];
        this.getAllUsersLength = 1;
        return;
      }
      if (Object.keys(this.getAllUsersCount).length === 0) {
        this.ready = true;
        /**
         * Fired when the shard turns ready
         * @event Shard#ready
         */
        super.emit("ready");
      }
    }
  }

  /**
   * Tells the shard to connect
   */
  connect() {
    if (this.ws && this.ws.readyState != WebSocket.CLOSED) {
      this.emit("error", new Error("Existing connection detected"), this.id);
      return;
    }
    ++this.connectAttempts;
    this.connecting = true;
    return this.initializeWS();
  }

  createGuild(_guild) {
    this.client.guildShardMap[_guild.id] = this.id;
    const guild = this.client.guilds.add(_guild, this.client, true);
    if (this.client.options.getAllUsers && guild.members.size < guild.memberCount) {
      this.getGuildMembers(guild.id, {
        presences: this.client.options.intents && this.client.options.intents & Constants.Intents.guildPresences,
      });
    }
    return guild;
  }

  /**
   * Disconnects the shard
   * @arg {Object?} [options] Shard disconnect options
   * @arg {String | Boolean} [options.reconnect] false means destroy everything, true means you want to reconnect in the future, "auto" will autoreconnect
   * @arg {Error} [error] The error that causes the disconnect
   */
  disconnect(options = {}, error) {
    if (!this.ws) {
      return;
    }

    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }

    if (this.ws.readyState !== WebSocket.CLOSED) {
      this.ws.removeListener("message", this._onWSMessage);
      this.ws.removeListener("close", this._onWSClose);
      try {
        if (options.reconnect && this.sessionID) {
          if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.close(4901, "Eris: reconnect");
          } else {
            this.emit("debug", `Terminating websocket (state: ${this.ws.readyState})`, this.id);
            this.ws.terminate();
          }
        } else {
          this.ws.close(1000, "Eris: normal");
        }
      } catch (err) {
        this.emit("error", err, this.id);
      }
    }
    this.ws = null;
    this.reset();

    if (error) {
      this.emit("error", error, this.id);
    }

    /**
     * Fired when the shard disconnects
     * @event Shard#disconnect
     * @prop {Error?} err The error, if any
     */
    super.emit("disconnect", error);

    if (this.sessionID && this.connectAttempts >= this.client.options.maxResumeAttempts) {
      this.emit("debug", `Automatically invalidating session due to excessive resume attempts | Attempt ${this.connectAttempts}`, this.id);
      this.sessionID = null;
      this.resumeURL = null;
    }

    if (options.reconnect === "auto" && this.client.options.autoreconnect) {
      /**
       * Fired when stuff happens and gives more info
       * @event Client#debug
       * @prop {String} message The debug message
       * @prop {Number} id The ID of the shard
       */
      if (this.sessionID) {
        this.emit("debug", `Immediately reconnecting for potential resume | Attempt ${this.connectAttempts}`, this.id);
        this.client.shards.connect(this);
      } else {
        this.emit("debug", `Queueing reconnect in ${this.reconnectInterval}ms | Attempt ${this.connectAttempts}`, this.id);
        setTimeout(() => {
          this.client.shards.connect(this);
        }, this.reconnectInterval);
        this.reconnectInterval = Math.min(Math.round(this.reconnectInterval * (Math.random() * 2 + 1)), 30000);
      }
    } else if (!options.reconnect) {
      this.hardReset();
    }
  }

  /**
   * Update the bot's AFK status.
   * @arg {Boolean} afk Whether the bot user is AFK or not
   */
  editAFK(afk) {
    this.presence.afk = !!afk;

    this.sendStatusUpdate();
  }

  /**
   * Updates the bot's status on all guilds the shard is in
   * @arg {String} [status] Sets the bot's status, either "online", "idle", "dnd", or "invisible"
   * @arg {Array | Object} [activities] Sets the bot's activities. A single activity object is also accepted for backwards compatibility
   * @arg {String} [activities[].name] The name of the activity. Note: When setting a custom status, use `state` instead
   * @arg {Number} activities[].type The type of the activity. 0 is playing, 1 is streaming (Twitch only), 2 is listening, 3 is watching, 4 is custom status, 5 is competing in
   * @arg {String} [activities[].url] The URL of the activity
   * @arg {String} [activities[].state] The state of the activity. This is the text to be displayed as the bots custom status
   */
  editStatus(status, activities) {
    if (activities === undefined && typeof status === "object") {
      activities = status;
      status = undefined;
    }
    if (status) {
      this.presence.status = status;
    }
    if (activities === null) {
      activities = [];
    } else if (activities && !Array.isArray(activities)) {
      activities = [activities];
    }
    if (activities !== undefined) {
      this.presence.activities = activities;
    }

    this.sendStatusUpdate();
  }

  emit(event, ...args) {
    this.client.emit.call(this.client, event, ...args);
    if (event !== "error" || this.listeners("error").length > 0) {
      super.emit.call(this, event, ...args);
    }
  }

  getGuildMembers(guildID, timeout) {
    if (this.getAllUsersCount.hasOwnProperty(guildID)) {
      throw new Error("Cannot request all members while an existing request is processing");
    }
    this.getAllUsersCount[guildID] = true;
    // Using intents, request one guild at a time
    if (this.client.options.intents) {
      if (!(this.client.options.intents & Constants.Intents.guildMembers)) {
        throw new Error("Cannot request all members without guildMembers intent");
      }
      this.requestGuildMembers([guildID], timeout);
    } else {
      if (this.getAllUsersLength + 3 + guildID.length > 4048) { // 4096 - "{\"op\":8,\"d\":{\"guild_id\":[],\"query\":\"\",\"limit\":0}}".length + 1 for lazy comma offset
        this.requestGuildMembers(this.getAllUsersQueue);
        this.getAllUsersQueue = [guildID];
        this.getAllUsersLength = 1 + guildID.length + 3;
      } else {
        this.getAllUsersQueue.push(guildID);
        this.getAllUsersLength += guildID.length + 3;
      }
    }
  }

  hardReset() {
    this.reset();
    this.seq = 0;
    this.sessionID = null;
    this.resumeURL = null;
    this.reconnectInterval = 1000;
    this.connectAttempts = 0;
    this.ws = null;
    this.heartbeatInterval = null;
    this.guildCreateTimeout = null;
    this.globalBucket = new Bucket(120, 60000, { reservedTokens: 5 });
    this.presenceUpdateBucket = new Bucket(5, 20000);
    this.presence = JSON.parse(JSON.stringify(this.client.presence)); // Fast copy
    Object.defineProperty(this, "_token", {
      configurable: true,
      enumerable: false,
      writable: true,
      value: this.client._token,
    });
  }

  heartbeat(normal) {
    // Can only heartbeat after identify/resume succeeds, session will be killed otherwise, discord/discord-api-docs#1619
    if (this.status === "resuming" || this.status === "identifying") {
      return;
    }
    if (normal) {
      if (!this.lastHeartbeatAck) {
        this.emit("debug", "Heartbeat timeout; " + JSON.stringify({
          lastReceived: this.lastHeartbeatReceived,
          lastSent: this.lastHeartbeatSent,
          interval: this.heartbeatInterval,
          status: this.status,
          timestamp: Date.now(),
        }));
        return this.disconnect({
          reconnect: "auto",
        }, new Error("Server didn't acknowledge previous heartbeat, possible lost connection"));
      }
      this.lastHeartbeatAck = false;
    }
    this.lastHeartbeatSent = Date.now();
    this.sendWS(Constants.GatewayOPCodes.HEARTBEAT, this.seq, true);
  }

  identify() {
    if (this.client.options.compress && !ZlibSync) {
      /**
       * Fired when the shard encounters an error
       * @event Client#error
       * @prop {Error} err The error
       * @prop {Number} id The ID of the shard
       */
      this.emit("error", new Error("pako/zlib-sync not found, cannot decompress data"));
      return;
    }
    this.status = "identifying";
    const identify = {
      token: this._token,
      v: Constants.GATEWAY_VERSION,
      compress: !!this.client.options.compress,
      large_threshold: this.client.options.largeThreshold,
      intents: this.client.options.intents,
      properties: {
        os: process.platform,
        browser: "Eris",
        device: "Eris",
      },
    };
    if (this.client.options.maxShards > 1) {
      identify.shard = [this.id, this.client.options.maxShards];
    }
    if (this.presence.status) {
      identify.presence = this.presence;
    }
    this.sendWS(Constants.GatewayOPCodes.IDENTIFY, identify);
  }

  initializeWS() {
    if (!this._token) {
      return this.disconnect(null, new Error("Token not specified"));
    }

    this.status = "connecting";
    if (this.client.options.compress) {
      this.emit("debug", "Initializing zlib-sync-based compression");
      this._zlibSync = new ZlibSync.Inflate({
        chunkSize: 128 * 1024,
      });
    }
    if (this.sessionID) {
      if (!this.resumeURL) {
        this.emit("warn", "Resume url is not currently present. Discord may disconnect you quicker.");
      }
      this.ws = new WebSocket(this.resumeURL || this.client.gatewayURL, this.client.options.ws);
    } else {
      this.ws = new WebSocket(this.client.gatewayURL, this.client.options.ws);
    }
    this.ws.on("open", this._onWSOpen);
    this.ws.on("message", this._onWSMessage);
    this.ws.on("error", this._onWSError);
    this.ws.on("close", this._onWSClose);

    this.connectTimeout = setTimeout(() => {
      if (this.connecting) {
        this.disconnect({
          reconnect: "auto",
        }, new Error("Connection timeout"));
      }
    }, this.client.options.connectionTimeout);
  }

  onPacket(packet) {
    if (this.listeners("rawWS").length > 0 || this.client.listeners("rawWS").length) {
      /**
       * Fired when the shard receives a websocket packet
       * @event Client#rawWS
       * @prop {Object} packet The packet
       * @prop {Number} id The ID of the shard
       */
      this.emit("rawWS", packet, this.id);
    }

    if (packet.s) {
      if (packet.s > this.seq + 1 && this.ws && this.status !== "resuming") {
        /**
         * Fired to warn of something weird but non-breaking happening
         * @event Client#warn
         * @prop {String} message The warning message
         * @prop {Number} id The ID of the shard
         */
        this.emit("warn", `Non-consecutive sequence (${this.seq} -> ${packet.s})`, this.id);
      }
      this.seq = packet.s;
    }

    switch (packet.op) {
      case Constants.GatewayOPCodes.DISPATCH: {
        if (!this.client.options.disableEvents[packet.t]) {
          this.wsEvent(packet);
        }
        break;
      }
      case Constants.GatewayOPCodes.HEARTBEAT: {
        this.heartbeat();
        break;
      }
      case Constants.GatewayOPCodes.INVALID_SESSION: {
        this.seq = 0;
        this.sessionID = null;
        this.resumeURL = null;
        this.emit("warn", "Invalid session, reidentifying!", this.id);
        this.identify();
        break;
      }
      case Constants.GatewayOPCodes.RECONNECT: {
        this.emit("debug", "Reconnecting due to server request", this.id);
        this.disconnect({
          reconnect: "auto",
        });
        break;
      }
      case Constants.GatewayOPCodes.HELLO: {
        if (packet.d.heartbeat_interval > 0) {
          if (this.heartbeatInterval) {
            clearInterval(this.heartbeatInterval);
          }
          this.heartbeatInterval = setInterval(() => this.heartbeat(true), packet.d.heartbeat_interval);
        }

        this.discordServerTrace = packet.d._trace;
        this.connecting = false;
        if (this.connectTimeout) {
          clearTimeout(this.connectTimeout);
        }
        this.connectTimeout = null;

        if (this.sessionID) {
          this.resume();
        } else {
          this.identify();
          // Cannot heartbeat when resuming, discord/discord-api-docs#1619
          this.heartbeat();
        }
        /**
         * Fired when a shard receives an OP:10/HELLO packet
         * @event Client#hello
         * @prop {Array<String>} trace The Discord server trace of the gateway and session servers
         * @prop {Number} id The ID of the shard
         */
        this.emit("hello", packet.d._trace, this.id);
        break;
      }
      case Constants.GatewayOPCodes.HEARTBEAT_ACK: {
        this.lastHeartbeatAck = true;
        this.lastHeartbeatReceived = Date.now();
        this.latency = this.lastHeartbeatReceived - this.lastHeartbeatSent;
        break;
      }
      default: {
        this.emit("unknown", packet, this.id);
        break;
      }
    }
  }

  requestGuildMembers(guildID, options) {
    const opts = {
      guild_id: guildID,
      limit: (options && options.limit) || 0,
      user_ids: options && options.userIDs,
      query: options && options.query,
      nonce: Date.now().toString() + Math.random().toString(36),
      presences: options && options.presences,
    };
    if (!opts.user_ids && !opts.query) {
      opts.query = "";
    }
    if (!opts.query && !opts.user_ids && (this.client.options.intents && !(this.client.options.intents & Constants.Intents.guildMembers))) {
      throw new Error("Cannot request all members without guildMembers intent");
    }
    if (opts.presences && (this.client.options.intents && !(this.client.options.intents & Constants.Intents.guildPresences))) {
      throw new Error("Cannot request members presences without guildPresences intent");
    }
    if (opts.user_ids && opts.user_ids.length > 100) {
      throw new Error("Cannot request more than 100 users by their ID");
    }
    this.sendWS(Constants.GatewayOPCodes.REQUEST_GUILD_MEMBERS, opts);
    return new Promise((res) => this.requestMembersPromise[opts.nonce] = {
      res: res,
      received: 0,
      members: [],
      timeout: setTimeout(() => {
        res(this.requestMembersPromise[opts.nonce].members);
        delete this.requestMembersPromise[opts.nonce];
      }, (options && options.timeout) || this.client.options.requestTimeout),
    });
  }

  requestGuildSoundboardSounds(options) {
    const opts = {
      guild_ids: options.guildIDs,
    };
    const soundboardSounds = options.guildIDs.reduce((obj, key) => {
      obj[key] = undefined;
      return obj;
    }, {});
    const nonce = Date.now().toString() + Math.random().toString(36);
    this.sendWS(Constants.GatewayOPCodes.REQUEST_SOUNDBOARD_SOUNDS, opts);
    return new Promise((res) => this.requestSoundboardSoundsPromise[nonce] = {
      res: res,
      soundboardSounds: soundboardSounds,
      timeout: setTimeout(() => {
        res(this.requestSoundboardSoundsPromise[nonce].soundboardSounds);
        delete this.requestSoundboardSoundsPromise[nonce];
      }, (options && options.timeout) || this.client.options.requestTimeout),
    });
  }

  reset() {
    this.connecting = false;
    this.ready = false;
    this.preReady = false;
    if (this.requestMembersPromise !== undefined) {
      for (const guildID in this.requestMembersPromise) {
        if (!this.requestMembersPromise.hasOwnProperty(guildID)) {
          continue;
        }
        clearTimeout(this.requestMembersPromise[guildID].timeout);
        this.requestMembersPromise[guildID].res(this.requestMembersPromise[guildID].received);
      }
    }
    this.requestMembersPromise = {};
    this.requestSoundboardSoundsPromise = {};
    this.getAllUsersCount = {};
    this.getAllUsersQueue = [];
    this.getAllUsersLength = 1;
    this.latency = Infinity;
    this.lastHeartbeatAck = true;
    this.lastHeartbeatReceived = null;
    this.lastHeartbeatSent = null;
    this.status = "disconnected";
    if (this.connectTimeout) {
      clearTimeout(this.connectTimeout);
    }
    this.connectTimeout = null;
  }

  restartGuildCreateTimeout() {
    if (this.guildCreateTimeout) {
      clearTimeout(this.guildCreateTimeout);
      this.guildCreateTimeout = null;
    }
    if (!this.ready) {
      if (this.client.unavailableGuilds.size === 0) {
        return this.checkReady();
      }
      this.guildCreateTimeout = setTimeout(() => {
        this.checkReady();
      }, this.client.options.guildCreateTimeout);
    }
  }

  resume() {
    this.status = "resuming";
    this.sendWS(Constants.GatewayOPCodes.RESUME, {
      token: this._token,
      session_id: this.sessionID,
      seq: this.seq,
    });
  }

  sendStatusUpdate() {
    this.sendWS(Constants.GatewayOPCodes.PRESENCE_UPDATE, {
      activities: this.presence.activities,
      afk: !!this.presence.afk,
      since: this.presence.status === "idle" ? Date.now() : null,
      status: this.presence.status,
    });
  }

  sendWS(op, _data, priority = false) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      let i = 0;
      let waitFor = 1;
      const func = () => {
        if (++i >= waitFor && this.ws && this.ws.readyState === WebSocket.OPEN) {
          const data = Erlpack ? Erlpack.pack({ op: op, d: _data }) : JSON.stringify({ op: op, d: _data });
          this.ws.send(data);
          if (_data.token) {
            delete _data.token;
          }
          this.emit("debug", JSON.stringify({ op: op, d: _data }), this.id);
        }
      };
      if (op === Constants.GatewayOPCodes.PRESENCE_UPDATE) {
        ++waitFor;
        this.presenceUpdateBucket.queue(func, priority);
      }
      this.globalBucket.queue(func, priority);
    }
  }

  wsEvent(packet) {
    switch (packet.t) {
      case "AUTO_MODERATION_ACTION_EXECUTION": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in AUTO_MODERATION_ACTION_EXECUTION`);
          break;
        }

        /**
         * Fired when an auto moderation action is executed.
         * @event Client#autoModerationActionExecution
         * @prop {Guild} guild The guild associated with the action
         * @prop {Object} action The exection action
         */
        this.emit("autoModerationActionExecution", guild, {
          action: packet.d.action,
          alertSystemMessageID: packet.d.alert_system_message_id,
          channelID: packet.d.channel_id,
          content: packet.d.content,
          guildID: packet.d.guild_id,
          matchedContent: packet.d.matched_content,
          matchedKeyword: packet.d.matched_keyword,
          messageID: packet.d.message_id,
          ruleID: packet.d.rule_id,
          ruleTriggerType: packet.d.rule_trigger_type,
          userID: packet.d.user_id,
        });
        break;
      }

      case "AUTO_MODERATION_RULE_CREATE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in AUTO_MODERATION_RULE_CREATE`);
          break;
        }

        /**
         * Fired when an auto moderation rule is created
         * @event Client#autoModerationRuleCreate
         * @prop {Guild} guild The guild associated with the rule
         * @prop {AutoModerationRule} rule The created rule
         */
        this.emit("autoModerationRuleCreate", guild, guild.autoModerationRules.add(packet.d, this.client));
        break;
      }
      case "AUTO_MODERATION_RULE_DELETE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in AUTO_MODERATION_RULE_DELETE`);
          break;
        }

        /**
         * Fired when an auto moderation rule is deleted
         * @event Client#autoModerationRuleDelete
         * @prop {Guild} guild The guild associated with the rule
         * @prop {AutoModerationRule} rule The deleted rule
         */
        this.emit("autoModerationRuleDelete", guild, guild.autoModerationRules.remove(packet.d) || new AutoModerationRule(packet.d, this.client));
        break;
      }
      case "AUTO_MODERATION_RULE_UPDATE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in AUTO_MODERATION_RULE_DELETE`);
          break;
        }

        const rule = guild.autoModerationRules.get(packet.d.id);
        let oldRule = null;

        oldRule = {
          actions: rule.actions,
          enabled: rule.enabled,
          eventType: rule.eventType,
          exemptChannels: rule.exemptChannels,
          exemptRoles: rule.exemptRoles,
          name: rule.name,
          triggerMetadata: rule.triggerMetadata,
        };

        /**
         * Fired when an auto moderation rule is updated
         * @event Client#autoModerationRuleUpdate
         * @prop {Guild} guild The guild associated with the rule
         * @prop {AutoModerationRule} rule The updated role
         * @prop {Object?} oldRule The old rule. If the rule was uncached, this will be null
         */
        this.emit("autoModerationRuleUpdate", guild, guild.autoModerationRules.update(packet.d, this.client), oldRule);
        break;
      }
      case "PRESENCE_UPDATE": {
        if (packet.d.user.username !== undefined) {
          let user = this.client.users.get(packet.d.user.id);
          let oldUser = null;
          if (user && (user.username !== packet.d.user.username || user.globalName !== packet.d.user.global_name || user.discriminator !== packet.d.user.discriminator || user.avatar !== packet.d.user.avatar || (packet.d.user.avatar_decoration_data && user.avatarDecorationData && user.avatarDecorationData.asset !== packet.d.user.avatar_decoration_data.asset))) {
            oldUser = {
              username: user.username,
              globalName: user.globalName,
              discriminator: user.discriminator,
              avatar: user.avatar,
              avatarDecorationData: user.avatarDecorationData,
            };
          }
          if (!user || oldUser) {
            user = this.client.users.update(packet.d.user, this.client);
            /**
             * Fired when a user's avatar, avatar decoration, discriminator, display name or username changes
             * @event Client#userUpdate
             * @prop {User} user The updated user
             * @prop {Object?} oldUser The old user data. If the user was uncached, this will be null
             * @prop {String?} oldUser.avatar The hash of the user's avatar, or null if no avatar
             * @prop {Object?} oldUser.avatarDecorationData The data of the user's avatar decoration, including the asset and sku ID, or null if no avatar decoration
             * @prop {String} oldUser.discriminator The discriminator of the user
             * @prop {String?} oldUser.globalName The user's display name, if it is set. For bots, this is the application name
             * @prop {String} oldUser.username The username of the user
             */
            this.emit("userUpdate", user, oldUser);
          }
        }
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", "Rogue presence update: " + JSON.stringify(packet), this.id);
          break;
        }
        let member = guild.members.get(packet.d.id = packet.d.user.id);
        let oldPresence = null;
        if (member) {
          oldPresence = {
            activities: member.activities,
            clientStatus: member.clientStatus,
            status: member.status,
          };
        }
        if ((!member && packet.d.user.username) || oldPresence) {
          member = guild.members.update(packet.d, guild);
          /**
           * Fired when a guild member's status or game changes
           * @event Client#presenceUpdate
           * @prop {Member} other The updated member
           * @prop {Object?} oldPresence The old presence data. If the user was offline when the bot started and the client option getAllUsers is not true, this will be null
           * @prop {Array<Object>?} oldPresence.activities The member's current activities
           * @prop {Object?} oldPresence.clientStatus The member's per-client status
           * @prop {String} oldPresence.clientStatus.desktop The member's status on desktop. Either "online", "idle", "dnd", or "offline". Will be "offline" for bots
           * @prop {String} oldPresence.clientStatus.mobile The member's status on mobile. Either "online", "idle", "dnd", or "offline". Will be "offline" for bots
           * @prop {String} oldPresence.clientStatus.web The member's status on web. Either "online", "idle", "dnd", or "offline". Will be "online" for bots
           * @prop {String} oldPresence.status The other user's old status. Either "online", "idle", or "offline"
           */
          this.emit("presenceUpdate", member, oldPresence);
        }
        break;
      }
      case "VOICE_STATE_UPDATE": { // (╯°□°)╯︵ ┻━┻
        if (packet.d.guild_id && packet.d.user_id === this.client.user.id) {
          const voiceConnection = this.client.voiceConnections.get(packet.d.guild_id);
          if (voiceConnection) {
            if (packet.d.channel_id === null) {
              this.client.voiceConnections.leave(packet.d.guild_id);
            } else if (voiceConnection.channelID !== packet.d.channel_id) {
              voiceConnection.switchChannel(packet.d.channel_id, true);
            }
          }
        }
        if (packet.d.self_stream === undefined) {
          packet.d.self_stream = false;
        }
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          break;
        }
        let member = guild.members.get(packet.d.id = packet.d.user_id);
        if (!member) {
          if (!packet.d.member) {
            this.emit("voiceStateUpdate", {
              id: packet.d.user_id,
              voiceState: {
                deaf: packet.d.deaf,
                mute: packet.d.mute,
                selfDeaf: packet.d.self_deaf,
                selfMute: packet.d.self_mute,
                selfStream: packet.d.self_stream,
                selfVideo: packet.d.self_video,
              },
            }, null);
            break;
          }
          // Updates the member cache with this member for future events.
          packet.d.member.id = packet.d.user_id;
          member = guild.members.add(packet.d.member, guild);

          const channel = guild.channels.find((channel) => (channel.type === Constants.ChannelTypes.GUILD_VOICE || channel.type === Constants.ChannelTypes.GUILD_STAGE_VOICE) && channel.voiceMembers.get(packet.d.id));
          if (channel) {
            channel.voiceMembers.remove(packet.d);
            this.emit("debug", "VOICE_STATE_UPDATE member null but in channel: " + packet.d.id, this.id);
          }
        }
        const oldState = {
          deaf: member.voiceState.deaf,
          mute: member.voiceState.mute,
          selfDeaf: member.voiceState.selfDeaf,
          selfMute: member.voiceState.selfMute,
          selfStream: member.voiceState.selfStream,
          selfVideo: member.voiceState.selfVideo,
        };
        const oldChannelID = member.voiceState.channelID;
        member.update(packet.d, this.client);
        if (oldChannelID != packet.d.channel_id) {
          let oldChannel, newChannel;
          if (oldChannelID) {
            oldChannel = guild.channels.get(oldChannelID);
            if (oldChannel && oldChannel.type !== Constants.ChannelTypes.GUILD_VOICE && oldChannel.type !== Constants.ChannelTypes.GUILD_STAGE_VOICE) {
              this.emit("warn", "Old channel not a recognized voice channel: " + oldChannelID, this.id);
              oldChannel = null;
            }
          }
          if (packet.d.channel_id && (newChannel = guild.channels.get(packet.d.channel_id)) && (newChannel.type === Constants.ChannelTypes.GUILD_VOICE || newChannel.type === Constants.ChannelTypes.GUILD_STAGE_VOICE)) { // Welcome to Discord, where one can "join" text channels
            if (oldChannel) {
              /**
               * Fired when a guild member switches voice channels
               * @event Client#voiceChannelSwitch
               * @prop {Member} member The member
               * @prop {VoiceChannel | StageChannel} newChannel The new voice channel
               * @prop {VoiceChannel | StageChannel} oldChannel The old voice channel
               */
              oldChannel.voiceMembers.remove(member);
              this.emit("voiceChannelSwitch", newChannel.voiceMembers.add(member, guild), newChannel, oldChannel);
            } else {
              /**
               * Fired when a guild member joins a voice channel. This event is not fired when a member switches voice channels, see `voiceChannelSwitch`
               * @event Client#voiceChannelJoin
               * @prop {Member} member The member
               * @prop {VoiceChannel | StageChannel} newChannel The voice channel
               */
              this.emit("voiceChannelJoin", newChannel.voiceMembers.add(member, guild), newChannel);
            }
          } else if (oldChannel) {
            oldChannel.voiceMembers.remove(member);
            /**
             * Fired when a guild member leaves a voice channel. This event is not fired when a member switches voice channels, see `voiceChannelSwitch`
             * @event Client#voiceChannelLeave
             * @prop {Member?} member The member
             * @prop {VoiceChannel | StageChannel} oldChannel The voice channel
             */
            this.emit("voiceChannelLeave", member, oldChannel);
          }
        }
        if (oldState.mute !== member.voiceState.mute || oldState.deaf !== member.voiceState.deaf || oldState.selfMute !== member.voiceState.selfMute || oldState.selfDeaf !== member.voiceState.selfDeaf || oldState.selfStream !== member.voiceState.selfStream || oldState.selfVideo !== member.voiceState.selfVideo) {
          /**
           * Fired when a guild member's voice state changes
           * @event Client#voiceStateUpdate
           * @prop {Member | Object} member The member. If the member is not cached and Discord doesn't send a member payload, this will be an object with `id` and `voiceState` keys. No other property is guaranteed
           * @prop {Object?} oldState The old voice state of the member. If the above caveat applies, this will be null
           * @prop {Boolean} oldState.deaf The previous server deaf status
           * @prop {Boolean} oldState.mute The previous server mute status
           * @prop {Boolean} oldState.selfDeaf The previous self deaf status
           * @prop {Boolean} oldState.selfMute The previous self mute status
           * @prop {Boolean} oldState.selfStream The previous self stream status
           * @prop {Boolean} oldState.selfVideo The previous self video status
           */
          this.emit("voiceStateUpdate", member, oldState);
        }
        break;
      }
      case "VOICE_CHANNEL_STATUS_UPDATE": {
        const channel = this.client.getChannel(packet.d.id);
        if (!channel) {
          break;
        }
        const oldChannel = {
          status: channel.status,
        };
        channel.update({
          status: packet.d.status,
        });
        /**
         * Fired when a voice channel status is updated
         * @event Client#voiceChannelStatusUpdate
         * @prop {VoiceChannel} channel The updated voice channel
         * @prop {Object} oldChannel The old channel data
         * @prop {String?} oldChannel.status The old voice channel status
         */
        this.emit("voiceChannelStatusUpdate", channel, oldChannel);
        break;
      }
      case "VOICE_CHANNEL_EFFECT_SEND": {
        const channel = this.client.getChannel(packet.d.channel_id) || { id: packet.d.channel_id };
        const guild = this.client.guilds.get(packet.d.guild_id) || { id: packet.d.guild_id };
        const user = this.client.users.get(packet.d.user_id) || { id: packet.d.user_id };
        /**
         * Fired when a user sends a voice channel effect
         * @event Client#voiceChannelEffectSend
         * @prop {Object} effect The effect that was sent
         * @prop {Number?} effect.animationID The animation ID
         * @prop {Number?} effect.animationType The animation type
         * @prop {VoiceChannel | StageChannel | Object} effect.channel The voice channel the effect was sent in. If the channel is not cached, this will be an object with an `id` key. No other property is guaranteed
         * @prop {Object?} effect.emoji The emoji sent
         * @prop {Guild | Object} effect.guild The guild the effect was sent in. If the guild is not cached, this will be an object with an `id` key. No other property is guaranteed
         * @prop {(String | Number)?} effect.soundID The sound ID
         * @prop {Number?} effect.soundVolume The volume of the sound (a number between 0 and 1)
         * @prop {User | Object} effect.user The user that sent the effect. If the user is not cached, this will be an object with an `id` key. No other property is guaranteed
         */
        this.emit("voiceChannelEffectSend", {
          channel: channel,
          guild: guild,
          user: user,
          emoji: packet.d.emoji,
          animationType: packet.d.animation_type,
          animationID: packet.d.animation_id,
          soundID: packet.d.sound_id,
          soundVolume: packet.d.sound_volume,
        });
        break;
      }
      case "TYPING_START": {
        let member = null;
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (guild) {
          packet.d.member.id = packet.d.user_id;
          member = guild.members.update(packet.d.member, guild);
        }
        if (this.client.listeners("typingStart").length > 0) {
          /**
           * Fired when a user begins typing
           * @event Client#typingStart
           * @prop {DMChannel | TextChannel | NewsChannel | Object} channel The text channel the user is typing in. If the channel is not cached, this will be an object with an `id` key. No other property is guaranteed
           * @prop {User | Object} user The user. If the user is not cached, this will be an object with an `id` key. No other property is guaranteed
           * @prop {Member?} member The guild member, if typing in a guild channel, or `null`, if typing in a DMChannel
           */
          this.emit("typingStart", this.client.getChannel(packet.d.channel_id) || { id: packet.d.channel_id }, this.client.users.get(packet.d.user_id) || { id: packet.d.user_id }, member);
        }
        break;
      }
      case "MESSAGE_CREATE": {
        const channel = this.client.getChannel(packet.d.channel_id);
        if (channel) { // MESSAGE_CREATE just when deleting o.o
          channel.lastMessageID = packet.d.id;
          if (channel instanceof ThreadChannel) {
            channel.messageCount++;
            channel.totalMessageSent++;
          }
          /**
           * Fired when a message is created
           * @event Client#messageCreate
           * @prop {Message} message The message.
           */
          this.emit("messageCreate", channel.messages.add(packet.d, this.client));
        } else {
          this.emit("messageCreate", new Message(packet.d, this.client));
        }
        break;
      }
      case "MESSAGE_UPDATE": {
        const channel = this.client.getChannel(packet.d.channel_id);
        if (!channel) {
          packet.d.channel = {
            id: packet.d.channel_id,
          };
          this.emit("messageUpdate", packet.d, null);
          break;
        }
        const message = channel.messages.get(packet.d.id);
        let oldMessage = null;
        if (message) {
          oldMessage = {
            attachments: message.attachments,
            channelMentions: message.channelMentions,
            content: message.content,
            editedTimestamp: message.editedTimestamp,
            embeds: message.embeds,
            flags: message.flags,
            mentionedBy: message.mentionedBy,
            mentions: message.mentions,
            pinned: message.pinned,
            poll: message.poll,
            roleMentions: message.roleMentions,
            tts: message.tts,
          };
        } else if (!packet.d.timestamp) {
          packet.d.channel = channel;
          this.emit("messageUpdate", packet.d, null);
          break;
        }
        /**
         * Fired when a message is updated
         * @event Client#messageUpdate
         * @prop {Message} message The updated message. If oldMessage is null, it is recommended to discard this event, since the message data will be very incomplete (only `id` and `channel` are guaranteed). If the channel isn't cached, `channel` will be an object with an `id` key.
         * @prop {Object?} oldMessage The old message data. If the message was cached, this will return the full old message. Otherwise, it will be null
         * @prop {Array<Object>} oldMessage.attachments Array of attachments
         * @prop {Array<String>} oldMessage.channelMentions Array of mentions channels' ids.
         * @prop {String} oldMessage.content Message content
         * @prop {Number} oldMessage.editedTimestamp Timestamp of latest message edit
         * @prop {Array<Object>} oldMessage.embeds Array of embeds
         * @prop {Number} oldMessage.flags Old message flags (see constants)
         * @prop {Object} oldMessage.mentionedBy Object of if different things mention the bot user
         * @prop {Array<User>} oldMessage.mentions Array of mentioned users' ids
         * @prop {Boolean} oldMessage.pinned Whether the message was pinned or not
         * @prop {Object} oldMessage.poll A poll object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/poll#poll-object) for object structure
         * @prop {Array<String>} oldMessage.roleMentions Array of mentioned roles' ids.
         * @prop {Boolean} oldMessage.tts Whether to play the message using TTS or not
         */
        this.emit("messageUpdate", channel.messages.update(packet.d, this.client), oldMessage);
        break;
      }
      case "MESSAGE_DELETE": {
        const channel = this.client.getChannel(packet.d.channel_id);
        if (channel instanceof ThreadChannel) {
          channel.messageCount--;
        }
        /**
         * Fired when a cached message is deleted
         * @event Client#messageDelete
         * @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id` and `channel` keys. If the channel is not cached, channel will be an object with an `id` key. If the uncached message is from a guild, the message will also contain a `guildID` key, and the channel will contain a `guild` with an `id` key. No other property is guaranteed.
         */
        this.emit("messageDelete", (channel && channel.messages.remove(packet.d)) || {
          id: packet.d.id,
          channel: channel || {
            id: packet.d.channel_id,
            guild: packet.d.guild_id ? { id: packet.d.guild_id } : undefined,
          },
          guildID: packet.d.guild_id,
        });
        break;
      }
      case "MESSAGE_DELETE_BULK": {
        const channel = this.client.getChannel(packet.d.channel_id);
        if (channel instanceof ThreadChannel) {
          channel.messageCount -= packet.d.ids.length;
        }
        /**
         * Fired when a bulk delete occurs
         * @event Client#messageDeleteBulk
         * @prop {Array<Message> | Array<Object>} messages An array of (potentially partial) message objects. If a message is not cached, it will be an object with `id` and `channel` keys If the uncached messages are from a guild, the messages will also contain a `guildID` key, and the channel will contain a `guild` with an `id` key. No other property is guaranteed
         */
        this.emit("messageDeleteBulk", packet.d.ids.map((id) => ((channel && channel.messages.remove({ id })) || {
          id: id,
          channel: { id: packet.d.channel_id, guild: packet.d.guild_id ? { id: packet.d.guild_id } : undefined },
          guildID: packet.d.guild_id,
        })));
        break;
      }
      case "MESSAGE_REACTION_ADD": {
        const channel = this.client.getChannel(packet.d.channel_id);
        let message;
        let member;
        if (channel) {
          message = channel.messages.get(packet.d.message_id);
          if (channel.guild) {
            if (packet.d.member) {
              // Updates the member cache with this member for future events.
              packet.d.member.id = packet.d.user_id;
              member = channel.guild.members.update(packet.d.member, channel.guild);
            }
          }
        }
        if (message) {
          const reaction = packet.d.emoji.id ? `${packet.d.emoji.name}:${packet.d.emoji.id}` : packet.d.emoji.name;
          if (message.reactions[reaction]) {
            ++message.reactions[reaction].count;
            ++message.reactions[reaction].count_details[packet.d.burst ? "burst" : "normal"];
            if (packet.d.user_id === this.client.user.id) {
              message.reactions[reaction].me = true;
              if (packet.d.burst) {
                message.reactions[reaction].me_burst = true;
              }
            }
            message.reactions[reaction].burst_colors = packet.d.burst_colors;
          } else {
            message.reactions[reaction] = {
              burst_colors: packet.d.burst_colors,
              count: 1,
              count_details: {
                burst: +packet.d.burst,
                normal: +!packet.d.burst,
              },
              me: packet.d.user_id === this.client.user.id && !packet.d.burst,
              me_burst: packet.d.user_id === this.client.user.id && packet.d.burst,
              type: packet.d.type,
            };
          }
        } else {
          message = {
            id: packet.d.message_id,
            channel: channel || { id: packet.d.channel_id },
          };

          if (packet.d.guild_id) {
            message.guildID = packet.d.guild_id;
            if (!message.channel.guild) {
              message.channel.guild = { id: packet.d.guild_id };
            }
          }
        }
        /**
         * Fired when someone adds a reaction to a message
         * @event Client#messageReactionAdd
         * @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id`, `channel`, and if inside a guild, `guildID` keys. If the channel is not cached, channel key will be an object with only an id. `guildID` will be present if the message was sent in a guild channel. No other property is guaranteed
         * @prop {Object} emoji The reaction emoji object
         * @prop {Boolean?} emoji.animated Whether the emoji is animated or not
         * @prop {String?} emoji.id The emoji ID (null for non-custom emojis)
         * @prop {String} emoji.name The emoji name
         * @prop {Member | Object} reactor The member, if the reaction is in a guild. If the reaction is not in a guild, this will be an object with an `id` key. No other property is guaranteed
         * @prop {Boolean} burst Whether the reaction is a super reaction
         */
        this.emit("messageReactionAdd", message, packet.d.emoji, member || { id: packet.d.user_id }, packet.d.burst);
        break;
      }
      case "MESSAGE_REACTION_REMOVE": {
        const channel = this.client.getChannel(packet.d.channel_id);
        let message;
        if (channel) {
          message = channel.messages.get(packet.d.message_id);
        }
        if (message) {
          const reaction = packet.d.emoji.id ? `${packet.d.emoji.name}:${packet.d.emoji.id}` : packet.d.emoji.name;
          const reactionObj = message.reactions[reaction];
          if (reactionObj) {
            --reactionObj.count;
            --reactionObj.count_details[packet.d.burst ? "burst" : "normal"];
            if (reactionObj.count === 0) {
              delete message.reactions[reaction];
            } else if (packet.d.user_id === this.client.user.id) {
              message.reactions[reaction].me = false;
              if (packet.d.burst) {
                message.reactions[reaction].me_burst = false;
              }
            }
          }
        } else {
          message = {
            id: packet.d.message_id,
            channel: channel || { id: packet.d.channel_id },
          };

          if (packet.d.guild_id) {
            message.guildID = packet.d.guild_id;
            if (!message.channel.guild) {
              message.channel.guild = { id: packet.d.guild_id };
            }
          }
        }
        /**
         * Fired when someone removes a reaction from a message
         * @event Client#messageReactionRemove
         * @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id`, `channel`, and if inside a guild, `guildID` keys. If the channel is not cached, channel key will be an object with only an id. `guildID` will be present if the message was sent in a guild channel. No other property is guaranteed
         * @prop {Object} emoji The reaction emoji object
         * @prop {Boolean?} emoji.animated Whether the emoji is animated or not
         * @prop {String?} emoji.id The ID of the emoji (null for non-custom emojis)
         * @prop {String} emoji.name The emoji name
         * @prop {String} userID The ID of the user that removed the reaction
         * @prop {Boolean} burst Whether the reaction is a super reaction
         */
        this.emit("messageReactionRemove", message, packet.d.emoji, packet.d.user_id, packet.d.burst);
        break;
      }
      case "MESSAGE_REACTION_REMOVE_ALL": {
        const channel = this.client.getChannel(packet.d.channel_id);
        let message;
        if (channel) {
          message = channel.messages.get(packet.d.message_id);
          if (message) {
            message.reactions = {};
          }
        }
        if (!message) {
          message = {
            id: packet.d.message_id,
            channel: channel || { id: packet.d.channel_id },
          };
          if (packet.d.guild_id) {
            message.guildID = packet.d.guild_id;
            if (!message.channel.guild) {
              message.channel.guild = { id: packet.d.guild_id };
            }
          }
        }
        /**
         * Fired when all reactions are removed from a message
         * @event Client#messageReactionRemoveAll
         * @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id`, `channel`, and if inside a guild, `guildID` keys. If the channel is not cached, channel key will be an object with only an id. No other property is guaranteed
         */
        this.emit("messageReactionRemoveAll", message);
        break;
      }
      case "MESSAGE_REACTION_REMOVE_EMOJI": {
        const channel = this.client.getChannel(packet.d.channel_id);
        let message;
        if (channel) {
          message = channel.messages.get(packet.d.message_id);
          if (message) {
            const reaction = packet.d.emoji.id ? `${packet.d.emoji.name}:${packet.d.emoji.id}` : packet.d.emoji.name;
            delete message.reactions[reaction];
          }
        }
        if (!message) {
          message = {
            id: packet.d.message_id,
            channel: channel || { id: packet.d.channel_id },
          };
          if (packet.d.guild_id) {
            message.guildID = packet.d.guild_id;
            if (!message.channel.guild) {
              message.channel.guild = { id: packet.d.guild_id };
            }
          }
        }
        /**
         * Fired when someone removes all reactions from a message for a single emoji
         * @event Client#messageReactionRemoveEmoji
         * @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id` and `channel` keys. If the channel is not cached, channel key will be an object with only an id. No other property is guaranteed
         * @prop {Object} emoji The reaction emoji object
         * @prop {Boolean?} emoji.animated Whether the emoji is animated or not
         * @prop {String?} emoji.id The ID of the emoji (null for non-custom emojis)
         * @prop {String} emoji.name The emoji name
         */
        this.emit("messageReactionRemoveEmoji", message, packet.d.emoji);
        break;
      }
      case "MESSAGE_POLL_VOTE_ADD": {
        const user = this.client.users.get(packet.d.user_id);
        const channel = this.client.getChannel(packet.d.channel_id);
        let message;
        if (channel) {
          message = channel.messages.get(packet.d.message_id);
        }
        if (!message) {
          message = {
            id: packet.d.message_id,
            channel: channel || { id: packet.d.channel_id },
          };
          if (packet.d.guild_id) {
            message.guildID = packet.d.guild_id;
            if (!message.channel.guild) {
              message.channel.guild = { id: packet.d.guild_id };
            }
          }
        } else if (message.poll.results && message.poll.results.answer_counts) {
          const answer = message.poll.results.answer_counts.find((answer) => answer.id === packet.d.answer_id);
          if (answer) {
            answer.count++;
            if (packet.d.user_id === this.client.user.id) {
              answer.me_voted = true;
            }
          } else {
            message.poll.results.answer_counts.push({
              id: packet.d.answer_id,
              count: 1,
              me_voted: packet.d.user_id === this.client.user.id,
            });
          }
        }
        /**
         * Fired when a user votes on a poll. If the poll allows multiple selection, one event will be sent per answer
         * @event Client#messagePollVoteAdd
         * @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id` and `channel` keys. If the channel is not cached, channel key will be an object with an id and a `guild` key with only an id. No other property is guaranteed
         * @prop {User | Object} user The user who voted on the poll. If the user is not cached, this will be an object with only an `id` key
         * @prop {Number} answerID The ID of the answer
         */
        this.emit("messagePollVoteAdd", message, user || { id: packet.d.user_id }, packet.d.answer_id);
        break;
      }
      case "MESSAGE_POLL_VOTE_REMOVE": {
        const user = this.client.users.get(packet.d.user_id);
        const channel = this.client.getChannel(packet.d.channel_id);
        let message;
        if (channel) {
          message = channel.messages.get(packet.d.message_id);
        }
        if (!message) {
          message = {
            id: packet.d.message_id,
            channel: channel || { id: packet.d.channel_id },
          };
          if (packet.d.guild_id) {
            message.guildID = packet.d.guild_id;
            if (!message.channel.guild) {
              message.channel.guild = { id: packet.d.guild_id };
            }
          }
        } else if (message.poll.results && message.poll.results.answer_counts) {
          const answer = message.poll.results.answer_counts.find((answer) => answer.id === packet.d.answer_id);
          if (answer) {
            answer.count--;
            if (packet.d.user_id === this.client.user.id) {
              answer.me_voted = false;
            }
          }
        }
        /**
         * Fired when a user removes their vote on a poll. If the poll allows multiple selection, one event will be sent per answer
         * @event Client#messagePollVoteRemove
         * @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id` and `channel` keys. If the channel is not cached, channel key will be an object with an id and a `guild` key with only an id. No other property is guaranteed
         * @prop {User | Object} user The user who removed their vote from the poll. If the user is not cached, this will be an object with only an `id` key
         * @prop {Number} answerID The ID of the answer
         */
        this.emit("messagePollVoteRemove", message, user || { id: packet.d.user_id }, packet.d.answer_id);
        break;
      }
      case "GUILD_AUDIT_LOG_ENTRY_CREATE": {
        /**
         * Fired when a guild audit log entry is created
         * @event Client#guildAuditLogEntryCreate
         * @prop {GuildAuditLogEntry} guildAuditLogEntry The created guild audit log entry
         */
        this.emit("guildAuditLogEntryCreate", new GuildAuditLogEntry(packet.d, this.client));
        break;
      }
      case "GUILD_MEMBER_ADD": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) { // Eventual Consistency™ (╯°□°)╯︵ ┻━┻
          this.emit("debug", `Missing guild ${packet.d.guild_id} in GUILD_MEMBER_ADD`);
          break;
        }
        packet.d.id = packet.d.user.id;
        ++guild.memberCount;
        /**
         * Fired when a member joins a server
         * @event Client#guildMemberAdd
         * @prop {Guild} guild The guild
         * @prop {Member} member The member
         */
        this.emit("guildMemberAdd", guild, guild.members.add(packet.d, guild));
        break;
      }
      case "GUILD_MEMBER_UPDATE": {
        // Check for member update if guildPresences intent isn't set, to prevent emitting twice
        if (!(this.client.options.intents & Constants.Intents.guildPresences) && packet.d.user.username !== undefined) {
          let user = this.client.users.get(packet.d.user.id);
          let oldUser = null;
          if (user && (user.username !== packet.d.user.username || user.globalName !== packet.d.user.global_name || user.discriminator !== packet.d.user.discriminator || user.avatar !== packet.d.user.avatar || (packet.d.user.avatar_decoration_data && user.avatarDecorationData && user.avatarDecorationData.asset !== packet.d.user.avatar_decoration_data.asset))) {
            oldUser = {
              username: user.username,
              globalName: user.globalName,
              discriminator: user.discriminator,
              avatar: user.avatar,
              avatarDecorationData: user.avatarDecorationData,
            };
          }
          if (!user || oldUser) {
            user = this.client.users.update(packet.d.user, this.client);
            this.emit("userUpdate", user, oldUser);
          }
        }
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in GUILD_MEMBER_UPDATE`);
          break;
        }
        let member = guild.members.get(packet.d.id = packet.d.user.id);
        let oldMember = null;
        if (member) {
          oldMember = {
            avatar: member.avatar,
            communicationDisabledUntil: member.communicationDisabledUntil,
            flags: member.flags,
            roles: member.roles,
            nick: member.nick,
            premiumSince: member.premiumSince,
            pending: member.pending,
          };
        }
        member = guild.members.update(packet.d, guild);
        /**
         * Fired when a member's guild avatar, roles or nickname are updated or they start boosting a server
         * @event Client#guildMemberUpdate
         * @prop {Guild} guild The guild
         * @prop {Member} member The updated member
         * @prop {Object?} oldMember The old member data, or null if the member wasn't cached
         * @prop {String?} oldMember.avatar The hash of the member's guild avatar, or null if no guild avatar
         * @prop {Number?} oldMember.communicationDisabledUntil Timestamp of previous timeout expiry. If `null`, the member was not timed out
         * @prop {Number?} oldMember.flags The member's flags (see Constants). Defaults to 0
         * @prop {String?} oldMember.nick The server nickname of the member
         * @prop {Boolean?} oldMember.pending Whether the member has passed the guild's Membership Screening requirements
         * @prop {Number?} oldMember.premiumSince Timestamp of when the member boosted the guild
         * @prop {Array<String>} oldMember.roles An array of role IDs this member is a part of
         */
        this.emit("guildMemberUpdate", guild, member, oldMember);
        break;
      }
      case "GUILD_MEMBER_REMOVE": {
        if (packet.d.user.id === this.client.user.id) { // The bot is probably leaving
          break;
        }
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          break;
        }
        --guild.memberCount;
        packet.d.id = packet.d.user.id;
        /**
         * Fired when a member leaves a server
         * @event Client#guildMemberRemove
         * @prop {Guild} guild The guild
         * @prop {Member | Object} member The member. If the member is not cached, this will be an object with `id` and `user` key
         */
        this.emit("guildMemberRemove", guild, guild.members.remove(packet.d) || {
          id: packet.d.id,
          user: new User(packet.d.user, this.client),
        });
        break;
      }
      case "GUILD_CREATE": {
        if (!packet.d.unavailable) {
          const guild = this.createGuild(packet.d);
          if (this.ready) {
            if (this.client.unavailableGuilds.remove(packet.d)) {
              /**
               * Fired when a guild becomes available
               * @event Client#guildAvailable
               * @prop {Guild} guild The guild
               */
              this.emit("guildAvailable", guild);
            } else {
              /**
               * Fired when a guild is created. This happens when:
               * - the client creates a guild
               * - the client joins a guild
               * @event Client#guildCreate
               * @prop {Guild} guild The guild
               */
              this.emit("guildCreate", guild);
            }
          } else {
            this.client.unavailableGuilds.remove(packet.d);
            this.restartGuildCreateTimeout();
          }
        } else {
          this.client.guilds.remove(packet.d);
          /**
           * Fired when an unavailable guild is created
           * @event Client#unavailableGuildCreate
           * @prop {UnavailableGuild} guild The unavailable guild
           */
          this.emit("unavailableGuildCreate", this.client.unavailableGuilds.add(packet.d, this.client));
        }
        break;
      }
      case "GUILD_UPDATE": {
        const guild = this.client.guilds.get(packet.d.id);
        if (!guild) {
          this.emit("debug", `Guild ${packet.d.id} undefined in GUILD_UPDATE`);
          break;
        }
        const oldGuild = {
          afkChannelID: guild.afkChannelID,
          afkTimeout: guild.afkTimeout,
          autoRemoved: guild.autoRemoved,
          banner: guild.banner,
          defaultNotifications: guild.defaultNotifications,
          description: guild.description,
          discoverySplash: guild.discoverySplash,
          emojiCount: guild.emojiCount,
          emojis: guild.emojis,
          explicitContentFilter: guild.explicitContentFilter,
          features: guild.features,
          icon: guild.icon,
          keywords: guild.keywords,
          large: guild.large,
          maxMembers: guild.maxMembers,
          maxVideoChannelUsers: guild.maxVideoChannelUsers,
          mfaLevel: guild.mfaLevel,
          name: guild.name,
          nsfw: guild.nsfw,
          nsfwLevel: guild.nsfwLevel,
          ownerID: guild.ownerID,
          preferredLocale: guild.preferredLocale,
          premiumProgressBarEnabled: guild.premiumProgressBarEnabled,
          premiumSubscriptionCount: guild.premiumSubscriptionCount,
          premiumTier: guild.premiumTier,
          primaryCategory: guild.primaryCategory,
          primaryCategoryID: guild.primaryCategoryID,
          publicUpdatesChannelID: guild.publicUpdatesChannelID,
          rulesChannelID: guild.rulesChannelID,
          splash: guild.splash,
          stickers: guild.stickers,
          systemChannelFlags: guild.systemChannelFlags,
          systemChannelID: guild.systemChannelID,
          vanityURL: guild.vanityURL,
          verificationLevel: guild.verificationLevel,
          welcomeScreen: guild.welcomeScreen && {
            description: guild.welcomeScreen.description,
            welcomeChannels: guild.welcomeScreen.welcomeChannels,
          },
        };
        /**
         * Fired when a guild is updated
         * @event Client#guildUpdate
         * @prop {Guild} guild The guild
         * @prop {Object} oldGuild The old guild data
         * @prop {String?} oldGuild.afkChannelID The ID of the AFK voice channel
         * @prop {Number} oldGuild.afkTimeout The AFK timeout in seconds
         * @prop {Boolean?} oldGuild.autoRemoved Whether the guild was automatically removed from Discovery
         * @prop {String?} oldGuild.banner The hash of the guild banner image, or null if no splash (VIP only)
         * @prop {Number} oldGuild.defaultNotifications The default notification settings for the guild. 0 is "All Messages", 1 is "Only @mentions"
         * @prop {String?} oldGuild.description The description for the guild (VIP only)
         * @prop {Number?} oldGuild.emojiCount The number of emojis in the guild
         * @prop {Array<Object>} oldGuild.emojis An array of guild emojis
         * @prop {Number} oldGuild.explicitContentFilter The explicit content filter level for the guild. 0 is off, 1 is on for people without roles, 2 is on for all
         * @prop {Array<String>} oldGuild.features An array of guild features
         * @prop {String?} oldGuild.icon The hash of the guild icon, or null if no icon
         * @prop {Array<String>?} oldGuild.keywords The guild's discovery keywords
         * @prop {Boolean} oldGuild.large Whether the guild is "large" by "some Discord standard"
         * @prop {Number?} oldGuild.maxMembers The maximum number of members for this guild
         * @prop {Number?} oldGuild.maxVideoChannelUsers The max number of users allowed in a video channel
         * @prop {Number} oldGuild.mfaLevel The admin 2FA level for the guild. 0 is not required, 1 is required
         * @prop {String} oldGuild.name The name of the guild
         * @prop {Boolean} oldGuild.nsfw [DEPRECATED] Whether the guild is designated as NSFW by Discord
         * @prop {Number} oldGuild.nsfwLevel The guild NSFW level designated by Discord
         * @prop {String} oldGuild.ownerID The ID of the user that is the guild owner
         * @prop {String} oldGuild.preferredLocale Preferred "COMMUNITY" guild language used in server discovery and notices from Discord
         * @prop {Boolean} oldGuild.premiumProgressBarEnabled If the boost progress bar is enabled
         * @prop {Number?} oldGuild.premiumSubscriptionCount The total number of users currently boosting this guild
         * @prop {Number} oldGuild.premiumTier Nitro boost level of the guild
         * @prop {Object?} oldGuild.primaryCategory The guild's primary discovery category
         * @prop {Number?} oldGuild.primaryCategoryID The guild's primary discovery category ID
         * @prop {String?} oldGuild.publicUpdatesChannelID ID of the guild's updates channel if the guild has "COMMUNITY" features
         * @prop {String?} oldGuild.rulesChannelID The channel where "COMMUNITY" guilds display rules and/or guidelines
         * @prop {String?} oldGuild.splash The hash of the guild splash image, or null if no splash (VIP only)
         * @prop {Array<Object>?} stickers An array of guild sticker objects
         * @prop {Number} oldGuild.systemChannelFlags the flags for the system channel
         * @prop {String?} oldGuild.systemChannelID The ID of the default channel for system messages (built-in join messages and boost messages)
         * @prop {String?} oldGuild.vanityURL The vanity URL of the guild (VIP only)
         * @prop {Number} oldGuild.verificationLevel The guild verification level
         * @prop {Object?} oldGuild.welcomeScreen The welcome screen of a Community guild, shown to new members
         * @prop {Object} oldGuild.welcomeScreen.description The description in the welcome screen
         * @prop {Array<Object>} oldGuild.welcomeScreen.welcomeChannels The list of channels in the welcome screens. Each channels have the following properties: `channelID`, `description`, `emojiID`, `emojiName`. `emojiID` and `emojiName` properties can be null.
         */
        this.emit("guildUpdate", this.client.guilds.update(packet.d, this.client), oldGuild);
        break;
      }
      case "GUILD_DELETE": {
        const voiceConnection = this.client.voiceConnections.get(packet.d.id);
        if (voiceConnection) {
          if (voiceConnection.channelID) {
            this.client.leaveVoiceChannel(voiceConnection.channelID);
          } else {
            this.client.voiceConnections.leave(packet.d.id);
          }
        }

        delete this.client.guildShardMap[packet.d.id];
        const guild = this.client.guilds.remove(packet.d);
        if (guild) { // Discord sends GUILD_DELETE for guilds that were previously unavailable in READY
          guild.channels.forEach((channel) => {
            delete this.client.channelGuildMap[channel.id];
          });
        }
        if (packet.d.unavailable) {
          /**
           * Fired when a guild becomes unavailable
           * @event Client#guildUnavailable
           * @prop {Guild} guild The guild
           */
          this.emit("guildUnavailable", this.client.unavailableGuilds.add(packet.d, this.client));
        } else {
          /**
           * Fired when a guild is deleted. This happens when:
           * - the client left the guild
           * - the client was kicked/banned from the guild
           * - the guild was literally deleted
           * @event Client#guildDelete
           * @prop {Guild | Object} guild The guild. If the guild was not cached, it will be an object with an `id` key. No other property is guaranteed
           */
          this.emit("guildDelete", guild || {
            id: packet.d.id,
          });
        }
        break;
      }
      case "GUILD_BAN_ADD": {
        /**
         * Fired when a user is banned from a guild
         * @event Client#guildBanAdd
         * @prop {Guild} guild The guild
         * @prop {User} user The banned user
         */
        this.emit("guildBanAdd", this.client.guilds.get(packet.d.guild_id), this.client.users.update(packet.d.user, this.client));
        break;
      }
      case "GUILD_BAN_REMOVE": {
        /**
         * Fired when a user is unbanned from a guild
         * @event Client#guildBanRemove
         * @prop {Guild} guild The guild
         * @prop {User} user The banned user
         */
        this.emit("guildBanRemove", this.client.guilds.get(packet.d.guild_id), this.client.users.update(packet.d.user, this.client));
        break;
      }
      case "GUILD_ROLE_CREATE": {
        /**
         * Fired when a guild role is created
         * @event Client#guildRoleCreate
         * @prop {Guild} guild The guild
         * @prop {Role} role The role
         */
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in GUILD_ROLE_CREATE`);
          break;
        }
        this.emit("guildRoleCreate", guild, guild.roles.add(packet.d.role, guild));
        break;
      }
      case "GUILD_ROLE_UPDATE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Guild ${packet.d.guild_id} undefined in GUILD_ROLE_UPDATE`);
          break;
        }
        const role = guild.roles.add(packet.d.role, guild);
        if (!role) {
          this.emit("debug", `Role ${packet.d.role} in guild ${packet.d.guild_id} undefined in GUILD_ROLE_UPDATE`);
          break;
        }
        const oldRole = {
          color: role.color,
          flags: role.flags,
          hoist: role.hoist,
          icon: role.icon,
          managed: role.managed,
          mentionable: role.mentionable,
          name: role.name,
          permissions: role.permissions,
          position: role.position,
          tags: role.tags,
          unicodeEmoji: role.unicodeEmoji,
        };
        /**
         * Fired when a guild role is updated
         * @event Client#guildRoleUpdate
         * @prop {Guild} guild The guild
         * @prop {Role} role The updated role
         * @prop {Object} oldRole The old role data
         * @prop {Number} oldRole.color The hex color of the role in base 10
         * @prop {Number} oldRole.flags The flags of the role (see constants)
         * @prop {Boolean} oldRole.hoist Whether users with the role are hoisted in the user list or not
         * @prop {String?} oldRole.icon The hash of the role's icon, or null if no icon
         * @prop {Boolean} oldRole.managed Whether a guild integration manages the role or not
         * @prop {Boolean} oldRole.mentionable Whether the role is mentionable or not
         * @prop {String} oldRole.name The name of the role
         * @prop {Permission} oldRole.permissions The permissions number of the role
         * @prop {Number} oldRole.position The position of the role
         * @prop {Object?} oldRole.tags The tags of the role
         * @prop {String?} oldRole.unicodeEmoji Unicode emoji for the role
         */
        this.emit("guildRoleUpdate", guild, guild.roles.update(packet.d.role, guild), oldRole);
        break;
      }
      case "GUILD_ROLE_DELETE": {
        /**
         * Fired when a guild role is deleted
         * @event Client#guildRoleDelete
         * @prop {Guild} guild The guild
         * @prop {Role} role The role
         */
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in GUILD_ROLE_DELETE`);
          break;
        }
        if (!guild.roles.has(packet.d.role_id)) {
          this.emit("debug", `Missing role ${packet.d.role_id} in GUILD_ROLE_DELETE`);
          break;
        }
        this.emit("guildRoleDelete", guild, guild.roles.remove({ id: packet.d.role_id }));
        break;
      }
      case "INVITE_CREATE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in INVITE_CREATE`);
          break;
        }
        const channel = this.client.getChannel(packet.d.channel_id);
        if (!channel) {
          this.emit("debug", `Missing channel ${packet.d.channel_id} in INVITE_CREATE`);
          break;
        }
        /**
         * Fired when a guild invite is created
         * @event Client#inviteCreate
         * @prop {Guild} guild The guild this invite was created in.
         * @prop {Invite} invite The invite that was created
         */
        this.emit("inviteCreate", guild, new Invite({
          ...packet.d,
          guild,
          channel,
        }, this.client));
        break;
      }
      case "INVITE_DELETE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in INVITE_DELETE`);
          break;
        }
        const channel = this.client.getChannel(packet.d.channel_id);
        if (!channel) {
          this.emit("debug", `Missing channel ${packet.d.channel_id} in INVITE_DELETE`);
          break;
        }
        /**
         * Fired when a guild invite is deleted
         * @event Client#inviteDelete
         * @prop {Guild} guild The guild this invite was created in.
         * @prop {Invite} invite The invite that was deleted
         */
        this.emit("inviteDelete", guild, new Invite({
          ...packet.d,
          guild,
          channel,
        }, this.client));
        break;
      }
      case "CHANNEL_CREATE": {
        const channel = Channel.from(packet.d, this.client);
        if (packet.d.guild_id) {
          if (!channel.guild) {
            channel.guild = this.client.guilds.get(packet.d.guild_id);
            if (!channel.guild) {
              this.emit("debug", `Received CHANNEL_CREATE for channel in missing guild ${packet.d.guild_id}`);
              break;
            }
          }
          channel.guild.channels.add(channel, this.client);
          this.client.channelGuildMap[packet.d.id] = packet.d.guild_id;
          /**
           * Fired when a channel is created
           * @event Client#channelCreate
           * @prop {CategoryChannel | ForumChannel | GuildChannel | NewsChannel | TextChannel | VoiceChannel} channel The channel
           */
          this.emit("channelCreate", channel);
        } else {
          this.emit("warn", new Error("Unhandled CHANNEL_CREATE type: " + JSON.stringify(packet, null, 2)));
          break;
        }
        break;
      }
      case "CHANNEL_UPDATE": {
        let channel = this.client.getChannel(packet.d.id);
        if (!channel) {
          break;
        }
        let oldChannel;
        if (channel instanceof GuildChannel) {
          oldChannel = {
            availableTags: channel.availableTags,
            bitrate: channel.bitrate,
            defaultAutoArchiveDuration: channel.defaultAutoArchiveDuration,
            defaultForumLayout: channel.defaultForumLayout,
            defaultReactionEmoji: channel.defaultReactionEmoji,
            defaultSortOrder: channel.defaultSortOrder,
            defaultThreadRateLimitPerUser: channel.defaultThreadRateLimitPerUser,
            flags: channel.flags,
            name: channel.name,
            nsfw: channel.nsfw,
            parentID: channel.parentID,
            permissionOverwrites: channel.permissionOverwrites,
            position: channel.position,
            rateLimitPerUser: channel.rateLimitPerUser,
            rtcRegion: channel.rtcRegion,
            topic: channel.topic,
            type: channel.type,
            userLimit: channel.userLimit,
            videoQualityMode: channel.videoQualityMode,
          };
        } else {
          this.emit("warn", `Unexpected CHANNEL_UPDATE for channel ${packet.d.id} with type ${oldType}`);
        }
        const oldType = channel.type;
        if (oldType === packet.d.type) {
          channel.update(packet.d);
        } else {
          this.emit("debug", `Channel ${packet.d.id} changed from type ${oldType} to ${packet.d.type}`);
          const newChannel = Channel.from(packet.d, this.client);
          if (packet.d.guild_id) {
            const guild = this.client.guilds.get(packet.d.guild_id);
            if (!guild) {
              this.emit("debug", `Received CHANNEL_UPDATE for channel in missing guild ${packet.d.guild_id}`);
              break;
            }
            guild.channels.remove(channel);
            guild.channels.add(newChannel, this.client);
          } else if (channel instanceof DMChannel) {
            this.client.dmChannels.remove(channel);
            this.client.dmChannels.add(newChannel, this.client);
          } else {
            this.emit("warn", new Error("Unhandled CHANNEL_UPDATE type: " + JSON.stringify(packet, null, 2)));
            break;
          }
          channel = newChannel;
        }

        /**
         * Fired when a channel is updated
         * @event Client#channelUpdate
         * @prop {CategoryChannel | DMChannel | ForumChannel | GuildChannel | TextChannel | VoiceChannel | NewsChannel} channel The updated channel
         * @prop {Object} oldChannel The old channel data
         * @prop {Array<Object>} oldChannel.availableTags The available tags that can be applied to threads in a forum/media channel. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#forum-tag-object) for object structure (forum/media channels only, max 20)
         * @prop {Number} oldChannel.bitrate The bitrate of the channel (voice channels only)
         * @prop {Number} oldChannel.defaultAutoArchiveDuration The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) (text/news/forum/media channels only)
         * @prop {Number} oldChannel.defaultForumLayout The default forum layout type used to display posts in forum channels (forum channels only)
         * @prop {Object} oldChannel.defaultReactionEmoji The emoji to show in the add reaction button on a thread in a forum/media channel (forum/media channels only)
         * @prop {Number} oldChannel.defaultSortOrder The default sort order type used to order posts in forum/media channels (forum/media channels only)
         * @prop {Number} oldChannel.defaultThreadRateLimitPerUser The initial rateLimitPerUser to set on newly created threads in a channel (text/forum/media channels only)
         * @prop {Number?} oldChannel.flags The flags for the channel combined as a bitfield (thread/forum/media channels only)
         * @prop {String} oldChannel.name The name of the channel
         * @prop {Boolean} oldChannel.nsfw Whether the channel is NSFW or not (text channels only)
         * @prop {String?} oldChannel.parentID The ID of the category this channel belongs to (guild channels only)
         * @prop {Collection} oldChannel.permissionOverwrites Collection of PermissionOverwrites in this channel (guild channels only)
         * @prop {Number} oldChannel.position The position of the channel (guild channels only)
         * @prop {Number?} oldChannel.rateLimitPerUser The time in seconds a user has to wait before sending another message (0-21600) (text/voice/stage/forum/media channels only)
         * @prop {String?} oldChannel.rtcRegion The RTC region ID of the channel (automatic when `null`) (voice channels only)
         * @prop {String?} oldChannel.topic The topic of the channel (text channels only)
         * @prop {Number} oldChannel.type The type of the old channel (text/news channels only)
         * @prop {Number?} oldChannel.userLimit The max number of users that can join the channel (voice channels only)
         * @prop {Number?} oldChannel.videoQualityMode The camera video quality mode of the channel (voice channels only)
         */
        this.emit("channelUpdate", channel, oldChannel);
        break;
      }
      case "CHANNEL_DELETE": {
        if (packet.d.type === Constants.ChannelTypes.DM || packet.d.type === undefined) {
          if (this.id === 0) {
            const channel = this.client.dmChannels.remove(packet.d);
            if (channel) {
              delete this.client.dmChannelMap[channel.recipient.id];
              /**
               * Fired when a channel is deleted
               * @event Client#channelDelete
               * @prop {CategoryChannel | DMChannel | ForumChannel | NewsChannel | TextChannel | VoiceChannel} channel The channel
               */
              this.emit("channelDelete", channel);
            }
          }
        } else if (packet.d.guild_id) {
          delete this.client.channelGuildMap[packet.d.id];
          const guild = this.client.guilds.get(packet.d.guild_id);
          if (!guild) {
            this.emit("debug", `Missing guild ${packet.d.guild_id} in CHANNEL_DELETE`);
            break;
          }
          const channel = guild.channels.remove(packet.d);
          if (!channel) {
            break;
          }
          if (channel.type === Constants.ChannelTypes.GUILD_VOICE || channel.type === Constants.ChannelTypes.GUILD_STAGE_VOICE) {
            channel.voiceMembers.forEach((member) => {
              channel.voiceMembers.remove(member);
              this.emit("voiceChannelLeave", member, channel);
            });
          }
          this.emit("channelDelete", channel);
        } else {
          this.emit("warn", new Error("Unhandled CHANNEL_DELETE type: " + JSON.stringify(packet, null, 2)));
        }
        break;
      }
      case "GUILD_MEMBERS_CHUNK": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Received GUILD_MEMBERS_CHUNK, but guild ${packet.d.guild_id} is ` + (this.client.unavailableGuilds.has(packet.d.guild_id) ? "unavailable" : "missing"), this.id);
          break;
        }

        const members = packet.d.members.map((member) => {
          member.id = member.user.id;
          return guild.members.add(member, guild);
        });

        if (packet.d.presences) {
          packet.d.presences.forEach((presence) => {
            const member = guild.members.get(presence.user.id);
            if (member) {
              member.update(presence);
            }
          });
        }

        if (this.requestMembersPromise.hasOwnProperty(packet.d.nonce)) {
          this.requestMembersPromise[packet.d.nonce].members.push(...members);
        }

        if (packet.d.chunk_index >= packet.d.chunk_count - 1) {
          if (this.requestMembersPromise.hasOwnProperty(packet.d.nonce)) {
            clearTimeout(this.requestMembersPromise[packet.d.nonce].timeout);
            this.requestMembersPromise[packet.d.nonce].res(this.requestMembersPromise[packet.d.nonce].members);
            delete this.requestMembersPromise[packet.d.nonce];
          }
          if (this.getAllUsersCount.hasOwnProperty(guild.id)) {
            delete this.getAllUsersCount[guild.id];
            this.checkReady();
          }
        }

        /**
         * Fired when Discord sends member chunks
         * @event Client#guildMemberChunk
         * @prop {Guild} guild The guild the chunked members are in
         * @prop {Array<Member>} members The members in the chunk
         */
        this.emit("guildMemberChunk", guild, members);

        this.lastHeartbeatAck = true;

        break;
      }
      case "RESUMED":
      case "READY": {
        this.connectAttempts = 0;
        this.reconnectInterval = 1000;

        this.connecting = false;
        if (this.connectTimeout) {
          clearTimeout(this.connectTimeout);
        }
        this.connectTimeout = null;
        this.status = "ready";
        this.presence.status = "online";
        this.client.shards._readyPacketCB(this.id);

        if (packet.t === "RESUMED") {
          // Can only heartbeat after resume succeeds, discord/discord-api-docs#1619
          this.heartbeat();

          this.preReady = true;
          this.ready = true;

          /**
           * Fired when a shard finishes resuming
           * @event Shard#resume
           */
          super.emit("resume");
          break;
        } else {
          this.resumeURL = `${packet.d.resume_gateway_url}?v=${Constants.GATEWAY_VERSION}&encoding=${Erlpack ? "etf" : "json"}`;

          if (this.client.options.compress) {
            this.resumeURL += "&compress=zlib-stream";
          }
        }

        this.client.user = this.client.users.update(new ExtendedUser(packet.d.user, this.client), this.client);
        if (this.client.user.bot) {
          this.client.bot = true;
          if (!this.client._token.startsWith("Bot ")) {
            this.client._token = "Bot " + this.client._token;
          }
        } else {
          this.emit("warn", new Error("User accounts are against Discord Terms of Service and not supported"));
        }

        if (packet.d._trace) {
          this.discordServerTrace = packet.d._trace;
        }

        this.sessionID = packet.d.session_id;

        packet.d.guilds.forEach((guild) => {
          if (guild.unavailable) {
            this.client.guilds.remove(guild);
            this.client.unavailableGuilds.add(guild, this.client, true);
          } else {
            this.client.unavailableGuilds.remove(this.createGuild(guild));
          }
        });

        packet.d.private_channels.forEach((channel) => {
          if (channel.type === undefined || channel.type === Constants.ChannelTypes.DM) {
            this.client.dmChannelMap[channel.recipients[0].id] = channel.id;
            this.client.dmChannels.add(channel, this.client, true);
          } else {
            this.emit("warn", new Error("Unhandled READY private_channel type: " + JSON.stringify(channel, null, 2)));
          }
        });

        this.client.application = packet.d.application;

        this.preReady = true;
        /**
         * Fired when a shard finishes processing the ready packet
         * @event Client#shardPreReady
         * @prop {Number} id The ID of the shard
         */
        this.emit("shardPreReady", this.id);

        if (this.client.unavailableGuilds.size > 0 && packet.d.guilds.length > 0) {
          this.restartGuildCreateTimeout();
        } else {
          this.checkReady();
        }

        break;
      }
      case "VOICE_SERVER_UPDATE": {
        packet.d.session_id = this.sessionID;
        packet.d.user_id = this.client.user.id;
        packet.d.shard = this;
        this.client.voiceConnections.voiceServerUpdate(packet.d);
        break;
      }
      case "USER_UPDATE": {
        let user = this.client.users.get(packet.d.id);
        let oldUser = null;
        if (user) {
          oldUser = {
            username: user.username,
            discriminator: user.discriminator,
            avatar: user.avatar,
            avatarDecorationData: user.avatarDecorationData,
          };
        }
        user = this.client.users.update(packet.d, this.client);
        this.emit("userUpdate", user, oldUser);
        break;
      }
      case "GUILD_EMOJIS_UPDATE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        let oldEmojis = null;
        let emojis = packet.d.emojis;
        if (guild) {
          oldEmojis = guild.emojis;
          guild.update(packet.d);
          emojis = guild.emojis;
        }
        /**
         * Fired when a guild's emojis are updated
         * @event Client#guildEmojisUpdate
         * @prop {Guild} guild The guild. If the guild is uncached, this is an object with an ID key. No other property is guaranteed
         * @prop {Array} emojis The updated emojis of the guild
         * @prop {Array?} oldEmojis The old emojis of the guild. If the guild is uncached, this will be null
         */
        this.emit("guildEmojisUpdate", guild || { id: packet.d.guild_id }, emojis, oldEmojis);
        break;
      }
      case "GUILD_STICKERS_UPDATE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        let oldStickers = null;
        let stickers = packet.d.stickers;
        if (guild) {
          oldStickers = guild.stickers;
          guild.update(packet.d);
          stickers = guild.stickers;
        }
        /**
         * Fired when a guild's stickers are updated
         * @event Client#guildStickersUpdate
         * @prop {Guild} guild The guild. If the guild is uncached, this is an object with an ID key. No other property is guaranteed
         * @prop {Array} stickers The updated stickers of the guild
         * @prop {Array?} oldStickers The old stickers of the guild. If the guild is uncached, this will be null
         */
        this.emit("guildStickersUpdate", guild || { id: packet.d.guild_id }, stickers, oldStickers);
        break;
      }
      case "CHANNEL_PINS_UPDATE": {
        const channel = this.client.getChannel(packet.d.channel_id);
        if (!channel) {
          this.emit("debug", `CHANNEL_PINS_UPDATE target channel ${packet.d.channel_id} not found`);
          break;
        }
        const oldTimestamp = channel.lastPinTimestamp;
        channel.lastPinTimestamp = Date.parse(packet.d.last_pin_timestamp);
        /**
         * Fired when a channel pin timestamp is updated
         * @event Client#channelPinUpdate
         * @prop {DMChannel | TextChannel | NewsChannel} channel The channel
         * @prop {Number} timestamp The new timestamp
         * @prop {Number} oldTimestamp The old timestamp
         */
        this.emit("channelPinUpdate", channel, channel.lastPinTimestamp, oldTimestamp);
        break;
      }
      case "WEBHOOKS_UPDATE": {
        /**
         * Fired when a channel's webhooks are updated
         * @event Client#webhooksUpdate
         * @prop {Object} data The update data
         * @prop {String} data.channelID The ID of the channel that webhooks were updated in
         * @prop {String} data.guildID The ID of the guild that webhooks were updated in
         */
        this.emit("webhooksUpdate", {
          channelID: packet.d.channel_id,
          guildID: packet.d.guild_id,
        });
        break;
      }
      case "THREAD_CREATE": {
        const channel = Channel.from(packet.d, this.client);
        if (!channel.guild) {
          channel.guild = this.client.guilds.get(packet.d.guild_id);
          if (!channel.guild) {
            this.emit("debug", `Received THREAD_CREATE for channel in missing guild ${packet.d.guild_id}`);
            break;
          }
        }
        channel.guild.threads.add(channel, this.client);
        this.client.threadGuildMap[packet.d.id] = packet.d.guild_id;

        const parent = channel.guild.channels.get(channel.parentID);

        if (parent instanceof ForumChannel) {
          parent.lastMessageID = channel.id;
        }

        /**
         * Fired when a channel is created
         * @event Client#threadCreate
         * @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The channel
         */
        this.emit("threadCreate", channel);
        break;
      }
      case "THREAD_UPDATE": {
        const channel = this.client.getChannel(packet.d.id);
        if (!channel) {
          const thread = Channel.from(packet.d, this.client);
          this.emit("threadUpdate", this.client.guilds.get(packet.d.guild_id).threads.add(thread, this.client), null);
          this.client.threadGuildMap[packet.d.id] = packet.d.guild_id;
          break;
        }
        if (!(channel instanceof ThreadChannel)) {
          this.emit("warn", `Unexpected THREAD_UPDATE for channel ${packet.d.id} with type ${channel.type}`);
          break;
        }
        const oldChannel = {
          appliedTags: channel.appliedTags,
          autoArchiveDuration: channel.autoArchiveDuration,
          flags: channel.flags,
          name: channel.name,
          rateLimitPerUser: channel.rateLimitPerUser,
          threadMetadata: channel.threadMetadata,
        };
        channel.update(packet.d);

        /**
         * Fired when a thread channel is updated
         * @event Client#threadUpdate
         * @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The updated channel
         * @prop {Object?} oldChannel The old thread channel. This will be null if the channel was uncached
         * @prop {Array<String>?} oldChannel.appliedTags The IDs of the set of tags that have been applied to a thread in a forum/media channel
         * @prop {Number} oldChannel.autoArchiveDuration The duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080
         * @prop {Number} oldChannel.flags The flags for the channel combined as a bitfield
         * @prop {String} oldChannel.name The name of the channel
         * @prop {Number} oldChannel.rateLimitPerUser The time in seconds a user has to wait before sending another message (0-21600)
         * @prop {Object} oldChannel.threadMetadata Metadata for the thread
         * @prop {Boolean} oldChannel.threadMetadata.archived Whether the thread is archived
         * @prop {Number} oldChannel.threadMetadata.archiveTimestamp Timestamp when the thread's archive status was last changed, used for calculating recent activity
         * @prop {Number} oldChannel.threadMetadata.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080
         * @prop {Boolean?} oldChannel.threadMetadata.locked Whether the thread is locked
         */
        this.emit("threadUpdate", channel, oldChannel);
        break;
      }
      case "THREAD_DELETE": {
        delete this.client.threadGuildMap[packet.d.id];
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in THREAD_DELETE`);
          break;
        }
        const channel = guild.threads.remove(packet.d);
        if (!channel) {
          break;
        }
        /**
         * Fired when a thread channel is deleted
         * @event Client#threadDelete
         * @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The channel
         */
        this.emit("threadDelete", channel);
        break;
      }
      case "THREAD_LIST_SYNC": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in THREAD_LIST_SYNC`);
          break;
        }
        const deletedThreads = (packet.d.channel_ids || guild.threads.map((c) => c.id)) // REVIEW Is this a good name?
          .filter((c) => !packet.d.threads.some((t) => t.id === c)).map((id) => guild.threads.remove({ id }) || { id });
        const activeThreads = packet.d.threads.map((t) => guild.threads.update(t, this.client));
        const joinedThreadsMember = packet.d.members.map((m) => guild.threads.get(m.id).members.update(m, this.client));
        /**
         * Fired when the current user gains access to a channel
         * @event Client#threadListSync
         * @prop {Guild} guild The guild where threads are being synced
         * @prop {Array<NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel | Object>} deletedThreads An array of synced threads that the current user no longer has access to. If a thread channel is uncached, it will be an object with an `id` key. No other property is guaranteed
         * @prop {Array<NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel>} activeThreads An array of synced active threads that the current user can access
         * @prop {Array<ThreadMember>} joinedThreadsMember An array of thread member objects where the current user has been added in a synced thread channel
         */
        this.emit("threadListSync", guild, deletedThreads, activeThreads, joinedThreadsMember);
        break;
      }
      case "THREAD_MEMBER_UPDATE": {
        const channel = this.client.getChannel(packet.d.id);
        if (!channel) {
          this.emit("debug", `Missing channel ${packet.d.id} in THREAD_MEMBER_UPDATE`);
          break;
        }
        let oldMember = null;
        // Thanks Discord
        packet.d.thread_id = packet.d.id;
        let member = channel.members.get((packet.d.id = packet.d.user_id));
        if (member) {
          oldMember = {
            flags: member.flags,
          };
        }
        member = channel.members.update(packet.d, this.client);
        /**
         * Fired when a thread member is updated
         * @event Client#threadMemberUpdate
         * @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The channel
         * @prop {ThreadMember} member The updated thread member
         * @prop {Object} oldMember The old thread member data
         * @prop {Number} oldMember.flags User thread settings
         */
        this.emit("threadMemberUpdate", channel, member, oldMember);
        break;
      }
      case "THREAD_MEMBERS_UPDATE": {
        const channel = this.client.getChannel(packet.d.id);
        if (!channel) {
          this.emit("debug", `Missing channel ${packet.d.id} in THREAD_MEMBERS_UPDATE`);
          break;
        }
        channel.update(packet.d);
        let addedMembers;
        let removedMembers;
        if (packet.d.added_members) {
          addedMembers = packet.d.added_members.map((m) => {
            if (m.presence) {
              m.presence.id = m.presence.user.id;
              this.client.users.update(m.presence.user, this.client);
            }

            m.thread_id = m.id;
            m.id = m.user_id;
            m.member.id = m.member.user.id;
            const guild = this.client.guilds.get(packet.d.guild_id);
            if (guild) {
              if (m.presence) {
                guild.members.update(m.presence, guild);
              }
              guild.members.update(m.member, guild);
            }
            return channel.members.update(m, this.client);
          });
        }
        if (packet.d.removed_member_ids) {
          removedMembers = packet.d.removed_member_ids.map((id) => channel.members.remove({ id }) || { id });
        }
        /**
         * Fired when anyone is added or removed from a thread. If the `guildMembers` intent is not specified, this will only apply for the current user
         * @event Client#threadMembersUpdate
         * @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The thread channel
         * @prop {Array<ThreadMember>} addedMembers An array of members that were added to the thread channel
         * @prop {Array<ThreadMember | Object>} removedMembers An array of members that were removed from the thread channel. If a member is uncached, it will be an object with an `id` key. No other property is guaranteed
         */
        this.emit("threadMembersUpdate", channel, addedMembers || [], removedMembers || []);
        break;
      }
      case "STAGE_INSTANCE_CREATE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("debug", `Missing guild ${packet.d.guild_id} in STAGE_INSTANCE_CREATE`);
          break;
        }
        /**
         * Fired when a stage instance is created
         * @event Client#stageInstanceCreate
         * @prop {StageInstance} stageInstance The stage instance
         */
        this.emit("stageInstanceCreate", guild.stageInstances.add(packet.d, this.client));
        break;
      }
      case "STAGE_INSTANCE_UPDATE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("stageInstanceUpdate", packet.d, null);
          break;
        }
        const stageInstance = guild.stageInstances.get(packet.d.id);
        let oldStageInstance = null;
        if (stageInstance) {
          oldStageInstance = {
            discoverableDisabled: stageInstance.discoverableDisabled,
            privacyLevel: stageInstance.privacyLevel,
            topic: stageInstance.topic,
          };
        }
        /**
         * Fired when a stage instance is updated
         * @event Client#stageInstanceUpdate
         * @prop {StageInstance} stageInstance The stage instance
         * @prop {Object?} oldStageInstance The old stage instance. If the stage instance was cached, this will be an object with the properties below. Otherwise, it will be null
         * @prop {Boolean} oldStageInstance.discoverableDisabled Whether or not stage discovery was disabled
         * @prop {Number} oldStageInstance.privacyLevel The privacy level of the stage instance. 1 is public, 2 is guild only
         * @prop {String} oldStageInstance.topic The stage instance topic
         */
        this.emit("stageInstanceUpdate", guild.stageInstances.update(packet.d, this.client), oldStageInstance);
        break;
      }
      case "STAGE_INSTANCE_DELETE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("stageInstanceDelete", new StageInstance(packet.d, this.client));
          break;
        }
        /**
         * Fired when a stage instance is deleted
         * @event Client#stageInstanceDelete
         * @prop {StageInstance} stageInstance The deleted stage instance
         */
        this.emit("stageInstanceDelete", guild.stageInstances.remove(packet.d) || new StageInstance(packet.d, this.client));
        break;
      }
      case "GUILD_SCHEDULED_EVENT_CREATE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("guildScheduledEventCreate", new GuildScheduledEvent(packet.d, this.client));
          break;
        }

        /**
         * Fired when a guild scheduled event is created
         * @event Client#guildScheduledEventCreate
         * @prop {GuildScheduledEvent} event The event
         */
        this.emit("guildScheduledEventCreate", guild.events.add(packet.d, this.client));
        break;
      }
      case "GUILD_SCHEDULED_EVENT_UPDATE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("guildScheduledEventUpdate", new GuildScheduledEvent(packet.d, this.client), null);
          break;
        }

        const event = guild.events.get(packet.d.id);
        let oldEvent = null;
        if (event) {
          oldEvent = {
            channel: event.channel,
            description: event.description,
            entityID: event.entityID,
            entityMetadata: event.entityMetadata,
            entityType: event.entityType,
            image: event.image,
            name: event.name,
            privacyLevel: event.privacyLevel,
            scheduledEndTime: event.scheduledEndTime,
            scheduledStartTime: event.scheduledStartTime,
            status: event.status,
          };
        }

        /**
         * Fired when a guild scheduled event is updated
         * @event Client#guildScheduledEventUpdate
         * @prop {GuildScheduledEvent} event The updated event
         * @prop {Object?} oldEvent The old guild event data, or null if the event wasn't cached.
         * @prop {(VoiceChannel | StageChannel | Object)?} oldEvent.channel The channel where the event is held
         * @prop {String?} oldEvent.description The description of the event
         * @prop {String?} oldEvent.entityID The Entity ID associated to the event
         * @prop {Object?} oldEvent.entityMetadata Metadata for the event
         * @prop {String?} oldEvent.entityMetadata.location Location of the event
         * @prop {Number} oldEvent.entityType The event entity type
         * @prop {String?} oldEvent.image The hash of the event's image
         * @prop {String} oldEvent.name The name of the event
         * @prop {Number} oldEvent.privacyLevel The privacy level of the event
         * @prop {Number?} oldEvent.scheduledEndTime The time the event will start
         * @prop {Number} oldEvent.scheduledStartTime The time the event will start
         * @prop {Number} oldEvent.status The status of the guild scheduled event
         */
        this.emit("guildScheduledEventUpdate", guild.events.update(packet.d, this.client), oldEvent);
        break;
      }
      case "GUILD_SCHEDULED_EVENT_DELETE": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("guildScheduledEventDelete", new GuildScheduledEvent(packet.d, this.client));
          break;
        }
        /**
         * Fired when a guild scheduled event is deleted
         * @event Client#guildScheduledEventDelete
         * @prop {GuildScheduledEvent} event The event that was deleted.
         */
        this.emit("guildScheduledEventDelete", guild.events.remove(packet.d) || new GuildScheduledEvent(packet.d, this.client));
        break;
      }
      case "GUILD_SCHEDULED_EVENT_USER_ADD": {
        const user = this.client.users.get(packet.d.user_id) || { id: packet.d.user_id };

        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("guildScheduledEventUserAdd", { id: packet.d.guild_scheduled_event_id, guild: { id: packet.d.guild_id } }, user);
          break;
        }

        const event = guild.events.get(packet.d.guild_scheduled_event_id);
        if (event) {
          ++event.userCount;
        }

        /**
         * Fired when an user has subscribed to a Guild Event.
         * @event Client#guildScheduledEventUserAdd
         * @prop {GuildScheduledEvent | Object} event The guild event that the user subscribed to. If the event is uncached, this will be an object with `id` and `guild` keys. No other property is guaranteed
         * @prop {User | Object} user The user that subscribed to the Guild Event. If the user is uncached, this will be an object with an `id` key. No other property is guaranteed
         */
        this.emit("guildScheduledEventUserAdd", event || { id: packet.d.guild_scheduled_event_id, guild: guild }, user);
        break;
      }
      case "GUILD_SCHEDULED_EVENT_USER_REMOVE": {
        const user = this.client.users.get(packet.d.user_id) || { id: packet.d.user_id };

        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("guildScheduledEventUserRemove", { id: packet.d.guild_scheduled_event_id, guild: { id: packet.d.guild_id } }, user);
          break;
        }

        const event = guild.events.get(packet.d.guild_scheduled_event_id);
        if (event) {
          --event.userCount;
        }

        /**
         * Fired when an user has unsubscribed from a Guild Event.
         * @event Client#guildScheduledEventUserRemove
         * @prop {GuildScheduledEvent | string} event The guild event that the user unsubscribed to. This will be the guild event ID if the guild was uncached
         * @prop {User | string} user The user that unsubscribed to the Guild Event. This will be the user ID if the user was uncached
         */
        this.emit("guildScheduledEventUserRemove", event || { id: packet.d.guild_scheduled_event_id, guild: guild }, user);
        break;
      }
      case "INTERACTION_CREATE": {
        /**
         * Fired when an interaction is created
         * @event Client#interactionCreate
         * @prop {PingInteraction | CommandInteraction | ComponentInteraction | AutocompleteInteraction | ModalSubmitInteraction | UnknownInteraction} interaction The Interaction that was created
         */
        this.emit("interactionCreate", Interaction.from(packet.d, this.client));
        break;
      }
      case "APPLICATION_COMMAND_PERMISSIONS_UPDATE": {
        /**
         * Fired when an application command's permissions are updated
         * @event Client#applicationCommandPermissionsUpdate
         * @prop {Object} guildApplicationCommandPermissions The updated command permissions
         */
        this.emit("applicationCommandPermissionsUpdate", packet.d);
        break;
      }
      case "GUILD_INTEGRATIONS_UPDATE": {
        /**
         * Fired when a guild integration is updated
         * @event Client#guildIntegrationsUpdate
         * @prop {Guild} guild The guild where the integration was updated. If the guild isn't cached, this will be an object with an `id` key. No other properties are guaranteed
         */
        this.emit("guildIntegrationsUpdate", this.client.guilds.get(packet.d.guild_id) || { id: packet.d.guild_id });
        break;
      }
      case "GUILD_SOUNDBOARD_SOUND_CREATE": {
        packet.d.id = packet.d.sound_id;
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("guildSoundboardSoundCreate", new SoundboardSound(packet.d, this.client));
          break;
        }
        /**
         * Fired when a guild soundboard sound is created
         * @event Client#guildSoundboardSoundCreate
         * @prop {SoundboardSound} sound The created soundboard sound
         */
        this.emit("guildSoundboardSoundCreate", guild.soundboardSounds.add(packet.d, this.client));
        break;
      }
      case "GUILD_SOUNDBOARD_SOUND_DELETE": {
        packet.d.id = packet.d.sound_id;
        const guild = this.client.guilds.get(packet.d.guild_id);
        /**
         * Fired when a guild soundboard sound is deleted
         * @event Client#guildSoundboardSoundDelete
         * @prop {SoundboardSound} sound The deleted soundboard sound. If the soundboard sound isn't cached, this will be an object with `id` and `guild` keys. If the guild isn't cached, it will be an object with an `id` key. No other properties are guaranteed
         */
        this.emit("guildSoundboardSoundDelete", (guild && guild.soundboardSounds.remove(packet.d)) || {
          id: packet.d.id,
          guild: guild || {
            id: packet.d.guild_id,
          },
        });
        break;
      }
      case "GUILD_SOUNDBOARD_SOUND_UPDATE": {
        packet.d.id = packet.d.sound_id;
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("guildSoundboardSoundUpdate", new SoundboardSound(packet.d, this.client), null);
          break;
        }

        const sound = guild.soundboardSounds.get(packet.d.id);
        let oldSound = null;
        if (sound) {
          oldSound = {
            name: sound.name,
            volume: sound.volume,
            emojiID: sound.emojiID,
            emojiName: sound.emojiName,
            available: sound.available,
          };
        }

        /**
         * Fired when a guild soundboard sound is updated
         * @event Client#guildSoundboardSoundUpdate
         * @prop {SoundboardSound} sound The updated soundboard sound
         * @prop {Object} oldSound The old soundboard sound data, or null if not cached
         * @prop {Boolean} oldSound.available Whether the soundboard sound was available or not
         * @prop {String?} oldSound.emojiID The ID of the relating custom emoji
         * @prop {String?} oldSound.emojiName The name of the relating default emoji
         * @prop {String} oldSound.name The name of the soundboard sound
         * @prop {Number} oldSound.volume The volume of the soundboard sound, between 0 and 1
         */
        this.emit("guildSoundboardSoundUpdate", guild.soundboardSounds.update(packet.d, this.client), oldSound);
        break;
      }
      case "GUILD_SOUNDBOARD_SOUNDS_UPDATE": {
        packet.d.soundboard_sounds = packet.d.soundboard_sounds.map((sound) => {
          sound.id = sound.sound_id;
          return sound;
        });
        const guild = this.client.guilds.get(packet.d.guild_id);
        if (!guild) {
          this.emit("guildSoundboardSoundsUpdate", { id: packet.d.guild_id }, packet.d.soundboard_sounds.map((sound) => new SoundboardSound(sound, this.client)), null);
          break;
        }

        const oldSounds = packet.d.soundboard_sounds.map((_sound) => {
          const sound = guild.soundboardSounds.get(_sound.id);
          if (!sound) {
            return null;
          }
          return {
            name: sound.name,
            volume: sound.volume,
            emojiID: sound.emojiID,
            emojiName: sound.emojiName,
            available: sound.available,
          };
        });

        /**
         * Fired when multiple guild soundboard sounds are updated
         * @event Client#guildSoundboardSoundsUpdate
         * @prop {Guild} guild The guild. If the guild is uncached, this is an object with an ID key. No other property is guaranteed
         * @prop {Array<SoundboardSound>} sounds The updated soundboard sounds
         * @prop {Array<Object>} oldSounds The old soundboard sounds data, or null if not cached
         * @prop {Boolean} oldSounds[].available Whether the soundboard sound was available or not
         * @prop {String?} oldSounds[].emojiID The ID of the relating custom emoji
         * @prop {String?} oldSounds[].emojiName The name of the relating default emoji
         * @prop {String} oldSounds[].name The name of the soundboard sound
         * @prop {Number} oldSounds[].volume The volume of the soundboard sound, between 0 and 1
         */
        this.emit("guildSoundboardSoundsUpdate", guild, packet.d.soundboard_sounds.map((sound) => guild.soundboardSounds.update(sound, this.client)), oldSounds);
        break;
      }
      case "SOUNDBOARD_SOUNDS": {
        const guild = this.client.guilds.get(packet.d.guild_id);
        const sounds = packet.d.soundboard_sounds.map((sound) => {
          sound.id = sound.sound_id;
          const s = guild ? guild.soundboardSounds.update(sound, this.client) : new SoundboardSound(sound, this.client);
          return s;
        });

        /**
         * Fired when Discord sends a guild's soundboard sounds. Sent in response to `fetchSoundboardSounds()`
         * @prop {Guild} guild The ID of the guild. If the guild is uncached, this will be an object with an `id` key. No other properties are guaranteed
         * @prop {Array<SoundboardSound>} sounds The guild's soundboard sounds
         */
        this.emit("soundboardSounds", guild || { id: packet.d.guild_id }, sounds);
        this.lastHeartbeatAck = true;

        for (const nonce in this.requestSoundboardSoundsPromise) {
          if (packet.d.guild_id in this.requestSoundboardSoundsPromise[nonce].soundboardSounds) {
            this.requestSoundboardSoundsPromise[nonce].soundboardSounds[packet.d.guild_id] = sounds;
            if (Object.values(this.requestSoundboardSoundsPromise[nonce].soundboardSounds).every((v) => v !== undefined)) {
              clearTimeout(this.requestSoundboardSoundsPromise[nonce].timeout);
              this.requestSoundboardSoundsPromise[nonce].res(this.requestSoundboardSoundsPromise[nonce].soundboardSounds);
              delete this.requestSoundboardSoundsPromise[nonce];
            }
          }
        }
        break;
      }
      default: {
        /**
         * Fired when the shard encounters an unknown packet
         * @event Client#unknown
         * @prop {Object} packet The unknown packet
         * @prop {Number} id The ID of the shard
         */
        this.emit("unknown", packet, this.id);
        break;
      }
    }
  }

  _onWSClose(code, reason) {
    reason = reason.toString();
    this.emit("debug", "WS disconnected: " + JSON.stringify({
      code: code,
      reason: reason,
      status: this.status,
    }));
    let err = !code || code === 1000 ? null : new Error(code + ": " + reason);
    let reconnect = "auto";
    if (code) {
      this.emit("debug", `${code === 1000 ? "Clean" : "Unclean"} WS close: ${code}: ${reason}`, this.id);
      if (code === 4001) {
        err = new Error("Gateway received invalid OP code");
      } else if (code === 4002) {
        err = new Error("Gateway received invalid message");
      } else if (code === 4003) {
        err = new Error("Not authenticated");
        this.sessionID = null;
        this.resumeURL = null;
      } else if (code === 4004) {
        err = new Error("Authentication failed");
        this.sessionID = null;
        this.resumeURL = null;
        reconnect = false;
        this.emit("error", new Error(`Invalid token: ${this._token}`));
      } else if (code === 4005) {
        err = new Error("Already authenticated");
      } else if (code === 4006 || code === 4009) {
        err = new Error("Invalid session");
        this.sessionID = null;
        this.resumeURL = null;
      } else if (code === 4007) {
        err = new Error("Invalid sequence number: " + this.seq);
        this.seq = 0;
      } else if (code === 4008) {
        err = new Error("Gateway connection was ratelimited");
      } else if (code === 4010) {
        err = new Error("Invalid shard key");
        this.sessionID = null;
        this.resumeURL = null;
        reconnect = false;
      } else if (code === 4011) {
        err = new Error("Shard has too many guilds (>2500)");
        this.sessionID = null;
        this.resumeURL = null;
        reconnect = false;
      } else if (code === 4013) {
        err = new Error("Invalid intents specified");
        this.sessionID = null;
        this.resumeURL = null;
        reconnect = false;
      } else if (code === 4014) {
        err = new Error("Disallowed intents specified");
        this.sessionID = null;
        this.resumeURL = null;
        reconnect = false;
      } else if (code === 1006) {
        err = new Error("Connection reset by peer");
      } else if (code !== 1000 && reason) {
        err = new Error(code + ": " + reason);
      }
      if (err) {
        err.code = code;
      }
    } else {
      this.emit("debug", "WS close: unknown code: " + reason, this.id);
    }
    this.disconnect({
      reconnect,
    }, err);
  }

  _onWSError(err) {
    this.emit("error", err, this.id);
  }

  _onWSMessage(data) {
    try {
      if (data instanceof ArrayBuffer) {
        if (this.client.options.compress || Erlpack) {
          data = Buffer.from(data);
        }
      } else if (Array.isArray(data)) { // Fragmented messages
        data = Buffer.concat(data); // Copyfull concat is slow, but no alternative
      }
      if (this.client.options.compress) {
        if (data.length >= 4 && data.readUInt32BE(data.length - 4) === 0xFFFF) {
          this._zlibSync.push(data, ZlibSync.Z_SYNC_FLUSH);
          if (this._zlibSync.err) {
            this.emit("error", new Error(`zlib error ${this._zlibSync.err}: ${this._zlibSync.msg}`));
            return;
          }

          data = Buffer.from(this._zlibSync.result);
          if (Erlpack) {
            return this.onPacket(Erlpack.unpack(data));
          } else {
            return this.onPacket(JSON.parse(data.toString()));
          }
        } else {
          this._zlibSync.push(data, false);
        }
      } else if (Erlpack) {
        return this.onPacket(Erlpack.unpack(data));
      } else {
        return this.onPacket(JSON.parse(data.toString()));
      }
    } catch (err) {
      this.emit("error", err, this.id);
    }
  }

  _onWSOpen() {
    this.status = "handshaking";
    /**
     * Fired when the shard establishes a connection
     * @event Client#connect
     * @prop {Number} id The ID of the shard
     */
    this.emit("connect", this.id);
    this.lastHeartbeatAck = true;
  }

  [util.inspect.custom]() {
    return Base.prototype[util.inspect.custom].call(this);
  }

  toString() {
    return Base.prototype.toString.call(this);
  }

  toJSON(props = []) {
    return Base.prototype.toJSON.call(this, [
      "connectAttempts",
      "connecting",
      "discordServerTrace",
      "getAllUsersCount",
      "getAllUsersLength",
      "getAllUsersQueue",
      "lastHeartbeatAck",
      "lastHeartbeatReceived",
      "lastHeartbeatSent",
      "latency",
      "preReady",
      "ready",
      "reconnectInterval",
      "seq",
      "sessionID",
      "status",
      ...props,
    ]);
  }
}

module.exports = Shard;