summaryrefslogtreecommitdiff
path: root/node_modules/discord.js/src/client/websocket/WebSocketManager.js
diff options
context:
space:
mode:
authorsowgro <tpoke.ferrari@gmail.com>2023-09-02 19:12:47 -0400
committersowgro <tpoke.ferrari@gmail.com>2023-09-02 19:12:47 -0400
commite4450c8417624b71d779cb4f41692538f9165e10 (patch)
treeb70826542223ecdf8a7a259f61b0a1abb8a217d8 /node_modules/discord.js/src/client/websocket/WebSocketManager.js
downloadsowbot3-e4450c8417624b71d779cb4f41692538f9165e10.tar.gz
sowbot3-e4450c8417624b71d779cb4f41692538f9165e10.tar.bz2
sowbot3-e4450c8417624b71d779cb4f41692538f9165e10.zip
first commit
Diffstat (limited to 'node_modules/discord.js/src/client/websocket/WebSocketManager.js')
-rw-r--r--node_modules/discord.js/src/client/websocket/WebSocketManager.js394
1 files changed, 394 insertions, 0 deletions
diff --git a/node_modules/discord.js/src/client/websocket/WebSocketManager.js b/node_modules/discord.js/src/client/websocket/WebSocketManager.js
new file mode 100644
index 0000000..f62610b
--- /dev/null
+++ b/node_modules/discord.js/src/client/websocket/WebSocketManager.js
@@ -0,0 +1,394 @@
+'use strict';
+
+const EventEmitter = require('node:events');
+const process = require('node:process');
+const { setImmediate } = require('node:timers');
+const { Collection } = require('@discordjs/collection');
+const {
+ WebSocketManager: WSWebSocketManager,
+ WebSocketShardEvents: WSWebSocketShardEvents,
+ CompressionMethod,
+ CloseCodes,
+} = require('@discordjs/ws');
+const { GatewayCloseCodes, GatewayDispatchEvents } = require('discord-api-types/v10');
+const WebSocketShard = require('./WebSocketShard');
+const PacketHandlers = require('./handlers');
+const { DiscordjsError, ErrorCodes } = require('../../errors');
+const Events = require('../../util/Events');
+const Status = require('../../util/Status');
+const WebSocketShardEvents = require('../../util/WebSocketShardEvents');
+
+let zlib;
+
+try {
+ zlib = require('zlib-sync');
+} catch {} // eslint-disable-line no-empty
+
+const BeforeReadyWhitelist = [
+ GatewayDispatchEvents.Ready,
+ GatewayDispatchEvents.Resumed,
+ GatewayDispatchEvents.GuildCreate,
+ GatewayDispatchEvents.GuildDelete,
+ GatewayDispatchEvents.GuildMembersChunk,
+ GatewayDispatchEvents.GuildMemberAdd,
+ GatewayDispatchEvents.GuildMemberRemove,
+];
+
+const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete];
+
+const UNRESUMABLE_CLOSE_CODES = [
+ CloseCodes.Normal,
+ GatewayCloseCodes.AlreadyAuthenticated,
+ GatewayCloseCodes.InvalidSeq,
+];
+
+const reasonIsDeprecated = 'the reason property is deprecated, use the code property to determine the reason';
+let deprecationEmittedForInvalidSessionEvent = false;
+let deprecationEmittedForDestroyedEvent = false;
+
+/**
+ * The WebSocket manager for this client.
+ * <info>This class forwards raw dispatch events,
+ * read more about it here {@link https://discord.com/developers/docs/topics/gateway}</info>
+ * @extends {EventEmitter}
+ */
+class WebSocketManager extends EventEmitter {
+ constructor(client) {
+ super();
+
+ /**
+ * The client that instantiated this WebSocketManager
+ * @type {Client}
+ * @readonly
+ * @name WebSocketManager#client
+ */
+ Object.defineProperty(this, 'client', { value: client });
+
+ /**
+ * The gateway this manager uses
+ * @type {?string}
+ */
+ this.gateway = null;
+
+ /**
+ * A collection of all shards this manager handles
+ * @type {Collection<number, WebSocketShard>}
+ */
+ this.shards = new Collection();
+
+ /**
+ * An array of queued events before this WebSocketManager became ready
+ * @type {Object[]}
+ * @private
+ * @name WebSocketManager#packetQueue
+ */
+ Object.defineProperty(this, 'packetQueue', { value: [] });
+
+ /**
+ * The current status of this WebSocketManager
+ * @type {Status}
+ */
+ this.status = Status.Idle;
+
+ /**
+ * If this manager was destroyed. It will prevent shards from reconnecting
+ * @type {boolean}
+ * @private
+ */
+ this.destroyed = false;
+
+ /**
+ * The internal WebSocketManager from `@discordjs/ws`.
+ * @type {WSWebSocketManager}
+ * @private
+ */
+ this._ws = null;
+ }
+
+ /**
+ * The average ping of all WebSocketShards
+ * @type {number}
+ * @readonly
+ */
+ get ping() {
+ const sum = this.shards.reduce((a, b) => a + b.ping, 0);
+ return sum / this.shards.size;
+ }
+
+ /**
+ * Emits a debug message.
+ * @param {string} message The debug message
+ * @param {?number} [shardId] The id of the shard that emitted this message, if any
+ * @private
+ */
+ debug(message, shardId) {
+ this.client.emit(
+ Events.Debug,
+ `[WS => ${typeof shardId === 'number' ? `Shard ${shardId}` : 'Manager'}] ${message}`,
+ );
+ }
+
+ /**
+ * Connects this manager to the gateway.
+ * @private
+ */
+ async connect() {
+ const invalidToken = new DiscordjsError(ErrorCodes.TokenInvalid);
+ const { shards, shardCount, intents, ws } = this.client.options;
+ if (this._ws && this._ws.options.token !== this.client.token) {
+ await this._ws.destroy({ code: CloseCodes.Normal, reason: 'Login with differing token requested' });
+ this._ws = null;
+ }
+ if (!this._ws) {
+ const wsOptions = {
+ intents: intents.bitfield,
+ rest: this.client.rest,
+ token: this.client.token,
+ largeThreshold: ws.large_threshold,
+ version: ws.version,
+ shardIds: shards === 'auto' ? null : shards,
+ shardCount: shards === 'auto' ? null : shardCount,
+ initialPresence: ws.presence,
+ retrieveSessionInfo: shardId => this.shards.get(shardId).sessionInfo,
+ updateSessionInfo: (shardId, sessionInfo) => {
+ this.shards.get(shardId).sessionInfo = sessionInfo;
+ },
+ compression: zlib ? CompressionMethod.ZlibStream : null,
+ };
+ if (ws.buildIdentifyThrottler) wsOptions.buildIdentifyThrottler = ws.buildIdentifyThrottler;
+ if (ws.buildStrategy) wsOptions.buildStrategy = ws.buildStrategy;
+ this._ws = new WSWebSocketManager(wsOptions);
+ this.attachEvents();
+ }
+
+ const {
+ url: gatewayURL,
+ shards: recommendedShards,
+ session_start_limit: sessionStartLimit,
+ } = await this._ws.fetchGatewayInformation().catch(error => {
+ throw error.status === 401 ? invalidToken : error;
+ });
+
+ const { total, remaining } = sessionStartLimit;
+
+ this.debug(`Fetched Gateway Information
+ URL: ${gatewayURL}
+ Recommended Shards: ${recommendedShards}`);
+
+ this.debug(`Session Limit Information
+ Total: ${total}
+ Remaining: ${remaining}`);
+
+ this.gateway = `${gatewayURL}/`;
+
+ this.client.options.shardCount = await this._ws.getShardCount();
+ this.client.options.shards = await this._ws.getShardIds();
+ this.totalShards = this.client.options.shards.length;
+ for (const id of this.client.options.shards) {
+ if (!this.shards.has(id)) {
+ const shard = new WebSocketShard(this, id);
+ this.shards.set(id, shard);
+
+ shard.on(WebSocketShardEvents.AllReady, unavailableGuilds => {
+ /**
+ * Emitted when a shard turns ready.
+ * @event Client#shardReady
+ * @param {number} id The shard id that turned ready
+ * @param {?Set<Snowflake>} unavailableGuilds Set of unavailable guild ids, if any
+ */
+ this.client.emit(Events.ShardReady, shard.id, unavailableGuilds);
+
+ this.checkShardsReady();
+ });
+ shard.status = Status.Connecting;
+ }
+ }
+
+ await this._ws.connect();
+
+ this.shards.forEach(shard => {
+ if (shard.listenerCount(WebSocketShardEvents.InvalidSession) > 0 && !deprecationEmittedForInvalidSessionEvent) {
+ process.emitWarning(
+ 'The WebSocketShard#invalidSession event is deprecated and will never emit.',
+ 'DeprecationWarning',
+ );
+
+ deprecationEmittedForInvalidSessionEvent = true;
+ }
+ if (shard.listenerCount(WebSocketShardEvents.Destroyed) > 0 && !deprecationEmittedForDestroyedEvent) {
+ process.emitWarning(
+ 'The WebSocketShard#destroyed event is deprecated and will never emit.',
+ 'DeprecationWarning',
+ );
+
+ deprecationEmittedForDestroyedEvent = true;
+ }
+ });
+ }
+
+ /**
+ * Attaches event handlers to the internal WebSocketShardManager from `@discordjs/ws`.
+ * @private
+ */
+ attachEvents() {
+ this._ws.on(WSWebSocketShardEvents.Debug, ({ message, shardId }) => this.debug(message, shardId));
+ this._ws.on(WSWebSocketShardEvents.Dispatch, ({ data, shardId }) => {
+ this.client.emit(Events.Raw, data, shardId);
+ this.emit(data.t, data.d, shardId);
+ const shard = this.shards.get(shardId);
+ this.handlePacket(data, shard);
+ if (shard.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(data.t)) {
+ shard.gotGuild(data.d.id);
+ }
+ });
+
+ this._ws.on(WSWebSocketShardEvents.Ready, ({ data, shardId }) => {
+ this.shards.get(shardId).onReadyPacket(data);
+ });
+
+ this._ws.on(WSWebSocketShardEvents.Closed, ({ code, shardId }) => {
+ const shard = this.shards.get(shardId);
+ shard.emit(WebSocketShardEvents.Close, { code, reason: reasonIsDeprecated, wasClean: true });
+ if (UNRESUMABLE_CLOSE_CODES.includes(code) && this.destroyed) {
+ shard.status = Status.Disconnected;
+ /**
+ * Emitted when a shard's WebSocket disconnects and will no longer reconnect.
+ * @event Client#shardDisconnect
+ * @param {CloseEvent} event The WebSocket close event
+ * @param {number} id The shard id that disconnected
+ */
+ this.client.emit(Events.ShardDisconnect, { code, reason: reasonIsDeprecated, wasClean: true }, shardId);
+ this.debug(GatewayCloseCodes[code], shardId);
+ return;
+ }
+
+ this.shards.get(shardId).status = Status.Connecting;
+ /**
+ * Emitted when a shard is attempting to reconnect or re-identify.
+ * @event Client#shardReconnecting
+ * @param {number} id The shard id that is attempting to reconnect
+ */
+ this.client.emit(Events.ShardReconnecting, shardId);
+ });
+ this._ws.on(WSWebSocketShardEvents.Hello, ({ shardId }) => {
+ const shard = this.shards.get(shardId);
+ if (shard.sessionInfo) {
+ shard.closeSequence = shard.sessionInfo.sequence;
+ shard.status = Status.Resuming;
+ } else {
+ shard.status = Status.Identifying;
+ }
+ });
+
+ this._ws.on(WSWebSocketShardEvents.Resumed, ({ shardId }) => {
+ const shard = this.shards.get(shardId);
+ shard.status = Status.Ready;
+ /**
+ * Emitted when the shard resumes successfully
+ * @event WebSocketShard#resumed
+ */
+ shard.emit(WebSocketShardEvents.Resumed);
+ });
+
+ this._ws.on(WSWebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency, shardId }) => {
+ this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`, shardId);
+ const shard = this.shards.get(shardId);
+ shard.lastPingTimestamp = heartbeatAt;
+ shard.ping = latency;
+ });
+
+ this._ws.on(WSWebSocketShardEvents.Error, ({ error, shardId }) => {
+ /**
+ * Emitted whenever a shard's WebSocket encounters a connection error.
+ * @event Client#shardError
+ * @param {Error} error The encountered error
+ * @param {number} shardId The shard that encountered this error
+ */
+ this.client.emit(Events.ShardError, error, shardId);
+ });
+ }
+
+ /**
+ * Broadcasts a packet to every shard this manager handles.
+ * @param {Object} packet The packet to send
+ * @private
+ */
+ broadcast(packet) {
+ for (const shardId of this.shards.keys()) this._ws.send(shardId, packet);
+ }
+
+ /**
+ * Destroys this manager and all its shards.
+ * @private
+ */
+ async destroy() {
+ if (this.destroyed) return;
+ // TODO: Make a util for getting a stack
+ this.debug(`Manager was destroyed. Called by:\n${new Error().stack}`);
+ this.destroyed = true;
+ await this._ws?.destroy({ code: CloseCodes.Normal });
+ }
+
+ /**
+ * Processes a packet and queues it if this WebSocketManager is not ready.
+ * @param {Object} [packet] The packet to be handled
+ * @param {WebSocketShard} [shard] The shard that will handle this packet
+ * @returns {boolean}
+ * @private
+ */
+ handlePacket(packet, shard) {
+ if (packet && this.status !== Status.Ready) {
+ if (!BeforeReadyWhitelist.includes(packet.t)) {
+ this.packetQueue.push({ packet, shard });
+ return false;
+ }
+ }
+
+ if (this.packetQueue.length) {
+ const item = this.packetQueue.shift();
+ setImmediate(() => {
+ this.handlePacket(item.packet, item.shard);
+ }).unref();
+ }
+
+ if (packet && PacketHandlers[packet.t]) {
+ PacketHandlers[packet.t](this.client, packet, shard);
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks whether the client is ready to be marked as ready.
+ * @private
+ */
+ checkShardsReady() {
+ if (this.status === Status.Ready) return;
+ if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.Ready)) {
+ return;
+ }
+
+ this.triggerClientReady();
+ }
+
+ /**
+ * Causes the client to be marked as ready and emits the ready event.
+ * @private
+ */
+ triggerClientReady() {
+ this.status = Status.Ready;
+
+ this.client.readyTimestamp = Date.now();
+
+ /**
+ * Emitted when the client becomes ready to start working.
+ * @event Client#ready
+ * @param {Client} client The client
+ */
+ this.client.emit(Events.ClientReady, this.client);
+
+ this.handlePacket();
+ }
+}
+
+module.exports = WebSocketManager;