feat: Internal sharding (#2902)

* internal sharding

* ready event

* the square deal

* the new deal

* the second new deal

* add actual documentation

* the new freedom

* the great society

* federal intervention

* some of requested changes

* i ran out of things to call these

* destroy this

* fix: Client#uptime went missing

* fix(Client): destroy the client on login failure

This may happen duo invalid sharding config / invalid token / user requested destroy

* fix(Client): reject login promise when the client is destroyed before ready

* fix(WebSocketManager): remove redundancy in destroy method (#2491)

* typo(ErrorMessages): duo -> duo to

* typo(ErrorMessages): duo -> due

* fix: docs and options

* docs(WebSocketManager): WebSockethard -> WebSocketShard (#2502)

* fix(ClientUser): lazily load to account for extended user structure (#2501)

* docs(WebSocketShard): document class to make it visible in documentation (#2504)

* fix: WebSocketShard#reconnect

* fix: presenceUpdate & userUpdate
* presenceUpdate wasn't really being handled at all
* userUpdate handled incorrectly because as of v7 in the Discord API, it comes inside presenceUpdate

* re-add raw event

* member is now part of message create payload

* feat: Add functionality to support multiple servers with different shards (#2395)

* Added functionallity to spawn multiple sharding managers due to adding start and end shards

* Small fixes and limiting shard amount to max recommended

* Forgot a check in spawn()

* Fixed indentation

* Removed optiosn object documentation for totalShards

* More fixes and a check that the startShard + amount doesnt go over the recommended shard amount

* fix getting max recommended

* Removed async from constructor (my fault)

* Changed start and end shard to a shardList or "auto" + fixed some brainfarts with isNaN

* Changed the loop and totalShard count calculation

* shards are actually 0 based

* Fixed a problem with the gateway and handled some range errors and type errors

* Changed Number.isNan to isNaN and changed a few Integer checks to use Number.isInteger

* Added check if shardList contains smth greater than totalShards; made spawn use totalShards again; shardList will be ignored and rebuild if totalShards is 'auto'; fixed docs

* ShardingManager#spawn now uses a for..of loop; fixed the if statement inside the new for..of loop to still work as intended; made the totalShards be set to a new amount if smth manual is put into ShardingManager#spawn just like before; Fixed some spelling

* internal sharding

* ready event

* the square deal

* the new deal

* the second new deal

* add actual documentation

* the new freedom

* the great society

* federal intervention

* some of requested changes

* i ran out of things to call these

* destroy this

* fix: Client#uptime went missing

* fix(Client): destroy the client on login failure

This may happen duo invalid sharding config / invalid token / user requested destroy

* fix(Client): reject login promise when the client is destroyed before ready

* fix(WebSocketManager): remove redundancy in destroy method (#2491)

* typo(ErrorMessages): duo -> duo to

* typo(ErrorMessages): duo -> due

* fix: docs and options

* docs(WebSocketManager): WebSockethard -> WebSocketShard (#2502)

* fix(ClientUser): lazily load to account for extended user structure (#2501)

* docs(WebSocketShard): document class to make it visible in documentation (#2504)

* fix: WebSocketShard#reconnect

* fix: presenceUpdate & userUpdate
* presenceUpdate wasn't really being handled at all
* userUpdate handled incorrectly because as of v7 in the Discord API, it comes inside presenceUpdate

* Internal Sharding adaptation

Adapted to internal sharding
Fixed a bug where non ready invalidated sessions wouldnt respawn

* Fixed shardCount not retrieving

* Fixing style

removed unnecessary parenthesis

* Fixing and rebasing

lets hope i didnt dun hecklered it

* Fixing my own retardation

* Thanks git rebase

* fix: assigning member in message create payload

* fix: resumes

* fix: IS wont give up reconnecting now

* docs: add missing docs mostly

* fix: found lost methods

* fix: WebSocketManager#broadcast check if shard exists

* fix: ShardClientUtil#id returning undefined

* feat: handle new session rate limits (#2796)

* feat: handle new session rate limits

* i have no idea what i was doing last night

* fix if statement weirdness

* fix: re-add presence parsing from ClientOptions (#2893)

* resolve conflicts

* typings: missing typings

* re-add missing linter rule

* fix: replacing ClientUser wrongly

* address unecessary performance waste

* docs: missing disconnect event

* fix(typings): Fix 2 issues with typings (#2909)

* (Typings) Update typings to reflect current ClientOptions

* fix(Typings) fixes a bug with Websockets and DOM Types

* fix travis

* feat: allow setting presence per shard

* add WebSocketManager#shardX events

* adjust typings, docs and performance issues

* readjust shard events, now provide shardId parameter instead

* fix: ready event should check shardCount, not actualShardCount

* fix: re-add replayed parameter of Client#resume

* fix(Sharding): fixes several things in Internal Sharding (#2914)

* fix(Sharding) fixes several things in Internal Sharding

* add default value for shards property

* better implement checking for shards array

* fix travis & some casing

* split shard count into 2 words

* update to latest Internal Sharding, fix requested changes

* make sure totalShardCount is a number

* fix comment

* fix small typo

* dynamically set totalShardCount if either shards or shardCount is provided

* consistency: rename shardID to shardId

* remove Client#shardIds

* fix: typo in GuildIntegrationsUpdate handler

* fix: incorrect packet data being passed in some events (#2919)

* fix: edgecase of ShardingManager and totalShardCount (#2918)

* fix: Client#userUpdate being passed wrong parameter
and fix a potential edgecase of returning null in ClientUser#edit from this event

* fix consistency and typings issues

* consistency: shardId instances renamed to shardID

* typings: fix typings regarding WebSocket

* style(.eslintrc): remove additional whitespace

* fix(Client): remove ondisconnect handler on timeout

* docs(BaseClient): fix typo of Immediate

* nitpick: typings, private fields and methods

* typo: improve grammar a bit

* fix: error assigning client in WebSocketManager

* typo: actually spell milliseconds properly
This commit is contained in:
Isabella
2018-11-03 13:21:23 -05:00
committed by GitHub
parent 18f065867c
commit f3cad81f53
95 changed files with 1145 additions and 1393 deletions

View File

@@ -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"
},

View File

@@ -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<Immediate>}
* @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);
}

View File

@@ -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<Snowflake, VoiceConnection>}
@@ -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');
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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
*/

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
};
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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,
});

View File

@@ -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<WebSocketShard|number|string>}
* @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();
}
}
}

View File

@@ -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;

View File

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

View File

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

View File

@@ -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);
}
};

View File

@@ -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);
}
};

