buddy

node MVC discord bot
Log | Files | Refs | README

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;