mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
Rewrite WebSocket internals (#1410)
* Start rewriting Manager and Connection * more stuff * stuff * Fix ready bug * some stuff i forgot * fix some stuff * add stupid heartbeat ack like seriously who cares * woo! * fix a bug * rate limit the dumb websocket * stuff * hdocs * Docs * Remove ClientManager#setupKeepAlive as it is now redundant * Change Client._pingTimestamp to a getter that fetches the timestamp from the WebSocketConnection * are you happy now eslint smh * make gus happy * Add CloseEvent external doc * Make sure to emit 'reconnecting' when actually reconnecting * ffs * Fix RESUME logic * Add heartbeat ack debug messages, including latency data * Dumb stuff for Gus * thx eslint * more dumb stuff * more dumb crap smh gus i h8 u * moar messages * fix for using wrong status, causing certain events not to be fired (#1422)
This commit is contained in:
@@ -156,13 +156,6 @@ class Client extends EventEmitter {
|
||||
*/
|
||||
this.pings = [];
|
||||
|
||||
/**
|
||||
* Timestamp of the latest ping's start time
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this._pingTimestamp = 0;
|
||||
|
||||
/**
|
||||
* Timeouts set by {@link Client#setTimeout} that are still active
|
||||
* @type {Set<Timeout>}
|
||||
@@ -182,6 +175,15 @@ class Client extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp of the latest ping's start time
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
get _pingTimestamp() {
|
||||
return this.ws.connection ? this.ws.connection.lastPingTimestamp : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current status of the client's connection to Discord
|
||||
* @type {?number}
|
||||
|
||||
@@ -15,7 +15,7 @@ class ClientDataManager {
|
||||
}
|
||||
|
||||
get pastReady() {
|
||||
return this.client.ws.status === Constants.Status.READY;
|
||||
return this.client.ws.connection.status === Constants.Status.READY;
|
||||
}
|
||||
|
||||
newGuild(data) {
|
||||
|
||||
@@ -20,6 +20,14 @@ class ClientManager {
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The status of the client
|
||||
* @type {number}
|
||||
*/
|
||||
get status() {
|
||||
return this.connection ? this.connection.status : Constants.Status.IDLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects the Client to the WebSocket
|
||||
* @param {string} token The authorization token
|
||||
@@ -47,17 +55,9 @@ class ClientManager {
|
||||
}, reject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a keep-alive interval to keep the Client's connection valid
|
||||
* @param {number} time The interval in milliseconds at which heartbeat packets should be sent
|
||||
*/
|
||||
setupKeepAlive(time) {
|
||||
this.heartbeatInterval = time;
|
||||
this.client.setInterval(() => this.client.ws.heartbeat(true), time);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.client.ws.destroy();
|
||||
this.client.rest.destroy();
|
||||
if (!this.client.user) return Promise.resolve();
|
||||
if (this.client.user.bot) {
|
||||
this.client.token = null;
|
||||
|
||||
@@ -15,6 +15,12 @@ class RESTManager {
|
||||
this.globallyRateLimited = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
for (const handlerID in this.handlers) {
|
||||
this.handlers[handlerID].destroy();
|
||||
}
|
||||
}
|
||||
|
||||
push(handler, apiRequest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
handler.push({
|
||||
|
||||
@@ -45,6 +45,10 @@ class RequestHandler {
|
||||
* Attempts to get this RequestHandler to process its current queue
|
||||
*/
|
||||
handle() {} // eslint-disable-line no-empty-function
|
||||
|
||||
destroy() {
|
||||
this.queue = [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RequestHandler;
|
||||
|
||||
@@ -48,39 +48,42 @@ class SequentialRequestHandler extends RequestHandler {
|
||||
execute(item) {
|
||||
this.busy = true;
|
||||
return new Promise(resolve => {
|
||||
item.request.gen().end((err, res) => {
|
||||
if (res && res.headers) {
|
||||
this.requestLimit = Number(res.headers['x-ratelimit-limit']);
|
||||
this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
|
||||
this.requestRemaining = Number(res.headers['x-ratelimit-remaining']);
|
||||
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
|
||||
}
|
||||
if (err) {
|
||||
if (err.status === 429) {
|
||||
this.queue.unshift(item);
|
||||
this.restManager.client.setTimeout(() => {
|
||||
this.globalLimit = false;
|
||||
resolve();
|
||||
}, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset);
|
||||
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
|
||||
} else {
|
||||
item.reject(err);
|
||||
resolve(err);
|
||||
item.request
|
||||
.gen()
|
||||
.on('error', e => item.reject(e))
|
||||
.end((err, res) => {
|
||||
if (res && res.headers) {
|
||||
this.requestLimit = Number(res.headers['x-ratelimit-limit']);
|
||||
this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
|
||||
this.requestRemaining = Number(res.headers['x-ratelimit-remaining']);
|
||||
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
|
||||
}
|
||||
} else {
|
||||
this.globalLimit = false;
|
||||
const data = res && res.body ? res.body : {};
|
||||
item.resolve(data);
|
||||
if (this.requestRemaining === 0) {
|
||||
this.restManager.client.setTimeout(
|
||||
if (err) {
|
||||
if (err.status === 429) {
|
||||
this.queue.unshift(item);
|
||||
this.restManager.client.setTimeout(() => {
|
||||
this.globalLimit = false;
|
||||
resolve();
|
||||
}, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset);
|
||||
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
|
||||
} else {
|
||||
item.reject(err);
|
||||
resolve(err);
|
||||
}
|
||||
} else {
|
||||
this.globalLimit = false;
|
||||
const data = res && res.body ? res.body : {};
|
||||
item.resolve(data);
|
||||
if (this.requestRemaining === 0) {
|
||||
this.restManager.client.setTimeout(
|
||||
() => resolve(data),
|
||||
this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset
|
||||
);
|
||||
} else {
|
||||
resolve(data);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const browser = require('os').platform() === 'browser';
|
||||
const EventEmitter = require('events');
|
||||
const Constants = require('../../util/Constants');
|
||||
const zlib = require('zlib');
|
||||
const PacketManager = require('./packets/WebSocketPacketManager');
|
||||
const erlpack = (function findErlpack() {
|
||||
try {
|
||||
const e = require('erlpack');
|
||||
@@ -26,45 +28,137 @@ const WebSocket = (function findWebSocket() {
|
||||
*/
|
||||
class WebSocketConnection extends EventEmitter {
|
||||
/**
|
||||
* @param {WebSocketManager} manager the WebSocket manager
|
||||
* @param {string} gateway Websocket gateway to connect to
|
||||
*/
|
||||
constructor(gateway) {
|
||||
super(gateway);
|
||||
this.ws = new WebSocket(gateway);
|
||||
if (browser) this.ws.binaryType = 'arraybuffer';
|
||||
this.ws.onmessage = this.eventMessage.bind(this);
|
||||
this.ws.onopen = this.emit.bind(this, 'open');
|
||||
this.ws.onclose = this.emit.bind(this, 'close');
|
||||
this.ws.onerror = this.emit.bind(this, 'error');
|
||||
constructor(manager, gateway) {
|
||||
super();
|
||||
/**
|
||||
* WebSocket Manager of this connection
|
||||
* @type {WebSocketManager}
|
||||
*/
|
||||
this.manager = manager;
|
||||
/**
|
||||
* Client this belongs to
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = manager.client;
|
||||
/**
|
||||
* WebSocket connection itself
|
||||
* @type {WebSocket}
|
||||
*/
|
||||
this.ws = null;
|
||||
/**
|
||||
* Current sequence of the WebSocket
|
||||
* @type {number}
|
||||
*/
|
||||
this.sequence = -1;
|
||||
/**
|
||||
* Current status of the client
|
||||
* @type {number}
|
||||
*/
|
||||
this.status = Constants.Status.IDLE;
|
||||
/**
|
||||
* Packet Manager of the connection
|
||||
* @type {WebSocketPacketManager}
|
||||
*/
|
||||
this.packetManager = new PacketManager(this);
|
||||
/**
|
||||
* Last time a ping was sent (a timestamp)
|
||||
* @type {number}
|
||||
*/
|
||||
this.lastPingTimestamp = 0;
|
||||
/**
|
||||
* Contains the rate limit queue and metadata
|
||||
* @type {Object}
|
||||
*/
|
||||
this.ratelimit = {
|
||||
queue: [],
|
||||
remaining: 120,
|
||||
resetTime: -1,
|
||||
};
|
||||
this.connect(gateway);
|
||||
/**
|
||||
* Events that are disabled (will not be processed)
|
||||
* @type {Object}
|
||||
*/
|
||||
this.disabledEvents = {};
|
||||
/**
|
||||
* Sequence on WebSocket close
|
||||
* @type {number}
|
||||
*/
|
||||
this.closeSequence = 0;
|
||||
for (const event of this.client.options.disabledEvents) this.disabledEvents[event] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the websocket gets a message
|
||||
* @param {Object} event Close event object
|
||||
* @returns {Promise<boolean>}
|
||||
* Causes the client to be marked as ready and emits the ready event
|
||||
* @returns {void}
|
||||
*/
|
||||
eventMessage(event) {
|
||||
try {
|
||||
const data = this.unpack(event.data);
|
||||
this.emit('packet', data);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (this.listenerCount('decodeError')) this.emit('decodeError', err);
|
||||
return false;
|
||||
triggerReady() {
|
||||
if (this.status === Constants.Status.READY) {
|
||||
this.debug('Tried to mark self as ready, but already ready');
|
||||
return;
|
||||
}
|
||||
this.status = Constants.Status.READY;
|
||||
this.client.emit(Constants.Events.READY);
|
||||
this.packetManager.handleQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data over the websocket
|
||||
* @param {string|Buffer} data Data to send
|
||||
* Checks whether the client is ready to be marked as ready
|
||||
* @returns {void}
|
||||
*/
|
||||
send(data) {
|
||||
this.ws.send(this.pack(data));
|
||||
checkIfReady() {
|
||||
if (this.status === Constants.Status.READY || this.status === Constants.Status.NEARLY) return false;
|
||||
let unavailableGuilds = 0;
|
||||
for (const guild of this.client.guilds.values()) {
|
||||
if (!guild.available) unavailableGuilds++;
|
||||
}
|
||||
if (unavailableGuilds === 0) {
|
||||
this.status = Constants.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.fetchMembers());
|
||||
Promise.all(promises)
|
||||
.then(() => this.triggerReady())
|
||||
.catch(e => {
|
||||
this.debug(`Failed to fetch all members before ready! ${e}`);
|
||||
this.triggerReady();
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Util
|
||||
/**
|
||||
* Emits a debug message
|
||||
* @param {string} message Debug message
|
||||
* @returns {void}
|
||||
*/
|
||||
debug(message) {
|
||||
if (message instanceof Error) message = message.stack;
|
||||
return this.manager.debug(`[connection] ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack data using JSON or Erlpack
|
||||
* @param {*} data Data to pack
|
||||
* Attempts to serialise data from the WebSocket
|
||||
* @param {string|Object} data Data to unpack
|
||||
* @returns {Object}
|
||||
*/
|
||||
unpack(data) {
|
||||
if (erlpack && typeof data !== 'string') {
|
||||
if (data instanceof ArrayBuffer) data = Buffer.from(new Uint8Array(data));
|
||||
return erlpack.unpack(data);
|
||||
} else if (data instanceof ArrayBuffer || data instanceof Buffer) {
|
||||
data = zlib.inflateSync(data).toString();
|
||||
}
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs an object ready to be sent
|
||||
* @param {Object} data Data to pack
|
||||
* @returns {string|Buffer}
|
||||
*/
|
||||
pack(data) {
|
||||
@@ -72,45 +166,288 @@ class WebSocketConnection extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack data using JSON or Erlpack
|
||||
* @param {string|ArrayBuffer|Buffer} data Data to unpack
|
||||
* @returns {string|Object}
|
||||
* Processes the current WebSocket queue
|
||||
*/
|
||||
unpack(data) {
|
||||
if (erlpack && typeof data !== 'string') {
|
||||
if (data instanceof ArrayBuffer) data = Buffer.from(new Uint8Array(data));
|
||||
return erlpack.unpack(data);
|
||||
} else {
|
||||
if (data instanceof ArrayBuffer || data instanceof Buffer) data = this.inflate(data);
|
||||
return JSON.parse(data);
|
||||
processQueue() {
|
||||
if (this.ratelimit.remaining === 0) return;
|
||||
if (this.ratelimit.queue.length === 0) return;
|
||||
if (this.ratelimit.remaining === 120) {
|
||||
this.ratelimit.resetTimer = setTimeout(() => {
|
||||
this.ratelimit.remaining = 120;
|
||||
this.processQueue();
|
||||
}, 120e3); // eslint-disable-line
|
||||
}
|
||||
while (this.ratelimit.remaining > 0) {
|
||||
const item = this.ratelimit.queue.shift();
|
||||
if (!item) return;
|
||||
this._send(item);
|
||||
this.ratelimit.remaining--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zlib inflate data
|
||||
* @param {string|Buffer} data Data to inflate
|
||||
* @returns {string|Buffer}
|
||||
* Sends data, bypassing the queue
|
||||
* @param {Object} data Packet to send
|
||||
* @returns {void}
|
||||
*/
|
||||
inflate(data) {
|
||||
return erlpack ? data : zlib.inflateSync(data).toString();
|
||||
_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(this.pack(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* State of the WebSocket
|
||||
* @type {number}
|
||||
* @readonly
|
||||
* Adds data to the queue to be sent
|
||||
* @param {Object} data Packet to send
|
||||
* @returns {void}
|
||||
*/
|
||||
get readyState() {
|
||||
return this.ws.readyState;
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the WebSocket
|
||||
* @param {number} code Close code
|
||||
* @param {string} [reason] Close reason
|
||||
* Creates a connection to a gateway
|
||||
* @param {string} gateway 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}
|
||||
*/
|
||||
close(code, reason) {
|
||||
this.ws.close(code, reason);
|
||||
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;
|
||||
}
|
||||
this.gateway = gateway;
|
||||
this.debug(`Connecting to ${gateway}`);
|
||||
const ws = this.ws = new WebSocket(gateway);
|
||||
if (browser) ws.binaryType = 'arraybuffer';
|
||||
ws.onmessage = this.onMessage.bind(this);
|
||||
ws.onopen = this.onOpen.bind(this);
|
||||
ws.onerror = this.onError.bind(this);
|
||||
ws.onclose = this.onClose.bind(this);
|
||||
this.status = Constants.Status.CONNECTING;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the connection
|
||||
* @returns {boolean}
|
||||
*/
|
||||
destroy() {
|
||||
const ws = this.ws;
|
||||
if (!ws) {
|
||||
this.debug('Attempted to destroy WebSocket but no connection exists!');
|
||||
return false;
|
||||
}
|
||||
this.heartbeat(-1);
|
||||
ws.close(1000);
|
||||
this.packetManager.handleQueue();
|
||||
this.ws = null;
|
||||
this.status = Constants.Status.DISCONNECTED;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a message is received
|
||||
* @param {Event} event Event received
|
||||
* @returns {boolean}
|
||||
*/
|
||||
onMessage(event) {
|
||||
try {
|
||||
this.onPacket(this.unpack(event.data));
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.debug(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
this.client.emit('raw', packet);
|
||||
switch (packet.op) {
|
||||
case Constants.OPCodes.HELLO:
|
||||
return this.heartbeat(packet.d.heartbeat_interval);
|
||||
case Constants.OPCodes.RECONNECT:
|
||||
return this.reconnect();
|
||||
case Constants.OPCodes.INVALID_SESSION:
|
||||
if (!packet.d) this.sessionID = null;
|
||||
this.debug('Session invalidated -- will identify with a new session');
|
||||
return this.identify(packet.d ? 2500 : 0);
|
||||
case Constants.OPCodes.HEARTBEAT_ACK:
|
||||
return this.ackHeartbeat();
|
||||
case Constants.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) {
|
||||
this.gateway = event.target.url;
|
||||
this.debug(`Connected to gateway ${this.gateway}`);
|
||||
this.identify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes a reconnection to the gateway
|
||||
*/
|
||||
reconnect() {
|
||||
this.debug('Attemping to reconnect in 5500ms...');
|
||||
this.client.emit(Constants.Events.RECONNECTING);
|
||||
this.connect(this.gateway, 5500, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever an error occurs with the WebSocket
|
||||
* @param {Error} error Error that occurred
|
||||
*/
|
||||
onError(error) {
|
||||
this.client.emit(Constants.Events.ERROR, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @external CloseEvent
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Called whenever a connection to the gateway is closed
|
||||
* @param {CloseEvent} event Close event that was received
|
||||
*/
|
||||
onClose(event) {
|
||||
this.debug(`Closed: ${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 (Constants.WSCodes[event.code]) {
|
||||
this.debug(Constants.WSCodes[event.code]);
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
this.reconnect();
|
||||
}
|
||||
|
||||
// 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: Constants.OPCodes.HEARTBEAT,
|
||||
d: this.sequence,
|
||||
});
|
||||
}
|
||||
|
||||
// Identification
|
||||
/**
|
||||
* Identifies the client on a connection
|
||||
* @param {number} [after] How long to wait before identifying
|
||||
* @returns {void}
|
||||
*/
|
||||
identify(after) {
|
||||
if (after) return this.client.setTimeout(this.identify.apply(this), after);
|
||||
return this.sessionID ? this.identifyResume() : this.identifyNew();
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies as a new connection on the gateway
|
||||
* @returns {void}
|
||||
*/
|
||||
identifyNew() {
|
||||
if (!this.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);
|
||||
|
||||
// Sharding stuff
|
||||
const { shardId, shardCount } = this.client.options;
|
||||
if (shardCount > 0) d.shard = [Number(shardId), Number(shardCount)];
|
||||
|
||||
// Send the payload
|
||||
this.debug('Identifying as a new session');
|
||||
this.send({ op: Constants.OPCodes.IDENTIFY, d });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes a session on the gateway
|
||||
* @returns {void}
|
||||
*/
|
||||
identifyResume() {
|
||||
if (!this.sessionID) {
|
||||
this.debug('Warning: wanted to resume but session ID not available; identifying as a new session instead');
|
||||
return this.identifyNew();
|
||||
}
|
||||
this.debug(`Attempting to resume session ${this.sessionID}`);
|
||||
|
||||
const d = {
|
||||
token: this.client.token,
|
||||
session_id: this.sessionID,
|
||||
seq: this.sequence,
|
||||
};
|
||||
|
||||
return this.send({
|
||||
op: Constants.OPCodes.RESUME,
|
||||
d,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,328 +1,89 @@
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const Constants = require('../../util/Constants');
|
||||
const PacketManager = require('./packets/WebSocketPacketManager');
|
||||
const WebSocketConnection = require('./WebSocketConnection');
|
||||
|
||||
/**
|
||||
* The WebSocket Manager of the Client
|
||||
* WebSocket Manager of the Client
|
||||
* @private
|
||||
*/
|
||||
class WebSocketManager extends EventEmitter {
|
||||
constructor(client) {
|
||||
super();
|
||||
/**
|
||||
* The Client that instantiated this WebSocketManager
|
||||
* Client that instantiated this WebSocketManager
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* A WebSocket Packet manager, it handles all the messages
|
||||
* @type {PacketManager}
|
||||
* WebSocket connection of this manager
|
||||
* @type {?WebSocketConnection}
|
||||
*/
|
||||
this.packetManager = new PacketManager(this);
|
||||
|
||||
/**
|
||||
* The status of the WebSocketManager, a type of Constants.Status. It defaults to IDLE.
|
||||
* @type {number}
|
||||
*/
|
||||
this.status = Constants.Status.IDLE;
|
||||
|
||||
/**
|
||||
* The session ID of the connection, null if not yet available.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.sessionID = null;
|
||||
|
||||
/**
|
||||
* The packet count of the client, null if not yet available.
|
||||
* @type {?number}
|
||||
*/
|
||||
this.sequence = -1;
|
||||
|
||||
/**
|
||||
* The gateway address for this WebSocket connection, null if not yet available.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.gateway = null;
|
||||
|
||||
/**
|
||||
* Whether READY was emitted normally (all packets received) or not
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.normalReady = false;
|
||||
|
||||
/**
|
||||
* The WebSocket connection to the gateway
|
||||
* @type {?WebSocket}
|
||||
*/
|
||||
this.ws = null;
|
||||
|
||||
/**
|
||||
* An object with keys that are websocket event names that should be ignored
|
||||
* @type {Object}
|
||||
*/
|
||||
this.disabledEvents = {};
|
||||
for (const event of client.options.disabledEvents) this.disabledEvents[event] = true;
|
||||
|
||||
this.first = true;
|
||||
|
||||
this.lastHeartbeatAck = true;
|
||||
|
||||
this._remainingReset = 0;
|
||||
|
||||
this._trace = [];
|
||||
this.resumeStart = -1;
|
||||
this.connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects the client to a given gateway
|
||||
* @param {string} gateway The gateway to connect to
|
||||
* Sends a heartbeat on the available connection
|
||||
* @returns {void}
|
||||
*/
|
||||
_connect(gateway) {
|
||||
this.client.emit('debug', `Connecting to gateway ${gateway}`);
|
||||
this.normalReady = false;
|
||||
if (this.status !== Constants.Status.RECONNECTING) this.status = Constants.Status.CONNECTING;
|
||||
this.ws = new WebSocketConnection(gateway);
|
||||
this.ws.on('open', this.eventOpen.bind(this));
|
||||
this.ws.on('packet', this.eventPacket.bind(this));
|
||||
this.ws.on('close', this.eventClose.bind(this));
|
||||
this.ws.on('error', this.eventError.bind(this));
|
||||
this._queue = [];
|
||||
this._remaining = 120;
|
||||
this.client.setInterval(() => {
|
||||
this._remaining = 120;
|
||||
this._remainingReset = Date.now();
|
||||
}, 60e3);
|
||||
}
|
||||
|
||||
connect(gateway = this.gateway) {
|
||||
this.gateway = gateway;
|
||||
if (this.first) {
|
||||
this._connect(gateway);
|
||||
this.first = false;
|
||||
} else {
|
||||
this.client.setTimeout(() => this._connect(gateway), 7500);
|
||||
}
|
||||
}
|
||||
|
||||
heartbeat(normal) {
|
||||
if (normal && !this.lastHeartbeatAck) {
|
||||
this.tryReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.emit('debug', 'Sending heartbeat');
|
||||
this.client._pingTimestamp = Date.now();
|
||||
this.client.ws.send({
|
||||
op: Constants.OPCodes.HEARTBEAT,
|
||||
d: this.sequence,
|
||||
}, true);
|
||||
|
||||
this.lastHeartbeatAck = false;
|
||||
heartbeat() {
|
||||
if (!this.connection) return this.debug('No connection to heartbeat');
|
||||
return this.connection.heartbeat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a packet to the gateway
|
||||
* @param {Object} data An object that can be JSON stringified
|
||||
* @param {boolean} force Whether or not to send the packet immediately
|
||||
* Emits a debug event
|
||||
* @param {string} message Debug message
|
||||
* @returns {void}
|
||||
*/
|
||||
send(data, force = false) {
|
||||
if (force) {
|
||||
this._send(data);
|
||||
return;
|
||||
}
|
||||
this._queue.push(data);
|
||||
this.doQueue();
|
||||
debug(message) {
|
||||
return this.client.emit('debug', `[ws] ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the client
|
||||
* @returns {void} Whether or not destruction was successful
|
||||
*/
|
||||
destroy() {
|
||||
if (this.ws) this.ws.close(1000);
|
||||
this._queue = [];
|
||||
this.status = Constants.Status.IDLE;
|
||||
}
|
||||
|
||||
_send(data) {
|
||||
if (this.ws.readyState === WebSocketConnection.WebSocket.OPEN) {
|
||||
this.emit('send', data);
|
||||
this.ws.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
doQueue() {
|
||||
const item = this._queue[0];
|
||||
if (!(this.ws.readyState === WebSocketConnection.WebSocket.OPEN && item)) return;
|
||||
if (this.remaining === 0) {
|
||||
this.client.setTimeout(this.doQueue.bind(this), Date.now() + (this._remainingReset || 120e3));
|
||||
return;
|
||||
}
|
||||
this._remaining--;
|
||||
this._send(item);
|
||||
this._queue.shift();
|
||||
this.doQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run whenever the gateway connections opens up
|
||||
*/
|
||||
eventOpen() {
|
||||
this.client.emit('debug', 'Connection to gateway opened');
|
||||
this.lastHeartbeatAck = true;
|
||||
if (this.sessionID) this._sendResume();
|
||||
else this._sendNewIdentify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a gateway resume packet, in cases of unexpected disconnections.
|
||||
*/
|
||||
_sendResume() {
|
||||
if (!this.sessionID) {
|
||||
this._sendNewIdentify();
|
||||
return;
|
||||
}
|
||||
this.client.emit('debug', 'Identifying as resumed session');
|
||||
this.resumeStart = this.sequence;
|
||||
const payload = {
|
||||
token: this.client.token,
|
||||
session_id: this.sessionID,
|
||||
seq: this.sequence,
|
||||
};
|
||||
|
||||
this.send({
|
||||
op: Constants.OPCodes.RESUME,
|
||||
d: payload,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a new identification packet, in cases of new connections or failed reconnections.
|
||||
*/
|
||||
_sendNewIdentify() {
|
||||
this.reconnecting = false;
|
||||
const payload = this.client.options.ws;
|
||||
payload.token = this.client.token;
|
||||
if (this.client.options.shardCount > 0) {
|
||||
payload.shard = [Number(this.client.options.shardId), Number(this.client.options.shardCount)];
|
||||
}
|
||||
this.client.emit('debug', 'Identifying as new session');
|
||||
this.send({
|
||||
op: Constants.OPCodes.IDENTIFY,
|
||||
d: payload,
|
||||
});
|
||||
this.sequence = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @external CloseEvent
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Run whenever the connection to the gateway is closed, it will try to reconnect the client.
|
||||
* @param {CloseEvent} event The WebSocket close event
|
||||
*/
|
||||
eventClose(event) {
|
||||
this.emit('close', event);
|
||||
this.client.clearInterval(this.client.manager.heartbeatInterval);
|
||||
this.status = Constants.Status.DISCONNECTED;
|
||||
this._queue = [];
|
||||
/**
|
||||
* Emitted whenever the client websocket is disconnected
|
||||
* @event Client#disconnect
|
||||
* @param {CloseEvent} event The WebSocket close event
|
||||
*/
|
||||
if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT, event);
|
||||
if ([4004, 4010, 4011].includes(event.code)) return;
|
||||
if (!this.reconnecting && event.code !== 1000) this.tryReconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run whenever a message is received from the WebSocket. Returns `true` if the message
|
||||
* was handled properly.
|
||||
* @param {Object} data The received websocket data
|
||||
* @returns {boolean}
|
||||
*/
|
||||
eventPacket(data) {
|
||||
if (data === null) {
|
||||
this.eventError(new Error(Constants.Errors.BAD_WS_MESSAGE));
|
||||
if (!this.connection) {
|
||||
this.debug('Attempted to destroy WebSocket but no connection exists!');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.client.emit('raw', data);
|
||||
|
||||
if (data.op === Constants.OPCodes.HELLO) this.client.manager.setupKeepAlive(data.d.heartbeat_interval);
|
||||
return this.packetManager.handle(data);
|
||||
return this.connection.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run whenever an error occurs with the WebSocket connection. Tries to reconnect
|
||||
* @param {Error} err The encountered error
|
||||
* Send a packet on the available WebSocket
|
||||
* @param {Object} packet Packet to send
|
||||
* @returns {void}
|
||||
*/
|
||||
eventError(err) {
|
||||
/**
|
||||
* Emitted whenever the Client encounters a serious connection error
|
||||
* @event Client#error
|
||||
* @param {Error} error The encountered error
|
||||
*/
|
||||
if (this.client.listenerCount('error') > 0) this.client.emit('error', err);
|
||||
this.tryReconnect();
|
||||
}
|
||||
|
||||
_emitReady(normal = true) {
|
||||
/**
|
||||
* Emitted when the Client becomes ready to start working
|
||||
* @event Client#ready
|
||||
*/
|
||||
this.status = Constants.Status.READY;
|
||||
this.client.emit(Constants.Events.READY);
|
||||
this.packetManager.handleQueue();
|
||||
this.normalReady = normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on new packets before `READY` to see if the Client is ready yet, if it is prepares
|
||||
* the `READY` event.
|
||||
*/
|
||||
checkIfReady() {
|
||||
if (this.status !== Constants.Status.READY && this.status !== Constants.Status.NEARLY) {
|
||||
let unavailableCount = 0;
|
||||
for (const guildID of this.client.guilds.keys()) {
|
||||
unavailableCount += this.client.guilds.get(guildID).available ? 0 : 1;
|
||||
}
|
||||
if (unavailableCount === 0) {
|
||||
this.status = Constants.Status.NEARLY;
|
||||
if (this.client.options.fetchAllMembers) {
|
||||
const promises = this.client.guilds.map(g => g.fetchMembers());
|
||||
Promise.all(promises).then(() => this._emitReady(), e => {
|
||||
this.client.emit(Constants.Events.WARN, 'Error in pre-ready guild member fetching');
|
||||
this.client.emit(Constants.Events.ERROR, e);
|
||||
this._emitReady();
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._emitReady();
|
||||
}
|
||||
send(packet) {
|
||||
if (!this.connection) {
|
||||
this.debug('No connection to websocket');
|
||||
return;
|
||||
}
|
||||
this.connection.send(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to reconnect the client, changing the status to Constants.Status.RECONNECTING.
|
||||
* Connects the client to a gateway
|
||||
* @param {string} gateway Gateway to connect to
|
||||
* @returns {boolean}
|
||||
*/
|
||||
tryReconnect() {
|
||||
if (
|
||||
this.status === Constants.Status.RECONNECTING ||
|
||||
this.status === Constants.Status.CONNECTING ||
|
||||
!this.client.token
|
||||
) return;
|
||||
this.status = Constants.Status.RECONNECTING;
|
||||
this.ws.close();
|
||||
this.packetManager.handleQueue();
|
||||
/**
|
||||
* Emitted when the Client tries to reconnect after being disconnected
|
||||
* @event Client#reconnecting
|
||||
*/
|
||||
this.client.emit(Constants.Events.RECONNECTING);
|
||||
this.connect(this.client.ws.gateway);
|
||||
connect(gateway) {
|
||||
if (!this.connection) {
|
||||
this.connection = new WebSocketConnection(this, gateway);
|
||||
return true;
|
||||
}
|
||||
switch (this.connection.status) {
|
||||
case Constants.Status.IDLE:
|
||||
case Constants.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ const Constants = require('../../../util/Constants');
|
||||
|
||||
const BeforeReadyWhitelist = [
|
||||
Constants.WSEvents.READY,
|
||||
Constants.WSEvents.RESUMED,
|
||||
Constants.WSEvents.GUILD_CREATE,
|
||||
Constants.WSEvents.GUILD_DELETE,
|
||||
Constants.WSEvents.GUILD_MEMBERS_CHUNK,
|
||||
@@ -10,8 +11,8 @@ const BeforeReadyWhitelist = [
|
||||
];
|
||||
|
||||
class WebSocketPacketManager {
|
||||
constructor(websocketManager) {
|
||||
this.ws = websocketManager;
|
||||
constructor(connection) {
|
||||
this.ws = connection;
|
||||
this.handlers = {};
|
||||
this.queue = [];
|
||||
|
||||
@@ -63,35 +64,12 @@ class WebSocketPacketManager {
|
||||
|
||||
handleQueue() {
|
||||
this.queue.forEach((element, index) => {
|
||||
this.handle(this.queue[index]);
|
||||
this.handle(this.queue[index], true);
|
||||
this.queue.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
setSequence(s) {
|
||||
if (s && s > this.ws.sequence) this.ws.sequence = s;
|
||||
}
|
||||
|
||||
handle(packet) {
|
||||
if (packet.op === Constants.OPCodes.RECONNECT) {
|
||||
this.setSequence(packet.s);
|
||||
this.ws.tryReconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (packet.op === Constants.OPCodes.INVALID_SESSION) {
|
||||
this.client.emit('debug', `SESSION INVALID! Waiting to reconnect: ${packet.d}`);
|
||||
if (packet.d) {
|
||||
setTimeout(() => {
|
||||
this.ws._sendResume();
|
||||
}, 2500);
|
||||
} else {
|
||||
this.ws.sessionID = null;
|
||||
this.ws._sendNewIdentify();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
handle(packet, queue = false) {
|
||||
if (packet.op === Constants.OPCodes.HEARTBEAT_ACK) {
|
||||
this.ws.client._pong(this.ws.client._pingTimestamp);
|
||||
this.ws.lastHeartbeatAck = true;
|
||||
@@ -109,7 +87,7 @@ class WebSocketPacketManager {
|
||||
this.ws.checkIfReady();
|
||||
}
|
||||
|
||||
this.setSequence(packet.s);
|
||||
this.ws.setSequence(packet.s);
|
||||
|
||||
if (this.ws.disabledEvents[packet.t] !== undefined) return false;
|
||||
|
||||
@@ -120,6 +98,8 @@ class WebSocketPacketManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (!queue && this.queue.length > 0) this.handleQueue();
|
||||
|
||||
if (this.handlers[packet.t]) return this.handlers[packet.t].handle(packet);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -59,10 +59,12 @@ class ReadyHandler extends AbstractHandler {
|
||||
});
|
||||
}
|
||||
|
||||
client.setTimeout(() => {
|
||||
if (!client.ws.normalReady) client.ws._emitReady(false);
|
||||
const t = client.setTimeout(() => {
|
||||
client.ws.connection.triggerReady();
|
||||
}, 1200 * data.guilds.length);
|
||||
|
||||
client.once('ready', () => client.clearTimeout(t));
|
||||
|
||||
const ws = this.packetManager.ws;
|
||||
|
||||
ws.sessionID = data.session_id;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
const AbstractHandler = require('./AbstractHandler');
|
||||
const Constants = require('../../../../util/Constants');
|
||||
|
||||
class ResumedHandler extends AbstractHandler {
|
||||
handle(packet) {
|
||||
const client = this.packetManager.client;
|
||||
const ws = client.ws;
|
||||
const ws = client.ws.connection;
|
||||
|
||||
ws._trace = packet.d._trace;
|
||||
|
||||
const replayed = ws.sequence - ws.resumeStart;
|
||||
ws.resumeStart = -1;
|
||||
ws.status = Constants.Status.READY;
|
||||
this.packetManager.handleQueue();
|
||||
|
||||
const replayed = ws.sequence - ws.closeSequence;
|
||||
|
||||
client.emit('debug', `RESUMED ${ws._trace.join(' -> ')} | replayed ${replayed} events. `);
|
||||
client.emit('resume', replayed);
|
||||
|
||||
ws.heartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,13 @@ exports.DefaultOptions = {
|
||||
},
|
||||
};
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
exports.Errors = {
|
||||
NO_TOKEN: 'Request to use token, but token was unavailable to the client.',
|
||||
NO_BOT_ACCOUNT: 'Only bot accounts are able to make use of this feature.',
|
||||
|
||||
@@ -4,20 +4,25 @@ const Discord = require('../');
|
||||
const request = require('superagent');
|
||||
const fs = require('fs');
|
||||
|
||||
const client = new Discord.Client({ fetchAllMembers: false, apiRequestMethod: 'sequential' });
|
||||
console.time('magic');
|
||||
|
||||
const client = new Discord.Client({ fetchAllMembers: true, apiRequestMethod: 'sequential' });
|
||||
|
||||
const { email, password, token, usertoken, song } = require('./auth.json');
|
||||
|
||||
client.login(token).then(atoken => console.log('logged in with token ' + atoken)).catch(console.error);
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log('ready');
|
||||
console.log(`ready with ${client.users.size} users`);
|
||||
console.timeEnd('magic');
|
||||
});
|
||||
|
||||
client.on('userUpdate', (o, n) => {
|
||||
console.log(o.username, n.username);
|
||||
});
|
||||
|
||||
client.on('debug', console.log);
|
||||
|
||||
client.on('emojiCreate', e => console.log('create!!', e.name));
|
||||
client.on('emojiDelete', e => console.log('delete!!', e.name));
|
||||
client.on('emojiUpdate', (o, n) => console.log('update!!', o.name, n.name));
|
||||
@@ -43,7 +48,7 @@ client.on('message', message => {
|
||||
let count = 0;
|
||||
let ecount = 0;
|
||||
for(let x = 0; x < 4000; x++) {
|
||||
message.channel.sendMessage(`this is message ${x} of 3999`)
|
||||
message.channel.send(`this is message ${x} of 3999`)
|
||||
.then(m => {
|
||||
count++;
|
||||
console.log('reached', count, ecount);
|
||||
@@ -57,7 +62,7 @@ client.on('message', message => {
|
||||
}
|
||||
|
||||
if (message.content === 'myperms?') {
|
||||
message.channel.sendMessage('Your permissions are:\n' +
|
||||
message.channel.send('Your permissions are:\n' +
|
||||
JSON.stringify(message.channel.permissionsFor(message.author).serialize(), null, 4));
|
||||
}
|
||||
|
||||
@@ -78,7 +83,7 @@ client.on('message', message => {
|
||||
.get('url')
|
||||
.end((err, res) => {
|
||||
client.user.setAvatar(res.body).catch(console.error)
|
||||
.then(user => message.channel.sendMessage('Done!'));
|
||||
.then(user => message.channel.send('Done!'));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,11 +104,11 @@ client.on('message', message => {
|
||||
m += `I am aware of ${client.channels.size} channels overall\n`;
|
||||
m += `I am aware of ${client.guilds.size} guilds overall\n`;
|
||||
m += `I am aware of ${client.users.size} users overall\n`;
|
||||
message.channel.sendMessage(m).then(msg => msg.edit('nah')).catch(console.error);
|
||||
message.channel.send(m).then(msg => msg.edit('nah')).catch(console.error);
|
||||
}
|
||||
|
||||
if (message.content === 'messageme!') {
|
||||
message.author.sendMessage('oh, hi there!').catch(e => console.log(e.stack));
|
||||
message.author.send('oh, hi there!').catch(e => console.log(e.stack));
|
||||
}
|
||||
|
||||
if (message.content === 'don\'t dm me') {
|
||||
@@ -113,7 +118,7 @@ client.on('message', message => {
|
||||
if (message.content.startsWith('kick')) {
|
||||
message.guild.member(message.mentions[0]).kick().then(member => {
|
||||
console.log(member);
|
||||
message.channel.sendMessage('Kicked!' + member.user.username);
|
||||
message.channel.send('Kicked!' + member.user.username);
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
@@ -121,10 +126,10 @@ client.on('message', message => {
|
||||
let i = 1;
|
||||
const start = Date.now();
|
||||
while (i <= 20) {
|
||||
message.channel.sendMessage(`Testing my rates, item ${i} of 20`);
|
||||
message.channel.send(`Testing my rates, item ${i} of 20`);
|
||||
i++;
|
||||
}
|
||||
message.channel.sendMessage('last one...').then(m => {
|
||||
message.channel.send('last one...').then(m => {
|
||||
const diff = Date.now() - start;
|
||||
m.reply(`Each message took ${diff / 21}ms to send`);
|
||||
});
|
||||
@@ -132,7 +137,7 @@ client.on('message', message => {
|
||||
|
||||
if (message.content === 'makerole') {
|
||||
message.guild.createRole().then(role => {
|
||||
message.channel.sendMessage(`Made role ${role.name}`);
|
||||
message.channel.send(`Made role ${role.name}`);
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
@@ -148,15 +153,15 @@ function chanLoop(channel) {
|
||||
|
||||
client.on('message', msg => {
|
||||
if (msg.content.startsWith('?raw')) {
|
||||
msg.channel.sendMessage('```' + msg.content + '```');
|
||||
msg.channel.send('```' + msg.content + '```');
|
||||
}
|
||||
|
||||
if (msg.content.startsWith('#eval') && msg.author.id === '66564597481480192') {
|
||||
try {
|
||||
const com = eval(msg.content.split(" ").slice(1).join(" "));
|
||||
msg.channel.sendMessage('```\n' + com + '```');
|
||||
msg.channel.send('```\n' + com + '```');
|
||||
} catch(e) {
|
||||
msg.channel.sendMessage('```\n' + e + '```');
|
||||
msg.channel.send('```\n' + e + '```');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -191,12 +196,12 @@ client.on('message', msg => {
|
||||
|
||||
client.on('messageReactionAdd', (reaction, user) => {
|
||||
if (reaction.message.channel.id !== '222086648706498562') return;
|
||||
reaction.message.channel.sendMessage(`${user.username} added reaction ${reaction.emoji}, count is now ${reaction.count}`);
|
||||
reaction.message.channel.send(`${user.username} added reaction ${reaction.emoji}, count is now ${reaction.count}`);
|
||||
});
|
||||
|
||||
client.on('messageReactionRemove', (reaction, user) => {
|
||||
if (reaction.message.channel.id !== '222086648706498562') return;
|
||||
reaction.message.channel.sendMessage(`${user.username} removed reaction ${reaction.emoji}, count is now ${reaction.count}`);
|
||||
reaction.message.channel.send(`${user.username} removed reaction ${reaction.emoji}, count is now ${reaction.count}`);
|
||||
});
|
||||
|
||||
client.on('message', m => {
|
||||
@@ -205,7 +210,7 @@ client.on('message', m => {
|
||||
m.channel.fetchMessage(mID).then(rM => {
|
||||
for (const reaction of rM.reactions.values()) {
|
||||
reaction.fetchUsers().then(users => {
|
||||
m.channel.sendMessage(
|
||||
m.channel.send(
|
||||
`The following gave that message ${reaction.emoji}:\n` +
|
||||
`${users.map(u => u.username).map(t => `- ${t}`).join('\n')}`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user