Initial commit

This commit is contained in:
March 7th
2022-03-19 17:37:45 +07:00
commit ac49705f3e
282 changed files with 39756 additions and 0 deletions

View File

@@ -0,0 +1,394 @@
'use strict';
const EventEmitter = require('node:events');
const { setImmediate } = require('node:timers');
const { setTimeout: sleep } = require('node:timers/promises');
const { Collection } = require('@discordjs/collection');
const { GatewayCloseCodes, GatewayDispatchEvents, Routes } = require('discord-api-types/v9');
const WebSocketShard = require('./WebSocketShard');
const PacketHandlers = require('./handlers');
const { Error } = require('../../errors');
const Events = require('../../util/Events');
const ShardEvents = require('../../util/ShardEvents');
const Status = require('../../util/Status');
const BeforeReadyWhitelist = [
GatewayDispatchEvents.Ready,
GatewayDispatchEvents.Resumed,
GatewayDispatchEvents.GuildCreate,
GatewayDispatchEvents.GuildDelete,
GatewayDispatchEvents.GuildMembersChunk,
GatewayDispatchEvents.GuildMemberAdd,
GatewayDispatchEvents.GuildMemberRemove,
];
const UNRECOVERABLE_CLOSE_CODES = [
GatewayCloseCodes.AuthenticationFailed,
GatewayCloseCodes.InvalidShard,
GatewayCloseCodes.ShardingRequired,
GatewayCloseCodes.InvalidIntents,
GatewayCloseCodes.DisallowedIntents,
];
const UNRESUMABLE_CLOSE_CODES = [1000, GatewayCloseCodes.AlreadyAuthenticated, GatewayCloseCodes.InvalidSeq];
/**
* 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;
/**
* The amount of shards this manager handles
* @private
* @type {number}
*/
this.totalShards = this.client.options.shards.length;
/**
* A collection of all shards this manager handles
* @type {Collection<number, WebSocketShard>}
*/
this.shards = new Collection();
/**
* An array of shards to be connected or that need to reconnect
* @type {Set<WebSocketShard>}
* @private
* @name WebSocketManager#shardQueue
*/
Object.defineProperty(this, 'shardQueue', { value: new Set(), writable: true });
/**
* 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;
/**
* If this manager is currently reconnecting one or multiple shards
* @type {boolean}
* @private
*/
this.reconnecting = false;
}
/**
* 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 {?WebSocketShard} [shard] The shard that emitted this message, if any
* @private
*/
debug(message, shard) {
this.client.emit(Events.Debug, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`);
}
/**
* Connects this manager to the gateway.
* @private
*/
async connect() {
const invalidToken = new Error(GatewayCloseCodes[GatewayCloseCodes.AuthenticationFailed]);
const {
url: gatewayURL,
shards: recommendedShards,
session_start_limit: sessionStartLimit,
} = await this.client.api.gateway.bot.get().catch(error => {
throw error.httpStatus === 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}/`;
let { shards } = this.client.options;
if (shards === 'auto') {
this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`);
this.totalShards = this.client.options.shardCount = recommendedShards;
shards = this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i);
}
this.totalShards = shards.length;
this.debug(`Spawning shards: ${shards.join(', ')}`);
this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id)));
return this.createShards();
}
/**
* Handles the creation of a shard.
* @returns {Promise<boolean>}
* @private
*/
async createShards() {
// If we don't have any shards to handle, return
if (!this.shardQueue.size) return false;
const [shard] = this.shardQueue;
this.shardQueue.delete(shard);
if (!shard.eventsAttached) {
shard.on(ShardEvents.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);
if (!this.shardQueue.size) this.reconnecting = false;
this.checkShardsReady();
});
shard.on(ShardEvents.Close, event => {
if (event.code === 1_000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) {
/**
* 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, event, shard.id);
this.debug(GatewayCloseCodes[event.code], shard);
return;
}
if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) {
// These event codes cannot be resumed
shard.sessionId = null;
}
/**
* 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, shard.id);
this.shardQueue.add(shard);
if (shard.sessionId) {
this.debug(`Session id is present, attempting an immediate reconnect...`, shard);
this.reconnect();
} else {
shard.destroy({ reset: true, emit: false, log: false });
this.reconnect();
}
});
shard.on(ShardEvents.InvalidSession, () => {
this.client.emit(Events.ShardReconnecting, shard.id);
});
shard.on(ShardEvents.Destroyed, () => {
this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard);
this.client.emit(Events.ShardReconnecting, shard.id);
this.shardQueue.add(shard);
this.reconnect();
});
shard.eventsAttached = true;
}
this.shards.set(shard.id, shard);
try {
await shard.connect();
} catch (error) {
if (error?.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) {
throw new Error(GatewayCloseCodes[error.code]);
// Undefined if session is invalid, error event for regular closes
} else if (!error || error.code) {
this.debug('Failed to connect to the gateway, requeueing...', shard);
this.shardQueue.add(shard);
} else {
throw error;
}
}
// If we have more shards, add a 5s delay
if (this.shardQueue.size) {
this.debug(`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`);
await sleep(5_000);
return this.createShards();
}
return true;
}
/**
* Handles reconnects for this manager.
* @private
* @returns {Promise<boolean>}
*/
async reconnect() {
if (this.reconnecting || this.status !== Status.Ready) return false;
this.reconnecting = true;
try {
await this.createShards();
} catch (error) {
this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`);
if (error.httpStatus !== 401) {
this.debug(`Possible network error occurred. Retrying in 5s...`);
await sleep(5_000);
this.reconnecting = false;
return this.reconnect();
}
// If we get an error at this point, it means we cannot reconnect anymore
if (this.client.listenerCount(Events.Invalidated)) {
/**
* Emitted when the client's session becomes invalidated.
* You are expected to handle closing the process gracefully and preventing a boot loop
* if you are listening to this event.
* @event Client#invalidated
*/
this.client.emit(Events.Invalidated);
// Destroy just the shards. This means you have to handle the cleanup yourself
this.destroy();
} else {
this.client.destroy();
}
} finally {
this.reconnecting = false;
}
return true;
}
/**
* Broadcasts a packet to every shard this manager handles.
* @param {Object} packet The packet to send
* @private
*/
broadcast(packet) {
for (const shard of this.shards.values()) shard.send(packet);
}
/**
* Destroys this manager and all its shards.
* @private
*/
destroy() {
if (this.destroyed) return;
this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`);
this.destroyed = true;
this.shardQueue.clear();
for (const shard of this.shards.values()) shard.destroy({ closeCode: 1_000, reset: true, emit: false, log: false });
}
/**
* 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;

View File

@@ -0,0 +1,787 @@
'use strict';
const EventEmitter = require('node:events');
const { setTimeout, setInterval, clearTimeout, clearInterval } = require('node:timers');
const { GatewayDispatchEvents, GatewayIntentBits, GatewayOpcodes } = require('discord-api-types/v9');
const WebSocket = require('../../WebSocket');
const Events = require('../../util/Events');
const IntentsBitField = require('../../util/IntentsBitField');
const ShardEvents = require('../../util/ShardEvents');
const Status = require('../../util/Status');
const STATUS_KEYS = Object.keys(Status);
const CONNECTION_STATE = Object.keys(WebSocket.WebSocket);
let zlib;
try {
zlib = require('zlib-sync');
} catch {} // eslint-disable-line no-empty
/**
* Represents a Shard's WebSocket connection
*/
class WebSocketShard extends EventEmitter {
constructor(manager, id) {
super();
/**
* The WebSocketManager of the shard
* @type {WebSocketManager}
*/
this.manager = manager;
/**
* The shard's id
* @type {number}
*/
this.id = id;
/**
* The current status of the shard
* @type {Status}
*/
this.status = Status.Idle;
/**
* The current sequence of the shard
* @type {number}
* @private
*/
this.sequence = -1;
/**
* The sequence of the shard after close
* @type {number}
* @private
*/
this.closeSequence = 0;
/**
* The current session id of the shard
* @type {?string}
* @private
*/
this.sessionId = null;
/**
* The previous heartbeat ping of the shard
* @type {number}
*/
this.ping = -1;
/**
* The last time a ping was sent (a timestamp)
* @type {number}
* @private
*/
this.lastPingTimestamp = -1;
/**
* If we received a heartbeat ack back. Used to identify zombie connections
* @type {boolean}
* @private
*/
this.lastHeartbeatAcked = true;
/**
* Contains the rate limit queue and metadata
* @name WebSocketShard#ratelimit
* @type {Object}
* @private
*/
Object.defineProperty(this, 'ratelimit', {
value: {
queue: [],
total: 120,
remaining: 120,
time: 60e3,
timer: null,
},
});
/**
* The WebSocket connection for the current shard
* @name WebSocketShard#connection
* @type {?WebSocket}
* @private
*/
Object.defineProperty(this, 'connection', { value: null, writable: true });
/**
* @external Inflate
* @see {@link https://www.npmjs.com/package/zlib-sync}
*/
/**
* The compression to use
* @name WebSocketShard#inflate
* @type {?Inflate}
* @private
*/
Object.defineProperty(this, 'inflate', { value: null, writable: true });
/**
* The HELLO timeout
* @name WebSocketShard#helloTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'helloTimeout', { value: null, writable: true });
/**
* If the manager attached its event handlers on the shard
* @name WebSocketShard#eventsAttached
* @type {boolean}
* @private
*/
Object.defineProperty(this, 'eventsAttached', { value: false, writable: true });
/**
* A set of guild ids this shard expects to receive
* @name WebSocketShard#expectedGuilds
* @type {?Set<string>}
* @private
*/
Object.defineProperty(this, 'expectedGuilds', { value: null, writable: true });
/**
* The ready timeout
* @name WebSocketShard#readyTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'readyTimeout', { value: null, writable: true });
/**
* Time when the WebSocket connection was opened
* @name WebSocketShard#connectedAt
* @type {number}
* @private
*/
Object.defineProperty(this, 'connectedAt', { value: 0, writable: true });
}
/**
* Emits a debug event.
* @param {string} message The debug message
* @private
*/
debug(message) {
this.manager.debug(message, this);
}
/**
* Connects the shard to the gateway.
* @private
* @returns {Promise<void>} A promise that will resolve if the shard turns ready successfully,
* or reject if we couldn't connect
*/
connect() {
const { gateway, client } = this.manager;
if (this.connection?.readyState === WebSocket.OPEN && this.status === Status.Ready) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const cleanup = () => {
this.removeListener(ShardEvents.Close, onClose);
this.removeListener(ShardEvents.Ready, onReady);
this.removeListener(ShardEvents.Resumed, onResumed);
this.removeListener(ShardEvents.InvalidSession, onInvalidOrDestroyed);
this.removeListener(ShardEvents.Destroyed, onInvalidOrDestroyed);
};
const onReady = () => {
cleanup();
resolve();
};
const onResumed = () => {
cleanup();
resolve();
};
const onClose = event => {
cleanup();
reject(event);
};
const onInvalidOrDestroyed = () => {
cleanup();
// eslint-disable-next-line prefer-promise-reject-errors
reject();
};
this.once(ShardEvents.Ready, onReady);
this.once(ShardEvents.Resumed, onResumed);
this.once(ShardEvents.Close, onClose);
this.once(ShardEvents.InvalidSession, onInvalidOrDestroyed);
this.once(ShardEvents.Destroyed, onInvalidOrDestroyed);
if (this.connection?.readyState === WebSocket.OPEN) {
this.debug('An open connection was found, attempting an immediate identify.');
this.identify();
return;
}
if (this.connection) {
this.debug(`A connection object was found. Cleaning up before continuing.
State: ${CONNECTION_STATE[this.connection.readyState]}`);
this.destroy({ emit: false });
}
const wsQuery = { v: client.options.ws.version };
if (zlib) {
this.inflate = new zlib.Inflate({
chunkSize: 65535,
flush: zlib.Z_SYNC_FLUSH,
to: WebSocket.encoding === 'json' ? 'string' : '',
});
wsQuery.compress = 'zlib-stream';
}
this.debug(
`[CONNECT]
Gateway : ${gateway}
Version : ${client.options.ws.version}
Encoding : ${WebSocket.encoding}
Compression: ${zlib ? 'zlib-stream' : 'none'}`,
);
this.status = this.status === Status.Disconnected ? Status.Reconnecting : Status.Connecting;
this.setHelloTimeout();
this.connectedAt = Date.now();
const ws = (this.connection = WebSocket.create(gateway, wsQuery));
ws.onopen = this.onOpen.bind(this);
ws.onmessage = this.onMessage.bind(this);
ws.onerror = this.onError.bind(this);
ws.onclose = this.onClose.bind(this);
});
}
/**
* Called whenever a connection is opened to the gateway.
* @private
*/
onOpen() {
this.debug(`[CONNECTED] Took ${Date.now() - this.connectedAt}ms`);
this.status = Status.Nearly;
}
/**
* Called whenever a message is received.
* @param {MessageEvent} event Event received
* @private
*/
onMessage({ data }) {
let raw;
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
if (zlib) {
const l = data.length;
const flush =
l >= 4 && data[l - 4] === 0x00 && data[l - 3] === 0x00 && data[l - 2] === 0xff && data[l - 1] === 0xff;
this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH);
if (!flush) return;
raw = this.inflate.result;
} else {
raw = data;
}
let packet;
try {
packet = WebSocket.unpack(raw);
} catch (err) {
this.manager.client.emit(Events.ShardError, err, this.id);
return;
}
this.manager.client.emit(Events.Raw, packet, this.id);
if (packet.op === GatewayOpcodes.Dispatch) this.manager.emit(packet.t, packet.d, this.id);
this.onPacket(packet);
}
/**
* Called whenever an error occurs with the WebSocket.
* @param {ErrorEvent} event The error that occurred
* @private
*/
onError(event) {
const error = event?.error ?? event;
if (!error) return;
/**
* 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.manager.client.emit(Events.ShardError, error, this.id);
}
/**
* @external CloseEvent
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
*/
/**
* @external ErrorEvent
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent}
*/
/**
* @external MessageEvent
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent}
*/
/**
* Called whenever a connection to the gateway is closed.
* @param {CloseEvent} event Close event that was received
* @private
*/
onClose(event) {
if (this.sequence !== -1) this.closeSequence = this.sequence;
this.sequence = -1;
this.debug(`[CLOSE]
Event Code: ${event.code}
Clean : ${event.wasClean}
Reason : ${event.reason ?? 'No reason received'}`);
this.setHeartbeatTimer(-1);
this.setHelloTimeout(-1);
// If we still have a connection object, clean up its listeners
if (this.connection) this._cleanupConnection();
this.status = Status.Disconnected;
/**
* Emitted when a shard's WebSocket closes.
* @private
* @event WebSocketShard#close
* @param {CloseEvent} event The received event
*/
this.emit(ShardEvents.Close, event);
}
/**
* Called whenever a packet is received.
* @param {Object} packet The received packet
* @private
*/
onPacket(packet) {
if (!packet) {
this.debug(`Received broken packet: '${packet}'.`);
return;
}
switch (packet.t) {
case GatewayDispatchEvents.Ready:
/**
* Emitted when the shard receives the READY payload and is now waiting for guilds
* @event WebSocketShard#ready
*/
this.emit(ShardEvents.Ready);
this.sessionId = packet.d.session_id;
this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id));
this.status = Status.WaitingForGuilds;
this.debug(`[READY] Session ${this.sessionId}.`);
this.lastHeartbeatAcked = true;
this.sendHeartbeat('ReadyHeartbeat');
break;
case GatewayDispatchEvents.Resumed: {
/**
* Emitted when the shard resumes successfully
* @event WebSocketShard#resumed
*/
this.emit(ShardEvents.Resumed);
this.status = Status.Ready;
const replayed = packet.s - this.closeSequence;
this.debug(`[RESUMED] Session ${this.sessionId} | Replayed ${replayed} events.`);
this.lastHeartbeatAcked = true;
this.sendHeartbeat('ResumeHeartbeat');
break;
}
}
if (packet.s > this.sequence) this.sequence = packet.s;
switch (packet.op) {
case GatewayOpcodes.Hello:
this.setHelloTimeout(-1);
this.setHeartbeatTimer(packet.d.heartbeat_interval);
this.identify();
break;
case GatewayOpcodes.Reconnect:
this.debug('[RECONNECT] Discord asked us to reconnect');
this.destroy({ closeCode: 4_000 });
break;
case GatewayOpcodes.InvalidSession:
this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`);
// If we can resume the session, do so immediately
if (packet.d) {
this.identifyResume();
return;
}
// Reset the sequence
this.sequence = -1;
// Reset the session id as it's invalid
this.sessionId = null;
// Set the status to reconnecting
this.status = Status.Reconnecting;
// Finally, emit the INVALID_SESSION event
this.emit(ShardEvents.InvalidSession);
break;
case GatewayOpcodes.HeartbeatAck:
this.ackHeartbeat();
break;
case GatewayOpcodes.Heartbeat:
this.sendHeartbeat('HeartbeatRequest', true);
break;
default:
this.manager.handlePacket(packet, this);
if (this.status === Status.WaitingForGuilds && packet.t === GatewayDispatchEvents.GuildCreate) {
this.expectedGuilds.delete(packet.d.id);
this.checkReady();
}
}
}
/**
* Checks if the shard can be marked as ready
* @private
*/
checkReady() {
// Step 0. Clear the ready timeout, if it exists
if (this.readyTimeout) {
clearTimeout(this.readyTimeout);
this.readyTimeout = null;
}
// Step 1. If we don't have any other guilds pending, we are ready
if (!this.expectedGuilds.size) {
this.debug('Shard received all its guilds. Marking as fully ready.');
this.status = Status.Ready;
/**
* Emitted when the shard is fully ready.
* This event is emitted if:
* * all guilds were received by this shard
* * the ready timeout expired, and some guilds are unavailable
* @event WebSocketShard#allReady
* @param {?Set<string>} unavailableGuilds Set of unavailable guilds, if any
*/
this.emit(ShardEvents.AllReady);
return;
}
const hasGuildsIntent = new IntentsBitField(this.manager.client.options.intents).has(GatewayIntentBits.Guilds);
// Step 2. Create a timeout that will mark the shard as ready if there are still unavailable guilds
// * The timeout is 15 seconds by default
// * This can be optionally changed in the client options via the `waitGuildTimeout` option
// * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds.
const { waitGuildTimeout } = this.manager.client.options;
this.readyTimeout = setTimeout(
() => {
this.debug(
`Shard ${hasGuildsIntent ? 'did' : 'will'} not receive any more guild packets` +
`${hasGuildsIntent ? ` in ${waitGuildTimeout} ms` : ''}.\nUnavailable guild count: ${
this.expectedGuilds.size
}`,
);
this.readyTimeout = null;
this.status = Status.Ready;
this.emit(ShardEvents.AllReady, this.expectedGuilds);
},
0,
).unref();
}
/**
* Sets the HELLO packet timeout.
* @param {number} [time] If set to -1, it will clear the hello timeout
* @private
*/
setHelloTimeout(time) {
if (time === -1) {
if (this.helloTimeout) {
this.debug('Clearing the HELLO timeout.');
clearTimeout(this.helloTimeout);
this.helloTimeout = null;
}
return;
}
this.debug('Setting a HELLO timeout for 20s.');
this.helloTimeout = setTimeout(() => {
this.debug('Did not receive HELLO in time. Destroying and connecting again.');
this.destroy({ reset: true, closeCode: 4009 });
}, 20_000).unref();
}
/**
* Sets the heartbeat timer for this shard.
* @param {number} time If -1, clears the interval, any other number sets an interval
* @private
*/
setHeartbeatTimer(time) {
if (time === -1) {
if (this.heartbeatInterval) {
this.debug('Clearing the heartbeat interval.');
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
return;
}
this.debug(`Setting a heartbeat interval for ${time}ms.`);
// Sanity checks
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), time).unref();
}
/**
* Sends a heartbeat to the WebSocket.
* If this shard didn't receive a heartbeat last time, it will destroy it and reconnect
* @param {string} [tag='HeartbeatTimer'] What caused this heartbeat to be sent
* @param {boolean} [ignoreHeartbeatAck] If we should send the heartbeat forcefully.
* @private
*/
sendHeartbeat(
tag = 'HeartbeatTimer',
ignoreHeartbeatAck = [Status.WaitingForGuilds, Status.Identifying, Status.Resuming].includes(this.status),
) {
if (ignoreHeartbeatAck && !this.lastHeartbeatAcked) {
this.debug(`[${tag}] Didn't process heartbeat ack yet but we are still connected. Sending one now.`);
} else if (!this.lastHeartbeatAcked) {
this.debug(
`[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting.
Status : ${STATUS_KEYS[this.status]}
Sequence : ${this.sequence}
Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}`,
);
this.destroy({ closeCode: 4009, reset: true });
return;
}
this.debug(`[${tag}] Sending a heartbeat.`);
this.lastHeartbeatAcked = false;
this.lastPingTimestamp = Date.now();
this.send({ op: GatewayOpcodes.Heartbeat, d: this.sequence }, true);
}
/**
* Acknowledges a heartbeat.
* @private
*/
ackHeartbeat() {
this.lastHeartbeatAcked = true;
const latency = Date.now() - this.lastPingTimestamp;
this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`);
this.ping = latency;
}
/**
* Identifies the client on the connection.
* @private
* @returns {void}
*/
identify() {
return this.sessionId ? this.identifyResume() : this.identifyNew();
}
/**
* Identifies as a new connection on the gateway.
* @private
*/
identifyNew() {
const { client } = this.manager;
if (!client.token) {
this.debug('[IDENTIFY] No token available to identify a new session.');
return;
}
this.status = Status.Identifying;
// Clone the identify payload and assign the token and shard info
const d = {
...client.options.ws,
token: client.token,
};
this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount} with intents: 32767`);
this.send({ op: GatewayOpcodes.Identify, d }, true);
}
/**
* Resumes a session on the gateway.
* @private
*/
identifyResume() {
if (!this.sessionId) {
this.debug('[RESUME] No session id was present; identifying as a new session.');
this.identifyNew();
return;
}
this.status = Status.Resuming;
this.debug(`[RESUME] Session ${this.sessionId}, sequence ${this.closeSequence}`);
const d = {
token: this.manager.client.token,
session_id: this.sessionId,
seq: this.closeSequence,
};
this.send({ op: GatewayOpcodes.Resume, d }, true);
}
/**
* Adds a packet to the queue to be sent to the gateway.
* <warn>If you use this method, make sure you understand that you need to provide
* a full [Payload](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-commands).
* Do not use this method if you don't know what you're doing.</warn>
* @param {Object} data The full packet to send
* @param {boolean} [important=false] If this packet should be added first in queue
*/
send(data, important = false) {
this.ratelimit.queue[important ? 'unshift' : 'push'](data);
this.processQueue();
}
/**
* Sends data, bypassing the queue.
* @param {Object} data Packet to send
* @returns {void}
* @private
*/
_send(data) {
if (this.connection?.readyState !== WebSocket.OPEN) {
this.debug(`Tried to send packet '${JSON.stringify(data)}' but no WebSocket is available!`);
this.destroy({ closeCode: 4_000 });
return;
}
this.connection.send(WebSocket.pack(data), err => {
if (err) this.manager.client.emit(Events.ShardError, err, this.id);
});
}
/**
* Processes the current WebSocket queue.
* @returns {void}
* @private
*/
processQueue() {
if (this.ratelimit.remaining === 0) return;
if (this.ratelimit.queue.length === 0) return;
if (this.ratelimit.remaining === this.ratelimit.total) {
this.ratelimit.timer = setTimeout(() => {
this.ratelimit.remaining = this.ratelimit.total;
this.processQueue();
}, this.ratelimit.time).unref();
}
while (this.ratelimit.remaining > 0) {
const item = this.ratelimit.queue.shift();
if (!item) return;
this._send(item);
this.ratelimit.remaining--;
}
}
/**
* Destroys this shard and closes its WebSocket connection.
* @param {Object} [options={ closeCode: 1000, reset: false, emit: true, log: true }] Options for destroying the shard
* @private
*/
destroy({ closeCode = 1_000, reset = false, emit = true, log = true } = {}) {
if (log) {
this.debug(`[DESTROY]
Close Code : ${closeCode}
Reset : ${reset}
Emit DESTROYED: ${emit}`);
}
// Step 0: Remove all timers
this.setHeartbeatTimer(-1);
this.setHelloTimeout(-1);
// Step 1: Close the WebSocket connection, if any, otherwise, emit DESTROYED
if (this.connection) {
// If the connection is currently opened, we will (hopefully) receive close
if (this.connection.readyState === WebSocket.OPEN) {
this.connection.close(closeCode);
} else {
// Connection is not OPEN
this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`);
// Remove listeners from the connection
this._cleanupConnection();
// Attempt to close the connection just in case
try {
this.connection.close(closeCode);
} catch {
// No-op
}
// Emit the destroyed event if needed
if (emit) this._emitDestroyed();
}
} else if (emit) {
// We requested a destroy, but we had no connection. Emit destroyed
this._emitDestroyed();
}
// Step 2: Null the connection object
this.connection = null;
// Step 3: Set the shard status to Disconnected
this.status = Status.Disconnected;
// Step 4: Cache the old sequence (use to attempt a resume)
if (this.sequence !== -1) this.closeSequence = this.sequence;
// Step 5: Reset the sequence and session id if requested
if (reset) {
this.sequence = -1;
this.sessionId = null;
}
// Step 6: reset the rate limit data
this.ratelimit.remaining = this.ratelimit.total;
this.ratelimit.queue.length = 0;
if (this.ratelimit.timer) {
clearTimeout(this.ratelimit.timer);
this.ratelimit.timer = null;
}
}
/**
* Cleans up the WebSocket connection listeners.
* @private
*/
_cleanupConnection() {
this.connection.onopen = this.connection.onclose = this.connection.onerror = this.connection.onmessage = null;
}
/**
* Emits the DESTROYED event on the shard
* @private
*/
_emitDestroyed() {
/**
* Emitted when a shard is destroyed, but no WebSocket connection was present.
* @private
* @event WebSocketShard#destroyed
*/
this.emit(ShardEvents.Destroyed);
}
}
module.exports = WebSocketShard;

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ChannelCreate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ChannelDelete.handle(packet.d);
};

View File

@@ -0,0 +1,22 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, { d: data }) => {
const channel = client.channels.cache.get(data.channel_id);
const time = data.last_pin_timestamp ? Date.parse(data.last_pin_timestamp) : null;
if (channel) {
// Discord sends null for last_pin_timestamp if the last pinned message was removed
channel.lastPinTimestamp = time;
/**
* Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event,
* not much information can be provided easily here - you need to manually check the pins yourself.
* @event Client#channelPinsUpdate
* @param {TextBasedChannels} channel The channel that the pins update occurred in
* @param {Date} time The time of the pins update
*/
client.emit(Events.ChannelPinsUpdate, channel, time);
}
};

View File

@@ -0,0 +1,16 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, packet) => {
const { old, updated } = client.actions.ChannelUpdate.handle(packet.d);
if (old && updated) {
/**
* Emitted whenever a channel is updated - e.g. name change, topic change, channel type change.
* @event Client#channelUpdate
* @param {DMChannel|GuildChannel} oldChannel The channel before the update
* @param {DMChannel|GuildChannel} newChannel The channel after the update
*/
client.emit(Events.ChannelUpdate, old, updated);
}
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildBanAdd.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildBanRemove.handle(packet.d);
};

View File

@@ -0,0 +1,26 @@
'use strict';
const Events = require('../../../util/Events');
const Status = require('../../../util/Status');
module.exports = (client, { d: data }, shard) => {
let guild = client.guilds.cache.get(data.id);
if (guild) {
if (!guild.available && !data.unavailable) {
// A newly available guild
guild._patch(data);
}
} else {
// A new guild
data.shardId = shard.id;
guild = client.guilds._add(data);
if (client.ws.status === Status.Ready) {
/**
* Emitted whenever the client joins a guild.
* @event Client#guildCreate
* @param {Guild} guild The created guild
*/
client.emit(Events.GuildCreate, guild);
}
}
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildDelete.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildEmojisUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildIntegrationsUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,36 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const Events = require('../../../util/Events');
module.exports = (client, { d: data }) => {
const guild = client.guilds.cache.get(data.guild_id);
if (!guild) return;
const members = new Collection();
for (const member of data.members) members.set(member.user.id, guild.members._add(member));
if (data.presences) {
for (const presence of data.presences) guild.presences._add(Object.assign(presence, { guild }));
}
/**
* Represents the properties of a guild members chunk
* @typedef {Object} GuildMembersChunk
* @property {number} index Index of the received chunk
* @property {number} count Number of chunks the client should receive
* @property {?string} nonce Nonce for this chunk
*/
/**
* Emitted whenever a chunk of guild members is received (all members come from the same guild).
* @event Client#guildMembersChunk
* @param {Collection<Snowflake, GuildMember>} members The members in the chunk
* @param {Guild} guild The guild related to the member chunk
* @param {GuildMembersChunk} chunk Properties of the received chunk
*/
client.emit(Events.GuildMembersChunk, members, guild, {
count: data.chunk_count,
index: data.chunk_index,
nonce: data.nonce,
});
};

View File

@@ -0,0 +1,20 @@
'use strict';
const Events = require('../../../util/Events');
const Status = require('../../../util/Status');
module.exports = (client, { d: data }, shard) => {
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
guild.memberCount++;
const member = guild.members._add(data);
if (shard.status === Status.Ready) {
/**
* Emitted whenever a user joins a guild.
* @event Client#guildMemberAdd
* @param {GuildMember} member The member that has joined a guild
*/
client.emit(Events.GuildMemberAdd, member);
}
}
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet, shard) => {
client.actions.GuildMemberRemove.handle(packet.d, shard);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet, shard) => {
client.actions.GuildMemberUpdate.handle(packet.d, shard);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildRoleCreate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildRoleDelete.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildRoleUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildScheduledEventCreate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildScheduledEventDelete.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildScheduledEventUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildScheduledEventUserAdd.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildScheduledEventUserRemove.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildStickersUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.InteractionCreate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.InviteCreate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.InviteDelete.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageCreate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageDelete.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageDeleteBulk.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageReactionAdd.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageReactionRemove.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageReactionRemoveAll.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageReactionRemoveEmoji.handle(packet.d);
};

View File

@@ -0,0 +1,16 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, packet) => {
const { old, updated } = client.actions.MessageUpdate.handle(packet.d);
if (old && updated) {
/**
* Emitted whenever a message is updated - e.g. embed or content change.
* @event Client#messageUpdate
* @param {Message} oldMessage The message before the update
* @param {Message} newMessage The message after the update
*/
client.emit(Events.MessageUpdate, old, updated);
}
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.PresenceUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,41 @@
'use strict';
const ClientApplication = require('../../../structures/ClientApplication');
const User = require('../../../structures/User');
let ClientUser;
module.exports = (client, { d: data }, shard) => {
//console.log(data);
client.session_id = data.session_id;
if (client.user) {
client.user._patch(data.user);
} else {
ClientUser ??= require('../../../structures/ClientUser');
client.user = new ClientUser(client, data.user);
client.users.cache.set(client.user.id, client.user);
}
client.user.setAFK(true);
for (const guild of data.guilds) {
guild.shardId = shard.id;
client.guilds._add(guild);
}
for (const r of data.relationships) {
if(r.type == 1) {
client.friends.cache.set(r.id, new User(client, r.user));
} else if(r.type == 2) {
client.blocked.cache.set(r.id, new User(client, r.user));
}
}
if (client.application) {
client.application._patch(data.application);
} else {
client.application = new ClientApplication(client, data.application);
}
shard.checkReady();
};

View File

@@ -0,0 +1,14 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, packet, shard) => {
const replayed = shard.sequence - shard.closeSequence;
/**
* Emitted when a shard resumes successfully.
* @event Client#shardResume
* @param {number} id The shard id that resumed
* @param {number} replayedEvents The amount of replayed events
*/
client.emit(Events.ShardResume, shard.id, replayed);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.StageInstanceCreate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.StageInstanceDelete.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.StageInstanceUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ThreadCreate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ThreadDelete.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ThreadListSync.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ThreadMembersUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ThreadMemberUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,16 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, packet) => {
const { old, updated } = client.actions.ChannelUpdate.handle(packet.d);
if (old && updated) {
/**
* Emitted whenever a thread is updated - e.g. name change, archive state change, locked state change.
* @event Client#threadUpdate
* @param {ThreadChannel} oldThread The thread before the update
* @param {ThreadChannel} newThread The thread after the update
*/
client.emit(Events.ThreadUpdate, old, updated);
}
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.TypingStart.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.UserUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,6 @@
'use strict';
module.exports = (client, packet) => {
client.emit('debug', `[VOICE] received voice server: ${JSON.stringify(packet)}`);
client.voice.onVoiceServer(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.VoiceStateUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.WebhooksUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,58 @@
'use strict';
const handlers = Object.fromEntries([
['READY', require('./READY')],
['RESUMED', require('./RESUMED')],
['GUILD_CREATE', require('./GUILD_CREATE')],
['GUILD_DELETE', require('./GUILD_DELETE')],
['GUILD_UPDATE', require('./GUILD_UPDATE')],
['INVITE_CREATE', require('./INVITE_CREATE')],
['INVITE_DELETE', require('./INVITE_DELETE')],
['GUILD_MEMBER_ADD', require('./GUILD_MEMBER_ADD')],
['GUILD_MEMBER_REMOVE', require('./GUILD_MEMBER_REMOVE')],
['GUILD_MEMBER_UPDATE', require('./GUILD_MEMBER_UPDATE')],
['GUILD_MEMBERS_CHUNK', require('./GUILD_MEMBERS_CHUNK')],
['GUILD_INTEGRATIONS_UPDATE', require('./GUILD_INTEGRATIONS_UPDATE')],
['GUILD_ROLE_CREATE', require('./GUILD_ROLE_CREATE')],
['GUILD_ROLE_DELETE', require('./GUILD_ROLE_DELETE')],
['GUILD_ROLE_UPDATE', require('./GUILD_ROLE_UPDATE')],
['GUILD_BAN_ADD', require('./GUILD_BAN_ADD')],
['GUILD_BAN_REMOVE', require('./GUILD_BAN_REMOVE')],
['GUILD_EMOJIS_UPDATE', require('./GUILD_EMOJIS_UPDATE')],
['CHANNEL_CREATE', require('./CHANNEL_CREATE')],
['CHANNEL_DELETE', require('./CHANNEL_DELETE')],
['CHANNEL_UPDATE', require('./CHANNEL_UPDATE')],
['CHANNEL_PINS_UPDATE', require('./CHANNEL_PINS_UPDATE')],
['MESSAGE_CREATE', require('./MESSAGE_CREATE')],
['MESSAGE_DELETE', require('./MESSAGE_DELETE')],
['MESSAGE_UPDATE', require('./MESSAGE_UPDATE')],
['MESSAGE_DELETE_BULK', require('./MESSAGE_DELETE_BULK')],
['MESSAGE_REACTION_ADD', require('./MESSAGE_REACTION_ADD')],
['MESSAGE_REACTION_REMOVE', require('./MESSAGE_REACTION_REMOVE')],
['MESSAGE_REACTION_REMOVE_ALL', require('./MESSAGE_REACTION_REMOVE_ALL')],
['MESSAGE_REACTION_REMOVE_EMOJI', require('./MESSAGE_REACTION_REMOVE_EMOJI')],
['THREAD_CREATE', require('./THREAD_CREATE')],
['THREAD_UPDATE', require('./THREAD_UPDATE')],
['THREAD_DELETE', require('./THREAD_DELETE')],
['THREAD_LIST_SYNC', require('./THREAD_LIST_SYNC')],
['THREAD_MEMBER_UPDATE', require('./THREAD_MEMBER_UPDATE')],
['THREAD_MEMBERS_UPDATE', require('./THREAD_MEMBERS_UPDATE')],
['USER_UPDATE', require('./USER_UPDATE')],
['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')],
['TYPING_START', require('./TYPING_START')],
['VOICE_STATE_UPDATE', require('./VOICE_STATE_UPDATE')],
['VOICE_SERVER_UPDATE', require('./VOICE_SERVER_UPDATE')],
['WEBHOOKS_UPDATE', require('./WEBHOOKS_UPDATE')],
['INTERACTION_CREATE', require('./INTERACTION_CREATE')],
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')],
['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')],
['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')],
['GUILD_STICKERS_UPDATE', require('./GUILD_STICKERS_UPDATE')],
['GUILD_SCHEDULED_EVENT_CREATE', require('./GUILD_SCHEDULED_EVENT_CREATE')],
['GUILD_SCHEDULED_EVENT_UPDATE', require('./GUILD_SCHEDULED_EVENT_UPDATE')],
['GUILD_SCHEDULED_EVENT_DELETE', require('./GUILD_SCHEDULED_EVENT_DELETE')],
['GUILD_SCHEDULED_EVENT_USER_ADD', require('./GUILD_SCHEDULED_EVENT_USER_ADD')],
['GUILD_SCHEDULED_EVENT_USER_REMOVE', require('./GUILD_SCHEDULED_EVENT_USER_REMOVE')],
]);
module.exports = handlers;