VoiceWebSocket.js (7644B)
1 'use strict'; 2 3 const EventEmitter = require('events'); 4 const WebSocket = require('../../../WebSocket'); 5 const { Error } = require('../../../errors'); 6 const { OPCodes, VoiceOPCodes } = require('../../../util/Constants'); 7 8 /** 9 * Represents a Voice Connection's WebSocket. 10 * @extends {EventEmitter} 11 * @private 12 */ 13 class VoiceWebSocket extends EventEmitter { 14 constructor(connection) { 15 super(); 16 /** 17 * The Voice Connection that this WebSocket serves 18 * @type {VoiceConnection} 19 */ 20 this.connection = connection; 21 22 /** 23 * How many connection attempts have been made 24 * @type {number} 25 */ 26 this.attempts = 0; 27 28 this.dead = false; 29 this.connection.on('closing', this.shutdown.bind(this)); 30 } 31 32 /** 33 * The client of this voice WebSocket 34 * @type {Client} 35 * @readonly 36 */ 37 get client() { 38 return this.connection.client; 39 } 40 41 shutdown() { 42 this.emit('debug', `[WS] shutdown requested`); 43 this.dead = true; 44 this.reset(); 45 } 46 47 /** 48 * Resets the current WebSocket. 49 */ 50 reset() { 51 this.emit('debug', `[WS] reset requested`); 52 if (this.ws) { 53 if (this.ws.readyState !== WebSocket.CLOSED) this.ws.close(); 54 this.ws = null; 55 } 56 this.clearHeartbeat(); 57 } 58 59 /** 60 * Starts connecting to the Voice WebSocket Server. 61 */ 62 connect() { 63 this.emit('debug', `[WS] connect requested`); 64 if (this.dead) return; 65 if (this.ws) this.reset(); 66 if (this.attempts >= 5) { 67 this.emit('debug', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts)); 68 return; 69 } 70 71 this.attempts++; 72 73 /** 74 * The actual WebSocket used to connect to the Voice WebSocket Server. 75 * @type {WebSocket} 76 */ 77 this.ws = WebSocket.create(`wss://${this.connection.authentication.endpoint}/`, { v: 4 }); 78 this.emit('debug', `[WS] connecting, ${this.attempts} attempts, ${this.ws.url}`); 79 this.ws.onopen = this.onOpen.bind(this); 80 this.ws.onmessage = this.onMessage.bind(this); 81 this.ws.onclose = this.onClose.bind(this); 82 this.ws.onerror = this.onError.bind(this); 83 } 84 85 /** 86 * Sends data to the WebSocket if it is open. 87 * @param {string} data The data to send to the WebSocket 88 * @returns {Promise<string>} 89 */ 90 send(data) { 91 this.emit('debug', `[WS] >> ${data}`); 92 return new Promise((resolve, reject) => { 93 if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', data); 94 this.ws.send(data, null, error => { 95 if (error) reject(error); 96 else resolve(data); 97 }); 98 }); 99 } 100 101 /** 102 * JSON.stringify's a packet and then sends it to the WebSocket Server. 103 * @param {Object} packet The packet to send 104 * @returns {Promise<string>} 105 */ 106 sendPacket(packet) { 107 try { 108 packet = JSON.stringify(packet); 109 } catch (error) { 110 return Promise.reject(error); 111 } 112 return this.send(packet); 113 } 114 115 /** 116 * Called whenever the WebSocket opens. 117 */ 118 onOpen() { 119 this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`); 120 this.sendPacket({ 121 op: OPCodes.DISPATCH, 122 d: { 123 server_id: this.connection.channel.guild.id, 124 user_id: this.client.user.id, 125 token: this.connection.authentication.token, 126 session_id: this.connection.authentication.sessionID, 127 }, 128 }).catch(() => { 129 this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED')); 130 }); 131 } 132 133 /** 134 * Called whenever a message is received from the WebSocket. 135 * @param {MessageEvent} event The message event that was received 136 * @returns {void} 137 */ 138 onMessage(event) { 139 try { 140 return this.onPacket(WebSocket.unpack(event.data, 'json')); 141 } catch (error) { 142 return this.onError(error); 143 } 144 } 145 146 /** 147 * Called whenever the connection to the WebSocket server is lost. 148 */ 149 onClose() { 150 this.emit('debug', `[WS] closed`); 151 if (!this.dead) this.client.setTimeout(this.connect.bind(this), this.attempts * 1000); 152 } 153 154 /** 155 * Called whenever an error occurs with the WebSocket. 156 * @param {Error} error The error that occurred 157 */ 158 onError(error) { 159 this.emit('debug', `[WS] Error: ${error}`); 160 this.emit('error', error); 161 } 162 163 /** 164 * Called whenever a valid packet is received from the WebSocket. 165 * @param {Object} packet The received packet 166 */ 167 onPacket(packet) { 168 this.emit('debug', `[WS] << ${JSON.stringify(packet)}`); 169 switch (packet.op) { 170 case VoiceOPCodes.HELLO: 171 this.setHeartbeat(packet.d.heartbeat_interval); 172 break; 173 case VoiceOPCodes.READY: 174 /** 175 * Emitted once the voice WebSocket receives the ready packet. 176 * @param {Object} packet The received packet 177 * @event VoiceWebSocket#ready 178 */ 179 this.emit('ready', packet.d); 180 break; 181 /* eslint-disable no-case-declarations */ 182 case VoiceOPCodes.SESSION_DESCRIPTION: 183 packet.d.secret_key = new Uint8Array(packet.d.secret_key); 184 /** 185 * Emitted once the Voice Websocket receives a description of this voice session. 186 * @param {Object} packet The received packet 187 * @event VoiceWebSocket#sessionDescription 188 */ 189 this.emit('sessionDescription', packet.d); 190 break; 191 case VoiceOPCodes.CLIENT_CONNECT: 192 this.connection.ssrcMap.set(+packet.d.audio_ssrc, packet.d.user_id); 193 break; 194 case VoiceOPCodes.CLIENT_DISCONNECT: 195 const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id); 196 if (streamInfo) { 197 this.connection.receiver.packets.streams.delete(packet.d.user_id); 198 streamInfo.stream.push(null); 199 } 200 break; 201 case VoiceOPCodes.SPEAKING: 202 /** 203 * Emitted whenever a speaking packet is received. 204 * @param {Object} data 205 * @event VoiceWebSocket#startSpeaking 206 */ 207 this.emit('startSpeaking', packet.d); 208 break; 209 default: 210 /** 211 * Emitted when an unhandled packet is received. 212 * @param {Object} packet 213 * @event VoiceWebSocket#unknownPacket 214 */ 215 this.emit('unknownPacket', packet); 216 break; 217 } 218 } 219 220 /** 221 * Sets an interval at which to send a heartbeat packet to the WebSocket. 222 * @param {number} interval The interval at which to send a heartbeat packet 223 */ 224 setHeartbeat(interval) { 225 if (!interval || isNaN(interval)) { 226 this.onError(new Error('VOICE_INVALID_HEARTBEAT')); 227 return; 228 } 229 if (this.heartbeatInterval) { 230 /** 231 * Emitted whenever the voice WebSocket encounters a non-fatal error. 232 * @param {string} warn The warning 233 * @event VoiceWebSocket#warn 234 */ 235 this.emit('warn', 'A voice heartbeat interval is being overwritten'); 236 this.client.clearInterval(this.heartbeatInterval); 237 } 238 this.heartbeatInterval = this.client.setInterval(this.sendHeartbeat.bind(this), interval); 239 } 240 241 /** 242 * Clears a heartbeat interval, if one exists. 243 */ 244 clearHeartbeat() { 245 if (!this.heartbeatInterval) { 246 this.emit('warn', 'Tried to clear a heartbeat interval that does not exist'); 247 return; 248 } 249 this.client.clearInterval(this.heartbeatInterval); 250 this.heartbeatInterval = null; 251 } 252 253 /** 254 * Sends a heartbeat packet. 255 */ 256 sendHeartbeat() { 257 this.sendPacket({ op: VoiceOPCodes.HEARTBEAT, d: Math.floor(Math.random() * 10e10) }).catch(() => { 258 this.emit('warn', 'Tried to send heartbeat, but connection is not open'); 259 this.clearHeartbeat(); 260 }); 261 } 262 } 263 264 module.exports = VoiceWebSocket;