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

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

@@ -1,45 +0,0 @@
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 guild = client.guilds.get(data.guild_id);
if (guild) {
// Update the state
const oldState = guild.voiceStates.has(data.user_id) ?
guild.voiceStates.get(data.user_id)._clone() :
new VoiceState(guild, { user_id: data.user_id });
const newState = guild.voiceStates.add(data);
// Get the member
let member = guild.members.get(data.user_id);
if (member && data.member) {
member._patch(data.member);
} else if (data.member && data.member.user && data.member.joined_at) {
member = guild.members.add(data.member);
}
// Emit event
if (member && member.user.id === client.user.id && data.channel_id) {
client.emit('self.voiceStateUpdate', data);
}
client.emit(Events.VOICE_STATE_UPDATE, oldState, newState);
}
}
}
/**
* Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes.
* @event Client#voiceStateUpdate
* @param {?VoiceState} oldState The voice state before the update
* @param {VoiceState} newState The voice state after the update
*/
module.exports = VoiceStateUpdateHandler;

View File

@@ -1,19 +0,0 @@
const AbstractHandler = require('./AbstractHandler');
const { Events } = require('../../../../util/Constants');
class WebhooksUpdate extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
const channel = client.channels.get(data.channel_id);
if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel);
}
}
/**
* Emitted whenever a guild text channel has its webhooks changed.
* @event Client#webhookUpdate
* @param {TextChannel} channel The channel that had a webhook update
*/
module.exports = WebhooksUpdate;