Skip to content

Instantly share code, notes, and snippets.

@fs-c
Created March 14, 2018 16:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fs-c/cf54ce69e7bb8f066b270277d9b7ac82 to your computer and use it in GitHub Desktop.
Save fs-c/cf54ce69e7bb8f066b270277d9b7ac82 to your computer and use it in GitHub Desktop.
const WS_VER = process.env.WS_VER || '6';
const WS_URL = `wss://gateway.discord.gg/?v=${WS_VER}&encoding=json`;
const WebSocket = require('ws');
const EventEmitter = require('events');
const debug = require('debug')('discord');
const { payloads } = require('./payloads');
const Client = module.exports = class extends EventEmitter {
constructor(token) {
super();
if (!token) {
throw new Error('No token!');
}
// All values which default to undefined may be set in the process
// of connecting, do not count on them being set at any point in time.
this.socket;
this.token = token;
this.state = 'dormant';
this.connection = {
sequence: 0,
ping: undefined,
sessionID: undefined,
}
this.heartbeat = {
last: 0,
timer: undefined, // Periodic heartbeats.
timeout: undefined, // Check for timeout.
interval: 15 * 1000, // Most likely overwritten by server response.
maxDelay: 15 * 1000, // Max delay for heartbeat response.
}
this.reconnect = {
timer: undefined,
delay: 1000 * 60,
}
}
get payloads() {
return payloads.bind(this)();
}
set state(updated) {
debug(`changed state from %o to %o`, this.state, updated);
this.state = updated;
}
connect() { return new Promise((resolve, reject) => {
this.state = this.reconnect.timer ? 'reconnecting' : 'connecting';
if (this.reconnect.timer) {
this.reconnect.timer = undefined;
}
this.socket = new WebSocket(WS_URL);
this.socket.on('message', (data) => this.handleSocket(data));
this.socket.on('open', () => {
resolve();
this.state = 'connected';
});
this.socket.on('close', (code, reason) => {
debug('connection closed: %o (%o)', code, reason)
this.kill();
debug('reconnecting in %o', this.reconnect.delay);
this.reconnect.timer = setTimeout(this.connect, this.reconnect.delay);
});
this.socket.on('error', (err) => {
this.state = 'errored';
debug('encountered error %o', err.message);
});
})}
// Clear any timers, reset defaults.
kill() {
clearInterval(this.heartbeat.timer);
this.heartbeat.timer = undefined;
clearInterval(this.heartbeat.timeout);
this.heartbeat.timeout = undefined;
clearInterval(this.reconnect.timer);
this.reconnect.timer = undefined;
debug('cleared timers');
if (this.socket.readyState <= 1) {
this.socket.close();
this.state = 'closed';
}
}
send(data) {
if (!this.socket || this.socket.readyState !== 1) {
return;
}
this.socket.send(data, (err) => {
if (err)
});
}
handleSocket(raw) {
const message = JSON.parse(raw);
const data = message.d;
debug('received message (%o)', message.t || message.op);
switch (message.op) {
// Dispatch.
case 0: this.sequence++;
break;
// Invalid session.
case 9:
debug('invalid session');
this.connect.sequence = 0;
this.connection.sessionID = null;
this.send(4000, 'Received an invalid session ID.')
break;
// Hello.
case 10:
debug('socket hello');
if (this.sequence && this.sessionID) {
debug('resuming');
// We were already connected before, resume connection.
this.send(this.payloads.resume);
} else {
debug('identifying');
// Initial connect, we have to identify ourselves.
this.send(this.payloads.identify);
}
if (this.heartbeat.timer) {
// Clear the heartbeat timer if it exists.
clearInterval(this.heartbeat.timer);
}
// Try to get the interval from sent data, fallback to default if
// it's not provided (rare).
if (data.heartbeat_interval) {
this.heartbeat.interval = data.heartbeat_interval;
}
debug('sending heartbeats with %oms interval',
this.heartbeat.interval);
// Send periodic heartbeats.
this.heartbeat.timer = setInterval(() => {
this.heartbeat.last = Date.now(); // To measure our ping.
// This will get cleared in the heartbeat ACK; if it doesn't,
// we've exceeded our ping limit and should close the connection.
this.heartbeat.timeout = setTimeout(() => {
this.socket.close(4000, 'No heartbeat received.');
}, this.heartbeat.maxDelay);
debug('sending heartbeat');
this.send(this.payloads.heartbeat);
}, this.heartbeat.interval)
break;
// Heartbeat acknowledgement.
case 11:
clearInterval(this.heartbeat.timeout); // Clear the timeout catch.
// Calculate our ping.
this.connection.ping = Date.now() - this.heartbeat.last;
debug('heartbeat acknowledged with %oms delay', this.connection.ping);
break;
default: debug('unknown opcode: %o', message.op);
}
switch (message.t) {
case 'READY':
this.connection.sessionID = data.session_id;
break;
case 'MESSAGE_CREATE':
// Data should be a message object, see:
// https://discordapp.com/developers/docs/resources/channel#message-object
this.emit('message', data);
break;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment