UNPKG

discord-voip

Version:

Discord VoIP library used by discord-player

1,486 lines (1,470 loc) 353 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/index.ts var src_exports = {}; __export(src_exports, { AudioPlayer: () => AudioPlayer, AudioPlayerError: () => AudioPlayerError, AudioPlayerStatus: () => AudioPlayerStatus, AudioResource: () => AudioResource, NoSubscriberBehavior: () => NoSubscriberBehavior, PlayerSubscription: () => PlayerSubscription, StreamType: () => StreamType, VoiceConnection: () => VoiceConnection, VoiceConnectionDisconnectReason: () => VoiceConnectionDisconnectReason, VoiceConnectionStatus: () => VoiceConnectionStatus, createAudioPlayer: () => createAudioPlayer, createAudioResource: () => createAudioResource, entersState: () => entersState, getGroups: () => getGroups, getVoiceConnection: () => getVoiceConnection, getVoiceConnections: () => getVoiceConnections, joinVoiceChannel: () => joinVoiceChannel, version: () => version }); module.exports = __toCommonJS(src_exports); // src/VoiceConnection.ts var import_node_events6 = require("events"); // src/DataStore.ts var import_v10 = require("discord-api-types/v10"); function createJoinVoiceChannelPayload(config) { return { op: import_v10.GatewayOpcodes.VoiceStateUpdate, // eslint-disable-next-line id-length d: { guild_id: config.guildId, channel_id: config.channelId, self_deaf: config.selfDeaf, self_mute: config.selfMute } }; } __name(createJoinVoiceChannelPayload, "createJoinVoiceChannelPayload"); var groups = /* @__PURE__ */ new Map(); groups.set("default", /* @__PURE__ */ new Map()); function getOrCreateGroup(group) { const existing = groups.get(group); if (existing) return existing; const map = /* @__PURE__ */ new Map(); groups.set(group, map); return map; } __name(getOrCreateGroup, "getOrCreateGroup"); function getGroups() { return groups; } __name(getGroups, "getGroups"); function getVoiceConnections(group = "default") { return groups.get(group); } __name(getVoiceConnections, "getVoiceConnections"); function getVoiceConnection(guildId, group = "default") { return getVoiceConnections(group)?.get(guildId); } __name(getVoiceConnection, "getVoiceConnection"); function untrackVoiceConnection(voiceConnection) { return getVoiceConnections(voiceConnection.joinConfig.group)?.delete( voiceConnection.joinConfig.guildId ); } __name(untrackVoiceConnection, "untrackVoiceConnection"); function trackVoiceConnection(voiceConnection) { return getOrCreateGroup(voiceConnection.joinConfig.group).set( voiceConnection.joinConfig.guildId, voiceConnection ); } __name(trackVoiceConnection, "trackVoiceConnection"); var FRAME_LENGTH = 20; var audioCycleInterval; var nextTime = -1; var audioPlayers = []; function audioCycleStep() { if (nextTime === -1) return; nextTime += FRAME_LENGTH; const available = audioPlayers.filter((player) => player.checkPlayable()); for (const player of available) { player["_stepDispatch"](); } prepareNextAudioFrame(available); } __name(audioCycleStep, "audioCycleStep"); function prepareNextAudioFrame(players) { const nextPlayer = players.shift(); if (!nextPlayer) { if (nextTime >= 0) { audioCycleInterval = setTimeout( () => audioCycleStep(), Math.max(nextTime - Date.now(), 1) ); } return; } nextPlayer["_stepPrepare"](); setImmediate(() => prepareNextAudioFrame(players)); } __name(prepareNextAudioFrame, "prepareNextAudioFrame"); function hasAudioPlayer(target) { return audioPlayers.includes(target); } __name(hasAudioPlayer, "hasAudioPlayer"); function addAudioPlayer(player) { if (hasAudioPlayer(player)) return player; audioPlayers.push(player); if (audioPlayers.length === 1) { nextTime = Date.now(); setImmediate(() => audioCycleStep()); } return player; } __name(addAudioPlayer, "addAudioPlayer"); function deleteAudioPlayer(player) { const index = audioPlayers.indexOf(player); if (index === -1) return; audioPlayers.splice(index, 1); if (audioPlayers.length === 0) { nextTime = -1; if (audioCycleInterval !== void 0) clearTimeout(audioCycleInterval); } } __name(deleteAudioPlayer, "deleteAudioPlayer"); // src/networking/Networking.ts var import_node_buffer6 = require("buffer"); var import_node_crypto = __toESM(require("crypto")); var import_node_events5 = require("events"); var import_v82 = require("discord-api-types/voice/v8"); // src/util/Secretbox.ts var import_node_buffer = require("buffer"); var libs = { // eslint-disable-next-line @typescript-eslint/no-explicit-any "sodium-native": /* @__PURE__ */ __name((sodium) => ({ crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => { const cipherText = import_node_buffer.Buffer.alloc( plaintext.length + sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES ); sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( cipherText, plaintext, additionalData, null, nonce2, key ); return cipherText; }, "crypto_aead_xchacha20poly1305_ietf_encrypt") }), "sodium-native"), // eslint-disable-next-line @typescript-eslint/no-explicit-any sodium: /* @__PURE__ */ __name((sodium) => ({ crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => { return sodium.api.crypto_aead_xchacha20poly1305_ietf_encrypt( plaintext, additionalData, null, nonce2, key ); }, "crypto_aead_xchacha20poly1305_ietf_encrypt") }), "sodium"), // eslint-disable-next-line @typescript-eslint/no-explicit-any "libsodium-wrappers": /* @__PURE__ */ __name((sodium) => ({ crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => { return sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( plaintext, additionalData, null, nonce2, key ); }, "crypto_aead_xchacha20poly1305_ietf_encrypt") }), "libsodium-wrappers"), // eslint-disable-next-line @typescript-eslint/no-explicit-any "@stablelib/xchacha20poly1305": /* @__PURE__ */ __name((stablelib) => ({ crypto_aead_xchacha20poly1305_ietf_encrypt(cipherText, additionalData, nonce2, key) { const crypto2 = new stablelib.XChaCha20Poly1305(key); return crypto2.seal(nonce2, cipherText, additionalData); } }), "@stablelib/xchacha20poly1305"), // eslint-disable-next-line @typescript-eslint/no-explicit-any "@noble/ciphers/chacha": /* @__PURE__ */ __name((noble) => ({ crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, nonce2, key) { const chacha = noble.xchacha20poly1305(key, nonce2, additionalData); return chacha.encrypt(plaintext); } }), "@noble/ciphers/chacha") }; libs["sodium-javascript"] = libs["sodium-native"]; var validLibs = Object.keys(libs); var fallbackError = /* @__PURE__ */ __name(() => { throw new Error( `Cannot play audio as no valid encryption package is installed. - Install one of the following packages: ${validLibs.join(", ")} - Use the generateDependencyReport() function for more information. ` ); }, "fallbackError"); var methods = { crypto_aead_xchacha20poly1305_ietf_encrypt: fallbackError }; void (async () => { for (const libName of Object.keys(libs)) { try { const lib = await import(libName); if (libName === "libsodium-wrappers" && lib.ready) await lib.ready; Object.assign(methods, libs[libName](lib)); break; } catch { } } })(); // src/util/util.ts var noop = /* @__PURE__ */ __name(() => { }, "noop"); // src/networking/DAVESession.ts var import_node_buffer3 = require("buffer"); var import_node_events2 = require("events"); // src/audio/AudioPlayer.ts var import_node_buffer2 = require("buffer"); var import_node_events = require("events"); // src/audio/AudioPlayerError.ts var _AudioPlayerError = class _AudioPlayerError extends Error { constructor(error, resource) { super(error.message); /** * The resource associated with the audio player at the time the error was thrown. */ __publicField(this, "resource"); this.resource = resource; this.name = error.name; this.stack = error.stack; } }; __name(_AudioPlayerError, "AudioPlayerError"); var AudioPlayerError = _AudioPlayerError; // src/audio/PlayerSubscription.ts var _PlayerSubscription = class _PlayerSubscription { constructor(connection, player) { /** * The voice connection of this subscription. */ __publicField(this, "connection"); /** * The audio player of this subscription. */ __publicField(this, "player"); this.connection = connection; this.player = player; } /** * Unsubscribes the connection from the audio player, meaning that the * audio player cannot stream audio to it until a new subscription is made. */ unsubscribe() { this.connection["onSubscriptionRemoved"](this); this.player["unsubscribe"](this); } }; __name(_PlayerSubscription, "PlayerSubscription"); var PlayerSubscription = _PlayerSubscription; // src/audio/AudioPlayer.ts var SILENCE_FRAME = import_node_buffer2.Buffer.from([248, 255, 254]); var NoSubscriberBehavior = /* @__PURE__ */ ((NoSubscriberBehavior2) => { NoSubscriberBehavior2["Pause"] = "pause"; NoSubscriberBehavior2["Play"] = "play"; NoSubscriberBehavior2["Stop"] = "stop"; return NoSubscriberBehavior2; })(NoSubscriberBehavior || {}); var AudioPlayerStatus = /* @__PURE__ */ ((AudioPlayerStatus2) => { AudioPlayerStatus2["AutoPaused"] = "autopaused"; AudioPlayerStatus2["Buffering"] = "buffering"; AudioPlayerStatus2["Idle"] = "idle"; AudioPlayerStatus2["Paused"] = "paused"; AudioPlayerStatus2["Playing"] = "playing"; return AudioPlayerStatus2; })(AudioPlayerStatus || {}); function stringifyState(state) { return JSON.stringify({ ...state, resource: Reflect.has(state, "resource"), stepTimeout: Reflect.has(state, "stepTimeout") }); } __name(stringifyState, "stringifyState"); var _AudioPlayer = class _AudioPlayer extends import_node_events.EventEmitter { /** * Creates a new AudioPlayer. */ constructor(options = {}) { super(); /** * The state that the AudioPlayer is in. */ __publicField(this, "_state"); /** * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio * to the streams in this list. */ __publicField(this, "subscribers", []); /** * The behavior that the player should follow when it enters certain situations. */ __publicField(this, "behaviors"); /** * The debug logger function, if debugging is enabled. */ __publicField(this, "debug"); this._state = { status: "idle" /* Idle */ }; this.behaviors = { noSubscriber: "pause" /* Pause */, maxMissedFrames: 5, ...options.behaviors }; this.debug = options.debug === false ? null : (message) => this.emit("debug", message); } /** * A list of subscribed voice connections that can currently receive audio to play. */ get playable() { return this.subscribers.filter( ({ connection }) => connection.state.status === "ready" /* Ready */ ).map(({ connection }) => connection); } /** * Subscribes a VoiceConnection to the audio player's play list. If the VoiceConnection is already subscribed, * then the existing subscription is used. * * @remarks * This method should not be directly called. Instead, use VoiceConnection#subscribe. * @param connection - The connection to subscribe * @returns The new subscription if the voice connection is not yet subscribed, otherwise the existing subscription */ // @ts-ignore subscribe(connection) { const existingSubscription = this.subscribers.find( (subscription) => subscription.connection === connection ); if (!existingSubscription) { const subscription = new PlayerSubscription(connection, this); this.subscribers.push(subscription); setImmediate(() => this.emit("subscribe", subscription)); return subscription; } return existingSubscription; } /** * Unsubscribes a subscription - i.e. removes a voice connection from the play list of the audio player. * * @remarks * This method should not be directly called. Instead, use PlayerSubscription#unsubscribe. * @param subscription - The subscription to remove * @returns Whether or not the subscription existed on the player and was removed */ // @ts-ignore unsubscribe(subscription) { const index = this.subscribers.indexOf(subscription); const exists = index !== -1; if (exists) { this.subscribers.splice(index, 1); subscription.connection.setSpeaking(false); this.emit("unsubscribe", subscription); } return exists; } /** * The state that the player is in. */ get state() { return this._state; } /** * Sets a new state for the player, performing clean-up operations where necessary. */ set state(newState) { const oldState = this._state; const newResource = Reflect.get(newState, "resource"); if (oldState.status !== "idle" /* Idle */ && oldState.resource !== newResource) { oldState.resource.playStream.on("error", noop); oldState.resource.playStream.off("error", oldState.onStreamError); oldState.resource.audioPlayer = void 0; oldState.resource.playStream.destroy(); oldState.resource.playStream.read(); } if (oldState.status === "buffering" /* Buffering */ && (newState.status !== "buffering" /* Buffering */ || newState.resource !== oldState.resource)) { oldState.resource.playStream.off("end", oldState.onFailureCallback); oldState.resource.playStream.off("close", oldState.onFailureCallback); oldState.resource.playStream.off("finish", oldState.onFailureCallback); oldState.resource.playStream.off("readable", oldState.onReadableCallback); } if (newState.status === "idle" /* Idle */) { this._signalStopSpeaking(); deleteAudioPlayer(this); } if (newResource) { addAudioPlayer(this); } const didChangeResources = oldState.status !== "idle" /* Idle */ && newState.status === "playing" /* Playing */ && oldState.resource !== newState.resource; this._state = newState; this.emit("stateChange", oldState, this._state); if (oldState.status !== newState.status || didChangeResources) { this.emit(newState.status, oldState, this._state); } this.debug?.( `state change: from ${stringifyState(oldState)} to ${stringifyState( newState )}` ); } /** * Plays a new resource on the player. If the player is already playing a resource, the existing resource is destroyed * (it cannot be reused, even in another player) and is replaced with the new resource. * * @remarks * The player will transition to the Playing state once playback begins, and will return to the Idle state once * playback is ended. * * If the player was previously playing a resource and this method is called, the player will not transition to the * Idle state during the swap over. * @param resource - The resource to play * @throws Will throw if attempting to play an audio resource that has already ended, or is being played by another player */ play(resource) { if (resource.ended) { throw new Error("Cannot play a resource that has already ended."); } if (resource.audioPlayer) { if (resource.audioPlayer === this) { return; } throw new Error( "Resource is already being played by another audio player." ); } resource.audioPlayer = this; const onStreamError = /* @__PURE__ */ __name((error) => { if (this.state.status !== "idle" /* Idle */) { this.emit("error", new AudioPlayerError(error, this.state.resource)); } if (this.state.status !== "idle" /* Idle */ && this.state.resource === resource) { this.state = { status: "idle" /* Idle */ }; } }, "onStreamError"); resource.playStream.once("error", onStreamError); if (resource.started) { this.state = { status: "playing" /* Playing */, missedFrames: 0, playbackDuration: 0, resource, onStreamError }; } else { const onReadableCallback = /* @__PURE__ */ __name(() => { if (this.state.status === "buffering" /* Buffering */ && this.state.resource === resource) { this.state = { status: "playing" /* Playing */, missedFrames: 0, playbackDuration: 0, resource, onStreamError }; } }, "onReadableCallback"); const onFailureCallback = /* @__PURE__ */ __name(() => { if (this.state.status === "buffering" /* Buffering */ && this.state.resource === resource) { this.state = { status: "idle" /* Idle */ }; } }, "onFailureCallback"); resource.playStream.once("readable", onReadableCallback); resource.playStream.once("end", onFailureCallback); resource.playStream.once("close", onFailureCallback); resource.playStream.once("finish", onFailureCallback); this.state = { status: "buffering" /* Buffering */, resource, onReadableCallback, onFailureCallback, onStreamError }; } } /** * Pauses playback of the current resource, if any. * * @param interpolateSilence - If true, the player will play 5 packets of silence after pausing to prevent audio glitches * @returns `true` if the player was successfully paused, otherwise `false` */ pause(interpolateSilence = true) { if (this.state.status !== "playing" /* Playing */) return false; this.state = { ...this.state, status: "paused" /* Paused */, silencePacketsRemaining: interpolateSilence ? 5 : 0 }; return true; } /** * Unpauses playback of the current resource, if any. * * @returns `true` if the player was successfully unpaused, otherwise `false` */ unpause() { if (this.state.status !== "paused" /* Paused */) return false; this.state = { ...this.state, status: "playing" /* Playing */, missedFrames: 0 }; return true; } /** * Stops playback of the current resource and destroys the resource. The player will either transition to the Idle state, * or remain in its current state until the silence padding frames of the resource have been played. * * @param force - If true, will force the player to enter the Idle state even if the resource has silence padding frames * @returns `true` if the player will come to a stop, otherwise `false` */ stop(force = false) { if (this.state.status === "idle" /* Idle */) return false; if (force || this.state.resource.silencePaddingFrames === 0) { this.state = { status: "idle" /* Idle */ }; } else if (this.state.resource.silenceRemaining === -1) { this.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames; } return true; } /** * Checks whether the underlying resource (if any) is playable (readable) * * @returns `true` if the resource is playable, otherwise `false` */ checkPlayable() { const state = this._state; if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return false; if (!state.resource.readable) { this.state = { status: "idle" /* Idle */ }; return false; } return true; } /** * Called roughly every 20ms by the global audio player timer. Dispatches any audio packets that are buffered * by the active connections of this audio player. */ // @ts-ignore _stepDispatch() { const state = this._state; if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return; for (const connection of this.playable) { connection.dispatchAudio(); } } /** * Called roughly every 20ms by the global audio player timer. Attempts to read an audio packet from the * underlying resource of the stream, and then has all the active connections of the audio player prepare it * (encrypt it, append header data) so that it is ready to play at the start of the next cycle. */ // @ts-ignore _stepPrepare() { const state = this._state; if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return; const playable = this.playable; if (state.status === "autopaused" /* AutoPaused */ && playable.length > 0) { this.state = { ...state, status: "playing" /* Playing */, missedFrames: 0 }; } if (state.status === "paused" /* Paused */ || state.status === "autopaused" /* AutoPaused */) { if (state.silencePacketsRemaining > 0) { state.silencePacketsRemaining--; this._preparePacket(SILENCE_FRAME, playable, state); if (state.silencePacketsRemaining === 0) { this._signalStopSpeaking(); } } return; } if (playable.length === 0) { if (this.behaviors.noSubscriber === "pause" /* Pause */) { this.state = { ...state, status: "autopaused" /* AutoPaused */, silencePacketsRemaining: 5 }; return; } else if (this.behaviors.noSubscriber === "stop" /* Stop */) { this.stop(true); } } const packet = state.resource.read(); if (state.status === "playing" /* Playing */) { if (packet) { this._preparePacket(packet, playable, state); state.missedFrames = 0; } else { this._preparePacket(SILENCE_FRAME, playable, state); state.missedFrames++; if (state.missedFrames >= this.behaviors.maxMissedFrames) { this.stop(); } } } } /** * Signals to all the subscribed connections that they should send a packet to Discord indicating * they are no longer speaking. Called once playback of a resource ends. */ _signalStopSpeaking() { for (const { connection } of this.subscribers) { connection.setSpeaking(false); } } /** * Instructs the given connections to each prepare this packet to be played at the start of the * next cycle. * * @param packet - The Opus packet to be prepared by each receiver * @param receivers - The connections that should play this packet */ _preparePacket(packet, receivers, state) { state.playbackDuration += 20; for (const connection of receivers) { connection.prepareAudioPacket(packet); } } }; __name(_AudioPlayer, "AudioPlayer"); var AudioPlayer = _AudioPlayer; function createAudioPlayer(options) { return new AudioPlayer(options); } __name(createAudioPlayer, "createAudioPlayer"); // src/networking/DAVESession.ts var TRANSITION_EXPIRY = 10; var TRANSITION_EXPIRY_PENDING_DOWNGRADE = 24; var DEFAULT_DECRYPTION_FAILURE_TOLERANCE = 36; var Davey; try { Davey = require("@snazzah/davey"); } catch { Davey = new Proxy({}, { get(_, prop) { throw new Error( `DAVE protocol support requires the @snazzah/davey package. Please install it to use this feature. Attempted to access property ${String( prop )} on the DAVESession (class), but Davey is not available.` ); } }); } function getMaxProtocolVersion() { return Davey.DAVE_PROTOCOL_VERSION; } __name(getMaxProtocolVersion, "getMaxProtocolVersion"); var _DAVESession = class _DAVESession extends import_node_events2.EventEmitter { constructor(protocolVersion, userId, channelId, options) { super(); /** * The channel id represented by this session. */ __publicField(this, "channelId"); /** * The user id represented by this session. */ __publicField(this, "userId"); /** * The protocol version being used. */ __publicField(this, "protocolVersion"); /** * The last transition id executed. */ __publicField(this, "lastTransitionId"); /** * The pending transitions, mapped by their ID and the protocol version. */ __publicField(this, "pendingTransitions", /* @__PURE__ */ new Map()); /** * Whether this session was downgraded previously. */ __publicField(this, "downgraded", false); /** * The amount of consecutive failures encountered when decrypting. */ __publicField(this, "consecutiveFailures", 0); /** * The amount of consecutive failures needed to attempt to recover. */ __publicField(this, "failureTolerance"); /** * Whether this session is currently re-initializing due to an invalid transition. */ __publicField(this, "reinitializing", false); /** * The underlying DAVE Session of this wrapper. */ __publicField(this, "session"); this.protocolVersion = protocolVersion; this.userId = userId; this.channelId = channelId; this.failureTolerance = options.decryptionFailureTolerance ?? DEFAULT_DECRYPTION_FAILURE_TOLERANCE; } /** * The current voice privacy code of the session. Will be `null` if there is no session. */ get voicePrivacyCode() { if (this.protocolVersion === 0 || !this.session?.voicePrivacyCode) { return null; } return this.session.voicePrivacyCode; } /** * Gets the verification code for a user in the session. * * @throws Will throw if there is not an active session or the user id provided is invalid or not in the session. */ async getVerificationCode(userId) { if (!this.session) throw new Error("Session not available"); return this.session.getVerificationCode(userId); } /** * Re-initializes (or initializes) the underlying session. */ reinit() { if (this.protocolVersion > 0) { if (this.session) { this.session.reinit(this.protocolVersion, this.userId, this.channelId); this.emit( "debug", `Session reinitialized for protocol version ${this.protocolVersion}` ); } else { this.session = new Davey.DAVESession( this.protocolVersion, this.userId, this.channelId ); this.emit( "debug", `Session initialized for protocol version ${this.protocolVersion}` ); } this.emit("keyPackage", this.session.getSerializedKeyPackage()); } else if (this.session) { this.session.reset(); this.session.setPassthroughMode(true, TRANSITION_EXPIRY); this.emit("debug", "Session reset"); } } /** * Set the external sender for this session. * * @param externalSender - The external sender */ setExternalSender(externalSender) { if (!this.session) throw new Error("No session available"); this.session.setExternalSender(externalSender); this.emit("debug", "Set MLS external sender"); } /** * Prepare for a transition. * * @param data - The transition data * @returns Whether we should signal to the voice server that we are ready */ prepareTransition(data) { this.emit( "debug", `Preparing for transition (${data.transition_id}, v${data.protocol_version})` ); this.pendingTransitions.set(data.transition_id, data.protocol_version); if (data.transition_id === 0) { this.executeTransition(data.transition_id); } else { if (data.protocol_version === 0) this.session?.setPassthroughMode( true, TRANSITION_EXPIRY_PENDING_DOWNGRADE ); return true; } return false; } /** * Execute a transition. * * @param transitionId - The transition id to execute on */ executeTransition(transitionId) { this.emit("debug", `Executing transition (${transitionId})`); if (!this.pendingTransitions.has(transitionId)) { this.emit( "debug", `Received execute transition, but we don't have a pending transition for ${transitionId}` ); return false; } const oldVersion = this.protocolVersion; this.protocolVersion = this.pendingTransitions.get(transitionId); if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) { this.downgraded = true; this.emit("debug", "Session downgraded"); } else if (transitionId > 0 && this.downgraded) { this.downgraded = false; this.session?.setPassthroughMode(true, TRANSITION_EXPIRY); this.emit("debug", "Session upgraded"); } this.reinitializing = false; this.lastTransitionId = transitionId; this.emit( "debug", `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})` ); this.pendingTransitions.delete(transitionId); return true; } /** * Prepare for a new epoch. * * @param data - The epoch data */ prepareEpoch(data) { this.emit("debug", `Preparing for epoch (${data.epoch})`); if (data.epoch === 1) { this.protocolVersion = data.protocol_version; this.reinit(); } } /** * Recover from an invalid transition by re-initializing. * * @param transitionId - The transition id to invalidate */ recoverFromInvalidTransition(transitionId) { if (this.reinitializing) return; this.emit("debug", `Invalidating transition ${transitionId}`); this.reinitializing = true; this.consecutiveFailures = 0; this.emit("invalidateTransition", transitionId); this.reinit(); } /** * Processes proposals from the MLS group. * * @param payload - The binary message payload * @param connectedClients - The set of connected client IDs * @returns The payload to send back to the voice server, if there is one */ processProposals(payload, connectedClients) { if (!this.session) throw new Error("No session available"); const optype = payload.readUInt8(0); const { commit, welcome } = this.session.processProposals( optype, payload.subarray(1), Array.from(connectedClients) ); this.emit("debug", "MLS proposals processed"); if (!commit) return; return welcome ? import_node_buffer3.Buffer.concat([commit, welcome]) : commit; } /** * Processes a commit from the MLS group. * * @param payload - The payload * @returns The transaction id and whether it was successful */ processCommit(payload) { if (!this.session) throw new Error("No session available"); const transitionId = payload.readUInt16BE(0); try { this.session.processCommit(payload.subarray(2)); if (transitionId === 0) { this.reinitializing = false; this.lastTransitionId = transitionId; } else { this.pendingTransitions.set(transitionId, this.protocolVersion); } this.emit( "debug", `MLS commit processed (transition id: ${transitionId})` ); return { transitionId, success: true }; } catch (error) { this.emit( "debug", `MLS commit errored from transition ${transitionId}: ${error}` ); this.recoverFromInvalidTransition(transitionId); return { transitionId, success: false }; } } /** * Processes a welcome from the MLS group. * * @param payload - The payload * @returns The transaction id and whether it was successful */ processWelcome(payload) { if (!this.session) throw new Error("No session available"); const transitionId = payload.readUInt16BE(0); try { this.session.processWelcome(payload.subarray(2)); if (transitionId === 0) { this.reinitializing = false; this.lastTransitionId = transitionId; } else { this.pendingTransitions.set(transitionId, this.protocolVersion); } this.emit( "debug", `MLS welcome processed (transition id: ${transitionId})` ); return { transitionId, success: true }; } catch (error) { this.emit( "debug", `MLS welcome errored from transition ${transitionId}: ${error}` ); this.recoverFromInvalidTransition(transitionId); return { transitionId, success: false }; } } /** * Encrypt a packet using end-to-end encryption. * * @param packet - The packet to encrypt */ encrypt(packet) { if (this.protocolVersion === 0 || !this.session?.ready || packet.equals(SILENCE_FRAME)) return packet; return this.session.encryptOpus(packet); } /** * Decrypt a packet using end-to-end encryption. * * @param packet - The packet to decrypt * @param userId - The user id that sent the packet * @returns The decrypted packet, or `null` if the decryption failed but should be ignored */ decrypt(packet, userId) { const canDecrypt = this.session?.ready && (this.protocolVersion !== 0 || this.session?.canPassthrough(userId)); if (packet.equals(SILENCE_FRAME) || !canDecrypt || !this.session) return packet; try { const buffer = this.session.decrypt( userId, // @ts-expect-error - const enum is exported and works (todo: drop const modifier on Davey end) Davey.MediaType.AUDIO, packet ); this.consecutiveFailures = 0; return buffer; } catch (error) { if (!this.reinitializing && this.pendingTransitions.size === 0) { this.consecutiveFailures++; this.emit( "debug", `Failed to decrypt a packet (${this.consecutiveFailures} consecutive fails)` ); if (this.consecutiveFailures > this.failureTolerance) { if (this.lastTransitionId) this.recoverFromInvalidTransition(this.lastTransitionId); else throw error; } } else if (this.reinitializing) { this.emit( "debug", "Failed to decrypt a packet (reinitializing session)" ); } else if (this.pendingTransitions.size > 0) { this.emit( "debug", `Failed to decrypt a packet (${this.pendingTransitions.size} pending transition[s])` ); } } return null; } /** * Resets the session. */ destroy() { try { this.session?.reset(); } catch { } } }; __name(_DAVESession, "DAVESession"); var DAVESession = _DAVESession; // src/networking/VoiceUDPSocket.ts var import_node_buffer4 = require("buffer"); var import_node_dgram = require("dgram"); var import_node_events3 = require("events"); var import_node_net = require("net"); function parseLocalPacket(message) { const packet = import_node_buffer4.Buffer.from(message); const ip = packet.subarray(8, packet.indexOf(0, 8)).toString("utf8"); if (!(0, import_node_net.isIPv4)(ip)) { throw new Error("Malformed IP address"); } const port = packet.readUInt16BE(packet.length - 2); return { ip, port }; } __name(parseLocalPacket, "parseLocalPacket"); var KEEP_ALIVE_INTERVAL = 5e3; var MAX_COUNTER_VALUE = 2 ** 32 - 1; var _VoiceUDPSocket = class _VoiceUDPSocket extends import_node_events3.EventEmitter { /** * Creates a new VoiceUDPSocket. * * @param remote - Details of the remote socket */ constructor(remote) { super(); /** * The underlying network Socket for the VoiceUDPSocket. */ __publicField(this, "socket"); /** * The socket details for Discord (remote) */ __publicField(this, "remote"); /** * The counter used in the keep alive mechanism. */ __publicField(this, "keepAliveCounter", 0); /** * The buffer used to write the keep alive counter into. */ __publicField(this, "keepAliveBuffer"); /** * The Node.js interval for the keep-alive mechanism. */ __publicField(this, "keepAliveInterval"); /** * The time taken to receive a response to keep alive messages. * * @deprecated This field is no longer updated as keep alive messages are no longer tracked. */ __publicField(this, "ping"); this.socket = (0, import_node_dgram.createSocket)("udp4"); this.socket.on("error", (error) => this.emit("error", error)); this.socket.on("message", (buffer) => this.onMessage(buffer)); this.socket.on("close", () => this.emit("close")); this.remote = remote; this.keepAliveBuffer = import_node_buffer4.Buffer.alloc(8); this.keepAliveInterval = setInterval( () => this.keepAlive(), KEEP_ALIVE_INTERVAL ); setImmediate(() => this.keepAlive()); } /** * Called when a message is received on the UDP socket. * * @param buffer - The received buffer */ onMessage(buffer) { this.emit("message", buffer); } /** * Called at a regular interval to check whether we are still able to send datagrams to Discord. */ keepAlive() { this.keepAliveBuffer.writeUInt32LE(this.keepAliveCounter, 0); this.send(this.keepAliveBuffer); this.keepAliveCounter++; if (this.keepAliveCounter > MAX_COUNTER_VALUE) { this.keepAliveCounter = 0; } } /** * Sends a buffer to Discord. * * @param buffer - The buffer to send */ send(buffer) { this.socket.send(buffer, this.remote.port, this.remote.ip); } /** * Closes the socket, the instance will not be able to be reused. */ destroy() { try { this.socket.close(); } catch { } clearInterval(this.keepAliveInterval); } /** * Performs IP discovery to discover the local address and port to be used for the voice connection. * * @param ssrc - The SSRC received from Discord */ async performIPDiscovery(ssrc) { return new Promise((resolve, reject) => { const listener = /* @__PURE__ */ __name((message) => { try { if (message.readUInt16BE(0) !== 2) return; const packet = parseLocalPacket(message); this.socket.off("message", listener); resolve(packet); } catch { } }, "listener"); this.socket.on("message", listener); this.socket.once( "close", () => reject(new Error("Cannot perform IP discovery - socket closed")) ); const discoveryBuffer = import_node_buffer4.Buffer.alloc(74); discoveryBuffer.writeUInt16BE(1, 0); discoveryBuffer.writeUInt16BE(70, 2); discoveryBuffer.writeUInt32BE(ssrc, 4); this.send(discoveryBuffer); }); } }; __name(_VoiceUDPSocket, "VoiceUDPSocket"); var VoiceUDPSocket = _VoiceUDPSocket; // src/networking/VoiceWebSocket.ts var import_node_buffer5 = require("buffer"); var import_node_events4 = require("events"); var import_v8 = require("discord-api-types/voice/v8"); var import_ws = __toESM(require("ws")); var _VoiceWebSocket = class _VoiceWebSocket extends import_node_events4.EventEmitter { /** * Creates a new VoiceWebSocket. * * @param address - The address to connect to */ constructor(address, debug) { super(); /** * The current heartbeat interval, if any. */ __publicField(this, "heartbeatInterval"); /** * The time (milliseconds since UNIX epoch) that the last heartbeat acknowledgement packet was received. * This is set to 0 if an acknowledgement packet hasn't been received yet. */ __publicField(this, "lastHeartbeatAck"); /** * The time (milliseconds since UNIX epoch) that the last heartbeat was sent. This is set to 0 if a heartbeat * hasn't been sent yet. */ __publicField(this, "lastHeartbeatSend"); /** * The number of consecutively missed heartbeats. */ __publicField(this, "missedHeartbeats", 0); /** * The last recorded ping. */ __publicField(this, "ping"); /** * The last sequence number acknowledged from Discord. Will be `-1` if no sequence numbered messages have been received. */ __publicField(this, "sequence", -1); /** * The debug logger function, if debugging is enabled. */ __publicField(this, "debug"); /** * The underlying WebSocket of this wrapper. */ __publicField(this, "ws"); this.ws = new import_ws.default(address); this.ws.onmessage = (err) => this.onMessage(err); this.ws.onopen = (err) => this.emit("open", err); this.ws.onerror = (err) => this.emit("error", err instanceof Error ? err : err.error); this.ws.onclose = (err) => this.emit("close", err); this.lastHeartbeatAck = 0; this.lastHeartbeatSend = 0; this.debug = debug ? (message) => this.emit("debug", message) : null; } /** * Destroys the VoiceWebSocket. The heartbeat interval is cleared, and the connection is closed. */ destroy() { try { this.debug?.("destroyed"); this.setHeartbeatInterval(-1); this.ws.close(1e3); } catch (error) { const err = error; this.emit("error", err); } } /** * Handles message events on the WebSocket. Attempts to JSON parse the messages and emit them * as packets. Binary messages will be parsed and emitted. * * @param event - The message event */ onMessage(event) { if (event.data instanceof import_node_buffer5.Buffer || event.data instanceof ArrayBuffer) { const buffer = event.data instanceof ArrayBuffer ? import_node_buffer5.Buffer.from(event.data) : event.data; const seq = buffer.readUInt16BE(0); const op = buffer.readUInt8(2); const payload = buffer.subarray(3); this.sequence = seq; this.debug?.( `<< [bin] opcode ${op}, seq ${seq}, ${payload.byteLength} bytes` ); this.emit("binary", { op, seq, payload }); return; } else if (typeof event.data !== "string") { return; } this.debug?.(`<< ${event.data}`); let packet; try { packet = JSON.parse(event.data); } catch (error) { const err = error; this.emit("error", err); return; } if (packet.seq) { this.sequence = packet.seq; } if (packet.op === import_v8.VoiceOpcodes.HeartbeatAck) { this.lastHeartbeatAck = Date.now(); this.missedHeartbeats = 0; this.ping = this.lastHeartbeatAck - this.lastHeartbeatSend; } this.emit("packet", packet); } /** * Sends a JSON-stringifiable packet over the WebSocket. * * @param packet - The packet to send */ sendPacket(packet) { try { const stringified = JSON.stringify(packet); this.debug?.(`>> ${stringified}`); this.ws.send(stringified); } catch (error) { const err = error; this.emit("error", err); } } /** * Sends a binary message over the WebSocket. * * @param opcode - The opcode to use * @param payload - The payload to send */ sendBinaryMessage(opcode, payload) { try { const message = import_node_buffer5.Buffer.concat([new Uint8Array([opcode]), payload]); this.debug?.(`>> [bin] opcode ${opcode}, ${payload.byteLength} bytes`); this.ws.send(message); } catch (error) { const err = error; this.emit("error", err); } } /** * Sends a heartbeat over the WebSocket. */ sendHeartbeat() { this.lastHeartbeatSend = Date.now(); this.missedHeartbeats++; const nonce2 = this.lastHeartbeatSend; this.sendPacket({ op: import_v8.VoiceOpcodes.Heartbeat, // eslint-disable-next-line id-length d: { // eslint-disable-next-line id-length t: nonce2, seq_ack: this.sequence } }); } /** * Sets/clears an interval to send heartbeats over the WebSocket. * * @param ms - The interval in milliseconds. If negative, the interval will be unset */ setHeartbeatInterval(ms) { if (this.heartbeatInterval !== void 0) clearInterval(this.heartbeatInterval); if (ms > 0) { this.heartbeatInterval = setInterval(() => { if (this.lastHeartbeatSend !== 0 && this.missedHeartbeats >= 3) { this.ws.close(); this.setHeartbeatInterval(-1); } this.sendHeartbeat(); }, ms); } } }; __name(_VoiceWebSocket, "VoiceWebSocket"); var VoiceWebSocket = _VoiceWebSocket; // src/networking/Networking.ts var CHANNELS = 2; var TIMESTAMP_INC = 48e3 / 100 * CHANNELS; var MAX_NONCE_SIZE = 2 ** 32 - 1; var SUPPORTED_ENCRYPTION_MODES = [ import_v82.VoiceEncryptionMode.AeadXChaCha20Poly1305RtpSize ]; if (import_node_crypto.default.getCiphers().includes("aes-256-gcm")) { SUPPORTED_ENCRYPTION_MODES.unshift(import_v82.VoiceEncryptionMode.AeadAes256GcmRtpSize); } var nonce = import_node_buffer6.Buffer.alloc(24); function stringifyState2(state) { return JSON.stringify({ ...state, ws: Reflect.has(state, "ws"), udp: Reflect.has(state, "udp") }); } __name(stringifyState2, "stringifyState"); function chooseEncryptionMode(options) { const option = options.find( (option2) => SUPPORTED_ENCRYPTION_MODES.includes(option2) ); if (!option) { throw new Error( `No compatible encryption modes. Available include: ${options.join( ", " )}` ); } return option; } __name(chooseEncryptionMode, "chooseEncryptionMode"); function randomNBit(numberOfBits) { return Math.floor(Math.random() * 2 ** numberOfBits); } __name(randomNBit, "randomNBit"); var _Networking = class _Networking extends import_node_events5.EventEmitter { /** * Creates a new Networking instance. */ constructor(connectionOptions, options) { super(); __publicField(this, "_state"); /** * The debug logger function, if debugging is enabled. */ __publicField(this, "debug"); /** * The options used to create this Networking instance. */ __publicField(this, "options"); this.onWsOpen = this.onWsOpen.bind(this); this.onChildError = this.onChildError.bind(this); this.onWsPacket = this.onWsPacket.bind(this); this.onWsBinary = this.onWsBinary.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.onDaveDebug = this.onDaveDebug.bind(this); this.onDaveKeyPackage = this.onDaveKeyPackage.bind(this); this.onDaveInvalidateTransition = this.onDaveInvalidateTransition.bind(this); this.debug = options?.debug ? (message) => this.emit("debug", message) : null; this._state = { code: 0 /* OpeningWs */, ws: this.createWebSocket(connectionOptions.endpoint), connectionOptions }; this.options = options; } /** * Destroys the Networking instance, transitioning it into the Closed state. */ destroy() { this.state = { code: 6 /* Closed */ }; } /** * The current state of the networking instance. * * @remarks * The setter will perform clean-up operations where necessary. */ get state() { return this._state; } set state(newState) { const oldWs = Reflect.get(this._state, "ws"); const newWs = Reflect.get(newState, "ws"); 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("binary", this.onWsBinary); oldWs.off("close", this.onWsClose); oldWs.destroy(); } const oldUdp = Reflect.get(this._state, "udp"); const newUdp = Reflect.get(newState, "udp"); 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 oldDave = Reflect.get(this._state, "dave"); const newDave = Reflect.get(newState, "dave"); if (oldDave && oldDave !== newDave) { oldDave.off("error", this.onChildError); oldDave.off("debug", this.onDaveDebug); oldDave.off("keyPackage", this.onDaveKeyPackage); oldDave.off("invalidateTransition", this.onDaveInvalidateTransition); oldDave.destroy();