View File

@@ -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);
};

View File

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

View File

@@ -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);
}
}
};

View File

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

View File

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

View File

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

View File

@@ -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<Snowflake, GuildMember>} members The members in the chunk
* @param {Guild} guild The guild related to the member chunk
*/
client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild);
};

View File

@@ -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);
}
}
};

View File

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

View File

@@ -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);
}
}
}
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
module.exports = (client, packet) => {
client.actions.GuildSync.handle(packet.d);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
};

View File

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

View File

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

View File

@@ -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);
}
};

View File

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

View File

@@ -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();
};

View File

@@ -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);
};

View File

@@ -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);
}
};

View File

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

View File

@@ -0,0 +1,3 @@
module.exports = (client, packet) => {
client.emit('self.voiceServer', packet.d);
};

View File

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

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,11 +0,0 @@
class AbstractHandler {
constructor(packetManager) {
this.packetManager = packetManager;
}
handle(packet) {
return packet;
}
}
module.exports = AbstractHandler;

View File

@@ -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
*/

View File

@@ -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;

View File

@@ -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.
* <warn>The `time` parameter will be a Unix Epoch Date object when there are no pins left.</warn>
* @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
*/

View File

@@ -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
*/

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<Snowflake, GuildMember>} members The members in the chunk
* @param {Guild} guild The guild related to the member chunk
*/
module.exports = GuildMembersChunkHandler;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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.
* <info>Disabling {@link Client#presenceUpdate} will cause this event to only fire
* on {@link ClientUser} update.</info>
* @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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}`,

View File

@@ -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,
});
/**

View File

@@ -49,7 +49,7 @@ class ShardClientUtil {
* @readonly
*/
get id() {
return this.client.options.shardId;
return this.client.options.shards;
}
/**

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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<Presence>}
* @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 });
}
/**

View File

@@ -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 */

View File

@@ -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',
};
/**

View File

@@ -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();

View File

@@ -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', () => {

81
typings/index.d.ts vendored
View File

@@ -80,17 +80,20 @@ declare module 'discord.js' {
export class BaseClient extends EventEmitter {
constructor(options?: ClientOptions);
private _intervals: Set<NodeJS.Timer>;
private _timeouts: Set<NodeJS.Timer>;
private _intervals: Set<NodeJS.Timer>;
private _immediates: Set<NodeJS.Immediate>;
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<Snowflake, VoiceConnection>;
public ws: WebSocketManager;
public createVoiceBroadcast(): VoiceBroadcast;
public destroy(): void;
public fetchApplication(): Promise<ClientApplication>;
public fetchInvite(invite: InviteResolvable): Promise<Invite>;
public fetchVoiceRegions(): Promise<Collection<string, VoiceRegion>>;
@@ -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<Presence>;
public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise<ClientUser>;
public setPresence(data: PresenceData): Promise<Presence>;
public setStatus(status: PresenceStatus): Promise<Presence>;
public setStatus(status: PresenceStatus, shardID?: number | number[]): Promise<Presence>;
public setUsername(username: string): Promise<ClientUser>;
}
@@ -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;