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