import { VoiceOpcodes } from 'discord-api-types/voice/v4' ; import { VoiceUDPSocket } from './VoiceUDPSocket' ; import { VoiceWebSocket } from './VoiceWebSocket' ; import * as secretbox from '../util/Secretbox' ; import { Awaited , noop } from '../util/util' ; import type { CloseEvent } from 'ws' ; import { TypedEmitter } from 'tiny-typed-emitter' ; const CHANNELS = 2 ; const TIMESTAMP_INC = ( 48000 / 100 ) * CHANNELS ; const MAX_NONCE_SIZE = 2 ** 32 - 1 ; export const SUPPORTED_ENCRYPTION_MODES = [ 'xsalsa20_poly1305_lite' , 'xsalsa20_poly1305_suffix' , 'xsalsa20_poly1305' ] ; /**
 * The different statuses that a networking instance can hold. The order
 * of the states between OpeningWs and Ready is chronological (first the
 * instance enters OpeningWs, then it enters Identifying etc.)
 */ export enum NetworkingStatusCode { OpeningWs , Identifying , UdpHandshaking , SelectingProtocol , Ready , Resuming , Closed } /**
 * The initial Networking state. Instances will be in this state when a WebSocket connection to a Discord
 * voice gateway is being opened.
 */ export interface NetworkingOpeningWsState { code : NetworkingStatusCode . OpeningWs ; ws : VoiceWebSocket ; connectionOptions : ConnectionOptions ; } /**
 * The state that a Networking instance will be in when it is attempting to authorize itself.
 */ export interface NetworkingIdentifyingState { code : NetworkingStatusCode . Identifying ; ws : VoiceWebSocket ; connectionOptions : ConnectionOptions ; } /**
 * The state that a Networking instance will be in when opening a UDP connection to the IP and port provided
 * by Discord, as well as performing IP discovery.
 */ export interface NetworkingUdpHandshakingState { code : NetworkingStatusCode . UdpHandshaking ; ws : VoiceWebSocket ; udp : VoiceUDPSocket ; connectionOptions : ConnectionOptions ; connectionData : Pick < ConnectionData , 'ssrc' > ; } /**
 * The state that a Networking instance will be in when selecting an encryption protocol for audio packets.
 */ export interface NetworkingSelectingProtocolState { code : NetworkingStatusCode . SelectingProtocol ; ws : VoiceWebSocket ; udp : VoiceUDPSocket ; connectionOptions : ConnectionOptions ; connectionData : Pick < ConnectionData , 'ssrc' > ; } /**
 * The state that a Networking instance will be in when it has a fully established connection to a Discord
 * voice server.
 */ export interface NetworkingReadyState { code : NetworkingStatusCode . Ready ; ws : VoiceWebSocket ; udp : VoiceUDPSocket ; connectionOptions : ConnectionOptions ; connectionData : ConnectionData ; preparedPacket ? : Buffer ; } /**
 * The state that a Networking instance will be in when its connection has been dropped unexpectedly, and it
 * is attempting to resume an existing session.
 */ export interface NetworkingResumingState { code : NetworkingStatusCode . Resuming ; ws : VoiceWebSocket ; udp : VoiceUDPSocket ; connectionOptions : ConnectionOptions ; connectionData : ConnectionData ; preparedPacket ? : Buffer ; } /**
 * The state that a Networking instance will be in when it has been destroyed. It cannot be recovered from this
 * state.
 */ export interface NetworkingClosedState { code : NetworkingStatusCode . Closed ; } /**
 * The various states that a networking instance can be in.
 */ export type NetworkingState = NetworkingOpeningWsState | NetworkingIdentifyingState | NetworkingUdpHandshakingState | NetworkingSelectingProtocolState | NetworkingReadyState | NetworkingResumingState | NetworkingClosedState ; /**
 * Details required to connect to the Discord voice gateway. These details
 * are first received on the main bot gateway, in the form of VOICE_SERVER_UPDATE
 * and VOICE_STATE_UPDATE packets.
 */ interface ConnectionOptions { serverId : string ; userId : string ; sessionId : string ; token : string ; endpoint : string ; } /**
 * Information about the current connection, e.g. which encryption mode is to be used on
 * the connection, timing information for playback of streams.
 */ export interface ConnectionData { ssrc : number ; encryptionMode : string ; secretKey : Uint8Array ; sequence : number ; timestamp : number ; packetsPlayed : number ; nonce : number ; nonceBuffer : Buffer ; speaking : boolean ; } /**
 * An empty buffer that is reused in packet encryption by many different networking instances.
 */ const nonce = Buffer . alloc ( 24 ) ; export interface NetworkingEvents { debug : ( message : string ) => Awaited < void > ; error : ( error : Error ) => Awaited < void > ; stateChange : ( oldState : NetworkingState , newState : NetworkingState ) => Awaited < void > ; close : ( code : number ) => Awaited < void > ; } /**
 * Manages the networking required to maintain a voice connection and dispatch audio packets
 */ export class Networking extends TypedEmitter < NetworkingEvents > { private _state : NetworkingState ; /**
     * The debug logger function, if debugging is enabled.
     */ private readonly debug : null | ( ( message : string ) => void ) ; /**
     * Creates a new Networking instance.
     */ public constructor ( options , debug ) { super ( ) ; this . onWsOpen = this . onWsOpen . bind ( this ) ; this . onChildError = this . onChildError . bind ( this ) ; this . onWsPacket = this . onWsPacket . bind ( this ) ; this . onWsClose = this . onWsClose . bind ( this ) ; this . onWsDebug = this . onWsDebug . bind ( this ) ; this . onUdpDebug = this . onUdpDebug . bind ( this ) ; this . onUdpClose = this . onUdpClose . bind ( this ) ; this . debug = debug ? ( message ) => this . emit ( 'debug' , message ) : null ; this . _state = { code : NetworkingStatusCode . OpeningWs , ws : this . createWebSocket ( options . endpoint ) , connectionOptions : options , } ; } /**
     * Destroys the Networking instance, transitioning it into the Closed state.
     */ public destroy ( ) { this . state = { code : NetworkingStatusCode . Closed , } ; } /**
     * The current state of the networking instance.
     */ public get state ( ) : NetworkingState { return this . _state ; } /**
     * Sets a new state for the networking instance, performing clean-up operations where necessary.
     */ public set state ( newState ) { const oldWs = Reflect . get ( this . _state , 'ws' ) as VoiceWebSocket | undefined ; const newWs = Reflect . get ( newState , 'ws' ) as VoiceWebSocket | undefined ; if ( oldWs && oldWs !== newWs ) { oldWs . off ( 'debug' , this . onWsDebug ) ; oldWs . on ( 'error' , noop ) ; oldWs . off ( 'error' , this . onChildError ) ; oldWs . off ( 'open' , this . onWsOpen ) ; oldWs . off ( 'packet' , this . onWsPacket ) ; oldWs . off ( 'close' , this . onWsClose ) ; oldWs . destroy ( ) ; } const oldUdp = Reflect . get ( this . _state , 'udp' ) as VoiceUDPSocket | undefined ; const newUdp = Reflect . get ( newState , 'udp' ) as VoiceUDPSocket | undefined ; if ( oldUdp && oldUdp !== newUdp ) { oldUdp . on ( 'error' , noop ) ; oldUdp . off ( 'error' , this . onChildError ) ; oldUdp . off ( 'close' , this . onUdpClose ) ; oldUdp . off ( 'debug' , this . onUdpDebug ) ; oldUdp . destroy ( ) ; } const oldState = this . _state ; this . _state = newState ; this . emit ( 'stateChange' , oldState , newState ) ; event type { string } this . debug ?. ( `state change:\nfrom ${ stringifyState ( oldState ) }\nto ${ stringifyState ( newState ) }` ) ; } param endpoint param debug private createWebSocket ( endpoint ) { const ws = new VoiceWebSocket ( `wss://${ endpoint }?v=4` , Boolean ( this . debug ) ) ; ws . on ( 'error' , this . onChildError ) ; ws . once ( 'open' , this . onWsOpen ) ; ws . on ( 'packet' , this . onWsPacket ) ; ws . once ( 'close' , this . onWsClose ) ; ws . on ( 'debug' , this . onWsDebug ) ; return ws ; } param error private onChildError ( error ) { this . emit ( 'error' , error ) ; } /**
     * Called when the WebSocket opens. Depending on the state that the instance is in,
     * it will either identify with a new session, or it will attempt to resume an existing session.
     */ private onWsOpen ( ) { if ( this . state . code === NetworkingStatusCode . OpeningWs ) { const packet = { op : VoiceOpcodes . Identify , d : { server_id : this . state . connectionOptions . serverId , user_id : this . state . connectionOptions . userId , session_id : this . state . connectionOptions . sessionId , token : this . state . connectionOptions . token , } , } ; this . state . ws . sendPacket ( packet ) ; this . state = { ... this . state , code : NetworkingStatusCode . Identifying , } ; } else if ( this . state . code === NetworkingStatusCode . Resuming ) { const packet = { op : VoiceOpcodes . Resume , d : { server_id : this . state . connectionOptions . serverId , session_id : this . state . connectionOptions . sessionId , token : this . state . connectionOptions . token , } , } ; this . state . ws . sendPacket ( packet ) ; } } param code private onWsClose ( { code } ) { const canResume = code === 4015 || code < 4000 ; if ( canResume && this . state . code === NetworkingStatusCode . Ready ) { this . state = { ... this . state , code : NetworkingStatusCode . Resuming , ws : this . createWebSocket ( this . state . connectionOptions . endpoint ) , } ; } else if ( this . state . code !== NetworkingStatusCode . Closed ) { this . destroy ( ) ; this . emit ( 'close' , code ) ; } } /**
     * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord.
     */ private onUdpClose ( ) { if ( this . state . code === NetworkingStatusCode . Ready ) { this . state = { ... this . state , code : NetworkingStatusCode . Resuming , ws : this . createWebSocket ( this . state . connectionOptions . endpoint ) , } ; } } param packet private onWsPacket ( packet ) { if ( packet . op === VoiceOpcodes . Hello && this . state . code !== NetworkingStatusCode . Closed ) { this . state . ws . setHeartbeatInterval ( packet . d . heartbeat_interval ) ; } else if ( packet . op === VoiceOpcodes . Ready && this . state . code === NetworkingStatusCode . Identifying ) { const { ip , port , ssrc , modes } = packet . d ; const udp = new VoiceUDPSocket ( { ip , port } ) ; udp . on ( 'error' , this . onChildError ) ; udp . on ( 'debug' , this . onUdpDebug ) ; udp . once ( 'close' , this . onUdpClose ) ; udp . performIPDiscovery ( ssrc ) . then ( ( localConfig ) => { if ( this . state . code !== NetworkingStatusCode . UdpHandshaking ) return ; this . state . ws . sendPacket ( { op : VoiceOpcodes . SelectProtocol , d : { protocol : 'udp' , data : { address : localConfig . ip , port : localConfig . port , mode : chooseEncryptionMode ( modes ) , } , } , } ) ; this . state = { ... this . state , code : NetworkingStatusCode . SelectingProtocol , } ; } ) . catch ( ( error ) => this . emit ( 'error' , error ) ) ; this . state = { ... this . state , code : NetworkingStatusCode . UdpHandshaking , udp , connectionData : { ssrc , } , } ; } else if ( packet . op === VoiceOpcodes . SessionDescription && this . state . code === NetworkingStatusCode . SelectingProtocol ) { const { mode : encryptionMode , secret_key : secretKey } = packet . d ; this . state = { ... this . state , code : NetworkingStatusCode . Ready , connectionData : { ... this . state . connectionData , encryptionMode , secretKey : new Uint8Array ( secretKey ) , sequence : randomNBit ( 16 ) , timestamp : randomNBit ( 32 ) , nonce : 0 , nonceBuffer : Buffer . alloc ( 24 ) , speaking : false , packetsPlayed : 0 , } , } ; } else if ( packet . op === VoiceOpcodes . Resumed && this . state . code === NetworkingStatusCode . Resuming ) { this . state = { ... this . state , code : NetworkingStatusCode . Ready , } ; this . state . connectionData . speaking = false ; } } param message private onWsDebug ( message ) { this . debug ?. ( `[WS] ${ message }` ) ; } param message private onUdpDebug ( message ) { this . debug ?. ( `[UDP] ${ message }` ) ; } remarks param opusPacket returns public prepareAudioPacket ( opusPacket ) { const state = this . state ; if ( state . code !== NetworkingStatusCode . Ready ) return ; state . preparedPacket = this . createAudioPacket ( opusPacket , state . connectionData ) ; return state . preparedPacket ; } /**
     * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet
     * is consumed and cannot be dispatched again.
     */ public dispatchAudio ( ) { const state = this . state ; if ( state . code !== NetworkingStatusCode . Ready ) return false ; if ( typeof state . preparedPacket !== 'undefined' ) { this . playAudioPacket ( state . preparedPacket ) ; state . preparedPacket = undefined ; return true ; } return false ; } param audioPacket private playAudioPacket ( audioPacket ) { const state = this . state ; if ( state . code !== NetworkingStatusCode . Ready ) return ; const { connectionData } = state ; connectionData . packetsPlayed ++ ; connectionData . sequence ++ ; connectionData . timestamp += TIMESTAMP_INC ; if ( connectionData . sequence >= 2 ** 16 ) connectionData . sequence = 0 ; if ( connectionData . timestamp >= 2 ** 32 ) connectionData . timestamp = 0 ; this . setSpeaking ( true ) ; state . udp . send ( audioPacket ) ; } param speaking public setSpeaking ( speaking ) { const state = this . state ; if ( state . code !== NetworkingStatusCode . Ready ) return ; if ( state . connectionData . speaking === speaking ) return ; state . connectionData . speaking = speaking ; state . ws . sendPacket ( { op : VoiceOpcodes . Speaking , d : { speaking : speaking ? 1 : 0 , delay : 0 , ssrc : state . connectionData . ssrc , } , } ) ; } param opusPacket param connectionData private createAudioPacket ( opusPacket , connectionData ) { const packetBuffer = Buffer . alloc ( 12 ) ; packetBuffer [ 0 ] = 0x80 ; packetBuffer [ 1 ] = 0x78 ; const { sequence , timestamp , ssrc } = connectionData ; packetBuffer . writeUIntBE ( sequence , 2 , 2 ) ; packetBuffer . writeUIntBE ( timestamp , 4 , 4 ) ; packetBuffer . writeUIntBE ( ssrc , 8 , 4 ) ; packetBuffer . copy ( nonce , 0 , 0 , 12 ) ; return Buffer . concat ( [ packetBuffer , ... this . encryptOpusPacket ( opusPacket , connectionData ) ] ) ; } param opusPacket param connectionData private encryptOpusPacket ( opusPacket , connectionData ) { const { secretKey , encryptionMode } = connectionData ; if ( encryptionMode === 'xsalsa20_poly1305_lite' ) { connectionData . nonce ++ ; if ( connectionData . nonce > MAX_NONCE_SIZE ) connectionData . nonce = 0 ; connectionData . nonceBuffer . writeUInt32BE ( connectionData . nonce , 0 ) ; return [ secretbox . methods . close ( opusPacket , connectionData . nonceBuffer , secretKey ) , connectionData . nonceBuffer . slice ( 0 , 4 ) , ] ; } else if ( encryptionMode === 'xsalsa20_poly1305_suffix' ) { const random = secretbox . methods . random ( 24 , connectionData . nonceBuffer ) ; return [ secretbox . methods . close ( opusPacket , random , secretKey ) , random ] ; } return [ secretbox . methods . close ( opusPacket , nonce , secretKey ) ] ; } } param n function randomNBit ( n ) { return Math . floor ( Math . random ( ) * 2 ** n ) ; } param state function stringifyState ( state ) { return JSON . stringify ( { ... state , ws : Reflect . has ( state , 'ws' ) , udp : Reflect . has ( state , 'udp' ) , } ) ; } param options function chooseEncryptionMode ( options ) { const option = options . find ( ( option ) => SUPPORTED_ENCRYPTION_MODES . includes ( option ) ) ; if ( ! option ) { throw new Error ( `No compatible encryption modes. Available include: ${ options . join ( ', ' ) }` ) ; } return option ; }