diff --git a/package.json b/package.json index 4f27bf482..c3b4b40c4 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "node-fetch": "^2.1.2", "pako": "^1.0.0", "prism-media": "amishshah/prism-media", + "setimmediate": "^1.0.5", "tweetnacl": "^1.0.0", "ws": "^6.0.0" }, diff --git a/src/client/BaseClient.js b/src/client/BaseClient.js index 6e671f99e..c9fce5908 100644 --- a/src/client/BaseClient.js +++ b/src/client/BaseClient.js @@ -1,3 +1,4 @@ +require('setimmediate'); const EventEmitter = require('events'); const RESTManager = require('../rest/RESTManager'); const Util = require('../util/Util'); @@ -25,6 +26,13 @@ class BaseClient extends EventEmitter { */ this._intervals = new Set(); + /** + * Intervals set by {@link BaseClient#setImmediate} that are still active + * @type {Set} + * @private + */ + this._immediates = new Set(); + /** * The options the client was instantiated with * @type {ClientOptions} @@ -53,10 +61,12 @@ class BaseClient extends EventEmitter { * Destroys all assets used by the base client. */ destroy() { - for (const t of this._timeouts) clearTimeout(t); - for (const i of this._intervals) clearInterval(i); + for (const t of this._timeouts) this.clearTimeout(t); + for (const i of this._intervals) this.clearInterval(i); + for (const i of this._immediates) this.clearImmediate(i); this._timeouts.clear(); this._intervals.clear(); + this._immediates.clear(); } /** @@ -106,6 +116,27 @@ class BaseClient extends EventEmitter { this._intervals.delete(interval); } + /** + * Sets an immediate that will be automatically cancelled if the client is destroyed. + * @param {Function} fn Function to execute + * @param {...*} args Arguments for the function + * @returns {Immediate} + */ + setImmediate(fn, ...args) { + const immediate = setImmediate(fn, ...args); + this._immediates.add(immediate); + return immediate; + } + + /** + * Clears an immediate. + * @param {Immediate} immediate Immediate to cancel + */ + clearImmediate(immediate) { + clearImmediate(immediate); + this._immediates.delete(immediate); + } + toJSON(...props) { return Util.flatten(this, { domain: false }, ...props); } diff --git a/src/client/Client.js b/src/client/Client.js index 4b3160dfe..339e55646 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -1,6 +1,5 @@ const BaseClient = require('./BaseClient'); const Permissions = require('../util/Permissions'); -const ClientManager = require('./ClientManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); const WebSocketManager = require('./websocket/WebSocketManager'); const ActionsManager = require('./actions/ActionsManager'); @@ -15,7 +14,8 @@ const UserStore = require('../stores/UserStore'); const ChannelStore = require('../stores/ChannelStore'); const GuildStore = require('../stores/GuildStore'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); -const { Events, browser } = require('../util/Constants'); +const { Events, WSCodes, browser, DefaultOptions } = require('../util/Constants'); +const { delayFor } = require('../util/Util'); const DataResolver = require('../util/DataResolver'); const Structures = require('../util/Structures'); const { Error, TypeError, RangeError } = require('../errors'); @@ -31,45 +31,34 @@ class Client extends BaseClient { constructor(options = {}) { super(Object.assign({ _tokenType: 'Bot' }, options)); - // Figure out the shard details - if (!browser && process.env.SHARDING_MANAGER) { - // Try loading workerData if it's present - let workerData; - try { - workerData = require('worker_threads').workerData; - } catch (err) { - // Do nothing + // Obtain shard details from environment or if present, worker threads + let data = process.env; + try { + // Test if worker threads module is present and used + data = require('worker_threads').workerData || data; + } catch (_) { + // Do nothing + } + if (this.options.shards === DefaultOptions.shards) { + if ('SHARDS' in data) { + this.options.shards = JSON.parse(data.SHARDS); } - - if (!this.options.shardId) { - if (workerData && 'SHARD_ID' in workerData) { - this.options.shardId = workerData.SHARD_ID; - } else if ('SHARD_ID' in process.env) { - this.options.shardId = Number(process.env.SHARD_ID); - } - } - if (!this.options.shardCount) { - if (workerData && 'SHARD_COUNT' in workerData) { - this.options.shardCount = workerData.SHARD_COUNT; - } else if ('SHARD_COUNT' in process.env) { - this.options.shardCount = Number(process.env.SHARD_COUNT); - } + } + if (this.options.totalShardCount === DefaultOptions.totalShardCount) { + if ('TOTAL_SHARD_COUNT' in data) { + this.options.totalShardCount = Number(data.TOTAL_SHARD_COUNT); + } else if (Array.isArray(this.options.shards)) { + this.options.totalShardCount = this.options.shards.length; + } else { + this.options.totalShardCount = this.options.shardCount; } } this._validateOptions(); - /** - * The manager of the client - * @type {ClientManager} - * @private - */ - this.manager = new ClientManager(this); - /** * The WebSocket manager of the client * @type {WebSocketManager} - * @private */ this.ws = new WebSocketManager(this); @@ -155,54 +144,11 @@ class Client extends BaseClient { */ this.broadcasts = []; - /** - * Previous heartbeat pings of the websocket (most recent first, limited to three elements) - * @type {number[]} - */ - this.pings = []; - if (this.options.messageSweepInterval > 0) { this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000); } } - /** - * Timestamp of the latest ping's start time - * @type {number} - * @readonly - * @private - */ - get _pingTimestamp() { - return this.ws.connection ? this.ws.connection.lastPingTimestamp : 0; - } - - /** - * Current status of the client's connection to Discord - * @type {?Status} - * @readonly - */ - get status() { - return this.ws.connection ? this.ws.connection.status : null; - } - - /** - * How long it has been since the client last entered the `READY` state in milliseconds - * @type {?number} - * @readonly - */ - get uptime() { - return this.readyAt ? Date.now() - this.readyAt : null; - } - - /** - * Average heartbeat ping of the websocket, obtained by averaging the {@link Client#pings} property - * @type {number} - * @readonly - */ - get ping() { - return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length; - } - /** * All active voice connections that have been established, mapped by guild ID * @type {Collection} @@ -235,6 +181,15 @@ class Client extends BaseClient { return this.readyAt ? this.readyAt.getTime() : null; } + /** + * How long it has been since the client last entered the `READY` state in milliseconds + * @type {?number} + * @readonly + */ + get uptime() { + return this.readyAt ? Date.now() - this.readyAt : null; + } + /** * Creates a voice broadcast. * @returns {VoiceBroadcast} @@ -252,15 +207,54 @@ class Client extends BaseClient { * @example * client.login('my token'); */ - login(token = this.token) { - return new Promise((resolve, reject) => { - if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); - token = token.replace(/^Bot\s*/i, ''); - this.manager.connectToWebSocket(token, resolve, reject); - }).catch(e => { - this.destroy(); - return Promise.reject(e); + async login(token = this.token) { + if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); + this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); + this.emit(Events.DEBUG, `Authenticating using token ${token}`); + let endpoint = this.api.gateway; + if (this.options.shardCount === 'auto') endpoint = endpoint.bot; + const res = await endpoint.get(); + if (this.options.presence) { + this.options.ws.presence = await this.presence._parse(this.options.presence); + } + if (res.session_start_limit && res.session_start_limit.remaining === 0) { + const { session_start_limit: { reset_after } } = res; + this.emit(Events.DEBUG, `Exceeded identify threshold, setting a timeout for ${reset_after} ms`); + await delayFor(reset_after); + } + const gateway = `${res.url}/`; + if (this.options.shardCount === 'auto') { + this.emit(Events.DEBUG, `Using recommended shard count ${res.shards}`); + this.options.shardCount = res.shards; + this.options.totalShardCount = res.shards; + } + this.emit(Events.DEBUG, `Using gateway ${gateway}`); + this.ws.connect(gateway); + await new Promise((resolve, reject) => { + const onready = () => { + clearTimeout(timeout); + this.removeListener(Events.DISCONNECT, ondisconnect); + resolve(); + }; + const ondisconnect = event => { + clearTimeout(timeout); + this.removeListener(Events.READY, onready); + this.destroy(); + if (WSCodes[event.code]) { + reject(new Error(WSCodes[event.code])); + } + }; + const timeout = setTimeout(() => { + this.removeListener(Events.READY, onready); + this.removeListener(Events.DISCONNECT, ondisconnect); + this.destroy(); + reject(new Error('WS_CONNECTION_TIMEOUT')); + }, this.options.shardCount * 25e3); + if (timeout.unref !== undefined) timeout.unref(); + this.once(Events.READY, onready); + this.once(Events.DISCONNECT, ondisconnect); }); + return token; } /** @@ -269,7 +263,8 @@ class Client extends BaseClient { */ destroy() { super.destroy(); - return this.manager.destroy(); + this.ws.destroy(); + this.token = null; } /** @@ -386,22 +381,10 @@ class Client extends BaseClient { return super.toJSON({ readyAt: false, broadcasts: false, - pings: false, presences: false, }); } - /** - * Adds a ping to {@link Client#pings}. - * @param {number} startTime Starting time of the ping - * @private - */ - _pong(startTime) { - this.pings.unshift(Date.now() - startTime); - if (this.pings.length > 3) this.pings.length = 3; - this.ws.lastHeartbeatAck = true; - } - /** * Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script * with the client as `this`. @@ -419,17 +402,13 @@ class Client extends BaseClient { * @private */ _validateOptions(options = this.options) { // eslint-disable-line complexity - if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number'); + if (options.shardCount !== 'auto' && (typeof options.shardCount !== 'number' || isNaN(options.shardCount))) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number or "auto"'); } - if (typeof options.shardId !== 'number' || isNaN(options.shardId)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shardId', 'a number'); - } - if (options.shardCount < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 0'); - if (options.shardId < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', 'at least 0'); - if (options.shardId !== 0 && options.shardId >= options.shardCount) { - throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', 'less than shardCount'); + if (options.shards && typeof options.shards !== 'number' && !Array.isArray(options.shards)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shards', 'a number or array'); } + if (options.shardCount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 1'); if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number'); } @@ -451,9 +430,6 @@ class Client extends BaseClient { if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) { throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number'); } - if (typeof options.internalSharding !== 'boolean') { - throw new TypeError('CLIENT_INVALID_OPTION', 'internalSharding', 'a boolean'); - } if (!(options.disabledEvents instanceof Array)) { throw new TypeError('CLIENT_INVALID_OPTION', 'disabledEvents', 'an Array'); } diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js deleted file mode 100644 index e6d107822..000000000 --- a/src/client/ClientManager.js +++ /dev/null @@ -1,70 +0,0 @@ -const { Events, Status } = require('../util/Constants'); -const { Error } = require('../errors'); - -/** - * Manages the state and background tasks of the client. - * @private - */ -class ClientManager { - constructor(client) { - /** - * The client that instantiated this Manager - * @type {Client} - */ - this.client = client; - - /** - * The heartbeat interval - * @type {?number} - */ - this.heartbeatInterval = null; - } - - /** - * The status of the client - * @readonly - * @type {number} - */ - get status() { - return this.connection ? this.connection.status : Status.IDLE; - } - - /** - * Connects the client to the WebSocket. - * @param {string} token The authorization token - * @param {Function} resolve Function to run when connection is successful - * @param {Function} reject Function to run when connection fails - */ - connectToWebSocket(token, resolve, reject) { - this.client.emit(Events.DEBUG, `Authenticated using token ${token}`); - this.client.token = token; - const timeout = this.client.setTimeout(() => reject(new Error('WS_CONNECTION_TIMEOUT')), 1000 * 300); - this.client.api.gateway.get().then(async res => { - if (this.client.options.presence != null) { // eslint-disable-line eqeqeq - const presence = await this.client.presence._parse(this.client.options.presence); - this.client.options.ws.presence = presence; - this.client.presence.patch(presence); - } - const gateway = `${res.url}/`; - this.client.emit(Events.DEBUG, `Using gateway ${gateway}`); - this.client.ws.connect(gateway); - this.client.ws.connection.once('error', reject); - this.client.ws.connection.once('close', event => { - if (event.code === 4004) reject(new Error('TOKEN_INVALID')); - if (event.code === 4010) reject(new Error('SHARDING_INVALID')); - if (event.code === 4011) reject(new Error('SHARDING_REQUIRED')); - }); - this.client.once(Events.READY, () => { - resolve(token); - this.client.clearTimeout(timeout); - }); - }, reject); - } - - destroy() { - this.client.ws.destroy(); - if (this.client.user) this.client.token = null; - } -} - -module.exports = ClientManager; diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 9e44f4260..f349b9bcd 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -19,13 +19,17 @@ class ActionsManager { this.register(require('./GuildRoleCreate')); this.register(require('./GuildRoleDelete')); this.register(require('./GuildRoleUpdate')); + this.register(require('./PresenceUpdate')); this.register(require('./UserUpdate')); + this.register(require('./VoiceStateUpdate')); this.register(require('./GuildEmojiCreate')); this.register(require('./GuildEmojiDelete')); this.register(require('./GuildEmojiUpdate')); this.register(require('./GuildEmojisUpdate')); this.register(require('./GuildRolesPositionUpdate')); this.register(require('./GuildChannelsPositionUpdate')); + this.register(require('./GuildIntegrationsUpdate')); + this.register(require('./WebhooksUpdate')); } register(Action) { diff --git a/src/client/actions/GuildIntegrationsUpdate.js b/src/client/actions/GuildIntegrationsUpdate.js new file mode 100644 index 000000000..e9c3bdbf4 --- /dev/null +++ b/src/client/actions/GuildIntegrationsUpdate.js @@ -0,0 +1,18 @@ +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildIntegrationsUpdate extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); + } +} + +module.exports = GuildIntegrationsUpdate; + +/** + * Emitted whenever a guild integration is updated + * @event Client#guildIntegrationsUpdate + * @param {Guild} guild The guild whose integrations were updated + */ diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 2e0a1d8cc..febeb73a0 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -2,7 +2,7 @@ const Action = require('./Action'); const { Events, Status } = require('../../util/Constants'); class GuildMemberRemoveAction extends Action { - handle(data) { + handle(data, shard) { const client = this.client; const guild = client.guilds.get(data.guild_id); let member = null; @@ -13,7 +13,7 @@ class GuildMemberRemoveAction extends Action { guild.voiceStates.delete(member.id); member.deleted = true; guild.members.remove(member.id); - if (client.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); + if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); } } return { guild, member }; diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index aebc0d389..9308aedb1 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -10,7 +10,9 @@ class MessageCreateAction extends Action { if (existing) return { message: existing }; const message = channel.messages.add(data); const user = message.author; - const member = channel.guild ? channel.guild.member(user) : null; + let member = null; + if (message.member && channel.guild) member = channel.guild.members.add(message.member); + else if (channel.guild) member = channel.guild.member(user); channel.lastMessageID = data.id; if (user) { user.lastMessageID = data.id; diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js new file mode 100644 index 000000000..6da03caf2 --- /dev/null +++ b/src/client/actions/PresenceUpdate.js @@ -0,0 +1,38 @@ +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class PresenceUpdateAction extends Action { + handle(data) { + let cached = this.client.users.get(data.user.id); + if (!cached && data.user.username) cached = this.client.users.add(data.user); + if (!cached) return; + + if (data.user && data.user.username) { + if (!cached.equals(data.user)) this.client.actions.UserUpdate.handle(data); + } + + const guild = this.client.guilds.get(data.guild_id); + if (!guild) return; + + let member = guild.members.get(cached.id); + if (!member && data.status !== 'offline') { + member = guild.members.add({ user: cached, roles: data.roles, deaf: false, mute: false }); + this.client.emit(Events.GUILD_MEMBER_AVAILABLE, member); + } + + if (member) { + if (this.client.listenerCount(Events.PRESENCE_UPDATE) === 0) { + guild.presences.add(data); + return; + } + const old = member._clone(); + if (member.presence) old.frozenPresence = member.presence._clone(); + guild.presences.add(data); + this.client.emit(Events.PRESENCE_UPDATE, old, member); + } else { + guild.presences.add(data); + } + } +} + +module.exports = PresenceUpdateAction; diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index de796db2d..60adc1746 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -5,19 +5,14 @@ class UserUpdateAction extends Action { handle(data) { const client = this.client; - if (client.user) { - if (client.user.equals(data)) { - return { - old: client.user, - updated: client.user, - }; - } + const newUser = client.users.get(data.user.id); + const oldUser = newUser._update(data.user); - const oldUser = client.user._update(data); - client.emit(Events.USER_UPDATE, oldUser, client.user); + if (!oldUser.equals(newUser)) { + client.emit(Events.USER_UPDATE, oldUser, newUser); return { old: oldUser, - updated: client.user, + updated: newUser, }; } diff --git a/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js similarity index 75% rename from src/client/websocket/packets/handlers/VoiceStateUpdate.js rename to src/client/actions/VoiceStateUpdate.js index e423d0d0b..0eb9c5a57 100644 --- a/src/client/websocket/packets/handlers/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -1,13 +1,10 @@ -const AbstractHandler = require('./AbstractHandler'); - -const { Events } = require('../../../../util/Constants'); -const VoiceState = require('../../../../structures/VoiceState'); - -class VoiceStateUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); +const VoiceState = require('../../structures/VoiceState'); +class VoiceStateUpdate extends Action { + handle(data) { + const client = this.client; const guild = client.guilds.get(data.guild_id); if (guild) { // Update the state @@ -42,4 +39,4 @@ class VoiceStateUpdateHandler extends AbstractHandler { * @param {VoiceState} newState The voice state after the update */ -module.exports = VoiceStateUpdateHandler; +module.exports = VoiceStateUpdate; diff --git a/src/client/websocket/packets/handlers/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js similarity index 57% rename from src/client/websocket/packets/handlers/WebhooksUpdate.js rename to src/client/actions/WebhooksUpdate.js index 7ed2721e3..5ffc41a40 100644 --- a/src/client/websocket/packets/handlers/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -1,10 +1,9 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); -class WebhooksUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; +class WebhooksUpdate extends Action { + handle(data) { + const client = this.client; const channel = client.channels.get(data.channel_id); if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); } diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 9bec81509..004cf2019 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -169,7 +169,7 @@ class VoiceConnection extends EventEmitter { self_deaf: false, }, options); - this.client.ws.send({ + this.channel.guild.shard.send({ op: OPCodes.VOICE_STATE_UPDATE, d: options, }); diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 960bc0b75..4e4eec1ee 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,88 +1,281 @@ -const EventEmitter = require('events'); -const { Events, Status } = require('../../util/Constants'); -const WebSocketConnection = require('./WebSocketConnection'); +const WebSocketShard = require('./WebSocketShard'); +const { Events, Status, WSEvents } = require('../../util/Constants'); +const PacketHandlers = require('./handlers'); + +const BeforeReadyWhitelist = [ + WSEvents.READY, + WSEvents.RESUMED, + WSEvents.GUILD_CREATE, + WSEvents.GUILD_DELETE, + WSEvents.GUILD_MEMBERS_CHUNK, + WSEvents.GUILD_MEMBER_ADD, + WSEvents.GUILD_MEMBER_REMOVE, +]; /** * WebSocket Manager of the client. - * @private */ -class WebSocketManager extends EventEmitter { +class WebSocketManager { constructor(client) { - super(); /** * The client that instantiated this WebSocketManager * @type {Client} + * @readonly */ - this.client = client; + Object.defineProperty(this, 'client', { value: client }); /** - * The WebSocket connection of this manager - * @type {?WebSocketConnection} + * The gateway this WebSocketManager uses. + * @type {?string} */ - this.connection = null; + this.gateway = undefined; + + /** + * An array of shards spawned by this WebSocketManager. + * @type {WebSocketShard[]} + */ + this.shards = []; + + /** + * An array of queued shards to be spawned by this WebSocketManager. + * @type {Array} + * @private + */ + this.spawnQueue = []; + + /** + * Whether or not this WebSocketManager is currently spawning shards. + * @type {boolean} + * @private + */ + this.spawning = false; + + /** + * An array of queued events before this WebSocketManager became ready. + * @type {object[]} + * @private + */ + this.packetQueue = []; + + /** + * The current status of this WebSocketManager. + * @type {number} + */ + this.status = Status.IDLE; + + /** + * The current session limit of the client. + * @type {?Object} + * @prop {number} total Total number of identifies available + * @prop {number} remaining Number of identifies remaining + * @prop {number} reset_after Number of milliseconds after which the limit resets + */ + this.sessionStartLimit = null; } /** - * Sends a heartbeat on the available connection. - * @returns {void} + * The average ping of all WebSocketShards + * @type {number} + * @readonly */ - heartbeat() { - if (!this.connection) return this.debug('No connection to heartbeat'); - return this.connection.heartbeat(); + get ping() { + const sum = this.shards.reduce((a, b) => a + b.ping, 0); + return sum / this.shards.length; } /** * Emits a debug event. * @param {string} message Debug message * @returns {void} + * @private */ debug(message) { - return this.client.emit(Events.DEBUG, `[ws] ${message}`); + this.client.emit(Events.DEBUG, `[connection] ${message}`); } /** - * Destroy the client. - * @returns {void} Whether or not destruction was successful + * Handles the session identify rate limit for a shard. + * @param {WebSocketShard} shard Shard to handle + * @private */ - destroy() { - if (!this.connection) { - this.debug('Attempted to destroy WebSocket but no connection exists!'); + async _handleSessionLimit(shard) { + this.sessionStartLimit = await this.client.api.gateway.bot.get().then(r => r.session_start_limit); + const { remaining, reset_after } = this.sessionStartLimit; + if (remaining !== 0) { + this.spawn(); + } else { + shard.debug(`Exceeded identify threshold, setting a timeout for ${reset_after} ms`); + setTimeout(() => this.spawn(), this.sessionStartLimit.reset_after); + } + } + + /** + * Used to spawn WebSocketShards. + * @param {?WebSocketShard|WebSocketShard[]|number|string} query The WebSocketShards to be spawned + * @returns {void} + * @private + */ + spawn(query) { + if (query !== undefined) { + if (Array.isArray(query)) { + for (const item of query) { + if (!this.spawnQueue.includes(item)) this.spawnQueue.push(item); + } + } else if (!this.spawnQueue.includes(query)) { + this.spawnQueue.push(query); + } + } + + if (this.spawning || !this.spawnQueue.length) return; + + this.spawning = true; + let item = this.spawnQueue.shift(); + + if (typeof item === 'string' && !isNaN(item)) item = Number(item); + if (typeof item === 'number') { + const shard = new WebSocketShard(this, item, this.shards[item]); + this.shards[item] = shard; + shard.once(Events.READY, () => { + this.spawning = false; + this.client.setTimeout(() => this._handleSessionLimit(shard), 5000); + }); + shard.once(Events.INVALIDATED, () => { + this.spawning = false; + }); + } else if (item instanceof WebSocketShard) { + item.reconnect(); + } + } + + /** + * Creates a connection to a gateway. + * @param {string} [gateway=this.gateway] The gateway to connect to + * @returns {void} + * @private + */ + connect(gateway = this.gateway) { + this.gateway = gateway; + + if (typeof this.client.options.shards === 'number') { + this.debug('Spawning 1 shard'); + this.spawn(this.client.options.shards); + } else if (Array.isArray(this.client.options.shards)) { + this.debug(`Spawning ${this.client.options.shards.length} shards`); + for (let i = 0; i < this.client.options.shards.length; i++) { + this.spawn(this.client.options.shards[i]); + } + } else { + this.debug(`Spawning ${this.client.options.shardCount} shards`); + for (let i = 0; i < this.client.options.shardCount; i++) { + this.spawn(i); + } + } + } + + /** + * 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, shardID: shard.id }); + return false; + } + } + + if (this.packetQueue.length) { + const item = this.packetQueue.shift(); + this.client.setImmediate(() => { + this.handlePacket(item.packet, this.shards[item.shardID]); + }); + } + + if (packet && PacketHandlers[packet.t]) { + PacketHandlers[packet.t](this.client, packet, shard); + } + + return false; + } + + /** + * Checks whether the client is ready to be marked as ready. + * @returns {boolean} + * @private + */ + checkReady() { + if (this.shards.filter(s => s).length !== this.client.options.shardCount || + this.shards.some(s => s && s.status !== Status.READY)) { return false; } - return this.connection.destroy(); + + let unavailableGuilds = 0; + for (const guild of this.client.guilds.values()) { + if (!guild.available) unavailableGuilds++; + } + if (unavailableGuilds === 0) { + this.status = Status.NEARLY; + if (!this.client.options.fetchAllMembers) return this.triggerReady(); + // Fetch all members before marking self as ready + const promises = this.client.guilds.map(g => g.members.fetch()); + Promise.all(promises) + .then(() => this.triggerReady()) + .catch(e => { + this.debug(`Failed to fetch all members before ready! ${e}`); + this.triggerReady(); + }); + } + return true; } /** - * Send a packet on the available WebSocket. - * @param {Object} packet Packet to send + * Causes the client to be marked as ready and emits the ready event. * @returns {void} + * @private */ - send(packet) { - if (!this.connection) { - this.debug('No connection to websocket'); + triggerReady() { + if (this.status === Status.READY) { + this.debug('Tried to mark self as ready, but already ready'); return; } - this.connection.send(packet); + this.status = Status.READY; + + /** + * Emitted when the client becomes ready to start working. + * @event Client#ready + */ + this.client.emit(Events.READY); + + this.handlePacket(); } /** - * Connects the client to a gateway. - * @param {string} gateway The gateway to connect to - * @returns {boolean} + * Broadcasts a message to every shard in this WebSocketManager. + * @param {*} packet The packet to send */ - connect(gateway) { - if (!this.connection) { - this.connection = new WebSocketConnection(this, gateway); - return true; + broadcast(packet) { + for (const shard of this.shards) { + if (!shard) continue; + shard.send(packet); } - switch (this.connection.status) { - case Status.IDLE: - case Status.DISCONNECTED: - this.connection.connect(gateway, 5500); - return true; - default: - this.debug(`Couldn't connect to ${gateway} as the websocket is at state ${this.connection.status}`); - return false; + } + + /** + * Destroys all shards. + * @returns {void} + * @private + */ + destroy() { + this.gateway = undefined; + // Lock calls to spawn + this.spawning = true; + + for (const shard of this.shards) { + if (!shard) continue; + shard.destroy(); } } } diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketShard.js similarity index 52% rename from src/client/websocket/WebSocketConnection.js rename to src/client/websocket/WebSocketShard.js index 12e90075c..1c51be0a9 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketShard.js @@ -1,25 +1,21 @@ const EventEmitter = require('events'); -const { Events, OPCodes, Status, WSCodes } = require('../../util/Constants'); -const PacketManager = require('./packets/WebSocketPacketManager'); const WebSocket = require('../../WebSocket'); +const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants'); +let zlib; try { - var zlib = require('zlib-sync'); + zlib = require('zlib-sync'); if (!zlib.Inflate) zlib = require('pako'); } catch (err) { zlib = require('pako'); } /** - * Abstracts a WebSocket connection with decoding/encoding for the Discord gateway. - * @private + * Represents a Shard's Websocket connection. */ -class WebSocketConnection extends EventEmitter { - /** - * @param {WebSocketManager} manager The WebSocket manager - * @param {string} gateway The WebSocket gateway to connect to - */ - constructor(manager, gateway) { +class WebSocketShard extends EventEmitter { + constructor(manager, id, oldShard) { super(); + /** * The WebSocket Manager of this connection * @type {WebSocketManager} @@ -27,242 +23,240 @@ class WebSocketConnection extends EventEmitter { this.manager = manager; /** - * The client this belongs to - * @type {Client} - */ - this.client = manager.client; - - /** - * The WebSocket connection itself - * @type {WebSocket} - */ - this.ws = null; - - /** - * The current sequence of the WebSocket + * The id of the this shard. * @type {number} */ - this.sequence = -1; + this.id = id; /** - * The current sessionID of the WebSocket - * @type {string} - */ - this.sessionID = null; - - /** - * The current status of the client - * @type {number} + * The current status of the shard + * @type {Status} */ this.status = Status.IDLE; /** - * The Packet Manager of the connection - * @type {WebSocketPacketManager} - */ - this.packetManager = new PacketManager(this); - - /** - * The last time a ping was sent (a timestamp) + * The current sequence of the WebSocket * @type {number} + * @private */ - this.lastPingTimestamp = 0; - - /** - * Contains the rate limit queue and metadata - * @type {Object} - */ - this.ratelimit = { - queue: [], - remaining: 120, - total: 120, - time: 60e3, - resetTimer: null, - }; - - /** - * Events that are disabled (will not be processed) - * @type {Object} - */ - this.disabledEvents = {}; - for (const event of this.client.options.disabledEvents) this.disabledEvents[event] = true; + this.sequence = oldShard ? oldShard.sequence : -1; /** * The sequence on WebSocket close * @type {number} + * @private */ this.closeSequence = 0; /** - * Whether or not the WebSocket is expecting to be closed - * @type {boolean} + * The current session id of the WebSocket + * @type {?string} + * @private */ - this.expectingClose = false; + this.sessionID = oldShard && oldShard.sessionID; - this.inflate = null; - this.connect(gateway); - } - - /** - * Causes the client to be marked as ready and emits the ready event. - * @returns {void} - */ - triggerReady() { - if (this.status === Status.READY) { - this.debug('Tried to mark self as ready, but already ready'); - return; - } /** - * Emitted when the client becomes ready to start working. - * @event Client#ready + * Previous heartbeat pings of the websocket (most recent first, limited to three elements) + * @type {number[]} */ - this.status = Status.READY; - this.client.emit(Events.READY); - this.packetManager.handleQueue(); + this.pings = []; + + /** + * The last time a ping was sent (a timestamp) + * @type {number} + * @private + */ + this.lastPingTimestamp = -1; + + /** + * List of servers the shard is connected to + * @type {string[]} + * @private + */ + this.trace = []; + + /** + * Contains the rate limit queue and metadata + * @type {Object} + * @private + */ + this.ratelimit = { + queue: [], + total: 120, + remaining: 120, + time: 60e3, + timer: null, + }; + + /** + * The WebSocket connection for the current shard + * @type {?WebSocket} + * @private + */ + this.ws = null; + + /** + * @external Inflate + * @see {@link https://www.npmjs.com/package/zlib-sync} + */ + + /** + * The compression to use + * @type {?Inflate} + * @private + */ + this.inflate = null; + + this.connect(); } /** - * Checks whether the client is ready to be marked as ready. - * @returns {void} + * Average heartbeat ping of the websocket, obtained by averaging the WebSocketShard#pings property + * @type {number} + * @readonly */ - checkIfReady() { - if (this.status === Status.READY || this.status === Status.NEARLY) return false; - let unavailableGuilds = 0; - for (const guild of this.client.guilds.values()) { - if (!guild.available) unavailableGuilds++; - } - if (unavailableGuilds === 0) { - this.status = Status.NEARLY; - if (!this.client.options.fetchAllMembers) return this.triggerReady(); - // Fetch all members before marking self as ready - const promises = this.client.guilds.map(g => g.members.fetch()); - Promise.all(promises) - .then(() => this.triggerReady()) - .catch(e => { - this.debug(`Failed to fetch all members before ready! ${e}`); - this.triggerReady(); - }); - } - return true; + get ping() { + const sum = this.pings.reduce((a, b) => a + b, 0); + return sum / this.pings.length; } - // Util /** - * Emits a debug message. + * Emits a debug event. * @param {string} message Debug message - * @returns {void} + * @private */ debug(message) { - if (message instanceof Error) message = message.stack; - return this.manager.debug(`[connection] ${message}`); + this.manager.debug(`[shard ${this.id}] ${message}`); } /** - * Processes the current WebSocket queue. + * Sends a heartbeat or sets an interval for sending heartbeats. + * @param {number} [time] If -1, clears the interval, any other number sets an interval + * If no value is given, a heartbeat will be sent instantly + * @private */ - processQueue() { - if (this.ratelimit.remaining === 0) return; - if (this.ratelimit.queue.length === 0) return; - if (this.ratelimit.remaining === this.ratelimit.total) { - this.ratelimit.resetTimer = this.client.setTimeout(() => { - this.ratelimit.remaining = this.ratelimit.total; - this.processQueue(); - }, this.ratelimit.time); - } - while (this.ratelimit.remaining > 0) { - const item = this.ratelimit.queue.shift(); - if (!item) return; - this._send(item); - this.ratelimit.remaining--; - } - } - - /** - * Sends data, bypassing the queue. - * @param {Object} data Packet to send - * @returns {void} - */ - _send(data) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${data} but no WebSocket is available!`); + heartbeat(time) { + if (!isNaN(time)) { + if (time === -1) { + this.debug('Clearing heartbeat interval'); + this.manager.client.clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } else { + this.debug(`Setting a heartbeat interval for ${time}ms`); + this.heartbeatInterval = this.manager.client.setInterval(() => this.heartbeat(), time); + } return; } - this.ws.send(WebSocket.pack(data)); + + this.debug('Sending a heartbeat'); + this.lastPingTimestamp = Date.now(); + this.send({ + op: OPCodes.HEARTBEAT, + d: this.sequence, + }); } /** - * Adds data to the queue to be sent. - * @param {Object} data Packet to send - * @returns {void} + * Acknowledges a heartbeat. + * @private */ - send(data) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${data} but no WebSocket is available!`); - return; - } - this.ratelimit.queue.push(data); - this.processQueue(); + ackHeartbeat() { + const latency = Date.now() - this.lastPingTimestamp; + this.debug(`Heartbeat acknowledged, latency of ${latency}ms`); + this.pings.unshift(latency); + if (this.pings.length > 3) this.pings.length = 3; } /** - * Creates a connection to a gateway. - * @param {string} gateway The gateway to connect to - * @param {number} [after=0] How long to wait before connecting - * @param {boolean} [force=false] Whether or not to force a new connection even if one already exists - * @returns {boolean} + * Connects the shard to a gateway. + * @private */ - connect(gateway = this.gateway, after = 0, force = false) { - if (after) return this.client.setTimeout(() => this.connect(gateway, 0, force), after); // eslint-disable-line - if (this.ws && !force) { - this.debug('WebSocket connection already exists'); - return false; - } else if (typeof gateway !== 'string') { - this.debug(`Tried to connect to an invalid gateway: ${gateway}`); - return false; - } + connect() { this.inflate = new zlib.Inflate({ chunkSize: 65535, flush: zlib.Z_SYNC_FLUSH, to: WebSocket.encoding === 'json' ? 'string' : '', }); - this.expectingClose = false; - this.gateway = gateway; + const gateway = this.manager.gateway; this.debug(`Connecting to ${gateway}`); const ws = this.ws = WebSocket.create(gateway, { - v: this.client.options.ws.version, + v: this.manager.client.options.ws.version, compress: 'zlib-stream', }); - ws.onmessage = this.onMessage.bind(this); ws.onopen = this.onOpen.bind(this); + ws.onmessage = this.onMessage.bind(this); ws.onerror = this.onError.bind(this); ws.onclose = this.onClose.bind(this); this.status = Status.CONNECTING; - return true; } /** - * Destroys the connection. + * Called whenever a packet is received + * @param {Object} packet Packet received * @returns {boolean} + * @private */ - destroy() { - const ws = this.ws; - if (!ws) { - this.debug('Attempted to destroy WebSocket but no connection exists!'); + onPacket(packet) { + if (!packet) { + this.debug('Received null packet'); return false; } - this.heartbeat(-1); - this.expectingClose = true; - ws.close(1000); - this.packetManager.handleQueue(); - this.ws = null; - this.status = Status.DISCONNECTED; - this.ratelimit.remaining = this.ratelimit.total; - return true; + + this.manager.client.emit(Events.RAW, packet, this.id); + + switch (packet.t) { + case WSEvents.READY: + this.sessionID = packet.d.session_id; + this.trace = packet.d._trace; + this.status = Status.READY; + this.debug(`READY ${this.trace.join(' -> ')} ${this.sessionID}`); + this.heartbeat(); + break; + case WSEvents.RESUMED: { + this.trace = packet.d._trace; + this.status = Status.READY; + const replayed = packet.s - this.sequence; + this.debug(`RESUMED ${this.trace.join(' -> ')} | replayed ${replayed} events.`); + this.heartbeat(); + break; + } + } + + if (packet.s > this.sequence) this.sequence = packet.s; + + switch (packet.op) { + case OPCodes.HELLO: + this.identify(); + return this.heartbeat(packet.d.heartbeat_interval); + case OPCodes.RECONNECT: + return this.reconnect(); + case OPCodes.INVALID_SESSION: + if (!packet.d) this.sessionID = null; + this.sequence = -1; + this.debug('Session invalidated'); + return this.reconnect(Events.INVALIDATED); + case OPCodes.HEARTBEAT_ACK: + return this.ackHeartbeat(); + case OPCodes.HEARTBEAT: + return this.heartbeat(); + default: + return this.manager.handlePacket(packet, this); + } + } + + /** + * Called whenever a connection is opened to the gateway. + * @param {Event} event Received open event + * @private + */ + onOpen() { + this.debug('Connection open'); } /** * Called whenever a message is received. * @param {Event} event Event received + * @private */ onMessage({ data }) { if (data instanceof ArrayBuffer) data = new Uint8Array(data); @@ -278,89 +272,40 @@ class WebSocketConnection extends EventEmitter { let packet; try { packet = WebSocket.unpack(this.inflate.result); + this.manager.client.emit(Events.RAW, packet); } catch (err) { - this.client.emit('debug', err); + this.manager.client.emit(Events.ERROR, err); return; } + if (packet.t === 'READY') { + /** + * Emitted when a shard becomes ready + * @event WebSocketShard#ready + */ + this.emit(Events.READY); + + /** + * Emitted when a shard becomes ready + * @event Client#shardReady + * @param {number} shardID The id of the shard + */ + this.manager.client.emit(Events.SHARD_READY, this.id); + } this.onPacket(packet); - if (this.client.listenerCount('raw')) this.client.emit('raw', packet); - } - - /** - * Sets the current sequence of the connection. - * @param {number} s New sequence - */ - setSequence(s) { - this.sequence = s > this.sequence ? s : this.sequence; - } - - /** - * Called whenever a packet is received. - * @param {Object} packet Received packet - * @returns {boolean} - */ - onPacket(packet) { - if (!packet) { - this.debug('Received null packet'); - return false; - } - switch (packet.op) { - case OPCodes.HELLO: - return this.heartbeat(packet.d.heartbeat_interval); - case OPCodes.RECONNECT: - return this.reconnect(); - case OPCodes.INVALID_SESSION: - if (!packet.d) this.sessionID = null; - this.sequence = -1; - this.debug('Session invalidated -- will identify with a new session'); - return this.identify(packet.d ? 2500 : 0); - case OPCodes.HEARTBEAT_ACK: - return this.ackHeartbeat(); - case OPCodes.HEARTBEAT: - return this.heartbeat(); - default: - return this.packetManager.handle(packet); - } - } - - /** - * Called whenever a connection is opened to the gateway. - * @param {Event} event Received open event - */ - onOpen(event) { - if (event && event.target && event.target.url) this.gateway = event.target.url; - this.debug(`Connected to gateway ${this.gateway}`); - this.identify(); - } - - /** - * Causes a reconnection to the gateway. - */ - reconnect() { - this.debug('Attempting to reconnect in 5500ms...'); - /** - * Emitted whenever the client tries to reconnect to the WebSocket. - * @event Client#reconnecting - */ - this.client.emit(Events.RECONNECTING); - this.connect(this.gateway, 5500, true); } /** * Called whenever an error occurs with the WebSocket. * @param {Error} error The error that occurred + * @private */ onError(error) { if (error && error.message === 'uWs client connection error') { this.reconnect(); return; } - /** - * Emitted whenever the client's WebSocket encounters a connection error. - * @event Client#error - * @param {Error} error The encountered error - */ - this.client.emit(Events.ERROR, error); + this.emit(Events.INVALIDATED); + this.manager.client.emit(Events.ERROR, error); } /** @@ -371,90 +316,50 @@ class WebSocketConnection extends EventEmitter { /** * Called whenever a connection to the gateway is closed. * @param {CloseEvent} event Close event that was received + * @private */ onClose(event) { - this.debug(`${this.expectingClose ? 'Client' : 'Server'} closed the WebSocket connection: ${event.code}`); this.closeSequence = this.sequence; - // Reset the state before trying to fix anything this.emit('close', event); - this.heartbeat(-1); - // Should we reconnect? if (event.code === 1000 ? this.expectingClose : WSCodes[event.code]) { - this.expectingClose = false; /** * Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect. * @event Client#disconnect * @param {CloseEvent} event The WebSocket close event + * @param {number} shardID The shard that disconnected */ - this.client.emit(Events.DISCONNECT, event); + this.manager.client.emit(Events.DISCONNECT, event, this.id); + this.debug(WSCodes[event.code]); - this.destroy(); return; } - this.expectingClose = false; - this.reconnect(); + this.reconnect(Events.INVALIDATED); } - // Heartbeat - /** - * Acknowledges a heartbeat. - */ - ackHeartbeat() { - this.debug(`Heartbeat acknowledged, latency of ${Date.now() - this.lastPingTimestamp}ms`); - this.client._pong(this.lastPingTimestamp); - } - - /** - * Sends a heartbeat or sets an interval for sending heartbeats. - * @param {number} [time] If -1, clears the interval, any other number sets an interval - * If no value is given, a heartbeat will be sent instantly - */ - heartbeat(time) { - if (!isNaN(time)) { - if (time === -1) { - this.debug('Clearing heartbeat interval'); - this.client.clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; - } else { - this.debug(`Setting a heartbeat interval for ${time}ms`); - this.heartbeatInterval = this.client.setInterval(() => this.heartbeat(), time); - } - return; - } - this.debug('Sending a heartbeat'); - this.lastPingTimestamp = Date.now(); - this.send({ - op: OPCodes.HEARTBEAT, - d: this.sequence, - }); - } - - // Identification /** * Identifies the client on a connection. - * @param {number} [after] How long to wait before identifying * @returns {void} + * @private */ - identify(after) { - if (after) return this.client.setTimeout(this.identify.bind(this), after); + identify() { return this.sessionID ? this.identifyResume() : this.identifyNew(); } /** * Identifies as a new connection on the gateway. * @returns {void} + * @private */ identifyNew() { - if (!this.client.token) { + if (!this.manager.client.token) { this.debug('No token available to identify a new session with'); return; } // Clone the generic payload and assign the token - const d = Object.assign({ token: this.client.token }, this.client.options.ws); + const d = Object.assign({ token: this.manager.client.token }, this.manager.client.options.ws); - // Sharding stuff - const { shardId, shardCount } = this.client.options; - if (shardCount > 0) d.shard = [Number(shardId), Number(shardCount)]; + const { totalShardCount } = this.manager.client.options; + d.shard = [this.id, Number(totalShardCount)]; // Send the payload this.debug('Identifying as a new session'); @@ -464,6 +369,7 @@ class WebSocketConnection extends EventEmitter { /** * Resumes a session on the gateway. * @returns {void} + * @private */ identifyResume() { if (!this.sessionID) { @@ -473,7 +379,7 @@ class WebSocketConnection extends EventEmitter { this.debug(`Attempting to resume session ${this.sessionID}`); const d = { - token: this.client.token, + token: this.manager.client.token, session_id: this.sessionID, seq: this.sequence, }; @@ -483,6 +389,85 @@ class WebSocketConnection extends EventEmitter { d, }); } -} -module.exports = WebSocketConnection; + /** + * Adds data to the queue to be sent. + * @param {Object} data Packet to send + * @returns {void} + */ + send(data) { + this.ratelimit.queue.push(data); + this.processQueue(); + } + + /** + * Sends data, bypassing the queue. + * @param {Object} data Packet to send + * @returns {void} + * @private + */ + _send(data) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.debug(`Tried to send packet ${data} but no WebSocket is available!`); + return; + } + this.ws.send(WebSocket.pack(data)); + } + + /** + * 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.resetTimer = this.manager.client.setTimeout(() => { + this.ratelimit.remaining = this.ratelimit.total; + this.processQueue(); + }, this.ratelimit.time); + } + while (this.ratelimit.remaining > 0) { + const item = this.ratelimit.queue.shift(); + if (!item) return; + this._send(item); + this.ratelimit.remaining--; + } + } + + /** + * Triggers a shard reconnect. + * @param {?string} [event] The event for the shard to emit + * @returns {void} + * @private + */ + reconnect(event) { + this.heartbeat(-1); + this.status = Status.RECONNECTING; + + /** + * Emitted whenever a shard tries to reconnect to the WebSocket. + * @event Client#reconnecting + */ + this.manager.client.emit(Events.RECONNECTING, this.id); + + if (event === Events.INVALIDATED) this.emit(event); + this.manager.spawn(this.id); + } + + /** + * Destroys the current shard and terminates its connection. + * @returns {void} + * @private + */ + destroy() { + this.heartbeat(-1); + this.expectingClose = true; + if (this.ws) this.ws.close(1000); + this.ws = null; + this.status = Status.DISCONNECTED; + this.ratelimit.remaining = this.ratelimit.total; + } +} +module.exports = WebSocketShard; diff --git a/src/client/websocket/handlers/CHANNEL_CREATE.js b/src/client/websocket/handlers/CHANNEL_CREATE.js new file mode 100644 index 000000000..3074254fc --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_CREATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.ChannelCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/CHANNEL_DELETE.js b/src/client/websocket/handlers/CHANNEL_DELETE.js new file mode 100644 index 000000000..158ccb352 --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.ChannelDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js new file mode 100644 index 000000000..1272dbbb1 --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -0,0 +1,20 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const channel = client.channels.get(data.channel_id); + const time = new Date(data.last_pin_timestamp); + + if (channel && time) { + // Discord sends null for last_pin_timestamp if the last pinned message was removed + channel.lastPinTimestamp = time.getTime() || null; + + /** + * 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 {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occured in + * @param {Date} time The time of the pins update + */ + client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); + } +}; diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js new file mode 100644 index 000000000..46d9037a2 --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js @@ -0,0 +1,15 @@ +const { Events } = require('../../../util/Constants'); + +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. + * @event Client#channelUpdate + * @param {DMChannel|GroupDMChannel|GuildChannel} oldChannel The channel before the update + * @param {DMChannel|GroupDMChannel|GuildChannel} newChannel The channel after the update + */ + client.emit(Events.CHANNEL_UPDATE, old, updated); + } +}; + diff --git a/src/client/websocket/handlers/GUILD_BAN_ADD.js b/src/client/websocket/handlers/GUILD_BAN_ADD.js new file mode 100644 index 000000000..00772c8cc --- /dev/null +++ b/src/client/websocket/handlers/GUILD_BAN_ADD.js @@ -0,0 +1,14 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const guild = client.guilds.get(data.guild_id); + const user = client.users.get(data.user.id); + + /** + * Emitted whenever a member is banned from a guild. + * @event Client#guildBanAdd + * @param {Guild} guild The guild that the ban occurred in + * @param {User} user The user that was banned + */ + if (guild && user) client.emit(Events.GUILD_BAN_ADD, guild, user); +}; diff --git a/src/client/websocket/handlers/GUILD_BAN_REMOVE.js b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js new file mode 100644 index 000000000..08483a835 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildBanRemove.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js new file mode 100644 index 000000000..05250ed6e --- /dev/null +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -0,0 +1,26 @@ +const { Events, Status } = require('../../../util/Constants'); + +module.exports = async (client, { d: data }, shard) => { + let guild = client.guilds.get(data.id); + if (guild) { + if (!guild.available && !data.unavailable) { + // A newly available guild + guild._patch(data); + client.ws.checkReady(); + } + } else { + // A new guild + data.shardID = shard.id; + guild = client.guilds.add(data); + const emitEvent = client.ws.status === Status.READY; + if (emitEvent) { + /** + * Emitted whenever the client joins a guild. + * @event Client#guildCreate + * @param {Guild} guild The created guild + */ + if (client.options.fetchAllMembers) await guild.members.fetch(); + client.emit(Events.GUILD_CREATE, guild); + } + } +}; diff --git a/src/client/websocket/handlers/GUILD_DELETE.js b/src/client/websocket/handlers/GUILD_DELETE.js new file mode 100644 index 000000000..19d1b3b0a --- /dev/null +++ b/src/client/websocket/handlers/GUILD_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js new file mode 100644 index 000000000..5fa5a9d54 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildEmojisUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js new file mode 100644 index 000000000..6c1a0cfd3 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildIntegrationsUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js new file mode 100644 index 000000000..7178264d2 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js @@ -0,0 +1,17 @@ +const { Events } = require('../../../util/Constants'); +const Collection = require('../../../util/Collection'); + +module.exports = (client, { d: data }) => { + const guild = client.guilds.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)); + /** + * Emitted whenever a chunk of guild members is received (all members come from the same guild). + * @event Client#guildMembersChunk + * @param {Collection} members The members in the chunk + * @param {Guild} guild The guild related to the member chunk + */ + client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild); +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js new file mode 100644 index 000000000..367058a5b --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js @@ -0,0 +1,17 @@ +const { Events, Status } = require('../../../util/Constants'); + +module.exports = (client, { d: data }, shard) => { + const guild = client.guilds.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.GUILD_MEMBER_ADD, member); + } + } +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js new file mode 100644 index 000000000..b00da0e63 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet, shard) => { + client.actions.GuildMemberRemove.handle(packet.d, shard); +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js new file mode 100644 index 000000000..be4f573ad --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js @@ -0,0 +1,20 @@ +const { Status, Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }, shard) => { + const guild = client.guilds.get(data.guild_id); + if (guild) { + const member = guild.members.get(data.user.id); + if (member) { + const old = member._update(data); + if (shard.status === Status.READY) { + /** + * Emitted whenever a guild member changes - i.e. new role, removed role, nickname. + * @event Client#guildMemberUpdate + * @param {GuildMember} oldMember The member before the update + * @param {GuildMember} newMember The member after the update + */ + client.emit(Events.GUILD_MEMBER_UPDATE, old, member); + } + } + } +}; diff --git a/src/client/websocket/handlers/GUILD_ROLE_CREATE.js b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js new file mode 100644 index 000000000..b6ea80381 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildRoleCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_ROLE_DELETE.js b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js new file mode 100644 index 000000000..d1093cb27 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildRoleDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js new file mode 100644 index 000000000..c1f526c57 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildRoleUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_SYNC.js b/src/client/websocket/handlers/GUILD_SYNC.js new file mode 100644 index 000000000..f27da424e --- /dev/null +++ b/src/client/websocket/handlers/GUILD_SYNC.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildSync.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_UPDATE.js b/src/client/websocket/handlers/GUILD_UPDATE.js new file mode 100644 index 000000000..0f3e24f74 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_CREATE.js b/src/client/websocket/handlers/MESSAGE_CREATE.js new file mode 100644 index 000000000..bc9303fd5 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_CREATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_DELETE.js b/src/client/websocket/handlers/MESSAGE_DELETE.js new file mode 100644 index 000000000..09062196c --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js new file mode 100644 index 000000000..a927b3b14 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageDeleteBulk.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js new file mode 100644 index 000000000..d81762124 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js @@ -0,0 +1,6 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet) => { + const { user, reaction } = client.actions.MessageReactionAdd.handle(packet.d); + if (reaction) client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); +}; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js new file mode 100644 index 000000000..8b9f22a17 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageReactionRemove.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js new file mode 100644 index 000000000..2323cfe05 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageReactionRemoveAll.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_UPDATE.js b/src/client/websocket/handlers/MESSAGE_UPDATE.js new file mode 100644 index 000000000..9be750c7e --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_UPDATE.js @@ -0,0 +1,14 @@ +const { Events } = require('../../../util/Constants'); + +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.MESSAGE_UPDATE, old, updated); + } +}; diff --git a/src/client/websocket/handlers/PRESENCE_UPDATE.js b/src/client/websocket/handlers/PRESENCE_UPDATE.js new file mode 100644 index 000000000..89b9f0e25 --- /dev/null +++ b/src/client/websocket/handlers/PRESENCE_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.PresenceUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js new file mode 100644 index 000000000..f22968d3d --- /dev/null +++ b/src/client/websocket/handlers/READY.js @@ -0,0 +1,16 @@ +let ClientUser; + +module.exports = (client, { d: data }, shard) => { + if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); + const clientUser = new ClientUser(client, data.user); + client.user = clientUser; + client.readyAt = new Date(); + client.users.set(clientUser.id, clientUser); + + for (const guild of data.guilds) { + guild.shardID = shard.id; + client.guilds.add(guild); + } + + client.ws.checkReady(); +}; diff --git a/src/client/websocket/handlers/RESUMED.js b/src/client/websocket/handlers/RESUMED.js new file mode 100644 index 000000000..6cc355e98 --- /dev/null +++ b/src/client/websocket/handlers/RESUMED.js @@ -0,0 +1,12 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet, shard) => { + const replayed = shard.sequence - shard.closeSequence; + /** + * Emitted when the client gateway resumes. + * @event Client#resume + * @param {number} replayed The number of events that were replayed + * @param {number} shardID The ID of the shard that resumed + */ + client.emit(Events.RESUMED, replayed, shard.id); +}; diff --git a/src/client/websocket/handlers/TYPING_START.js b/src/client/websocket/handlers/TYPING_START.js new file mode 100644 index 000000000..ac01d30d9 --- /dev/null +++ b/src/client/websocket/handlers/TYPING_START.js @@ -0,0 +1,16 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const channel = client.channels.get(data.channel_id); + const user = client.users.get(data.user_id); + + if (channel && user) { + /** + * Emitted whenever a user starts typing in a channel. + * @event Client#typingStart + * @param {Channel} channel The channel the user started typing in + * @param {User} user The user that started typing + */ + client.emit(Events.TYPING_START, channel, user); + } +}; diff --git a/src/client/websocket/handlers/USER_UPDATE.js b/src/client/websocket/handlers/USER_UPDATE.js new file mode 100644 index 000000000..3c5b859c3 --- /dev/null +++ b/src/client/websocket/handlers/USER_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.UserUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js new file mode 100644 index 000000000..c8ac3883c --- /dev/null +++ b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.emit('self.voiceServer', packet.d); +}; diff --git a/src/client/websocket/handlers/VOICE_STATE_UPDATE.js b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js new file mode 100644 index 000000000..a9527adaa --- /dev/null +++ b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.VoiceStateUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/WEBHOOKS_UPDATE.js b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js new file mode 100644 index 000000000..b1afb91c5 --- /dev/null +++ b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.WebhooksUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js new file mode 100644 index 000000000..77bcf8fd0 --- /dev/null +++ b/src/client/websocket/handlers/index.js @@ -0,0 +1,11 @@ +const { WSEvents } = require('../../../util/Constants'); + +const handlers = {}; + +for (const name of Object.keys(WSEvents)) { + try { + handlers[name] = require(`./${name}.js`); + } catch (err) {} // eslint-disable-line no-empty +} + +module.exports = handlers; diff --git a/src/client/websocket/packets/WebSocketPacketManager.js b/src/client/websocket/packets/WebSocketPacketManager.js deleted file mode 100644 index a58871380..000000000 --- a/src/client/websocket/packets/WebSocketPacketManager.js +++ /dev/null @@ -1,104 +0,0 @@ -const { OPCodes, Status, WSEvents } = require('../../../util/Constants'); - -const BeforeReadyWhitelist = [ - WSEvents.READY, - WSEvents.RESUMED, - WSEvents.GUILD_CREATE, - WSEvents.GUILD_DELETE, - WSEvents.GUILD_MEMBERS_CHUNK, - WSEvents.GUILD_MEMBER_ADD, - WSEvents.GUILD_MEMBER_REMOVE, -]; - -class WebSocketPacketManager { - constructor(connection) { - this.ws = connection; - this.handlers = {}; - this.queue = []; - - this.register(WSEvents.READY, require('./handlers/Ready')); - this.register(WSEvents.RESUMED, require('./handlers/Resumed')); - this.register(WSEvents.GUILD_CREATE, require('./handlers/GuildCreate')); - this.register(WSEvents.GUILD_DELETE, require('./handlers/GuildDelete')); - this.register(WSEvents.GUILD_UPDATE, require('./handlers/GuildUpdate')); - this.register(WSEvents.GUILD_BAN_ADD, require('./handlers/GuildBanAdd')); - this.register(WSEvents.GUILD_BAN_REMOVE, require('./handlers/GuildBanRemove')); - this.register(WSEvents.GUILD_MEMBER_ADD, require('./handlers/GuildMemberAdd')); - this.register(WSEvents.GUILD_MEMBER_REMOVE, require('./handlers/GuildMemberRemove')); - this.register(WSEvents.GUILD_MEMBER_UPDATE, require('./handlers/GuildMemberUpdate')); - this.register(WSEvents.GUILD_ROLE_CREATE, require('./handlers/GuildRoleCreate')); - this.register(WSEvents.GUILD_ROLE_DELETE, require('./handlers/GuildRoleDelete')); - this.register(WSEvents.GUILD_ROLE_UPDATE, require('./handlers/GuildRoleUpdate')); - this.register(WSEvents.GUILD_EMOJIS_UPDATE, require('./handlers/GuildEmojisUpdate')); - this.register(WSEvents.GUILD_MEMBERS_CHUNK, require('./handlers/GuildMembersChunk')); - this.register(WSEvents.GUILD_INTEGRATIONS_UPDATE, require('./handlers/GuildIntegrationsUpdate')); - this.register(WSEvents.CHANNEL_CREATE, require('./handlers/ChannelCreate')); - this.register(WSEvents.CHANNEL_DELETE, require('./handlers/ChannelDelete')); - this.register(WSEvents.CHANNEL_UPDATE, require('./handlers/ChannelUpdate')); - this.register(WSEvents.CHANNEL_PINS_UPDATE, require('./handlers/ChannelPinsUpdate')); - this.register(WSEvents.PRESENCE_UPDATE, require('./handlers/PresenceUpdate')); - this.register(WSEvents.USER_UPDATE, require('./handlers/UserUpdate')); - this.register(WSEvents.VOICE_STATE_UPDATE, require('./handlers/VoiceStateUpdate')); - this.register(WSEvents.TYPING_START, require('./handlers/TypingStart')); - this.register(WSEvents.MESSAGE_CREATE, require('./handlers/MessageCreate')); - this.register(WSEvents.MESSAGE_DELETE, require('./handlers/MessageDelete')); - this.register(WSEvents.MESSAGE_UPDATE, require('./handlers/MessageUpdate')); - this.register(WSEvents.MESSAGE_DELETE_BULK, require('./handlers/MessageDeleteBulk')); - this.register(WSEvents.VOICE_SERVER_UPDATE, require('./handlers/VoiceServerUpdate')); - this.register(WSEvents.MESSAGE_REACTION_ADD, require('./handlers/MessageReactionAdd')); - this.register(WSEvents.MESSAGE_REACTION_REMOVE, require('./handlers/MessageReactionRemove')); - this.register(WSEvents.MESSAGE_REACTION_REMOVE_ALL, require('./handlers/MessageReactionRemoveAll')); - this.register(WSEvents.WEBHOOKS_UPDATE, require('./handlers/WebhooksUpdate')); - } - - get client() { - return this.ws.client; - } - - register(event, Handler) { - this.handlers[event] = new Handler(this); - } - - handleQueue() { - this.queue.forEach((element, index) => { - this.handle(this.queue[index], true); - this.queue.splice(index, 1); - }); - } - - handle(packet, queue = false) { - if (packet.op === OPCodes.HEARTBEAT_ACK) { - this.ws.client._pong(this.ws.client._pingTimestamp); - this.ws.lastHeartbeatAck = true; - this.ws.client.emit('debug', 'Heartbeat acknowledged'); - } else if (packet.op === OPCodes.HEARTBEAT) { - this.client.ws.send({ - op: OPCodes.HEARTBEAT, - d: this.client.ws.sequence, - }); - this.ws.client.emit('debug', 'Received gateway heartbeat'); - } - - if (this.ws.status === Status.RECONNECTING) { - this.ws.reconnecting = false; - this.ws.checkIfReady(); - } - - this.ws.setSequence(packet.s); - - if (this.ws.disabledEvents[packet.t] !== undefined) return false; - - if (this.ws.status !== Status.READY) { - if (BeforeReadyWhitelist.indexOf(packet.t) === -1) { - this.queue.push(packet); - return false; - } - } - - if (!queue && this.queue.length > 0) this.handleQueue(); - if (this.handlers[packet.t]) return this.handlers[packet.t].handle(packet); - return false; - } -} - -module.exports = WebSocketPacketManager; diff --git a/src/client/websocket/packets/handlers/AbstractHandler.js b/src/client/websocket/packets/handlers/AbstractHandler.js deleted file mode 100644 index c1c2a5a2d..000000000 --- a/src/client/websocket/packets/handlers/AbstractHandler.js +++ /dev/null @@ -1,11 +0,0 @@ -class AbstractHandler { - constructor(packetManager) { - this.packetManager = packetManager; - } - - handle(packet) { - return packet; - } -} - -module.exports = AbstractHandler; diff --git a/src/client/websocket/packets/handlers/ChannelCreate.js b/src/client/websocket/packets/handlers/ChannelCreate.js deleted file mode 100644 index 5ccc05708..000000000 --- a/src/client/websocket/packets/handlers/ChannelCreate.js +++ /dev/null @@ -1,15 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class ChannelCreateHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.ChannelCreate.handle(packet.d); - } -} - -module.exports = ChannelCreateHandler; - -/** - * Emitted whenever a channel is created. - * @event Client#channelCreate - * @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was created - */ diff --git a/src/client/websocket/packets/handlers/ChannelDelete.js b/src/client/websocket/packets/handlers/ChannelDelete.js deleted file mode 100644 index 68eb9a903..000000000 --- a/src/client/websocket/packets/handlers/ChannelDelete.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class ChannelDeleteHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.ChannelDelete.handle(packet.d); - } -} - -module.exports = ChannelDeleteHandler; diff --git a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js deleted file mode 100644 index b8cb64019..000000000 --- a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js +++ /dev/null @@ -1,37 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -/* -{ t: 'CHANNEL_PINS_UPDATE', - s: 666, - op: 0, - d: - { last_pin_timestamp: '2016-08-28T17:37:13.171774+00:00', - channel_id: '314866471639044027' } } -*/ - -class ChannelPinsUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const channel = client.channels.get(data.channel_id); - const time = new Date(data.last_pin_timestamp); - if (channel && time) { - // Discord sends null for last_pin_timestamp if the last pinned message was removed - channel.lastPinTimestamp = time.getTime() || null; - - client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); - } - } -} - -module.exports = ChannelPinsUpdate; - -/** - * 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. - * The `time` parameter will be a Unix Epoch Date object when there are no pins left. - * @event Client#channelPinsUpdate - * @param {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occurred in - * @param {Date} time The time when the last pinned message was pinned - */ diff --git a/src/client/websocket/packets/handlers/ChannelUpdate.js b/src/client/websocket/packets/handlers/ChannelUpdate.js deleted file mode 100644 index f0d1873a0..000000000 --- a/src/client/websocket/packets/handlers/ChannelUpdate.js +++ /dev/null @@ -1,20 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class ChannelUpdateHandler extends AbstractHandler { - handle(packet) { - const { old, updated } = this.packetManager.client.actions.ChannelUpdate.handle(packet.d); - if (old && updated) { - this.packetManager.client.emit(Events.CHANNEL_UPDATE, old, updated); - } - } -} - -module.exports = ChannelUpdateHandler; - -/** - * Emitted whenever a channel is updated - e.g. name change, topic change. - * @event Client#channelUpdate - * @param {DMChannel|GroupDMChannel|GuildChannel} oldChannel The channel before the update - * @param {DMChannel|GroupDMChannel|GuildChannel} newChannel The channel after the update - */ diff --git a/src/client/websocket/packets/handlers/GuildBanAdd.js b/src/client/websocket/packets/handlers/GuildBanAdd.js deleted file mode 100644 index 89f57b78d..000000000 --- a/src/client/websocket/packets/handlers/GuildBanAdd.js +++ /dev/null @@ -1,23 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class GuildBanAddHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - const user = client.users.add(data.user); - if (guild && user) client.emit(Events.GUILD_BAN_ADD, guild, user); - } -} - -/** - * Emitted whenever a member is banned from a guild. - * @event Client#guildBanAdd - * @param {Guild} guild The guild that the ban occurred in - * @param {User} user The user that was banned - */ - -module.exports = GuildBanAddHandler; diff --git a/src/client/websocket/packets/handlers/GuildBanRemove.js b/src/client/websocket/packets/handlers/GuildBanRemove.js deleted file mode 100644 index c4edbdeb6..000000000 --- a/src/client/websocket/packets/handlers/GuildBanRemove.js +++ /dev/null @@ -1,20 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); - -class GuildBanRemoveHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildBanRemove.handle(data); - } -} - -/** - * Emitted whenever a member is unbanned from a guild. - * @event Client#guildBanRemove - * @param {Guild} guild The guild that the unban occurred in - * @param {User} user The user that was unbanned - */ - -module.exports = GuildBanRemoveHandler; diff --git a/src/client/websocket/packets/handlers/GuildCreate.js b/src/client/websocket/packets/handlers/GuildCreate.js deleted file mode 100644 index 96c5ae987..000000000 --- a/src/client/websocket/packets/handlers/GuildCreate.js +++ /dev/null @@ -1,33 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class GuildCreateHandler extends AbstractHandler { - async handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - - let guild = client.guilds.get(data.id); - if (guild) { - if (!guild.available && !data.unavailable) { - // A newly available guild - guild._patch(data); - this.packetManager.ws.checkIfReady(); - } - } else { - // A new guild - guild = client.guilds.add(data); - const emitEvent = client.ws.connection.status === Status.READY; - if (emitEvent) { - /** - * Emitted whenever the client joins a guild. - * @event Client#guildCreate - * @param {Guild} guild The created guild - */ - if (client.options.fetchAllMembers) await guild.members.fetch(); - client.emit(Events.GUILD_CREATE, guild); - } - } - } -} - -module.exports = GuildCreateHandler; diff --git a/src/client/websocket/packets/handlers/GuildDelete.js b/src/client/websocket/packets/handlers/GuildDelete.js deleted file mode 100644 index 58d60bc1a..000000000 --- a/src/client/websocket/packets/handlers/GuildDelete.js +++ /dev/null @@ -1,16 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildDeleteHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - client.actions.GuildDelete.handle(packet.d); - } -} - -/** - * Emitted whenever a guild kicks the client or the guild is deleted/left. - * @event Client#guildDelete - * @param {Guild} guild The guild that was deleted - */ - -module.exports = GuildDeleteHandler; diff --git a/src/client/websocket/packets/handlers/GuildEmojisUpdate.js b/src/client/websocket/packets/handlers/GuildEmojisUpdate.js deleted file mode 100644 index 2906e74fc..000000000 --- a/src/client/websocket/packets/handlers/GuildEmojisUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildEmojisUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildEmojisUpdate.handle(data); - } -} - -module.exports = GuildEmojisUpdate; diff --git a/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js b/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js deleted file mode 100644 index 5adfb5b0f..000000000 --- a/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js +++ /dev/null @@ -1,19 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class GuildIntegrationsHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); - } -} - -module.exports = GuildIntegrationsHandler; - -/** - * Emitted whenever a guild integration is updated - * @event Client#guildIntegrationsUpdate - * @param {Guild} guild The guild whose integrations were updated - */ diff --git a/src/client/websocket/packets/handlers/GuildMemberAdd.js b/src/client/websocket/packets/handlers/GuildMemberAdd.js deleted file mode 100644 index 15201b825..000000000 --- a/src/client/websocket/packets/handlers/GuildMemberAdd.js +++ /dev/null @@ -1,27 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class GuildMemberAddHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - if (guild) { - guild.memberCount++; - const member = guild.members.add(data); - if (client.ws.connection.status === Status.READY) { - client.emit(Events.GUILD_MEMBER_ADD, member); - } - } - } -} - -module.exports = GuildMemberAddHandler; - -/** - * Emitted whenever a user joins a guild. - * @event Client#guildMemberAdd - * @param {GuildMember} member The member that has joined a guild - */ diff --git a/src/client/websocket/packets/handlers/GuildMemberRemove.js b/src/client/websocket/packets/handlers/GuildMemberRemove.js deleted file mode 100644 index 6ec1bfe64..000000000 --- a/src/client/websocket/packets/handlers/GuildMemberRemove.js +++ /dev/null @@ -1,13 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); - -class GuildMemberRemoveHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildMemberRemove.handle(data); - } -} - -module.exports = GuildMemberRemoveHandler; diff --git a/src/client/websocket/packets/handlers/GuildMemberUpdate.js b/src/client/websocket/packets/handlers/GuildMemberUpdate.js deleted file mode 100644 index 901893690..000000000 --- a/src/client/websocket/packets/handlers/GuildMemberUpdate.js +++ /dev/null @@ -1,29 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class GuildMemberUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - if (guild) { - const member = guild.members.get(data.user.id); - if (member) { - const old = member._update(data); - if (client.ws.connection.status === Status.READY) { - /** - * Emitted whenever a guild member's details (e.g. role, nickname) are changed - * @event Client#guildMemberUpdate - * @param {GuildMember} oldMember The member before the update - * @param {GuildMember} newMember The member after the update - */ - client.emit(Events.GUILD_MEMBER_UPDATE, old, member); - } - } - } - } -} - -module.exports = GuildMemberUpdateHandler; diff --git a/src/client/websocket/packets/handlers/GuildMembersChunk.js b/src/client/websocket/packets/handlers/GuildMembersChunk.js deleted file mode 100644 index 4e821f5cc..000000000 --- a/src/client/websocket/packets/handlers/GuildMembersChunk.js +++ /dev/null @@ -1,28 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); -const Collection = require('../../../../util/Collection'); - -class GuildMembersChunkHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.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)); - - client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild); - - client.ws.lastHeartbeatAck = true; - } -} - -/** - * Emitted whenever a chunk of guild members is received (all members come from the same guild). - * @event Client#guildMembersChunk - * @param {Collection} members The members in the chunk - * @param {Guild} guild The guild related to the member chunk - */ - -module.exports = GuildMembersChunkHandler; diff --git a/src/client/websocket/packets/handlers/GuildRoleCreate.js b/src/client/websocket/packets/handlers/GuildRoleCreate.js deleted file mode 100644 index 8581d53f6..000000000 --- a/src/client/websocket/packets/handlers/GuildRoleCreate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildRoleCreateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildRoleCreate.handle(data); - } -} - -module.exports = GuildRoleCreateHandler; diff --git a/src/client/websocket/packets/handlers/GuildRoleDelete.js b/src/client/websocket/packets/handlers/GuildRoleDelete.js deleted file mode 100644 index 63439b0fe..000000000 --- a/src/client/websocket/packets/handlers/GuildRoleDelete.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildRoleDeleteHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildRoleDelete.handle(data); - } -} - -module.exports = GuildRoleDeleteHandler; diff --git a/src/client/websocket/packets/handlers/GuildRoleUpdate.js b/src/client/websocket/packets/handlers/GuildRoleUpdate.js deleted file mode 100644 index 6fbdc109a..000000000 --- a/src/client/websocket/packets/handlers/GuildRoleUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildRoleUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildRoleUpdate.handle(data); - } -} - -module.exports = GuildRoleUpdateHandler; diff --git a/src/client/websocket/packets/handlers/GuildUpdate.js b/src/client/websocket/packets/handlers/GuildUpdate.js deleted file mode 100644 index 70eff52c4..000000000 --- a/src/client/websocket/packets/handlers/GuildUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildUpdate.handle(data); - } -} - -module.exports = GuildUpdateHandler; diff --git a/src/client/websocket/packets/handlers/MessageCreate.js b/src/client/websocket/packets/handlers/MessageCreate.js deleted file mode 100644 index 31f5f2806..000000000 --- a/src/client/websocket/packets/handlers/MessageCreate.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageCreateHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.MessageCreate.handle(packet.d); - } -} - -module.exports = MessageCreateHandler; diff --git a/src/client/websocket/packets/handlers/MessageDelete.js b/src/client/websocket/packets/handlers/MessageDelete.js deleted file mode 100644 index 831a0ae06..000000000 --- a/src/client/websocket/packets/handlers/MessageDelete.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageDeleteHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.MessageDelete.handle(packet.d); - } -} - -module.exports = MessageDeleteHandler; diff --git a/src/client/websocket/packets/handlers/MessageDeleteBulk.js b/src/client/websocket/packets/handlers/MessageDeleteBulk.js deleted file mode 100644 index 0077dbf81..000000000 --- a/src/client/websocket/packets/handlers/MessageDeleteBulk.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageDeleteBulkHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.MessageDeleteBulk.handle(packet.d); - } -} - -module.exports = MessageDeleteBulkHandler; diff --git a/src/client/websocket/packets/handlers/MessageReactionAdd.js b/src/client/websocket/packets/handlers/MessageReactionAdd.js deleted file mode 100644 index 34e5c6138..000000000 --- a/src/client/websocket/packets/handlers/MessageReactionAdd.js +++ /dev/null @@ -1,13 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class MessageReactionAddHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const { user, reaction } = client.actions.MessageReactionAdd.handle(data); - if (reaction) client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); - } -} - -module.exports = MessageReactionAddHandler; diff --git a/src/client/websocket/packets/handlers/MessageReactionRemove.js b/src/client/websocket/packets/handlers/MessageReactionRemove.js deleted file mode 100644 index cddde7033..000000000 --- a/src/client/websocket/packets/handlers/MessageReactionRemove.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageReactionRemove extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.MessageReactionRemove.handle(data); - } -} - -module.exports = MessageReactionRemove; diff --git a/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js b/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js deleted file mode 100644 index 303da9ca0..000000000 --- a/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageReactionRemoveAll extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.MessageReactionRemoveAll.handle(data); - } -} - -module.exports = MessageReactionRemoveAll; diff --git a/src/client/websocket/packets/handlers/MessageUpdate.js b/src/client/websocket/packets/handlers/MessageUpdate.js deleted file mode 100644 index 33e45b19b..000000000 --- a/src/client/websocket/packets/handlers/MessageUpdate.js +++ /dev/null @@ -1,20 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class MessageUpdateHandler extends AbstractHandler { - handle(packet) { - const { old, updated } = this.packetManager.client.actions.MessageUpdate.handle(packet.d); - if (old && updated) { - this.packetManager.client.emit(Events.MESSAGE_UPDATE, old, updated); - } - } -} - -module.exports = MessageUpdateHandler; - -/** - * 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 - */ diff --git a/src/client/websocket/packets/handlers/PresenceUpdate.js b/src/client/websocket/packets/handlers/PresenceUpdate.js deleted file mode 100644 index 2892d44c1..000000000 --- a/src/client/websocket/packets/handlers/PresenceUpdate.js +++ /dev/null @@ -1,68 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class PresenceUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - let user = client.users.get(data.user.id); - const guild = client.guilds.get(data.guild_id); - - // Step 1 - if (!user) { - if (data.user.username) { - user = client.users.add(data.user); - } else { - return; - } - } - - const oldUser = user._update(data.user); - if (!user.equals(oldUser)) { - client.emit(Events.USER_UPDATE, oldUser, user); - } - - if (guild) { - let oldPresence = guild.presences.get(user.id); - if (oldPresence) oldPresence = oldPresence._clone(); - let member = guild.members.get(user.id); - if (!member && data.status !== 'offline') { - member = guild.members.add({ - user, - roles: data.roles, - deaf: false, - mute: false, - }); - client.emit(Events.GUILD_MEMBER_AVAILABLE, member); - } - guild.presences.add(Object.assign(data, { guild })); - if (member && client.listenerCount(Events.PRESENCE_UPDATE)) { - client.emit(Events.PRESENCE_UPDATE, oldPresence, member.presence); - } - } - } -} - -/** - * Emitted whenever a guild member's presence (e.g. status, activity) is changed. - * @event Client#presenceUpdate - * @param {?Presence} oldPresence The presence before the update, if one at all - * @param {Presence} newPresence The presence after the update - */ - -/** - * Emitted whenever a user's details (e.g. username, avatar) are changed. - * Disabling {@link Client#presenceUpdate} will cause this event to only fire - * on {@link ClientUser} update. - * @event Client#userUpdate - * @param {User} oldUser The user before the update - * @param {User} newUser The user after the update - */ - -/** - * Emitted whenever a member becomes available in a large guild. - * @event Client#guildMemberAvailable - * @param {GuildMember} member The member that became available - */ - -module.exports = PresenceUpdateHandler; diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js deleted file mode 100644 index 5b14c8339..000000000 --- a/src/client/websocket/packets/handlers/Ready.js +++ /dev/null @@ -1,41 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); -let ClientUser; - -class ReadyHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - - client.ws.heartbeat(); - - client.presence.userID = data.user.id; - if (!ClientUser) ClientUser = require('../../../../structures/ClientUser'); - const clientUser = new ClientUser(client, data.user); - client.user = clientUser; - client.readyAt = new Date(); - client.users.set(clientUser.id, clientUser); - - for (const guild of data.guilds) client.guilds.add(guild); - - const t = client.setTimeout(() => { - client.ws.connection.triggerReady(); - }, 1200 * data.guilds.length); - - client.setMaxListeners(data.guilds.length + 10); - - client.once('ready', () => { - client.setMaxListeners(10); - client.clearTimeout(t); - }); - - const ws = this.packetManager.ws; - - ws.sessionID = data.session_id; - ws._trace = data._trace; - client.emit(Events.DEBUG, `READY ${ws._trace.join(' -> ')} ${ws.sessionID}`); - ws.checkIfReady(); - } -} - -module.exports = ReadyHandler; diff --git a/src/client/websocket/packets/handlers/Resumed.js b/src/client/websocket/packets/handlers/Resumed.js deleted file mode 100644 index cd7cab770..000000000 --- a/src/client/websocket/packets/handlers/Resumed.js +++ /dev/null @@ -1,28 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class ResumedHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const ws = client.ws.connection; - - ws._trace = packet.d._trace; - - ws.status = Status.READY; - this.packetManager.handleQueue(); - - const replayed = ws.sequence - ws.closeSequence; - - ws.debug(`RESUMED ${ws._trace.join(' -> ')} | replayed ${replayed} events.`); - client.emit(Events.RESUMED, replayed); - ws.heartbeat(); - } -} - -/** - * Emitted whenever a WebSocket resumes. - * @event Client#resumed - * @param {number} replayed The number of events that were replayed - */ - -module.exports = ResumedHandler; diff --git a/src/client/websocket/packets/handlers/TypingStart.js b/src/client/websocket/packets/handlers/TypingStart.js deleted file mode 100644 index 52a0f6ba8..000000000 --- a/src/client/websocket/packets/handlers/TypingStart.js +++ /dev/null @@ -1,68 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class TypingStartHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const channel = client.channels.get(data.channel_id); - const user = client.users.get(data.user_id); - const timestamp = new Date(data.timestamp * 1000); - - if (channel && user) { - if (channel.type === 'voice') { - client.emit(Events.WARN, `Discord sent a typing packet to voice channel ${channel.id}`); - return; - } - if (channel._typing.has(user.id)) { - const typing = channel._typing.get(user.id); - typing.lastTimestamp = timestamp; - typing.resetTimeout(tooLate(channel, user)); - } else { - channel._typing.set(user.id, new TypingData(client, timestamp, timestamp, tooLate(channel, user))); - client.emit(Events.TYPING_START, channel, user); - } - } - } -} - -class TypingData { - constructor(client, since, lastTimestamp, _timeout) { - this.client = client; - this.since = since; - this.lastTimestamp = lastTimestamp; - this._timeout = _timeout; - } - - resetTimeout(_timeout) { - this.client.clearTimeout(this._timeout); - this._timeout = _timeout; - } - - get elapsedTime() { - return Date.now() - this.since; - } -} - -function tooLate(channel, user) { - return channel.client.setTimeout(() => { - channel.client.emit(Events.TYPING_STOP, channel, user, channel._typing.get(user.id)); - channel._typing.delete(user.id); - }, 6000); -} - -/** - * Emitted whenever a user starts typing in a channel. - * @event Client#typingStart - * @param {Channel} channel The channel the user started typing in - * @param {User} user The user that started typing - */ - -/** - * Emitted whenever a user stops typing in a channel. - * @event Client#typingStop - * @param {Channel} channel The channel the user stopped typing in - * @param {User} user The user that stopped typing - */ - -module.exports = TypingStartHandler; diff --git a/src/client/websocket/packets/handlers/UserUpdate.js b/src/client/websocket/packets/handlers/UserUpdate.js deleted file mode 100644 index bc34f347d..000000000 --- a/src/client/websocket/packets/handlers/UserUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class UserUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.UserUpdate.handle(data); - } -} - -module.exports = UserUpdateHandler; diff --git a/src/client/websocket/packets/handlers/VoiceServerUpdate.js b/src/client/websocket/packets/handlers/VoiceServerUpdate.js deleted file mode 100644 index 97885d6cd..000000000 --- a/src/client/websocket/packets/handlers/VoiceServerUpdate.js +++ /dev/null @@ -1,19 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -/* -{ - "token": "my_token", - "guild_id": "41771983423143937", - "endpoint": "smart.loyal.discord.gg" -} -*/ - -class VoiceServerUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.emit('self.voiceServer', data); - } -} - -module.exports = VoiceServerUpdate; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 2d1bc3c2c..2e2ec97bb 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -6,6 +6,7 @@ const Messages = { TOKEN_INVALID: 'An invalid token was provided.', TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.', + WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.', WS_CONNECTION_TIMEOUT: 'The connection to the gateway timed out.', WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`, diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index a43bb7e92..3b25c1d0a 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -29,7 +29,7 @@ class Shard extends EventEmitter { this.manager = manager; /** - * ID of the shard + * ID of the shard in the manager * @type {number} */ this.id = id; @@ -51,8 +51,10 @@ class Shard extends EventEmitter { * @type {Object} */ this.env = Object.assign({}, process.env, { - SHARD_ID: this.id, - SHARD_COUNT: this.manager.totalShards, + SHARDING_MANAGER: true, + SHARDS: this.id, + TOTAL_SHARD_COUNT: this.manager.totalShards, + DISCORD_TOKEN: this.manager.token, }); /** diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 4aa35a3b2..9a2a746bc 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -49,7 +49,7 @@ class ShardClientUtil { * @readonly */ get id() { - return this.client.options.shardId; + return this.client.options.shards; } /** diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 7cedc37ef..9bfce6fe2 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -27,7 +27,8 @@ class ShardingManager extends EventEmitter { /** * @param {string} file Path to your shard script file * @param {Object} [options] Options for the sharding manager - * @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto" + * @param {string|number[]} [options.totalShards='auto'] Number of total shards of all shard managers or "auto" + * @param {string|number[]} [options.shardList='auto'] List of shards to spawn or "auto" * @param {ShardingManagerMode} [options.mode='process'] Which mode to use for shards * @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting * @param {string[]} [options.shardArgs=[]] Arguments to pass to the shard script when spawning @@ -58,16 +59,33 @@ class ShardingManager extends EventEmitter { if (!stats.isFile()) throw new Error('CLIENT_INVALID_OPTION', 'File', 'a file'); /** - * Amount of shards that this manager is going to spawn - * @type {number|string} + * List of shards this sharding manager spawns + * @type {string|number[]} */ - this.totalShards = options.totalShards; + this.shardList = options.shardList || 'auto'; + if (this.shardList !== 'auto') { + if (!Array.isArray(this.shardList)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array.'); + } + this.shardList = [...new Set(this.shardList)]; + if (this.shardList.length < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardList', 'at least 1 ID.'); + if (this.shardList.some(shardID => typeof shardID !== 'number' || isNaN(shardID) || + !Number.isInteger(shardID) || shardID < 0)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array of postive integers.'); + } + } + + /** + * Amount of shards that all sharding managers spawn in total + * @type {number} + */ + this.totalShards = options.totalShards || 'auto'; if (this.totalShards !== 'auto') { if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.'); } if (this.totalShards < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.'); - if (this.totalShards !== Math.floor(this.totalShards)) { + if (!Number.isInteger(this.totalShards)) { throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.'); } } @@ -150,21 +168,31 @@ class ShardingManager extends EventEmitter { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.'); } if (amount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.'); - if (amount !== Math.floor(amount)) { + if (!Number.isInteger(amount)) { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.'); } } // Make sure this many shards haven't already been spawned if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size); - this.totalShards = amount; + if (this.shardList === 'auto' || this.totalShards === 'auto' || this.totalShards !== amount) { + this.shardList = [...Array(amount).keys()]; + } + if (this.totalShards === 'auto' || this.totalShards !== amount) { + this.totalShards = amount; + } + + if (this.shardList.some(shardID => shardID >= amount)) { + throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', + 'bigger than the highest shardID in the shardList option.'); + } // Spawn the shards - for (let s = 1; s <= amount; s++) { + for (const shardID of this.shardList) { const promises = []; - const shard = this.createShard(); + const shard = this.createShard(shardID); promises.push(shard.spawn(waitForReady)); - if (delay > 0 && s !== amount) promises.push(Util.delayFor(delay)); + if (delay > 0 && this.shards.size !== this.shardList.length - 1) promises.push(Util.delayFor(delay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop } diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 44494be49..6f95aec92 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -189,7 +189,7 @@ class GuildMemberStore extends DataStore { resolve(query || limit ? new Collection() : this); return; } - this.guild.client.ws.send({ + this.guild.shard.send({ op: OPCodes.REQUEST_GUILD_MEMBERS, d: { guild_id: this.guild.id, diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 90d1cb4a2..06924589d 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -11,7 +11,15 @@ class ClientPresence extends Presence { async set(presence) { const packet = await this._parse(presence); this.patch(packet); - this.client.ws.send({ op: OPCodes.STATUS_UPDATE, d: packet }); + if (typeof presence.shardID === 'undefined') { + this.client.ws.broadcast({ op: OPCodes.STATUS_UPDATE, d: packet }); + } else if (Array.isArray(presence.shardID)) { + for (const shardID of presence.shardID) { + this.client.ws.shards[shardID].send({ op: OPCodes.STATUS_UPDATE, d: packet }); + } + } else { + this.client.ws.shards[presence.shardID].send({ op: OPCodes.STATUS_UPDATE, d: packet }); + } return this; } diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 9827e16de..1b8d90d98 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -15,14 +15,14 @@ class ClientUser extends Structures.get('User') { */ this.verified = data.verified; - this._typing = new Map(); - /** * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account * @type {?boolean} */ this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; + this._typing = new Map(); + if (data.token) this.client.token = data.token; } @@ -39,7 +39,9 @@ class ClientUser extends Structures.get('User') { return this.client.api.users('@me').patch({ data }) .then(newData => { this.client.token = newData.token; - return this.client.actions.UserUpdate.handle(newData).updated; + const { updated } = this.client.actions.UserUpdate.handle(newData); + if (updated) return updated; + return this; }); } @@ -84,6 +86,7 @@ class ClientUser extends Structures.get('User') { * @property {string} [activity.name] Name of the activity * @property {ActivityType|number} [activity.type] Type of the activity * @property {string} [activity.url] Stream url + * @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on */ /** @@ -112,6 +115,7 @@ class ClientUser extends Structures.get('User') { /** * Sets the status of the client user. * @param {PresenceStatus} status Status to change to + * @param {?number|number[]} [shardID] Shard ID(s) to have the activity set on * @returns {Promise} * @example * // Set the client user's status @@ -119,8 +123,8 @@ class ClientUser extends Structures.get('User') { * .then(console.log) * .catch(console.error); */ - setStatus(status) { - return this.setPresence({ status }); + setStatus(status, shardID) { + return this.setPresence({ status, shardID }); } /** @@ -129,6 +133,7 @@ class ClientUser extends Structures.get('User') { * @type {Object} * @property {string} [url] Twitch stream URL * @property {ActivityType|number} [type] Type of the activity + * @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on */ /** @@ -143,10 +148,10 @@ class ClientUser extends Structures.get('User') { * .catch(console.error); */ setActivity(name, options = {}) { - if (!name) return this.setPresence({ activity: null }); + if (!name) return this.setPresence({ activity: null, shardID: options.shardID }); const activity = Object.assign({}, options, typeof name === 'object' ? name : { name }); - return this.setPresence({ activity }); + return this.setPresence({ activity, shardID: activity.shardID }); } /** diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 493c0df81..ffef8578b 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -74,6 +74,21 @@ class Guild extends Base { this._patch(data); if (!data.channels) this.available = false; } + + /** + * The id of the shard this Guild belongs to. + * @type {number} + */ + this.shardID = data.shardID; + } + + /** + * The Shard this Guild belongs to. + * @type {WebSocketShard} + * @readonly + */ + get shard() { + return this.client.ws.shards[this.shardID]; } /* eslint-disable complexity */ diff --git a/src/util/Constants.js b/src/util/Constants.js index 32413c257..e956c8a44 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -5,8 +5,10 @@ const browser = exports.browser = typeof window !== 'undefined'; /** * Options for a client. * @typedef {Object} ClientOptions - * @property {number} [shardId=0] ID of the shard to run - * @property {number} [shardCount=0] Total number of shards + * @property {number|number[]} [shards=0] ID of the shard to run, or an array of shard IDs + * @property {number} [shardCount=1] Total number of shards that will be spawned by this Client + * @property {number} [totalShardCount=1] The total amount of shards used by all processes of this bot + * (e.g. recommended shard count, shard count of the ShardingManager) * @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel * (-1 or Infinity for unlimited - don't do this without message sweeping, otherwise memory usage will climb * indefinitely) @@ -33,9 +35,9 @@ const browser = exports.browser = typeof window !== 'undefined'; * @property {HTTPOptions} [http] HTTP options */ exports.DefaultOptions = { - shardId: 0, - shardCount: 0, - internalSharding: false, + shards: 0, + shardCount: 1, + totalShardCount: 1, messageCacheMaxSize: 200, messageCacheLifetime: 0, messageSweepInterval: 0, @@ -86,10 +88,10 @@ exports.UserAgent = browser ? null : `DiscordBot (${Package.homepage.split('#')[0]}, ${Package.version}) Node.js/${process.version}`; exports.WSCodes = { - 1000: 'Connection gracefully closed', - 4004: 'Tried to identify with an invalid token', - 4010: 'Sharding data provided was invalid', - 4011: 'Shard would be on too many guilds if connected', + 1000: 'WS_CLOSE_REQUESTED', + 4004: 'TOKEN_INVALID', + 4010: 'SHARDING_INVALID', + 4011: 'SHARDING_REQUIRED', }; const AllowedImageFormats = [ @@ -253,6 +255,9 @@ exports.Events = { ERROR: 'error', WARN: 'warn', DEBUG: 'debug', + SHARD_READY: 'shardReady', + INVALIDATED: 'invalidated', + RAW: 'raw', }; /** diff --git a/test/shard.js b/test/shard.js index 5f5f17c49..5c185b5fa 100644 --- a/test/shard.js +++ b/test/shard.js @@ -2,7 +2,7 @@ const Discord = require('../'); const { token } = require('./auth.json'); const client = new Discord.Client({ - shardId: process.argv[2], + shardID: process.argv[2], shardCount: process.argv[3], }); @@ -20,8 +20,8 @@ client.on('message', msg => { process.send(123); client.on('ready', () => { - console.log('Ready', client.options.shardId); - if (client.options.shardId === 0) + console.log('Ready', client.options.shardID); + if (client.options.shardID === 0) setTimeout(() => { console.log('kek dying'); client.destroy(); diff --git a/test/tester1000.js b/test/tester1000.js index 99209c71a..d726188c5 100644 --- a/test/tester1000.js +++ b/test/tester1000.js @@ -4,7 +4,9 @@ const { token, prefix, owner } = require('./auth.js'); // eslint-disable-next-line no-console const log = (...args) => console.log(process.uptime().toFixed(3), ...args); -const client = new Discord.Client(); +const client = new Discord.Client({ + shardCount: 2, +}); client.on('debug', log); client.on('ready', () => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 2672d1845..ddda0bfef 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -80,17 +80,20 @@ declare module 'discord.js' { export class BaseClient extends EventEmitter { constructor(options?: ClientOptions); - private _intervals: Set; private _timeouts: Set; + private _intervals: Set; + private _immediates: Set; private readonly api: object; private rest: object; public options: ClientOptions; public clearInterval(interval: NodeJS.Timer): void; public clearTimeout(timeout: NodeJS.Timer): void; + public clearImmediate(timeout: NodeJS.Immediate): void; public destroy(): void; public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; + public setImmediate(fn: Function, delay: number, ...args: any[]): NodeJS.Immediate; public toJSON(...props: { [key: string]: boolean | string }[]): object; } @@ -133,31 +136,26 @@ declare module 'discord.js' { export class Client extends BaseClient { constructor(options?: ClientOptions); - private readonly _pingTimestamp: number; private actions: object; - private manager: ClientManager; private voice: object; - private ws: object; private _eval(script: string): any; - private _pong(startTime: number): void; private _validateOptions(options?: ClientOptions): void; public broadcasts: VoiceBroadcast[]; public channels: ChannelStore; public readonly emojis: GuildEmojiStore; public guilds: GuildStore; - public readonly ping: number; - public pings: number[]; - public readyAt: Date; + public readyAt: Date | null; public readonly readyTimestamp: number; public shard: ShardClientUtil; - public readonly status: Status; public token: string; public readonly uptime: number; - public user: ClientUser; + public user: ClientUser | null; public users: UserStore; public readonly voiceConnections: Collection; + public ws: WebSocketManager; public createVoiceBroadcast(): VoiceBroadcast; + public destroy(): void; public fetchApplication(): Promise; public fetchInvite(invite: InviteResolvable): Promise; public fetchVoiceRegions(): Promise>; @@ -171,7 +169,7 @@ declare module 'discord.js' { public on(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; public on(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; public on(event: 'debug' | 'warn', listener: (info: string) => void): this; - public on(event: 'disconnect', listener: (event: any) => void): this; + public on(event: 'disconnect', listener: (event: any, shardID: number) => void): this; public on(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; public on(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public on(event: 'error', listener: (error: Error) => void): this; @@ -189,10 +187,12 @@ declare module 'discord.js' { public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; - public on(event: 'ready' | 'reconnecting', listener: () => void): this; - public on(event: 'resumed', listener: (replayed: number) => void): this; + public on(event: 'ready', listener: () => void): this; + public on(event: 'reconnecting', listener: (shardID: number) => void): this; + public on(event: 'resumed', listener: (replayed: number, shardID: number) => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; + public on(event: 'shardReady', listener: (shardID: number) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; @@ -203,7 +203,7 @@ declare module 'discord.js' { public once(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; public once(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; public once(event: 'debug' | 'warn', listener: (info: string) => void): this; - public once(event: 'disconnect', listener: (event: any) => void): this; + public once(event: 'disconnect', listener: (event: any, shardID: number) => void): this; public once(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; public once(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public once(event: 'error', listener: (error: Error) => void): this; @@ -221,10 +221,12 @@ declare module 'discord.js' { public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; - public once(event: 'ready' | 'reconnecting', listener: () => void): this; - public once(event: 'resumed', listener: (replayed: number) => void): this; + public once(event: 'ready', listener: () => void): this; + public once(event: 'reconnecting', listener: (shardID: number) => void): this; + public once(event: 'resumed', listener: (replayed: number, shardID: number) => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; + public once(event: 'shardReady', listener: (shardID: number) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; @@ -252,18 +254,11 @@ declare module 'discord.js' { public toString(): string; } - class ClientManager { - constructor(client: Client); - public client: Client; - public heartbeatInterval: number; - public readonly status: number; - public connectToWebSocket(token: string, resolve: Function, reject: Function): void; - } - export interface ActivityOptions { name?: string; url?: string; type?: ActivityType | number; + shardID?: number | number[]; } export class ClientUser extends User { @@ -275,7 +270,7 @@ declare module 'discord.js' { public setAFK(afk: boolean): Promise; public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise; public setPresence(data: PresenceData): Promise; - public setStatus(status: PresenceStatus): Promise; + public setStatus(status: PresenceStatus, shardID?: number | number[]): Promise; public setUsername(username: string): Promise; } @@ -1289,6 +1284,31 @@ declare module 'discord.js' { constructor(id: string, token: string, options?: ClientOptions); } + export class WebSocketManager { + constructor(client: Client); + public readonly client: Client; + public gateway: string | undefined; + public readonly ping: number; + public shards: WebSocketShard[]; + public sessionStartLimit: { total: number; remaining: number; reset_after: number; }; + public status: Status; + public broadcast(packet: any): void; + } + + export class WebSocketShard extends EventEmitter { + constructor(manager: WebSocketManager, id: number, oldShard?: WebSocketShard); + public id: number; + public readonly ping: number; + public pings: number[]; + public status: Status; + public manager: WebSocketManager; + public send(data: object): void; + + public on(event: 'ready', listener: () => void): this; + + public once(event: 'ready', listener: () => void): this; + } + //#endregion //#region Stores @@ -1572,9 +1592,9 @@ declare module 'discord.js' { }; type ClientOptions = { - presence?: PresenceData; - shardId?: number; + shards?: number | number[]; shardCount?: number; + totalShardCount?: number; messageCacheMaxSize?: number; messageCacheLifetime?: number; messageSweepInterval?: number; @@ -1582,7 +1602,9 @@ declare module 'discord.js' { disableEveryone?: boolean; restWsBridgeTimeout?: number; restTimeOffset?: number; - retryLimit?: number, + restSweepInterval?: number; + retryLimit?: number; + presence?: PresenceData; disabledEvents?: WSEventType[]; ws?: WebSocketOptions; http?: HTTPOptions; @@ -1982,7 +2004,8 @@ declare module 'discord.js' { name?: string; type?: ActivityType | number; url?: string; - } + }; + shardID?: number | number[]; }; type PresenceResolvable = Presence | UserResolvable | Snowflake;