buddy

node MVC discord bot
Log | Files | Refs | README

PacketHandler.js (3595B)


      1 'use strict';
      2 
      3 const EventEmitter = require('events');
      4 const secretbox = require('../util/Secretbox');
      5 
      6 // The delay between packets when a user is considered to have stopped speaking
      7 // https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200
      8 const DISCORD_SPEAKING_DELAY = 250;
      9 
     10 class Readable extends require('stream').Readable {
     11   _read() {} // eslint-disable-line no-empty-function
     12 }
     13 
     14 class PacketHandler extends EventEmitter {
     15   constructor(receiver) {
     16     super();
     17     this.nonce = Buffer.alloc(24);
     18     this.receiver = receiver;
     19     this.streams = new Map();
     20     this.speakingTimeouts = new Map();
     21   }
     22 
     23   get connection() {
     24     return this.receiver.connection;
     25   }
     26 
     27   _stoppedSpeaking(userID) {
     28     const streamInfo = this.streams.get(userID);
     29     if (streamInfo && streamInfo.end === 'silence') {
     30       this.streams.delete(userID);
     31       streamInfo.stream.push(null);
     32     }
     33   }
     34 
     35   makeStream(user, end) {
     36     if (this.streams.has(user)) return this.streams.get(user).stream;
     37     const stream = new Readable();
     38     stream.on('end', () => this.streams.delete(user));
     39     this.streams.set(user, { stream, end });
     40     return stream;
     41   }
     42 
     43   parseBuffer(buffer) {
     44     const { secret_key, mode } = this.receiver.connection.authentication;
     45 
     46     // Choose correct nonce depending on encryption
     47     let end;
     48     if (mode === 'xsalsa20_poly1305_lite') {
     49       buffer.copy(this.nonce, 0, buffer.length - 4);
     50       end = buffer.length - 4;
     51     } else if (mode === 'xsalsa20_poly1305_suffix') {
     52       buffer.copy(this.nonce, 0, buffer.length - 24);
     53       end = buffer.length - 24;
     54     } else {
     55       buffer.copy(this.nonce, 0, 0, 12);
     56     }
     57 
     58     // Open packet
     59     let packet = secretbox.methods.open(buffer.slice(12, end), this.nonce, secret_key);
     60     if (!packet) return new Error('Failed to decrypt voice packet');
     61     packet = Buffer.from(packet);
     62 
     63     // Strip RTP Header Extensions (one-byte only)
     64     if (packet[0] === 0xbe && packet[1] === 0xde && packet.length > 4) {
     65       const headerExtensionLength = packet.readUInt16BE(2);
     66       let offset = 4;
     67       for (let i = 0; i < headerExtensionLength; i++) {
     68         const byte = packet[offset];
     69         offset++;
     70         if (byte === 0) continue;
     71         offset += 1 + (0b1111 & (byte >> 4));
     72       }
     73       // Skip over undocumented Discord byte
     74       offset++;
     75 
     76       packet = packet.slice(offset);
     77     }
     78 
     79     return packet;
     80   }
     81 
     82   push(buffer) {
     83     const ssrc = buffer.readUInt32BE(8);
     84     const userStat = this.connection.ssrcMap.get(ssrc);
     85     if (!userStat) return;
     86 
     87     let speakingTimeout = this.speakingTimeouts.get(ssrc);
     88     if (typeof speakingTimeout === 'undefined') {
     89       this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: userStat.speaking });
     90       speakingTimeout = this.receiver.connection.client.setTimeout(() => {
     91         try {
     92           this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: 0 });
     93           this.receiver.connection.client.clearTimeout(speakingTimeout);
     94           this.speakingTimeouts.delete(ssrc);
     95         } catch {
     96           // Connection already closed, ignore
     97         }
     98       }, DISCORD_SPEAKING_DELAY);
     99       this.speakingTimeouts.set(ssrc, speakingTimeout);
    100     } else {
    101       speakingTimeout.refresh();
    102     }
    103 
    104     let stream = this.streams.get(userStat.userID);
    105     if (!stream) return;
    106     stream = stream.stream;
    107     const opusPacket = this.parseBuffer(buffer);
    108     if (opusPacket instanceof Error) {
    109       this.emit('error', opusPacket);
    110       return;
    111     }
    112     stream.push(opusPacket);
    113   }
    114 }
    115 
    116 module.exports = PacketHandler;