Created
March 14, 2018 16:17
-
-
Save fs-c/cf54ce69e7bb8f066b270277d9b7ac82 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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