diff --git a/.env.example b/.env.example index d0d3c392..fb65cc35 100644 --- a/.env.example +++ b/.env.example @@ -18,31 +18,12 @@ MONITOR_GUILD_ID=your_guild_id_here # Target guild for text monitoring TEXT_GUILD_ID=optional_text_guild_id # Override text capture guild (falls back to MONITOR_GUILD_ID) TEXT_CHANNEL_ID=optional_text_channel_id # Restrict text capture to a single channel -# === Voice Channels === -# VOICE_GUILD_ID= # Guild for voice connection (optional) -# VOICE_CHANNEL_ID= # Channel for voice connection (optional) - -# === Recording === -RECORDINGS_DIR=./recordings # Audio file output directory (default: ./recordings) -RECORDING_SEGMENT_MS=5000 # OGG segment duration in ms (default: 5000) - -# === Decoder === -DECODER_ROTATE_MS=5000 # Opus decoder rotation interval in ms (default: 5000) -DECODER_COOLDOWN_MS=30000 # Decoder error cooldown in ms (default: 30000) - -# === Audio === -AUDIO_STREAM_SILENCE_DURATION_MS=3000 # Silence threshold in ms before stopping stream (default: 3000) -PACKET_FILTER_MIN_SIZE=8 # Minimum Opus packet size in bytes (default: 8) -OPUS_FRAME_SIZE=960 # Opus frame size in samples (default: 960) -AUDIO_SAMPLE_RATE=48000 # Audio sample rate in Hz (default: 48000) -AUDIO_CHANNELS=2 # Number of audio channels (default: 2) AVATAR_SIZE=64 # User avatar size in pixels (default: 64) # === Webserver === WEBSERVER_PORT=4001 # Backend HTTP/WS server port (default: 4001) # === Connection === -VOICE_CONNECTION_TIMEOUT_MS=15000 # Voice connection timeout in ms (default: 15000) RECONNECT_TIMEOUT_MS=5000 # Reconnect timeout in ms (default: 5000) # === Logging === @@ -69,11 +50,6 @@ POSTGRES_POOL_MAX=10 # Maximum pool connections (default: 10) # === Redis === REDIS_URL=redis://100.121.180.82:6379 # Redis connection string (default: redis://localhost:6379) -# === Voice PCM WebSocket (direct gateway→backend, bypasses Redis) === -VOICE_PCM_WS_ENABLED=true # Use direct WS for PCM audio (default: true) -BACKEND_WS_URL=ws://backend:4001/ws # Backend WebSocket URL for gateway PCM streaming -BACKEND_WS_TOKEN= # REQUIRED if VOICE_PCM_WS_ENABLED=true. Internal shared secret - # === Attachments === TELE_UPLOAD_URL=https://upload.asepharyana.my.id/api/upload # Attachment upload endpoint (default) ATTACHMENT_UPLOAD_TIMEOUT_MS=30000 # Upload timeout in ms (default: 30000) @@ -132,7 +108,6 @@ AUTO_DELETE_LOG_CHANNEL_ID= # Channel ID to log auto-delete actions # === Retention (0 = disabled) === RETENTION_MESSAGES_DAYS=0 # Message retention in days (default: 0 = off) RETENTION_ATTACHMENTS_DAYS=0 # Attachment retention in days (default: 0 = off) -RETENTION_VOICE_DAYS=0 # Voice recording retention in days (default: 0 = off) RETENTION_CLEANUP_INTERVAL_MS=86400000 # Cleanup interval in ms (default: 24h) RETENTION_DRY_RUN=true # Dry-run: log but do not delete (default: true) diff --git a/services/backend/package.json b/services/backend/package.json index c568db24..46d404a7 100644 --- a/services/backend/package.json +++ b/services/backend/package.json @@ -20,7 +20,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@discordjs/voice": "^0.19.2", "@orpc/server": "1.15.2", "axios": "^1.20.0", "dotenv": "^18.0.1", diff --git a/services/backend/src/e2e.test.ts b/services/backend/src/e2e.test.ts index 9ec4cb83..6145a5c3 100644 --- a/services/backend/src/e2e.test.ts +++ b/services/backend/src/e2e.test.ts @@ -41,20 +41,6 @@ describe("API Dashboard", () => { }); }); -describe("API Recordings", () => { - it("GET /recordings returns items with pagination", async () => { - const { status, body } = await api("/recordings?limit=5"); - expect(status).toBe(200); - expect(body).toHaveProperty("items"); - expect(Array.isArray(body.items)).toBe(true); - if (body.items.length > 0) { - expect(body.items[0]).toHaveProperty("id"); - expect(body.items[0]).toHaveProperty("username"); - expect(body.items[0]).toHaveProperty("created_at"); - } - }); -}); - describe("API Config", () => { it("GET /config returns 200", async () => { const { status } = await api("/config"); @@ -62,13 +48,6 @@ describe("API Config", () => { }); }); -describe("API Voice", () => { - it("GET /guilds returns 200", async () => { - const { status } = await api("/guilds"); - expect(status).toBe(200); - }); -}); - describe("API Negative", () => { it("GET /nonexistent returns 404", async () => { const { status } = await api("/nonexistent"); diff --git a/services/backend/src/modules/media/media.schema.ts b/services/backend/src/modules/media/media.schema.ts deleted file mode 100644 index 976d891b..00000000 --- a/services/backend/src/modules/media/media.schema.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from "zod"; - -export const mediaQueueSchema = z.object({ - source: z.string().min(1, "source is required"), - mode: z.enum(["music", "screen"]).default("music"), -}); - -export const mediaLoopSchema = z.object({ - loop: z.boolean().default(false), -}); - -export type MediaQueueInput = z.infer; -export type MediaLoopInput = z.infer; diff --git a/services/backend/src/modules/media/media.service.ts b/services/backend/src/modules/media/media.service.ts deleted file mode 100644 index 28e0b54d..00000000 --- a/services/backend/src/modules/media/media.service.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { - createChildLogger, - tryCommandThenFallback, -} from "../../shared/commandHelper.js"; -import { - COMMAND_MEDIA_LOOP, - COMMAND_MEDIA_QUEUE, - COMMAND_MEDIA_SKIP, - COMMAND_MEDIA_STOP, - MEDIA_STATUS_KEY, -} from "../../shared/index.js"; -import { publishCommand, readRedisStatus } from "../../shared/redis/index.js"; - -const logger = createChildLogger("media.service"); - -// --------------------------------------------------------------------------- -// Types — match frontend exactly -// --------------------------------------------------------------------------- - -export interface MediaItem { - id?: string; - source: string; - title: string; - mode?: "music" | "screen"; - durationMs?: number | null; - thumbnailUrl?: string | null; -} - -export interface MediaState { - playing: boolean; - /** null/absent when idle; "music" | "screen" while a track is active. */ - activeMode?: "music" | "screen" | null; - musicVolume: number; - loop: boolean; - current: MediaItem | null; - queue: MediaItem[]; -} - -// --------------------------------------------------------------------------- -// Defaults -// --------------------------------------------------------------------------- - -const DEFAULT_COMMAND_TIMEOUT_MS = 5000; - -const DEFAULT_STATE: MediaState = { - playing: false, - activeMode: null, - musicVolume: 0.3, - loop: false, - current: null, - queue: [], -}; - -// --------------------------------------------------------------------------- -// Normalisation — handle both boolean (new) and string (legacy) playing values -// --------------------------------------------------------------------------- - -function normalizeMediaState(raw: Record): MediaState { - const rawPlaying = raw.playing; - const playing = - rawPlaying === true || - rawPlaying === "playing" || - rawPlaying === "buffering"; - const mode = raw.activeMode; - const activeMode: "music" | "screen" | null = - mode === "music" || mode === "screen" ? mode : null; - return { - playing, - activeMode, - musicVolume: Number(raw.musicVolume ?? 0.3), - loop: Boolean(raw.loop ?? false), - current: (raw.current as MediaItem | null) ?? null, - queue: (raw.queue as MediaItem[]) ?? [], - }; -} - -type MediaReplyData = Record | MediaState; - -function _fromReply(data: MediaReplyData): MediaState { - return normalizeMediaState(data as Record); -} - -async function readStatusFallback(): Promise { - const cached = await readRedisStatus(MEDIA_STATUS_KEY); - return cached ? normalizeMediaState(cached) : DEFAULT_STATE; -} - -// --------------------------------------------------------------------------- -// Service methods -// --------------------------------------------------------------------------- - -/** - * Read media status from Redis key `media:status` set by discord-gateway. - */ -export async function getStatus(): Promise { - logger.debug("getStatus called"); - return readStatusFallback(); -} - -/** - * Queue a media source via Redis command to discord-gateway. - */ -export async function queue( - source: string, - mode: "music" | "screen" = "music", -): Promise { - logger.info({ source, mode }, "queue called"); - return tryCommandThenFallback( - () => - publishCommand( - COMMAND_MEDIA_QUEUE, - // NOTE: gateway MediaHandler reads `payload.url` (not `source`) — - // keep the field name aligned or playback silently no-ops. - { url: source, mode }, - DEFAULT_COMMAND_TIMEOUT_MS, - ), - () => readStatusFallback(), - "queue", - ); -} - -/** - * Skip current track via Redis command to discord-gateway. - */ -export async function skip(): Promise { - logger.info("skip called"); - return tryCommandThenFallback( - () => - publishCommand( - COMMAND_MEDIA_SKIP, - {}, - DEFAULT_COMMAND_TIMEOUT_MS, - ), - () => readStatusFallback(), - "skip", - ); -} - -/** - * Stop playback via Redis command to discord-gateway. - */ -export async function stop(): Promise { - logger.info("stop called"); - return tryCommandThenFallback( - () => - publishCommand( - COMMAND_MEDIA_STOP, - {}, - DEFAULT_COMMAND_TIMEOUT_MS, - ), - () => readStatusFallback(), - "stop", - ); -} - -/** - * Toggle loop mode (replay current track on natural end) via Redis command. - */ -export async function setLoop(loop: boolean): Promise { - logger.info({ loop }, "setLoop called"); - return tryCommandThenFallback( - () => - publishCommand( - COMMAND_MEDIA_LOOP, - { loop }, - DEFAULT_COMMAND_TIMEOUT_MS, - ), - () => readStatusFallback(), - "setLoop", - ); -} diff --git a/services/backend/src/modules/recordings/recordings.service.ts b/services/backend/src/modules/recordings/recordings.service.ts deleted file mode 100644 index 47b06acb..00000000 --- a/services/backend/src/modules/recordings/recordings.service.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { - and, - desc, - eq, - gte, - ilike, - lt, - lte, - or, - type SQL, - sql, -} from "drizzle-orm"; -import { getDatabase } from "../../shared/database/index.js"; -import { pgVoiceRecordingsTable } from "../../shared/index.js"; -import { createChildLogger } from "../../shared/logger/index.js"; - -const logger = createChildLogger("recordings.service"); - -export interface RecordingRow { - id: string; - user_id: string; - username: string; - avatar_url: string | null; - guild_id: string | null; - channel_id: string | null; - channel_name: string | null; - filename: string; - size_bytes: number; - download_url: string | null; - upload_status: string; - upload_error: string | null; - created_at: number; - uploaded_at: number | null; - transcription: string | null; -} - -export interface PaginatedRecordings { - items: RecordingRow[]; - nextCursor: string | null; - hasMore: boolean; -} - -export interface RecordingFilters { - channelId?: string; - userId?: string; - cursor?: string; - /** keyword search against transcription + username (ILIKE) */ - q?: string; - /** created_at lower bound (epoch ms) */ - startDate?: number; - /** created_at upper bound (epoch ms) */ - endDate?: number; -} - -export interface SpeakerSummary { - user_id: string; - username: string; - avatar_url: string | null; - clips: number; - /** summed MP3 bytes; ~128kbps → est duration in seconds */ - est_duration_s: number; - words: number; - transcribed: number; - last_at: number; -} - -export class RecordingsService { - async getRecent( - limit = 50, - filters: RecordingFilters = {}, - ): Promise { - logger.info({ limit, filters }, "getRecent called"); - const db = getDatabase(); - - const conditions: SQL[] = []; - - if (filters.cursor) { - conditions.push( - lt(pgVoiceRecordingsTable.created_at, Number(filters.cursor)), - ); - } - if (filters.channelId) { - conditions.push(eq(pgVoiceRecordingsTable.channel_id, filters.channelId)); - } - if (filters.userId) { - conditions.push(eq(pgVoiceRecordingsTable.user_id, filters.userId)); - } - if (filters.q) { - const like = `%${filters.q}%`; - const qOr = or( - ilike(pgVoiceRecordingsTable.transcription, like), - ilike(pgVoiceRecordingsTable.username, like), - ); - if (qOr) conditions.push(qOr); - } - if (filters.startDate) { - conditions.push( - gte(pgVoiceRecordingsTable.created_at, filters.startDate), - ); - } - if (filters.endDate) { - conditions.push(lte(pgVoiceRecordingsTable.created_at, filters.endDate)); - } - - const where = conditions.length > 0 ? and(...conditions) : undefined; - - const allRows = await db - .select({ - id: pgVoiceRecordingsTable.id, - user_id: pgVoiceRecordingsTable.user_id, - username: pgVoiceRecordingsTable.username, - avatar_url: pgVoiceRecordingsTable.avatar_url, - guild_id: pgVoiceRecordingsTable.guild_id, - channel_id: pgVoiceRecordingsTable.channel_id, - channel_name: pgVoiceRecordingsTable.channel_name, - filename: pgVoiceRecordingsTable.filename, - size_bytes: pgVoiceRecordingsTable.size_bytes, - download_url: pgVoiceRecordingsTable.download_url, - upload_status: pgVoiceRecordingsTable.upload_status, - upload_error: pgVoiceRecordingsTable.upload_error, - created_at: pgVoiceRecordingsTable.created_at, - uploaded_at: pgVoiceRecordingsTable.uploaded_at, - transcription: pgVoiceRecordingsTable.transcription, - }) - .from(pgVoiceRecordingsTable) - .where(where) - .orderBy(desc(pgVoiceRecordingsTable.created_at)) - .limit(limit + 1); - - const items = allRows.slice(0, limit) as unknown as RecordingRow[]; - const hasMore = allRows.length > limit; - const nextCursor = hasMore - ? String(items[items.length - 1]?.created_at) - : null; - - return { items, nextCursor, hasMore }; - } - - async deleteById(id: string): Promise { - const db = getDatabase(); - await db - .delete(pgVoiceRecordingsTable) - .where(eq(pgVoiceRecordingsTable.id, id)); - } - - /** - * Speaker leaderboard: aggregate clips / est. duration / transcribed words - * per user. `est_duration_s` derives from summed MP3 bytes at the uploader's - * 128 kbps transcode rate; `words` sums transcription word counts. - */ - async getSummary(): Promise { - const db = getDatabase(); - const t = pgVoiceRecordingsTable; - - const rows = await db - .select({ - user_id: t.user_id, - username: t.username, - avatar_url: t.avatar_url, - clips: sql`count(*)::int`, - total_bytes: sql`coalesce(sum(${t.size_bytes}), 0)`, - words: sql`coalesce(sum(array_length(string_to_array(${t.transcription}, ' '), 1)), 0)::int`, - transcribed: sql`count(${t.transcription})::int`, - last_at: sql`max(${t.created_at})`, - }) - .from(t) - .groupBy(t.user_id, t.username, t.avatar_url) - .orderBy(sql`clips desc, last_at desc`); - - return rows.map((r) => ({ - user_id: r.user_id, - username: r.username, - avatar_url: r.avatar_url, - clips: r.clips, - // 128 kbps = 128000 bps → bytes*8/128000 = seconds - est_duration_s: Math.round((r.total_bytes * 8) / 128000), - words: r.words, - transcribed: r.transcribed, - last_at: r.last_at, - })); - } -} - -export const recordingsService = new RecordingsService(); diff --git a/services/backend/src/modules/voice/live-speaker.ts b/services/backend/src/modules/voice/live-speaker.ts deleted file mode 100644 index 0009ca6b..00000000 --- a/services/backend/src/modules/voice/live-speaker.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Authoritative live-voice store. - * - * Single source of truth for who is present / speaking in voice. The backend - * WebSocket server is the one relay every frontend client connects to, so it - * is the correct place to aggregate the gateway's `voice_active_user` deltas - * into a shared snapshot. A late-joining browser must be able to see the same - * state as everyone else — this store makes that possible (seeded into the WS - * initial states and served via GET /api/voice/status). - */ - -export interface LiveSpeaker { - userId: string; - username: string; - avatar?: string | null; - speaking: boolean; - /** Epoch ms of the most recent activity (start OR end of speech). */ - lastActiveAt: number; -} - -const speakers = new Map(); - -const MAX_SPEAKERS = 200; - -/** - * Speakers inactive for longer than this are auto-expired from the snapshot. - * This handles the case where the gateway disconnects abruptly and never - * sends `speaking: false` for active users. - */ -const SPEAKER_TTL_MS = 30_000; - -/** Purge speakers that haven't been active recently. */ -function purgeStale(): void { - const cutoff = Date.now() - SPEAKER_TTL_MS; - for (const [id, s] of speakers) { - if (!s.speaking && s.lastActiveAt < cutoff) { - speakers.delete(id); - } - } -} - -/** - * Record a voice_active_user event. `speaking: true` upserts the speaker as - * active; `speaking: false` marks them inactive while keeping them briefly - * for the activity timeline (until TTL expiry). - */ -export function recordSpeaker(data: { - userId: string; - username?: string; - avatar?: string | null; - speaking: boolean; -}): void { - const { userId, speaking } = data; - const existing = speakers.get(userId); - const speaker: LiveSpeaker = { - userId, - username: data.username ?? existing?.username ?? "Unknown", - avatar: data.avatar ?? existing?.avatar ?? null, - speaking, - lastActiveAt: Date.now(), - }; - - if (speakers.size >= MAX_SPEAKERS && !existing) { - let oldestId: string | null = null; - let oldestTs = Infinity; - for (const [id, s] of speakers) { - if (!s.speaking && s.lastActiveAt < oldestTs) { - oldestTs = s.lastActiveAt; - oldestId = id; - } - } - if (oldestId) speakers.delete(oldestId); - else return; - } - - speakers.set(userId, speaker); -} - -/** - * All recently-active speakers, most recently active first. - * Stale (non-speaking + old) entries are auto-purged. - */ -export function getActiveSpeakers(): LiveSpeaker[] { - purgeStale(); - return [...speakers.values()].sort((a, b) => b.lastActiveAt - a.lastActiveAt); -} - -/** Only speakers currently flagged as speaking. */ -export function getSpeakingSpeakers(): LiveSpeaker[] { - purgeStale(); - return [...speakers.values()] - .filter((s) => s.speaking) - .sort((a, b) => b.lastActiveAt - a.lastActiveAt); -} - -/** - * Mark ALL tracked speakers as not-speaking and purge stale ones. - * Called when the gateway disconnects from voice — ensures the authoritative - * snapshot doesn't carry ghost speakers. - */ -export function clearAllSpeakers(): void { - for (const [, s] of speakers) { - s.speaking = false; - } - purgeStale(); -} - -/** Drop all tracked speakers (used on backend restart). */ -export function resetLiveSpeakers(): void { - speakers.clear(); -} diff --git a/services/backend/src/modules/voice/voice.schema.ts b/services/backend/src/modules/voice/voice.schema.ts deleted file mode 100644 index c8dc62da..00000000 --- a/services/backend/src/modules/voice/voice.schema.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from "zod"; - -export const connectVoiceSchema = z.object({ - guildId: z.string().min(1, "guildId is required"), - channelId: z.string().min(1, "channelId is required"), -}); - -export const voiceCommandSchema = z.object({ - command: z.string().min(1, "command is required"), -}); - -export type ConnectVoiceInput = z.infer; -export type VoiceCommandInput = z.infer; diff --git a/services/backend/src/modules/voice/voice.service.ts b/services/backend/src/modules/voice/voice.service.ts deleted file mode 100644 index 0896cd7a..00000000 --- a/services/backend/src/modules/voice/voice.service.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { eq } from "drizzle-orm"; -import { - createChildLogger, - tryCommandThenFallback, -} from "../../shared/commandHelper.js"; -import { getDatabase } from "../../shared/database/index.js"; -import { - COMMAND_GUILDS_LIST, - COMMAND_GUILDS_TEXT_CHANNELS, - COMMAND_VOICE_CHANNELS, - COMMAND_VOICE_CONNECT, - COMMAND_VOICE_DISCONNECT, - type CommandReply, - pgMessagesTable, - VOICE_STATUS_KEY, -} from "../../shared/index.js"; -import { publishCommand, readRedisStatus } from "../../shared/redis/index.js"; -import { getActiveSpeakers, type LiveSpeaker } from "./live-speaker.js"; - -const logger = createChildLogger("voice.service"); - -export interface Guild { - id: string; - name: string; - icon: string | null; -} - -export interface Channel { - id: string; - name: string; - type: "voice" | "text"; - /** Whether the selfbot account can actually join this voice channel. */ - joinable?: boolean; -} - -export interface GuildVoiceEntry { - guildId: string; - channelId: string; - channelName: string; - connectedAt: number; -} - -export interface VoiceStatus { - connected: boolean; - activeGuildId: string | null; - activeChannelId: string | null; - activeChannelName: string | null; - connections: GuildVoiceEntry[]; - /** - * Authoritative shared voice snapshot — who is present / speaking right - * now, aggregated server-side from the gateway's `voice_active_user` - * deltas. All browsers converge on this same list. - */ - activeSpeakers: LiveSpeaker[]; -} - -export const DEFAULT_VOICE_STATUS: VoiceStatus = { - connected: false, - activeGuildId: null, - activeChannelId: null, - activeChannelName: null, - connections: [], - activeSpeakers: [], -}; - -/** Attach the live speaker snapshot to any voice status payload. */ -function withActiveSpeakers>( - status: T, -): T & { activeSpeakers: LiveSpeaker[] } { - return { ...status, activeSpeakers: getActiveSpeakers() }; -} - -/** - * Wraps tryCommandThenFallback with a cleaner signature for use within this module. - * Attempts a Redis command first; on failure, falls back to the provided function. - */ -async function withFallback( - commandFn: () => Promise | null>, - fallbackFn: () => Promise, - name: string, -): Promise { - return tryCommandThenFallback(commandFn, fallbackFn, name); -} - -function readVoiceStatusFallback(): Promise { - return readRedisStatus(VOICE_STATUS_KEY).then((cached) => - withActiveSpeakers( - (cached as unknown as VoiceStatus) ?? DEFAULT_VOICE_STATUS, - ), - ); -} - -/** - * Get guilds — query from discord-gateway via Redis command for real names. - * Falls back to database (distinct guild_id from messages) if gateway unreachable. - */ -export async function getGuilds(): Promise { - logger.info("getGuilds called"); - return withFallback( - () => publishCommand(COMMAND_GUILDS_LIST, {}), - async () => { - const db = getDatabase(); - const rows = await db - .selectDistinct({ guild_id: pgMessagesTable.guild_id }) - .from(pgMessagesTable) - .orderBy(pgMessagesTable.guild_id); - return rows.map((row) => ({ - id: String(row.guild_id ?? ""), - name: `Guild ${String(row.guild_id).slice(0, 8)}`, - icon: null, - })); - }, - "getGuilds", - ); -} - -/** - * Get text channels — query from discord-gateway via Redis command for real names. - * Falls back to database if gateway unreachable. - */ -export async function getTextChannels(guildId: string): Promise { - logger.info({ guildId }, "getTextChannels called"); - return withFallback( - () => publishCommand(COMMAND_GUILDS_TEXT_CHANNELS, { guildId }), - async () => { - const db = getDatabase(); - const rows = await db - .selectDistinct({ channel_id: pgMessagesTable.channel_id }) - .from(pgMessagesTable) - .where(eq(pgMessagesTable.guild_id, guildId)) - .orderBy(pgMessagesTable.channel_id); - return rows.map((row) => ({ - id: String(row.channel_id ?? ""), - name: `Channel ${String(row.channel_id).slice(0, 8)}`, - type: "text" as const, - })); - }, - "getTextChannels", - ); -} - -/** - * Get voice channels — query from discord-gateway via Redis command. - */ -export async function getVoiceChannels(guildId: string): Promise { - logger.info({ guildId }, "getVoiceChannels called"); - const reply = await publishCommand(COMMAND_VOICE_CHANNELS, { - guildId, - }); - return reply?.success && reply.data ? reply.data : []; -} - -/** - * Get current voice connection status from Redis cache set by discord-gateway. - */ -export async function getVoiceStatus(): Promise { - logger.debug("getVoiceStatus called"); - const cached = await readRedisStatus(VOICE_STATUS_KEY); - return withActiveSpeakers( - (cached as unknown as VoiceStatus) ?? DEFAULT_VOICE_STATUS, - ); -} - -/** - * Connect to a voice channel via Redis command to discord-gateway. - */ -export async function connectVoice( - guildId: string, - channelId: string, -): Promise { - logger.info({ guildId, channelId }, "connectVoice called"); - return withFallback( - () => - publishCommand(COMMAND_VOICE_CONNECT, { - guildId, - channelId, - }), - () => readVoiceStatusFallback(), - "connectVoice", - ); -} - -/** - * Disconnect from voice via Redis command to discord-gateway. - */ -export async function disconnectVoice(): Promise { - logger.info("disconnectVoice called"); - return withFallback( - () => publishCommand(COMMAND_VOICE_DISCONNECT, {}), - () => readVoiceStatusFallback(), - "disconnectVoice", - ); -} diff --git a/services/backend/src/orpc/router.ts b/services/backend/src/orpc/router.ts index 9c960c8d..564c74b8 100644 --- a/services/backend/src/orpc/router.ts +++ b/services/backend/src/orpc/router.ts @@ -5,33 +5,13 @@ import { chatRequestSchema } from "../modules/chatbot/chatbot.schema"; import { chatbotService } from "../modules/chatbot/chatbot.service"; import { dashboardService } from "../modules/dashboard/dashboard.service"; import { knowledgeService } from "../modules/knowledge/knowledge.service"; -import { - mediaLoopSchema, - mediaQueueSchema, -} from "../modules/media/media.schema"; -import { - getStatus, - queue, - setLoop, - skip, - stop, -} from "../modules/media/media.service"; import { messageQuerySchema, semanticSearchSchema, } from "../modules/messages/messages.schema"; import { messagesService } from "../modules/messages/messages.service"; import { moderationService } from "../modules/moderation/moderation.service"; -import { recordingsService } from "../modules/recordings/recordings.service"; import { uiStateService } from "../modules/ui-state/ui-state.service"; -import { - connectVoice, - disconnectVoice, - getGuilds, - getTextChannels, - getVoiceChannels, - getVoiceStatus, -} from "../modules/voice/voice.service"; import { config } from "../shared/config/index"; import { publishCommandNoReply } from "../shared/redis/index"; @@ -238,86 +218,8 @@ const moderationRouter = { .handler(({ input }) => moderationService.getCoverage(input.days)), }; -// ── Media ──────────────────────────────────────────────────────── -const mediaRouter = { - status: os.handler(() => getStatus()), - queue: os.input(mediaQueueSchema).handler(async ({ input }) => { - await queue(input.source, input.mode); - return getStatus(); - }), - skip: os.handler(async () => { - await skip(); - return getStatus(); - }), - stop: os.handler(async () => { - await stop(); - return getStatus(); - }), - loop: os.input(mediaLoopSchema).handler(async ({ input }) => { - await setLoop(input.loop); - return getStatus(); - }), -}; - -// ── Voice ───────────────────────────────────────────────────────── -const voiceRouter = { - guilds: os.handler(() => getGuilds()), - textChannels: os - .input(z.object({ guildId: z.string() })) - .handler(({ input }) => getTextChannels(input.guildId)), - voiceChannels: os - .input(z.object({ guildId: z.string() })) - .handler(({ input }) => getVoiceChannels(input.guildId)), - status: os.handler(() => getVoiceStatus()), - connect: os - .input(z.object({ guildId: z.string(), channelId: z.string() })) - .handler(async ({ input }) => { - await connectVoice(input.guildId, input.channelId); - return getVoiceStatus(); - }), - disconnect: os.handler(async () => { - await disconnectVoice(); - return getVoiceStatus(); - }), - command: os - .input(z.object({ command: z.string().min(1) })) - .handler(async ({ input }) => { - await publishCommandNoReply(input.command); - return { success: true, command: input.command }; - }), -}; - -// ── Recordings ─────────────────────────────────────────────────── -const recordingsRouter = { - list: os - .input( - z.object({ - limit: z.coerce.number().int().positive().default(50), - channelId: z.string().optional(), - userId: z.string().optional(), - cursor: z.string().optional(), - q: z.string().optional(), - startDate: z.coerce.number().int().optional(), - endDate: z.coerce.number().int().optional(), - }), - ) - .handler(({ input }) => - recordingsService.getRecent(input.limit, { - channelId: input.channelId, - userId: input.userId, - cursor: input.cursor, - q: input.q, - startDate: input.startDate, - endDate: input.endDate, - }), - ), - delete: os.input(z.object({ id: z.string() })).handler(async ({ input }) => { - await recordingsService.deleteById(input.id); - return { ok: true }; - }), - summary: os.handler(async () => recordingsService.getSummary()), -}; - +// ── Recordings (removed) / Voice (removed) ───────────────────── +// Music/media playback (media.mjs) was removed with the voice feature. // ── Analysis (search) ────────────────────────────────────────────── const analysisRouter = { search: os @@ -416,11 +318,8 @@ const configRouter = { backlogSyncBatchSize: config.BACKLOG_SYNC_BATCH_SIZE, retentionMessagesDays: config.RETENTION_MESSAGES_DAYS, retentionAttachmentsDays: config.RETENTION_ATTACHMENTS_DAYS, - retentionVoiceDays: config.RETENTION_VOICE_DAYS, autoDeleteFlaggedEnabled: config.AUTO_DELETE_FLAGGED_ENABLED, aiAnalysisEnabled: config.AI_ANALYSIS_ENABLED, - voiceGuildId: config.VOICE_GUILD_ID || null, - voiceChannelId: config.VOICE_CHANNEL_ID || null, logLevel: config.LOG_LEVEL, })), }; @@ -438,9 +337,6 @@ export const appRouter = { dashboard: dashboardRouter, messages: messagesRouter, moderation: moderationRouter, - media: mediaRouter, - voice: voiceRouter, - recordings: recordingsRouter, analysis: analysisRouter, chatbot: chatbotRouter, config: configRouter, diff --git a/services/backend/src/shared/config/index.ts b/services/backend/src/shared/config/index.ts index 4d2502ba..fd1606c6 100644 --- a/services/backend/src/shared/config/index.ts +++ b/services/backend/src/shared/config/index.ts @@ -33,29 +33,6 @@ export const configSchema = z .transform((v) => v.split(",").filter(Boolean)) .describe("Thread IDs to exclude from capture"), - // ── Legacy voice ───────────────────────────────────────────────────── - VOICE_GUILD_ID: z.string().min(1).optional(), - VOICE_CHANNEL_ID: z.string().min(1).optional(), - - // ── Recording ──────────────────────────────────────────────────────── - RECORDINGS_DIR: z.string().default("./recordings"), - RECORDING_SEGMENT_MS: z.coerce.number().positive().default(5000), - - // ── Decoder ────────────────────────────────────────────────────────── - DECODER_ROTATE_MS: z.coerce.number().positive().default(5000), - DECODER_COOLDOWN_MS: z.coerce.number().positive().default(30000), - - // ── Audio ──────────────────────────────────────────────────────────── - AUDIO_STREAM_SILENCE_DURATION_MS: z.coerce - .number() - .positive() - .default(3000), - PACKET_FILTER_MIN_SIZE: z.coerce.number().positive().default(8), - OPUS_FRAME_SIZE: z.coerce.number().positive().default(960), - AUDIO_SAMPLE_RATE: z.coerce.number().positive().default(48000), - AUDIO_CHANNELS: z.coerce.number().positive().default(2), - AVATAR_SIZE: z.coerce.number().positive().default(64), - // ── Server ─────────────────────────────────────────────────────────── WEBSERVER_PORT: z.coerce.number().positive().default(3001), NODE_ENV: z @@ -92,17 +69,8 @@ export const configSchema = z // ── Redis ──────────────────────────────────────────────────────────── REDIS_URL: z.string().default("redis://localhost:6379"), - // ── Voice PCM WebSocket (direct gateway→backend, bypasses Redis) ──── - VOICE_PCM_WS_ENABLED: z - .string() - .optional() - .transform((v) => v === "true") - .default(true), - BACKEND_WS_URL: z.string().default("ws://backend:3000/ws"), - BACKEND_WS_TOKEN: z.string().optional().default(""), // ── Connection ─────────────────────────────────────────────────────── - VOICE_CONNECTION_TIMEOUT_MS: z.coerce.number().positive().default(15000), RECONNECT_TIMEOUT_MS: z.coerce.number().positive().default(5000), // ── Attachments ───────────────────────────────────────────────────── @@ -203,13 +171,6 @@ export const configSchema = z PISCINA_MAX_THREADS: z.coerce.number().int().positive().optional(), PISCINA_MEDIA_MAX_THREADS: z.coerce.number().int().positive().optional(), - // ── Voice Transcription ──────────────────────────────────────────────── - AI_VOICE_TRANSCRIPTION_ENABLED: z - .string() - .optional() - .transform((v) => v === "true") - .default(false), - // ── OpenAI Moderation ─────────────────────────────────────────────── OPENAI_MODERATION_API_KEY: z.string().optional(), OPENAI_MODERATION_BASE_URL: z @@ -252,7 +213,6 @@ export const configSchema = z // ── Retention ─────────────────────────────────────────────────────── RETENTION_MESSAGES_DAYS: z.coerce.number().int().min(0).default(0), RETENTION_ATTACHMENTS_DAYS: z.coerce.number().int().min(0).default(0), - RETENTION_VOICE_DAYS: z.coerce.number().int().min(0).default(0), RETENTION_CLEANUP_INTERVAL_MS: z.coerce .number() .positive() @@ -291,7 +251,6 @@ export const configSchema = z export type AppConfig = z.infer & { EFFECTIVE_TEXT_GUILD_ID?: string; - EFFECTIVE_VOICE_GUILD_ID?: string; EFFECTIVE_MONITOR_GUILD_IDS: string[]; }; @@ -301,7 +260,6 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { return { ...parsed, EFFECTIVE_TEXT_GUILD_ID: parsed.TEXT_GUILD_ID ?? parsed.MONITOR_GUILD_ID, - EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID, EFFECTIVE_MONITOR_GUILD_IDS: parsed.MONITOR_GUILD_IDS.length > 0 ? parsed.MONITOR_GUILD_IDS diff --git a/services/backend/src/shared/moderation-types.ts b/services/backend/src/shared/moderation-types.ts index 1ddee08b..5a0dfdc5 100644 --- a/services/backend/src/shared/moderation-types.ts +++ b/services/backend/src/shared/moderation-types.ts @@ -24,9 +24,6 @@ export interface BroadcasterClient { messageAnalyzed: (data: unknown) => void; attachmentCreated: (data: unknown) => void; attachmentUploaded: (data: unknown) => void; - voiceRecordingStarted: (data: unknown) => void; - voiceRecordingStopped: (data: unknown) => void; - voiceRecordingUploaded: (data: unknown) => void; analysisQueueStatus: (data: unknown) => void; } @@ -102,17 +99,6 @@ export interface AttachmentRecord { uploaded_at: number | null; } -export interface VoiceSegmentRecord { - id: string; - user_id: string; - session_id: string; - guild_id: string; - channel_id: string; - filename: string; - duration_ms: number; - created_at: number; -} - export interface DashboardMessage { id: string; channel_id: string; @@ -121,7 +107,7 @@ export interface DashboardMessage { avatar_url: string | null; content: string; created_at: number; - type: "text" | "image" | "voice"; + type: "text" | "image"; } export interface MessageQuery { @@ -154,23 +140,6 @@ export interface AnalysisResult { evidence?: string[]; } -export interface VoiceRecordingUploadData { - id: string; - user_id: string; - username: string; - avatar_url: string | null; - guild_id: string | null; - channel_id: string | null; - channel_name: string | null; - filename: string; - size_bytes: number; - download_url: string; - upload_status: string; - created_at: number; - uploaded_at: number; - transcription?: string | null; -} - export interface AnalysisQueueStatus { queuedConversations: number; activeRequests: number; @@ -221,7 +190,6 @@ export interface RetentionPolicy { channel_id: string | null; retention_days: number; apply_to_media: boolean; - apply_to_voice: boolean; enabled: boolean; created_at: number; updated_at: number; diff --git a/services/backend/src/shared/redis-channels.ts b/services/backend/src/shared/redis-channels.ts index e3d9e1f4..6f123c0b 100644 --- a/services/backend/src/shared/redis-channels.ts +++ b/services/backend/src/shared/redis-channels.ts @@ -15,11 +15,6 @@ export const DISCORD_MESSAGE_DELETED = "discord:message:deleted"; export const DISCORD_MESSAGE_ANALYZED = "discord:message:analyzed"; export const DISCORD_ATTACHMENT_CREATED = "discord:attachment:created"; export const DISCORD_ATTACHMENT_UPLOADED = "discord:attachment:uploaded"; -export const DISCORD_VOICE_STARTED = "discord:voice:started"; -export const DISCORD_VOICE_STOPPED = "discord:voice:stopped"; -export const DISCORD_VOICE_UPLOADED = "discord:voice:uploaded"; -export const DISCORD_VOICE_ACTIVE_USER = "discord:voice:active_user"; -export const DISCORD_VOICE_PCM = "discord:voice:pcm"; export const DISCORD_ANALYSIS_QUEUE_STATUS = "discord:analysis:queue_status"; export const DISCORD_REACTION_ADDED = "discord:reaction:added"; export const DISCORD_REACTION_REMOVED = "discord:reaction:removed"; @@ -37,35 +32,15 @@ export const DISCORD_MODERATION_ACTION = "discord:moderation:action"; // --------------------------------------------------------------------------- export const BACKEND_COMMAND = "backend:command"; -export const BACKEND_VOICE_TRANSMIT = "backend:voice:transmit"; export const BACKEND_COMMAND_REPLY_PREFIX = "backend:command:reply:"; -// --------------------------------------------------------------------------- -// Status keys (set by discord-gateway, read by backend via Redis GET) -// --------------------------------------------------------------------------- - -export const VOICE_STATUS_KEY = "voice:status"; -export const MEDIA_STATUS_KEY = "media:status"; - // --------------------------------------------------------------------------- // Command types (used as the `type` field in CommandMessage envelopes) // --------------------------------------------------------------------------- -export const COMMAND_VOICE_CONNECT = "voice:connect"; -export const COMMAND_VOICE_DISCONNECT = "voice:disconnect"; -export const COMMAND_VOICE_DISCONNECT_GUILD = "voice:disconnect:guild"; -export const COMMAND_VOICE_CHANNELS = "voice:channels"; -export const COMMAND_VOICE_TRANSMIT_START = "voice:transmit:start"; -export const COMMAND_VOICE_TRANSMIT_STOP = "voice:transmit:stop"; export const COMMAND_GUILDS_LIST = "guilds:list"; export const COMMAND_GUILDS_TEXT_CHANNELS = "guilds:text-channels"; -export const COMMAND_MEDIA_QUEUE = "media:queue"; -export const COMMAND_MEDIA_SKIP = "media:skip"; -export const COMMAND_MEDIA_STOP = "media:stop"; -export const COMMAND_MEDIA_VOLUME = "media:volume"; -export const COMMAND_MEDIA_LOOP = "media:loop"; export const COMMAND_MODERATION_ACTION = "moderation:action"; -export const DISCORD_VOICE_ANALYZED = "discord:voice:analyzed"; // --------------------------------------------------------------------------- // Event envelope — used by discord-gateway when publishing to Redis @@ -111,13 +86,7 @@ export const DISCORD_CHANNEL_TO_WS_EVENT: Record = { [DISCORD_MESSAGE_ANALYZED]: "message_analyzed", [DISCORD_ATTACHMENT_CREATED]: "attachment_created", [DISCORD_ATTACHMENT_UPLOADED]: "attachment_uploaded", - [DISCORD_VOICE_STARTED]: "voice_recording_started", - [DISCORD_VOICE_STOPPED]: "voice_recording_stopped", - [DISCORD_VOICE_UPLOADED]: "voice_recording_uploaded", [DISCORD_ANALYSIS_QUEUE_STATUS]: "analysis_queue_status", - [DISCORD_VOICE_ACTIVE_USER]: "voice_active_user", - [DISCORD_VOICE_PCM]: "voice_pcm_data", - [DISCORD_VOICE_ANALYZED]: "voice_analyzed", [DISCORD_REACTION_ADDED]: "reaction_added", [DISCORD_REACTION_REMOVED]: "reaction_removed", [DISCORD_THREAD_CREATED]: "thread_created", diff --git a/services/backend/src/ws/broadcast.ts b/services/backend/src/ws/broadcast.ts index 687828cc..a53347f8 100644 --- a/services/backend/src/ws/broadcast.ts +++ b/services/backend/src/ws/broadcast.ts @@ -49,7 +49,6 @@ export function clearBroadcastFunctions(): void { function shouldLog(type: string): boolean { if (!_enabled) return false; // Avoid logging high-volume events - if (type === "voice_pcm_data") return false; return true; } diff --git a/services/backend/src/ws/redis-bridge.ts b/services/backend/src/ws/redis-bridge.ts index c985304f..8ed8aad1 100644 --- a/services/backend/src/ws/redis-bridge.ts +++ b/services/backend/src/ws/redis-bridge.ts @@ -1,17 +1,8 @@ import Redis from "ioredis"; -import { - clearAllSpeakers, - recordSpeaker, -} from "../modules/voice/live-speaker.js"; import { config } from "../shared/config/index.js"; -import { - DISCORD_CHANNEL_TO_WS_EVENT, - DISCORD_VOICE_ACTIVE_USER, - DISCORD_VOICE_PCM, - DISCORD_VOICE_STOPPED, -} from "../shared/index.js"; +import { DISCORD_CHANNEL_TO_WS_EVENT } from "../shared/index.js"; import { createChildLogger } from "../shared/logger/index.js"; -import { broadcastBinary, broadcastEvent } from "./broadcast.js"; +import { broadcastEvent } from "./broadcast.js"; const logger = createChildLogger("ws.redis-bridge"); @@ -49,67 +40,10 @@ function handleSubscriptionMessage(channel: string, message: string): void { // We only want , not the full envelope. const data = envelope.data !== undefined ? envelope.data : envelope; - // Voice PCM: decode base64 → binary broadcast instead of JSON - if (channel === DISCORD_VOICE_PCM) { - const pcmPayload = data as { userId?: string; pcm?: string }; - if (pcmPayload?.pcm && pcmPayload?.userId) { - try { - const pcmBuffer = Buffer.from(pcmPayload.pcm, "base64"); - // Prepend userId as 4-byte FNV-1a hash - const userIdHash = hashUserId(pcmPayload.userId); - const binary = Buffer.alloc(4 + pcmBuffer.length); - binary.writeUInt32LE(userIdHash, 0); - pcmBuffer.copy(binary, 4); - broadcastBinary(binary); - return; - } catch { - // fallback to JSON broadcast on error - } - } - } - - // Aggregate live-voice state authoritatively BEFORE broadcasting. - // Every browser hears the same `voice_active_user` deltas, so the backend - // can maintain the single shared snapshot for late-joining clients. - if (channel === DISCORD_VOICE_ACTIVE_USER) { - const speaker = data as { - userId?: string; - username?: string; - avatar?: string | null; - speaking?: boolean; - }; - if (speaker?.userId) { - recordSpeaker({ - userId: speaker.userId, - username: speaker.username, - avatar: speaker.avatar, - speaking: Boolean(speaker.speaking), - }); - } - } - - // When the gateway stops voice recording (disconnects from voice channel), - // clear all speakers from the authoritative snapshot so frontends don't - // show ghost participants. - if (channel === DISCORD_VOICE_STOPPED) { - clearAllSpeakers(); - logger.info("Voice recording stopped — cleared all live speakers"); - } - logger.debug({ channel, eventType }, "Broadcasting Redis event"); broadcastEvent(eventType, data); } -/** Simple 32-bit FNV-1a hash for userId → 4-byte identifier */ -function hashUserId(userId: string): number { - let hash = 0x811c9dc5; - for (let i = 0; i < userId.length; i++) { - hash ^= userId.charCodeAt(i); - hash = Math.imul(hash, 0x01000193); - } - return hash >>> 0; -} - export async function startRedisBridge(): Promise { if (!config.REDIS_URL) { logger.info("Redis not configured, skipping Redis bridge"); diff --git a/services/backend/src/ws/server.ts b/services/backend/src/ws/server.ts index 8cffc9ff..bdc892ec 100644 --- a/services/backend/src/ws/server.ts +++ b/services/backend/src/ws/server.ts @@ -2,8 +2,6 @@ import type { IncomingMessage, Server } from "node:http"; import type { Duplex } from "node:stream"; import { WebSocket, WebSocketServer } from "ws"; import { messagesService } from "../modules/messages/messages.service.js"; -import { config } from "../shared/config/index.js"; -import { BACKEND_COMMAND, BACKEND_VOICE_TRANSMIT } from "../shared/index.js"; import { createChildLogger } from "../shared/logger/index.js"; import { setBroadcastFunctions } from "./broadcast.js"; @@ -17,8 +15,6 @@ interface BroadcastEvent { interface JsonMessage { type: string; - buffer?: string; - command?: string; payload?: Record; } @@ -54,36 +50,6 @@ async function sendInitialStates(ws: WebSocket): Promise { } catch (err) { logger.warn({ err }, "Failed to send initial ui_state"); } - - // Send initial media state - try { - const { getStatus } = await import("../modules/media/media.service.js"); - const mediaState = await getStatus(); - ws.send( - JSON.stringify({ - type: "media_state", - state: mediaState, - }), - ); - } catch (err) { - logger.warn({ err }, "Failed to send initial media_state"); - } - - // Send initial live-voice snapshot (shared authoritative state — a browser - // joining mid-call sees the same speakers as everyone else, not an empty DB). - try { - const { getActiveSpeakers } = await import( - "../modules/voice/live-speaker.js" - ); - ws.send( - JSON.stringify({ - type: "voice_state", - state: { activeSpeakers: getActiveSpeakers() }, - }), - ); - } catch (err) { - logger.warn({ err }, "Failed to send initial voice_state"); - } } export function closeWebSocketServer(): void { @@ -94,9 +60,7 @@ export function closeWebSocketServer(): void { } export function createWebSocketServer(server: Server): WebSocketServer { - // Separate tracking: gateway sends PCM → forwarded to frontend only const frontendClients = new Set(); - const gatewayClients = new Set(); const wss = new WebSocketServer({ noServer: true, perMessageDeflate: true }); _wss = wss; @@ -115,35 +79,6 @@ export function createWebSocketServer(server: Server): WebSocketServer { // Map-based dispatcher for JSON WebSocket message types const jsonHandlers = new Map(); - jsonHandlers.set("voice_transmit", async (_ws, message) => { - if (!message.buffer) return; - const { getCommandPublisher } = await import("../shared/redis/index.js"); - const publisher = getCommandPublisher(); - await publisher.publish( - BACKEND_VOICE_TRANSMIT, - JSON.stringify({ type: "pcm", buffer: message.buffer }), - ); - }); - - jsonHandlers.set("voice_command", async (_ws, message) => { - if (!message.command) return; - const { getCommandPublisher } = await import("../shared/redis/index.js"); - const publisher = getCommandPublisher(); - const commandId = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; - await publisher.publish( - BACKEND_COMMAND, - JSON.stringify({ - id: commandId, - type: message.command, - payload: message.payload ?? {}, - replyChannel: `reply:${commandId}`, - }), - ); - }); - - // Stream historical messages one-by-one over WS (no 50-row batch). - // The frontend requests it once per channel switch; the backend emits one - // `message_snapshot` frame per message so the UI renders progressively. jsonHandlers.set("stream_messages", async (ws, message) => { if (ws.readyState !== WebSocket.OPEN) return; const payload = (message.payload ?? {}) as { @@ -208,74 +143,15 @@ export function createWebSocketServer(server: Server): WebSocketServer { } }); - wss.on("connection", (ws: WebSocket, req) => { - // Parse auth token from query string - const rawUrl = req.url ?? "/"; - let isGateway = false; - - try { - const url = new URL(rawUrl, "http://localhost"); - const token = url.searchParams.get("token"); - isGateway = - token !== null && - config.BACKEND_WS_TOKEN !== "" && - token === config.BACKEND_WS_TOKEN; - } catch { - // Malformed URL — treat as frontend - } - - if (isGateway) { - gatewayClients.add(ws); - logger.info("Discord gateway WebSocket client authenticated"); - // Gateway doesn't need initial states - } else { - frontendClients.add(ws); - logger.info(`Frontend client connected (${frontendClients.size} total)`); - // Send initial states (user, ui, media) — fire-and-forget - sendInitialStates(ws).catch((err) => - logger.error({ err }, "sendInitialStates failed"), - ); - } + wss.on("connection", (ws: WebSocket) => { + frontendClients.add(ws); + logger.info(`Frontend client connected (${frontendClients.size} total)`); + // Send initial states (user, ui) — fire-and-forget + sendInitialStates(ws).catch((err) => + logger.error({ err }, "sendInitialStates failed"), + ); ws.on("message", (data: Buffer) => { - // Gateway PCM forward — broadcast raw binary to frontend clients only - if (isGateway && Buffer.isBuffer(data)) { - broadcastBinary(data); - return; - } - - // Handle binary PCM from browser (FE→Discord transmit) - // Format: 4-byte magic "PCM\0" + raw PCM Int16 LE - if ( - Buffer.isBuffer(data) && - data.length > 4 && - data[0] === 0x50 && // 'P' - data[1] === 0x43 && // 'C' - data[2] === 0x4d && // 'M' - data[3] === 0x00 // '\0' - ) { - const pcmBuffer = data.subarray(4); - const base64 = pcmBuffer.toString("base64"); - import("../shared/redis/index.js").then(({ getCommandPublisher }) => { - const publisher = getCommandPublisher(); - publisher - .publish( - BACKEND_VOICE_TRANSMIT, - JSON.stringify({ - type: "pcm", - buffer: base64, - }), - ) - .catch((err: Error) => { - logger.error( - { err }, - "Failed to publish voice transmit to Redis", - ); - }); - }); - return; - } - // Handle JSON messages from browser if ( typeof data === "string" || @@ -296,24 +172,15 @@ export function createWebSocketServer(server: Server): WebSocketServer { }); ws.on("close", () => { - if (isGateway) { - gatewayClients.delete(ws); - logger.info("Discord gateway WebSocket disconnected"); - } else { - frontendClients.delete(ws); - logger.info( - `Frontend client disconnected (${frontendClients.size} total)`, - ); - } + frontendClients.delete(ws); + logger.info( + `Frontend client disconnected (${frontendClients.size} total)`, + ); }); ws.on("error", (err: Error) => { logger.error({ err }, "WebSocket client error"); - if (isGateway) { - gatewayClients.delete(ws); - } else { - frontendClients.delete(ws); - } + frontendClients.delete(ws); }); }); diff --git a/services/discord-gateway/drizzle/migrations/0011_add_voice_transcription.sql b/services/discord-gateway/drizzle/migrations/0011_add_voice_transcription.sql deleted file mode 100644 index 17563ca4..00000000 --- a/services/discord-gateway/drizzle/migrations/0011_add_voice_transcription.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "voice_recordings" ADD COLUMN "transcription" text; diff --git a/services/discord-gateway/drizzle/migrations/0020_add_voice_auto_reconnect.sql b/services/discord-gateway/drizzle/migrations/0020_add_voice_auto_reconnect.sql deleted file mode 100644 index bf9c0092..00000000 --- a/services/discord-gateway/drizzle/migrations/0020_add_voice_auto_reconnect.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Voice auto-reconnect: persist desired voice state per guild. --- The gateway rejoins the same channel after restart/reboot and after an --- unexpected voice drop (kick / server move / voice server restart). --- Idempotent: skips if the table already exists. -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.tables - WHERE table_name = 'voice_auto_reconnect' - ) THEN - CREATE TABLE "voice_auto_reconnect" ( - "guild_id" text PRIMARY KEY NOT NULL, - "channel_id" text NOT NULL, - "channel_name" text, - "connected_at" bigint NOT NULL, - "updated_at" bigint NOT NULL - ); - END IF; -END $$; \ No newline at end of file diff --git a/services/discord-gateway/drizzle/migrations/meta/_journal.json b/services/discord-gateway/drizzle/migrations/meta/_journal.json index 6291143e..b0cadad5 100644 --- a/services/discord-gateway/drizzle/migrations/meta/_journal.json +++ b/services/discord-gateway/drizzle/migrations/meta/_journal.json @@ -79,13 +79,6 @@ "tag": "0010_add_reactions_and_edit_history", "breakpoints": true }, - { - "idx": 11, - "version": "7", - "when": 1785600000000, - "tag": "0011_add_voice_transcription", - "breakpoints": true - }, { "idx": 12, "version": "7", @@ -141,13 +134,6 @@ "when": 1787830000000, "tag": "0019_rename_server_name_to_server_nick", "breakpoints": true - }, - { - "idx": 20, - "version": "7", - "when": 1787860000000, - "tag": "0020_add_voice_auto_reconnect", - "breakpoints": true } ] } \ No newline at end of file diff --git a/services/discord-gateway/package.json b/services/discord-gateway/package.json index d2e1fb48..321b8951 100644 --- a/services/discord-gateway/package.json +++ b/services/discord-gateway/package.json @@ -1,12 +1,11 @@ { "name": "@bete/discord-gateway", "version": "1.0.0", - "description": "Discord Gateway service - handles message capture, voice recording, and AI moderation", + "description": "Discord Gateway service - handles message capture and AI moderation", "type": "module", "main": "dist/index.js", "pnpm": { "onlyBuiltDependencies": [ - "@discordjs/opus", "esbuild", "sharp" ] @@ -24,25 +23,19 @@ "test:e2e:live": "bash scripts/run-llm-e2e.sh" }, "dependencies": { - "@discordjs/opus": "^0.10.0", - "@discordjs/voice": "file:vendor/discord-voice-fork", - "@snazzah/davey": "^0.1.11", "@typesafe-ai/sdk": "^0.6.0", "axios": "^1.20.0", "discord.js-selfbot-v13": "^3.7.1", "dotenv": "^18.0.0", "drizzle-orm": "^0.45.2", "ioredis": "^6.0.0", - "libsodium-wrappers": "^0.8.4", "lru-cache": "^11.5.3", "openai": "^7.19.0", - "opusscript": "^0.1.1", "p-limit": "^7.3.3", "p-retry": "^8.0.1", "pg": "^8.23.0", "pino": "^10.3.1", "piscina": "^5.3.2", - "prism-media": "2.0.0-alpha.0", "sharp": "^0.35.4", "tiktoken": "^1.0.22", "ws": "^8.21.3", diff --git a/services/discord-gateway/src/app/bootstrap.ts b/services/discord-gateway/src/app/bootstrap.ts index 8913bc51..5e12e47d 100644 --- a/services/discord-gateway/src/app/bootstrap.ts +++ b/services/discord-gateway/src/app/bootstrap.ts @@ -33,18 +33,6 @@ import { startDigestScheduler } from "../modules/monitor/digestScheduler.js"; import { registerReactionCapture } from "../modules/reaction-tracking/index.js"; import { registerThreadCapture } from "../modules/thread-tracking/index.js"; import { registerPresenceCapture } from "../modules/user-presence/index.js"; -import { VoicePcmWsClient } from "../modules/voice-pcm-ws/index.js"; -import { startMuxerWorker } from "../modules/voice-recording/muxer.js"; -import { - registerSelfVoiceStateGuard, - setPcmWsClient, - setEventBroadcaster as setRecorderEventBroadcaster, -} from "../modules/voice-recording/recorder.js"; -import { - setVideoRecorderClient, - setVideoRecordingsDir, -} from "../modules/voice-recording/videoRecorder.js"; -import { VoiceController } from "../modules/voice-recording/voiceController.js"; import { config } from "../shared/config/config.js"; import { closeDatabase, @@ -53,11 +41,7 @@ import { } from "../shared/database/drizzle.js"; import { runMigrations } from "../shared/database/migrate.js"; import type * as schema from "../shared/database/schema.js"; -import { - attachmentsTable, - messagesTable, - voiceRecordingsTable, -} from "../shared/database/schema.js"; +import { attachmentsTable, messagesTable } from "../shared/database/schema.js"; import { createDiscordClientOptions } from "../shared/discord/clientOptions.js"; import { createGracefulShutdown } from "./shutdown.js"; @@ -124,7 +108,6 @@ function startRetentionCleanup(): void { dryRun, messagesDays: config.RETENTION_MESSAGES_DAYS, attachmentsDays: config.RETENTION_ATTACHMENTS_DAYS, - voiceDays: config.RETENTION_VOICE_DAYS, }, "Starting retention cleanup scheduler", ); @@ -144,13 +127,6 @@ function startRetentionCleanup(): void { dryRun, "attachments", ); - await deleteExpiredRecords( - voiceRecordingsTable, - voiceRecordingsTable.created_at, - config.RETENTION_VOICE_DAYS, - dryRun, - "voice recordings", - ); } // Run immediately on start, then schedule @@ -188,13 +164,6 @@ export async function initializeDiscordGateway() { logger.info("Creating Discord client"); const client = new Client(createDiscordClientOptions()); - const voiceController = new VoiceController(client); - - // Wire the video recorder (others' camera/screen share) to the selfbot's - // native watch/receive stack + its recordings dir. Best-effort: failures are - // logged inside, never fatal. - setVideoRecorderClient(client); - setVideoRecordingsDir(config.RECORDINGS_DIR); // Initialize Redis event broadcaster const redisPublisher = new RedisEventPublisher(config.REDIS_URL, logger); @@ -203,33 +172,13 @@ export async function initializeDiscordGateway() { // Initialize Redis command handler for backend→gateway commands const commandHandler = new CommandHandler(); - // Initialize Voice PCM WebSocket client (bypasses Redis for real-time audio) - let pcmWsClient: VoicePcmWsClient | undefined; - if (config.VOICE_PCM_WS_ENABLED && config.BACKEND_WS_TOKEN) { - pcmWsClient = new VoicePcmWsClient( - config.BACKEND_WS_URL, - config.BACKEND_WS_TOKEN, - ); - pcmWsClient.connect(); - setPcmWsClient(pcmWsClient); - logger.info({ url: config.BACKEND_WS_URL }, "Voice PCM WS client enabled"); - } else if (config.VOICE_PCM_WS_ENABLED && !config.BACKEND_WS_TOKEN) { - logger.warn( - "VOICE_PCM_WS_ENABLED=true but BACKEND_WS_TOKEN is empty — falling back to Redis for PCM", - ); - } else { - logger.info("Voice PCM WS disabled — using Redis for PCM"); - } - const gracefulShutdown = createGracefulShutdown({ logger, closeDatabase, - voiceController, client, eventBroadcaster, commandHandler, stopMetricsServer, - pcmWsClient, }); try { @@ -255,8 +204,6 @@ export async function initializeDiscordGateway() { client.on("debug", (msg) => { if ( - msg.includes("[VOICE") || - msg.includes("[ffmpeg") || msg.toLowerCase().includes("error") || msg.toLowerCase().includes("stream") ) { @@ -269,7 +216,6 @@ export async function initializeDiscordGateway() { client.on("ready", async () => { logger.info({ user: client.user?.tag }, "Bot logged in"); setMessageCaptureEventBroadcaster(eventBroadcaster); - setRecorderEventBroadcaster(eventBroadcaster); setModerationEventBroadcaster(eventBroadcaster); registerMessageCapture(client); startPendingAIAnalysisWorker(client, eventBroadcaster); @@ -281,30 +227,10 @@ export async function initializeDiscordGateway() { registerChannelTopicCapture(client, eventBroadcaster); registerGuildMemberEvents(client, eventBroadcaster); - // Start background workers - startMuxerWorker(); - // Start command handler after Discord is ready - commandHandler.start(client, voiceController); + commandHandler.start(client); logger.info("Command handler started"); - // Rejoin persisted voice channels (auto-reconnect on restart/reboot). - // Non-fatal: failures are logged inside autoReconnect. - void voiceController - .autoReconnect() - .catch((err) => - logger.warn( - { err: err instanceof Error ? err.message : String(err) }, - "Voice auto-reconnect on startup failed", - ), - ); - - // Attach the immediate self-undeafen/self-unmute guard so the bot reacts - // INSTANTLY when an admin server-mutes or server-deafens it (previously - // only re-asserted on video-watch/reconnect, leaving the bot muted for - // minutes). - registerSelfVoiceStateGuard(client); - // Start retention cleanup scheduler startRetentionCleanup(); // Start weekly moderation digest (public, automated) diff --git a/services/discord-gateway/src/app/retention.ts b/services/discord-gateway/src/app/retention.ts index 52bb96d1..94a2adbd 100644 --- a/services/discord-gateway/src/app/retention.ts +++ b/services/discord-gateway/src/app/retention.ts @@ -4,11 +4,7 @@ import { createChildLogger } from "@/shared/logger/index"; import { config } from "../shared/config/config.js"; import { getDatabase } from "../shared/database/drizzle.js"; import type * as schema from "../shared/database/schema.js"; -import { - attachmentsTable, - messagesTable, - voiceRecordingsTable, -} from "../shared/database/schema.js"; +import { attachmentsTable, messagesTable } from "../shared/database/schema.js"; const logger = createChildLogger("discord-gateway"); @@ -73,7 +69,6 @@ function startRetentionCleanup(): void { dryRun, messagesDays: config.RETENTION_MESSAGES_DAYS, attachmentsDays: config.RETENTION_ATTACHMENTS_DAYS, - voiceDays: config.RETENTION_VOICE_DAYS, }, "Starting retention cleanup scheduler", ); @@ -93,13 +88,6 @@ function startRetentionCleanup(): void { dryRun, "attachments", ); - await deleteExpiredRecords( - voiceRecordingsTable, - voiceRecordingsTable.created_at, - config.RETENTION_VOICE_DAYS, - dryRun, - "voice recordings", - ); } // Run immediately on start, then schedule diff --git a/services/discord-gateway/src/app/shutdown.ts b/services/discord-gateway/src/app/shutdown.ts index e8f808f2..94aaed0c 100644 --- a/services/discord-gateway/src/app/shutdown.ts +++ b/services/discord-gateway/src/app/shutdown.ts @@ -3,9 +3,6 @@ import type { createChildLogger } from "@/shared/logger/index"; import type { CommandHandler } from "../modules/command-handler/commandHandler.js"; import type { EventBroadcaster } from "../modules/event-broadcaster/index.js"; import type { stopMetricsServer } from "../modules/gateway-metrics/index.js"; -import type { VoicePcmWsClient } from "../modules/voice-pcm-ws/index.js"; -import { stopMuxerWorker } from "../modules/voice-recording/muxer.js"; -import type { VoiceController } from "../modules/voice-recording/voiceController.js"; import type { closeDatabase } from "../shared/database/drizzle.js"; type Logger = ReturnType; @@ -15,12 +12,10 @@ type StopMetricsServer = typeof stopMetricsServer; export interface GracefulShutdownOptions { logger: Logger; closeDatabase: CloseDatabase; - voiceController: VoiceController; client: Client; eventBroadcaster: EventBroadcaster; commandHandler: CommandHandler; stopMetricsServer?: StopMetricsServer; - pcmWsClient?: VoicePcmWsClient; } export function createGracefulShutdown(options: GracefulShutdownOptions) { @@ -37,26 +32,15 @@ export function createGracefulShutdown(options: GracefulShutdownOptions) { try { options.stopMetricsServer?.(); - stopMuxerWorker(); - // 1. Voice disconnect (may write final DB records) - options.logger.info("Stopping voice connection..."); - await options.voiceController.disconnect(); - - // 1b. Close PCM WebSocket client (after voice, before Redis) - if (options.pcmWsClient) { - options.logger.info("Closing voice PCM WS client..."); - await options.pcmWsClient.close(); - } - - // 2. Close Redis/pubsub after voice is done + // 1. Close Redis/pubsub options.logger.info("Closing event broadcaster..."); await options.eventBroadcaster.close(); options.logger.info("Closing command handler..."); await options.commandHandler.close(); - // 3. DB pool LAST – voice disconnect may finalize recordings + // 2. DB pool options.logger.info("Closing database..."); await options.closeDatabase(); options.logger.info("Database closed"); diff --git a/services/discord-gateway/src/index.ts b/services/discord-gateway/src/index.ts index 6d5737dc..fcc83042 100644 --- a/services/discord-gateway/src/index.ts +++ b/services/discord-gateway/src/index.ts @@ -1,5 +1,3 @@ -import "libsodium-wrappers"; -import "@snazzah/davey"; import "dotenv/config"; import { createChildLogger } from "@/shared/logger/index"; import { initializeDiscordGateway } from "./app/bootstrap.js"; diff --git a/services/discord-gateway/src/modules/command-handler/commandHandler.ts b/services/discord-gateway/src/modules/command-handler/commandHandler.ts index 4c03da7c..a7324a27 100644 --- a/services/discord-gateway/src/modules/command-handler/commandHandler.ts +++ b/services/discord-gateway/src/modules/command-handler/commandHandler.ts @@ -5,41 +5,17 @@ import { BACKEND_COMMAND, type CommandMessage, type CommandReply, - MEDIA_STATUS_KEY, - VOICE_STATUS_KEY, } from "../../shared/index.js"; import { createChildLogger } from "../../shared/logger/index.js"; -import type { VoiceController } from "../voice-recording/voiceController.js"; import { GuildHandler } from "./guild.handler.js"; import { type CommandHandlerFn, createHandlerRegistry, } from "./handler-registry.js"; -import { MediaHandler } from "./media.handler.js"; -import { wireMediaStatusWriter } from "./mediaStatusSink.js"; import { ModerationHandler } from "./moderation.handler.js"; -import { VideoHandler } from "./video.handler.js"; -import { VoiceHandler } from "./voice.handler.js"; const logger = createChildLogger("command-handler"); -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -interface VoiceStatusPayload { - connected: boolean; - activeGuildId: string | null; - activeChannelId: string | null; - activeChannelName: string | null; - connections: Array<{ - guildId: string; - channelId: string; - channelName: string; - connectedAt: number; - }>; -} - // --------------------------------------------------------------------------- // CommandHandler // --------------------------------------------------------------------------- @@ -47,13 +23,9 @@ interface VoiceStatusPayload { export class CommandHandler { private redisSub: Redis; private redisPub: Redis; - private voiceController: VoiceController | null = null; private registry: Map = new Map(); - private voiceHandler!: VoiceHandler; - private mediaHandler!: MediaHandler; private guildHandler!: GuildHandler; private moderationHandler!: ModerationHandler; - private videoHandler!: VideoHandler; constructor() { // Dedicated Redis connection needed because: Redis requires a dedicated @@ -77,30 +49,18 @@ export class CommandHandler { // ---- Lifecycle ---- /** - * Attach the Discord client and VoiceController, then subscribe to the Redis - * command channel. Must be called *after* the Discord client is created. + * Attach the Discord client, then subscribe to the Redis command channel. + * Must be called *after* the Discord client is created. */ - start(client: Client, voiceController: VoiceController): void { - this.voiceController = voiceController; - + start(client: Client): void { // Create domain-specific handlers with their dependencies - this.voiceHandler = new VoiceHandler(client, voiceController); - this.mediaHandler = new MediaHandler(); this.guildHandler = new GuildHandler(client); this.moderationHandler = new ModerationHandler(client); - this.videoHandler = new VideoHandler(client, voiceController); - - // Wire the media status sink so MediaHandler can persist status on - // queue advances that happen outside a command (natural track end). - wireMediaStatusWriter(this.redisPub); // Build the command registry this.registry = createHandlerRegistry( - this.voiceHandler, - this.mediaHandler, this.guildHandler, this.moderationHandler, - this.videoHandler, ); this.redisSub.on("message", (_channel, message) => { @@ -117,10 +77,6 @@ export class CommandHandler { logger.info(`Subscribed to Redis channel "${BACKEND_COMMAND}"`); } }); - - // Publish initial status snapshots so the backend knows the starting state. - this.publishVoiceStatus(); - this.publishMediaStatus(); } async close(): Promise { @@ -175,51 +131,5 @@ export class CommandHandler { } catch (err) { logger.error({ err }, "Failed to publish command reply"); } - - // Always refresh status keys after every command so the backend has - // the latest snapshot without polling. - this.publishVoiceStatus(); - this.publishMediaStatus(); - } - - // ---- Status publishing ---- - - private publishVoiceStatus(): void { - const raw = this.voiceController - ? this.voiceController.getStatus() - : { - ready: false, - connected: false, - activeGuildId: null, - activeChannelId: null, - activeChannelName: null, - connections: [], - }; - const status: VoiceStatusPayload = { - connected: raw.connected, - activeGuildId: raw.activeGuildId, - activeChannelId: raw.activeChannelId, - activeChannelName: raw.activeChannelName, - connections: raw.connections ?? [], - }; - - this.setKey(VOICE_STATUS_KEY, JSON.stringify(status)); - } - - private publishMediaStatus(): void { - this.setKey( - MEDIA_STATUS_KEY, - JSON.stringify(this.mediaHandler.getCurrentMediaStatus()), - ); - } - - /** - * Fire-and-forget SET using the persistent Redis publisher connection. - */ - private setKey(key: string, value: string): void { - this.redisPub.set(key, value).catch((err: unknown) => { - const msg = err instanceof Error ? err.message : String(err); - logger.warn({ key, error: msg }, "Failed to update Redis status key"); - }); } } diff --git a/services/discord-gateway/src/modules/command-handler/handler-registry.ts b/services/discord-gateway/src/modules/command-handler/handler-registry.ts index 1923d7bc..6bf8fb95 100644 --- a/services/discord-gateway/src/modules/command-handler/handler-registry.ts +++ b/services/discord-gateway/src/modules/command-handler/handler-registry.ts @@ -1,28 +1,12 @@ import { COMMAND_GUILDS_LIST, COMMAND_GUILDS_TEXT_CHANNELS, - COMMAND_MEDIA_LOOP, - COMMAND_MEDIA_QUEUE, - COMMAND_MEDIA_SKIP, - COMMAND_MEDIA_STOP, - COMMAND_MEDIA_VOLUME, COMMAND_MODERATION_ACTION, - COMMAND_VIDEO_UNWATCH, - COMMAND_VIDEO_WATCH, - COMMAND_VOICE_CHANNELS, - COMMAND_VOICE_CONNECT, - COMMAND_VOICE_DISCONNECT, - COMMAND_VOICE_DISCONNECT_GUILD, - COMMAND_VOICE_TRANSMIT_START, - COMMAND_VOICE_TRANSMIT_STOP, type CommandMessage, type CommandReply, } from "../../shared/index.js"; import type { GuildHandler } from "./guild.handler.js"; -import type { MediaHandler } from "./media.handler.js"; import type { ModerationHandler } from "./moderation.handler.js"; -import type { VideoHandler } from "./video.handler.js"; -import type { VoiceHandler } from "./voice.handler.js"; // --------------------------------------------------------------------------- // Types @@ -37,53 +21,11 @@ export type CommandHandlerFn = ( // --------------------------------------------------------------------------- export function createHandlerRegistry( - voiceHandler: VoiceHandler, - mediaHandler: MediaHandler, guildHandler: GuildHandler, moderationHandler: ModerationHandler, - videoHandler: VideoHandler, ): Map { const registry = new Map(); - // Voice commands - registry.set(COMMAND_VOICE_CONNECT, (cmd) => - voiceHandler.handleVoiceConnect(cmd), - ); - registry.set(COMMAND_VOICE_DISCONNECT, (cmd) => - voiceHandler.handleVoiceDisconnect(cmd), - ); - registry.set(COMMAND_VOICE_DISCONNECT_GUILD, (cmd) => - voiceHandler.handleVoiceDisconnectGuild(cmd), - ); - registry.set(COMMAND_VOICE_CHANNELS, (cmd) => - voiceHandler.handleVoiceChannels(cmd), - ); - registry.set(COMMAND_VOICE_TRANSMIT_START, (cmd) => - voiceHandler.handleVoiceTransmitStart(cmd), - ); - registry.set(COMMAND_VOICE_TRANSMIT_STOP, (cmd) => - voiceHandler.handleVoiceTransmitStop(cmd), - ); - - // Video watch commands - registry.set(COMMAND_VIDEO_WATCH, (cmd) => - videoHandler.handleVideoWatch(cmd), - ); - registry.set(COMMAND_VIDEO_UNWATCH, (cmd) => - videoHandler.handleVideoUnwatch(cmd), - ); - - // Media commands - registry.set(COMMAND_MEDIA_QUEUE, (cmd) => - mediaHandler.handleMediaQueue(cmd), - ); - registry.set(COMMAND_MEDIA_SKIP, (cmd) => mediaHandler.handleMediaSkip(cmd)); - registry.set(COMMAND_MEDIA_STOP, (cmd) => mediaHandler.handleMediaStop(cmd)); - registry.set(COMMAND_MEDIA_VOLUME, (cmd) => - mediaHandler.handleMediaVolume(cmd), - ); - registry.set(COMMAND_MEDIA_LOOP, (cmd) => mediaHandler.handleMediaLoop(cmd)); - // Guild commands registry.set(COMMAND_GUILDS_LIST, (cmd) => guildHandler.handleListGuilds(cmd), diff --git a/services/discord-gateway/src/modules/command-handler/media.handler.ts b/services/discord-gateway/src/modules/command-handler/media.handler.ts deleted file mode 100644 index 698608dc..00000000 --- a/services/discord-gateway/src/modules/command-handler/media.handler.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { StreamType } from "@discordjs/voice"; -import type { CommandMessage, CommandReply } from "../../shared/index.js"; -import { createChildLogger } from "../../shared/logger/index.js"; -import { - extractMediaInfo, - resolveMediaUrl, - transcodeToHighQualityOgg, -} from "../voice-recording/mediaSource.js"; -import type { - MediaMode, - MediaQueueItem, -} from "../voice-recording/mediaTypes.js"; -import { discordPlayer } from "../voice-recording/player.js"; -import { setMediaStatusKey } from "./mediaStatusSink.js"; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -interface MediaStatusItem { - id: string; - source: string; - title: string; - mode: MediaMode; - durationMs?: number | null; - thumbnailUrl?: string | null; -} - -export interface MediaStatusPayload { - playing: boolean; - activeMode: MediaMode | null; - musicVolume: number; - loop: boolean; - current: MediaStatusItem | null; - queue: MediaStatusItem[]; -} - -// --------------------------------------------------------------------------- -// Module-level queue -// --------------------------------------------------------------------------- - -const mediaQueue: MediaQueueItem[] = []; -let currentTrackItem: MediaQueueItem | null = null; -let loopEnabled = false; -/** Active ffmpeg transcode (killed on stop/skip). */ -let currentTranscodeCleanup: (() => void) | null = null; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function mapToStatusItem(item: MediaQueueItem): MediaStatusItem { - return { - id: item.id, - source: item.source, - title: item.title, - mode: item.mode, - durationMs: item.duration != null ? item.duration * 1000 : null, - thumbnailUrl: item.thumbnailUrl ?? null, - }; -} - -function buildStatusPayload(): MediaStatusPayload { - return { - playing: - currentTrackItem !== null && discordPlayer.getStatus() === "playing", - activeMode: currentTrackItem?.mode ?? null, - musicVolume: discordPlayer.getMusicVolume(), - loop: loopEnabled, - current: currentTrackItem ? mapToStatusItem(currentTrackItem) : null, - queue: mediaQueue.map(mapToStatusItem), - }; -} - -// --------------------------------------------------------------------------- -// MediaHandler -// --------------------------------------------------------------------------- - -export class MediaHandler { - private logger = createChildLogger("media-handler"); - - constructor() { - // Register auto-advance on natural track end. advanceQueue mutates the - // module-level currentTrackItem/queue, so we must re-publish the status - // key afterward: otherwise the backend's Redis `media:status` cache (and - // the frontend's 10s polling) stays stuck on the finished track. - discordPlayer.onIdle(() => { - this.advanceQueue() - .then(() => this.publishStatus()) - .catch((err) => { - this.logger.error({ err }, "Auto-advance failed"); - }); - }); - } - - /** - * Persist the latest media state to Redis so the backend/frontend see queue - * advances that happen outside a command (natural track end). CommandHandler - * owns the Redis status-key writes for command-triggered changes; this - * covers the side-effect-only path. - */ - private publishStatus(): void { - try { - setMediaStatusKey(this.getCurrentMediaStatus()); - } catch (err: unknown) { - this.logger.warn( - { error: err instanceof Error ? err.message : String(err) }, - "Failed to publish media status on track end", - ); - } - } - - getCurrentMediaStatus(): MediaStatusPayload { - return buildStatusPayload(); - } - - async handleMediaQueue(cmd: CommandMessage): Promise> { - // Accept both `url` (canonical) and `source` (legacy FE) for resilience. - const url = String(cmd.payload.url ?? cmd.payload.source ?? "").trim(); - const requestedBy = String(cmd.payload.requestedBy ?? "unknown"); - - if (!url) { - this.logger.warn("media:queue received without a URL"); - return { - id: cmd.id, - success: false, - data: null, - error: "url is required", - }; - } - - if (!discordPlayer.isConnected()) { - this.logger.warn("media:queue attempted without active voice connection"); - return { - id: cmd.id, - success: false, - data: null, - error: "Not connected to a voice channel. Connect to voice first.", - }; - } - - // Lightweight metadata fetch for display — the full resolve happens in playNext - let title: string = url; - let duration: number | undefined; - let thumbnailUrl: string | undefined; - try { - const info = await extractMediaInfo(url); - title = info.title ?? url; - duration = info.duration; - thumbnailUrl = info.thumbnail; - } catch { - this.logger.warn( - { url }, - "Could not pre-resolve media metadata, queueing blind", - ); - } - - const item: MediaQueueItem = { - id: randomUUID(), - source: url, - title, - kind: "url" as const, - mode: "music", - requestedBy, - addedAt: Date.now(), - status: "queued", - duration, - thumbnailUrl, - }; - - mediaQueue.push(item); - this.logger.info( - { url, title, queueLength: mediaQueue.length }, - "Media queued", - ); - - // If nothing is playing, start immediately (next tick so status publishes) - if (currentTrackItem === null) { - setImmediate(() => { - this.playNext().catch((err) => { - this.logger.error({ err }, "playNext after queue failed"); - }); - }); - } - - return { - id: cmd.id, - success: true, - data: buildStatusPayload(), - }; - } - - async handleMediaSkip(cmd: CommandMessage): Promise> { - if (currentTrackItem) { - discordPlayer.stop("music"); - currentTrackItem = null; - } - await this.playNext(); - return { - id: cmd.id, - success: true, - data: buildStatusPayload(), - }; - } - - async handleMediaStop(cmd: CommandMessage): Promise> { - discordPlayer.stop("music"); - currentTrackItem = null; - mediaQueue.length = 0; // Clear entire queue - return { - id: cmd.id, - success: true, - data: buildStatusPayload(), - }; - } - - async handleMediaVolume(cmd: CommandMessage): Promise> { - const volume = Number(cmd.payload.volume); - if (!Number.isFinite(volume)) { - return { - id: cmd.id, - success: false, - data: null, - error: "volume must be a number", - }; - } - discordPlayer.setMusicVolume(volume); - return { - id: cmd.id, - success: true, - data: buildStatusPayload(), - }; - } - - async handleMediaLoop(cmd: CommandMessage): Promise> { - loopEnabled = Boolean(cmd.payload.loop); - this.logger.info({ loop: loopEnabled }, "Media loop toggled"); - this.publishStatus(); - return { - id: cmd.id, - success: true, - data: buildStatusPayload(), - }; - } - - // --------------------------------------------------------------------------- - // Internal - // --------------------------------------------------------------------------- - - /** - * Advance to the next item in the queue. Called after natural track end - * (via onIdle) or after manual skip. - */ - private async playNext(): Promise { - // Stop any currently playing track (without triggering idle callback) - if (currentTrackItem) { - discordPlayer.stop("music"); - currentTrackItem = null; - } - currentTranscodeCleanup?.(); - currentTranscodeCleanup = null; - - const next = mediaQueue.shift(); - if (!next) { - this.logger.info("Queue empty — nothing to play"); - return; - } - - currentTrackItem = next; - next.status = "playing"; - - try { - this.logger.info( - { url: next.source, title: next.title }, - "Playing next from queue", - ); - - const resolution = await resolveMediaUrl(next.source); - - // Update title/duration with actual resolved values - next.title = resolution.title ?? next.title; - next.duration = resolution.duration ?? next.duration; - - // Music playback: transcode once to high-quality OggOpus (48kHz stereo, - // 192kbps) with volume baked into the encode. This avoids the double - // lossy encode that inlineVolume would cause and gives Discord the - // cleanest possible stream. - const transcoded = transcodeToHighQualityOgg( - resolution.stream, - discordPlayer.getMusicVolume(), - ); - - discordPlayer.playStream(transcoded.stream, "music", { - inputType: StreamType.OggOpus, - inlineVolume: false, - }); - currentTranscodeCleanup = transcoded.cleanup; - - this.logger.info({ title: next.title }, "Playback started"); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - this.logger.error( - { error: message, url: next.source, title: next.title }, - "Failed to play queued item, skipping to next", - ); - - currentTrackItem = null; - next.status = "failed"; - - // Try the next item in the queue - setImmediate(() => { - this.playNext().catch((err2) => { - this.logger.error( - { err: err2 }, - "playNext after error recovery failed", - ); - }); - }); - } - } - - /** - * Called by the idle callback — delegates to playNext since the player is - * already idle and currentTrackItem is already null. When loop mode is - * enabled and a music track ended naturally, requeue it so it plays again. - */ - private async advanceQueue(): Promise { - const finished = currentTrackItem; - currentTrackItem = null; - - if (loopEnabled && finished && finished.mode === "music") { - mediaQueue.unshift(finished); - this.logger.info( - { title: finished.title }, - "Loop enabled — replaying finished track", - ); - } - - await this.playNext(); - } -} diff --git a/services/discord-gateway/src/modules/command-handler/mediaStatusSink.ts b/services/discord-gateway/src/modules/command-handler/mediaStatusSink.ts deleted file mode 100644 index 3534b34b..00000000 --- a/services/discord-gateway/src/modules/command-handler/mediaStatusSink.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type Redis from "ioredis"; -import { createChildLogger } from "@/shared/logger/index"; -import { MEDIA_STATUS_KEY } from "../../shared/redis-channels.js"; - -/** - * Shared sink for writing the media status Redis key. - * - * CommandHandler owns the publisher + status writes for command-triggered - * changes (`publishMediaStatus`). MediaHandler needs to also persist status - * when the queue advances *outside* a command (natural track end / screen-share - * done), so we expose the real publisher here and let CommandHandler wire it - * once at startup. - */ -const logger = createChildLogger("media-status-sink"); - -let _setMediaStatusKey: ((payload: unknown) => void) | null = null; - -export function setMediaStatusWriter(writer: (payload: unknown) => void): void { - _setMediaStatusKey = writer; -} - -export function setMediaStatusKey(payload: unknown): void { - if (!_setMediaStatusKey) { - logger.warn("Media status writer not wired — skipping status publish"); - return; - } - _setMediaStatusKey(payload); -} - -export { MEDIA_STATUS_KEY }; - -export function wireMediaStatusWriter(redisPub: Redis): void { - setMediaStatusWriter((payload) => { - redisPub - .set(MEDIA_STATUS_KEY, JSON.stringify(payload)) - .catch((err: unknown) => { - const msg = err instanceof Error ? err.message : String(err); - logger.warn({ error: msg }, "Failed to update media status key"); - }); - }); -} diff --git a/services/discord-gateway/src/modules/command-handler/video.handler.ts b/services/discord-gateway/src/modules/command-handler/video.handler.ts deleted file mode 100644 index d00eca19..00000000 --- a/services/discord-gateway/src/modules/command-handler/video.handler.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { Client, Guild, VoiceChannel } from "discord.js-selfbot-v13"; -import type { CommandMessage, CommandReply } from "../../shared/index.js"; -import { - startStreamWatch, - stopStreamWatch, -} from "../voice-recording/streamWatchReceiver.js"; -import type { VoiceController } from "../voice-recording/voiceController.js"; - -// --------------------------------------------------------------------------- -// VideoHandler — manual screen-share / camera watch (selfbot-viable capture) -// --------------------------------------------------------------------------- -// -// On a selfbot (user token) there is NO automatic detection of other members' -// video (no VOICE_STATE_UPDATE for them, no member list). The only way to -// capture another member's SCREEN SHARE is an operator-initiated STREAM_WATCH -// (gateway op 20, NOT gated on bot-vs-user) — which this handler exposes as a -// backend→gateway Redis command. `streamWatchReceiver` does the DAVE handshake, -// per-burst `.mp4` segmentation (mirroring voice silence split), DB insert and -// Tele upload exactly like the audio path. -// -// Camera-of-others stays impossible on a selfbot (DAVE decrypt is keyed per real -// userId and a selfbot cannot learn others' ids), so this command targets SCREEN -// SHARE. Watching the operator's OWN camera/share is a separate (parameterized) -// follow-up. -export class VideoHandler { - constructor( - private client: Client | null, - private voiceController: VoiceController | null, - ) {} - - setClient(client: Client): void { - this.client = client; - } - - setVoiceController(vc: VoiceController): void { - this.voiceController = vc; - } - - /** - * Resolve the active voice channel for a guild from the voice controller. - * Prefers the controller's live connection; falls back to the client cache. - */ - private async resolveChannel( - guildId: string, - requestedChannelId?: string, - ): Promise<{ guild: Guild; channel: VoiceChannel } | null> { - const client = this.client; - if (!client) return null; - const guild = - client.guilds.cache.get(guildId) ?? - (await client.guilds.fetch(guildId).catch(() => null)); - if (!guild) return null; - - // If an explicit channelId was given, use it. - const channelId = - requestedChannelId || this.voiceController?.getStatus()?.activeChannelId; - if (channelId) { - const ch = (guild.channels.cache.get(channelId) ?? - (await guild.channels - .fetch(channelId) - .catch(() => null))) as VoiceChannel | null; - if (ch && ch.type === "GUILD_VOICE") return { guild, channel: ch }; - } - // Fallback: any voice channel the account is currently in. - const voiceCh = Array.from(guild.channels.cache.values()).find( - (c): c is VoiceChannel => - c.type === "GUILD_VOICE" && c.members?.has(client.user?.id ?? ""), - ); - return voiceCh ? { guild, channel: voiceCh } : null; - } - - async handleVideoWatch(cmd: CommandMessage): Promise> { - if (!this.client) { - return { - id: cmd.id, - success: false, - data: null, - error: "Gateway not initialized", - }; - } - const guildId = String(cmd.payload.guildId ?? ""); - const userId = String(cmd.payload.userId ?? ""); - const channelId = String(cmd.payload.channelId ?? ""); - if (!guildId || !userId) { - return { - id: cmd.id, - success: false, - data: null, - error: "guildId and userId are required", - }; - } - - const resolved = await this.resolveChannel(guildId, channelId || undefined); - if (!resolved) { - return { - id: cmd.id, - success: false, - data: null, - error: "No active voice channel to watch in this guild", - }; - } - if (userId === this.client.user?.id) { - // Self-watch is intentionally not enabled by default (see file header). - return { - id: cmd.id, - success: false, - data: null, - error: "Watching the selfbot's own stream is not enabled", - }; - } - - await startStreamWatch(resolved.channel, userId); - return { - id: cmd.id, - success: true, - data: { - status: "requested", - guildId, - channelId: resolved.channel.id, - userId, - }, - }; - } - - async handleVideoUnwatch( - cmd: CommandMessage, - ): Promise> { - const guildId = String(cmd.payload.guildId ?? ""); - const userId = String(cmd.payload.userId ?? ""); - if (!guildId || !userId) { - return { - id: cmd.id, - success: false, - data: null, - error: "guildId and userId are required", - }; - } - stopStreamWatch(guildId, userId); - return { - id: cmd.id, - success: true, - data: { status: "stopped", guildId, userId }, - }; - } -} diff --git a/services/discord-gateway/src/modules/command-handler/voice.handler.ts b/services/discord-gateway/src/modules/command-handler/voice.handler.ts deleted file mode 100644 index 5ec441d1..00000000 --- a/services/discord-gateway/src/modules/command-handler/voice.handler.ts +++ /dev/null @@ -1,229 +0,0 @@ -import type { Client } from "discord.js-selfbot-v13"; -import Redis from "ioredis"; -import { config } from "../../shared/config/config.js"; -import type { CommandMessage, CommandReply } from "../../shared/index.js"; -import { createChildLogger } from "../../shared/logger/index.js"; -import { discordPlayer } from "../voice-recording/player.js"; -import { voiceTransmitter } from "../voice-recording/transmitter.js"; -import type { VoiceController } from "../voice-recording/voiceController.js"; - -// --------------------------------------------------------------------------- -// VoiceHandler -// --------------------------------------------------------------------------- - -export class VoiceHandler { - private logger = createChildLogger("voice-handler"); - - constructor( - private client: Client | null, - private voiceController: VoiceController | null, - ) {} - - setClient(client: Client): void { - this.client = client; - } - - setVoiceController(voiceController: VoiceController): void { - this.voiceController = voiceController; - } - - async handleVoiceConnect( - cmd: CommandMessage, - ): Promise> { - if (!this.client || !this.voiceController) { - return { - id: cmd.id, - success: false, - data: null, - error: "Gateway not initialized", - }; - } - - const guildId = String(cmd.payload.guildId ?? ""); - const channelId = String(cmd.payload.channelId ?? ""); - - if (!guildId || !channelId) { - return { - id: cmd.id, - success: false, - data: null, - error: "guildId and channelId are required", - }; - } - - const status = await this.voiceController.connect(guildId, channelId); - return { id: cmd.id, success: true, data: status }; - } - - async handleVoiceDisconnect( - cmd: CommandMessage, - ): Promise> { - if (!this.voiceController) { - return { - id: cmd.id, - success: false, - data: null, - error: "Gateway not initialized", - }; - } - - const status = await this.voiceController.disconnect({ - clearPersisted: true, - intentional: true, - }); - return { id: cmd.id, success: true, data: status }; - } - - async handleVoiceDisconnectGuild( - cmd: CommandMessage, - ): Promise> { - if (!this.voiceController) { - return { - id: cmd.id, - success: false, - data: null, - error: "Gateway not initialized", - }; - } - - const guildId = String(cmd.payload.guildId ?? ""); - if (!guildId) { - return { - id: cmd.id, - success: false, - data: null, - error: "guildId is required", - }; - } - - await this.voiceController.disconnectGuild(guildId, { - clearPersisted: true, - intentional: true, - }); - const status = this.voiceController.getStatus(); - return { id: cmd.id, success: true, data: status }; - } - - async handleVoiceChannels( - cmd: CommandMessage, - ): Promise> { - if (!this.client) { - return { - id: cmd.id, - success: false, - data: null, - error: "Gateway not initialized", - }; - } - - const guildId = String(cmd.payload.guildId ?? ""); - if (!guildId) { - return { - id: cmd.id, - success: false, - data: null, - error: "guildId is required", - }; - } - - try { - const guild = await this.client.guilds.fetch(guildId); - const channels = await guild.channels.fetch(); - const voiceChannels = channels - .filter((c) => c?.type === "GUILD_VOICE") - .map((c) => ({ - id: c.id, - name: c.name, - type: "voice" as const, - // selfbot exposes joinable (permission check) — let FE filter - // channels the account actually may join. - joinable: (c as { joinable?: boolean }).joinable ?? true, - })); - - return { id: cmd.id, success: true, data: voiceChannels }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { id: cmd.id, success: false, data: null, error: msg }; - } - } - - async handleVoiceTransmitStart( - cmd: CommandMessage, - ): Promise> { - if (!discordPlayer.isConnected()) { - return { - id: cmd.id, - success: false, - data: null, - error: "Not connected to voice channel", - }; - } - - // Double-check: verify the voice controller also reports connected - if (this.voiceController) { - const vcStatus = this.voiceController.getStatus(); - if (!vcStatus.connected) { - this.logger.warn( - "Player reports connected but voice controller says disconnected — stale state", - ); - return { - id: cmd.id, - success: false, - data: null, - error: "Voice channel connection is stale — reconnect first", - }; - } - } - - try { - // IMPORTANT: transmitter needs its OWN Redis client because it calls - // .subscribe() which converts the connection to subscriber mode. Reusing - // the publish connection from CommandHandler would corrupt it and break - // every command reply + status update. - const transmitRedis = new Redis(config.REDIS_URL); - await voiceTransmitter.start(transmitRedis); - - const status = voiceTransmitter.getStatus(); - this.logger.info({ status }, "Voice transmit started"); - - return { - id: cmd.id, - success: true, - data: status, - }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - this.logger.error({ error: message }, "Failed to start voice transmit"); - return { - id: cmd.id, - success: false, - data: null, - error: message, - }; - } - } - - async handleVoiceTransmitStop( - cmd: CommandMessage, - ): Promise> { - try { - await voiceTransmitter.stop(); - this.logger.info("Voice transmit stopped"); - - return { - id: cmd.id, - success: true, - data: { status: "stopped" }, - }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - this.logger.error({ error: message }, "Failed to stop voice transmit"); - return { - id: cmd.id, - success: false, - data: null, - error: message, - }; - } - } -} diff --git a/services/discord-gateway/src/modules/event-broadcaster/eventBroadcaster.ts b/services/discord-gateway/src/modules/event-broadcaster/eventBroadcaster.ts index 1acfeaf1..83834279 100644 --- a/services/discord-gateway/src/modules/event-broadcaster/eventBroadcaster.ts +++ b/services/discord-gateway/src/modules/event-broadcaster/eventBroadcaster.ts @@ -112,87 +112,6 @@ export class EventBroadcaster { }); } - async voiceRecordingStarted(data: Record): Promise { - this.logger.debug({ data }, "Publishing voice_recording_started"); - await this.publisher.publish(EventChannels.VOICE_STARTED, { - type: "voice_recording_started", - data, - timestamp: Date.now(), - source: "discord-gateway", - }); - } - - async voiceRecordingStopped(data: Record): Promise { - this.logger.debug({ data }, "Publishing voice_recording_stopped"); - await this.publisher.publish(EventChannels.VOICE_STOPPED, { - type: "voice_recording_stopped", - data, - timestamp: Date.now(), - source: "discord-gateway", - }); - } - - async voiceRecordingUploaded(data: Record): Promise { - this.logger.debug({ data }, "Publishing voice_recording_uploaded"); - await this.publisher.publish(EventChannels.VOICE_UPLOADED, { - type: "voice_recording_uploaded", - data, - timestamp: Date.now(), - source: "discord-gateway", - }); - } - - /** - * Broadcasts PCM audio data for real-time voice streaming - * @param pcmBuffer - Raw PCM audio buffer - * @param userId - Discord user ID - * @param metadata - Optional metadata about the audio chunk - */ - async voicePcmData( - pcmBuffer: Buffer, - userId: string, - metadata?: Record, - ): Promise { - this.logger.debug( - { userId, pcmSize: pcmBuffer.length }, - "Publishing voice_pcm_data", - ); - await this.publisher.publish(EventChannels.VOICE_PCM, { - type: "voice_pcm_data", - data: { - userId, - pcm: pcmBuffer.toString("base64"), - metadata, - }, - timestamp: Date.now(), - source: "discord-gateway", - }); - } - - /** - * Broadcasts voice user activity state changes - * @param userId - Discord user ID - * @param data - User state data including username, avatar, and speaking status - */ - async voiceActiveUser( - userId: string, - data: { username: string; avatar: string; speaking: boolean }, - ): Promise { - this.logger.debug( - { userId, speaking: data.speaking }, - "Publishing voice_active_user", - ); - await this.publisher.publish(EventChannels.VOICE_ACTIVE_USER, { - type: "voice_active_user", - data: { - userId, - ...data, - }, - timestamp: Date.now(), - source: "discord-gateway", - }); - } - async reactionAdded(data: Record): Promise { this.logger.debug({ data }, "Publishing reaction_added"); await this.publisher.publish(EventChannels.REACTION_ADDED, { @@ -283,16 +202,6 @@ export class EventBroadcaster { }); } - async voiceAnalyzed(data: Record): Promise { - this.logger.debug({ data }, "Publishing voice_analyzed"); - await this.publisher.publish(EventChannels.VOICE_ANALYZED, { - type: "voice_analyzed", - data, - timestamp: Date.now(), - source: "discord-gateway", - }); - } - async moderationAction(data: Record): Promise { this.logger.debug({ data }, "Publishing moderation_action"); await this.publisher.publish(EventChannels.MODERATION_ACTION, { diff --git a/services/discord-gateway/src/modules/event-broadcaster/eventTypes.ts b/services/discord-gateway/src/modules/event-broadcaster/eventTypes.ts index 277716f7..d7a30a92 100644 --- a/services/discord-gateway/src/modules/event-broadcaster/eventTypes.ts +++ b/services/discord-gateway/src/modules/event-broadcaster/eventTypes.ts @@ -16,12 +16,6 @@ import { DISCORD_THREAD_CREATED, DISCORD_THREAD_DELETED, DISCORD_THREAD_UPDATED, - DISCORD_VOICE_ACTIVE_USER, - DISCORD_VOICE_ANALYZED, - DISCORD_VOICE_PCM, - DISCORD_VOICE_STARTED, - DISCORD_VOICE_STOPPED, - DISCORD_VOICE_UPLOADED, type DiscordGatewayEvent, } from "../../shared/index.js"; @@ -34,12 +28,6 @@ export const EventChannels = { MESSAGE_ANALYZED: DISCORD_MESSAGE_ANALYZED, ATTACHMENT_CREATED: DISCORD_ATTACHMENT_CREATED, ATTACHMENT_UPLOADED: DISCORD_ATTACHMENT_UPLOADED, - VOICE_STARTED: DISCORD_VOICE_STARTED, - VOICE_STOPPED: DISCORD_VOICE_STOPPED, - VOICE_UPLOADED: DISCORD_VOICE_UPLOADED, - // Real-time voice streaming channels - VOICE_ACTIVE_USER: DISCORD_VOICE_ACTIVE_USER, // Active speaker state updates - VOICE_PCM: DISCORD_VOICE_PCM, // Live PCM audio data stream ANALYSIS_QUEUE_STATUS: DISCORD_ANALYSIS_QUEUE_STATUS, REACTION_ADDED: DISCORD_REACTION_ADDED, REACTION_REMOVED: DISCORD_REACTION_REMOVED, @@ -50,7 +38,6 @@ export const EventChannels = { PRESENCE_UPDATED: DISCORD_PRESENCE_UPDATED, GUILD_MEMBER_ADDED: DISCORD_GUILD_MEMBER_ADDED, GUILD_MEMBER_REMOVED: DISCORD_GUILD_MEMBER_REMOVED, - VOICE_ANALYZED: DISCORD_VOICE_ANALYZED, MODERATION_ACTION: DISCORD_MODERATION_ACTION, } as const; diff --git a/services/discord-gateway/src/modules/message-capture/index.ts b/services/discord-gateway/src/modules/message-capture/index.ts index f36db8fc..a29e2312 100644 --- a/services/discord-gateway/src/modules/message-capture/index.ts +++ b/services/discord-gateway/src/modules/message-capture/index.ts @@ -9,7 +9,6 @@ export type { AIStatus, AttachmentRecord, MessageRecord, - VoiceSegmentRecord, } from "../message-capture/types.js"; export type { TextCaptureTarget } from "./messageCapture.js"; export { diff --git a/services/discord-gateway/src/modules/message-capture/types.ts b/services/discord-gateway/src/modules/message-capture/types.ts index c055716a..c760b555 100644 --- a/services/discord-gateway/src/modules/message-capture/types.ts +++ b/services/discord-gateway/src/modules/message-capture/types.ts @@ -1,11 +1,8 @@ -import type fs from "node:fs"; -import type prism from "prism-media"; import type { AnalysisQueueStatus, AttachmentRecord, MessageRecord, UserMetadata, - VoiceRecordingUploadData, } from "../../shared/index.js"; // Re-export all shared types for backward compatibility @@ -29,41 +26,8 @@ export type { ReviewStatus, RoleMetadata, UserMetadata, - VoiceRecordingUploadData, - VoiceSegmentRecord, } from "../../shared/index.js"; -// Types that are LOCAL ONLY (not in shared) — keep here -export interface SegmentState { - index: number; - startTime: number; - endTime: number | null; - filename: string; - jsonFilename: string; - oggStream: prism.opus.OggLogicalBitstream; - out: fs.WriteStream; -} - -export interface SegmentMetadata extends UserMetadata { - recordingSessionId: string; - sessionId: string; - sessionStartTime: number; - segmentIndex: number; - segmentMs: number; - startTime: number; - endTime: number; - durationMs: number; - filename: string; -} - -export interface PcmBroadcaster { - broadcastPcmToWeb?: (chunk: Buffer, userId: string) => void; - updateActiveUser?: ( - userId: string, - data: { username: string; avatar: string; speaking: boolean }, - ) => void; -} - // Local-only types (not shared across services) export type ModerationWsEvent = | { type: "ui_state"; state: unknown } @@ -73,6 +37,4 @@ export type ModerationWsEvent = | { type: "message_deleted"; data: { id: string; deleted_at: number } } | { type: "message_analyzed"; data: MessageRecord } | { type: "attachment_created"; data: AttachmentRecord } - | { type: "analysis_queue_status"; data: AnalysisQueueStatus } - | { type: "media_state"; state: unknown } - | { type: "voice_recording_uploaded"; data: VoiceRecordingUploadData }; + | { type: "analysis_queue_status"; data: AnalysisQueueStatus }; diff --git a/services/discord-gateway/src/modules/voice-pcm-ws/index.ts b/services/discord-gateway/src/modules/voice-pcm-ws/index.ts deleted file mode 100644 index 89ceaf49..00000000 --- a/services/discord-gateway/src/modules/voice-pcm-ws/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { VoicePcmWsClient } from "./voicePcmWsClient.js"; diff --git a/services/discord-gateway/src/modules/voice-pcm-ws/voicePcmWsClient.ts b/services/discord-gateway/src/modules/voice-pcm-ws/voicePcmWsClient.ts deleted file mode 100644 index 5a025e9b..00000000 --- a/services/discord-gateway/src/modules/voice-pcm-ws/voicePcmWsClient.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Direct WebSocket client for streaming voice PCM to the backend. - * - * Bypasses Redis pub/sub entirely — gateway connects as a WS client - * to the backend's existing WebSocket server and sends pre-formatted - * binary frames (4-byte FNV-1a userId hash + raw Int16LE PCM). - * The backend forwards these unchanged to frontend clients. - */ - -import WebSocket from "ws"; -import { createChildLogger } from "@/shared/logger/index"; - -const logger = createChildLogger("voice-pcm-ws"); - -/** 32-bit FNV-1a hash — mirrors backend/ws/redis-bridge.ts and frontend useAudioPlayback.ts */ -function hashUserId(userId: string): number { - let hash = 0x811c9dc5; - for (let i = 0; i < userId.length; i++) { - hash ^= userId.charCodeAt(i); - hash = Math.imul(hash, 0x01000193); - } - return hash >>> 0; -} - -export class VoicePcmWsClient { - private ws: WebSocket | null = null; - private readonly token: string; - private readonly url: string; - private reconnectTimer: ReturnType | null = null; - private closed = false; - private reconnectMs = 1000; - - constructor(url: string, token: string) { - this.url = url; - this.token = token; - } - - /** Open connection and begin automatic reconnect loop. */ - connect(): void { - if (this.closed) return; - this.doConnect(); - } - - /** - * Send a PCM chunk to the backend. - * - * Binary frame format (identical to what frontend expects): - * Byte 0–3: FNV-1a 32-bit userId hash (UInt32LE) - * Byte 4+: PCM audio (24kHz mono Int16LE) - * - * Silently drops if not connected — real-time audio, stale - * chunks from a disconnect gap are useless. - */ - sendPcm(userId: string, pcmBuffer: Buffer): void { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; - - const hash = hashUserId(userId); - const frame = Buffer.alloc(4 + pcmBuffer.length); - frame.writeUInt32LE(hash, 0); - pcmBuffer.copy(frame, 4); - - try { - this.ws.send(frame); - } catch (err) { - logger.warn({ err }, "Failed to send PCM frame"); - } - } - - /** Graceful close — send close frame, clear reconnect timer. */ - async close(): Promise { - this.closed = true; - this.clearReconnectTimer(); - - if (!this.ws) return; - - // Remove listeners so reconnect doesn't fire during close - this.ws.removeAllListeners(); - - if (this.ws.readyState === WebSocket.OPEN) { - this.ws.close(1000, "shutdown"); - } - - this.ws = null; - } - - // ── internals ──────────────────────────────────────────────────────── - - private getUrlWithAuth(): string { - const sep = this.url.includes("?") ? "&" : "?"; - return `${this.url}${sep}token=${encodeURIComponent(this.token)}`; - } - - private doConnect(): void { - if (this.closed) return; - - const authUrl = this.getUrlWithAuth(); - logger.info({ url: this.url }, "Connecting to backend WebSocket"); - - try { - this.ws = new WebSocket(authUrl); - } catch (err) { - logger.error({ err }, "Failed to create WebSocket"); - this.scheduleReconnect(); - return; - } - - this.ws.on("open", () => { - logger.info("Voice PCM WS client connected"); - this.reconnectMs = 1000; // reset backoff on successful connect - }); - - this.ws.on("close", (code, reason) => { - logger.warn( - { code, reason: reason.toString() }, - "Voice PCM WS client disconnected", - ); - this.ws = null; - this.scheduleReconnect(); - }); - - this.ws.on("error", (err) => { - logger.error({ err: err.message }, "Voice PCM WS client error"); - // on("close") fires after on("error") — reconnect handled there - }); - } - - private scheduleReconnect(): void { - if (this.closed) return; - this.clearReconnectTimer(); - - // Exponential backoff with jitter: 1s → 2s → 4s → ... → 30s cap - const delay = this.reconnectMs + Math.random() * 1000; - logger.info({ delayMs: Math.round(delay) }, "Scheduling WS reconnect"); - - this.reconnectTimer = setTimeout(() => { - this.reconnectTimer = null; - if (!this.closed) { - this.doConnect(); - } - }, delay); - - this.reconnectMs = Math.min(this.reconnectMs * 2, 30_000); - } - - private clearReconnectTimer(): void { - if (this.reconnectTimer !== null) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - } -} diff --git a/services/discord-gateway/src/modules/voice-recording/ffmpegProcess.ts b/services/discord-gateway/src/modules/voice-recording/ffmpegProcess.ts deleted file mode 100644 index 70d7eff0..00000000 --- a/services/discord-gateway/src/modules/voice-recording/ffmpegProcess.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { spawn } from "node:child_process"; -import { createChildLogger } from "@/shared/logger/index"; - -const logger = createChildLogger("ffmpeg-process"); - -export interface MuxFfmpegArgsOptions { - inputs: string[]; - filter: string; - output: string; - codec: string; - audioFrequency?: number; - audioChannels?: number; -} - -/** - * Builds ffmpeg argument array for muxing audio clips. - */ -export function buildMuxFfmpegArgs(options: MuxFfmpegArgsOptions): string[] { - const args: string[] = ["-y"]; - - for (const input of options.inputs) { - args.push("-i", input); - } - - args.push("-filter_complex", options.filter); - args.push("-map", "[out]"); - args.push("-codec:a", options.codec); - - if (options.audioFrequency !== undefined) { - args.push("-ar", String(options.audioFrequency)); - } - - if (options.audioChannels !== undefined) { - args.push("-ac", String(options.audioChannels)); - } - - args.push(options.output); - - return args; -} - -/** - * Runs ffmpeg with the given arguments. - * Resolves on successful (code 0) exit, rejects on error or non-zero exit. - */ -export function runFfmpeg(args: string[]): Promise { - return new Promise((resolve, reject) => { - logger.debug({ args }, "Starting ffmpeg"); - - const proc = spawn("ffmpeg", args, { - stdio: ["ignore", "pipe", "pipe"], - }); - - let stderrBuf = ""; - proc.stderr?.on("data", (chunk: Buffer) => { - stderrBuf += chunk.toString("utf8"); - }); - - proc.on("close", (code) => { - if (code === 0) { - logger.debug("ffmpeg completed successfully"); - resolve(); - } else { - const detail = stderrBuf.trim().slice(0, 2000); - logger.warn( - { exitCode: code, stderr: detail }, - "ffmpeg exited with non-zero code", - ); - reject(new Error(`ffmpeg exited with code ${code}: ${detail}`)); - } - }); - - proc.on("error", (err) => { - logger.error({ error: err.message }, "ffmpeg process error"); - reject(err); - }); - }); -} diff --git a/services/discord-gateway/src/modules/voice-recording/mediaSource.ts b/services/discord-gateway/src/modules/voice-recording/mediaSource.ts deleted file mode 100644 index e83b8f1e..00000000 --- a/services/discord-gateway/src/modules/voice-recording/mediaSource.ts +++ /dev/null @@ -1,573 +0,0 @@ -import { type ChildProcess, spawn } from "node:child_process"; -import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { PassThrough, type Readable } from "node:stream"; -import { StreamType } from "@discordjs/voice"; -import { createChildLogger } from "@/shared/logger/index"; - -const logger = createChildLogger("media-source"); - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export interface MediaInfo { - title: string; - duration: number; - thumbnail?: string; -} - -export interface MediaSourceResolution { - stream: Readable; - type: StreamType; - title?: string; - duration?: number; - info: MediaInfo; -} - -export interface ResolveOptions { - /** Timeout in milliseconds for the yt-dlp process. */ - timeout?: number; - /** - * yt-dlp format string override (e.g. "bestaudio[ext=m4a]"). - * Defaults to "bestaudio[ext=m4a]/bestaudio/best". - */ - quality?: string; -} - -export interface TranscodeResult { - stream: Readable; - /** Kill the ffmpeg child (used on stop/skip). */ - cleanup: () => void; -} - -/** - * Re-encode a source stream to high-quality OggOpus (48kHz stereo, 192kbps). - * - * Discord voice downmixes whatever we feed it to the channel's bitrate, so the - * best we can do is hand it a clean 48kHz stereo Opus stream instead of the - * raw source (which may be mono, low-bitrate, or a non-Opus container). The - * volume is baked into the encode with `-af volume=` so the player does not - * need inlineVolume re-encoding (double lossy encode). - */ -export function transcodeToHighQualityOgg( - input: Readable, - volume: number, -): TranscodeResult { - const proc = spawn( - "ffmpeg", - [ - "-hide_banner", - "-loglevel", - "error", - "-i", - "pipe:0", - "-vn", - "-ac", - "2", - "-ar", - "48000", - "-c:a", - "libopus", - "-b:a", - "192k", - "-af", - `volume=${volume}`, - "-f", - "ogg", - "pipe:1", - ], - { stdio: ["pipe", "pipe", "ignore"] }, - ); - - input.pipe(proc.stdin); - // ffmpeg teardown closes stdin while the upstream source may still write — - // swallow EPIPE / destroyed-stream errors so they don't crash the gateway. - proc.stdin.on("error", (err: NodeJS.ErrnoException) => { - if ( - err.code === "EPIPE" || - err.code === "ERR_STREAM_DESTROYED" || - err.code === "ERR_STREAM_WRITE_AFTER_END" - ) { - logger.debug( - { code: err.code }, - "Transcode stdin closed during teardown", - ); - } else { - logger.error({ error: err.message }, "Transcode stdin error"); - } - }); - activeProcesses.add(proc); - - const cleanup = () => { - activeProcesses.delete(proc); - if (proc.exitCode === null) { - proc.kill("SIGKILL"); - } - }; - - proc.once("exit", () => activeProcesses.delete(proc)); - - // If ffmpeg fails, surface the error to the consumer stream so the - // AudioPlayer's error handler can advance the queue. - const output = proc.stdout; - output.on("error", () => cleanup()); - - return { stream: output, cleanup }; -} - -// --------------------------------------------------------------------------- -// Internal state -// --------------------------------------------------------------------------- - -/** Tracks all spawned yt-dlp child processes for shutdown cleanup. */ -const activeProcesses: Set = new Set(); - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function parseSeconds(value: string): number { - const n = Number.parseFloat(value); - return Number.isFinite(n) ? n : 0; -} - -/** - * Read the first two newline-delimited lines from a Readable stdout stream. - * - * yt-dlp with `--print before_dl:title --print before_dl:duration` outputs: - * line 1: video title - * line 2: duration in seconds (float) - * rest: raw binary audio data - * - * Returns the parsed header and a new Readable that contains all remaining - * data (the audio stream). - */ -const MAX_HEADER_BUFFER = 65536; // 64KB safety limit for metadata headers - -/** - * Read the title + duration header lines from yt-dlp's STDERR. - * - * When yt-dlp streams media to stdout (`-o -`) it redirects its `--print` - * output to STDERR so the media stream on stdout stays clean. The first two - * meaningful lines on stderr are then the title and (before_dl) duration. - * - * Blank lines and `[...]` info prefixes are skipped. An `ERROR:` line is - * reported via `onError` so a failing download surfaces as a resolution error - * instead of a silent empty stream. - */ -function readStderrHeader( - stderr: Readable, - onError: (message: string) => void, - maxBufferSize: number = MAX_HEADER_BUFFER, -): Promise<{ title: string; duration: number }> { - return new Promise((resolve) => { - let buffer = ""; - let title = ""; - let duration = 0; - let done = false; - - const finish = () => { - if (done) return; - done = true; - stderr.removeListener("data", onData); - resolve({ title, duration }); - }; - - const onData = (chunk: Buffer) => { - if (done) return; - buffer += chunk.toString("utf8"); - if (buffer.length > maxBufferSize) { - finish(); - return; - } - while (!done) { - const nl = buffer.indexOf("\n"); - if (nl === -1) break; // need more data - const line = buffer.slice(0, nl).trim(); - buffer = buffer.slice(nl + 1); - - if (line.length === 0) continue; // blank line - if (line.startsWith("[")) continue; // "[info] ..." — not a header - if (line.startsWith("ERROR")) { - onError(line); - finish(); - return; - } - if (!title) { - title = line; - } else { - duration = parseSeconds(line); - finish(); - return; - } - } - }; - - stderr.on("data", onData); - stderr.on("end", finish); - }); -} - -function buildNotInstalledError(): Error { - return new Error( - "yt-dlp is not installed or not found in PATH. " + - 'Run "pnpm run install:yt-dlp" to install it.', - ); -} - -/** - * Build the yt-dlp --cookies args. YouTube blocks anonymous embeds with a - * "Sign in to confirm you're not a bot" 403 unless yt-dlp is given a logged- - * in account's cookies. The path is configurable via GMW_YT_COOKIES_PATH - * (default: the BWS-provided file the deploy writes to /etc/.../ytcookies.txt). - * If the file doesn't exist we pass nothing and fall back to anon (YouTube - * may 403 — playback will fail gracefully, not crash). - */ -var _cachedCookiePath: string | null = null; -function buildCookieArgs(): string[] { - // Single source of truth: BWS injects the account cookies via env - // (gmw_yt_downloader_cookies → GMW_YT_DOWNLOADER_COOKIES by bws-exec). - // We materialize them to a temp Netscape file because yt-dlp --cookies - // only accepts a file path, not stdin, and multiline env values are not - // reliable to pass directly on the spawn argv. Falls back to the on-disk - // file at GMW_YT_COOKIES_PATH (or /etc/gmw-discord-gateway/ytcookies.txt) - // which the Nix deploy writes from BWS once at start. - if (_cachedCookiePath) return ["--cookies", _cachedCookiePath]; - const envCookies = process.env.GMW_YT_DOWNLOADER_COOKIES?.trim(); - if (envCookies?.includes("LOGIN_INFO")) { - const fdPath = join(tmpdir(), `gmw-ytcookies.${process.pid}.txt`); - writeFileSync(fdPath, envCookies); - try { - chmodSync(fdPath, 0o600); - } catch { - /* best-effort */ - } - _cachedCookiePath = fdPath; - logger.info( - { cookiePath: fdPath, source: "GMW_YT_DOWNLOADER_COOKIES env" }, - "Using YouTube cookies (from BWS env)", - ); - return ["--cookies", fdPath]; - } - const cookiePath = - process.env.GMW_YT_COOKIES_PATH ?? "/etc/gmw-discord-gateway/ytcookies.txt"; - try { - if (cookiePath && existsSync(cookiePath)) { - // Never hand the ORIGINAL system file to yt-dlp: recent yt-dlp rewrites - // the cookie file on close (`--cookies` implies write-back). The system - // file is owned by another user (root/deploy) and the service user - // cannot write it → PermissionError → yt-dlp exits 1 → playback - // fails for every attempt. Copy to a per-run temp file (like the env - // branch above) so write-back lands somewhere we own; if the original - // is not readable we fall back to anonymous (YouTube may 403 → the - // screen-share controller retries via Invidious mirrors without auth). - let cookieContents: string; - try { - cookieContents = readFileSync(cookiePath, "utf8"); - } catch { - logger.warn( - { cookiePath }, - "Cookie file not readable; continuing without cookies (anon)", - ); - return []; - } - const fdPath = join(tmpdir(), `gmw-ytcookies.${process.pid}.txt`); - writeFileSync(fdPath, cookieContents); - try { - chmodSync(fdPath, 0o600); - } catch { - /* best-effort */ - } - _cachedCookiePath = fdPath; - logger.info( - { cookiePath: fdPath, source: "on-disk file (copied)" }, - "Using YouTube cookies for yt-dlp", - ); - return ["--cookies", fdPath]; - } - } catch { - /* ignore — fallback to anon */ - } - logger.warn( - "No YouTube cookies available; yt-dlp will use anonymous (YouTube may 403)", - ); - return []; -} - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -/** - * Resolve a media URL (YouTube, Spotify, etc.) to a playable audio stream. - * - * Spawns `yt-dlp` with `-o -` so the raw audio bytes stream on stdout. Since - * stdout is the media sink, yt-dlp emits its `--print before_dl:title` / - * `before_dl:duration` header lines on STDERR — the title + duration are read - * from there and the stdout media stream is returned untouched. - * - * The returned stream uses `StreamType.Arbitrary` — suitable for - * `DiscordPlayer.playStream()` with `inputType: StreamType.Arbitrary`. - * - * @throws If yt-dlp is not installed or the process exits with a non-zero code - * before the metadata headers have been parsed. - */ -export function resolveMediaUrl( - url: string, - options?: ResolveOptions, -): Promise { - return new Promise((resolve, reject) => { - // Fallback chain: YouTube gets progressive m4a audio; direct-file hosts - // (upload mirror / Telegram files) expose a single generic format with an - // arbitrary ID — `bestaudio` alone fails there ("Requested format is not - // available"), so fall back to `bestaudio` then plain `best`. - const format = options?.quality ?? "bestaudio[ext=m4a]/bestaudio/best"; - const cookieArgs = buildCookieArgs(); - const args = [ - "-f", - format, - "-o", - "-", - "--no-progress", - "--no-warnings", - ...cookieArgs, - "--print", - "before_dl:title", - "--print", - "before_dl:duration", - url, - ]; - - logger.info({ url }, "Spawning yt-dlp for media resolution"); - - const proc = spawn("yt-dlp", args, { - stdio: ["ignore", "pipe", "pipe"], - }); - - activeProcesses.add(proc); - - // With `-o -` yt-dlp streams the raw audio on stdout and moves its - // `--print` headers to stderr — pipe stdout immediately so the child - // never blocks on a full pipe while we wait for the headers on stderr. - const mediaStream = new PassThrough(); - // Teardown (player stop / ffmpeg exit) destroys this stream while - // yt-dlp may still push bytes — without a listener an EPIPE / - // ERR_STREAM_DESTROYED surfaces as an uncaughtException. - mediaStream.on("error", (err: NodeJS.ErrnoException) => { - if ( - err.code === "EPIPE" || - err.code === "ERR_STREAM_DESTROYED" || - err.code === "ERR_STREAM_WRITE_AFTER_END" - ) { - logger.debug({ code: err.code }, "Media stream closed during teardown"); - } else { - logger.error({ error: err.message }, "Media stream error"); - } - }); - proc.stdout.pipe(mediaStream); - - let stderrBuf = ""; - let resolved = false; - - // -- helpers ----------------------------------------------------------- - - const failOnce = (err: Error) => { - if (resolved) return; - resolved = true; - activeProcesses.delete(proc); - mediaStream.destroy(); - reject(err); - }; - - const resolveOnce = (info: MediaInfo) => { - if (resolved) return; - resolved = true; - activeProcesses.delete(proc); - resolve({ - stream: mediaStream, - type: StreamType.Arbitrary, - title: info.title, - duration: info.duration, - info, - }); - }; - - // -- spawn error (ENOENT etc.) ---------------------------------------- - - proc.on("error", (err: NodeJS.ErrnoException) => { - if (err.code === "ENOENT") { - failOnce(buildNotInstalledError()); - } else { - failOnce(new Error(`yt-dlp failed to start: ${err.message}`)); - } - }); - - // -- stderr: title + duration headers --------------------------------- - // Capture raw stderr too, for the exit-diagnostics in the close handler. - - const _MAX_STDERR = 4096; - if (proc.stderr) { - proc.stderr.on("data", (chunk: Buffer) => { - if (stderrBuf.length < _MAX_STDERR) { - stderrBuf += chunk - .toString("utf8") - .slice(0, _MAX_STDERR - stderrBuf.length); - } - }); - } - - readStderrHeader(proc.stderr, (message) => { - failOnce(new Error(message)); - }) - .then(({ title, duration }) => { - resolveOnce({ title: title || url, duration }); - }) - .catch((err: Error) => { - failOnce(err); - }); - - // -- process exit (non-zero means failure) ----------------------------- - - proc.on("close", (code, signal) => { - activeProcesses.delete(proc); - - if (resolved) return; - - if (code !== null && code !== 0) { - const detail = stderrBuf.trim() ? `: ${stderrBuf.trim()}` : ""; - failOnce(new Error(`yt-dlp exited with code ${code}${detail}`)); - } else if (signal) { - failOnce(new Error(`yt-dlp was killed by signal ${signal}`)); - } else { - // Exited cleanly but the header lines never surfaced (e.g. a direct - // file URL with no duration) — keep the media stream alive anyway. - resolveOnce({ title: url, duration: 0 }); - } - }); - - // -- optional timeout -------------------------------------------------- - - if (options?.timeout && options.timeout > 0) { - const timer = setTimeout(() => { - if (resolved) return; - logger.warn({ url, timeout: options.timeout }, "yt-dlp timed out"); - proc.kill("SIGTERM"); - failOnce(new Error(`yt-dlp timed out after ${options.timeout}ms`)); - }, options.timeout); - - proc.once("close", () => clearTimeout(timer)); - } - }); -} - -/** - * Extract metadata (title, duration, thumbnail) from a media URL - * without downloading the audio stream. - * - * Uses `yt-dlp --dump-json` and parses the JSON output. - * - * @throws If yt-dlp is not installed or the process exits with a non-zero - * code. - */ -export async function extractMediaInfo(url: string): Promise { - return new Promise((resolve, reject) => { - const args = ["--dump-json", "--no-warnings", url]; - - logger.debug({ url }, "Spawning yt-dlp for metadata extraction"); - - const proc = spawn("yt-dlp", args, { - stdio: ["pipe", "pipe", "pipe"], - }); - - activeProcesses.add(proc); - - let stdoutBuf = ""; - let stderrBuf = ""; - const MAX_STDERR = 4096; - const MAX_STDOUT = 1_048_576; // 1MB safety limit - - if (proc.stdout) { - proc.stdout.on("data", (chunk: Buffer) => { - if (stdoutBuf.length < MAX_STDOUT) { - stdoutBuf += chunk - .toString("utf8") - .slice(0, MAX_STDOUT - stdoutBuf.length); - } - }); - } - - if (proc.stderr) { - proc.stderr.on("data", (chunk: Buffer) => { - if (stderrBuf.length < MAX_STDERR) { - stderrBuf += chunk - .toString("utf8") - .slice(0, MAX_STDERR - stderrBuf.length); - } - }); - } - - proc.on("error", (err: NodeJS.ErrnoException) => { - activeProcesses.delete(proc); - if (err.code === "ENOENT") { - reject(buildNotInstalledError()); - } else { - reject(new Error(`yt-dlp failed to start: ${err.message}`)); - } - }); - - proc.on("close", (code) => { - activeProcesses.delete(proc); - - if (code !== 0) { - const detail = stderrBuf.trim() ? `: ${stderrBuf.trim()}` : ""; - reject( - new Error( - `yt-dlp metadata extraction exited with code ${code}${detail}`, - ), - ); - return; - } - - try { - const raw = JSON.parse(stdoutBuf.trim()) as Record; - resolve({ - title: String(raw.title ?? url), - duration: typeof raw.duration === "number" ? raw.duration : 0, - thumbnail: String(raw.thumbnail ?? "") || undefined, - }); - } catch (parseErr) { - reject( - new Error( - `Failed to parse yt-dlp JSON output: ${(parseErr as Error).message}`, - ), - ); - } - }); - }); -} - -/** - * Kill all active yt-dlp / screen-share merge ffmpeg child processes. - * - * Call during graceful shutdown to ensure no orphan processes remain. - */ -export function cleanup(): void { - if (activeProcesses.size === 0) return; - - logger.info( - { count: activeProcesses.size }, - "Killing active yt-dlp processes", - ); - - for (const proc of activeProcesses) { - try { - proc.kill("SIGTERM"); - } catch { - // Process may already be dead — ignore - } - } - - activeProcesses.clear(); -} diff --git a/services/discord-gateway/src/modules/voice-recording/mediaTypes.ts b/services/discord-gateway/src/modules/voice-recording/mediaTypes.ts deleted file mode 100644 index 7327e5c1..00000000 --- a/services/discord-gateway/src/modules/voice-recording/mediaTypes.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { Readable } from "node:stream"; -import type { StreamType } from "@discordjs/voice"; - -export type MediaMode = "music" | "screenshare"; -export type MediaSourceKind = - | "url" - | "local" - | "youtube" - | "spotify" - | "search"; -export type MediaQueueItemStatus = "queued" | "playing" | "failed"; - -export interface ResolvedMediaSource { - source: string; - title: string; - kind: MediaSourceKind; -} - -export interface MediaQueueItem extends ResolvedMediaSource { - id: string; - mode: MediaMode; - requestedBy: string; - addedAt: number; - status: MediaQueueItemStatus; - /** Duration in seconds, if known. */ - duration?: number; - /** Thumbnail/preview URL, if available. */ - thumbnailUrl?: string; -} - -export interface MediaState { - playing: boolean; - activeMode: MediaMode | null; - musicVolume: number; - loop: boolean; - current: MediaQueueItem | null; - queue: MediaQueueItem[]; -} - -export interface QueueMediaOptions { - mode?: MediaMode; - requestedBy?: string; -} - -export interface MusicPlayback { - done: Promise; - stop(): void; -} - -export interface MusicPlayer { - play(source: ResolvedMediaSource): MusicPlayback; -} - -export type DiscordPlayerOwner = "none" | "browser-bridge" | "music"; - -export interface DiscordPlayOptions { - inputType?: StreamType; - /** Enable volume control via resource.volume (required for setMusicVolume to work). */ - inlineVolume?: boolean; - volume?: number; -} - -export interface DiscordAudioPlayer { - getOwner(): DiscordPlayerOwner; - isConnected(): boolean; - playStream( - stream: Readable, - owner: DiscordPlayerOwner, - options?: DiscordPlayOptions, - ): void; - pause(owner?: DiscordPlayerOwner): void; - unpause(owner?: DiscordPlayerOwner): boolean; - stop(owner?: DiscordPlayerOwner): void; - getMusicVolume(): number; - setMusicVolume(volume: number): void; -} diff --git a/services/discord-gateway/src/modules/voice-recording/muxer.ts b/services/discord-gateway/src/modules/voice-recording/muxer.ts deleted file mode 100644 index 0b4f479c..00000000 --- a/services/discord-gateway/src/modules/voice-recording/muxer.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { eq } from "drizzle-orm"; -import type { NodePgDatabase } from "drizzle-orm/node-postgres"; -import { createChildLogger } from "@/shared/logger/index"; -import { getDatabase } from "../../shared/database/drizzle.js"; -import type * as schema from "../../shared/database/schema.js"; -import { muxerJobsTable } from "../../shared/database/schema.js"; -import { buildMuxFfmpegArgs, runFfmpeg } from "./ffmpegProcess.js"; - -const logger = createChildLogger("muxer"); - -// ─── Types ───────────────────────────────────────────────────────────────────────────── - -export interface MuxerJobData { - inputs: string[]; - output: string; - guildId: string; - channelId: string; - sessionId: string; -} - -// ─── State ─────────────────────────────────────────────────────────────── - -let pollTimer: ReturnType | null = null; - -// ─── Public API ────────────────────────────────────────────────────────── - -/** - * Enqueue a muxer job that merges multiple OGG files into one. - */ -export async function enqueueMuxerJob(data: MuxerJobData): Promise { - const db = getDatabase() as unknown as NodePgDatabase; - const id = `${data.sessionId}-${Date.now()}`; - - try { - await db.insert(muxerJobsTable).values({ - id, - data: JSON.stringify(data), - status: "pending", - attempts: 0, - maxAttempts: 3, - createdAt: Date.now(), - updatedAt: Date.now(), - }); - logger.info({ id, sessionId: data.sessionId }, "Muxer job enqueued"); - } catch (error) { - logger.error( - { id, error: error instanceof Error ? error.message : String(error) }, - "Failed to enqueue muxer job", - ); - } -} - -/** - * Start the background worker that polls for pending muxer jobs. - */ -export function startMuxerWorker(): void { - if (pollTimer) return; - logger.info("Starting muxer worker (interval: 10s)"); - - pollTimer = setInterval(() => { - processNextJobs().catch((err: unknown) => { - logger.error({ error: String(err) }, "Muxer worker tick failed"); - }); - }, 10_000); -} - -/** - * Stop the background worker. - */ -export function stopMuxerWorker(): void { - if (!pollTimer) return; - clearInterval(pollTimer); - pollTimer = null; - logger.info("Muxer worker stopped"); -} - -// ─── Internal ──────────────────────────────────────────────────────────── - -async function processNextJobs(): Promise { - const db = getDatabase() as unknown as NodePgDatabase; - - try { - const jobs = await db - .select() - .from(muxerJobsTable) - .where(eq(muxerJobsTable.status, "pending")) - .limit(5); - - if (jobs.length === 0) return; - - const results = await Promise.allSettled( - jobs.map((job) => processJob(db, job)), - ); - - for (let i = 0; i < results.length; i++) { - const result = results[i]; - if (result.status === "rejected") { - logger.error( - { jobId: jobs[i].id, error: result.reason }, - "Muxer job failed", - ); - } - } - } catch (error) { - logger.error( - { error: error instanceof Error ? error.message : String(error) }, - "Failed to fetch pending muxer jobs", - ); - } -} - -async function processJob( - db: NodePgDatabase, - job: typeof muxerJobsTable.$inferSelect, -): Promise { - // Mark as processing - await db - .update(muxerJobsTable) - .set({ status: "processing", updatedAt: Date.now() }) - .where(eq(muxerJobsTable.id, job.id)); - - try { - const data = JSON.parse(job.data) as MuxerJobData; - - if (!data.inputs || data.inputs.length < 2) { - throw new Error( - `Muxer job ${job.id} needs at least 2 inputs, got ${data.inputs?.length ?? 0}`, - ); - } - - logger.info( - { jobId: job.id, inputs: data.inputs.length, output: data.output }, - "Processing muxer job", - ); - - // Sequential concat of all OGG segments (amix = simultaneous mix, wrong for this) - const inputLabels = data.inputs.map((_, i) => `[${i}:a:0]`); - const n = data.inputs.length; - const filterComplex = `${inputLabels.join("")}concat=n=${n}:v=0:a=1[out]`; - - const args = buildMuxFfmpegArgs({ - inputs: data.inputs, - filter: filterComplex, - output: data.output, - codec: "libmp3lame", - audioFrequency: 48000, - audioChannels: 2, - }); - - await runFfmpeg(args); - - // Mark as completed - await db - .update(muxerJobsTable) - .set({ status: "completed", updatedAt: Date.now() }) - .where(eq(muxerJobsTable.id, job.id)); - - logger.info({ jobId: job.id, output: data.output }, "Muxer job completed"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - const newAttempts = job.attempts + 1; - - if (newAttempts >= job.maxAttempts) { - await db - .update(muxerJobsTable) - .set({ - status: "failed", - error: errMsg, - attempts: newAttempts, - updatedAt: Date.now(), - }) - .where(eq(muxerJobsTable.id, job.id)); - logger.error( - { jobId: job.id, error: errMsg }, - "Muxer job failed permanently", - ); - } else { - await db - .update(muxerJobsTable) - .set({ - status: "pending", - error: errMsg, - attempts: newAttempts, - updatedAt: Date.now(), - }) - .where(eq(muxerJobsTable.id, job.id)); - logger.warn( - { jobId: job.id, error: errMsg, attempt: newAttempts }, - "Muxer job will be retried", - ); - } - } -} diff --git a/services/discord-gateway/src/modules/voice-recording/packetFilter.ts b/services/discord-gateway/src/modules/voice-recording/packetFilter.ts deleted file mode 100644 index cdd8f773..00000000 --- a/services/discord-gateway/src/modules/voice-recording/packetFilter.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Transform, type TransformCallback } from "node:stream"; -import { createChildLogger } from "@/shared/logger/index"; - -const _logger = createChildLogger("packet-filter"); - -/** - * Transform stream to filter out audio packets that are too small. - * Packets that are too small are likely to fail decryption by Discord. - */ -export class PacketFilter extends Transform { - private minPacketSize: number; - private filteredCount: number = 0; - private totalCount: number = 0; - - constructor(minPacketSize: number = 10) { - super(); - this.minPacketSize = minPacketSize; - } - - _transform( - chunk: Buffer, - _encoding: string, - callback: TransformCallback, - ): void { - this.totalCount++; - - // Filter out undersized packets - if (chunk.length >= this.minPacketSize) { - this.push(chunk); - } else { - this.filteredCount++; - } - - callback(); - } - - /** Returns the number of packets filtered out and processed. */ - getStats(): { filtered: number; total: number } { - return { filtered: this.filteredCount, total: this.totalCount }; - } - - _flush(callback: TransformCallback): void { - callback(); - } -} diff --git a/services/discord-gateway/src/modules/voice-recording/player.ts b/services/discord-gateway/src/modules/voice-recording/player.ts deleted file mode 100644 index 25472fed..00000000 --- a/services/discord-gateway/src/modules/voice-recording/player.ts +++ /dev/null @@ -1,161 +0,0 @@ -import type { Readable } from "node:stream"; -import { - type AudioPlayer, - AudioPlayerStatus, - type AudioResource, - createAudioPlayer, - createAudioResource, - StreamType, - type VoiceConnection, -} from "@discordjs/voice"; -import { createChildLogger } from "@/shared/logger/index"; -import type { DiscordPlayerOwner, DiscordPlayOptions } from "./mediaTypes.js"; - -const logger = createChildLogger("player"); - -export class DiscordPlayer { - private player: AudioPlayer; - private connection: VoiceConnection | null = null; - private owner: DiscordPlayerOwner = "none"; - private resource: AudioResource | null = null; - private musicVolume = 0.3; - private idleCallback: (() => void) | null = null; - /** Set before manual stop() calls to distinguish from natural track end. */ - private manualStop = false; - - constructor() { - this.player = createAudioPlayer(); - - this.player.on(AudioPlayerStatus.Playing, () => { - logger.info("Audio player is now playing!"); - }); - - this.player.on(AudioPlayerStatus.Idle, () => { - // manualStop guards against both sync & async idle emission from stop() - if (this.manualStop || this.owner === "none") { - this.manualStop = false; - return; - } - // Natural track end — queue auto-advance - logger.info("Audio player finished — advancing queue"); - this.owner = "none"; - this.resource = null; - this.idleCallback?.(); - }); - - this.player.on("error", (error) => { - logger.error({ error: error.message }, "Audio player error"); - this.owner = "none"; - this.resource = null; - }); - } - - /** Register a callback fired when the player goes idle after a natural track end. */ - public onIdle(cb: () => void): void { - this.idleCallback = cb; - } - - public setConnection(connection: VoiceConnection) { - this.connection = connection; - this.connection.subscribe(this.player); - } - - public getOwner(): DiscordPlayerOwner { - return this.owner; - } - - public isConnected(): boolean { - return this.connection !== null; - } - - public playStream( - stream: Readable, - owner: DiscordPlayerOwner, - options: DiscordPlayOptions = {}, - ) { - if (owner === "none") { - throw new Error("Discord audio player owner is required"); - } - this.assertOwnerAvailable(owner); - - const resource = createAudioResource(stream, { - inputType: options.inputType ?? StreamType.OggOpus, - // Default to true so setMusicVolume/setResourceVolume works - inlineVolume: options.inlineVolume ?? true, - }); - - if (this.owner === owner) { - this.manualStop = true; - this.player.stop(); - } - this.resource = resource; - this.owner = owner; - if (owner === "music") { - const nextVolume = - options.volume !== undefined - ? this.normalizeVolume(options.volume) - : this.musicVolume; - this.musicVolume = nextVolume; - this.setResourceVolume(nextVolume); - } - this.player.play(resource); - this.unpause(owner); - this.connection?.subscribe(this.player); - } - - public getStatus(): AudioPlayerStatus { - return this.player.state.status; - } - - public pause(owner?: DiscordPlayerOwner) { - if (!this.canControl(owner)) return; - this.player.pause(true); - } - - public unpause(owner?: DiscordPlayerOwner): boolean { - if (!this.canControl(owner)) return false; - return this.player.unpause(); - } - - public stop(owner?: DiscordPlayerOwner) { - if (!this.canControl(owner)) return; - this.manualStop = true; - this.player.stop(); - this.owner = "none"; - this.resource = null; - } - - public getMusicVolume(): number { - return this.musicVolume; - } - - public setMusicVolume(volume: number): void { - const nextVolume = this.normalizeVolume(volume); - this.musicVolume = nextVolume; - if (this.owner === "music") { - this.setResourceVolume(nextVolume); - } - } - - private assertOwnerAvailable(owner: DiscordPlayerOwner): void { - if (this.owner !== "none" && this.owner !== owner) { - throw new Error(`Discord audio player is owned by ${this.owner}`); - } - } - - private canControl(owner?: DiscordPlayerOwner): boolean { - return !owner || this.owner === "none" || this.owner === owner; - } - - private normalizeVolume(volume: number): number { - if (!Number.isFinite(volume)) return this.musicVolume; - return Math.max(0, Math.min(1, volume)); - } - - private setResourceVolume(volume: number): void { - if (!this.resource?.volume) return; - this.resource.volume.setVolume(volume); - } -} - -export const discordPlayer = new DiscordPlayer(); diff --git a/services/discord-gateway/src/modules/voice-recording/recorder.ts b/services/discord-gateway/src/modules/voice-recording/recorder.ts deleted file mode 100644 index 9202ae65..00000000 --- a/services/discord-gateway/src/modules/voice-recording/recorder.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { promises as fsPromises } from "node:fs"; -import { - type DiscordGatewayAdapterCreator, - entersState, - getVoiceConnection, - joinVoiceChannel, - type VoiceConnection, - VoiceConnectionStatus, -} from "@discordjs/voice"; -import type { - Client, - Guild, - VoiceChannel, - VoiceState, -} from "discord.js-selfbot-v13"; -import { createChildLogger } from "@/shared/logger/index"; -import { retryWithBackoff } from "@/shared/utils/index"; -import { config } from "../../shared/config/config.js"; -import type { EventBroadcaster } from "../event-broadcaster/eventBroadcaster.js"; -import type { VoicePcmWsClient } from "../voice-pcm-ws/index.js"; -import { - createRecordingSession, - type RecordingSession, -} from "./recorder/sessionRecording.js"; -import { createSpeakingHandler } from "./recorder/speakingHandler.js"; -import { hookScreenShareAudio } from "./screenShareAudio.js"; -import { hookVideoReceiver } from "./videoReceiver.js"; -import { trackChannel, untrackChannel } from "./videoRecorder.js"; - -const logger = createChildLogger("recorder"); - -let _eventBroadcaster: EventBroadcaster | undefined; - -/** @internal Export for uploader.ts to broadcast voice_recording_uploaded events */ -export { _eventBroadcaster }; - -export function setEventBroadcaster(broadcaster: EventBroadcaster | undefined) { - _eventBroadcaster = broadcaster; -} - -/** - * Force the bot (self member) to be unmuted + undeafened at the SERVER level. - * - * Why: a server-muted / server-deafened bot cannot receive/record other - * members' audio properly (and definitely cannot receive their video/screen - * share). The bot's own voice state may be server-muted/deafened by an admin - * or by a prior state. This issues a REST guild-members PATCH with - * `mute:false, deaf:false`. - * - * Requires the `MUTE_MEMBERS` + `DEAFEN_MEMBERS` permissions on the bot - * (user has granted them). Failures are logged, never fatal — an unmute - * failure must not break the voice join/recording. - * - * Exported so the video-receive path (videoRecorder.ts → streamWatch) can - * re-assert it right before requesting STREAM_WATCH — a server-deafened bot - * is NOT sent the streamer's audiovisual RTP, which is why no video arrives - * even when the DAVE watch reaches Ready. Server-deaf can silently revert on - * reconnect/restart, so this is re-invoked on every video watch attempt. - */ -export async function forceSelfServerUnmuteUndeafen( - client: Client, - guild: Guild, -): Promise { - try { - const selfId = client.user?.id; - if (!selfId) return; - await guild.members.edit(selfId, { mute: false, deaf: false }); - // Verify the PATCH actually landed — read back the live member voice state. - const fresh = guild.members.cache.get(selfId); - const stillDeaf = fresh?.voice?.deaf ?? false; - logger.info( - { guildId: guild.id, stillDeaf }, - stillDeaf - ? "Forced bot server unmute + undeafen (but guild reports still deaf — will retry on next video)" - : "Forced bot server unmute + undeafen", - ); - } catch (err) { - logger.warn( - { - guildId: guild.id, - err: err instanceof Error ? err.message : String(err), - }, - "Could not force server unmute/undeafen (permissions not granted?)", - ); - } -} - -/** - * Attach an IMMEDIATE self-undeafen / self-unmute guard on `voiceStateUpdate`. - * - * Previous behaviour only re-asserted unmute/undeafen on video-watch attempts - * and after voice reconnects — so when an admin server-muted or - * server-deafened the bot the bot stayed muted/deafened until the *next* video - * watch or reconnect cycle (could be minutes or never if no streamer came on). - * - * This listener fires on EVERY guild voice-state change. When it detects that - * the bot's OWN voice state transitioned to serverDeaf||serverMute === true, - * it immediately re-issues `mute:false, deaf:false` so the bot is unmuted/ - * undeafened instantly (well within Discord's rate limit window for the bot's - * own member). Idempotent and best-effort — failures are logged, never fatal. - * - * Idempotent: safe to call multiple times; the listener is attached once. - */ -let _selfVoiceGuardAttached = false; -export function registerSelfVoiceStateGuard(client: Client): void { - if (_selfVoiceGuardAttached) return; - _selfVoiceGuardAttached = true; - client.on( - "voiceStateUpdate", - (oldState: VoiceState, newState: VoiceState) => { - void handleSelfVoiceStateUpdate(client, oldState, newState); - }, - ); -} - -async function handleSelfVoiceStateUpdate( - client: Client, - oldState: VoiceState, - newState: VoiceState, -): Promise { - // Only react to the bot's own voice state transitions. - const selfId = client.user?.id; - if (!selfId || newState.id !== selfId) return; - - const justServerMuted = !oldState.serverMute && newState.serverMute === true; - const justServerDeafed = !oldState.serverDeaf && newState.serverDeaf === true; - if (!justServerMuted && !justServerDeafed) return; - - const guild = newState.guild; - if (!guild) return; - - logger.info( - { - guildId: guild.id, - serverMuted: justServerMuted, - serverDeafed: justServerDeafed, - }, - "Bot was server-muted/deafened — immediately self-undeafening + unmuting", - ); - // Fire-and-forget the corrective edit; forceSelfServerUnmuteUndeafen logs. - void forceSelfServerUnmuteUndeafen(client, guild); -} - -let _pcmWsClient: VoicePcmWsClient | undefined; - -export function setPcmWsClient(client: VoicePcmWsClient | undefined) { - _pcmWsClient = client; -} - -const recordingsDir = config.RECORDINGS_DIR; - -// Ensure recordings directory exists -(async () => { - try { - await fsPromises.mkdir(recordingsDir, { recursive: true }); - } catch (error) { - // Directory might already exist, that's fine - if ((error as NodeJS.ErrnoException).code !== "EEXIST") { - logger.error({ error }, "Failed to create recordings directory"); - } - } -})(); - -const activeSessions = new Map(); - -export function resetActiveSessions(): void { - activeSessions.clear(); -} - -function finalizeActiveRecordingSession(guildId: string): void { - const session = activeSessions.get(guildId); - if (!session) return; - activeSessions.delete(guildId); - // Per-segment upload is the real flow; session metadata is written alongside - // each segment by finalizeSegment in segment.ts - logger.debug( - { sessionId: session.sessionId, guildId }, - "Active recording session finalized", - ); -} - -/** - * Join ke voice channel dan mulai merekam semua user yang bicara. - */ -export async function startRecording( - client: Client, - channel: VoiceChannel, -): Promise { - const connection = joinVoiceChannel({ - channelId: channel.id, - guildId: channel.guild.id, - adapterCreator: channel.guild - .voiceAdapterCreator as DiscordGatewayAdapterCreator, - selfDeaf: false, - selfMute: false, - debug: true, - }); - - logger.info({ channelName: channel.name }, "Joining voice channel"); - - connection.on("debug", (msg) => { - if (config.VERBOSE) { - logger.debug({ message: msg }, "Voice debug"); - } - }); - - connection.on("error", (err) => { - logger.error({ error: err }, "Voice connection error"); - }); - - // Wait until fully connected with retry logic - try { - await retryWithBackoff( - () => - entersState( - connection, - VoiceConnectionStatus.Ready, - config.VOICE_CONNECTION_TIMEOUT_MS, - ), - { - retries: 3, - minTimeout: 1000, - maxTimeout: 5000, - }, - ); - logger.info("Connected to voice channel. Recording started"); - - // Auto undeafen + unmute the bot itself at the server level (MUTE_MEMBERS / - // DEAFEN_MEMBERS permission granted). Server-muted/deafened bot can't - // receive others' audio/video. Fire-and-forget — never blocks recording. - void forceSelfServerUnmuteUndeafen(client, channel.guild); - - // Create recording session after connection is ready - const sessionStartTime = Date.now(); - const session = createRecordingSession({ - guildId: channel.guild.id, - channelId: channel.id, - channelName: channel.name, - startTime: sessionStartTime, - }); - activeSessions.set(channel.guild.id, session); - // Register this channel for video recording (others' camera/screen share). - trackChannel(channel.guild.id, channel); - } catch (err) { - logger.error({ error: err }, "Failed to connect to voice channel"); - connection.destroy(); - return null; - } - - const receiver = connection.receiver; - - // Use the extracted speaking handler for voice activity - const speakingHandler = createSpeakingHandler({ - client, - channel, - receiver, - eventBroadcaster: _eventBroadcaster, - activeSessions, - recordingsDir, - pcmSender: _pcmWsClient - ? (pcm, userId) => _pcmWsClient?.sendPcm(userId, pcm) - : undefined, - }); - - receiver.speaking.on("start", speakingHandler); - - // ── Screen-share audio capture ────────────────────────────────────── - // Discord GoLive sends screen-share audio on a SEPARATE SSRC from the - // user's microphone. `receiver.speaking` only fires for voice (mic) SSRCs, - // so screen-share audio is silently dropped unless we hook the UDP receiver - // to discover and register those SSRCs. - hookScreenShareAudio(receiver, speakingHandler); - - // ── Video capture (camera + screen share) ─────────────────────────── - // @discordjs/voice only decrypts/forwards audio (opus). Video RTP (H264, - // VP8/VP9/AV1) arrives on the same UDP socket but is dropped by the opus - // gate. hookVideoReceiver wraps onUdpMessage, decrypts video payload types - // with the connection secret key (reusing receiver.parsePacket), then - // depacketizes H264 to AnnexB and writes a raw .h264 stream per user. - // Order matters: video hook must be installed AFTER the screen-share audio - // hook so non-video packets delegate down the existing wrapper chain. - hookVideoReceiver(receiver, client, recordingsDir); - - // Handle unexpected disconnection - connection.on(VoiceConnectionStatus.Disconnected, async () => { - if (config.VERBOSE) { - logger.warn("Disconnected from voice channel. Reconnecting..."); - } - try { - await Promise.race([ - entersState( - connection, - VoiceConnectionStatus.Signalling, - config.RECONNECT_TIMEOUT_MS, - ), - entersState( - connection, - VoiceConnectionStatus.Connecting, - config.RECONNECT_TIMEOUT_MS, - ), - ]); - // Reconnected successfully - // Server-deaf can silently revert across a reconnect — re-assert the bot - // is undeafened + unmuted so video receive keeps working. - void forceSelfServerUnmuteUndeafen(client, channel.guild); - } catch { - logger.error("Could not reconnect. Destroying connection"); - connection.destroy(); - } - }); - - connection.on(VoiceConnectionStatus.Destroyed, () => { - finalizeActiveRecordingSession(channel.guild.id); - untrackChannel(channel.guild.id); - if (config.VERBOSE) { - logger.info("Voice connection destroyed"); - } - }); - - return connection; -} - -/** - * Stop recording and disconnect from voice channel. - */ -export function stopRecording(guildId: string): void { - const connection = getVoiceConnection(guildId); - if (connection) { - connection.destroy(); - if (config.VERBOSE) { - logger.info("Recording stopped and disconnected"); - } - } else { - logger.warn("No active connection to stop"); - } - untrackChannel(guildId); - - const session = activeSessions.get(guildId); - finalizeActiveRecordingSession(guildId); - - // Broadcast voice_recording_stopped + finalize session - if (session && _eventBroadcaster) { - const snapshot = session.snapshot(Date.now()); - const stoppedAt = Date.now(); - - _eventBroadcaster - .voiceRecordingStopped({ - guild_id: guildId, - session_id: session.sessionId, - duration_ms: snapshot.durationMs, - participants: snapshot.participants.length, - segment_count: snapshot.segments.length, - status: snapshot.status, - stopped_at: stoppedAt, - }) - .catch((err) => - logger.warn({ err }, "Failed to broadcast voice recording stopped"), - ); - - // Auto-enqueue muxer job if there are multiple segments - const segments = snapshot.segments; - if (segments.length >= 2) { - const outputFile = `${config.RECORDINGS_DIR}/merged/${session.sessionId}.mp3`; - import("./muxer.js") - .then(({ enqueueMuxerJob }) => { - enqueueMuxerJob({ - inputs: segments.map((s) => s.oggPath), - output: outputFile, - guildId, - channelId: snapshot.channelId, - sessionId: session.sessionId, - }).catch(() => {}); - }) - .catch(() => {}); - } - } -} diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/decoder.ts b/services/discord-gateway/src/modules/voice-recording/recorder/decoder.ts deleted file mode 100644 index e8111200..00000000 --- a/services/discord-gateway/src/modules/voice-recording/recorder/decoder.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { createRequire } from "node:module"; -import * as prism from "prism-media"; -import { createChildLogger } from "@/shared/logger/index"; -import { config } from "../../../shared/config/config.js"; - -const require = createRequire(import.meta.url); -const logger = createChildLogger("opus-decoder"); - -interface OpusDecoderRuntime { - isBun: boolean; - canLoadNativeOpus: boolean; -} - -export function shouldEnableDefaultOpusDecoder( - runtime: OpusDecoderRuntime, -): boolean { - return !runtime.isBun || runtime.canLoadNativeOpus; -} - -function canLoadNativeOpus(): boolean { - try { - require("@discordjs/opus"); - return true; - } catch { - return false; - } -} - -const defaultDecoderEnabled = shouldEnableDefaultOpusDecoder({ - isBun: Boolean(process.versions.bun), - canLoadNativeOpus: canLoadNativeOpus(), -}); - -export interface OpusDecoderOptions { - cooldownMs: number; - rotateMs: number; - createDecoder?: () => prism.opus.Decoder; - onData: (pcm: Buffer) => void; -} - -export class OpusDecoder { - private decoder: prism.opus.Decoder | null = null; - private disabledUntil = 0; - private createdAt = 0; - private readonly cooldownMs: number; - private readonly rotateMs: number; - private readonly createDecoderFn: () => prism.opus.Decoder; - private readonly onData: (pcm: Buffer) => void; - - constructor(options: OpusDecoderOptions) { - this.cooldownMs = options.cooldownMs; - this.rotateMs = options.rotateMs; - this.onData = options.onData; - this.createDecoderFn = - options.createDecoder ?? - (() => { - if (!defaultDecoderEnabled) { - throw new Error( - "Native @discordjs/opus is unavailable under Bun; web PCM decode disabled to avoid opusscript aborts", - ); - } - - return new prism.opus.Decoder({ - frameSize: config.OPUS_FRAME_SIZE, - channels: config.AUDIO_CHANNELS as 1 | 2, - rate: config.AUDIO_SAMPLE_RATE as - | 8000 - | 12000 - | 16000 - | 24000 - | 48000, - }); - }); - } - - rotateIfNeeded(): void { - if (!this.decoder || this.rotateMs <= 0) return; - if (Date.now() - this.createdAt < this.rotateMs) return; - this.destroy(); - this.ensureDecoder(); - } - - write(chunk: Buffer): void { - const decoder = this.ensureDecoder(); - if (!decoder) return; - try { - decoder.write(chunk); - } catch (error) { - logger.warn({ error }, "Opus decoder write failed, cooling down"); - this.coolDown(); - } - } - - destroy(): void { - if (!this.decoder) return; - this.decoder.removeAllListeners(); - this.decoder.destroy(); - this.decoder = null; - this.createdAt = 0; - } - - private ensureDecoder(): prism.opus.Decoder | null { - if (this.decoder) return this.decoder; - if (Date.now() < this.disabledUntil) return null; - try { - const decoder = this.createDecoderFn(); - decoder.on("data", this.onData); - decoder.on("error", (error) => { - logger.warn({ error }, "Opus decoder error, cooling down"); - this.coolDown(); - }); - this.decoder = decoder; - this.createdAt = Date.now(); - return decoder; - } catch (error) { - logger.warn({ error }, "Opus decoder init failed, cooling down"); - this.disabledUntil = Date.now() + this.cooldownMs; - return null; - } - } - - private coolDown(): void { - this.disabledUntil = Date.now() + this.cooldownMs; - this.destroy(); - } -} diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/metadata.ts b/services/discord-gateway/src/modules/voice-recording/recorder/metadata.ts deleted file mode 100644 index 408a5e91..00000000 --- a/services/discord-gateway/src/modules/voice-recording/recorder/metadata.ts +++ /dev/null @@ -1,107 +0,0 @@ -import path from "node:path"; -import type { Client, VoiceChannel } from "discord.js-selfbot-v13"; -import { createChildLogger } from "@/shared/logger/index"; -import { config } from "../../../shared/config/config.js"; -import type { - SegmentMetadata, - SegmentState, - UserMetadata, -} from "../../message-capture/types.js"; - -const logger = createChildLogger("voice-metadata"); - -/** LRU-ish cache: userId -> UserMetadata. Avoids Discord API calls in hotpath. */ -const metadataCache = new Map(); -const METADATA_CACHE_MAX = 200; - -function cacheMetadata(userId: string, metadata: UserMetadata): void { - if (metadataCache.size >= METADATA_CACHE_MAX) { - // Evict oldest entry via Map iteration (Map preserves insertion order) - const firstKey = metadataCache.keys().next().value; - if (firstKey) metadataCache.delete(firstKey); - } - metadataCache.set(userId, metadata); -} - -export async function collectUserMetadata( - client: Client, - userId: string, - channel: VoiceChannel, -): Promise { - const cached = metadataCache.get(userId); - if (cached) return cached; - - const user = - client.users.cache.get(userId) || - (await client.users.fetch(userId).catch(() => { - logger.warn({ userId }, "Failed to fetch user"); - return null; - })); - const member = - channel.guild.members.cache.get(userId) || - (await channel.guild.members.fetch(userId).catch(() => { - logger.warn({ userId }, "Failed to fetch guild member"); - return null; - })); - const username = user?.username ?? "Unknown User"; - const roles = - member?.roles.cache - .filter((role) => role.id !== channel.guild.id) - .sort((a, b) => b.position - a.position) - .map((role) => ({ - id: role.id, - name: role.name, - position: role.position, - })) ?? []; - - const result: UserMetadata = { - userId, - username, - tag: user?.tag ?? "Unknown#0000", - displayName: member?.displayName ?? username, - avatarUrl: - user?.displayAvatarURL({ - format: "png", - size: config.AVATAR_SIZE as - | 16 - | 32 - | 64 - | 128 - | 256 - | 512 - | 1024 - | 2048 - | 4096, - }) ?? "https://cdn.discordapp.com/embed/avatars/0.png", - bot: user?.bot ?? false, - roles, - highestRole: roles[0] ?? null, - joinedTimestamp: member?.joinedTimestamp ?? null, - }; - - cacheMetadata(userId, result); - return result; -} - -export function createSegmentMetadata( - user: UserMetadata, - segment: SegmentState, - sessionId: string, - recordingSessionId: string, - sessionStartTime: number, - recordingSegmentMs: number, -): SegmentMetadata { - const endTime = segment.endTime ?? Date.now(); - return { - ...user, - sessionId, - recordingSessionId, - sessionStartTime, - segmentIndex: segment.index, - segmentMs: recordingSegmentMs, - startTime: segment.startTime, - endTime, - durationMs: endTime - segment.startTime, - filename: path.basename(segment.filename), - }; -} diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/oggCrc.ts b/services/discord-gateway/src/modules/voice-recording/recorder/oggCrc.ts deleted file mode 100644 index 5c6957d5..00000000 --- a/services/discord-gateway/src/modules/voice-recording/recorder/oggCrc.ts +++ /dev/null @@ -1,56 +0,0 @@ -import fs from "node:fs"; - -/** - * Ogg page CRC-32 (RFC 3533): polynomial 0x04c11db7, init 0, MSB-first - * (no reflection), no final XOR. Table-driven, pure JS — no native deps. - * - * prism-media's OggLogicalBitstream writes pages with `crc: false` (node-crc - * was dropped from the dependency tree), which produces files whose page - * checksums are all zero. Strict players (ffmpeg, iOS) reject those with - * "CRC mismatch! / End of file". This re-computes the checksum of every page - * in place, yielding a spec-compliant Ogg file that any player can open. - */ -const CRC_TABLE = new Int32Array(256); -for (let i = 0; i < 256; i++) { - let c = i << 24; - for (let k = 0; k < 8; k++) { - c = c & 0x80000000 ? (c << 1) ^ 0x04c11db7 : c << 1; - } - CRC_TABLE[i] = c | 0; -} - -function oggCrc32(buffer: Buffer): number { - let c = 0; - for (let i = 0; i < buffer.length; i++) { - c = ((c << 8) ^ CRC_TABLE[((c >>> 24) ^ buffer[i]) & 0xff]) | 0; - } - return c >>> 0; -} - -/** - * Rewrites the CRC field (bytes 22-25) of every OggS page in `filePath`. - * Returns the number of pages fixed (0 if the file is not a valid Ogg stream). - */ -export function fixOggCrc(filePath: string): number { - const data = fs.readFileSync(filePath); - let pos = 0; - let fixed = 0; - - while (pos + 27 <= data.length) { - if (data.toString("latin1", pos, pos + 4) !== "OggS") break; - const nsegs = data[pos + 26]; - let bodyLen = 0; - for (let i = 0; i < nsegs; i++) bodyLen += data[pos + 27 + i]; - const pageEnd = pos + 27 + nsegs + bodyLen; - if (pageEnd > data.length) break; - - // Zero the checksum field, then CRC the whole page (RFC 3533). - data.writeUInt32LE(0, pos + 22); - data.writeUInt32LE(oggCrc32(data.subarray(pos, pageEnd)), pos + 22); - fixed++; - pos = pageEnd; - } - - if (fixed > 0) fs.writeFileSync(filePath, data); - return fixed; -} diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/segment.ts b/services/discord-gateway/src/modules/voice-recording/recorder/segment.ts deleted file mode 100644 index 77e72cf7..00000000 --- a/services/discord-gateway/src/modules/voice-recording/recorder/segment.ts +++ /dev/null @@ -1,131 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import * as prism from "prism-media"; -import { createChildLogger } from "@/shared/logger/index"; -import type { SegmentState } from "../../message-capture/types.js"; - -const logger = createChildLogger("voice-segment"); - -// --------------------------------------------------------------------------- -// Path helpers -// --------------------------------------------------------------------------- - -export function buildSegmentPaths( - userDir: string, - startTime: number, -): { filename: string; jsonFilename: string } { - return { - filename: path.join(userDir, `${startTime}.ogg`), - jsonFilename: path.join(userDir, `${startTime}.json`), - }; -} - -export function shouldRotateSegment( - startTime: number, - now: number, - recordingSegmentMs: number, -): boolean { - return recordingSegmentMs > 0 && now - startTime >= recordingSegmentMs; -} - -// --------------------------------------------------------------------------- -// SegmentManager -// --------------------------------------------------------------------------- - -export class SegmentManager { - private currentSegment: SegmentState | null = null; - private segmentIndex = 0; - - constructor( - private readonly userDir: string, - private readonly recordingSegmentMs: number, - ) {} - - open(oggPacketStream: NodeJS.ReadableStream): SegmentState { - const index = this.segmentIndex++; - const startTime = Date.now(); - const { filename, jsonFilename } = buildSegmentPaths( - this.userDir, - startTime, - ); - const oggStream = new prism.opus.OggLogicalBitstream({ - opusHead: new prism.opus.OpusHead({ channelCount: 2, sampleRate: 48000 }), - pageSizeControl: { maxPackets: 10 }, - crc: false, - }); - const out = fs.createWriteStream(filename); - oggPacketStream.pipe(oggStream).pipe(out); - - this.currentSegment = { - index, - startTime, - endTime: null, - filename, - jsonFilename, - oggStream, - out, - }; - - logger.debug( - { index, startTime, filename, userDir: this.userDir }, - "Segment opened", - ); - return this.currentSegment; - } - - close(oggPacketStream: NodeJS.ReadableStream): SegmentState | null { - if (!this.currentSegment) return null; - const segment = this.currentSegment; - segment.endTime = Date.now(); - oggPacketStream.unpipe(segment.oggStream); - segment.oggStream.end(); - this.currentSegment = null; - - // Get file size after closing - let fileSize = 0; - try { - const stat = fs.statSync(segment.filename); - fileSize = stat.size; - } catch { - // File might not exist yet - } - - logger.debug( - { - index: segment.index, - filename: segment.filename, - fileSize, - durationMs: (segment.endTime ?? 0) - segment.startTime, - }, - "Segment closed", - ); - return segment; - } - - rotateIfNeeded(oggPacketStream: NodeJS.ReadableStream): SegmentState | null { - if (!this.currentSegment) return null; - if ( - !shouldRotateSegment( - this.currentSegment.startTime, - Date.now(), - this.recordingSegmentMs, - ) - ) - return null; - - logger.debug( - { - index: this.currentSegment.index, - filename: this.currentSegment.filename, - durationMs: Date.now() - this.currentSegment.startTime, - }, - "Segment rotating", - ); - this.close(oggPacketStream); - return this.open(oggPacketStream); - } - - getCurrent(): SegmentState | null { - return this.currentSegment; - } -} diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/segmentFinalizer.ts b/services/discord-gateway/src/modules/voice-recording/recorder/segmentFinalizer.ts deleted file mode 100644 index ef4aef5b..00000000 --- a/services/discord-gateway/src/modules/voice-recording/recorder/segmentFinalizer.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { promises as fsPromises } from "node:fs"; -import { createChildLogger } from "@/shared/logger/index"; -import { config } from "../../../shared/config/config.js"; -import type { - SegmentState, - UserMetadata, -} from "../../message-capture/types.js"; -import { createSegmentMetadata } from "./metadata.js"; -import { fixOggCrc } from "./oggCrc.js"; -import type { RecordingSession } from "./sessionRecording.js"; -import { uploadRecordingSegment } from "./uploader.js"; - -const logger = createChildLogger("voice-segment-finalizer"); - -// --------------------------------------------------------------------------- -// SegmentFinalizerInput -// --------------------------------------------------------------------------- - -export interface SegmentFinalizerInput { - currentSegment: SegmentState; - userMetadata: UserMetadata; - activeSession: RecordingSession | undefined; - guildId: string; - channelId: string; - channelName: string; -} - -// --------------------------------------------------------------------------- -// finalizeSegment -// --------------------------------------------------------------------------- - -/** - * Handles the completion of an OGG segment: - * - Logs the saved segment (if VERBOSE) - * - Registers the segment with the active recording session - * - Writes the metadata JSON file alongside the OGG file - * - Triggers async upload of the segment to external storage - * - * This function is fire-and-forget for the metadata write and upload; - * errors are caught and logged without throwing. - */ -export function finalizeSegment(input: SegmentFinalizerInput): void { - const { - currentSegment, - userMetadata, - activeSession, - guildId, - channelId, - channelName, - } = input; - - const endTime = currentSegment.endTime ?? Date.now(); - const durationMs = endTime - currentSegment.startTime; - - // Discard only very short segments — a blip of ambient noise or a mic - // click. At 300ms, brief acknowledgements ("ya", "siap", "ok") are still - // kept while true artifacts are dropped. - const MIN_DURATION_MS = 300; - if (durationMs < MIN_DURATION_MS) { - logger.debug( - { filename: currentSegment.filename, durationMs }, - "Segment too short, discarding", - ); - // Clean up the OGG file - fsPromises.unlink(currentSegment.filename).catch(() => {}); - fsPromises.unlink(currentSegment.jsonFilename).catch(() => {}); - return; - } - - if (config.VERBOSE) { - logger.info({ filename: currentSegment.filename }, "Segment saved"); - } - - // Fix Ogg page CRCs before anything reads the file (upload/merge/transcode). - // prism-media writes pages with crc:false → checksums are zero → strict - // players (ffmpeg, iOS) reject the file. Re-compute in place, pure JS. - try { - const fixed = fixOggCrc(currentSegment.filename); - if (config.VERBOSE) { - logger.info( - { filename: currentSegment.filename, pages: fixed }, - "Ogg page CRCs fixed", - ); - } - } catch (err: unknown) { - logger.error( - { - filename: currentSegment.filename, - error: err instanceof Error ? err.message : String(err), - }, - "Failed to fix Ogg CRCs — segment may be unplayable", - ); - } - - // Register segment with the active recording session - if (activeSession) { - activeSession.registerSegment({ - user: userMetadata, - oggPath: currentSegment.filename, - jsonPath: currentSegment.jsonFilename, - startTime: currentSegment.startTime, - endTime, - }); - } - - // Write metadata JSON (async, fire-and-forget) - const metadata = createSegmentMetadata( - userMetadata, - currentSegment, - activeSession?.sessionId ?? `${userMetadata.userId}-0`, - activeSession?.sessionId ?? `${guildId}-${channelId}-0`, - activeSession?.startTime ?? 0, - config.RECORDING_SEGMENT_MS, - ); - - fsPromises - .writeFile(currentSegment.jsonFilename, JSON.stringify(metadata, null, 2)) - .then(() => { - if (config.VERBOSE) { - logger.info( - { jsonFile: currentSegment.jsonFilename }, - "Metadata saved", - ); - } - }) - .catch((err: unknown) => { - logger.error( - { error: err instanceof Error ? err.message : String(err) }, - "Failed to write segment metadata", - ); - }); - - // Trigger async voice segment upload (fire-and-forget) - const segmentId = `${userMetadata.userId}-${currentSegment.startTime}`; - uploadRecordingSegment({ - id: segmentId, - oggPath: currentSegment.filename, - userId: userMetadata.userId, - username: userMetadata.username, - avatarUrl: userMetadata.avatarUrl, - guildId, - channelId, - channelName, - }).catch((err: unknown) => { - const msg = err instanceof Error ? err.message : String(err); - logger.error({ segmentId, error: msg }, "Upload segment trigger failed"); - }); -} diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/sessionRecording.ts b/services/discord-gateway/src/modules/voice-recording/recorder/sessionRecording.ts deleted file mode 100644 index 2ba8e653..00000000 --- a/services/discord-gateway/src/modules/voice-recording/recorder/sessionRecording.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { createChildLogger } from "@/shared/logger/index"; -import type { UserMetadata } from "../../message-capture/types.js"; - -const logger = createChildLogger("recording-session"); - -export interface RecordingSessionOptions { - guildId: string; - channelId: string; - channelName: string; - startTime: number; -} - -export interface SessionParticipant { - userId: string; - username: string; - tag: string; - displayName: string; - avatarUrl: string; -} - -export interface SessionSegmentRef { - userId: string; - oggPath: string; - jsonPath: string; - startTime: number; - endTime: number; - durationMs: number; - offsetMs: number; -} - -export interface SessionRecordingMetadata { - sessionId: string; - guildId: string; - channelId: string; - channelName: string; - startTime: number; - endTime: number; - durationMs: number; - status: "pending" | "completed" | "failed" | "empty"; - outputFile: string | null; - participants: SessionParticipant[]; - segments: SessionSegmentRef[]; - error?: string; -} - -export interface RecordingSession { - readonly sessionId: string; - readonly startTime: number; - registerSegment(input: { - user: UserMetadata; - oggPath: string; - jsonPath: string; - startTime: number; - endTime: number; - }): void; - snapshot(endTime: number): SessionRecordingMetadata; -} - -export function createRecordingSession( - options: RecordingSessionOptions, -): RecordingSession { - const sessionId = `${options.guildId}-${options.channelId}-${options.startTime}`; - const participants = new Map(); - const segments: SessionSegmentRef[] = []; - - logger.info( - { - sessionId, - guildId: options.guildId, - channelId: options.channelId, - channelName: options.channelName, - }, - "Recording session created", - ); - - return { - sessionId, - startTime: options.startTime, - - registerSegment(input) { - participants.set(input.user.userId, { - userId: input.user.userId, - username: input.user.username, - tag: input.user.tag, - displayName: input.user.displayName, - avatarUrl: input.user.avatarUrl, - }); - segments.push({ - userId: input.user.userId, - oggPath: input.oggPath, - jsonPath: input.jsonPath, - startTime: input.startTime, - endTime: input.endTime, - durationMs: input.endTime - input.startTime, - offsetMs: input.startTime - options.startTime, - }); - logger.debug( - { sessionId, userId: input.user.userId, segmentCount: segments.length }, - "Segment registered in session", - ); - }, - - snapshot(endTime: number): SessionRecordingMetadata { - return { - sessionId, - guildId: options.guildId, - channelId: options.channelId, - channelName: options.channelName, - startTime: options.startTime, - endTime, - durationMs: endTime - options.startTime, - status: "pending", - outputFile: null, - participants: Array.from(participants.values()), - segments: [...segments], - }; - }, - }; -} diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/speakingHandler.ts b/services/discord-gateway/src/modules/voice-recording/recorder/speakingHandler.ts deleted file mode 100644 index b1e484b8..00000000 --- a/services/discord-gateway/src/modules/voice-recording/recorder/speakingHandler.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { promises as fsPromises } from "node:fs"; -import path from "node:path"; -import type { VoiceConnection } from "@discordjs/voice"; -import type { Client, VoiceChannel } from "discord.js-selfbot-v13"; -import { createChildLogger } from "@/shared/logger/index"; -import type { EventBroadcaster } from "../../event-broadcaster/eventBroadcaster.js"; -import type { - SegmentState, - UserMetadata, -} from "../../message-capture/types.js"; -import { collectUserMetadata } from "./metadata.js"; -import { finalizeSegment } from "./segmentFinalizer.js"; -import type { RecordingSession } from "./sessionRecording.js"; -import { setupUserStream } from "./streamSetup.js"; - -const logger = createChildLogger("speaking-handler"); - -export interface SpeakingHandlerContext { - client: Client; - channel: VoiceChannel; - receiver: VoiceConnection["receiver"]; - eventBroadcaster: EventBroadcaster | undefined; - activeSessions: Map; - recordingsDir: string; - /** Direct WS sender for real-time PCM — takes priority over Redis if set. */ - pcmSender?: (pcm: Buffer, userId: string) => void; -} - -/** - * Creates the event handler for `receiver.speaking.on("start", handler)`. - * - * # Why this is structured the way it is (recording-completeness) - * - * `receiver.speaking"start"` fires from `onUdpMessage` the moment the FIRST - * Opus packet for a user arrives, and the opus bytes are forwarded to the - * subscription stream **only if a subscription already exists** — otherwise - * they are dropped (`subscriptions.get(userId)` is undefined → `return`). - * - * That means the subscription MUST be created synchronously (no `await` before - * it): every frame between "start" and the subscribe call is otherwise silently - * discarded → the START of a burst is MISSING (the user's core complaint). - * The previous code `await`ed `collectUserMetadata` (a Discord REST roundtrip, - * slow on cache miss) BEFORE subscribing — that was the main source of misses. - * - * Metadata is now fetched in the background. If the speaker turns out to be a - * bot, the just-started burst is discarded (unlinked, never uploaded). - */ -export function createSpeakingHandler( - ctx: SpeakingHandlerContext, -): (userId: string) => Promise { - const { - client, - channel, - receiver, - eventBroadcaster, - activeSessions, - recordingsDir, - pcmSender, - } = ctx; - - return async (userId: string) => { - // Skip the bot's own audio. - if (userId === client.user?.id) return; - - // Synchronous guard BEFORE any await. Two "start" events can race during a - // subscription; guard here so we never create a second subscription. - if (receiver.subscriptions.has(userId)) return; - - logger.debug({ userId }, "Voice activity detected"); - - // Optimistic speaking:true (real metadata arrives async). - eventBroadcaster?.voiceActiveUser(userId, { - username: userId, - avatar: "", - speaking: true, - }); - - // Subscribe IMMEDIATELY (synchronously — guard is already done above, no - // `await` since then), so the subscription exists before any Opus frame - // arrives. Frames are then buffered by the AudioReceiveStream while we - // finish setup below (mkdir is non-network and fast). - const userDir = path.join(recordingsDir, userId); - const { audioStream, packetFilter, segmentManager, decoder } = - setupUserStream({ - userId, - receiver, - userDir, - onPcmData: (pcm) => { - if (pcmSender) { - pcmSender(pcm, userId); - } else { - eventBroadcaster?.voicePcmData(pcm, userId); - } - }, - }); - - // Ensure per-user recording directory (subscription is already live, so - // this await does NOT drop audio — frames buffer in the subscription). - await fsPromises.mkdir(userDir, { recursive: true }).catch(() => { - // Directory already exists, ignore. - }); - - const activeSession = activeSessions.get(channel.guild.id); - - // Mutable per-burst state. - let userMetadata: UserMetadata | null = null; - let accepted = true; // false once we learn it's a bot → discard burst - let finalized = false; - - /** Unlink the given segment's files (bot/error discard path). */ - const discardSegmentFiles = (seg: SegmentState | null): void => { - if (!seg) return; - fsPromises.unlink(seg.filename).catch(() => {}); - fsPromises.unlink(seg.jsonFilename).catch(() => {}); - }; - - const emitSpeakingFalse = (): void => { - eventBroadcaster?.voiceActiveUser(userId, { - username: userMetadata?.username ?? userId, - avatar: userMetadata?.avatarUrl ?? "", - speaking: false, - }); - }; - - /** - * Close the current (per-burst) segment and — once the underlying file has - * finished flushing to disk — finalize it (or discard it if bot/error). - * Safe to call multiple times (guarded by `finalized`). - */ - const finishBurst = (): void => { - if (finalized) return; - finalized = true; - const seg = segmentManager.close(oggPacketStream); - decoder.destroy(); - - const doFinalize = (): void => { - Promise.resolve(userMetadata).then((meta) => { - if (meta && accepted && seg) { - try { - finalizeSegment({ - currentSegment: seg, - userMetadata: meta, - activeSession, - guildId: channel.guild.id, - channelId: channel.id, - channelName: channel.name, - }); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - logger.error({ userId, error: msg }, "Segment finalize failed"); - } - } else { - // Bot audio, metadata failure, or no segment opened — discard. - discardSegmentFiles(seg); - } - }); - }; - - // finalizeSegment reads the OGG file, so wait for the write stream to - // flush before touching it. - if (seg?.out.writableFinished) { - doFinalize(); - } else { - seg?.out.once("finish", doFinalize); - } - }; - - // Attach audioStream handlers BEFORE pipe() (prevents data loss). - audioStream.on("data", (chunk: Buffer) => { - if (chunk.length < 8) return; - // One segment per burst — no time-based rotation here. Only rotate the - // web-PCM broadcast decoder to bound its memory. - decoder.rotateIfNeeded(); - decoder.write(chunk); - }); - - audioStream.on("end", () => { - finishBurst(); - emitSpeakingFalse(); - }); - - audioStream.on("error", (error: Error) => { - logger.error({ userId, error: error.message }, "Audio stream error"); - finishBurst(); - emitSpeakingFalse(); - }); - - // Pipe for OGG recording (handlers already attached). - const oggPacketStream = audioStream.pipe(packetFilter); - - // Open the (single, per-burst) segment. - const currentSegment = segmentManager.open(oggPacketStream); - - // Handle file-write errors on the underlying write stream. - currentSegment.out.on("error", (err: unknown) => { - const msg = err instanceof Error ? err.message : String(err); - logger.error({ userId, error: msg }, "File write error"); - }); - - // Handle packet-filter errors (rare) by closing the burst. - packetFilter.on("error", (err: Error) => { - logger.error({ userId, error: err.message }, "PacketFilter error"); - finishBurst(); - emitSpeakingFalse(); - }); - - // Fetch user metadata in the background; reject bots by discarding burst. - collectUserMetadata(client, userId, channel) - .then((meta) => { - if (meta.bot) { - accepted = false; - finishBurst(); - emitSpeakingFalse(); - return; - } - userMetadata = meta; - eventBroadcaster?.voiceActiveUser(userId, { - username: meta.username, - avatar: meta.avatarUrl, - speaking: true, - }); - }) - .catch((err: unknown) => { - const msg = err instanceof Error ? err.message : String(err); - logger.warn( - { userId, error: msg }, - "Metadata fetch failed, dropping burst", - ); - accepted = false; - finishBurst(); - emitSpeakingFalse(); - }); - }; -} diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/streamSetup.ts b/services/discord-gateway/src/modules/voice-recording/recorder/streamSetup.ts deleted file mode 100644 index b8056fa3..00000000 --- a/services/discord-gateway/src/modules/voice-recording/recorder/streamSetup.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { VoiceConnection } from "@discordjs/voice"; -import { EndBehaviorType } from "@discordjs/voice"; -import { createChildLogger } from "@/shared/logger/index"; -import { config } from "../../../shared/config/config.js"; -import { PacketFilter } from "../packetFilter.js"; -import { OpusDecoder } from "./decoder.js"; -import { SegmentManager } from "./segment.js"; - -const logger = createChildLogger("stream-setup"); - -export interface StreamSetupInput { - userId: string; - receiver: VoiceConnection["receiver"]; - userDir: string; - onPcmData: (pcm: Buffer) => void; -} - -export interface StreamSetupResult { - audioStream: NodeJS.ReadableStream; - packetFilter: PacketFilter; - segmentManager: SegmentManager; - decoder: OpusDecoder; -} - -/** - * Creates the audio stream subscription, decoder, packet filter, and segment - * manager for a user who started speaking. - * - * NOTE: This function does NOT pipe the audio stream through the packet filter. - * The caller must attach event handlers to `audioStream` BEFORE calling - * `audioStream.pipe(packetFilter)` to prevent data loss from race conditions. - */ -export function setupUserStream(input: StreamSetupInput): StreamSetupResult { - const { userId, receiver, userDir, onPcmData } = input; - - logger.debug({ userId }, "Setting up user audio stream"); - - // Subscribe to the audio stream from the Discord voice receiver - const audioStream = receiver.subscribe(userId, { - end: { - behavior: EndBehaviorType.AfterSilence, - duration: config.AUDIO_STREAM_SILENCE_DURATION_MS, - }, - }); - - const packetFilter = new PacketFilter(config.PACKET_FILTER_MIN_SIZE); - const segmentManager = new SegmentManager( - userDir, - config.RECORDING_SEGMENT_MS, - ); - - // Create decoder for web broadcast (PCM downsampling) - const decoder = new OpusDecoder({ - cooldownMs: config.DECODER_COOLDOWN_MS, - rotateMs: config.DECODER_ROTATE_MS, - onData: (pcm: Buffer) => { - // Downsample 48kHz stereo -> 24kHz mono (left channel, every 2nd frame) - // Use typed array views for efficient access instead of read/writeInt16LE - const inputView = new Int16Array( - pcm.buffer, - pcm.byteOffset, - pcm.byteLength / 2, - ); - const outBuf = Buffer.alloc(inputView.length / 2); // 48k stereo -> 24k mono = 1/4 size - const outputView = new Int16Array( - outBuf.buffer, - outBuf.byteOffset, - outBuf.byteLength / 2, - ); - for (let i = 0; i < outputView.length; i++) { - outputView[i] = inputView[i * 4]; // left channel, every 2nd stereo frame - } - onPcmData(outBuf); - }, - }); - - logger.debug({ userId }, "User audio stream setup complete"); - - return { - audioStream, - packetFilter, - segmentManager, - decoder, - }; -} diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/uploader.ts b/services/discord-gateway/src/modules/voice-recording/recorder/uploader.ts deleted file mode 100644 index 2269c8c5..00000000 --- a/services/discord-gateway/src/modules/voice-recording/recorder/uploader.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { execFile } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; -import { promisify } from "node:util"; -import { createChildLogger } from "@/shared/logger/index"; -import { config } from "../../../shared/config/config.js"; -import { - insertVoiceRecording, - updateVoiceRecordingAsFailed, - updateVoiceRecordingAsUploaded, - updateVoiceRecordingTranscription, -} from "../../../shared/database/voiceRecordingRepo.js"; -import { uploadToTele } from "../../../shared/uploader.js"; -import { transcribeRecording } from "../voiceTranscriber.js"; - -const logger = createChildLogger("recording-uploader"); -const execFileAsync = promisify(execFile); - -/** - * Transcode an Opus/OGG segment to MP3 so recordings are playable on any - * device (Safari/iPhone cannot play OGG). Uses PATH-resolved ffmpeg from the - * Nix closure. Returns the path to the temporary MP3 file. - */ -async function transcodeToMp3(oggPath: string): Promise { - const mp3Path = oggPath.replace(/\.ogg$/i, ".mp3"); - await execFileAsync( - "ffmpeg", - [ - "-y", - "-i", - oggPath, - "-codec:a", - "libmp3lame", - "-b:a", - "128k", - "-ar", - "48000", - "-ac", - "2", - mp3Path, - ], - { timeout: 30_000 }, - ); - return mp3Path; -} - -/** - * Uploads a recorded segment OGG file to external server and registers in database - */ -export async function uploadRecordingSegment(input: { - id: string; - oggPath: string; - userId: string; - username: string; - avatarUrl: string | null; - guildId: string | null; - channelId: string | null; - channelName: string | null; -}): Promise { - const { - id, - oggPath, - userId, - username, - avatarUrl, - guildId, - channelId, - channelName, - } = input; - const fileName = path.basename(oggPath); - - try { - // 1. Get file size and insert initial pending state to DB - const stats = await fs.promises.stat(oggPath); - await insertVoiceRecording({ - id, - user_id: userId, - username, - avatar_url: avatarUrl, - guild_id: guildId, - channel_id: channelId, - channel_name: channelName, - filename: fileName, - size_bytes: stats.size, - upload_status: "pending", - created_at: Date.now(), - }); - - // 1b. Transcode segment to MP3 (universal playback; Safari can't play OGG) - const mp3Path = await transcodeToMp3(oggPath); - const mp3Name = path.basename(mp3Path); - const mp3Stats = await fs.promises.stat(mp3Path); - - // 2. Perform async upload with retry logic - const fileBuffer = await fs.promises.readFile(mp3Path); - const uploadResult = await uploadToTele({ - buffer: fileBuffer, - filename: mp3Name, - contentType: "audio/mpeg", - uploadUrl: config.TELE_UPLOAD_URL, - retries: 3, - }); - const downloadUrl = uploadResult.url; - - // 2b. Clean up the temporary MP3 (source OGG is kept for transcription) - await fs.promises.unlink(mp3Path).catch(() => {}); - - // 3. Update DB to uploaded state (filename/size reflect the MP3 artifact) - await updateVoiceRecordingAsUploaded( - id, - downloadUrl, - Date.now(), - mp3Name, - mp3Stats.size, - ); - logger.info({ id, downloadUrl }, "Recording segment uploaded successfully"); - - // 4. Broadcast via Redis EventBroadcaster (forwarded to WebSocket clients by backend) - const { _eventBroadcaster } = await import("../recorder.js"); - if (_eventBroadcaster) { - _eventBroadcaster - .voiceRecordingUploaded({ - id, - user_id: userId, - username, - avatar_url: avatarUrl, - guild_id: guildId, - channel_id: channelId, - channel_name: channelName, - filename: mp3Name, - size_bytes: mp3Stats.size, - download_url: downloadUrl, - upload_status: "uploaded", - created_at: Date.now(), - uploaded_at: Date.now(), - }) - .catch((err: unknown) => { - logger.warn( - { err }, - "Failed to broadcast voice recording upload event", - ); - }); - } - - // 5. Fire-and-forget voice transcription - if (config.AI_VOICE_TRANSCRIPTION_ENABLED) { - transcribeRecording(oggPath).then((transcription) => { - if (transcription) { - updateVoiceRecordingTranscription(id, transcription).catch( - (err: unknown) => { - logger.warn({ id, err }, "Failed to persist transcription"); - }, - ); - } - }); - } - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error({ id, error: errorMsg }, "Failed to upload voice recording"); - await updateVoiceRecordingAsFailed(id, errorMsg).catch((err: unknown) => { - logger.error({ id, err }, "Failed to write failure state to DB"); - }); - } -} diff --git a/services/discord-gateway/src/modules/voice-recording/screenShareAudio.ts b/services/discord-gateway/src/modules/voice-recording/screenShareAudio.ts deleted file mode 100644 index 6c22ed30..00000000 --- a/services/discord-gateway/src/modules/voice-recording/screenShareAudio.ts +++ /dev/null @@ -1,222 +0,0 @@ -import type { VoiceReceiver, VoiceUserData } from "@discordjs/voice"; -import { createChildLogger } from "@/shared/logger/index"; - -const logger = createChildLogger("screen-share-audio"); - -/** - * Hooks screen-share audio capture into the voice receiver. - * - * ## Background - * - * Discord GoLive (screen share) sends audio on **separate SSRCs** from the - * user's microphone. In `@discordjs/voice` v0.19, `VoiceReceiver.onUdpMessage` - * does: - * - * ```js - * const userData = this.ssrcMap.get(ssrc); - * if (!userData) return; // ← DROPS screen-share audio SSRC - * ``` - * - * `ssrcMap` is only populated from `VOICE_STATE_UPDATE` / `VOICE_SERVER_UPDATE` - * WebSocket packets, which carry the **voice audioSSRC** only. When a user - * starts an audio+video screen-share, Discord sends additional RTP packets on - * new SSRCs that are *never* registered in `ssrcMap` → they are silently - * discarded → `receiver.speaking` never fires → screen-share audio is missing. - * - * ## Fix - * - * 1. Wrap `onUdpMessage` to inspect every incoming RTP packet's SSRC. - * 2. If the SSRC isn't in `ssrcMap`, check whether it looks like a screen-share - * audio stream (OPRUS payload type 120, RTP version 2). - * 3. Clone the owning user's VoiceUserData into `ssrcMap` under the new SSRC - * so the *original* (un-patched) `onUdpMessage` picks it up, decrypts it, - * and forwards the Opus packet to the existing subscription stream. - * 4. Emit a synthetic `"start"` speaking event so the existing - * `speakingHandler` sets up the full pipeline (decoder, packet filter, - * segment manager, event handlers) for that userId if not already. - */ -export function hookScreenShareAudio( - receiver: VoiceReceiver, - speakingHandler: (userId: string) => Promise, -): void { - // Cache the original (un-bound) method so we can delegate to it. - const original = receiver.onUdpMessage; - - receiver.onUdpMessage = (msg: Buffer) => { - // ── 1. Detect screen-share SSRCs BEFORE the original discards them ── - if (isLikelyScreenShareAudio(msg, receiver)) { - const ssrc = msg.readUInt32BE(8); - const userData = getSsrcMapEntry(receiver, ssrc); - - if (!userData) { - // SSRC not registered — try to infer owner and register it - const owner = inferScreenShareOwner(ssrc, receiver); - if (owner) { - registerScreenShareSsrc(receiver, ssrc, owner); - logger.info( - { userId: owner.userId, ssrc, kind: "screenshare-audio" }, - "Registered screen-share audio SSRC in ssrcMap", - ); - // Trigger the speaking handler to ensure pipeline is ready - void speakingHandler(owner.userId).catch((err) => - logger.error( - { userId: owner.userId, error: err.message }, - "Speaking handler for screen-share failed", - ), - ); - } else { - logger.warn( - { ssrc }, - "Screen-share audio SSRC found but owner unknown", - ); - } - } - } - - // ── 2. Delegate to the original handler ── - // It will now find the SSRC (we registered it above) and forward the - // decrypted Opus packet to the subscription stream. - original.call(receiver, msg); - }; - - // ── 3. Listen for dynamic ssrcMap updates ────────────────────────────── - receiver.ssrcMap.on("create", (data: VoiceUserData) => { - if (data.videoSSRC !== undefined) { - logger.info( - { userId: data.userId, videoSSRC: data.videoSSRC }, - "Screen-share video started", - ); - void speakingHandler(data.userId).catch((err) => - logger.error( - { userId: data.userId, error: err.message }, - "Handler for screen-share start failed", - ), - ); - } - }); - - receiver.ssrcMap.on( - "update", - (_old: VoiceUserData | undefined, neu: VoiceUserData) => { - if (_old?.videoSSRC !== neu.videoSSRC && neu.videoSSRC !== undefined) { - logger.info( - { userId: neu.userId, videoSSRC: neu.videoSSRC }, - "Screen-share video SSRC appeared", - ); - void speakingHandler(neu.userId).catch((err) => - logger.error( - { userId: neu.userId, error: err.message }, - "Handler for screen-share update failed", - ), - ); - } - }, - ); -} - -// ─── Helpers ─────────────────────────────────────────────────────────────── - -/** - * Check if a UDP packet looks like a screen-share audio RTP packet. - * - * Voice packets have: RTP version 2 (top 2 bits of byte 0), and payload type 120 (OPRUS). - * We also require that the SSRC is NOT already in ssrcMap (that's handled - * by the original onUdpMessage). - */ -function isLikelyScreenShareAudio( - msg: Buffer, - receiver: VoiceReceiver, -): boolean { - if (msg.length <= 8) return false; - const ssrc = msg.readUInt32BE(8); - // Already registered as a known voice SSRC? - if (getSsrcMapEntry(receiver, ssrc)) return false; - - const rtpVersion = msg[0] >> 6; - const payloadType = msg[1] & 127; - // OPRUS payload type is 120 in Discord voice - return rtpVersion === 2 && payloadType === 120; -} - -/** - * Safely read an entry from the SSRCMap via the public `get` API. - */ -function getSsrcMapEntry( - receiver: VoiceReceiver, - ssrc: number, -): VoiceUserData | undefined { - try { - return receiver.ssrcMap.get(ssrc); - } catch { - return undefined; - } -} - -/** - * Infer which user owns a screen-share audio SSRC by proximity to their - * known voice audioSSRC (Discord allocates SSRCs in small increments). - */ -function inferScreenShareOwner( - ssrc: number, - receiver: VoiceReceiver, -): { userId: string } | null { - try { - // Iterate known SSRCs via internal _map (the public API only gets by ssrc) - const map = getSsrcInternalMap(receiver.ssrcMap); - if (!map) return null; - - for (const [, data] of map.entries()) { - if (data.audioSSRC && Math.abs(data.audioSSRC - ssrc) < 200_000) { - return { userId: data.userId }; - } - } - return null; - } catch { - return null; - } -} - -/** - * Register a new SSRC in the internal ssrcMap so the original onUdpMessage - * picks it up. We clone the user's existing VoiceUserData (so decryption - * keys, userId mapping, etc. all work) under the new SSRC key. - */ -function registerScreenShareSsrc( - receiver: VoiceReceiver, - ssrc: number, - owner: { userId: string }, -): void { - const map = getSsrcInternalMap(receiver.ssrcMap); - if (!map) return; - - // Find the owner's existing VoiceUserData and clone it under the new SSRC - for (const [, data] of map.entries()) { - if (data.userId === owner.userId) { - map.set(ssrc, { ...data }); - return; - } - } - - // Fallback: register with minimal data (userId only) - map.set(ssrc, { - userId: owner.userId, - audioSSRC: ssrc, - }); -} - -/** - * Access the private `_map` inside an SSRCMap instance. - * - * SSRCMap in @discordjs/voice ≤ v0.19 stores its entries in a private - * `#map` (JS private field) or `_map` depending on the build target. - * We access it defensively for read + write so we can register new SSRCs. - */ -function getSsrcInternalMap( - ssrcMap: VoiceReceiver["ssrcMap"], -): Map | undefined { - const asAny = ssrcMap as unknown as Record; - // v0.19 ESM build uses _map - const m1 = asAny._map; - if (m1 instanceof Map) return m1 as Map; - return undefined; -} diff --git a/services/discord-gateway/src/modules/voice-recording/streamWatchReceiver.ts b/services/discord-gateway/src/modules/voice-recording/streamWatchReceiver.ts deleted file mode 100644 index 48a1292b..00000000 --- a/services/discord-gateway/src/modules/voice-recording/streamWatchReceiver.ts +++ /dev/null @@ -1,894 +0,0 @@ -/** - * DAVE-capable stream-watch video receiver for OTHER members (camera + screen - * share). Replaces the dead selfbot-v13 path (which Discord rejects with WS - * close 4017 "E2EE/DAVE protocol required" because that lib's voice stack - * predates DAVE). - * - * HOW IT WORKS (verified architecture, 2026-08-31): - * - Watching a member's stream is a SEPARATE RTC connection, not the guild - * audio socket. We send the gateway signal STREAM_WATCH (op 20) with - * stream_key `guild:::`; Discord replies with gateway events - * STREAM_CREATE (d.rtc_server_id) + STREAM_SERVER_UPDATE (d.token, - * d.endpoint) that together describe a dedicated voice RTC for the watch. - * - We open a `@discordjs/voice` `Networking` connection pointed at that RTC - * (endpoint/token/serverId = rtc_server_id + the bot's sessionId). `Networking` - * performs the FULL DAVE handshake (identify with max_dave_protocol_version, - * IP discovery, select-protocol, DAVE MLS transitions) — the identical path - * that already works for audio. It is a public export. - * - On Ready, `Networking` exposes `state.udp` (VoiceUDPSocket). We hook its - * `message` event. Video RTP (H264) arrives there; we decrypt via the DAVE - * session with `MediaType.VIDEO` (the wrapper's default is AUDIO), then feed - * the existing `H264Depacketizer` → AnnexB `.h264` → `muxToMp4`. - * - * All failures are best-effort and NEVER break the gateway's audio recording. - */ - -import crypto from "node:crypto"; -import { - createWriteStream, - promises as fsPromises, - type WriteStream, -} from "node:fs"; -import { stat } from "node:fs/promises"; -import path from "node:path"; -import { - getVoiceConnection, - Networking, - type NetworkingState, -} from "@discordjs/voice"; -import type Davey from "@snazzah/davey"; -import { MediaType } from "@snazzah/davey"; -import type { Client, VoiceChannel } from "discord.js-selfbot-v13"; -import { createChildLogger } from "@/shared/logger/index"; -import { H264Depacketizer, muxToMp4 } from "./videoReceiver.js"; - -const logger = createChildLogger("stream-watch"); -const _log = createChildLogger("stream-watch-decrypt"); - -// Faithful ports of @discordjs/voice receive constants (dist/index.mjs). -const AUTH_TAG_LENGTH = 16; -const UNPADDED_NONCE_LENGTH = 4; -const HEADER_EXTENSION_BYTE = Buffer.from([190, 222]); // 0xBE 0xDE -/** Opus RTP payload type (120). Video arrives on non-opus payload types. */ -const RTP_OPUS_PAYLOAD_TYPE = 120; - -/** - * Video silence threshold — matches voice recording AfterSilence (4000ms). - * When no H264 video RTP packets arrive for this long, the current segment - * is closed and muxed to MP4 (like voice's "end of burst"). A new segment - * opens when packets resume. - */ -const VIDEO_SILENCE_MS = 4000; -/** - * Minimum segment duration — skip registering segments shorter than this - * (avoids creating tiny MP4 files from brief camera flashes). - */ -const VIDEO_MIN_SEGMENT_MS = 1000; - -/** A watched user's live DAVE stream connection + file state. */ -interface WatchState { - uid: string; - guildId: string; - channelId: string; - net: Networking; - depacketizer: H264Depacketizer; - filePath: string; - out: WriteStream; - bytesWritten: number; - lastPacketAt: number; - startedAt: number; - /** Timestamp of the current segment's first packet (for silence detection). */ - segmentStartAt: number; - /** Per-watch segment counter — makes each segment's filename unique. */ - segmentSeq: number; - /** True while a segment close is in-flight (rejects new packet writes). */ - closing: boolean; - _diag?: { video: number; maxLen: number; pts: Set }; - _diagNext?: number; -} - -const watches = new Map(); // key `${guildId}:${uid}` -const pendingServerId = new Map(); // key `${guildId}:${uid}` -> rtc_server_id (from STREAM_CREATE) - -let _client: Client | undefined; -let _rawAttached = false; -let _recordingsDir = "/var/lib/gmw/recordings"; - -export function setStreamWatchRecordingsDir(dir: string): void { - _recordingsDir = dir; -} - -/** - * Silence detection interval — runs every 2s and checks each active watch for - * VIDEO_SILENCE_MS of inactivity (no H264 packets). On silence, the current - * segment is closed, muxed to MP4, registered in the DB, and uploaded. - * Matches voice recording's AfterSilence behavior. - */ -const _silenceInterval = setInterval(() => { - const now = Date.now(); - for (const [watchKey, watch] of watches) { - if (!watch.out || !watch.segmentStartAt) continue; - const silenceDuration = now - watch.lastPacketAt; - if (silenceDuration < VIDEO_SILENCE_MS) continue; - - const segmentDuration = now - watch.segmentStartAt; - logger.info( - { - userId: watch.uid, - watchKey, - segmentDurationMs: segmentDuration, - bytes: watch.bytesWritten, - }, - `Video silence detected (${silenceDuration}ms) — closing segment`, - ); - - // Fire-and-forget: close + finalize the segment. - void closeCurrentSegment(watch); - } -}, 2_000); -// Prevent the interval from keeping the process alive. -if (_silenceInterval.unref) _silenceInterval.unref(); - -/** Test-only reset. */ -export function __resetStreamWatchState(): void { - for (const [, w] of watches) closeWatch(w); - watches.clear(); - pendingServerId.clear(); - _rawAttached = false; - _client = undefined; -} - -/** Bind the selfbot client (needed for STREAM_WATCH + raw). Idempotent. */ -export function setStreamWatchClient(client: Client | undefined): void { - _client = client; - if (!client || _rawAttached) return; - _rawAttached = true; - client.on("raw", (packet: unknown) => { - try { - handleRaw(packet); - } catch (err) { - logger.warn( - { err: err instanceof Error ? err.message : String(err) }, - "stream-watch raw handler error (ignoring)", - ); - } - }); -} - -const streamKeyFor = (channelId: string, guildId: string, uid: string) => - `guild:${guildId}:${channelId}:${uid}`; - -/** Parse `guild:::` back into parts. */ -function parseStreamKey( - key: string, -): { guildId: string; channelId: string; uid: string } | null { - const m = /^guild:(\d+):(\d+):(\d+)$/.exec(key); - if (!m) return null; - return { guildId: m[1], channelId: m[2], uid: m[3] }; -} - -function handleRaw(packet: unknown): void { - const p = packet as { t?: string; d?: Record } | null; - if (!p?.t || !p?.d) return; - const t = p.t; - const streamKey = p.d.stream_key as string | undefined; - if (!streamKey) return; - const key = parseStreamKey(streamKey); - if (!key) return; - const watchKey = `${key.guildId}:${key.uid}`; - - if (t === "STREAM_CREATE") { - const rtc = String(p.d.rtc_server_id ?? ""); - pendingServerId.set(watchKey, rtc); - logger.info( - { guildId: key.guildId, userId: key.uid, rtcServerId: rtc }, - "STREAM_CREATE received (watch RTC authorized)", - ); - } else if (t === "STREAM_SERVER_UPDATE") { - const endpoint = String(p.d.endpoint ?? ""); - const token = String(p.d.token ?? ""); - const rtc = - pendingServerId.get(watchKey) ?? String(p.d.rtc_server_id ?? ""); - logger.info( - { guildId: key.guildId, userId: key.uid, endpoint, rtcServerId: rtc }, - "STREAM_SERVER_UPDATE received — connecting DAVE watch", - ); - connectWatch(key.guildId, key.channelId, key.uid, endpoint, token, rtc); - } else if (t === "STREAM_DELETE") { - logger.info( - { guildId: key.guildId, userId: key.uid }, - "STREAM_DELETE received — closing watch", - ); - pendingServerId.delete(watchKey); - closeWatchByKey(watchKey); - } else if (t === "STREAM_UPDATE") { - // paused / resumed — ignore - } -} - -/** - * Initiate watching `userId`'s stream in `channel`. Sends STREAM_WATCH and - * waits for STREAM_CREATE / STREAM_SERVER_UPDATE (handled by handleRaw) to - * actually open the DAVE connection. Best-effort. - */ -export async function startStreamWatch( - channel: VoiceChannel, - userId: string, -): Promise { - if (!_client) { - logger.warn("startStreamWatch: no client set"); - return; - } - const watchKey = `${channel.guild.id}:${userId}`; - if (watches.has(watchKey)) return; // already watching - // Re-assert the bot is server-undeafened + unmuted BEFORE requesting video. - // A server-deafened bot is NOT sent the streamer's audiovisual RTP by - // Discord, so no video would arrive even though the DAVE watch reaches - // Ready. Server-deaf can silently revert on reconnect/restart, so this is - // re-invoked on every watch attempt. Dynamic import avoids a static module - // cycle (recorder ⇄ videoRecorder ⇄ streamWatchReceiver). - const { forceSelfServerUnmuteUndeafen } = await import("./recorder.js"); - void forceSelfServerUnmuteUndeafen(_client, channel.guild); - const sk = streamKeyFor(channel.id, channel.guild.id, userId); - logger.info( - { userId, guildId: channel.guild.id, channelId: channel.id, streamKey: sk }, - "Sending STREAM_WATCH to authorize video receive", - ); - try { - ( - _client as unknown as { ws: { broadcast: (d: unknown) => unknown } } - ).ws.broadcast({ - op: 20, // STREAM_WATCH - d: { stream_key: sk }, - }); - } catch (err) { - logger.warn( - { err: err instanceof Error ? err.message : String(err) }, - "Failed to send STREAM_WATCH", - ); - } -} - -function connectWatch( - guildId: string, - channelId: string, - uid: string, - endpoint: string, - token: string, - serverId: string, -): void { - const watchKey = `${guildId}:${uid}`; - if (watches.has(watchKey)) return; - const botId = _client?.user?.id; - if (!botId) return; - - // The bot's ACTIVE voice-session id, from the guild @discordjs/voice - // connection (the watch RTC identify must carry the SAME session the bot is - // joined in with). The gateway's audio connection is the voice session; the - // selfbot client's voice manager is NOT established anymore. - const guildConn = getVoiceConnection(guildId); - const sessionId = - ( - guildConn as unknown as { - state?: { - networking?: { - state?: { connectionOptions?: { sessionId?: string } }; - }; - connectionOptions?: { sessionId?: string }; - }; - } - )?.state?.networking?.state?.connectionOptions?.sessionId ?? - ( - guildConn as unknown as { - state?: { connectionOptions?: { sessionId?: string } }; - } - )?.state?.connectionOptions?.sessionId; - - logger.info( - { userId: uid, endpoint, serverId, hasSession: !!sessionId }, - "Opening DAVE Networking to watch RTC", - ); - - // CRITICAL: for a STREAM-WATCH RTC the DAVE MLS group is keyed to - // `rtc_server_id - 1` (see Discord-RE StreamConnection.daveChannelId = - // BigInt(serverId)-1n), NOT the guild voice channel id. Passing the guild - // voice channel id as `connectionOptions.channelId` makes the Davey session - // derive the wrong MLS group → "WrongGroupId" on proposals → never Ready. - // `serverId` stays the rtc_server_id (used in the voice identify server_id). - const daveChannelId = (() => { - try { - return (BigInt(serverId) - 1n).toString(); - } catch { - return channelId; // fallback: non-numeric serverId — use channel id - } - })(); - - const net = new Networking( - { - endpoint, - serverId, - userId: botId, - sessionId: sessionId ?? "none", - token, - channelId: daveChannelId, - }, - { daveEncryption: true, debug: true }, - ); - - const uidProps = uid; - const netAny = net as unknown as { - state: NetworkingState & { - udp?: { - on: (e: string, cb: (m: Buffer) => void) => void; - off: (e: string, cb: (m: Buffer) => void) => void; - }; - dave?: { session?: Davey.DAVESession }; - connectionData?: { - encryptionMode?: string; - nonceBuffer?: Buffer; - secretKey?: Buffer | Uint8Array; - dave?: { session?: Davey.DAVESession }; - }; - }; - }; - - const onMessage = (msg: Buffer): void => { - handleUdpMessage(watchKey, netAny, uidProps, msg); - }; - - net.on("stateChange", (oldState, newState) => { - const s = newState as { code?: number }; - const code = s?.code; - const oldCode = (oldState as { code?: number })?.code; - logger.info( - { userId: uid, code, oldCode, hasUdp: !!netAny.state.udp }, - `watch-state ${oldCode}->${code}`, - ); - if (code === 4 /* Ready */) { - const udp = netAny.state.udp; - if (udp) { - udp.on("message", onMessage); - logger.info( - { userId: uid }, - "DAVE watch READY — listening for video RTP", - ); - } else { - logger.warn({ userId: uid }, "watch ready but no UDP socket"); - } - } - }); - net.on("error", (err) => { - logger.warn( - { userId: uid, err: err instanceof Error ? err.message : String(err) }, - "watch Networking error", - ); - closeWatchByKey(watchKey); - }); - net.on("debug", (msg) => { - logger.info({ userId: uid, msg }, "watch-djs-debug"); - }); - net.on("close", () => { - closeWatchByKey(watchKey); - }); - - watches.set(watchKey, { - uid, - guildId, - channelId, - net, - depacketizer: new H264Depacketizer(), - filePath: "", - out: undefined as unknown as WriteStream, - bytesWritten: 0, - lastPacketAt: Date.now(), - startedAt: Date.now(), - segmentStartAt: 0, - segmentSeq: 0, - closing: false, - }); -} - -/** - * Faithful port of @discordjs/voice `VoiceReceiver.decrypt` (audio path), but - * applied to a VIDEO packet. Strips the RTP header (12 + CSRC/extension), then - * legacy-AES-decrypts the wrapped payload, returning the DAVE-encrypted payload - * (still E2EE — Davey removes the NEXT layer). - */ -function legacyDecryptLayer( - buffer: Buffer, - mode: string, - nonce2: Buffer, - secretKey: Buffer | Uint8Array, -): Buffer { - buffer.copy(nonce2, 0, buffer.length - UNPADDED_NONCE_LENGTH); - let headerSize = 12; - const first = buffer.readUint8(); - if ((first >> 4) & 1) headerSize += 4; - const header = buffer.subarray(0, headerSize); - const encrypted = buffer.subarray( - headerSize, - buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH, - ); - const authTag = buffer.subarray( - buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH, - buffer.length - UNPADDED_NONCE_LENGTH, - ); - switch (mode) { - case "aead_aes256_gcm_rtpsize": { - const decipheriv = crypto.createDecipheriv( - "aes-256-gcm", - secretKey, - nonce2, - ); - decipheriv.setAAD(header); - decipheriv.setAuthTag(authTag); - return Buffer.concat([decipheriv.update(encrypted), decipheriv.final()]); - } - case "aead_xchacha20_poly1305_rtpsize": { - // Not statically importable (sodium-wrappers is an optional peer); the - // gateway negotiates aead_aes256_gcm_rtpsize in practice. If a server - // ever selects xchacha20 we log once and skip the burst rather than - // mis-decrypt. - return Buffer.alloc(0); - } - default: { - logger.warn( - { mode }, - "Unsupported video decryption mode (skipping burst)", - ); - return Buffer.alloc(0); - } - } -} - -/** - * Replicates @discordjs/voice `VoiceReceiver.parsePacket` (audio), parameterized - * for VIDEO: strips header + padding + RTP header-extension, decrypts the legacy - * AES layer, then DAVE-decrypts with `MediaType.VIDEO` (audio's wrapper hardcodes - * AUDIO). Returns the clear H264 RTP payload, or null on any failure. - */ -function decryptVideoPacket( - buffer: Buffer, - connectionData: { - encryptionMode?: string; - nonceBuffer?: Buffer; - secretKey?: Buffer | Uint8Array; - }, - daveSession: { session?: Davey.DAVESession } | undefined, - userId: string, -): Buffer | null { - const { encryptionMode, nonceBuffer, secretKey } = connectionData; - if (!encryptionMode || !nonceBuffer || !secretKey || !daveSession?.session) - return null; - if (buffer.length < AUTH_TAG_LENGTH + UNPADDED_NONCE_LENGTH + 12) return null; - - let packet: Buffer; - try { - packet = legacyDecryptLayer(buffer, encryptionMode, nonceBuffer, secretKey); - } catch (e) { - _log.debug( - { userId, err: String(e), msgLen: buffer.length }, - "legacyDecryptLayer threw", - ); - return null; // per-packet legacy decrypt failures are transient - } - if (!packet || packet.length === 0) { - _log.debug( - { userId, msgLen: buffer.length }, - "legacyDecryptLayer returned empty", - ); - return null; - } - - // RTP padding (P bit) — strip trailing padding bytes (see parsePacket). - const hasPadding = buffer[0] !== 0 && Boolean(buffer[0] & 32); - if (hasPadding) { - const paddingAmount = packet[packet.length - 1]; - if (paddingAmount < packet.length) { - packet = packet.subarray(0, packet.length - paddingAmount); - } - } - // RTP header extension (0xBE 0xDE at bytes 12-13) — skip extension words. - if (buffer.subarray(12, 14).compare(HEADER_EXTENSION_BYTE) === 0) { - const headerExtensionLength = buffer.subarray(14).readUInt16BE(); - packet = packet.subarray(4 * headerExtensionLength); - } - - // DAVE layer — decrypt as VIDEO (audio wrapper hardcodes MediaType.AUDIO). - // If the DAVE video decryptor is missing (e.g. MLS handshake only set up - // audio decryptors, or the streamer's video stream wasn't part of the - // welcome/proposals), decrypt() throws / returns empty. We fall back: - // 1) MediaType.VIDEO - // 2) MediaType.AUDIO (some Discord streams tag video packets as AUDIO) - // 3) passthrough — return the legacy-decrypted payload as-is (GoLive - // screen-share bursts are sometimes sent unencrypted above the AES - // layer; DAVE passthrough mode permits this). - try { - const decrypted = daveSession.session.decrypt( - userId, - MediaType.VIDEO, - packet, - ); - if (decrypted && decrypted.length > 0) return decrypted; - _log.debug( - { userId, decryptedLen: decrypted?.length, pktLen: packet.length }, - "DAVE VIDEO decrypt returned empty/falsy", - ); - } catch (e) { - _log.debug({ userId, err: String(e) }, "DAVE VIDEO decrypt threw"); - // fall through to AUDIO attempt - } - try { - const decryptedAudio = daveSession.session.decrypt( - userId, - MediaType.AUDIO, - packet, - ); - if (decryptedAudio && decryptedAudio.length > 0) return decryptedAudio; - _log.debug( - { userId, decryptedLen: decryptedAudio?.length, pktLen: packet.length }, - "DAVE AUDIO decrypt returned empty/falsy", - ); - } catch (e) { - _log.debug({ userId, err: String(e) }, "DAVE AUDIO decrypt threw"); - // fall through to passthrough - } - // Passthrough: assume unencrypted above AES layer (or decryptor unavailable). - _log.debug( - { - userId, - passthroughLen: packet.length, - firstBytes: packet.subarray(0, 8).toString("hex"), - }, - "VIDEO passthrough (decrypt failed for all modes)", - ); - return packet.length > 0 ? packet : null; -} - -function handleUdpMessage( - watchKey: string, - net: { - state: { - connectionData?: { - encryptionMode?: string; - nonceBuffer?: Buffer; - secretKey?: Buffer | Uint8Array; - }; - dave?: { session?: Davey.DAVESession }; - }; - }, - uid: string, - msg: Buffer, -): void { - if (msg.length <= 12) return; - const payloadType = msg[1] & 127; - // Opus = 120. Anything else on the video/watch socket is video (H264 etc.). - if (payloadType === RTP_OPUS_PAYLOAD_TYPE) return; - const watch = watches.get(watchKey); - if (!watch) return; - const conn = net.state.connectionData ?? {}; - // Diagnose: is a DAVE session actually attached, and is it ready to decrypt? - const dave = net.state.dave; - const s = (dave as { session?: { ready?: boolean } } | undefined)?.session; - const daveReady = Boolean(s?.ready); - const haveKey = Boolean( - conn.encryptionMode && conn.nonceBuffer && conn.secretKey, - ); - // Aggregate stats — track distinct PTs and max packet size so we can tell - // whether real H264 (large, PT 96-127) arrives vs only small control packets. - const now = Date.now(); - const stat = - watch._diag ?? - ({ video: 0, maxLen: 0, pts: new Set() } as { - video: number; - maxLen: number; - pts: Set; - }); - stat.video += 1; - if (msg.length > stat.maxLen) stat.maxLen = msg.length; - stat.pts.add(payloadType); - watch._diag = stat; - const diagCount = stat.video; - const dueAt = watch._diagNext ?? 0; - if (diagCount === 1 || now >= dueAt) { - watch._diagNext = now + 20_000; - const pts = [...stat.pts].join(","); - const ssrc = msg.readUInt32BE(8); - const first = msg[0]; - const flags = msg[1]; - logger.info( - { - userId: uid, - video: stat.video, - maxLen: stat.maxLen, - pts, - haveKey, - daveAttached: !!dave?.session, - daveReady, - ssrc, - firstByte: first, - flagsByte: flags, - seq: msg.readUInt16BE(2), - hex: msg.subarray(0, 32).toString("hex"), - }, - `VIDEO-PKT diag video=${stat.video} maxLen=${stat.maxLen} pts=[${pts}] haveKey=${haveKey} dave=${!!dave?.session} ready=${daveReady} ssrc=${ssrc} first=${first} flags=${flags} hex=${msg.subarray(0, 32).toString("hex")}`, - ); - } - const decrypted = decryptVideoPacket( - msg, - conn, - // djs/voice stores the DAVESession at net.state.dave (NOT inside - // connectionData) — look it up at the top-level state so DAVE layer - // decrypt (MediaType.VIDEO) actually runs. - net.state.dave, - uid, - ); - if (!decrypted) { - // Log why — no other visibility into per-packet decrypt failures. - _log.debug( - { - videoCount: stat?.video, - daveAttached: !!net.state.dave?.session, - msgLen: msg.length, - }, - "decryptVideoPacket returned NULL — packet dropped", - ); - return; - } - - if (!watch.out) { - // First successful decrypt — open output file. - void openOutput(watchKey, watch, decrypted); - return; - } - // Reject writes while a segment close is in-flight (silence detected). - if (watch.closing) return; - watch.lastPacketAt = Date.now(); - let nals: Buffer[]; - try { - nals = watch.depacketizer.push(decrypted); - } catch { - return; - } - for (const nal of nals) { - watch.bytesWritten += nal.length; - if (!watch.out.write(nal)) { - watch.out.once("drain", () => {}); - } - } -} - -async function openOutput( - _watchKey: string, - watch: WatchState, - _first: Buffer, -): Promise { - if (watch.out) return; - const dir = path.join(_recordingsDir, watch.uid); - await fsPromises.mkdir(dir, { recursive: true }); - // Filename includes the per-watch segment counter so a new segment after - // silence never collides with the (possibly still in-flight) previous one. - const seq = watch.segmentSeq++; - const filePath = path.join( - dir, - `video-${watch.channelId}-${watch.startedAt}-${seq}.h264`, - ); - watch.filePath = filePath; - watch.out = createWriteStream(filePath); - // Swallow EPIPE / ERR_STREAM_WRITE_AFTER_END on the segment file when a - // close races an in-flight UDP write — an unhandled stream 'error' here - // would crash the gateway (same class as the media EPIPE incidents). - watch.out.on("error", () => {}); - watch.segmentStartAt = Date.now(); - logger.info( - { userId: watch.uid, path: filePath }, - "Video segment opened (DAVE video RTP received)", - ); -} - -/** - * Close the current segment's write stream, mux the H264 to MP4, and - * register the recording in the DB + upload. After this, the watch is - * ready for a new segment (silence ended → packets resume). - * Mirrors voice recording's finalizeSegment + uploadRecordingSegment flow. - */ -async function closeCurrentSegment(watch: WatchState): Promise { - if (!watch.out || watch.out.destroyed || watch.closing) return; - // Mark closing FIRST (synchronously) so the UDP handler stops writing. - watch.closing = true; - const filePath = watch.filePath; - const segmentStart = watch.segmentStartAt; - const bytes = watch.bytesWritten; - - // Flush the write stream to disk. - const flushed = new Promise((resolve) => { - if (!watch.out || watch.out.destroyed) { - resolve(); - return; - } - watch.out.once("finish", () => resolve()); - watch.out.end(); - }); - await flushed; - - // Reset watch state for the next segment. - watch.out = undefined as unknown as WriteStream; - watch.filePath = ""; - watch.bytesWritten = 0; - watch.segmentStartAt = 0; - watch.closing = false; - // Reset the depacketizer so a partial FU-A fragment from this segment - // doesn't bleed into the next segment (it only writes complete NALs). - watch.depacketizer.reset(); - - if (!filePath) return; - - // Discard very short segments (<1s) — avoids creating tiny MP4 files. - const durationMs = Date.now() - segmentStart; - if (durationMs < VIDEO_MIN_SEGMENT_MS) { - logger.debug( - { userId: watch.uid, durationMs }, - "Video segment too short, discarding", - ); - fsPromises.unlink(filePath).catch(() => {}); - return; - } - - // Mux H264 to MP4 and register in DB + upload. - try { - const mp4 = await muxToMp4(filePath); - await finalizeVideoSegment({ - mp4Path: mp4, - userId: watch.uid, - guildId: watch.guildId, - channelId: watch.channelId, - startTime: segmentStart, - durationMs, - bytes, - }); - logger.info( - { userId: watch.uid, mp4, durationMs, bytes }, - "Video segment finalized + uploaded", - ); - } catch (err) { - logger.warn( - { - userId: watch.uid, - err: err instanceof Error ? err.message : String(err), - }, - "Video segment finalize failed (raw .h264 kept)", - ); - } -} - -/** - * Register a completed video MP4 segment in the DB and upload it. - * Reuses the voice_recordings table (filename indicates video). - */ -async function finalizeVideoSegment(input: { - mp4Path: string; - userId: string; - guildId: string; - channelId: string; - startTime: number; - durationMs: number; - bytes: number; -}): Promise { - const { mp4Path, userId, guildId, channelId, startTime, durationMs, bytes } = - input; - const segmentId = `video-${userId}-${startTime}`; - const fileName = path.basename(mp4Path); - - try { - // Get file size and register in DB. - const fileStats = await stat(mp4Path); - const { insertVoiceRecording } = await import( - "../../shared/database/voiceRecordingRepo.js" - ); - await insertVoiceRecording({ - id: segmentId, - user_id: userId, - username: userId, // Will be enriched by frontend from user_profiles - avatar_url: null, - guild_id: guildId, - channel_id: channelId, - channel_name: null, - filename: fileName, - size_bytes: fileStats.size, - upload_status: "pending", - created_at: Date.now(), - }); - - // Upload MP4 to TeleUploader. - const { uploadToTele } = await import("../../shared/uploader.js"); - const { config } = await import("../../shared/config/config.js"); - const fileBuffer = await fsPromises.readFile(mp4Path); - const uploadResult = await uploadToTele({ - buffer: fileBuffer, - filename: fileName, - contentType: "video/mp4", - uploadUrl: config.TELE_UPLOAD_URL, - retries: 3, - }); - - // Update DB with upload URL. - const { updateVoiceRecordingAsUploaded } = await import( - "../../shared/database/voiceRecordingRepo.js" - ); - await updateVoiceRecordingAsUploaded( - segmentId, - uploadResult.url, - Date.now(), - ); - logger.info( - { segmentId, url: uploadResult.url, durationMs, bytes }, - "Video segment uploaded successfully", - ); - - // Broadcast via EventBroadcaster. - const { _eventBroadcaster } = await import("./recorder.js"); - if (_eventBroadcaster) { - _eventBroadcaster - .voiceRecordingUploaded({ - id: segmentId, - user_id: userId, - username: userId, - avatar_url: null, - guild_id: guildId, - channel_id: channelId, - channel_name: null, - filename: fileName, - size_bytes: fileStats.size, - download_url: uploadResult.url, - upload_status: "uploaded", - created_at: Date.now(), - uploaded_at: Date.now(), - }) - .catch(() => {}); - } - } catch (err) { - const errorMsg = err instanceof Error ? err.message : String(err); - logger.error( - { segmentId, error: errorMsg }, - "Failed to upload video segment", - ); - // Mark as failed in DB (best-effort). - const { updateVoiceRecordingAsFailed } = await import( - "../../shared/database/voiceRecordingRepo.js" - ); - await updateVoiceRecordingAsFailed(segmentId, errorMsg).catch(() => {}); - } -} - -function closeWatch(watch: WatchState): void { - if (!watch) return; - try { - watch.net.destroy(); - } catch { - /* ignore */ - } - // Finalize any open segment (mux to MP4 + DB + upload). - if (watch.out && !watch.out.destroyed) { - void closeCurrentSegment(watch); - } -} - -export function closeWatchByKey(watchKey: string): void { - const watch = watches.get(watchKey); - if (!watch) return; - watches.delete(watchKey); - pendingServerId.delete(watchKey); - closeWatch(watch); -} - -/** Stop watching `userId` in `guildId` (streaming stopped / user left). */ -export function stopStreamWatch(guildId: string, userId: string): void { - closeWatchByKey(`${guildId}:${userId}`); -} - -/** Stop ALL watches for a guild (voice disconnect / channel untrack). */ -export function stopAllStreamWatches(guildId: string): void { - for (const key of [...watches.keys()]) { - if (key.startsWith(`${guildId}:`)) closeWatchByKey(key); - } -} diff --git a/services/discord-gateway/src/modules/voice-recording/transmitter.ts b/services/discord-gateway/src/modules/voice-recording/transmitter.ts deleted file mode 100644 index 24bd2f3b..00000000 --- a/services/discord-gateway/src/modules/voice-recording/transmitter.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { spawn } from "node:child_process"; -import { PassThrough } from "node:stream"; -import { StreamType } from "@discordjs/voice"; -import type Redis from "ioredis"; -import { BACKEND_VOICE_TRANSMIT } from "../../shared/index.js"; -import { createChildLogger } from "../../shared/logger/index.js"; -import { discordPlayer } from "./player.js"; - -const logger = createChildLogger("transmitter"); - -/** - * Handles real-time PCM audio transmission from browser to Discord voice channel. - * - * Pipeline: - * Browser Mic → base64 PCM (24kHz mono s16le) → Redis → - * FFmpeg (encode to OggOpus) → discordPlayer (StreamType.OggOpus) → Discord Voice - */ -export class VoiceTransmitter { - private redisSub: Redis | null = null; - private pcmStream: PassThrough | null = null; - private ffmpegProcess: ReturnType | null = null; - private isActive = false; - private readonly TRANSMIT_CHANNEL = BACKEND_VOICE_TRANSMIT; - /** Queue for PCM chunks when backpressure is active */ - private backpressureQueue: Buffer[] = []; - /** Max queued chunks before dropping oldest (prevents unbounded memory growth) */ - private static readonly MAX_QUEUE = 500; - /** Serialise start/stop to prevent races between rapid toggle commands */ - private gate = Promise.resolve(); - /** Set true before sending SIGTERM so exit handler knows it's intentional */ - private _expectedExit = false; - /** Auto-stop timer: if no PCM received for this long, stop transmitter */ - private _activityTimer: ReturnType | null = null; - /** How long to wait without PCM before auto-stopping (10s) */ - private static readonly ACTIVITY_TIMEOUT_MS = 10_000; - /** Max stderr bytes to retain for error reporting */ - private static readonly MAX_STDERR_BYTES = 4096; - - /** - * Start listening for PCM audio data from Redis and stream to Discord - */ - async start(redis: Redis): Promise { - logger.info("Transmitter start requested"); - - // Serialise with stop() so concurrent start+stop don't tear each other down - const prev = this.gate; - let release: () => void = () => {}; - this.gate = new Promise((r) => { - release = r; - }); - await prev; - - try { - if (this.isActive) { - logger.warn("Voice transmitter already active"); - return; - } - - this.isActive = true; - - // Create PCM input stream - this.pcmStream = new PassThrough(); - this.pcmStream.setMaxListeners(32); // drain listeners accumulate during backpressure - // Voice teardown (stop / disconnect / ffmpeg exit) destroys this stream - // while Redis PCM messages may still be in flight. Without a listener, - // EPIPE / ERR_STREAM_DESTROYED / ERR_STREAM_WRITE_AFTER_END surface as - // an uncaughtException and crash the whole gateway. - this.pcmStream.on("error", (err: NodeJS.ErrnoException) => { - if ( - err.code === "EPIPE" || - err.code === "ERR_STREAM_DESTROYED" || - err.code === "ERR_STREAM_WRITE_AFTER_END" - ) { - logger.debug( - { code: err.code }, - "PCM stream closed during voice teardown — ignoring", - ); - } else { - logger.error({ error: err.message }, "PCM stream error"); - } - }); - - // Spawn FFmpeg to encode 24kHz mono PCM → OggOpus - // Input: 24kHz mono s16le (raw PCM) - // Output: OGG container with Opus audio - this.ffmpegProcess = spawn( - "ffmpeg", - [ - "-f", - "s16le", // Input format: signed 16-bit little-endian - "-ar", - "24000", // Input sample rate: 24kHz - "-ac", - "1", // Input channels: mono - "-i", - "pipe:0", // Read from stdin - "-f", - "ogg", // Output format: OGG - "-c:a", - "libopus", // Codec: Opus - "-b:a", - "96k", // Bitrate: 96kbps - "-ar", - "48000", // Output sample rate: 48kHz - "-ac", - "2", // Output channels: stereo - "-application", - "lowdelay", // Low delay mode for real-time - "-frame_duration", - "20", // 20ms frames - "-packet_loss", - "0", // No packet loss expected - "pipe:1", // Write to stdout - ], - { - stdio: ["pipe", "pipe", "pipe"], - }, - ); - - // Pipe PCM data to FFmpeg stdin - if (this.ffmpegProcess.stdin) { - this.pcmStream.pipe(this.ffmpegProcess.stdin); - } - - // Log FFmpeg stderr for debugging (cap to prevent unbounded growth) - const stderrChunks: Buffer[] = []; - let stderrLen = 0; - this.ffmpegProcess.stderr?.on("data", (chunk: Buffer) => { - stderrLen += chunk.length; - if (stderrLen <= VoiceTransmitter.MAX_STDERR_BYTES) { - stderrChunks.push(chunk); - } - }); - - this.ffmpegProcess.on("error", (err) => { - const msg = - err.message === "spawn ffmpeg ENOENT" - ? "FFmpeg/avconv not found! Install ffmpeg in the container." - : err.message; - logger.error({ error: msg }, "FFmpeg process error"); - }); - - this.ffmpegProcess.on("exit", (code) => { - // SIGTERM from stop() is expected — don't log as error - if (code !== 0 && !this._expectedExit) { - const stderr = Buffer.concat(stderrChunks).toString(); - logger.error( - { code, stderr: stderr.slice(-500) }, - "FFmpeg exited with error — auto-stopping transmitter", - ); - // FFmpeg crashed — auto-stop to prevent silent audio loss. - // Fire-and-forget; stop() serialises with the gate. - void this.stop(); - } else { - logger.debug({ code }, "FFmpeg process exited"); - } - }); - - // Play FFmpeg stdout (OggOpus) to Discord - if (this.ffmpegProcess.stdout) { - discordPlayer.playStream(this.ffmpegProcess.stdout, "browser-bridge", { - inputType: StreamType.OggOpus, - inlineVolume: true, - }); - } - - logger.info( - "Voice transmitter pipeline ready (PCM → FFmpeg → OggOpus → Discord)", - ); - - // Subscribe to Redis channel for PCM data - await redis.subscribe(this.TRANSMIT_CHANNEL); - // Assign AFTER subscription succeeds — prevents race with stop() - this.redisSub = redis; - logger.info( - { channel: this.TRANSMIT_CHANNEL }, - "Subscribed to transmit channel", - ); - // Start activity timer — will auto-stop if no PCM arrives within timeout - this.resetActivityTimer(); - - this.redisSub.on("message", (channel, message) => { - if ( - !this.isActive || - channel !== this.TRANSMIT_CHANNEL || - !this.pcmStream - ) - return; - - try { - const data = JSON.parse(message); - if (data.type === "pcm" && data.buffer) { - const pcmBuffer = Buffer.from(data.buffer, "base64"); - const stream = this.pcmStream; - const canContinue = stream.write(pcmBuffer); - // Reset activity timer — PCM is flowing - this.resetActivityTimer(); - // Backpressure: queue until drain (cap to prevent memory leak) - if (!canContinue) { - if (this.backpressureQueue.length >= VoiceTransmitter.MAX_QUEUE) { - // Drop oldest chunks to free memory — real-time audio, - // stale data is useless - const dropCount = Math.floor(VoiceTransmitter.MAX_QUEUE * 0.25); - this.backpressureQueue.splice(0, dropCount); - logger.debug( - { dropped: dropCount }, - "Backpressure overflow — dropping oldest PCM chunks", - ); - } - this.backpressureQueue.push(pcmBuffer); - stream.once("drain", () => { - const currentStream = this.pcmStream; - if (!currentStream || !this.isActive) return; - // Flush queued chunks - while (this.backpressureQueue.length > 0) { - const queued = this.backpressureQueue.shift(); - if (!queued) break; - try { - if (!currentStream.write(queued)) break; - } catch (err) { - logger.debug( - { - error: err instanceof Error ? err.message : String(err), - }, - "PCM flush write failed during teardown — ignoring", - ); - break; - } - } - }); - } - } - } catch (err) { - logger.error({ error: err }, "Failed to process PCM data"); - } - }); - - logger.info("Voice transmitter started"); - } finally { - release?.(); - } - } - - /** - * Reset the voice activity timer. Called on every PCM chunk received. - * If no chunks arrive for ACTIVITY_TIMEOUT_MS, auto-stop the transmitter - * to prevent dead air and wasted resources. - */ - private resetActivityTimer(): void { - this.clearActivityTimer(); - this._activityTimer = setTimeout(() => { - if (!this.isActive) return; - logger.warn( - { timeoutMs: VoiceTransmitter.ACTIVITY_TIMEOUT_MS }, - "Voice activity timeout — no PCM received, auto-stopping transmitter", - ); - void this.stop(); - }, VoiceTransmitter.ACTIVITY_TIMEOUT_MS); - } - - private clearActivityTimer(): void { - if (this._activityTimer !== null) { - clearTimeout(this._activityTimer); - this._activityTimer = null; - } - } - - /** - * Stop transmitting and clean up resources - */ - async stop(): Promise { - logger.info("Transmitter stop requested"); - - // Serialise with start() so concurrent start+stop don't tear each other down - const prev = this.gate; - let release: () => void = () => {}; - this.gate = new Promise((r) => { - release = r; - }); - await prev; - - try { - if (!this.isActive) { - logger.debug("Voice transmitter already stopped"); - return; - } - - this.isActive = false; - - this.clearActivityTimer(); - this.backpressureQueue = []; - - if (this.pcmStream) { - this.pcmStream.removeAllListeners("drain"); - this.pcmStream.end(); - this.pcmStream = null; - } - - // Flag FFmpeg exit as expected so the exit handler doesn't log it as error - this._expectedExit = true; - if (this.ffmpegProcess) { - this.ffmpegProcess.kill("SIGTERM"); - this.ffmpegProcess = null; - } - - if (this.redisSub) { - await this.redisSub.unsubscribe(this.TRANSMIT_CHANNEL); - this.redisSub - .quit() - .catch((err) => - logger.warn({ err }, "Failed to quit Redis subscriber"), - ); - this.redisSub = null; - } - - discordPlayer.stop("browser-bridge"); - logger.info("Voice transmitter stopped"); - } finally { - release?.(); - } - } - - /** - * Check if transmitter is currently active - */ - getStatus(): { active: boolean; channel: string } { - return { - active: this.isActive, - channel: this.TRANSMIT_CHANNEL, - }; - } -} - -export const voiceTransmitter = new VoiceTransmitter(); diff --git a/services/discord-gateway/src/modules/voice-recording/videoReceiver.ts b/services/discord-gateway/src/modules/voice-recording/videoReceiver.ts deleted file mode 100644 index 1ab61e5b..00000000 --- a/services/discord-gateway/src/modules/voice-recording/videoReceiver.ts +++ /dev/null @@ -1,455 +0,0 @@ -import { spawn } from "node:child_process"; -import { createWriteStream, mkdirSync, type WriteStream } from "node:fs"; -import { stat, unlink } from "node:fs/promises"; -import path from "node:path"; -import type { VoiceReceiver } from "@discordjs/voice"; -import type { Client } from "discord.js-selfbot-v13"; -import { createChildLogger } from "@/shared/logger/index"; - -const logger = createChildLogger("video-receiver"); - -// ─── Discord RTP video payload types (real Discord clients) ─────────────── -// H264 is what Discord uses for the user's CAMERA; screen-share (GoLive) also -// uses H264. VP8/VP9/AV1 appear for some share types / forced codecs. -export const VIDEO_PAYLOAD_TYPES = new Set([ - 96, 98, 101, 102, 106, 116, 126, 127, -]); - -// H264 NAL unit types (single byte, after the 1-byte NAL header) -const NAL_SPS = 7; -const NAL_PPS = 8; -const NAL_IDR = 5; - -// RTP H264 packet types (byte 0 of the payload) -const H264_STAP_A = 24; -const H264_FU_A = 28; - -/** - * Pure RTP-H264 depacketizer → AnnexB (prefix NALs with 00 00 00 01 start codes). - * - * Input: RTP payload bytes (after the 12-byte RTP header, after decryption). - * Output: a list of AnnexB NAL buffers (each with start code) ready to append - * to a `.h264` elementary stream file. - * - * Reassembles FU-A fragmented NALs and STAP-A aggregated NALs into standalone - * NALs with start codes. Fragments that don't form a complete NAL are buffered. - */ -export class H264Depacketizer { - /** True after we've seen an SPS/PPS/IDR so we only start writing on a keyframe. */ - private sawConfig = false; - /** Buffer for an in-progress FU-A NAL. */ - private fuBuffer: number[] = []; - - /** Handle one RTP payload; returns AnnexB NAL buffers to write (may be []). */ - push(payload: Buffer): Buffer[] { - const out: Buffer[] = []; - if (payload.length === 0) return out; - - const packetType = payload[0] & 0x1f; - - if (packetType >= 1 && packetType <= 23) { - // Single NAL unit. - const nalType = payload[0] & 0x1f; - this.maybeEnableConfig(nalType); - const annexB = this.toAnnexB(payload); - if (this.sawConfig && annexB) out.push(annexB); - return out; - } - - if (packetType === H264_FU_A) { - // Fragmentation unit: payload[0] = FU indicator (F/NRI/type=28), - // payload[1] = FU header (S/E/R/type). - const fuHeader = payload[1]; - const start = (fuHeader & 0x80) !== 0; - const end = (fuHeader & 0x40) !== 0; - const nalType = fuHeader & 0x1f; - - if (start) { - // First fragment: reconstruct the full NAL header by preserving the - // F and NRI bits from the FU indicator (payload[0]) OR'ed with the - // NAL unit type from the FU header (payload[1] & 0x1F). - const nalHeader = (payload[0] & 0xe0) | nalType; - this.fuBuffer = [nalHeader, ...payload.subarray(2)]; - } else { - // Continuation. (Ignore if no buffer — a lost first fragment.) - if (this.fuBuffer.length === 0) { - // We missed the start; the NAL type unknown, skip this fragment. - return out; - } - if (payload.length > 2) { - this.fuBuffer.push(...payload.subarray(2)); - } - } - - if (end) { - const nal = Buffer.from(this.fuBuffer); - this.fuBuffer = []; - this.maybeEnableConfig(nalType); - if (this.sawConfig && nal.length > 0) { - out.push(this.toAnnexB(nal)); - } - } - return out; - } - - if (packetType === H264_STAP_A) { - // Aggregation packet: list of (2-byte NAL size, NAL data). - let offset = 1; - while (offset < payload.length) { - if (offset + 2 > payload.length) break; - const naluLength = payload.readUInt16BE(offset); - offset += 2; - if (offset + naluLength > payload.length) break; - const nalu = payload.subarray(offset, offset + naluLength); - offset += naluLength; - const nalType = nalu[0] & 0x1f; - this.maybeEnableConfig(nalType); - const annexB = this.toAnnexB(nalu); - if (this.sawConfig && annexB) out.push(annexB); - } - } - return out; - } - - private maybeEnableConfig(nalType: number): void { - if (nalType === NAL_SPS || nalType === NAL_PPS || nalType === NAL_IDR) { - this.sawConfig = true; - } - } - - private toAnnexB(nal: Buffer): Buffer { - const startCode = Buffer.from([0x00, 0x00, 0x00, 0x01]); - return Buffer.concat([startCode, nal]); - } - - reset(): void { - this.sawConfig = false; - this.fuBuffer = []; - } -} - -// ─── Per-user video burst state ──────────────────────────────────────────── - -interface VideoBurst { - userId: string; - depacketizer: H264Depacketizer; - /** video SSRC this burst is sourced from (may be camera or screenshare). */ - ssrc: number; - createdAt: number; - filePath: string; - out: WriteStream; - /** count of AnnexB NAL bytes written (for logging/health). */ - bytesWritten: number; - lastPacketAt: number; -} - -/** - * Listens for other users' VIDEO RTP (camera + screen share) on the same UDP - * socket @discordjs/voice receives audio from, decrypts each video packet with - * the connection's secret key (via receiver.parsePacket), depacketizes H264 to - * AnnexB, and writes a raw `.h264` elementary stream per user-per-burst. - * - * Wraps `receiver.onUdpMessage` like screenShareAudio.ts. Packets with a video - * payload type are consumed here; ALL other packets (opus mic + screen-share - * audio) are delegated to the current handler (the existing wrapper chain). - */ -export function hookVideoReceiver( - receiver: VoiceReceiver, - client: Client, - recordingsDir: string, -): void { - const original = receiver.onUdpMessage; - const bursts = new Map(); - - // The UDP socket delivers video on SSRCs that are NOT the audioSSRC key of - // ssrcMap. Build a videoSSRC→userId index from VoiceUserData as it updates so - // we can attribute packets without SSRC-proximity guessing. - const videoSsrcToUser = new Map(); - - receiver.ssrcMap.on("create", (data) => { - if (data.videoSSRC !== undefined) { - videoSsrcToUser.set(data.videoSSRC, data.userId); - logger.info( - { userId: data.userId, videoSSRC: data.videoSSRC }, - "Video SSRC appeared (camera/screenshare start)", - ); - } - }); - receiver.ssrcMap.on("update", (_old, neu) => { - if (neu.videoSSRC !== undefined) { - videoSsrcToUser.set(neu.videoSSRC, neu.userId); - } - }); - - /** Close a burst's file (flush + end) then mux to a playable mp4. */ - const closeBurst = (userId: string): void => { - const burst = bursts.get(userId); - if (!burst) return; - bursts.delete(userId); - const { ssrc, bytesWritten, createdAt, filePath } = burst; - // Wait for the write stream to fully flush (file descriptor closed) before - // ffmpeg reads it — otherwise the mux can read a truncated tail. - const flushed = new Promise((resolve) => { - burst.out.once("finish", () => resolve()); - burst.out.end(); - }); - logger.info( - { userId, ssrc, bytes: bytesWritten, durationMs: Date.now() - createdAt }, - "Video burst closed", - ); - // Phase B: remux the raw H264 elementary stream into a self-contained, - // playable MP4 (fast `-c copy`, no re-encode) and drop the raw file. - void flushed - .then(() => muxToMp4(filePath)) - .then((mp4) => { - logger.info({ userId, ssrc, mp4 }, "Video muxed to mp4"); - }) - .catch((err) => { - logger.warn( - { - userId, - ssrc, - err: err instanceof Error ? err.message : String(err), - }, - "Video mux to mp4 failed (keeping raw .h264)", - ); - }); - }; - - const openBurst = (userId: string, ssrc: number): VideoBurst | null => { - try { - const userDir = path.join(recordingsDir, userId); - mkdirSync(userDir, { recursive: true }); - const filePath = path.join(userDir, `video-${ssrc}-${Date.now()}.h264`); - const out = createWriteStream(filePath); - const burst: VideoBurst = { - userId, - ssrc, - depacketizer: new H264Depacketizer(), - createdAt: Date.now(), - filePath, - out, - bytesWritten: 0, - lastPacketAt: Date.now(), - }; - bursts.set(userId, burst); - logger.info({ userId, ssrc, filePath }, "Video burst opened"); - return burst; - } catch (err) { - logger.warn( - { userId, ssrc, err: err instanceof Error ? err.message : String(err) }, - "Failed to open video burst file", - ); - return null; - } - }; - - receiver.onUdpMessage = (msg: Buffer) => { - if (msg.length <= 8) { - original.call(receiver, msg); - return; - } - - const payloadType = msg[1] & 127; - // DIAGNOSTIC: log any RTP packet whose payload type isn't Opus (120) so we - // can confirm whether Discord actually delivers video RTP to the bot. - // (Non-opus = video/experimental; if we see these, video IS arriving and the - // attribution/signaling below is the only gap.) - if (payloadType !== 120) { - logger.warn( - { payloadType, ssrc: msg.readUInt32BE(8) }, - "DIAG: non-opus RTP packet on voice socket", - ); - } - if (!VIDEO_PAYLOAD_TYPES.has(payloadType)) { - // Not video — let the existing handler (audio + screen-share audio) take it. - original.call(receiver, msg); - return; - } - - // ── It's a video RTP packet. Decrypt + depacketize + write. ── - const ssrc = msg.readUInt32BE(8); - - // Attribute to a user. - let userId = videoSsrcToUser.get(ssrc); - if (!userId) { - // Fallback: maybe we registered the SSRC under an audio entry via the - // screen-share-audio hook, or proximity. Skip bot's own stream. - const candidate = inferVideoOwner(msg, receiver); - if (candidate && candidate.userId !== client.user?.id) { - userId = candidate.userId; - videoSsrcToUser.set(ssrc, candidate.userId); - } - } - if (!userId || userId === client.user?.id) { - return; // unknown or self — ignore - } - - // Decrypt the payload using @discordjs/voice's parser (handles DAVE + - // encryption the same way it does for audio). - let decrypted: Buffer; - try { - const receiverAny = receiver as unknown as { - parsePacket: ( - buffer: Buffer, - mode: string, - nonce: Buffer, - secretKey: Uint8Array, - userId: string, - ) => Buffer; - connectionData: { - encryptionMode?: string; - nonceBuffer?: Buffer; - secretKey?: Uint8Array; - }; - }; - const { encryptionMode, nonceBuffer, secretKey } = - receiverAny.connectionData; - if (!encryptionMode || !nonceBuffer || !secretKey) return; - // decrypted payload = RTP header already stripped by parsePacket - decrypted = receiverAny.parsePacket( - msg, - encryptionMode, - nonceBuffer, - secretKey, - userId, - ); - } catch { - // Decryption failed — transient; skip this packet. - return; - } - - // Depacketize the (video) RTP payload → AnnexB. - let burst = bursts.get(userId); - if (!burst) { - const opened = openBurst(userId, ssrc); - if (!opened) return; - burst = opened; - } - burst.lastPacketAt = Date.now(); - - let nals: Buffer[]; - try { - nals = burst.depacketizer.push(decrypted); - } catch { - return; - } - if (nals.length === 0) return; - - for (const nal of nals) { - burst.bytesWritten += nal.length; - if (!burst.out.write(nal)) { - burst.out.once("drain", () => {}); - } - } - }; - - // If a user's video SSRC disappears, close their burst after a short grace so - // the tail flushes. - receiver.ssrcMap.on("delete", (data) => { - // data = the deleted VoiceUserData (userId + audioSSRC). Close any open - // video burst for that user so the mux starts as soon as they stop sharing. - if (data?.userId) { - setTimeout(() => closeBurst(data.userId), 500); - } - }); - - setInterval(() => { - const now = Date.now(); - for (const [userId, burst] of bursts) { - if (now - burst.lastPacketAt > 5000) { - closeBurst(userId); - } - } - }, 2000); - // Prevent the interval from keeping the process alive. - // (The gateway owns the process; a single 2s timer is negligible.) -} - -/** - * Infer which user owns a video SSRC by proximity to their known audioSSRC - * (Discord allocates a user's video SSRC near their audio SSRC). - */ -function inferVideoOwner( - msg: Buffer, - receiver: VoiceReceiver, -): { userId: string } | null { - const ssrc = msg.readUInt32BE(8); - try { - const map = getSsrcInternalMap(receiver.ssrcMap); - if (!map) return null; - for (const [, data] of map.entries()) { - if (data.audioSSRC && Math.abs(data.audioSSRC - ssrc) < 400_000) { - return { userId: data.userId }; - } - } - } catch { - // ignore - } - return null; -} - -/** Access the private `_map` inside SSRCMap (mirrors screenShareAudio.ts). */ -function getSsrcInternalMap( - ssrcMap: VoiceReceiver["ssrcMap"], -): - | Map - | undefined { - const asAny = ssrcMap as unknown as Record; - // @discordjs/voice 0.19.x exposes the map as `map` (public) — but some - // versions used `_map`. Accept both so attribute fallback keeps working. - const m1 = asAny.map ?? asAny._map; - if (m1 instanceof Map) return m1 as Map; - // SSRCMap could also be the fork's class — same shape. - const m2 = asAny.map; - if (m2 instanceof Map) return m2 as Map; - return undefined; -} - -/** - * Remux a raw H264 elementary stream into a playable MP4 with `-c copy` (no - * re-encode, fast). On success the raw `.h264` is removed and the `.mp4` path - * returned. On any failure the raw file is LEFT in place (caller keeps it). - */ -export async function muxToMp4(rawPath: string): Promise { - const mp4Path = rawPath.replace(/\.h264$/, ".mp4"); - - await new Promise((resolve, reject) => { - const proc = spawn( - "ffmpeg", - [ - "-hide_banner", - "-loglevel", - "error", - "-f", - "h264", - "-i", - rawPath, - "-c", - "copy", - "-movflags", - "+faststart", - "-y", - mp4Path, - ], - { stdio: ["ignore", "ignore", "pipe"] }, - ); - let stderr = ""; - proc.stderr?.on("data", (chunk) => { - stderr += chunk.toString(); - }); - proc.on("error", (err) => reject(err)); - proc.on("close", (code) => { - if (code === 0) resolve(); - else reject(new Error(`ffmpeg exit ${code ?? "?"}: ${stderr.trim()}`)); - }); - }); - - // Sanity check: only delete the raw file if the mp4 is non-empty. - const mp4Stat = await stat(mp4Path).catch(() => null); - if (!mp4Stat || mp4Stat.size === 0) { - throw new Error("mp4 mux produced an empty file"); - } - await unlink(rawPath).catch(() => {}); - return mp4Path; -} diff --git a/services/discord-gateway/src/modules/voice-recording/videoRecorder.ts b/services/discord-gateway/src/modules/voice-recording/videoRecorder.ts deleted file mode 100644 index 264a82e2..00000000 --- a/services/discord-gateway/src/modules/voice-recording/videoRecorder.ts +++ /dev/null @@ -1,385 +0,0 @@ -/** - * Video recorder (camera + screen share) for OTHER members. - * - * ARCHITECTURE (2026-08-31, Phase D — DAVE-capable stream-watch): - * - * The previous selfbot-v13 path (discord.js-selfbot-v13 `joinStreamConnection`) - * is DEAD because Discord now mandates DAVE (E2EE) on all voice RTC connections. - * The selfbot's voice stack identifies with NO `max_dave_protocol_version` → - * Discord closes the WS with code 4017 ("E2EE/DAVE protocol required") on - * every attempt, making selfbot-based video-receive impossible. - * - * The working path: - * 1. `voiceStateUpdate` detects `streaming` on a member in a tracked channel. - * 2. `streamWatchReceiver.startStreamWatch(channel, userId)` is called. - * - It sends `STREAM_WATCH` (op 20) via the gateway WS. - * - Discord replies with `STREAM_CREATE` + `STREAM_SERVER_UPDATE` containing - * the stream's RTC endpoint/token/serverId. - * - `streamWatchReceiver` builds a `@discordjs/voice` `Networking` connection - * to the stream RTC with DAVE (the same class that handles guild audio). - * - It hooks the UDP `message` event, decrypts H264 video via Davey - * `MediaType.VIDEO`, and writes to `.h264` → muxed to `.mp4`. - * 3. `streamWatchReceiver.stopStreamWatch(guildId, userId)` tears everything down. - * - * This module is the thin orchestration layer: detect → delegate → teardown. - * All failures are best-effort and NEVER break the gateway's audio recording. - */ - -import type { Client, VoiceChannel, VoiceState } from "discord.js-selfbot-v13"; -import { createChildLogger } from "@/shared/logger/index"; -import { - setStreamWatchClient, - setStreamWatchRecordingsDir, - startStreamWatch, - stopAllStreamWatches, - stopStreamWatch, -} from "./streamWatchReceiver.js"; - -const logger = createChildLogger("video-recorder"); - -/** Channels currently under audio recording, keyed by guildId. */ -const watchedChannels = new Map(); - -/** - * GUILD_CREATE.voice_states captured at gateway WS connect (the selfbot lib - * drops them), keyed by guildId. Consumed by scanExistingStreamers when the - * channel is tracked (voice join happens AFTER GUILD_CREATE, so we must store - * these until trackChannel runs). - */ -const pendingGuildCreateVoiceStates = new Map< - string, - Array<{ - user_id?: unknown; - channel_id?: unknown; - self_video?: boolean; - self_stream?: boolean; - }> ->(); - -let _client: Client | undefined; -let _listenerAttached = false; -// DIAG: collect all raw event types for ~10s after first raw event -const _allRawTypes = new Set(); -let _rawDiagLogged = false; -/** Resolved at runtime from config (kept default as fallback). */ -let _recordingsDir = "/var/lib/gmw/recordings"; - -export function setVideoRecordingsDir(dir: string) { - _recordingsDir = dir; - setStreamWatchRecordingsDir(dir); -} - -/** Test-only: clear all module-level state. */ -export function __resetVideoRecorderState(): void { - watchedChannels.clear(); - pendingGuildCreateVoiceStates.clear(); - _listenerAttached = false; - _client = undefined; - _allRawTypes.clear(); - _rawDiagLogged = false; -} - -/** - * Set the selfbot client and register the singleton voiceStateUpdate listener - * (idempotent). Called once at gateway bootstrap. Also passes the client to - * streamWatchReceiver for raw event handling (STREAM_CREATE/SERVER_UPDATE). - */ -export function setVideoRecorderClient(client: Client | undefined) { - _client = client; - setStreamWatchClient(client); - if (!client || _listenerAttached) return; - _listenerAttached = true; - client.on( - "voiceStateUpdate", - (oldState: VoiceState, newState: VoiceState) => { - void handleVoiceStateUpdate(oldState, newState); - }, - ); - // THE selfbot-v13 GUILD_CREATE handler DROPS `d.voice_states` (it only sends - // GUILD_SUBSCRIPTIONS_BULK, never parses the initial voice-state list), so - // `channel.members` is empty on gateway restart and users who were ALREADY - // in voice (camera/screen on) are never detected. Hook the raw GUILD_CREATE - // payload ourselves and scan the voice_states array — this is the only - // user-token-compatible source of "who is in the channel right now". - client.on("raw", (packet: unknown) => { - const p = packet as { - t?: string; - d?: { voice_states?: unknown; id?: unknown }; - } | null; - if (!p?.t) return; - // DIAG: log all event types seen after READY (belt-and-suspenders) - if (_allRawTypes.size < 50) _allRawTypes.add(p.t); - // DIAG: after 10s, dump all raw types seen - if (!_rawDiagLogged) { - _rawDiagLogged = true; - setTimeout(() => { - logger.info( - { rawEventTypes: [..._allRawTypes].sort() }, - "RAW DIAG: all WS raw event types (10s snapshot)", - ); - }, 10_000); - } - // Log significant events - if ( - p.t === "GUILD_CREATE" || - p.t === "READY" || - p.t === "GUILD_MEMBERS_CHUNK" || - (p.t.startsWith("GUILD") && !p.t.includes("UPDATE")) - ) { - const d = p.d as Record; - const dAny = d as unknown as { - broadcaster_user_ids?: unknown; - sessions?: unknown; - }; - logger.info( - { - rawType: p.t, - hasVoiceStates: Array.isArray(d?.voice_states), - voiceStatesCount: Array.isArray(d?.voice_states) - ? (d.voice_states as unknown[]).length - : -1, - broadcasterUserIds: dAny.broadcaster_user_ids, - sessionVoice: Array.isArray(dAny.sessions) - ? ( - dAny.sessions as Array<{ - voice?: unknown; - session_id?: unknown; - }> - ) - .map((s) => s.voice) - .filter(Boolean) - .slice(0, 5) - : undefined, - dKeys: Object.keys(d ?? {}).slice(0, 10), - }, - "RAW diag: GUILD_CREATE/READY/GUILD_MEMBERS_CHUNK", - ); - } - if (p.t !== "GUILD_CREATE") return; - const vs = p.d?.voice_states; - handleGuildCreateVoiceStates(p.d as Record, vs); - }); -} - -/** Register a channel whose audio recording is active (video should follow). */ -export function trackChannel(guildId: string, channel: VoiceChannel) { - watchedChannels.set(guildId, channel); - // When the bot joins a channel that ALREADY has people streaming (screen - // share / camera on BEFORE the bot joined), no voiceStateUpdate with - // streaming:true fires for them. Scan every member now and start watching - // anyone already streaming, so we don't miss a live stream in progress. - // Fire-and-forget: REST fallback may be needed if guild cache is cold. - void scanExistingStreamers(channel); -} - -/** - * Scan all members currently in `channel` and start watching anyone whose - * voice state has `streaming` or `self_video` set (screen share / camera - * already on when the bot joined). Fire-and-forget per member — idempotent - * (startVideoRecording is a no-op if a watch already exists for that user). - * - * BUG FIX (2026-09-02 — user report "tidak mendeteksi user yg sudah ada"): - * `channel.members` is empty after a gateway restart because (a) the selfbot - * library's GUILD_CREATE handler DROPS `d.voice_states` (it only sends - * GUILD_SUBSCRIPTIONS_BULK, never parses the initial voice state list), and - * (b) guild member cache isn't populated yet. We now capture voice_states - * raw (Path C, the reliable selfbot-compatible source), fall back to - * guild.members.fetch() (Path D, may 403 for user tokens), then cache. - */ -export async function scanExistingStreamers( - channel: VoiceChannel, -): Promise { - const selfId = _client?.user?.id; - const videoUsers: { userId: string; source: string }[] = []; - - // ── Path A: guild cache has members ──────────────────────────────────── - const members = channel.members; - if (members && members.size > 0) { - for (const [, member] of members) { - if (member.id === selfId) continue; - const vs = member.voice; - const hasVideo = Boolean(vs?.streaming || vs?.selfVideo); - if (hasVideo) { - videoUsers.push({ userId: member.id, source: "cache" }); - } - } - } - - // ── Path C: GUILD_CREATE.voice_states (buffered at WS connect) ───────── - // The selfbot lib drops `d.voice_states` from GUILD_CREATE, but we capture - // it in the raw listener. This is the ONLY selfbot-compatible source of - // "who is already in voice with video" — not gated by REST permissions. - if (videoUsers.length === 0) { - const vsList = pendingGuildCreateVoiceStates.get(channel.guild.id); - if (Array.isArray(vsList) && vsList.length > 0) { - for (const vs of vsList) { - const uid = String(vs.user_id ?? ""); - if (!uid || uid === selfId) continue; - if (String(vs.channel_id ?? "") !== channel.id) continue; - const hasVideo = Boolean(vs.self_video || vs.self_stream); - if (!hasVideo) continue; - videoUsers.push({ userId: uid, source: "guild-create-voice-states" }); - } - } - } - - // ── Path D: cache empty → fetch guild members via REST ──────────────── - // GET /channels/{id}/voice-states is BOT-ONLY since 2023 — user tokens get - // 404. The user-token-compatible way to discover who is in the voice - // channel: fetch the guild member list (GET /guilds/{id}/members — used - // elsewhere by the selfbot) which populates member.voice states, then scan - // `channel.members` again. NOTE: user tokens may also get 403 on this REST - // path (verified 2026-09-02) — Path C is the reliable one; Path D is a - // best-effort fallback for real bot tokens. - if (videoUsers.length === 0 && members && members.size === 0 && _client) { - try { - await channel.guild.members.fetch(); - // Re-read members now that cache is warm. - const warmMembers = channel.members; - if (warmMembers && warmMembers.size > 0) { - for (const [, member] of warmMembers) { - if (member.id === selfId) continue; - const vs = member.voice; - const hasVideo = Boolean(vs?.streaming || vs?.selfVideo); - if (hasVideo) { - videoUsers.push({ userId: member.id, source: "rest-members" }); - } - } - } - logger.info( - { - channelId: channel.id, - fetchedMembers: warmMembers?.size ?? 0, - foundVideo: videoUsers.length, - }, - "Fetched guild members via REST for pre-existing video detection", - ); - } catch (err) { - logger.warn( - { - channelId: channel.id, - err: err instanceof Error ? err.message : String(err), - }, - "Failed to fetch guild members via REST — video detection may miss pre-existing users", - ); - } - } - - // ── Start watching detected video users ──────────────────────────────── - let watched = 0; - for (const { userId, source } of videoUsers) { - startVideoRecording(channel, userId); - watched++; - logger.info({ userId, source }, "Detected pre-existing video user on join"); - } - if (watched > 0) { - logger.info( - { guildId: channel.guild.id, watched }, - "Scanned pre-existing streamers on join", - ); - } -} - -/** Unregister a channel (voice stopped). Tear down any video watches. */ -export function untrackChannel(guildId: string) { - watchedChannels.delete(guildId); - stopAllStreamWatches(guildId); -} - -/** - * Parse `GUILD_CREATE.d.voice_states` and buffer them for when trackChannel - * runs. This is the authoritative, user-token-friendly list of everyone - * already in voice when the gateway connects — but GUILD_CREATE arrives BEFORE - * the voice join, so we can't start watches yet. Store per-guild and consume - * in scanExistingStreamers. - */ -function handleGuildCreateVoiceStates( - data: Record, - voiceStates: unknown, -): void { - try { - const guildId = String(data.id ?? data.guild_id ?? ""); - if (!guildId) return; - if (!Array.isArray(voiceStates)) return; - const list = voiceStates as Array<{ - user_id?: unknown; - channel_id?: unknown; - self_video?: boolean; - self_stream?: boolean; - }>; - pendingGuildCreateVoiceStates.set(guildId, list); - logger.info( - { guildId, voiceStates: list.length }, - "Buffered GUILD_CREATE.voice_states for pre-existing video detection", - ); - } catch (err) { - logger.warn( - { err: err instanceof Error ? err.message : String(err) }, - "GUILD_CREATE voice_states parse error (ignoring)", - ); - } -} - -async function handleVoiceStateUpdate( - _oldState: VoiceState, - newState: VoiceState, -): Promise { - try { - const selfId = _client?.user?.id; - if (selfId && newState.id === selfId) return; // never record bot's own video - - const guildId = newState.guild?.id; - const channel = guildId ? watchedChannels.get(guildId) : undefined; - if (!channel) return; // not an actively-recorded channel - - const streaming = Boolean(newState.streaming); - const cameraOn = Boolean(newState.selfVideo); - const hasVideo = streaming || cameraOn; - const inChannel = newState.channelId === channel.id; - - if (hasVideo && inChannel) { - startVideoRecording(channel, newState.id); - } else if (!inChannel || !hasVideo) { - stopVideoRecording(channel.guild.id, newState.id); - } - } catch (err) { - logger.warn( - { err: err instanceof Error ? err.message : String(err) }, - "Video voice-state handler error (ignoring)", - ); - } -} - -/** - * Begin watching the video (camera / screen share) of `userId` in `channel`. - * Delegates to `streamWatchReceiver` which handles STREAM_WATCH + DAVE connection. - */ -export function startVideoRecording( - channel: VoiceChannel, - userId: string, -): void { - try { - const selfId = _client?.user?.id; - if (selfId && userId === selfId) return; - void startStreamWatch(channel, userId); - } catch (err) { - logger.warn( - { - userId, - guildId: channel.guild.id, - err: err instanceof Error ? err.message : String(err), - }, - "startVideoRecording error (best-effort, ignoring)", - ); - } -} - -/** Stop watching `userId`'s video in `guildId`. */ -export function stopVideoRecording(guildId: string, userId: string): void { - stopStreamWatch(guildId, userId); -} - -/** Tear down ALL video watches for a guild (e.g. on voice disconnect). */ -export function stopAllVideoRecordings(guildId: string): void { - stopAllStreamWatches(guildId); -} diff --git a/services/discord-gateway/src/modules/voice-recording/voiceController.ts b/services/discord-gateway/src/modules/voice-recording/voiceController.ts deleted file mode 100644 index a8649871..00000000 --- a/services/discord-gateway/src/modules/voice-recording/voiceController.ts +++ /dev/null @@ -1,392 +0,0 @@ -import type { VoiceConnection } from "@discordjs/voice"; -import { VoiceConnectionStatus } from "@discordjs/voice"; -import type { Client, Guild, VoiceChannel } from "discord.js-selfbot-v13"; -import { - deleteVoiceAutoReconnect, - listVoiceAutoReconnects, - upsertVoiceAutoReconnect, -} from "@/shared/database/voiceAutoReconnectRepo.js"; -import { AppError } from "@/shared/errors/index"; -import { createChildLogger } from "@/shared/logger/index"; -import { discordPlayer } from "./player.js"; -import { startRecording, stopRecording } from "./recorder.js"; - -const logger = createChildLogger("voice-controller"); - -// ─── Types ─────────────────────────────────────────────────────────────── - -export interface GuildVoiceState { - guildId: string; - channelId: string; - channelName: string; - connectedAt: number; -} - -export interface VoiceStatus { - ready: boolean; - connected: boolean; - activeGuildId: string | null; - activeChannelId: string | null; - activeChannelName: string | null; - /** Multi-guild: list of all active connections */ - connections: GuildVoiceState[]; -} - -export interface DisconnectOptions { - /** Clear the persisted auto-reconnect row (explicit user leave). */ - clearPersisted?: boolean; - /** Mark the disconnect as intentional so the rejoin monitor stops. */ - intentional?: boolean; -} - -const REJOIN_MAX_ATTEMPTS = 5; -const REJOIN_BASE_DELAY_MS = 2_000; -const REJOIN_MAX_DELAY_MS = 30_000; - -// ─── VoiceController ───────────────────────────────────────────────────── - -export class VoiceController { - private connections = new Map(); - private connecting = new Set(); - /** Guilds whose disconnect was intentional (user leave / shutdown). */ - private intentionalGuilds = new Set(); - /** Last known desired voice state, kept independent of live connection map. */ - private rejoinState = new Map(); - /** Current live connection per guild — for stale-callback invalidation. */ - private liveConnections = new Map(); - /** Per-guild rejoin attempt counters + timers. */ - private rejoinAttempts = new Map(); - private rejoinTimers = new Map(); - - constructor(private readonly client: Client) {} - - getStatus(): VoiceStatus { - logger.debug("getStatus called"); - - const primaryGuildId = this.connections.keys().next().value ?? null; - const primary = primaryGuildId - ? this.connections.get(primaryGuildId) - : undefined; - - return { - ready: this.client.isReady(), - connected: this.connections.size > 0, - activeGuildId: primary?.guildId ?? null, - activeChannelId: primary?.channelId ?? null, - activeChannelName: primary?.channelName ?? null, - connections: Array.from(this.connections.values()), - }; - } - - async connect(guildId: string, channelId: string): Promise { - logger.info({ guildId, channelId }, "connect called"); - if (!this.client.isReady()) { - throw new AppError( - "Discord client is not ready", - "CLIENT_NOT_READY", - 409, - ); - } - - if (this.connecting.has(guildId)) { - throw new AppError( - `Voice connection for guild ${guildId} is already in progress`, - "CONNECT_IN_PROGRESS", - 409, - ); - } - - this.connecting.add(guildId); - - try { - // Disconnect existing connection for this guild first - if (this.connections.has(guildId)) { - await this.disconnectGuild(guildId, { intentional: true }); - } - - // Clear any outstanding rejoin work for this guild — we are (re)joining now. - this.clearRejoin(guildId); - this.intentionalGuilds.delete(guildId); - - const guild = this.getGuild(guildId); - const channel = - guild.channels.cache.get(channelId) ?? - (await guild.channels.fetch(channelId).catch(() => null)); - - if (!channel) { - // Channel no longer exists — drop the persisted row so we don't retry forever. - await deleteVoiceAutoReconnect(guildId).catch(() => {}); - throw new AppError( - "Voice channel not found", - "VOICE_CHANNEL_NOT_FOUND", - 404, - ); - } - - if (channel.type !== "GUILD_VOICE") { - throw new AppError( - "Selected channel is not a voice channel", - "INVALID_CHANNEL_TYPE", - 400, - ); - } - - const connection = await startRecording( - this.client, - channel as VoiceChannel, - ); - if (!connection) { - throw new AppError( - "Failed to connect to voice channel", - "VOICE_CONNECT_FAILED", - 500, - ); - } - - // If this is the first connection, set it as the player's connection - if (this.connections.size === 0) { - discordPlayer.setConnection(connection as VoiceConnection); - } - - const state: GuildVoiceState = { - guildId, - channelId, - channelName: channel.name, - connectedAt: Date.now(), - }; - this.connections.set(guildId, state); - this.rejoinState.set(guildId, state); - this.liveConnections.set(guildId, connection as VoiceConnection); - - // Persist desired voice state for auto-reconnect (restart/reboot/kick). - const now = Date.now(); - await upsertVoiceAutoReconnect({ - guild_id: guildId, - channel_id: channelId, - channel_name: channel.name, - connected_at: now, - updated_at: now, - }).catch((err) => { - logger.warn( - { err: err instanceof Error ? err.message : String(err) }, - "Failed to persist voice auto-reconnect state", - ); - }); - - this.monitorConnection(guildId, connection); - - logger.info( - { guildId, channelId, channelName: channel.name }, - "Voice connected", - ); - - return this.getStatus(); - } finally { - this.connecting.delete(guildId); - } - } - - /** - * Auto-reconnect on startup/restart: rejoin every persisted voice channel. - * Called from the client `ready` handler. Non-fatal per-guild. - */ - async autoReconnect(): Promise { - let states: Array<{ - guild_id: string; - channel_id: string; - channel_name: string | null; - }> = []; - try { - states = await listVoiceAutoReconnects(); - } catch (err) { - logger.warn( - { err: err instanceof Error ? err.message : String(err) }, - "Failed to load voice auto-reconnect states", - ); - return; - } - - if (states.length === 0) { - logger.info("No persisted voice state — skipping auto-reconnect"); - return; - } - - logger.info( - { count: states.length }, - "Auto-reconnecting to persisted voice channels", - ); - - // Fire in parallel; each failure is logged but doesn't block the others. - await Promise.allSettled( - states.map(async (s) => { - try { - await this.connect(s.guild_id, s.channel_id); - } catch (err) { - logger.error( - { - guildId: s.guild_id, - channelId: s.channel_id, - channelName: s.channel_name, - err: err instanceof Error ? err.message : String(err), - }, - "Auto-reconnect failed for guild", - ); - } - }), - ); - } - - async disconnect(opts?: DisconnectOptions): Promise { - // Default semantics for graceful shutdown: intentional (no rejoin during - // teardown) but keep the persisted row so we rejoin on next boot. - const effective = opts ?? { intentional: true }; - logger.info({ opts: effective }, "disconnect called"); - - const guildIds = Array.from(this.connections.keys()); - for (const gid of guildIds) { - await this.disconnectGuild(gid, effective); - } - - discordPlayer.stop(); - return this.getStatus(); - } - - async disconnectGuild( - guildId: string, - opts?: DisconnectOptions, - ): Promise { - logger.info({ guildId, opts }, "disconnectGuild called"); - if (opts?.intentional) { - this.intentionalGuilds.add(guildId); - } - if (opts?.clearPersisted) { - await deleteVoiceAutoReconnect(guildId).catch(() => {}); - // Manual leave — forget the desired state entirely. - this.rejoinState.delete(guildId); - this.intentionalGuilds.add(guildId); - } - if (this.connections.has(guildId)) { - stopRecording(guildId); - this.connections.delete(guildId); - } - this.liveConnections.delete(guildId); - this.clearRejoin(guildId); - } - - // ─── Rejoin monitor ─────────────────────────────────────────────────── - - /** - * Watch a live connection. If it drops WITHOUT us marking the guild as - * intentional (user leave / shutdown), schedule a full rejoin with backoff. - */ - private monitorConnection( - guildId: string, - connection: VoiceConnection, - ): void { - const onDrop = () => { - // Ignore callbacks from a stale connection that has since been replaced. - if (this.liveConnections.get(guildId) !== connection) { - return; - } - - // This connection object is dead. - this.liveConnections.delete(guildId); - if (this.connections.get(guildId)?.guildId === guildId) { - this.connections.delete(guildId); - } - - if (this.intentionalGuilds.has(guildId)) { - logger.info({ guildId }, "Intentional disconnect — no auto-rejoin"); - return; - } - - logger.warn( - { guildId }, - "Voice dropped unexpectedly — scheduling rejoin", - ); - this.scheduleRejoin(guildId); - }; - - connection.on(VoiceConnectionStatus.Disconnected, onDrop); - connection.on(VoiceConnectionStatus.Destroyed, onDrop); - } - - private scheduleRejoin(guildId: string): void { - if (this.rejoinTimers.has(guildId)) { - return; // already scheduled - } - if (this.intentionalGuilds.has(guildId)) { - return; - } - - const attempt = (this.rejoinAttempts.get(guildId) ?? 0) + 1; - this.rejoinAttempts.set(guildId, attempt); - - const persisted = this.rejoinState.get(guildId); - if (!persisted) { - // Desired state was cleared — nothing to rejoin. - this.clearRejoin(guildId); - return; - } - - if (attempt > REJOIN_MAX_ATTEMPTS) { - logger.error( - { guildId, channelId: persisted.channelId, attempt }, - "Rejoin attempts exhausted — keeping persisted state for next restart", - ); - this.clearRejoin(guildId); - return; - } - - const delay = Math.min( - REJOIN_BASE_DELAY_MS * 2 ** (attempt - 1), - REJOIN_MAX_DELAY_MS, - ); - logger.info( - { guildId, channelId: persisted.channelId, attempt, delay }, - "Scheduling voice rejoin", - ); - - const timer = setTimeout(() => { - this.rejoinTimers.delete(guildId); - this.connect(guildId, persisted.channelId) - .then(() => { - this.rejoinAttempts.delete(guildId); - logger.info({ guildId }, "Auto-rejoin succeeded"); - }) - .catch((err) => { - logger.warn( - { - guildId, - err: err instanceof Error ? err.message : String(err), - attempt, - }, - "Auto-rejoin attempt failed", - ); - // Schedule the next attempt (the monitor is gone, so drive it here). - this.scheduleRejoin(guildId); - }); - }, delay); - - this.rejoinTimers.set(guildId, timer); - } - - private clearRejoin(guildId: string): void { - const timer = this.rejoinTimers.get(guildId); - if (timer) { - clearTimeout(timer); - this.rejoinTimers.delete(guildId); - } - this.rejoinAttempts.delete(guildId); - } - - private getGuild(guildId: string): Guild { - const guild = this.client.guilds.cache.get(guildId); - - if (!guild) { - throw new AppError("Guild not found", "GUILD_NOT_FOUND", 404); - } - - return guild; - } -} diff --git a/services/discord-gateway/src/modules/voice-recording/voiceTranscriber.ts b/services/discord-gateway/src/modules/voice-recording/voiceTranscriber.ts deleted file mode 100644 index 5d058538..00000000 --- a/services/discord-gateway/src/modules/voice-recording/voiceTranscriber.ts +++ /dev/null @@ -1,55 +0,0 @@ -// ─── Voice Transcription — AI-powered speech-to-text for voice recordings ──── - -import { createReadStream } from "node:fs"; -import OpenAI from "openai"; -import { createChildLogger } from "@/shared/logger/index"; -import { config } from "../../shared/config/config.js"; - -const logger = createChildLogger("voice-transcriber"); - -/** - * Transcribe a voice recording OGG file using OpenAI Whisper API. - * Returns the transcribed text or null on failure. - */ -export async function transcribeRecording( - oggPath: string, -): Promise { - if (!config.AI_VOICE_TRANSCRIPTION_ENABLED) return null; - if (!config.AI_LLM_API_KEY) { - logger.warn("AI_LLM_API_KEY not set, skipping transcription"); - return null; - } - - try { - const openai = new OpenAI({ - apiKey: config.AI_LLM_API_KEY, - baseURL: config.AI_LLM_BASE_URL, - maxRetries: 2, - timeout: 120_000, - }); - - const response = await openai.audio.transcriptions.create({ - file: createReadStream(oggPath), - model: config.AI_VOICE_TRANSCRIPTION_MODEL, - // No `language` param → Whisper auto-detects (handles id/en mixed). - // `json` (not `text`): 9router/omniroute proxies only json/verbose_json - // transcription responses; `text` returns 400 through the router. - response_format: "json", - }); - - // With response_format "json", the SDK returns an object with `text`. - const text = - (typeof response === "string" ? response : response.text)?.trim() || null; - if (!text) { - logger.warn({ oggPath }, "Empty transcription result"); - return null; - } - - logger.info({ oggPath, length: text.length }, "Transcription completed"); - return text; - } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - logger.error({ oggPath, error: msg }, "Transcription failed"); - return null; - } -} diff --git a/services/discord-gateway/src/shared/config/index.ts b/services/discord-gateway/src/shared/config/index.ts index 80916ade..2bf32401 100644 --- a/services/discord-gateway/src/shared/config/index.ts +++ b/services/discord-gateway/src/shared/config/index.ts @@ -49,32 +49,6 @@ export const configSchema = z .transform((v) => v.split(",").filter(Boolean)) .describe("User IDs to skip AI analysis for (captured but not analyzed)"), - // ── Legacy voice ───────────────────────────────────────────────────── - VOICE_GUILD_ID: z.string().min(1).optional(), - VOICE_CHANNEL_ID: z.string().min(1).optional(), - - // ── Recording ──────────────────────────────────────────────────────── - RECORDINGS_DIR: z.string().default("./recordings"), - RECORDING_SEGMENT_MS: z.coerce.number().positive().default(5000), - - // ── Decoder ────────────────────────────────────────────────────────── - DECODER_ROTATE_MS: z.coerce.number().positive().default(5000), - DECODER_COOLDOWN_MS: z.coerce.number().positive().default(30000), - - // ── Audio ──────────────────────────────────────────────────────────── - // AfterSilence: how long a voice burst may stay silent before the receive - // stream ends the segment. Raised 3000→4000 so natural pauses in speech - // (thinking gaps, interruptions) don't split one utterance into multiple - // segments ("terpotong"). Tunable via env; larger = fewer splits but a - // longer silent tail on each recording. - AUDIO_STREAM_SILENCE_DURATION_MS: z.coerce - .number() - .positive() - .default(4000), - PACKET_FILTER_MIN_SIZE: z.coerce.number().positive().default(8), - OPUS_FRAME_SIZE: z.coerce.number().positive().default(960), - AUDIO_SAMPLE_RATE: z.coerce.number().positive().default(48000), - AUDIO_CHANNELS: z.coerce.number().positive().default(2), AVATAR_SIZE: z.coerce.number().positive().default(64), // ── Server ─────────────────────────────────────────────────────────── @@ -148,17 +122,8 @@ export const configSchema = z TINYFISH_SEARCH_TIMEOUT_MS: z.coerce.number().positive().default(10000), TINYFISH_SEARCH_LOCATION: z.string().min(1).default("US"), TINYFISH_SEARCH_LANGUAGE: z.string().min(1).default("en"), - // ── Voice PCM WebSocket (direct gateway→backend, bypasses Redis) ──── - VOICE_PCM_WS_ENABLED: z - .string() - .optional() - .transform((v) => v === "true") - .default(true), - BACKEND_WS_URL: z.string().default("ws://backend:3000/ws"), - BACKEND_WS_TOKEN: z.string().optional().default(""), // ── Connection ─────────────────────────────────────────────────────── - VOICE_CONNECTION_TIMEOUT_MS: z.coerce.number().positive().default(15000), RECONNECT_TIMEOUT_MS: z.coerce.number().positive().default(5000), // ── Attachments ───────────────────────────────────────────────────── @@ -390,18 +355,6 @@ export const configSchema = z // tuning one never starves the other. PISCINA_MEDIA_MAX_THREADS: z.coerce.number().int().positive().default(2), - // ── Voice Transcription ──────────────────────────────────────────────── - AI_VOICE_TRANSCRIPTION_ENABLED: z - .string() - .optional() - .transform((v) => v === "true") - .default(false), - // Whisper model routed through the LLM base URL. Through 9router/omniroute - // use a provider-qualified id that has credentials (e.g. - // openrouter/openai/whisper-1) — bare `whisper-1` maps to the `openai` - // provider which the router rejects with "No credentials for provider". - AI_VOICE_TRANSCRIPTION_MODEL: z.string().default("whisper-1"), - // ── Auto Delete ───────────────────────────────────────────────────── AUTO_DELETE_FLAGGED_ENABLED: z .string() @@ -444,7 +397,6 @@ export const configSchema = z // ── Retention ─────────────────────────────────────────────────────── RETENTION_MESSAGES_DAYS: z.coerce.number().int().min(0).default(0), RETENTION_ATTACHMENTS_DAYS: z.coerce.number().int().min(0).default(0), - RETENTION_VOICE_DAYS: z.coerce.number().int().min(0).default(0), RETENTION_CLEANUP_INTERVAL_MS: z.coerce .number() .positive() @@ -483,7 +435,6 @@ export const configSchema = z export type AppConfig = z.infer & { EFFECTIVE_TEXT_GUILD_ID?: string; - EFFECTIVE_VOICE_GUILD_ID?: string; EFFECTIVE_MONITOR_GUILD_IDS: string[]; }; @@ -493,7 +444,6 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { return { ...parsed, EFFECTIVE_TEXT_GUILD_ID: parsed.TEXT_GUILD_ID ?? parsed.MONITOR_GUILD_ID, - EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID, EFFECTIVE_MONITOR_GUILD_IDS: parsed.MONITOR_GUILD_IDS.length > 0 ? parsed.MONITOR_GUILD_IDS diff --git a/services/discord-gateway/src/shared/database/schema/voice.ts b/services/discord-gateway/src/shared/database/schema/voice.ts deleted file mode 100644 index 799a22d9..00000000 --- a/services/discord-gateway/src/shared/database/schema/voice.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { pgVoiceRecordingsTable } from "../../../shared/index.js"; - -// Re-export shared table -export { pgVoiceRecordingsTable }; -export const voiceRecordingsTable = pgVoiceRecordingsTable; - -// Types -export type VoiceRecording = typeof voiceRecordingsTable.$inferSelect; -export type VoiceRecordingInsert = typeof voiceRecordingsTable.$inferInsert; diff --git a/services/discord-gateway/src/shared/database/voiceAutoReconnectRepo.ts b/services/discord-gateway/src/shared/database/voiceAutoReconnectRepo.ts deleted file mode 100644 index 3cb1e013..00000000 --- a/services/discord-gateway/src/shared/database/voiceAutoReconnectRepo.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { eq } from "drizzle-orm"; -import { getDatabase } from "./drizzle.js"; -import { - type VoiceAutoReconnect, - type VoiceAutoReconnectInsert, - voiceAutoReconnectTable, -} from "./schema.js"; - -/** - * Upsert a guild's desired voice state. - * Called after a successful connect so the gateway can rejoin after a - * restart/reboot or unexpected drop. - */ -export async function upsertVoiceAutoReconnect( - record: VoiceAutoReconnectInsert, -): Promise { - const db = getDatabase(); - const [row] = await db - .insert(voiceAutoReconnectTable) - .values(record) - .onConflictDoUpdate({ - target: voiceAutoReconnectTable.guild_id, - set: { - channel_id: record.channel_id, - channel_name: record.channel_name ?? null, - connected_at: record.connected_at, - updated_at: record.updated_at, - }, - }) - .returning(); - return row; -} - -/** List all persisted desired voice states (for startup auto-reconnect). */ -export async function listVoiceAutoReconnects(): Promise { - const db = getDatabase(); - return db.select().from(voiceAutoReconnectTable); -} - -/** Clear a guild's persisted voice state (explicit manual leave). */ -export async function deleteVoiceAutoReconnect(guildId: string): Promise { - const db = getDatabase(); - await db - .delete(voiceAutoReconnectTable) - .where(eq(voiceAutoReconnectTable.guild_id, guildId)); -} - -/** Fetch a single guild's persisted state, if any. */ -export async function getVoiceAutoReconnect( - guildId: string, -): Promise { - const db = getDatabase(); - const rows = await db - .select() - .from(voiceAutoReconnectTable) - .where(eq(voiceAutoReconnectTable.guild_id, guildId)) - .limit(1); - return rows[0]; -} diff --git a/services/discord-gateway/src/shared/database/voiceRecordingRepo.ts b/services/discord-gateway/src/shared/database/voiceRecordingRepo.ts deleted file mode 100644 index ba431f77..00000000 --- a/services/discord-gateway/src/shared/database/voiceRecordingRepo.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { desc, eq } from "drizzle-orm"; -import { createChildLogger } from "@/shared/logger/index"; -import { getDatabase } from "./drizzle.js"; -import { - type VoiceRecording, - type VoiceRecordingInsert, - voiceRecordingsTable, -} from "./schema.js"; - -const logger = createChildLogger("voice-recording-repo"); - -interface QueryBuilder extends PromiseLike { - from(...args: unknown[]): QueryBuilder; - where(...args: unknown[]): QueryBuilder; - orderBy(...args: unknown[]): QueryBuilder; - limit(...args: unknown[]): QueryBuilder; - offset(...args: unknown[]): QueryBuilder; - values(...args: unknown[]): QueryBuilder; - onConflictDoNothing(...args: unknown[]): QueryBuilder; - returning(...args: unknown[]): QueryBuilder; - set(...args: unknown[]): QueryBuilder; -} - -interface RecordingDatabase { - select(...args: unknown[]): QueryBuilder; - insert(...args: unknown[]): QueryBuilder; - update(...args: unknown[]): QueryBuilder; -} - -function db(): RecordingDatabase { - return getDatabase() as unknown as RecordingDatabase; -} - -export async function insertVoiceRecording( - recording: VoiceRecordingInsert, -): Promise { - try { - await db() - .insert(voiceRecordingsTable) - .values(recording) - .onConflictDoNothing(); - } catch (error) { - // Drizzle wraps PG errors — dig into cause + enumerable props for the real PG error - const err = error as Error & Record; - const detail: Record = { - message: err.message, - name: err.name, - }; - // node-postgres native error props - for (const k of [ - "code", - "detail", - "schema", - "table", - "constraint", - "severity", - ]) { - if (err[k] !== undefined) detail[k] = err[k]; - } - // drizzle may stash the original in .cause - const cause = err.cause; - if (cause instanceof Error) { - const causeErr = cause as Error & Record; - detail.cause = { - message: causeErr.message, - name: causeErr.name, - code: causeErr.code, - detail: causeErr.detail, - constraint: causeErr.constraint, - }; - } - logger.error( - { id: recording.id, error: detail }, - "Failed to insert voice recording", - ); - throw error; - } -} - -export async function updateVoiceRecordingAsUploaded( - id: string, - downloadUrl: string, - uploadedAt: number, - filename?: string, - sizeBytes?: number, -): Promise { - try { - await db() - .update(voiceRecordingsTable) - .set({ - download_url: downloadUrl, - upload_status: "uploaded", - uploaded_at: uploadedAt, - ...(filename !== undefined ? { filename } : {}), - ...(sizeBytes !== undefined ? { size_bytes: sizeBytes } : {}), - }) - .where(eq(voiceRecordingsTable.id, id)); - } catch (error) { - logger.error( - { id, error: error instanceof Error ? error.message : String(error) }, - "Failed to update voice recording status to uploaded", - ); - throw error; - } -} - -export async function updateVoiceRecordingAsFailed( - id: string, - error: string, -): Promise { - try { - await db() - .update(voiceRecordingsTable) - .set({ - upload_status: "failed", - upload_error: error, - }) - .where(eq(voiceRecordingsTable.id, id)); - } catch (error) { - logger.error( - { id, error: error instanceof Error ? error.message : String(error) }, - "Failed to update voice recording status to failed", - ); - throw error; - } -} - -export async function updateVoiceRecordingTranscription( - id: string, - transcription: string, -): Promise { - try { - await db() - .update(voiceRecordingsTable) - .set({ transcription }) - .where(eq(voiceRecordingsTable.id, id)); - } catch (error) { - logger.error( - { id, error: error instanceof Error ? error.message : String(error) }, - "Failed to update voice recording transcription", - ); - } -} - -export async function listVoiceRecordings( - limit = 100, -): Promise { - try { - const rows = await db() - .select() - .from(voiceRecordingsTable) - .orderBy(desc(voiceRecordingsTable.created_at)) - .limit(limit); - return rows as VoiceRecording[]; - } catch (error) { - logger.error( - { error: error instanceof Error ? error.message : String(error) }, - "Failed to list voice recordings", - ); - throw error; - } -} diff --git a/services/discord-gateway/src/shared/moderation-types.ts b/services/discord-gateway/src/shared/moderation-types.ts index 76ea3fde..835e6367 100644 --- a/services/discord-gateway/src/shared/moderation-types.ts +++ b/services/discord-gateway/src/shared/moderation-types.ts @@ -24,9 +24,6 @@ export interface BroadcasterClient { messageAnalyzed: (data: unknown) => void; attachmentCreated: (data: unknown) => void; attachmentUploaded: (data: unknown) => void; - voiceRecordingStarted: (data: unknown) => void; - voiceRecordingStopped: (data: unknown) => void; - voiceRecordingUploaded: (data: unknown) => void; analysisQueueStatus: (data: unknown) => void; } @@ -101,17 +98,6 @@ export interface AttachmentRecord { uploaded_at: number | null; } -export interface VoiceSegmentRecord { - id: string; - user_id: string; - session_id: string; - guild_id: string; - channel_id: string; - filename: string; - duration_ms: number; - created_at: number; -} - export interface DashboardMessage { id: string; channel_id: string; @@ -153,23 +139,6 @@ export interface AnalysisResult { evidence?: string[]; } -export interface VoiceRecordingUploadData { - id: string; - user_id: string; - username: string; - avatar_url: string | null; - guild_id: string | null; - channel_id: string | null; - channel_name: string | null; - filename: string; - size_bytes: number; - download_url: string; - upload_status: string; - created_at: number; - uploaded_at: number; - transcription?: string | null; -} - export interface AnalysisQueueStatus { queuedConversations: number; activeRequests: number; diff --git a/services/discord-gateway/src/shared/redis-channels.ts b/services/discord-gateway/src/shared/redis-channels.ts index b778c365..f7d7c948 100644 --- a/services/discord-gateway/src/shared/redis-channels.ts +++ b/services/discord-gateway/src/shared/redis-channels.ts @@ -15,11 +15,6 @@ export const DISCORD_MESSAGE_DELETED = "discord:message:deleted"; export const DISCORD_MESSAGE_ANALYZED = "discord:message:analyzed"; export const DISCORD_ATTACHMENT_CREATED = "discord:attachment:created"; export const DISCORD_ATTACHMENT_UPLOADED = "discord:attachment:uploaded"; -export const DISCORD_VOICE_STARTED = "discord:voice:started"; -export const DISCORD_VOICE_STOPPED = "discord:voice:stopped"; -export const DISCORD_VOICE_UPLOADED = "discord:voice:uploaded"; -export const DISCORD_VOICE_ACTIVE_USER = "discord:voice:active_user"; -export const DISCORD_VOICE_PCM = "discord:voice:pcm"; export const DISCORD_ANALYSIS_QUEUE_STATUS = "discord:analysis:queue_status"; export const DISCORD_REACTION_ADDED = "discord:reaction:added"; export const DISCORD_REACTION_REMOVED = "discord:reaction:removed"; @@ -37,37 +32,15 @@ export const DISCORD_MODERATION_ACTION = "discord:moderation:action"; // --------------------------------------------------------------------------- export const BACKEND_COMMAND = "backend:command"; -export const BACKEND_VOICE_TRANSMIT = "backend:voice:transmit"; export const BACKEND_COMMAND_REPLY_PREFIX = "backend:command:reply:"; -// --------------------------------------------------------------------------- -// Status keys (set by discord-gateway, read by backend via Redis GET) -// --------------------------------------------------------------------------- - -export const VOICE_STATUS_KEY = "voice:status"; -export const MEDIA_STATUS_KEY = "media:status"; - // --------------------------------------------------------------------------- // Command types (used as the `type` field in CommandMessage envelopes) // --------------------------------------------------------------------------- -export const COMMAND_VOICE_CONNECT = "voice:connect"; -export const COMMAND_VOICE_DISCONNECT = "voice:disconnect"; -export const COMMAND_VOICE_DISCONNECT_GUILD = "voice:disconnect:guild"; -export const COMMAND_VOICE_CHANNELS = "voice:channels"; -export const COMMAND_VOICE_TRANSMIT_START = "voice:transmit:start"; -export const COMMAND_VOICE_TRANSMIT_STOP = "voice:transmit:stop"; -export const COMMAND_VIDEO_WATCH = "video:watch"; -export const COMMAND_VIDEO_UNWATCH = "video:unwatch"; export const COMMAND_GUILDS_LIST = "guilds:list"; export const COMMAND_GUILDS_TEXT_CHANNELS = "guilds:text-channels"; -export const COMMAND_MEDIA_QUEUE = "media:queue"; -export const COMMAND_MEDIA_SKIP = "media:skip"; -export const COMMAND_MEDIA_STOP = "media:stop"; -export const COMMAND_MEDIA_VOLUME = "media:volume"; -export const COMMAND_MEDIA_LOOP = "media:loop"; export const COMMAND_MODERATION_ACTION = "moderation:action"; -export const DISCORD_VOICE_ANALYZED = "discord:voice:analyzed"; // --------------------------------------------------------------------------- // Event envelope — used by discord-gateway when publishing to Redis @@ -113,13 +86,7 @@ export const DISCORD_CHANNEL_TO_WS_EVENT: Record = { [DISCORD_MESSAGE_ANALYZED]: "message_analyzed", [DISCORD_ATTACHMENT_CREATED]: "attachment_created", [DISCORD_ATTACHMENT_UPLOADED]: "attachment_uploaded", - [DISCORD_VOICE_STARTED]: "voice_recording_started", - [DISCORD_VOICE_STOPPED]: "voice_recording_stopped", - [DISCORD_VOICE_UPLOADED]: "voice_recording_uploaded", [DISCORD_ANALYSIS_QUEUE_STATUS]: "analysis_queue_status", - [DISCORD_VOICE_ACTIVE_USER]: "voice_active_user", - [DISCORD_VOICE_PCM]: "voice_pcm_data", - [DISCORD_VOICE_ANALYZED]: "voice_analyzed", [DISCORD_REACTION_ADDED]: "reaction_added", [DISCORD_REACTION_REMOVED]: "reaction_removed", [DISCORD_THREAD_CREATED]: "thread_created", diff --git a/services/discord-gateway/tests/mediaResolve.test.ts b/services/discord-gateway/tests/mediaResolve.test.ts deleted file mode 100644 index 60e3895d..00000000 --- a/services/discord-gateway/tests/mediaResolve.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -// ═══════════════════════════════════════════════════════════════════════════════ -// resolveMediaUrl (music playback) resolution tests -// -// Guards the yt-dlp stdout-vs-stderr contract that broke music playback: -// - with `-o -` yt-dlp streams media on STDOUT and emits `--print` -// title/duration headers on STDERR -// - resolveMediaUrl must read headers from stderr and return the stdout -// media stream UNTOUCHED (the old code stripped binary "lines" from the -// WebM container, corrupting it → silent playback) -// -// yt-dlp is faked via a PATH shim; no network or real binaries needed. -// ═══════════════════════════════════════════════════════════════════════════════ - -import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; - -import { resolveMediaUrl } from "../src/modules/voice-recording/mediaSource.js"; - -// ─── fake bin dir ────────────────────────────────────────────────────────────── -let fakeBinDir: string | null = null; -const realPath = process.env.PATH; - -/** WebM-ish payload: EBML magic + a few bytes that must survive untouched. */ -const MEDIA_PAYLOAD = Buffer.concat([ - Buffer.from([0x1a, 0x45, 0xdf, 0xa3, 0x9f]), // EBML magic - Buffer.from("fake-webm-cluster-data\nwith-newlines\n", "utf8"), - Buffer.from([0x00, 0x11, 0x22, 0x33, 0x0a, 0xff, 0xee]), -]); - -beforeAll(() => { - fakeBinDir = mkdtempSync(join(tmpdir(), "gmw-fake-media-resolve-")); - - // Fake yt-dlp: reads GMW_FAKE_STDERR (what to write on stderr) and - // GMW_FAKE_STDOUT (what to write on stdout, default = MEDIA_PAYLOAD). - // Written with string concat so bash `${...}` isn't TS-interpolated. - const ytShim = - "#!/usr/bin/env bash\n" + - 'if [ -f "$GMW_FAKE_STDERR" ]; then cat "$GMW_FAKE_STDERR" >&2; fi\n' + - 'if [ -f "$GMW_FAKE_STDOUT" ]; then cat "$GMW_FAKE_STDOUT"; fi\n' + - "exit " + - (process.env.GMW_FAKE_EXIT ?? "0") + - "\n"; - writeFileSync(join(fakeBinDir, "yt-dlp"), ytShim); - chmodSync(join(fakeBinDir, "yt-dlp"), 0o755); - - process.env.PATH = `${fakeBinDir}:${process.env.PATH}`; -}); - -afterAll(() => { - if (fakeBinDir) { - rmSync(fakeBinDir, { recursive: true, force: true }); - } - process.env.PATH = realPath; -}); - -// ─── helpers ─────────────────────────────────────────────────────────────────── -function writeTemp(name: string, content: Buffer | string): string { - const p = join( - tmpdir(), - `gmw-resolve-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}-${name}`, - ); - writeFileSync(p, content); - return p; -} - -function collect(stream: NodeJS.ReadableStream): Promise { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - stream.on("data", (c: Buffer) => chunks.push(c)); - stream.on("error", reject); - stream.on("end", () => resolve(Buffer.concat(chunks))); - }); -} - -// ─── tests ───────────────────────────────────────────────────────────────────── -describe("resolveMediaUrl", () => { - it("reads title+duration from STDERR and returns the untouched stdout media", async () => { - const stderrPath = writeTemp("stderr", "【Test】Song Title\n184.5\n"); - const stdoutPath = writeTemp("stdout", MEDIA_PAYLOAD); - process.env.GMW_FAKE_STDERR = stderrPath; - process.env.GMW_FAKE_STDOUT = stdoutPath; - process.env.GMW_FAKE_EXIT = "0"; - - const resolution = await resolveMediaUrl("https://youtu.be/abc"); - - expect(resolution.title).toBe("【Test】Song Title"); - expect(resolution.duration).toBeCloseTo(184.5, 1); - - const bytes = await collect(resolution.stream); - expect(bytes).toEqual(MEDIA_PAYLOAD); // byte-for-byte intact - }); - - it("skips [info] prefix lines before the title header", async () => { - const stderrPath = writeTemp( - "stderr", - "[info] Downloading webpage\nTitle After Info\n123\n", - ); - const stdoutPath = writeTemp("stdout", MEDIA_PAYLOAD); - process.env.GMW_FAKE_STDERR = stderrPath; - process.env.GMW_FAKE_STDOUT = stdoutPath; - process.env.GMW_FAKE_EXIT = "0"; - - const resolution = await resolveMediaUrl("https://youtu.be/abc"); - - expect(resolution.title).toBe("Title After Info"); - expect(resolution.duration).toBe(123); - }); - - it("rejects when yt-dlp reports an ERROR on stderr", async () => { - const stderrPath = writeTemp( - "stderr", - "ERROR: [youtube] xyz: This video is unavailable\n", - ); - process.env.GMW_FAKE_STDERR = stderrPath; - delete process.env.GMW_FAKE_STDOUT; - process.env.GMW_FAKE_EXIT = "1"; - - await expect(resolveMediaUrl("https://youtu.be/abc")).rejects.toThrow( - /This video is unavailable/, - ); - }); - - it("rejects when yt-dlp is not installed", async () => { - const oldPath = process.env.PATH; - process.env.PATH = "/usr/bin:/bin"; // no fake yt-dlp - await expect(resolveMediaUrl("https://youtu.be/abc")).rejects.toThrow( - /yt-dlp is not installed/, - ); - process.env.PATH = oldPath; - }); -}); diff --git a/services/discord-gateway/tests/placeholder.test.ts b/services/discord-gateway/tests/placeholder.test.ts deleted file mode 100644 index 0df3f161..00000000 --- a/services/discord-gateway/tests/placeholder.test.ts +++ /dev/null @@ -1,446 +0,0 @@ -// ═══════════════════════════════════════════════════════════════════════════════ -// 1. AppError Hierarchy -// ═══════════════════════════════════════════════════════════════════════════════ - -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - AppError, - ConfigError, - DatabaseError, - NotFoundError, - UnauthorizedError, - ValidationError, -} from "../src/shared/errors/index.js"; - -describe("AppError subclasses", () => { - it("AppError carries code, statusCode, and details", () => { - const err = new AppError("err", "X", 500, { info: "test" }); - expect(err).toBeInstanceOf(Error); - expect(err.code).toBe("X"); - expect(err.statusCode).toBe(500); - expect(err.details).toEqual({ info: "test" }); - }); - - it("NotFoundError sets 404 status", () => { - expect(new NotFoundError("R").statusCode).toBe(404); - expect(new NotFoundError("R").code).toBe("NOT_FOUND"); - }); - - it("ValidationError sets 400 status", () => { - expect(new ValidationError("V").statusCode).toBe(400); - expect(new ValidationError("V").code).toBe("VALIDATION_ERROR"); - }); - - it("UnauthorizedError sets 401 status", () => { - expect(new UnauthorizedError().statusCode).toBe(401); - }); - - it("DatabaseError sets 500 status", () => { - expect(new DatabaseError("X").statusCode).toBe(500); - }); - - it("ConfigError sets 500 status", () => { - expect(new ConfigError("X").statusCode).toBe(500); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════════ -// 2. Shared Utilities -// ═══════════════════════════════════════════════════════════════════════════════ -import { - decodeCursor, - delay, - encodeCursor, - pageResult, - retryWithBackoff, -} from "../src/shared/utils/index.js"; - -describe("delay", () => { - afterEach(() => { - vi.useRealTimers(); - }); - - it("resolves after specified time with fake timers", async () => { - vi.useFakeTimers(); - const p = delay(250); - vi.advanceTimersByTime(250); - await expect(p).resolves.toBeUndefined(); - }); -}); - -describe("retryWithBackoff", () => { - afterEach(() => { - vi.useRealTimers(); - }); - - it("resolves on first attempt", async () => { - const fn = vi.fn().mockResolvedValue(42); - await expect(retryWithBackoff(fn)).resolves.toBe(42); - expect(fn).toHaveBeenCalledTimes(1); - }); - - it("throws after retries are exhausted", async () => { - const fn = vi.fn().mockRejectedValue(new Error("fail")); - await expect( - retryWithBackoff(fn, { retries: 1, minTimeout: 1, maxTimeout: 5 }), - ).rejects.toThrow("fail"); - expect(fn.mock.calls.length).toBeGreaterThanOrEqual(2); - }); - - it("rejects immediately when already aborted", async () => { - const ac = new AbortController(); - ac.abort(); - await expect( - retryWithBackoff(() => Promise.resolve("ok"), { signal: ac.signal }), - ).rejects.toThrow("Aborted"); - }); -}); - -describe("pagination utils", () => { - it("encode/decode round-trips correctly", () => { - const data = { created_at: 999, id: "id-1" }; - expect(decodeCursor(encodeCursor(data))).toEqual(data); - }); - - it("decodeCursor rejects invalid input with null", () => { - expect(decodeCursor()).toBeNull(); - expect(decodeCursor("")).toBeNull(); - expect(decodeCursor("!!!")).toBeNull(); - }); - - it("pageResult handles hasMore and no-more cases", () => { - const r1 = pageResult([{ id: "a", created_at: 1 }], 5); - expect(r1.nextCursor).toBeNull(); - - const r2 = pageResult( - [ - { id: "a", created_at: 1 }, - { id: "b", created_at: 2 }, - ], - 1, - ); - expect(r2.data).toHaveLength(1); - expect(r2.nextCursor).toBeTruthy(); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════════ -// 3. Redis Channel Constants -// ═══════════════════════════════════════════════════════════════════════════════ -import { - BACKEND_COMMAND, - COMMAND_GUILDS_LIST, - COMMAND_MEDIA_QUEUE, - COMMAND_MEDIA_SKIP, - COMMAND_MEDIA_STOP, - COMMAND_MEDIA_VOLUME, - COMMAND_MODERATION_ACTION, - COMMAND_VOICE_CONNECT, - COMMAND_VOICE_DISCONNECT, - DISCORD_ANALYSIS_QUEUE_STATUS, - DISCORD_ATTACHMENT_CREATED, - DISCORD_MESSAGE_ANALYZED, - DISCORD_MESSAGE_CREATED, - DISCORD_MESSAGE_DELETED, - DISCORD_MESSAGE_UPDATED, - DISCORD_VOICE_PCM, - DISCORD_VOICE_STARTED, - MEDIA_STATUS_KEY, - VOICE_STATUS_KEY, -} from "../src/shared/redis-channels.js"; - -describe("Redis channel constants", () => { - it("define event channel names", () => { - expect(DISCORD_MESSAGE_CREATED).toBe("discord:message:created"); - expect(DISCORD_MESSAGE_UPDATED).toBe("discord:message:updated"); - expect(DISCORD_MESSAGE_DELETED).toBe("discord:message:deleted"); - expect(DISCORD_MESSAGE_ANALYZED).toBe("discord:message:analyzed"); - expect(DISCORD_ATTACHMENT_CREATED).toBe("discord:attachment:created"); - expect(DISCORD_VOICE_STARTED).toBe("discord:voice:started"); - expect(DISCORD_VOICE_PCM).toBe("discord:voice:pcm"); - expect(DISCORD_ANALYSIS_QUEUE_STATUS).toBe("discord:analysis:queue_status"); - }); - - it("define command channel and status keys", () => { - expect(BACKEND_COMMAND).toBe("backend:command"); - expect(VOICE_STATUS_KEY).toBe("voice:status"); - expect(MEDIA_STATUS_KEY).toBe("media:status"); - }); - - it("define command type constants", () => { - expect(COMMAND_VOICE_CONNECT).toBe("voice:connect"); - expect(COMMAND_VOICE_DISCONNECT).toBe("voice:disconnect"); - expect(COMMAND_GUILDS_LIST).toBe("guilds:list"); - expect(COMMAND_MEDIA_QUEUE).toBe("media:queue"); - expect(COMMAND_MEDIA_SKIP).toBe("media:skip"); - expect(COMMAND_MEDIA_STOP).toBe("media:stop"); - expect(COMMAND_MEDIA_VOLUME).toBe("media:volume"); - expect(COMMAND_MODERATION_ACTION).toBe("moderation:action"); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════════ -// 4. Config Validation -// ═══════════════════════════════════════════════════════════════════════════════ -vi.hoisted(() => { - process.env.DISCORD_TOKEN = "test-discord-token-for-tests"; - process.env.DATABASE_URL = "postgres://test:test@localhost:5432/test"; -}); - -import { configSchema, loadConfig } from "../src/shared/config/index.js"; - -describe("Config validation", () => { - it("loadConfig succeeds with minimal valid env", () => { - const cfg = loadConfig({ - DISCORD_TOKEN: "abc", - DATABASE_URL: "postgres://localhost/db", - }); - expect(cfg.DISCORD_TOKEN).toBe("abc"); - // Defaults - expect(cfg.RECORDINGS_DIR).toBe("./recordings"); - expect(cfg.RECORDING_SEGMENT_MS).toBe(5000); - expect(cfg.NODE_ENV).toBe("development"); - expect(cfg.WEBSERVER_PORT).toBe(3001); - expect(cfg.OPUS_FRAME_SIZE).toBe(960); - expect(cfg.AUDIO_SAMPLE_RATE).toBe(48000); - expect(cfg.LOG_LEVEL).toBe("info"); - }); - - it("loadConfig throws ConfigError when DISCORD_TOKEN is missing", () => { - expect(() => loadConfig({})).toThrow(ConfigError); - }); - - it("schema parses boolean-string transforms correctly", () => { - const result = configSchema.parse({ - DISCORD_TOKEN: "tok", - DATABASE_URL: "pg://localhost/db", - VERBOSE: "true", - AI_ANALYSIS_ENABLED: "true", - AI_LLM_API_KEY: "sk-test", - }); - expect(result.AI_ANALYSIS_ENABLED).toBe(true); - }); - - it("schema provides sensible default for NODE_ENV", () => { - const result = configSchema.parse({ - DISCORD_TOKEN: "tok", - DATABASE_URL: "pg://localhost/db", - }); - expect(result.NODE_ENV).toBe("development"); - }); - - it("gateway loadConfig adds EFFECTIVE_TEXT_GUILD_ID from MONITOR_GUILD_ID", async () => { - const { loadConfig: gwLoadConfig } = await import( - "../src/shared/config/config.js" - ); - const cfg = gwLoadConfig({ - DISCORD_TOKEN: "tok", - DATABASE_URL: "pg://localhost/db", - MONITOR_GUILD_ID: "guild-1", - }); - expect(cfg.EFFECTIVE_TEXT_GUILD_ID).toBe("guild-1"); - }); - - it("gateway loadConfig prefers TEXT_GUILD_ID over MONITOR_GUILD_ID", async () => { - const { loadConfig: gwLoadConfig } = await import( - "../src/shared/config/config.js" - ); - const cfg = gwLoadConfig({ - DISCORD_TOKEN: "tok", - DATABASE_URL: "pg://localhost/db", - TEXT_GUILD_ID: "text-guild", - MONITOR_GUILD_ID: "monitor-guild", - }); - expect(cfg.EFFECTIVE_TEXT_GUILD_ID).toBe("text-guild"); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════════ -// 5. Pure function modules -// ═══════════════════════════════════════════════════════════════════════════════ -import { sniffImageMimeType } from "../src/modules/ai-moderation/mediaAnalysisClient.js"; - -describe("sniffImageMimeType", () => { - function buf(...bytes: number[]): Buffer { - const b = Buffer.alloc(12); - for (let i = 0; i < bytes.length; i++) b[i] = bytes[i]; - return b; - } - - it("detects JPEG", () => { - expect(sniffImageMimeType(buf(0xff, 0xd8, 0xff))).toBe("image/jpeg"); - }); - - it("detects PNG", () => { - expect( - sniffImageMimeType(buf(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a)), - ).toBe("image/png"); - }); - - it("detects GIF", () => { - expect(sniffImageMimeType(buf(0x47, 0x49, 0x46, 0x38))).toBe("image/gif"); - }); - - it("detects WebP", () => { - expect( - sniffImageMimeType( - buf(0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50), - ), - ).toBe("image/webp"); - }); - - it("detects AVIF", () => { - // ftyp box with avif brand at bytes 8-11 - expect( - sniffImageMimeType( - buf(0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66), - ), - ).toBe("image/avif"); - }); - - it("detects HEIC", () => { - // ftyp box with heic brand at bytes 8-11 - expect( - sniffImageMimeType( - buf(0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63), - ), - ).toBe("image/heic"); - }); - - it("returns null for short buffer (< 12 bytes)", () => { - expect(sniffImageMimeType(Buffer.alloc(3))).toBeNull(); - }); - - it("returns null for unrecognised data", () => { - expect(sniffImageMimeType(Buffer.alloc(12))).toBeNull(); - }); -}); - -import { - clampScore, - deriveRecommendedAction, - deriveSeverity, - hasDeferralAnalysis, -} from "../src/modules/ai-moderation/moderationResponseParser.js"; - -describe("severityDeriver", () => { - describe("clampScore", () => { - it("clamps values between 0 and 1", () => { - expect(clampScore(-0.5)).toBe(0); - expect(clampScore(0.5)).toBe(0.5); - expect(clampScore(1.5)).toBe(1); - }); - - it("handles undefined and NaN with fallback", () => { - expect(clampScore(undefined)).toBe(0); - expect(clampScore(NaN)).toBe(0); - }); - - it("allows custom fallback", () => { - expect(clampScore(undefined, 0.5)).toBe(0.5); - }); - }); - - describe("deriveSeverity", () => { - it("returns none for clean status", () => { - expect(deriveSeverity("clean", 0)).toBe("none"); - }); - - it("returns low for warn with low score", () => { - expect(deriveSeverity("warn", 0.5)).toBe("low"); - }); - - it("returns medium for warn with score >= 0.65", () => { - expect(deriveSeverity("warn", 0.65)).toBe("medium"); - }); - - it("returns critical for flagged with score >= 0.9", () => { - expect(deriveSeverity("flagged", 0.9)).toBe("critical"); - }); - - it("returns high for flagged with score >= 0.75", () => { - expect(deriveSeverity("flagged", 0.75)).toBe("high"); - }); - - it("returns medium for flagged with score < 0.75", () => { - expect(deriveSeverity("flagged", 0.5)).toBe("medium"); - }); - }); - - describe("deriveRecommendedAction", () => { - it("returns none for clean status", () => { - expect(deriveRecommendedAction("clean", "none")).toBe("none"); - }); - - it("returns review for warn with medium severity", () => { - expect(deriveRecommendedAction("warn", "medium")).toBe("review"); - }); - - it("returns warn for warn with low severity", () => { - expect(deriveRecommendedAction("warn", "low")).toBe("warn"); - }); - }); - - describe("hasDeferralAnalysis", () => { - it("detects Indonesian deferral: kurang konteks", () => { - expect(hasDeferralAnalysis("kurang konteks untuk menilai")).toBe(true); - }); - - it("detects English deferral: insufficient context", () => { - expect(hasDeferralAnalysis("insufficient context to moderate")).toBe( - true, - ); - }); - - it("detects cannot determine pattern", () => { - expect(hasDeferralAnalysis("cannot determine")).toBe(true); - }); - - it("returns false for non-deferral text", () => { - expect(hasDeferralAnalysis("This message is perfectly clean")).toBe( - false, - ); - }); - - it("returns false for exception pattern (decisive verdict)", () => { - expect( - hasDeferralAnalysis( - "tidak bisa menentukan karena tidak ada pelanggaran", - ), - ).toBe(false); - }); - }); -}); - -import { extractJson } from "../src/modules/ai-moderation/moderationResponseParser.js"; - -describe("extractJson", () => { - it("extracts from plain JSON string", () => { - expect(extractJson('{"a":1}')).toEqual({ a: 1 }); - }); - - it("extracts from JSON inside markdown code block", () => { - const input = '```json\n{"key": "value"}\n```'; - expect(extractJson(input)).toEqual({ key: "value" }); - }); - - it("extracts from JSON inside unlabeled code block", () => { - const input = '```\n{"nested": {"x": 42}}\n```'; - expect(extractJson(input)).toEqual({ nested: { x: 42 } }); - }); - - it("extracts JSON from surrounding text", () => { - const input = 'Here is the result: {"status": "ok"} end.'; - expect(extractJson(input)).toEqual({ status: "ok" }); - }); - - it("throws an error when no JSON is found", () => { - expect(() => extractJson("this has no json at all")).toThrow( - "No JSON object found", - ); - }); - - it("throws on empty string", () => { - expect(() => extractJson("")).toThrow("No JSON object found"); - }); -}); diff --git a/services/discord-gateway/tests/videoHandler.test.ts b/services/discord-gateway/tests/videoHandler.test.ts deleted file mode 100644 index 499f2910..00000000 --- a/services/discord-gateway/tests/videoHandler.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { VideoHandler } from "../src/modules/command-handler/video.handler.js"; -import * as streamWatch from "../src/modules/voice-recording/streamWatchReceiver.js"; - -// ─── mocks ───────────────────────────────────────────────────────────── -function makeClient({ botId = "bot1", channelType = "GUILD_VOICE" } = {}) { - const activeChannel = { - id: "c1", - name: "Lounge", - type: channelType, - members: { has: vi.fn(() => false) }, - }; - const guild = { - id: "g1", - channels: { - cache: new Map([["c1", activeChannel]]), - fetch: vi.fn().mockResolvedValue(new Map()), - }, - }; - return { - user: { id: botId }, - guilds: { - cache: new Map([["g1", guild]]), - fetch: vi.fn().mockResolvedValue(guild), - }, - }; -} - -function makeVoiceController(activeChannelId = "c1") { - return { - getStatus: vi.fn().mockReturnValue({ activeChannelId, connected: true }), - }; -} - -function makeCmd(type: string, payload: Record) { - return { id: "req-1", type, payload, replyChannel: "ch:1" }; -} - -beforeEach(() => { - vi.restoreAllMocks(); - vi.spyOn(streamWatch, "startStreamWatch").mockResolvedValue(); - vi.spyOn(streamWatch, "stopStreamWatch").mockImplementation(() => {}); -}); - -describe("VideoHandler", () => { - it("rejects when gateway not initialized", async () => { - const handler = new VideoHandler(null, null); - const reply = await handler.handleVideoWatch(makeCmd("video:watch", {})); - expect(reply.success).toBe(false); - expect(reply.error).toContain("not initialized"); - }); - - it("rejects when guildId/userId missing", async () => { - const handler = new VideoHandler(makeClient(), makeVoiceController()); - const reply = await handler.handleVideoWatch( - makeCmd("video:watch", { guildId: "g1" }), - ); - expect(reply.success).toBe(false); - expect(reply.error).toContain("guildId and userId are required"); - }); - - it("rejects watching the selfbot's own stream", async () => { - const handler = new VideoHandler( - makeClient({ botId: "bot1" }), - makeVoiceController(), - ); - const reply = await handler.handleVideoWatch( - makeCmd("video:watch", { guildId: "g1", userId: "bot1" }), - ); - expect(reply.success).toBe(false); - expect(reply.error).toContain("own stream"); - }); - - it("returns error when no active voice channel found", async () => { - // Freeze the cache so no channel resolves. - const client = makeClient(); - client.guilds.cache.get("g1").channels.cache.clear(); - const handler = new VideoHandler(client, makeVoiceController()); - const reply = await handler.handleVideoWatch( - makeCmd("video:watch", { guildId: "g1", userId: "user-x" }), - ); - expect(reply.success).toBe(false); - expect(reply.error).toContain("No active voice channel"); - }); - - it("calls startStreamWatch with resolved channel + userId and replies success", async () => { - const client = makeClient(); - const handler = new VideoHandler(client, makeVoiceController("c1")); - const reply = await handler.handleVideoWatch( - makeCmd("video:watch", { guildId: "g1", userId: "user-x" }), - ); - expect(reply.success).toBe(true); - expect(reply.data).toMatchObject({ - status: "requested", - guildId: "g1", - channelId: "c1", - userId: "user-x", - }); - expect(streamWatch.startStreamWatch).toHaveBeenCalledWith( - expect.objectContaining({ id: "c1" }), - "user-x", - ); - }); - - it("prefers an explicit channelId over the active one", async () => { - const client = makeClient(); - // Add a second voice channel. - const guild = client.guilds.cache.get("g1"); - guild.channels.cache.set("c9", { - id: "c9", - name: "Other", - type: "GUILD_VOICE", - members: { has: vi.fn(() => false) }, - }); - const handler = new VideoHandler(client, makeVoiceController("c1")); - const reply = await handler.handleVideoWatch( - makeCmd("video:watch", { - guildId: "g1", - userId: "user-x", - channelId: "c9", - }), - ); - expect(reply.success).toBe(true); - expect(streamWatch.startStreamWatch).toHaveBeenCalledWith( - expect.objectContaining({ id: "c9" }), - "user-x", - ); - }); - - it("handleVideoUnwatch calls stopStreamWatch", async () => { - const handler = new VideoHandler(makeClient(), makeVoiceController()); - const reply = await handler.handleVideoUnwatch( - makeCmd("video:unwatch", { guildId: "g1", userId: "user-x" }), - ); - expect(reply.success).toBe(true); - expect(streamWatch.stopStreamWatch).toHaveBeenCalledWith("g1", "user-x"); - }); - - it("handleVideoUnwatch rejects missing args", async () => { - const handler = new VideoHandler(makeClient(), makeVoiceController()); - const reply = await handler.handleVideoUnwatch( - makeCmd("video:unwatch", { guildId: "g1" }), - ); - expect(reply.success).toBe(false); - expect(reply.error).toContain("guildId and userId are required"); - }); -}); diff --git a/services/discord-gateway/tests/videoReceiver.test.ts b/services/discord-gateway/tests/videoReceiver.test.ts deleted file mode 100644 index f61c0068..00000000 --- a/services/discord-gateway/tests/videoReceiver.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { mkdtempSync, rmSync } from "node:fs"; -import { access, stat } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { SSRCMap } from "@discordjs/voice"; -import { describe, expect, it } from "vitest"; -import { - H264Depacketizer, - muxToMp4, -} from "../src/modules/voice-recording/videoReceiver.js"; - -// Build a single-NAL RTP payload: [NAL header, ...data] -function singleNal(nalHeader: number, data: number[] = []): Buffer { - return Buffer.from([nalHeader, ...data]); -} - -describe("H264Depacketizer", () => { - it("prefixes a single NAL with an AnnexB start code once config seen", () => { - const d = new H264Depacketizer(); - // SPS first (0x67 = nal ref idc 3, type 7) - const sps = d.push(singleNal(0x67, [1, 2, 3])); - expect(sps.length).toBe(1); - expect(sps[0].subarray(0, 4)).toEqual(Buffer.from([0, 0, 0, 1])); - expect(sps[0][4]).toBe(0x67); // start code + original NAL - }); - - it("drops NALs before seeing SPS/PPS/IDR (waits for a keyframe)", () => { - const d = new H264Depacketizer(); - // A non-IDR slice (type 1) before any config → dropped - const res = d.push(singleNal(0x21, [9, 9, 9])); - expect(res.length).toBe(0); - }); - - it("emits frames after config", () => { - const d = new H264Depacketizer(); - d.push(singleNal(0x67, [])); // SPS - const slice = d.push(singleNal(0x65, [1, 2, 3, 4])); // IDR (type 5) - expect(slice.length).toBe(1); - expect(slice[0].equals(Buffer.from([0, 0, 0, 1, 0x65, 1, 2, 3, 4]))).toBe( - true, - ); - }); - - it("reassembles FU-A fragments into one NAL", () => { - const d = new H264Depacketizer(); - d.push(singleNal(0x67, [])); // SPS config - // FU-A: payload[0]=FU indicator (0x7C=type28), payload[1]=FU header - // start fragment: S=1 E=0, NAL type 5 => 0x85 ; data [10, 11] - const start = Buffer.from([0x7c, 0x85, 10, 11]); - const middle = Buffer.from([0x7c, 0x05, 12, 13]); - const end = Buffer.from([0x7c, 0x45, 14, 15]); // E=1 - - expect(d.push(start).length).toBe(0); // not complete yet - expect(d.push(middle).length).toBe(0); - const final = d.push(end); - expect(final.length).toBe(1); - // NAL header reconstructed as (0x7C & 0xE0) | 5 = 0x65, then data 10..15 - expect( - final[0].equals(Buffer.from([0, 0, 0, 1, 0x65, 10, 11, 12, 13, 14, 15])), - ).toBe(true); - }); - - it("first fragment of a burst with unknown start is dropped gracefully", () => { - const d = new H264Depacketizer(); - d.push(singleNal(0x67, [])); // SPS - // A continuation fragment with no buffer → ignored, no crash - const orphan = Buffer.from([0x7c, 0x05, 99]); // S=0 - expect(d.push(orphan).length).toBe(0); - }); -}); - -describe("muxToMp4", () => { - it("remuxes a raw h264 file into a playable mp4 and deletes the raw file", async () => { - // Skip if ffmpeg is unavailable (headless Nix-less CI). - let ffmpegOk = true; - try { - execFileSync("ffmpeg", ["-version"], { stdio: "ignore" }); - } catch { - ffmpegOk = false; - } - if (!ffmpegOk) { - return; - } - - const dir = mkdtempSync(path.join(tmpdir(), "gmw-video-")); - const raw = path.join(dir, "clip.h264"); - try { - execFileSync( - "ffmpeg", - [ - "-hide_banner", - "-loglevel", - "error", - "-f", - "lavfi", - "-i", - "testsrc=duration=1:size=320x240:rate=10", - "-c:v", - "libx264", - "-preset", - "ultrafast", - "-profile:v", - "baseline", - "-pix_fmt", - "yuv420p", - "-f", - "h264", - "-y", - raw, - ], - { stdio: "ignore" }, - ); - - const mp4 = await muxToMp4(raw); - expect(mp4).toBe(path.join(dir, "clip.mp4")); - // raw file deleted, mp4 exists and is non-empty - await expect(access(raw)).rejects.toThrow(); - const s = await stat(mp4); - expect(s.size).toBeGreaterThan(0); - } finally { - rmSync(dir, { recursive: true, force: true }); - } - }); -}); - -// ─── Fork @discordjs/voice patch tests (video receive support) ───────────── -// These assert the behavior added in vendor/discord-voice-fork: -// 1. SSRCMap.update() accepts videoSSRC and get() resolves video SSRCs -// 2. The op-12 Speaking handler maps streams[].ssrc → videoSSRC -describe("discord-voice-fork videoSSRC", () => { - it("registers a videoSSRC and get() resolves it from the video SSRC", () => { - const map = new SSRCMap(); - // Simulates what the fork's onWsPacket(op 12) does: - // ssrcMap.update({ userId, audioSSRC, videoSSRC }) - map.update({ userId: "user-1", audioSSRC: 1001, videoSSRC: 2001 }); - - // Audio path unchanged: get by audioSSRC - expect(map.get(1001)?.userId).toBe("user-1"); - // Video path (fork): get by videoSSRC resolves to the same user - expect(map.get(2001)?.userId).toBe("user-1"); - expect(map.get(2001)?.videoSSRC).toBe(2001); - }); - - it("emits create event with videoSSRC for videoReceiver attribution", () => { - const map = new SSRCMap(); - const seen: Array<{ userId: string; videoSSRC?: number }> = []; - map.on("create", (data) => seen.push(data)); - - map.update({ userId: "user-2", audioSSRC: 1002, videoSSRC: 2002 }); - - expect(seen).toHaveLength(1); - expect(seen[0].videoSSRC).toBe(2002); - // videoReceiver.ts hooks this event to build videoSsrcToUser - expect(seen[0].userId).toBe("user-2"); - }); - - it("keeps audio-only behavior when no videoSSRC present", () => { - const map = new SSRCMap(); - map.update({ userId: "user-3", audioSSRC: 1003 }); - - expect(map.get(1003)?.userId).toBe("user-3"); - // No videoSSRC → get by a random SSRC returns undefined (no false match) - expect(map.get(2003)).toBeUndefined(); - }); -}); diff --git a/services/discord-gateway/tests/videoRecorder.test.ts b/services/discord-gateway/tests/videoRecorder.test.ts deleted file mode 100644 index df0996a6..00000000 --- a/services/discord-gateway/tests/videoRecorder.test.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import * as streamWatch from "../src/modules/voice-recording/streamWatchReceiver.js"; -import { - __resetVideoRecorderState, - setVideoRecorderClient, - setVideoRecordingsDir, - startVideoRecording, - stopVideoRecording, - trackChannel, - untrackChannel, -} from "../src/modules/voice-recording/videoRecorder.js"; - -// ─── mocks ───────────────────────────────────────────────────────────── -function makeChannel(guildId = "g1", channelId = "c1") { - return { - id: channelId, - guild: { - id: guildId, - members: { fetch: vi.fn().mockResolvedValue(new Map()) }, - }, - }; -} - -function makeClient() { - return { - user: { id: "bot1" }, - on: vi.fn(), - }; -} - -let seq = 0; -function makeUser() { - seq += 1; - return `user-${seq}`; -} - -beforeEach(() => { - __resetVideoRecorderState(); - vi.restoreAllMocks(); - setVideoRecordingsDir("/tmp/gmw-vidrec-test"); -}); - -describe("videoRecorder (stream-watch orchestration)", () => { - it("registers a voiceStateUpdate + raw listener on the client (idempotent)", () => { - const client = makeClient(); - setVideoRecorderClient(client); - setVideoRecorderClient(client); // idempotent (videoRecorder + streamWatch) - // one voiceStateUpdate (videoRecorder) + raw GUILD_CREATE (videoRecorder) - // + raw STREAM_* (streamWatch) = 3 - expect(client.on).toHaveBeenCalledTimes(3); - expect(client.on).toHaveBeenCalledWith( - "voiceStateUpdate", - expect.any(Function), - ); - expect(client.on).toHaveBeenCalledWith("raw", expect.any(Function)); - }); - - it("startVideoRecording delegates to streamWatchReceiver.startStreamWatch", () => { - const spy = vi - .spyOn(streamWatch, "startStreamWatch") - .mockImplementation(() => {}); - const ch = makeChannel(); - startVideoRecording(ch, makeUser()); - expect(spy).toHaveBeenCalledWith(ch, expect.any(String)); - }); - - it("refuses to record the bot's own video", () => { - setVideoRecorderClient(makeClient()); - const spy = vi - .spyOn(streamWatch, "startStreamWatch") - .mockImplementation(() => {}); - startVideoRecording(makeChannel(), "bot1"); - expect(spy).not.toHaveBeenCalled(); - }); - - it("stopVideoRecording calls streamWatchReceiver.stopStreamWatch", () => { - const spy = vi - .spyOn(streamWatch, "stopStreamWatch") - .mockImplementation(() => {}); - const u = makeUser(); - stopVideoRecording("g1", u); - expect(spy).toHaveBeenCalledWith("g1", u); - }); - - it("untrackChannel tears down all stream watches for the guild", () => { - const spy = vi - .spyOn(streamWatch, "stopAllStreamWatches") - .mockImplementation(() => {}); - untrackChannel("g1"); - expect(spy).toHaveBeenCalledWith("g1"); - }); - - it("the voiceStateUpdate handler starts a watch when a member starts streaming", () => { - const client = makeClient() as any; - setVideoRecorderClient(client); - trackChannel("g1", makeChannel()); - const startSpy = vi - .spyOn(streamWatch, "startStreamWatch") - .mockImplementation(() => {}); - const listener = client.on.mock.calls.find( - (c: unknown[]) => c[0] === "voiceStateUpdate", - )?.[1]; - listener(null, { - id: "u1", - guild: { id: "g1" }, - channelId: "c1", - streaming: true, - }); - expect(startSpy).toHaveBeenCalled(); - }); - - it("the voiceStateUpdate handler stops a watch when a member stops streaming", () => { - const client = makeClient() as any; - setVideoRecorderClient(client); - trackChannel("g1", makeChannel()); - const stopSpy = vi - .spyOn(streamWatch, "stopStreamWatch") - .mockImplementation(() => {}); - const listener = client.on.mock.calls.find( - (c: unknown[]) => c[0] === "voiceStateUpdate", - )?.[1]; - listener(null, { - id: "u1", - guild: { id: "g1" }, - channelId: "c1", - streaming: false, - }); - expect(stopSpy).toHaveBeenCalledWith("g1", "u1"); - }); - - it("trackChannel scans pre-existing streamers already in the channel (bot join case)", () => { - const client = makeClient() as any; - setVideoRecorderClient(client); - const spy = vi - .spyOn(streamWatch, "startStreamWatch") - .mockImplementation(() => {}); - const ch = { - id: "c1", - guild: { id: "g1" }, - members: new Map([ - ["u1", { id: "u1", voice: { streaming: true } }], // already streaming - ["u2", { id: "u2", voice: { streaming: false } }], // not streaming - ["bot1", { id: "bot1", voice: { streaming: true } }], // bot itself - ]), - }; - trackChannel("g1", ch as any); - // only the already-streaming non-bot member gets watched - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith(ch, "u1"); - }); - - it("trackChannel is a no-op when the channel has no members (or none streaming)", () => { - const spy = vi - .spyOn(streamWatch, "startStreamWatch") - .mockImplementation(() => {}); - trackChannel("g1", makeChannel()); // no members - expect(spy).not.toHaveBeenCalled(); - }); - - it("trackChannel falls back to REST guild.members.fetch() when cache is empty (cold start bug fix)", async () => { - const client = makeClient() as any; - setVideoRecorderClient(client); - const spy = vi - .spyOn(streamWatch, "startStreamWatch") - .mockImplementation(() => {}); - // Warmable guild: members.fetch() populates channel.members with a - // camera-on member (selfVideo) — the cold-start scenario where the bot - // joins a channel that already has someone on camera but the member cache - // hasn't been populated yet. - const cameraMember = { - id: "u-cam", - voice: { streaming: false, selfVideo: true }, - }; - const ch = { - id: "c1", - guild: { - id: "g1", - members: { - fetch: vi.fn().mockImplementation(async () => { - // Simulate REST fetch populating the channel member cache. - (ch as any).members = new Map([["u-cam", cameraMember]]); - return new Map([["u-cam", cameraMember]]); - }), - }, - }, - members: new Map(), // empty at join time — cold cache - }; - trackChannel("g1", ch as any); - // Allow the async scanExistingStreamers (fire-and-forget) to complete. - await vi.waitFor(() => { - expect(spy).toHaveBeenCalledWith(ch as any, "u-cam"); - }); - }); - - it("detects pre-existing camera user from buffered GUILD_CREATE.voice_states (selfbot fix)", async () => { - const client = makeClient() as any; - setVideoRecorderClient(client); - const spy = vi - .spyOn(streamWatch, "startStreamWatch") - .mockImplementation(() => {}); - const ch = { - id: "c1", - guild: { - id: "g1", - members: { fetch: vi.fn().mockResolvedValue(new Map()) }, - }, - members: new Map(), // empty — library drops voice_states - }; - // Simulate GUILD_CREATE arriving at WS connect (after setVideoRecorderClient - // registered the raw listener, before trackChannel runs). - const rawListener = client.on.mock.calls.find( - (c: unknown[]) => c[0] === "raw", - )?.[1]; - expect(rawListener).toBeDefined(); - rawListener({ - t: "GUILD_CREATE", - d: { - id: "g1", - voice_states: [ - { - user_id: "u-cam", - channel_id: "c1", - self_video: true, - self_stream: false, - }, - { - user_id: "u-share", - channel_id: "c1", - self_video: false, - self_stream: true, - }, - { - user_id: "u-novideo", - channel_id: "c1", - self_video: false, - self_stream: false, - }, - { - user_id: "bot1", - channel_id: "c1", - self_video: true, - self_stream: false, - }, // self — skip - { - user_id: "u-otherchan", - channel_id: "c9", - self_video: true, - self_stream: false, - }, // different channel — skip - ], - }, - }); - // Now track the channel — scanExistingStreamers consumes buffered states. - trackChannel("g1", ch as any); - await vi.waitFor(() => { - // camera user + screen-share user watched, self + no-video + other-channel skipped - expect(spy).toHaveBeenCalledWith(ch as any, "u-cam"); - expect(spy).toHaveBeenCalledWith(ch as any, "u-share"); - }); - expect(spy).not.toHaveBeenCalledWith(ch as any, "u-novideo"); - expect(spy).not.toHaveBeenCalledWith(ch as any, "bot1"); - expect(spy).not.toHaveBeenCalledWith(ch as any, "u-otherchan"); - }); -}); diff --git a/services/discord-gateway/vendor/discord-voice-fork/LICENSE b/services/discord-gateway/vendor/discord-voice-fork/LICENSE deleted file mode 100644 index 1675b6c1..00000000 --- a/services/discord-gateway/vendor/discord-voice-fork/LICENSE +++ /dev/null @@ -1,191 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - Copyright 2022 Noel Buechler - Copyright 2020 Amish Shah - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/services/discord-gateway/vendor/discord-voice-fork/README.md b/services/discord-gateway/vendor/discord-voice-fork/README.md deleted file mode 100644 index a7ec18a1..00000000 --- a/services/discord-gateway/vendor/discord-voice-fork/README.md +++ /dev/null @@ -1,120 +0,0 @@ -
-
-

- discord.js -

-
-

- Discord server - npm version - npm downloads - Build status - Last commit. - backers - Code coverage -

-

- Vercel - Cloudflare Workers -

-
- -## About - -`@discordjs/voice` is a TypeScript implementation of the Discord Voice API for Node.js. - -**Features:** - -- Send and receive\* audio in Discord voice-based channels -- A strong focus on reliability and predictable behavior -- Horizontal scalability and libraries other than [discord.js](https://discord.js.org/) are supported with custom adapters -- A robust audio processing system that can handle a wide range of audio sources - -\*_Audio receive is not documented by Discord so stable support is not guaranteed_ - -## Installation - -**Node.js 22.12.0 or newer is required.** - -```sh -npm install @discordjs/voice -yarn add @discordjs/voice -pnpm add @discordjs/voice -bun add @discordjs/voice -``` - -## Dependencies - -This library has several optional dependencies to support a variety -of different platforms. Install one dependency from each of the -categories shown below. The dependencies are listed in order of -preference for performance. If you can't install one of the options, -try installing another. - -**Encryption Libraries (npm install):** - -> [!NOTE] -> You only need to install one of these libraries if your system does not support `aes-256-gcm` (verify by running `require('node:crypto').getCiphers().includes('aes-256-gcm')`). - -- `sodium-native`: ^3.3.0 -- `sodium`: ^3.0.2 -- `@stablelib/xchacha20poly1305`: ^2.0.0 -- `@noble/ciphers`: ^1.0.0 -- `libsodium-wrappers`: ^0.7.9 - -**DAVE Protocol Libraries (e2ee)** - -> [!NOTE] -> At this time, `@snazzah/davey` is the only supported DAVE protocol library in this package, and comes pre-installed. In the future, we may support other libraries once they are created. - -- `@snazzah/davey`: ^0.1.6 - -**Opus Libraries (npm install):** - -- `@discordjs/opus`: ^0.4.0 -- `opusscript`: ^0.0.7 - -**FFmpeg:** - -- [`FFmpeg`](https://ffmpeg.org/) (installed and added to environment) -- `ffmpeg-static`: ^4.2.7 (npm install) - -## Examples - -The [voice-examples][voice-examples] repository contains examples on how to use this package. Feel free to check them out if you need a nudge in the right direction. - -## Links - -- [Website][website] ([source][website-source]) -- [Documentation][documentation] -- [Guide][guide] ([source][guide-source]) - Also see the v13 to v14 [Update Guide][guide-update], which includes updated and removed items from the library. -- [discord.js Discord server][discord] -- [Discord Developers Discord server][discord-developers] -- [GitHub][source] -- [npm][npm] -- [Related libraries][related-libs] - -## Contributing - -Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the -[documentation][documentation]. -See [the contribution guide][contributing] if you'd like to submit a PR. - -## Help - -If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle nudge in the right direction, please don't hesitate to join our official [discord.js Server][discord]. - -[website]: https://discord.js.org -[website-source]: https://github.com/discordjs/discord.js/tree/main/apps/website -[documentation]: https://discord.js.org/docs/packages/voice/stable -[guide]: https://discordjs.guide -[guide-source]: https://github.com/discordjs/discord.js/tree/main/apps/guide -[guide-update]: https://discordjs.guide/legacy/additional-info/changes-in-v14 -[discord]: https://discord.gg/djs -[discord-developers]: https://discord.gg/discord-developers -[source]: https://github.com/discordjs/discord.js/tree/main/packages/voice -[npm]: https://www.npmjs.com/package/@discordjs/voice -[related-libs]: https://discord.com/developers/docs/topics/community-resources#libraries -[contributing]: https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md -[voice-examples]: https://github.com/discordjs/voice-examples diff --git a/services/discord-gateway/vendor/discord-voice-fork/dist/index.d.mts b/services/discord-gateway/vendor/discord-voice-fork/dist/index.d.mts deleted file mode 100644 index 1d88f49d..00000000 --- a/services/discord-gateway/vendor/discord-voice-fork/dist/index.d.mts +++ /dev/null @@ -1,1930 +0,0 @@ -import { Buffer } from 'node:buffer'; -import { EventEmitter } from 'node:events'; -import { Readable, ReadableOptions } from 'node:stream'; -import prism from 'prism-media'; -import { VoiceDavePrepareTransitionData, VoiceDavePrepareEpochData, VoiceOpcodes, VoiceSendPayload, VoiceReceivePayload } from 'discord-api-types/voice/v8'; -import WebSocket, { MessageEvent } from 'ws'; -import { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v10'; - -/** - * The different types of stream that can exist within the pipeline. - */ -declare enum StreamType { - /** - * The type of the stream at this point is unknown. - */ - Arbitrary = "arbitrary", - /** - * The stream at this point is Opus audio encoded in an Ogg wrapper. - */ - OggOpus = "ogg/opus", - /** - * The stream at this point is Opus audio, and the stream is in object-mode. This is ready to play. - */ - Opus = "opus", - /** - * The stream at this point is s16le PCM. - */ - Raw = "raw", - /** - * The stream at this point is Opus audio encoded in a WebM wrapper. - */ - WebmOpus = "webm/opus" -} -/** - * The different types of transformers that can exist within the pipeline. - */ -declare enum TransformerType { - FFmpegOgg = "ffmpeg ogg", - FFmpegPCM = "ffmpeg pcm", - InlineVolume = "volume transformer", - OggOpusDemuxer = "ogg/opus demuxer", - OpusDecoder = "opus decoder", - OpusEncoder = "opus encoder", - WebmOpusDemuxer = "webm/opus demuxer" -} -/** - * Represents a pathway from one stream type to another using a transformer. - */ -interface Edge { - cost: number; - from: Node; - to: Node; - transformer(input: Readable | string): Readable; - type: TransformerType; -} -/** - * Represents a type of stream within the graph, e.g. an Opus stream, or a stream of raw audio. - */ -declare class Node { - /** - * The outbound edges from this node. - */ - readonly edges: Edge[]; - /** - * The type of stream for this node. - */ - readonly type: StreamType; - constructor(type: StreamType); - /** - * Creates an outbound edge from this node. - * - * @param edge - The edge to create - */ - addEdge(edge: Omit): void; -} - -/** - * Options that are set when creating a new audio resource. - * - * @typeParam Metadata - the type for the metadata (if any) of the audio resource - */ -interface CreateAudioResourceOptions { - /** - * Whether or not inline volume should be enabled. If enabled, you will be able to change the volume - * of the stream on-the-fly. However, this also increases the performance cost of playback. Defaults to `false`. - */ - inlineVolume?: boolean; - /** - * The type of the input stream. Defaults to `StreamType.Arbitrary`. - */ - inputType?: StreamType; - /** - * Optional metadata that can be attached to the resource (e.g. track title, random id). - * This is useful for identification purposes when the resource is passed around in events. - * See {@link AudioResource.metadata} - */ - metadata?: Metadata; - /** - * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches. - * Defaults to 5. - */ - silencePaddingFrames?: number; -} -/** - * Represents an audio resource that can be played by an audio player. - * - * @typeParam Metadata - the type for the metadata (if any) of the audio resource - */ -declare class AudioResource { - /** - * An object-mode Readable stream that emits Opus packets. This is what is played by audio players. - */ - readonly playStream: Readable; - /** - * The pipeline used to convert the input stream into a playable format. For example, this may - * contain an FFmpeg component for arbitrary inputs, and it may contain a VolumeTransformer component - * for resources with inline volume transformation enabled. - */ - readonly edges: readonly Edge[]; - /** - * Optional metadata that can be used to identify the resource. - */ - metadata: Metadata; - /** - * If the resource was created with inline volume transformation enabled, then this will be a - * prism-media VolumeTransformer. You can use this to alter the volume of the stream. - */ - readonly volume?: prism.VolumeTransformer; - /** - * If using an Opus encoder to create this audio resource, then this will be a prism-media opus.Encoder. - * You can use this to control settings such as bitrate, FEC, PLP. - */ - readonly encoder?: prism.opus.Encoder; - /** - * The audio player that the resource is subscribed to, if any. - */ - audioPlayer?: AudioPlayer | undefined; - /** - * The playback duration of this audio resource, given in milliseconds. - */ - playbackDuration: number; - /** - * Whether or not the stream for this resource has started (data has become readable) - */ - started: boolean; - /** - * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches. - */ - readonly silencePaddingFrames: number; - /** - * The number of remaining silence frames to play. If -1, the frames have not yet started playing. - */ - silenceRemaining: number; - constructor(edges: readonly Edge[], streams: readonly Readable[], metadata: Metadata, silencePaddingFrames: number); - /** - * Whether this resource is readable. If the underlying resource is no longer readable, this will still return true - * while there are silence padding frames left to play. - */ - get readable(): boolean; - /** - * Whether this resource has ended or not. - */ - get ended(): boolean; - /** - * Attempts to read an Opus packet from the audio resource. If a packet is available, the playbackDuration - * is incremented. - * - * @remarks - * It is advisable to check that the playStream is readable before calling this method. While no runtime - * errors will be thrown, you should check that the resource is still available before attempting to - * read from it. - * @internal - */ - read(): Buffer | null; -} -/** - * Creates an audio resource that can be played by audio players. - * - * @remarks - * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used. - * - * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created - * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg, - * Opus transcoders, and Ogg/WebM demuxers. - * @param input - The resource to play - * @param options - Configurable options for creating the resource - * @typeParam Metadata - the type for the metadata (if any) of the audio resource - */ -declare function createAudioResource(input: Readable | string, options: CreateAudioResourceOptions & Pick : Required>, 'metadata'>): AudioResource; -/** - * Creates an audio resource that can be played by audio players. - * - * @remarks - * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used. - * - * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created - * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg, - * Opus transcoders, and Ogg/WebM demuxers. - * @param input - The resource to play - * @param options - Configurable options for creating the resource - * @typeParam Metadata - the type for the metadata (if any) of the audio resource - */ -declare function createAudioResource(input: Readable | string, options?: Omit, 'metadata'>): AudioResource; - -/** - * An error emitted by an AudioPlayer. Contains an attached resource to aid with - * debugging and identifying where the error came from. - */ -declare class AudioPlayerError extends Error { - /** - * The resource associated with the audio player at the time the error was thrown. - */ - readonly resource: AudioResource; - constructor(error: Error, resource: AudioResource); -} - -/** - * Represents a subscription of a voice connection to an audio player, allowing - * the audio player to play audio on the voice connection. - */ -declare class PlayerSubscription { - /** - * The voice connection of this subscription. - */ - readonly connection: VoiceConnection; - /** - * The audio player of this subscription. - */ - readonly player: AudioPlayer; - constructor(connection: VoiceConnection, player: AudioPlayer); - /** - * Unsubscribes the connection from the audio player, meaning that the - * audio player cannot stream audio to it until a new subscription is made. - */ - unsubscribe(): void; -} - -/** - * Describes the behavior of the player when an audio packet is played but there are no available - * voice connections to play to. - */ -declare enum NoSubscriberBehavior { - /** - * Pauses playing the stream until a voice connection becomes available. - */ - Pause = "pause", - /** - * Continues to play through the resource regardless. - */ - Play = "play", - /** - * The player stops and enters the Idle state. - */ - Stop = "stop" -} -declare enum AudioPlayerStatus { - /** - * When the player has paused itself. Only possible with the "pause" no subscriber behavior. - */ - AutoPaused = "autopaused", - /** - * When the player is waiting for an audio resource to become readable before transitioning to Playing. - */ - Buffering = "buffering", - /** - * When there is currently no resource for the player to be playing. - */ - Idle = "idle", - /** - * When the player has been manually paused. - */ - Paused = "paused", - /** - * When the player is actively playing an audio resource. - */ - Playing = "playing" -} -/** - * Options that can be passed when creating an audio player, used to specify its behavior. - */ -interface CreateAudioPlayerOptions { - behaviors?: { - maxMissedFrames?: number; - noSubscriber?: NoSubscriberBehavior; - }; - debug?: boolean; -} -/** - * The state that an AudioPlayer is in when it has no resource to play. This is the starting state. - */ -interface AudioPlayerIdleState { - status: AudioPlayerStatus.Idle; -} -/** - * The state that an AudioPlayer is in when it is waiting for a resource to become readable. Once this - * happens, the AudioPlayer will enter the Playing state. If the resource ends/errors before this, then - * it will re-enter the Idle state. - */ -interface AudioPlayerBufferingState { - onFailureCallback: () => void; - onReadableCallback: () => void; - onStreamError: (error: Error) => void; - /** - * The resource that the AudioPlayer is waiting for - */ - resource: AudioResource; - status: AudioPlayerStatus.Buffering; -} -/** - * The state that an AudioPlayer is in when it is actively playing an AudioResource. When playback ends, - * it will enter the Idle state. - */ -interface AudioPlayerPlayingState { - /** - * The number of consecutive times that the audio resource has been unable to provide an Opus frame. - */ - missedFrames: number; - onStreamError: (error: Error) => void; - /** - * The playback duration in milliseconds of the current audio resource. This includes filler silence packets - * that have been played when the resource was buffering. - */ - playbackDuration: number; - /** - * The resource that is being played. - */ - resource: AudioResource; - status: AudioPlayerStatus.Playing; -} -/** - * The state that an AudioPlayer is in when it has either been explicitly paused by the user, or done - * automatically by the AudioPlayer itself if there are no available subscribers. - */ -interface AudioPlayerPausedState { - onStreamError: (error: Error) => void; - /** - * The playback duration in milliseconds of the current audio resource. This includes filler silence packets - * that have been played when the resource was buffering. - */ - playbackDuration: number; - /** - * The current resource of the audio player. - */ - resource: AudioResource; - /** - * How many silence packets still need to be played to avoid audio interpolation due to the stream suddenly pausing. - */ - silencePacketsRemaining: number; - status: AudioPlayerStatus.AutoPaused | AudioPlayerStatus.Paused; -} -/** - * The various states that the player can be in. - */ -type AudioPlayerState = AudioPlayerBufferingState | AudioPlayerIdleState | AudioPlayerPausedState | AudioPlayerPlayingState; -interface AudioPlayer extends EventEmitter { - /** - * Emitted when there is an error emitted from the audio resource played by the audio player - * - * @eventProperty - */ - on(event: 'error', listener: (error: AudioPlayerError) => void): this; - /** - * Emitted debugging information about the audio player - * - * @eventProperty - */ - on(event: 'debug', listener: (message: string) => void): this; - /** - * Emitted when the state of the audio player changes - * - * @eventProperty - */ - on(event: 'stateChange', listener: (oldState: AudioPlayerState, newState: AudioPlayerState) => void): this; - /** - * Emitted when the audio player is subscribed to a voice connection - * - * @eventProperty - */ - on(event: 'subscribe' | 'unsubscribe', listener: (subscription: PlayerSubscription) => void): this; - /** - * Emitted when the status of state changes to a specific status - * - * @eventProperty - */ - on(event: Event, listener: (oldState: AudioPlayerState, newState: AudioPlayerState & { - status: Event; - }) => void): this; -} -/** - * Used to play audio resources (i.e. tracks, streams) to voice connections. - * - * @remarks - * Audio players are designed to be re-used - even if a resource has finished playing, the player itself - * can still be used. - * - * The AudioPlayer drives the timing of playback, and therefore is unaffected by voice connections - * becoming unavailable. Its behavior in these scenarios can be configured. - */ -declare class AudioPlayer extends EventEmitter { - /** - * The state that the AudioPlayer is in. - */ - private _state; - /** - * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio - * to the streams in this list. - */ - private readonly subscribers; - /** - * The behavior that the player should follow when it enters certain situations. - */ - private readonly behaviors; - /** - * The debug logger function, if debugging is enabled. - */ - private readonly debug; - /** - * Creates a new AudioPlayer. - */ - constructor(options?: CreateAudioPlayerOptions); - /** - * A list of subscribed voice connections that can currently receive audio to play. - */ - get playable(): VoiceConnection[]; - /** - * 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 - */ - private subscribe; - /** - * 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 - */ - private unsubscribe; - /** - * The state that the player is in. - * - * @remarks - * The setter will perform clean-up operations where necessary. - */ - get state(): AudioPlayerState; - set state(newState: AudioPlayerState); - /** - * 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: AudioResource): void; - /** - * 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?: boolean): boolean; - /** - * Unpauses playback of the current resource, if any. - * - * @returns `true` if the player was successfully unpaused, otherwise `false` - */ - unpause(): boolean; - /** - * 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?: boolean): boolean; - /** - * Checks whether the underlying resource (if any) is playable (readable) - * - * @returns `true` if the resource is playable, otherwise `false` - */ - checkPlayable(): boolean; - /** - * 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. - */ - private _stepDispatch; - /** - * 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. - */ - private _stepPrepare; - /** - * 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. - */ - private _signalStopSpeaking; - /** - * 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 - */ - private _preparePacket; -} -/** - * Creates a new AudioPlayer to be used. - */ -declare function createAudioPlayer(options?: CreateAudioPlayerOptions): AudioPlayer; - -interface JoinConfig { - channelId: string | null; - group: string; - guildId: string; - selfDeaf: boolean; - selfMute: boolean; -} -/** - * Retrieves the map of group names to maps of voice connections. By default, all voice connections - * are created under the 'default' group. - * - * @returns The group map - */ -declare function getGroups(): Map>; -/** - * Retrieves all the voice connections under the 'default' group. - * - * @param group - The group to look up - * @returns The map of voice connections - */ -declare function getVoiceConnections(group?: 'default'): Map; -/** - * Retrieves all the voice connections under the given group name. - * - * @param group - The group to look up - * @returns The map of voice connections - */ -declare function getVoiceConnections(group: string): Map | undefined; -/** - * Finds a voice connection with the given guild id and group. Defaults to the 'default' group. - * - * @param guildId - The guild id of the voice connection - * @param group - the group that the voice connection was registered with - * @returns The voice connection, if it exists - */ -declare function getVoiceConnection(guildId: string, group?: string): VoiceConnection | undefined; - -interface SessionMethods { - canPassthrough(userId: string): boolean; - decrypt(userId: string, mediaType: 0 | 1, packet: Buffer): Buffer; - encryptOpus(packet: Buffer): Buffer; - getSerializedKeyPackage(): Buffer; - getVerificationCode(userId: string): Promise; - processCommit(commit: Buffer): void; - processProposals(optype: 0 | 1, proposals: Buffer, recognizedUserIds?: string[]): ProposalsResult; - processWelcome(welcome: Buffer): void; - ready: boolean; - reinit(protocolVersion: number, userId: string, channelId: string): void; - reset(): void; - setExternalSender(externalSender: Buffer): void; - setPassthroughMode(passthrough: boolean, expiry: number): void; - voicePrivacyCode: string; -} -interface ProposalsResult { - commit?: Buffer; - welcome?: Buffer; -} -interface TransitionResult { - success: boolean; - transitionId: number; -} -/** - * Options that dictate the session behavior. - */ -interface DAVESessionOptions { - decryptionFailureTolerance?: number | undefined; -} -interface DAVESession extends EventEmitter { - on(event: 'error', listener: (error: Error) => void): this; - on(event: 'debug', listener: (message: string) => void): this; - on(event: 'keyPackage', listener: (message: Buffer) => void): this; - on(event: 'invalidateTransition', listener: (transitionId: number) => void): this; -} -/** - * Manages the DAVE protocol group session. - */ -declare class DAVESession extends EventEmitter { - /** - * The channel id represented by this session. - */ - channelId: string; - /** - * The user id represented by this session. - */ - userId: string; - /** - * The protocol version being used. - */ - protocolVersion: number; - /** - * The last transition id executed. - */ - lastTransitionId?: number | undefined; - /** - * The pending transitions, mapped by their ID and the protocol version. - */ - private pendingTransitions; - /** - * Whether this session was downgraded previously. - */ - private downgraded; - /** - * The amount of consecutive failures encountered when decrypting. - */ - private consecutiveFailures; - /** - * The amount of consecutive failures needed to attempt to recover. - */ - private readonly failureTolerance; - /** - * Whether this session is currently re-initializing due to an invalid transition. - */ - reinitializing: boolean; - /** - * The underlying DAVE Session of this wrapper. - */ - session: SessionMethods | undefined; - constructor(protocolVersion: number, userId: string, channelId: string, options: DAVESessionOptions); - /** - * The current voice privacy code of the session. Will be `null` if there is no session. - */ - get voicePrivacyCode(): string | null; - /** - * 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. - */ - getVerificationCode(userId: string): Promise; - /** - * Re-initializes (or initializes) the underlying session. - */ - reinit(): void; - /** - * Set the external sender for this session. - * - * @param externalSender - The external sender - */ - setExternalSender(externalSender: Buffer): void; - /** - * Prepare for a transition. - * - * @param data - The transition data - * @returns Whether we should signal to the voice server that we are ready - */ - prepareTransition(data: VoiceDavePrepareTransitionData): boolean; - /** - * Execute a transition. - * - * @param transitionId - The transition id to execute on - */ - executeTransition(transitionId: number): boolean; - /** - * Prepare for a new epoch. - * - * @param data - The epoch data - */ - prepareEpoch(data: VoiceDavePrepareEpochData): void; - /** - * Recover from an invalid transition by re-initializing. - * - * @param transitionId - The transition id to invalidate - */ - recoverFromInvalidTransition(transitionId: number): void; - /** - * 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: Buffer, connectedClients: Set): Buffer | undefined; - /** - * Processes a commit from the MLS group. - * - * @param payload - The payload - * @returns The transaction id and whether it was successful - */ - processCommit(payload: Buffer): TransitionResult; - /** - * Processes a welcome from the MLS group. - * - * @param payload - The payload - * @returns The transaction id and whether it was successful - */ - processWelcome(payload: Buffer): TransitionResult; - /** - * Encrypt a packet using end-to-end encryption. - * - * @param packet - The packet to encrypt - */ - encrypt(packet: Buffer): Buffer; - /** - * 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: Buffer, userId: string): Buffer | null; - /** - * Resets the session. - */ - destroy(): void; -} - -/** - * Stores an IP address and port. Used to store socket details for the local client as well as - * for Discord. - */ -interface SocketConfig { - ip: string; - port: number; -} -interface VoiceUDPSocket extends EventEmitter { - on(event: 'error', listener: (error: Error) => void): this; - on(event: 'close', listener: () => void): this; - on(event: 'debug', listener: (message: string) => void): this; - on(event: 'message', listener: (message: Buffer) => void): this; -} -/** - * Manages the UDP networking for a voice connection. - */ -declare class VoiceUDPSocket extends EventEmitter { - /** - * The underlying network Socket for the VoiceUDPSocket. - */ - private readonly socket; - /** - * The socket details for Discord (remote) - */ - private readonly remote; - /** - * The counter used in the keep alive mechanism. - */ - private keepAliveCounter; - /** - * The buffer used to write the keep alive counter into. - */ - private readonly keepAliveBuffer; - /** - * The Node.js interval for the keep-alive mechanism. - */ - private readonly 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. - */ - ping?: number; - /** - * Creates a new VoiceUDPSocket. - * - * @param remote - Details of the remote socket - */ - constructor(remote: SocketConfig); - /** - * Called when a message is received on the UDP socket. - * - * @param buffer - The received buffer - */ - private onMessage; - /** - * Called at a regular interval to check whether we are still able to send datagrams to Discord. - */ - private keepAlive; - /** - * Sends a buffer to Discord. - * - * @param buffer - The buffer to send - */ - send(buffer: Buffer): void; - /** - * Closes the socket, the instance will not be able to be reused. - */ - destroy(): void; - /** - * Performs IP discovery to discover the local address and port to be used for the voice connection. - * - * @param ssrc - The SSRC received from Discord - */ - performIPDiscovery(ssrc: number): Promise; -} - -/** - * A binary WebSocket message. - */ -interface BinaryWebSocketMessage { - op: VoiceOpcodes; - payload: Buffer; - seq: number; -} -interface VoiceWebSocket extends EventEmitter { - on(event: 'error', listener: (error: Error) => void): this; - on(event: 'open', listener: (event: WebSocket.Event) => void): this; - on(event: 'close', listener: (event: WebSocket.CloseEvent) => void): this; - /** - * Debug event for VoiceWebSocket. - * - * @eventProperty - */ - on(event: 'debug', listener: (message: string) => void): this; - /** - * Packet event. - * - * @eventProperty - */ - on(event: 'packet', listener: (packet: any) => void): this; - /** - * Binary message event. - * - * @eventProperty - */ - on(event: 'binary', listener: (message: BinaryWebSocketMessage) => void): this; -} -/** - * An extension of the WebSocket class to provide helper functionality when interacting - * with the Discord Voice gateway. - */ -declare class VoiceWebSocket extends EventEmitter { - /** - * The current heartbeat interval, if any. - */ - private 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. - */ - private 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. - */ - private lastHeartbeatSend; - /** - * The number of consecutively missed heartbeats. - */ - private missedHeartbeats; - /** - * The last recorded ping. - */ - ping?: number; - /** - * The last sequence number acknowledged from Discord. Will be `-1` if no sequence numbered messages have been received. - */ - sequence: number; - /** - * The debug logger function, if debugging is enabled. - */ - private readonly debug; - /** - * The underlying WebSocket of this wrapper. - */ - private readonly ws; - /** - * Creates a new VoiceWebSocket. - * - * @param address - The address to connect to - */ - constructor(address: string, debug: boolean); - /** - * Destroys the VoiceWebSocket. The heartbeat interval is cleared, and the connection is closed. - */ - destroy(): void; - /** - * 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: MessageEvent): void; - /** - * Sends a JSON-stringifiable packet over the WebSocket. - * - * @param packet - The packet to send - */ - sendPacket(packet: VoiceSendPayload): void; - /** - * Sends a binary message over the WebSocket. - * - * @param opcode - The opcode to use - * @param payload - The payload to send - */ - sendBinaryMessage(opcode: VoiceOpcodes, payload: Buffer): void; - /** - * Sends a heartbeat over the WebSocket. - */ - private sendHeartbeat; - /** - * 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: number): void; -} - -/** - * 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.) - */ -declare enum NetworkingStatusCode { - OpeningWs = 0, - Identifying = 1, - UdpHandshaking = 2, - SelectingProtocol = 3, - Ready = 4, - Resuming = 5, - Closed = 6 -} -/** - * The initial Networking state. Instances will be in this state when a WebSocket connection to a Discord - * voice gateway is being opened. - */ -interface NetworkingOpeningWsState { - code: NetworkingStatusCode.OpeningWs; - connectionOptions: ConnectionOptions; - ws: VoiceWebSocket; -} -/** - * The state that a Networking instance will be in when it is attempting to authorize itself. - */ -interface NetworkingIdentifyingState { - code: NetworkingStatusCode.Identifying; - connectionOptions: ConnectionOptions; - ws: VoiceWebSocket; -} -/** - * 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. - */ -interface NetworkingUdpHandshakingState { - code: NetworkingStatusCode.UdpHandshaking; - connectionData: Pick; - connectionOptions: ConnectionOptions; - udp: VoiceUDPSocket; - ws: VoiceWebSocket; -} -/** - * The state that a Networking instance will be in when selecting an encryption protocol for audio packets. - */ -interface NetworkingSelectingProtocolState { - code: NetworkingStatusCode.SelectingProtocol; - connectionData: Pick; - connectionOptions: ConnectionOptions; - udp: VoiceUDPSocket; - ws: VoiceWebSocket; -} -/** - * The state that a Networking instance will be in when it has a fully established connection to a Discord - * voice server. - */ -interface NetworkingReadyState { - code: NetworkingStatusCode.Ready; - connectionData: ConnectionData; - connectionOptions: ConnectionOptions; - dave?: DAVESession | undefined; - preparedPacket?: Buffer | undefined; - udp: VoiceUDPSocket; - ws: VoiceWebSocket; -} -/** - * 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. - */ -interface NetworkingResumingState { - code: NetworkingStatusCode.Resuming; - connectionData: ConnectionData; - connectionOptions: ConnectionOptions; - dave?: DAVESession | undefined; - preparedPacket?: Buffer | undefined; - udp: VoiceUDPSocket; - ws: VoiceWebSocket; -} -/** - * The state that a Networking instance will be in when it has been destroyed. It cannot be recovered from this - * state. - */ -interface NetworkingClosedState { - code: NetworkingStatusCode.Closed; -} -/** - * The various states that a networking instance can be in. - */ -type NetworkingState = NetworkingClosedState | NetworkingIdentifyingState | NetworkingOpeningWsState | NetworkingReadyState | NetworkingResumingState | NetworkingSelectingProtocolState | NetworkingUdpHandshakingState; -/** - * 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 { - channelId: string; - endpoint: string; - serverId: string; - sessionId: string; - token: string; - userId: string; -} -/** - * Information about the current connection, e.g. which encryption mode is to be used on - * the connection, timing information for playback of streams. - */ -interface ConnectionData { - connectedClients: Set; - encryptionMode: string; - nonce: number; - nonceBuffer: Buffer; - packetsPlayed: number; - secretKey: Uint8Array; - sequence: number; - speaking: boolean; - ssrc: number; - timestamp: number; -} -/** - * Options for networking that dictate behavior. - */ -interface NetworkingOptions { - daveEncryption?: boolean | undefined; - debug?: boolean | undefined; - decryptionFailureTolerance?: number | undefined; -} -interface Networking extends EventEmitter { - /** - * Debug event for Networking. - * - * @eventProperty - */ - on(event: 'debug', listener: (message: string) => void): this; - on(event: 'error', listener: (error: Error) => void): this; - on(event: 'stateChange', listener: (oldState: NetworkingState, newState: NetworkingState) => void): this; - on(event: 'close', listener: (code: number) => void): this; - on(event: 'transitioned', listener: (transitionId: number) => void): this; -} -/** - * Manages the networking required to maintain a voice connection and dispatch audio packets - */ -declare class Networking extends EventEmitter { - private _state; - /** - * The debug logger function, if debugging is enabled. - */ - private readonly debug; - /** - * The options used to create this Networking instance. - */ - private readonly options; - /** - * Creates a new Networking instance. - */ - constructor(connectionOptions: ConnectionOptions, options: NetworkingOptions); - /** - * Destroys the Networking instance, transitioning it into the Closed state. - */ - destroy(): void; - /** - * The current state of the networking instance. - * - * @remarks - * The setter will perform clean-up operations where necessary. - */ - get state(): NetworkingState; - set state(newState: NetworkingState); - /** - * Creates a new WebSocket to a Discord Voice gateway. - * - * @param endpoint - The endpoint to connect to - * @param lastSequence - The last sequence to set for this WebSocket - */ - private createWebSocket; - /** - * Creates a new DAVE session for this voice connection if we can create one. - * - * @param protocolVersion - The protocol version to use - */ - private createDaveSession; - /** - * Propagates errors from the children VoiceWebSocket, VoiceUDPSocket and DAVESession. - * - * @param error - The error that was emitted by a child - */ - private onChildError; - /** - * 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; - /** - * Called when the WebSocket closes. Based on the reason for closing (given by the code parameter), - * the instance will either attempt to resume, or enter the closed state and emit a 'close' event - * with the close code, allowing the user to decide whether or not they would like to reconnect. - * - * @param code - The close code - */ - private onWsClose; - /** - * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord. - */ - private onUdpClose; - /** - * Called when a packet is received on the connection's WebSocket. - * - * @param packet - The received packet - */ - private onWsPacket; - /** - * Called when a binary message is received on the connection's WebSocket. - * - * @param message - The received message - */ - private onWsBinary; - /** - * Called when a new key package is ready to be sent to the voice server. - * - * @param keyPackage - The new key package - */ - private onDaveKeyPackage; - /** - * Called when the DAVE session wants to invalidate their transition and re-initialize. - * - * @param transitionId - The transition to invalidate - */ - private onDaveInvalidateTransition; - /** - * Propagates debug messages from the child WebSocket. - * - * @param message - The emitted debug message - */ - private onWsDebug; - /** - * Propagates debug messages from the child UDPSocket. - * - * @param message - The emitted debug message - */ - private onUdpDebug; - /** - * Propagates debug messages from the child DAVESession. - * - * @param message - The emitted debug message - */ - private onDaveDebug; - /** - * Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it. - * It will be stored within the instance, and can be played by dispatchAudio() - * - * @remarks - * Calling this method while there is already a prepared audio packet that has not yet been dispatched - * will overwrite the existing audio packet. This should be avoided. - * @param opusPacket - The Opus packet to encrypt - * @returns The audio packet that was prepared - */ - prepareAudioPacket(opusPacket: Buffer): Buffer | undefined; - /** - * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet - * is consumed and cannot be dispatched again. - */ - dispatchAudio(): boolean; - /** - * Plays an audio packet, updating timing metadata used for playback. - * - * @param audioPacket - The audio packet to play - */ - private playAudioPacket; - /** - * Sends a packet to the voice gateway indicating that the client has start/stopped sending - * audio. - * - * @param speaking - Whether or not the client should be shown as speaking - */ - setSpeaking(speaking: boolean): void; - /** - * Creates a new audio packet from an Opus packet. This involves encrypting the packet, - * then prepending a header that includes metadata. - * - * @param opusPacket - The Opus packet to prepare - * @param connectionData - The current connection data of the instance - * @param daveSession - The DAVE session to use for encryption - */ - private createAudioPacket; - /** - * Encrypts an Opus packet using the format agreed upon by the instance and Discord. - * - * @param opusPacket - The Opus packet to encrypt - * @param connectionData - The current connection data of the instance - * @param daveSession - The DAVE session to use for encryption - */ - private encryptOpusPacket; -} - -/** - * The different behaviors an audio receive stream can have for deciding when to end. - */ -declare enum EndBehaviorType { - /** - * The stream will only end when manually destroyed. - */ - Manual = 0, - /** - * The stream will end after a given time period of silence/no audio packets. - */ - AfterSilence = 1, - /** - * The stream will end after a given time period of no audio packets. - */ - AfterInactivity = 2 -} -type EndBehavior = { - behavior: EndBehaviorType.AfterInactivity | EndBehaviorType.AfterSilence; - duration: number; -} | { - behavior: EndBehaviorType.Manual; -}; -interface AudioReceiveStreamOptions extends ReadableOptions { - end: EndBehavior; -} -declare function createDefaultAudioReceiveStreamOptions(): AudioReceiveStreamOptions; -/** - * A readable stream of Opus packets received from a specific entity - * in a Discord voice connection. - */ -declare class AudioReceiveStream extends Readable { - /** - * The end behavior of the receive stream. - */ - readonly end: EndBehavior; - private endTimeout?; - constructor(options: AudioReceiveStreamOptions); - push(buffer: Buffer | null): boolean; - private renewEndTimeout; - _read(): void; -} - -/** - * The known data for a user in a Discord voice connection. - */ -interface VoiceUserData { - /** - * The SSRC of the user's audio stream. - */ - audioSSRC: number; - /** - * The Discord user id of the user. - */ - userId: string; - /** - * The SSRC of the user's video stream (if one exists) - * Cannot be 0. If undefined, the user has no video stream. - */ - videoSSRC?: number; -} -interface SSRCMap extends EventEmitter { - on(event: 'create', listener: (newData: VoiceUserData) => void): this; - on(event: 'update', listener: (oldData: VoiceUserData | undefined, newData: VoiceUserData) => void): this; - on(event: 'delete', listener: (deletedData: VoiceUserData) => void): this; -} -/** - * Maps audio SSRCs to data of users in voice connections. - */ -declare class SSRCMap extends EventEmitter { - /** - * The underlying map. - */ - private readonly map; - constructor(); - /** - * Updates the map with new user data - * - * @param data - The data to update with - */ - update(data: VoiceUserData): void; - /** - * Gets the stored voice data of a user. - * - * @param target - The target, either their user id or audio SSRC - */ - get(target: number | string): VoiceUserData | undefined; - /** - * Deletes the stored voice data about a user. - * - * @param target - The target of the delete operation, either their audio SSRC or user id - * @returns The data that was deleted, if any - */ - delete(target: number | string): VoiceUserData | undefined; -} - -interface SpeakingMap extends EventEmitter { - /** - * Emitted when a user starts speaking. - * - * @eventProperty - */ - on(event: 'start', listener: (userId: string) => void): this; - /** - * Emitted when a user ends speaking. - * - * @eventProperty - */ - on(event: 'end', listener: (userId: string) => void): this; -} -/** - * Tracks the speaking states of users in a voice channel. - */ -declare class SpeakingMap extends EventEmitter { - /** - * The delay after a packet is received from a user until they're marked as not speaking anymore. - */ - static readonly DELAY = 100; - /** - * The currently speaking users, mapped to the milliseconds since UNIX epoch at which they started speaking. - */ - readonly users: Map; - private readonly speakingTimeouts; - constructor(); - onPacket(userId: string): void; - private startTimeout; -} - -/** - * Attaches to a VoiceConnection, allowing you to receive audio packets from other - * users that are speaking. - * - * @beta - */ -declare class VoiceReceiver { - /** - * The attached connection of this receiver. - */ - readonly voiceConnection: VoiceConnection; - /** - * Maps SSRCs to Discord user ids. - */ - readonly ssrcMap: SSRCMap; - /** - * The current audio subscriptions of this receiver. - */ - readonly subscriptions: Map; - /** - * The connection data of the receiver. - * - * @internal - */ - connectionData: Partial; - /** - * The speaking map of the receiver. - */ - readonly speaking: SpeakingMap; - constructor(voiceConnection: VoiceConnection); - /** - * Called when a packet is received on the attached connection's WebSocket. - * - * @param packet - The received packet - * @internal - */ - onWsPacket(packet: VoiceReceivePayload): void; - private decrypt; - /** - * Parses an audio packet, decrypting it to yield an Opus packet. - * - * @param buffer - The buffer to parse - * @param mode - The encryption mode - * @param nonce - The nonce buffer used by the connection for encryption - * @param secretKey - The secret key used by the connection for encryption - * @param userId - The user id that sent the packet - * @returns The parsed Opus packet - */ - private parsePacket; - /** - * Called when the UDP socket of the attached connection receives a message. - * - * @param msg - The received message - * @internal - */ - onUdpMessage(msg: Buffer): void; - /** - * Creates a subscription for the given user id. - * - * @param target - The id of the user to subscribe to - * @returns A readable stream of Opus packets received from the target - */ - subscribe(userId: string, options?: Partial): AudioReceiveStream; -} - -/** - * Methods that are provided by the \@discordjs/voice library to implementations of - * Discord gateway DiscordGatewayAdapters. - */ -interface DiscordGatewayAdapterLibraryMethods { - /** - * Call this when the adapter can no longer be used (e.g. due to a disconnect from the main gateway) - */ - destroy(): void; - /** - * Call this when you receive a VOICE_SERVER_UPDATE payload that is relevant to the adapter. - * - * @param data - The inner data of the VOICE_SERVER_UPDATE payload - */ - onVoiceServerUpdate(data: GatewayVoiceServerUpdateDispatchData): void; - /** - * Call this when you receive a VOICE_STATE_UPDATE payload that is relevant to the adapter. - * - * @param data - The inner data of the VOICE_STATE_UPDATE payload - */ - onVoiceStateUpdate(data: GatewayVoiceStateUpdateDispatchData): void; -} -/** - * Methods that are provided by the implementer of a Discord gateway DiscordGatewayAdapter. - */ -interface DiscordGatewayAdapterImplementerMethods { - /** - * This will be called by \@discordjs/voice when the adapter can safely be destroyed as it will no - * longer be used. - */ - destroy(): void; - /** - * Implement this method such that the given payload is sent to the main Discord gateway connection. - * - * @param payload - The payload to send to the main Discord gateway connection - * @returns `false` if the payload definitely failed to send - in this case, the voice connection disconnects - */ - sendPayload(payload: any): boolean; -} -/** - * A function used to build adapters. It accepts a methods parameter that contains functions that - * can be called by the implementer when new data is received on its gateway connection. In return, - * the implementer will return some methods that the library can call - e.g. to send messages on - * the gateway, or to signal that the adapter can be removed. - */ -type DiscordGatewayAdapterCreator = (methods: DiscordGatewayAdapterLibraryMethods) => DiscordGatewayAdapterImplementerMethods; - -/** - * The various status codes a voice connection can hold at any one time. - */ -declare enum VoiceConnectionStatus { - /** - * The `VOICE_SERVER_UPDATE` and `VOICE_STATE_UPDATE` packets have been received, now attempting to establish a voice connection. - */ - Connecting = "connecting", - /** - * The voice connection has been destroyed and untracked, it cannot be reused. - */ - Destroyed = "destroyed", - /** - * The voice connection has either been severed or not established. - */ - Disconnected = "disconnected", - /** - * A voice connection has been established, and is ready to be used. - */ - Ready = "ready", - /** - * Sending a packet to the main Discord gateway to indicate we want to change our voice state. - */ - Signalling = "signalling" -} -/** - * The state that a VoiceConnection will be in when it is waiting to receive a VOICE_SERVER_UPDATE and - * VOICE_STATE_UPDATE packet from Discord, provided by the adapter. - */ -interface VoiceConnectionSignallingState { - adapter: DiscordGatewayAdapterImplementerMethods; - status: VoiceConnectionStatus.Signalling; - subscription?: PlayerSubscription | undefined; -} -/** - * The reasons a voice connection can be in the disconnected state. - */ -declare enum VoiceConnectionDisconnectReason { - /** - * When the WebSocket connection has been closed. - */ - WebSocketClose = 0, - /** - * When the adapter was unable to send a message requested by the VoiceConnection. - */ - AdapterUnavailable = 1, - /** - * When a VOICE_SERVER_UPDATE packet is received with a null endpoint, causing the connection to be severed. - */ - EndpointRemoved = 2, - /** - * When a manual disconnect was requested. - */ - Manual = 3 -} -/** - * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is - * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect. - */ -interface VoiceConnectionDisconnectedBaseState { - adapter: DiscordGatewayAdapterImplementerMethods; - status: VoiceConnectionStatus.Disconnected; - subscription?: PlayerSubscription | undefined; -} -/** - * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is - * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect. - */ -interface VoiceConnectionDisconnectedOtherState extends VoiceConnectionDisconnectedBaseState { - reason: Exclude; -} -/** - * The state that a VoiceConnection will be in when its WebSocket connection was closed. - * You can manually attempt to reconnect using VoiceConnection#reconnect. - */ -interface VoiceConnectionDisconnectedWebSocketState extends VoiceConnectionDisconnectedBaseState { - /** - * The close code of the WebSocket connection to the Discord voice server. - */ - closeCode: number; - reason: VoiceConnectionDisconnectReason.WebSocketClose; -} -/** - * The states that a VoiceConnection can be in when it is not connected to a Discord voice server nor is - * it attempting to connect. You can manually attempt to connect using VoiceConnection#reconnect. - */ -type VoiceConnectionDisconnectedState = VoiceConnectionDisconnectedOtherState | VoiceConnectionDisconnectedWebSocketState; -/** - * The state that a VoiceConnection will be in when it is establishing a connection to a Discord - * voice server. - */ -interface VoiceConnectionConnectingState { - adapter: DiscordGatewayAdapterImplementerMethods; - networking: Networking; - status: VoiceConnectionStatus.Connecting; - subscription?: PlayerSubscription | undefined; -} -/** - * The state that a VoiceConnection will be in when it has an active connection to a Discord - * voice server. - */ -interface VoiceConnectionReadyState { - adapter: DiscordGatewayAdapterImplementerMethods; - networking: Networking; - status: VoiceConnectionStatus.Ready; - subscription?: PlayerSubscription | undefined; -} -/** - * The state that a VoiceConnection will be in when it has been permanently been destroyed by the - * user and untracked by the library. It cannot be reconnected, instead, a new VoiceConnection - * needs to be established. - */ -interface VoiceConnectionDestroyedState { - status: VoiceConnectionStatus.Destroyed; -} -/** - * The various states that a voice connection can be in. - */ -type VoiceConnectionState = VoiceConnectionConnectingState | VoiceConnectionDestroyedState | VoiceConnectionDisconnectedState | VoiceConnectionReadyState | VoiceConnectionSignallingState; -interface VoiceConnection extends EventEmitter { - /** - * Emitted when there is an error emitted from the voice connection - * - * @eventProperty - */ - on(event: 'error', listener: (error: Error) => void): this; - /** - * Emitted debugging information about the voice connection - * - * @eventProperty - */ - on(event: 'debug', listener: (message: string) => void): this; - /** - * Emitted when the state of the voice connection changes - * - * @eventProperty - */ - on(event: 'stateChange', listener: (oldState: VoiceConnectionState, newState: VoiceConnectionState) => void): this; - /** - * Emitted when the end-to-end encrypted session has transitioned - * - * @eventProperty - */ - on(event: 'transitioned', listener: (transitionId: number) => void): this; - /** - * Emitted when the state of the voice connection changes to a specific status - * - * @eventProperty - */ - on(event: Event, listener: (oldState: VoiceConnectionState, newState: VoiceConnectionState & { - status: Event; - }) => void): this; -} -/** - * A connection to the voice server of a Guild, can be used to play audio in voice channels. - */ -declare class VoiceConnection extends EventEmitter { - /** - * The number of consecutive rejoin attempts. Initially 0, and increments for each rejoin. - * When a connection is successfully established, it resets to 0. - */ - rejoinAttempts: number; - /** - * The state of the voice connection. - */ - private _state; - /** - * A configuration storing all the data needed to reconnect to a Guild's voice server. - * - * @internal - */ - readonly joinConfig: JoinConfig; - /** - * The two packets needed to successfully establish a voice connection. They are received - * from the main Discord gateway after signalling to change the voice state. - */ - private readonly packets; - /** - * The receiver of this voice connection. You should join the voice channel with `selfDeaf` set - * to false for this feature to work properly. - */ - readonly receiver: VoiceReceiver; - /** - * The debug logger function, if debugging is enabled. - */ - private readonly debug; - /** - * The options used to create this voice connection. - */ - private readonly options; - /** - * Creates a new voice connection. - * - * @param joinConfig - The data required to establish the voice connection - * @param options - The options used to create this voice connection - */ - constructor(joinConfig: JoinConfig, options: CreateVoiceConnectionOptions); - /** - * The current state of the voice connection. - * - * @remarks - * The setter will perform clean-up operations where necessary. - */ - get state(): VoiceConnectionState; - set state(newState: VoiceConnectionState); - /** - * Registers a `VOICE_SERVER_UPDATE` packet to the voice connection. This will cause it to reconnect using the - * new data provided in the packet. - * - * @param packet - The received `VOICE_SERVER_UPDATE` packet - */ - private addServerPacket; - /** - * Registers a `VOICE_STATE_UPDATE` packet to the voice connection. Most importantly, it stores the id of the - * channel that the client is connected to. - * - * @param packet - The received `VOICE_STATE_UPDATE` packet - */ - private addStatePacket; - /** - * Called when the networking state changes, and the new ws/udp packet/message handlers need to be rebound - * to the new instances. - * - * @param newState - The new networking state - * @param oldState - The old networking state, if there is one - */ - private updateReceiveBindings; - /** - * Attempts to configure a networking instance for this voice connection using the received packets. - * Both packets are required, and any existing networking instance will be destroyed. - * - * @remarks - * This is called when the voice server of the connection changes, e.g. if the bot is moved into a - * different channel in the same guild but has a different voice server. In this instance, the connection - * needs to be re-established to the new voice server. - * - * The connection will transition to the Connecting state when this is called. - */ - configureNetworking(): void; - /** - * Called when the networking instance for this connection closes. If the close code is 4014 (do not reconnect), - * the voice connection will transition to the Disconnected state which will store the close code. You can - * decide whether or not to reconnect when this occurs by listening for the state change and calling reconnect(). - * - * @remarks - * If the close code was anything other than 4014, it is likely that the closing was not intended, and so the - * VoiceConnection will signal to Discord that it would like to rejoin the channel. This automatically attempts - * to re-establish the connection. This would be seen as a transition from the Ready state to the Signalling state. - * @param code - The close code - */ - private onNetworkingClose; - /** - * Called when the state of the networking instance changes. This is used to derive the state of the voice connection. - * - * @param oldState - The previous state - * @param newState - The new state - */ - private onNetworkingStateChange; - /** - * Propagates errors from the underlying network instance. - * - * @param error - The error to propagate - */ - private onNetworkingError; - /** - * Propagates debug messages from the underlying network instance. - * - * @param message - The debug message to propagate - */ - private onNetworkingDebug; - /** - * Propagates transitions from the underlying network instance. - * - * @param transitionId - The transition id - */ - private onNetworkingTransitioned; - /** - * Prepares an audio packet for dispatch. - * - * @param buffer - The Opus packet to prepare - */ - prepareAudioPacket(buffer: Buffer): Buffer | undefined; - /** - * Dispatches the previously prepared audio packet (if any) - */ - dispatchAudio(): boolean | undefined; - /** - * Prepares an audio packet and dispatches it immediately. - * - * @param buffer - The Opus packet to play - */ - playOpusPacket(buffer: Buffer): boolean | undefined; - /** - * Destroys the VoiceConnection, preventing it from connecting to voice again. - * This method should be called when you no longer require the VoiceConnection to - * prevent memory leaks. - * - * @param adapterAvailable - Whether the adapter can be used - */ - destroy(adapterAvailable?: boolean): void; - /** - * Disconnects the VoiceConnection, allowing the possibility of rejoining later on. - * - * @returns `true` if the connection was successfully disconnected - */ - disconnect(): boolean; - /** - * Attempts to rejoin (better explanation soon:tm:) - * - * @remarks - * Calling this method successfully will automatically increment the `rejoinAttempts` counter, - * which you can use to inform whether or not you'd like to keep attempting to reconnect your - * voice connection. - * - * A state transition from Disconnected to Signalling will be observed when this is called. - */ - rejoin(joinConfig?: Omit): boolean; - /** - * Updates the speaking status of the voice connection. This is used when audio players are done playing audio, - * and need to signal that the connection is no longer playing audio. - * - * @param enabled - Whether or not to show as speaking - */ - setSpeaking(enabled: boolean): false | void; - /** - * Subscribes to an audio player, allowing the player to play audio on this voice connection. - * - * @param player - The audio player to subscribe to - * @returns The created subscription - */ - subscribe(player: AudioPlayer): PlayerSubscription | undefined; - /** - * The latest ping (in milliseconds) for the WebSocket connection and audio playback for this voice - * connection, if this data is available. - * - * @remarks - * For this data to be available, the VoiceConnection must be in the Ready state, and its underlying - * WebSocket connection and UDP socket must have had at least one ping-pong exchange. - */ - get ping(): { - ws: number | undefined; - udp: number | undefined; - }; - /** - * The current voice privacy code of the encrypted session. - * - * @remarks - * For this data to be available, the VoiceConnection must be in the Ready state, - * and the connection would have to be end-to-end encrypted. - */ - get voicePrivacyCode(): string | undefined; - /** - * Gets the verification code for a user in the session. - * - * @throws Will throw if end-to-end encryption is not on or if the user id provided is not in the session. - */ - getVerificationCode(userId: string): Promise; - /** - * Called when a subscription of this voice connection to an audio player is removed. - * - * @param subscription - The removed subscription - */ - protected onSubscriptionRemoved(subscription: PlayerSubscription): void; -} - -/** - * The options that can be given when creating a voice connection. - */ -interface CreateVoiceConnectionOptions { - adapterCreator: DiscordGatewayAdapterCreator; - /** - * Whether to use the DAVE protocol for end-to-end encryption. Defaults to true. - */ - daveEncryption?: boolean | undefined; - /** - * If true, debug messages will be enabled for the voice connection and its - * related components. Defaults to false. - */ - debug?: boolean | undefined; - /** - * The amount of consecutive decryption failures needed to try to - * re-initialize the end-to-end encrypted session to recover. Defaults to 24. - */ - decryptionFailureTolerance?: number | undefined; -} -/** - * The options that can be given when joining a voice channel. - */ -interface JoinVoiceChannelOptions { - /** - * The id of the Discord voice channel to join. - */ - channelId: string; - /** - * An optional group identifier for the voice connection. - */ - group?: string; - /** - * The id of the guild that the voice channel belongs to. - */ - guildId: string; - /** - * Whether to join the channel deafened (defaults to true) - */ - selfDeaf?: boolean; - /** - * Whether to join the channel muted (defaults to true) - */ - selfMute?: boolean; -} -/** - * Creates a VoiceConnection to a Discord voice channel. - * - * @param options - the options for joining the voice channel - */ -declare function joinVoiceChannel(options: CreateVoiceConnectionOptions & JoinVoiceChannelOptions): VoiceConnection; - -declare const RTP_OPUS_PAYLOAD_TYPE = 120; - -/** - * Generates a report of the dependencies used by the \@discordjs/voice module. - * Useful for debugging. - */ -declare function generateDependencyReport(): string; - -/** - * Allows a voice connection a specified amount of time to enter a given state, otherwise rejects with an error. - * - * @param target - The voice connection that we want to observe the state change for - * @param status - The status that the voice connection should be in - * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation - */ -declare function entersState(target: VoiceConnection, status: VoiceConnectionStatus, timeoutOrSignal: AbortSignal | number): Promise; -/** - * Allows an audio player a specified amount of time to enter a given state, otherwise rejects with an error. - * - * @param target - The audio player that we want to observe the state change for - * @param status - The status that the audio player should be in - * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation - */ -declare function entersState(target: AudioPlayer, status: AudioPlayerStatus, timeoutOrSignal: AbortSignal | number): Promise; - -/** - * Takes an Opus Head, and verifies whether the associated Opus audio is suitable to play in a Discord voice channel. - * - * @param opusHead - The Opus Head to validate - * @returns `true` if suitable to play in a Discord voice channel, otherwise `false` - */ -declare function validateDiscordOpusHead(opusHead: Buffer): boolean; -/** - * The resulting information after probing an audio stream - */ -interface ProbeInfo { - /** - * The readable audio stream to use. You should use this rather than the input stream, as the probing - * function can sometimes read the input stream to its end and cause the stream to close. - */ - stream: Readable; - /** - * The recommended stream type for this audio stream. - */ - type: StreamType; -} -/** - * Attempt to probe a readable stream to figure out whether it can be demuxed using an Ogg or WebM Opus demuxer. - * - * @param stream - The readable stream to probe - * @param probeSize - The number of bytes to attempt to read before giving up on the probe - * @param validator - The Opus Head validator function - * @experimental - */ -declare function demuxProbe(stream: Readable, probeSize?: number, validator?: typeof validateDiscordOpusHead): Promise; - -/** - * The {@link https://github.com/discordjs/discord.js/blob/main/packages/voice#readme | @discordjs/voice} version - * that you are currently using. - */ -declare const version: string; - -export { AudioPlayer, type AudioPlayerBufferingState, AudioPlayerError, type AudioPlayerIdleState, type AudioPlayerPausedState, type AudioPlayerPlayingState, type AudioPlayerState, AudioPlayerStatus, AudioReceiveStream, type AudioReceiveStreamOptions, AudioResource, type ConnectionData, type ConnectionOptions, type CreateAudioPlayerOptions, type CreateAudioResourceOptions, type CreateVoiceConnectionOptions, DAVESession, type DiscordGatewayAdapterCreator, type DiscordGatewayAdapterImplementerMethods, type DiscordGatewayAdapterLibraryMethods, type Edge, type EndBehavior, EndBehaviorType, type JoinConfig, type JoinVoiceChannelOptions, Networking, type NetworkingClosedState, type NetworkingIdentifyingState, type NetworkingOpeningWsState, type NetworkingReadyState, type NetworkingResumingState, type NetworkingSelectingProtocolState, type NetworkingState, NetworkingStatusCode, type NetworkingUdpHandshakingState, NoSubscriberBehavior, Node, PlayerSubscription, type ProbeInfo, RTP_OPUS_PAYLOAD_TYPE, SSRCMap, type SocketConfig, SpeakingMap, StreamType, TransformerType, VoiceConnection, type VoiceConnectionConnectingState, type VoiceConnectionDestroyedState, VoiceConnectionDisconnectReason, type VoiceConnectionDisconnectedBaseState, type VoiceConnectionDisconnectedOtherState, type VoiceConnectionDisconnectedState, type VoiceConnectionDisconnectedWebSocketState, type VoiceConnectionReadyState, type VoiceConnectionSignallingState, type VoiceConnectionState, VoiceConnectionStatus, VoiceReceiver, VoiceUDPSocket, type VoiceUserData, VoiceWebSocket, createAudioPlayer, createAudioResource, createDefaultAudioReceiveStreamOptions, demuxProbe, entersState, generateDependencyReport, getGroups, getVoiceConnection, getVoiceConnections, joinVoiceChannel, validateDiscordOpusHead, version }; diff --git a/services/discord-gateway/vendor/discord-voice-fork/dist/index.d.ts b/services/discord-gateway/vendor/discord-voice-fork/dist/index.d.ts deleted file mode 100644 index 1d88f49d..00000000 --- a/services/discord-gateway/vendor/discord-voice-fork/dist/index.d.ts +++ /dev/null @@ -1,1930 +0,0 @@ -import { Buffer } from 'node:buffer'; -import { EventEmitter } from 'node:events'; -import { Readable, ReadableOptions } from 'node:stream'; -import prism from 'prism-media'; -import { VoiceDavePrepareTransitionData, VoiceDavePrepareEpochData, VoiceOpcodes, VoiceSendPayload, VoiceReceivePayload } from 'discord-api-types/voice/v8'; -import WebSocket, { MessageEvent } from 'ws'; -import { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v10'; - -/** - * The different types of stream that can exist within the pipeline. - */ -declare enum StreamType { - /** - * The type of the stream at this point is unknown. - */ - Arbitrary = "arbitrary", - /** - * The stream at this point is Opus audio encoded in an Ogg wrapper. - */ - OggOpus = "ogg/opus", - /** - * The stream at this point is Opus audio, and the stream is in object-mode. This is ready to play. - */ - Opus = "opus", - /** - * The stream at this point is s16le PCM. - */ - Raw = "raw", - /** - * The stream at this point is Opus audio encoded in a WebM wrapper. - */ - WebmOpus = "webm/opus" -} -/** - * The different types of transformers that can exist within the pipeline. - */ -declare enum TransformerType { - FFmpegOgg = "ffmpeg ogg", - FFmpegPCM = "ffmpeg pcm", - InlineVolume = "volume transformer", - OggOpusDemuxer = "ogg/opus demuxer", - OpusDecoder = "opus decoder", - OpusEncoder = "opus encoder", - WebmOpusDemuxer = "webm/opus demuxer" -} -/** - * Represents a pathway from one stream type to another using a transformer. - */ -interface Edge { - cost: number; - from: Node; - to: Node; - transformer(input: Readable | string): Readable; - type: TransformerType; -} -/** - * Represents a type of stream within the graph, e.g. an Opus stream, or a stream of raw audio. - */ -declare class Node { - /** - * The outbound edges from this node. - */ - readonly edges: Edge[]; - /** - * The type of stream for this node. - */ - readonly type: StreamType; - constructor(type: StreamType); - /** - * Creates an outbound edge from this node. - * - * @param edge - The edge to create - */ - addEdge(edge: Omit): void; -} - -/** - * Options that are set when creating a new audio resource. - * - * @typeParam Metadata - the type for the metadata (if any) of the audio resource - */ -interface CreateAudioResourceOptions { - /** - * Whether or not inline volume should be enabled. If enabled, you will be able to change the volume - * of the stream on-the-fly. However, this also increases the performance cost of playback. Defaults to `false`. - */ - inlineVolume?: boolean; - /** - * The type of the input stream. Defaults to `StreamType.Arbitrary`. - */ - inputType?: StreamType; - /** - * Optional metadata that can be attached to the resource (e.g. track title, random id). - * This is useful for identification purposes when the resource is passed around in events. - * See {@link AudioResource.metadata} - */ - metadata?: Metadata; - /** - * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches. - * Defaults to 5. - */ - silencePaddingFrames?: number; -} -/** - * Represents an audio resource that can be played by an audio player. - * - * @typeParam Metadata - the type for the metadata (if any) of the audio resource - */ -declare class AudioResource { - /** - * An object-mode Readable stream that emits Opus packets. This is what is played by audio players. - */ - readonly playStream: Readable; - /** - * The pipeline used to convert the input stream into a playable format. For example, this may - * contain an FFmpeg component for arbitrary inputs, and it may contain a VolumeTransformer component - * for resources with inline volume transformation enabled. - */ - readonly edges: readonly Edge[]; - /** - * Optional metadata that can be used to identify the resource. - */ - metadata: Metadata; - /** - * If the resource was created with inline volume transformation enabled, then this will be a - * prism-media VolumeTransformer. You can use this to alter the volume of the stream. - */ - readonly volume?: prism.VolumeTransformer; - /** - * If using an Opus encoder to create this audio resource, then this will be a prism-media opus.Encoder. - * You can use this to control settings such as bitrate, FEC, PLP. - */ - readonly encoder?: prism.opus.Encoder; - /** - * The audio player that the resource is subscribed to, if any. - */ - audioPlayer?: AudioPlayer | undefined; - /** - * The playback duration of this audio resource, given in milliseconds. - */ - playbackDuration: number; - /** - * Whether or not the stream for this resource has started (data has become readable) - */ - started: boolean; - /** - * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches. - */ - readonly silencePaddingFrames: number; - /** - * The number of remaining silence frames to play. If -1, the frames have not yet started playing. - */ - silenceRemaining: number; - constructor(edges: readonly Edge[], streams: readonly Readable[], metadata: Metadata, silencePaddingFrames: number); - /** - * Whether this resource is readable. If the underlying resource is no longer readable, this will still return true - * while there are silence padding frames left to play. - */ - get readable(): boolean; - /** - * Whether this resource has ended or not. - */ - get ended(): boolean; - /** - * Attempts to read an Opus packet from the audio resource. If a packet is available, the playbackDuration - * is incremented. - * - * @remarks - * It is advisable to check that the playStream is readable before calling this method. While no runtime - * errors will be thrown, you should check that the resource is still available before attempting to - * read from it. - * @internal - */ - read(): Buffer | null; -} -/** - * Creates an audio resource that can be played by audio players. - * - * @remarks - * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used. - * - * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created - * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg, - * Opus transcoders, and Ogg/WebM demuxers. - * @param input - The resource to play - * @param options - Configurable options for creating the resource - * @typeParam Metadata - the type for the metadata (if any) of the audio resource - */ -declare function createAudioResource(input: Readable | string, options: CreateAudioResourceOptions & Pick : Required>, 'metadata'>): AudioResource; -/** - * Creates an audio resource that can be played by audio players. - * - * @remarks - * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used. - * - * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created - * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg, - * Opus transcoders, and Ogg/WebM demuxers. - * @param input - The resource to play - * @param options - Configurable options for creating the resource - * @typeParam Metadata - the type for the metadata (if any) of the audio resource - */ -declare function createAudioResource(input: Readable | string, options?: Omit, 'metadata'>): AudioResource; - -/** - * An error emitted by an AudioPlayer. Contains an attached resource to aid with - * debugging and identifying where the error came from. - */ -declare class AudioPlayerError extends Error { - /** - * The resource associated with the audio player at the time the error was thrown. - */ - readonly resource: AudioResource; - constructor(error: Error, resource: AudioResource); -} - -/** - * Represents a subscription of a voice connection to an audio player, allowing - * the audio player to play audio on the voice connection. - */ -declare class PlayerSubscription { - /** - * The voice connection of this subscription. - */ - readonly connection: VoiceConnection; - /** - * The audio player of this subscription. - */ - readonly player: AudioPlayer; - constructor(connection: VoiceConnection, player: AudioPlayer); - /** - * Unsubscribes the connection from the audio player, meaning that the - * audio player cannot stream audio to it until a new subscription is made. - */ - unsubscribe(): void; -} - -/** - * Describes the behavior of the player when an audio packet is played but there are no available - * voice connections to play to. - */ -declare enum NoSubscriberBehavior { - /** - * Pauses playing the stream until a voice connection becomes available. - */ - Pause = "pause", - /** - * Continues to play through the resource regardless. - */ - Play = "play", - /** - * The player stops and enters the Idle state. - */ - Stop = "stop" -} -declare enum AudioPlayerStatus { - /** - * When the player has paused itself. Only possible with the "pause" no subscriber behavior. - */ - AutoPaused = "autopaused", - /** - * When the player is waiting for an audio resource to become readable before transitioning to Playing. - */ - Buffering = "buffering", - /** - * When there is currently no resource for the player to be playing. - */ - Idle = "idle", - /** - * When the player has been manually paused. - */ - Paused = "paused", - /** - * When the player is actively playing an audio resource. - */ - Playing = "playing" -} -/** - * Options that can be passed when creating an audio player, used to specify its behavior. - */ -interface CreateAudioPlayerOptions { - behaviors?: { - maxMissedFrames?: number; - noSubscriber?: NoSubscriberBehavior; - }; - debug?: boolean; -} -/** - * The state that an AudioPlayer is in when it has no resource to play. This is the starting state. - */ -interface AudioPlayerIdleState { - status: AudioPlayerStatus.Idle; -} -/** - * The state that an AudioPlayer is in when it is waiting for a resource to become readable. Once this - * happens, the AudioPlayer will enter the Playing state. If the resource ends/errors before this, then - * it will re-enter the Idle state. - */ -interface AudioPlayerBufferingState { - onFailureCallback: () => void; - onReadableCallback: () => void; - onStreamError: (error: Error) => void; - /** - * The resource that the AudioPlayer is waiting for - */ - resource: AudioResource; - status: AudioPlayerStatus.Buffering; -} -/** - * The state that an AudioPlayer is in when it is actively playing an AudioResource. When playback ends, - * it will enter the Idle state. - */ -interface AudioPlayerPlayingState { - /** - * The number of consecutive times that the audio resource has been unable to provide an Opus frame. - */ - missedFrames: number; - onStreamError: (error: Error) => void; - /** - * The playback duration in milliseconds of the current audio resource. This includes filler silence packets - * that have been played when the resource was buffering. - */ - playbackDuration: number; - /** - * The resource that is being played. - */ - resource: AudioResource; - status: AudioPlayerStatus.Playing; -} -/** - * The state that an AudioPlayer is in when it has either been explicitly paused by the user, or done - * automatically by the AudioPlayer itself if there are no available subscribers. - */ -interface AudioPlayerPausedState { - onStreamError: (error: Error) => void; - /** - * The playback duration in milliseconds of the current audio resource. This includes filler silence packets - * that have been played when the resource was buffering. - */ - playbackDuration: number; - /** - * The current resource of the audio player. - */ - resource: AudioResource; - /** - * How many silence packets still need to be played to avoid audio interpolation due to the stream suddenly pausing. - */ - silencePacketsRemaining: number; - status: AudioPlayerStatus.AutoPaused | AudioPlayerStatus.Paused; -} -/** - * The various states that the player can be in. - */ -type AudioPlayerState = AudioPlayerBufferingState | AudioPlayerIdleState | AudioPlayerPausedState | AudioPlayerPlayingState; -interface AudioPlayer extends EventEmitter { - /** - * Emitted when there is an error emitted from the audio resource played by the audio player - * - * @eventProperty - */ - on(event: 'error', listener: (error: AudioPlayerError) => void): this; - /** - * Emitted debugging information about the audio player - * - * @eventProperty - */ - on(event: 'debug', listener: (message: string) => void): this; - /** - * Emitted when the state of the audio player changes - * - * @eventProperty - */ - on(event: 'stateChange', listener: (oldState: AudioPlayerState, newState: AudioPlayerState) => void): this; - /** - * Emitted when the audio player is subscribed to a voice connection - * - * @eventProperty - */ - on(event: 'subscribe' | 'unsubscribe', listener: (subscription: PlayerSubscription) => void): this; - /** - * Emitted when the status of state changes to a specific status - * - * @eventProperty - */ - on(event: Event, listener: (oldState: AudioPlayerState, newState: AudioPlayerState & { - status: Event; - }) => void): this; -} -/** - * Used to play audio resources (i.e. tracks, streams) to voice connections. - * - * @remarks - * Audio players are designed to be re-used - even if a resource has finished playing, the player itself - * can still be used. - * - * The AudioPlayer drives the timing of playback, and therefore is unaffected by voice connections - * becoming unavailable. Its behavior in these scenarios can be configured. - */ -declare class AudioPlayer extends EventEmitter { - /** - * The state that the AudioPlayer is in. - */ - private _state; - /** - * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio - * to the streams in this list. - */ - private readonly subscribers; - /** - * The behavior that the player should follow when it enters certain situations. - */ - private readonly behaviors; - /** - * The debug logger function, if debugging is enabled. - */ - private readonly debug; - /** - * Creates a new AudioPlayer. - */ - constructor(options?: CreateAudioPlayerOptions); - /** - * A list of subscribed voice connections that can currently receive audio to play. - */ - get playable(): VoiceConnection[]; - /** - * 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 - */ - private subscribe; - /** - * 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 - */ - private unsubscribe; - /** - * The state that the player is in. - * - * @remarks - * The setter will perform clean-up operations where necessary. - */ - get state(): AudioPlayerState; - set state(newState: AudioPlayerState); - /** - * 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: AudioResource): void; - /** - * 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?: boolean): boolean; - /** - * Unpauses playback of the current resource, if any. - * - * @returns `true` if the player was successfully unpaused, otherwise `false` - */ - unpause(): boolean; - /** - * 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?: boolean): boolean; - /** - * Checks whether the underlying resource (if any) is playable (readable) - * - * @returns `true` if the resource is playable, otherwise `false` - */ - checkPlayable(): boolean; - /** - * 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. - */ - private _stepDispatch; - /** - * 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. - */ - private _stepPrepare; - /** - * 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. - */ - private _signalStopSpeaking; - /** - * 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 - */ - private _preparePacket; -} -/** - * Creates a new AudioPlayer to be used. - */ -declare function createAudioPlayer(options?: CreateAudioPlayerOptions): AudioPlayer; - -interface JoinConfig { - channelId: string | null; - group: string; - guildId: string; - selfDeaf: boolean; - selfMute: boolean; -} -/** - * Retrieves the map of group names to maps of voice connections. By default, all voice connections - * are created under the 'default' group. - * - * @returns The group map - */ -declare function getGroups(): Map>; -/** - * Retrieves all the voice connections under the 'default' group. - * - * @param group - The group to look up - * @returns The map of voice connections - */ -declare function getVoiceConnections(group?: 'default'): Map; -/** - * Retrieves all the voice connections under the given group name. - * - * @param group - The group to look up - * @returns The map of voice connections - */ -declare function getVoiceConnections(group: string): Map | undefined; -/** - * Finds a voice connection with the given guild id and group. Defaults to the 'default' group. - * - * @param guildId - The guild id of the voice connection - * @param group - the group that the voice connection was registered with - * @returns The voice connection, if it exists - */ -declare function getVoiceConnection(guildId: string, group?: string): VoiceConnection | undefined; - -interface SessionMethods { - canPassthrough(userId: string): boolean; - decrypt(userId: string, mediaType: 0 | 1, packet: Buffer): Buffer; - encryptOpus(packet: Buffer): Buffer; - getSerializedKeyPackage(): Buffer; - getVerificationCode(userId: string): Promise; - processCommit(commit: Buffer): void; - processProposals(optype: 0 | 1, proposals: Buffer, recognizedUserIds?: string[]): ProposalsResult; - processWelcome(welcome: Buffer): void; - ready: boolean; - reinit(protocolVersion: number, userId: string, channelId: string): void; - reset(): void; - setExternalSender(externalSender: Buffer): void; - setPassthroughMode(passthrough: boolean, expiry: number): void; - voicePrivacyCode: string; -} -interface ProposalsResult { - commit?: Buffer; - welcome?: Buffer; -} -interface TransitionResult { - success: boolean; - transitionId: number; -} -/** - * Options that dictate the session behavior. - */ -interface DAVESessionOptions { - decryptionFailureTolerance?: number | undefined; -} -interface DAVESession extends EventEmitter { - on(event: 'error', listener: (error: Error) => void): this; - on(event: 'debug', listener: (message: string) => void): this; - on(event: 'keyPackage', listener: (message: Buffer) => void): this; - on(event: 'invalidateTransition', listener: (transitionId: number) => void): this; -} -/** - * Manages the DAVE protocol group session. - */ -declare class DAVESession extends EventEmitter { - /** - * The channel id represented by this session. - */ - channelId: string; - /** - * The user id represented by this session. - */ - userId: string; - /** - * The protocol version being used. - */ - protocolVersion: number; - /** - * The last transition id executed. - */ - lastTransitionId?: number | undefined; - /** - * The pending transitions, mapped by their ID and the protocol version. - */ - private pendingTransitions; - /** - * Whether this session was downgraded previously. - */ - private downgraded; - /** - * The amount of consecutive failures encountered when decrypting. - */ - private consecutiveFailures; - /** - * The amount of consecutive failures needed to attempt to recover. - */ - private readonly failureTolerance; - /** - * Whether this session is currently re-initializing due to an invalid transition. - */ - reinitializing: boolean; - /** - * The underlying DAVE Session of this wrapper. - */ - session: SessionMethods | undefined; - constructor(protocolVersion: number, userId: string, channelId: string, options: DAVESessionOptions); - /** - * The current voice privacy code of the session. Will be `null` if there is no session. - */ - get voicePrivacyCode(): string | null; - /** - * 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. - */ - getVerificationCode(userId: string): Promise; - /** - * Re-initializes (or initializes) the underlying session. - */ - reinit(): void; - /** - * Set the external sender for this session. - * - * @param externalSender - The external sender - */ - setExternalSender(externalSender: Buffer): void; - /** - * Prepare for a transition. - * - * @param data - The transition data - * @returns Whether we should signal to the voice server that we are ready - */ - prepareTransition(data: VoiceDavePrepareTransitionData): boolean; - /** - * Execute a transition. - * - * @param transitionId - The transition id to execute on - */ - executeTransition(transitionId: number): boolean; - /** - * Prepare for a new epoch. - * - * @param data - The epoch data - */ - prepareEpoch(data: VoiceDavePrepareEpochData): void; - /** - * Recover from an invalid transition by re-initializing. - * - * @param transitionId - The transition id to invalidate - */ - recoverFromInvalidTransition(transitionId: number): void; - /** - * 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: Buffer, connectedClients: Set): Buffer | undefined; - /** - * Processes a commit from the MLS group. - * - * @param payload - The payload - * @returns The transaction id and whether it was successful - */ - processCommit(payload: Buffer): TransitionResult; - /** - * Processes a welcome from the MLS group. - * - * @param payload - The payload - * @returns The transaction id and whether it was successful - */ - processWelcome(payload: Buffer): TransitionResult; - /** - * Encrypt a packet using end-to-end encryption. - * - * @param packet - The packet to encrypt - */ - encrypt(packet: Buffer): Buffer; - /** - * 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: Buffer, userId: string): Buffer | null; - /** - * Resets the session. - */ - destroy(): void; -} - -/** - * Stores an IP address and port. Used to store socket details for the local client as well as - * for Discord. - */ -interface SocketConfig { - ip: string; - port: number; -} -interface VoiceUDPSocket extends EventEmitter { - on(event: 'error', listener: (error: Error) => void): this; - on(event: 'close', listener: () => void): this; - on(event: 'debug', listener: (message: string) => void): this; - on(event: 'message', listener: (message: Buffer) => void): this; -} -/** - * Manages the UDP networking for a voice connection. - */ -declare class VoiceUDPSocket extends EventEmitter { - /** - * The underlying network Socket for the VoiceUDPSocket. - */ - private readonly socket; - /** - * The socket details for Discord (remote) - */ - private readonly remote; - /** - * The counter used in the keep alive mechanism. - */ - private keepAliveCounter; - /** - * The buffer used to write the keep alive counter into. - */ - private readonly keepAliveBuffer; - /** - * The Node.js interval for the keep-alive mechanism. - */ - private readonly 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. - */ - ping?: number; - /** - * Creates a new VoiceUDPSocket. - * - * @param remote - Details of the remote socket - */ - constructor(remote: SocketConfig); - /** - * Called when a message is received on the UDP socket. - * - * @param buffer - The received buffer - */ - private onMessage; - /** - * Called at a regular interval to check whether we are still able to send datagrams to Discord. - */ - private keepAlive; - /** - * Sends a buffer to Discord. - * - * @param buffer - The buffer to send - */ - send(buffer: Buffer): void; - /** - * Closes the socket, the instance will not be able to be reused. - */ - destroy(): void; - /** - * Performs IP discovery to discover the local address and port to be used for the voice connection. - * - * @param ssrc - The SSRC received from Discord - */ - performIPDiscovery(ssrc: number): Promise; -} - -/** - * A binary WebSocket message. - */ -interface BinaryWebSocketMessage { - op: VoiceOpcodes; - payload: Buffer; - seq: number; -} -interface VoiceWebSocket extends EventEmitter { - on(event: 'error', listener: (error: Error) => void): this; - on(event: 'open', listener: (event: WebSocket.Event) => void): this; - on(event: 'close', listener: (event: WebSocket.CloseEvent) => void): this; - /** - * Debug event for VoiceWebSocket. - * - * @eventProperty - */ - on(event: 'debug', listener: (message: string) => void): this; - /** - * Packet event. - * - * @eventProperty - */ - on(event: 'packet', listener: (packet: any) => void): this; - /** - * Binary message event. - * - * @eventProperty - */ - on(event: 'binary', listener: (message: BinaryWebSocketMessage) => void): this; -} -/** - * An extension of the WebSocket class to provide helper functionality when interacting - * with the Discord Voice gateway. - */ -declare class VoiceWebSocket extends EventEmitter { - /** - * The current heartbeat interval, if any. - */ - private 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. - */ - private 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. - */ - private lastHeartbeatSend; - /** - * The number of consecutively missed heartbeats. - */ - private missedHeartbeats; - /** - * The last recorded ping. - */ - ping?: number; - /** - * The last sequence number acknowledged from Discord. Will be `-1` if no sequence numbered messages have been received. - */ - sequence: number; - /** - * The debug logger function, if debugging is enabled. - */ - private readonly debug; - /** - * The underlying WebSocket of this wrapper. - */ - private readonly ws; - /** - * Creates a new VoiceWebSocket. - * - * @param address - The address to connect to - */ - constructor(address: string, debug: boolean); - /** - * Destroys the VoiceWebSocket. The heartbeat interval is cleared, and the connection is closed. - */ - destroy(): void; - /** - * 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: MessageEvent): void; - /** - * Sends a JSON-stringifiable packet over the WebSocket. - * - * @param packet - The packet to send - */ - sendPacket(packet: VoiceSendPayload): void; - /** - * Sends a binary message over the WebSocket. - * - * @param opcode - The opcode to use - * @param payload - The payload to send - */ - sendBinaryMessage(opcode: VoiceOpcodes, payload: Buffer): void; - /** - * Sends a heartbeat over the WebSocket. - */ - private sendHeartbeat; - /** - * 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: number): void; -} - -/** - * 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.) - */ -declare enum NetworkingStatusCode { - OpeningWs = 0, - Identifying = 1, - UdpHandshaking = 2, - SelectingProtocol = 3, - Ready = 4, - Resuming = 5, - Closed = 6 -} -/** - * The initial Networking state. Instances will be in this state when a WebSocket connection to a Discord - * voice gateway is being opened. - */ -interface NetworkingOpeningWsState { - code: NetworkingStatusCode.OpeningWs; - connectionOptions: ConnectionOptions; - ws: VoiceWebSocket; -} -/** - * The state that a Networking instance will be in when it is attempting to authorize itself. - */ -interface NetworkingIdentifyingState { - code: NetworkingStatusCode.Identifying; - connectionOptions: ConnectionOptions; - ws: VoiceWebSocket; -} -/** - * 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. - */ -interface NetworkingUdpHandshakingState { - code: NetworkingStatusCode.UdpHandshaking; - connectionData: Pick; - connectionOptions: ConnectionOptions; - udp: VoiceUDPSocket; - ws: VoiceWebSocket; -} -/** - * The state that a Networking instance will be in when selecting an encryption protocol for audio packets. - */ -interface NetworkingSelectingProtocolState { - code: NetworkingStatusCode.SelectingProtocol; - connectionData: Pick; - connectionOptions: ConnectionOptions; - udp: VoiceUDPSocket; - ws: VoiceWebSocket; -} -/** - * The state that a Networking instance will be in when it has a fully established connection to a Discord - * voice server. - */ -interface NetworkingReadyState { - code: NetworkingStatusCode.Ready; - connectionData: ConnectionData; - connectionOptions: ConnectionOptions; - dave?: DAVESession | undefined; - preparedPacket?: Buffer | undefined; - udp: VoiceUDPSocket; - ws: VoiceWebSocket; -} -/** - * 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. - */ -interface NetworkingResumingState { - code: NetworkingStatusCode.Resuming; - connectionData: ConnectionData; - connectionOptions: ConnectionOptions; - dave?: DAVESession | undefined; - preparedPacket?: Buffer | undefined; - udp: VoiceUDPSocket; - ws: VoiceWebSocket; -} -/** - * The state that a Networking instance will be in when it has been destroyed. It cannot be recovered from this - * state. - */ -interface NetworkingClosedState { - code: NetworkingStatusCode.Closed; -} -/** - * The various states that a networking instance can be in. - */ -type NetworkingState = NetworkingClosedState | NetworkingIdentifyingState | NetworkingOpeningWsState | NetworkingReadyState | NetworkingResumingState | NetworkingSelectingProtocolState | NetworkingUdpHandshakingState; -/** - * 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 { - channelId: string; - endpoint: string; - serverId: string; - sessionId: string; - token: string; - userId: string; -} -/** - * Information about the current connection, e.g. which encryption mode is to be used on - * the connection, timing information for playback of streams. - */ -interface ConnectionData { - connectedClients: Set; - encryptionMode: string; - nonce: number; - nonceBuffer: Buffer; - packetsPlayed: number; - secretKey: Uint8Array; - sequence: number; - speaking: boolean; - ssrc: number; - timestamp: number; -} -/** - * Options for networking that dictate behavior. - */ -interface NetworkingOptions { - daveEncryption?: boolean | undefined; - debug?: boolean | undefined; - decryptionFailureTolerance?: number | undefined; -} -interface Networking extends EventEmitter { - /** - * Debug event for Networking. - * - * @eventProperty - */ - on(event: 'debug', listener: (message: string) => void): this; - on(event: 'error', listener: (error: Error) => void): this; - on(event: 'stateChange', listener: (oldState: NetworkingState, newState: NetworkingState) => void): this; - on(event: 'close', listener: (code: number) => void): this; - on(event: 'transitioned', listener: (transitionId: number) => void): this; -} -/** - * Manages the networking required to maintain a voice connection and dispatch audio packets - */ -declare class Networking extends EventEmitter { - private _state; - /** - * The debug logger function, if debugging is enabled. - */ - private readonly debug; - /** - * The options used to create this Networking instance. - */ - private readonly options; - /** - * Creates a new Networking instance. - */ - constructor(connectionOptions: ConnectionOptions, options: NetworkingOptions); - /** - * Destroys the Networking instance, transitioning it into the Closed state. - */ - destroy(): void; - /** - * The current state of the networking instance. - * - * @remarks - * The setter will perform clean-up operations where necessary. - */ - get state(): NetworkingState; - set state(newState: NetworkingState); - /** - * Creates a new WebSocket to a Discord Voice gateway. - * - * @param endpoint - The endpoint to connect to - * @param lastSequence - The last sequence to set for this WebSocket - */ - private createWebSocket; - /** - * Creates a new DAVE session for this voice connection if we can create one. - * - * @param protocolVersion - The protocol version to use - */ - private createDaveSession; - /** - * Propagates errors from the children VoiceWebSocket, VoiceUDPSocket and DAVESession. - * - * @param error - The error that was emitted by a child - */ - private onChildError; - /** - * 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; - /** - * Called when the WebSocket closes. Based on the reason for closing (given by the code parameter), - * the instance will either attempt to resume, or enter the closed state and emit a 'close' event - * with the close code, allowing the user to decide whether or not they would like to reconnect. - * - * @param code - The close code - */ - private onWsClose; - /** - * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord. - */ - private onUdpClose; - /** - * Called when a packet is received on the connection's WebSocket. - * - * @param packet - The received packet - */ - private onWsPacket; - /** - * Called when a binary message is received on the connection's WebSocket. - * - * @param message - The received message - */ - private onWsBinary; - /** - * Called when a new key package is ready to be sent to the voice server. - * - * @param keyPackage - The new key package - */ - private onDaveKeyPackage; - /** - * Called when the DAVE session wants to invalidate their transition and re-initialize. - * - * @param transitionId - The transition to invalidate - */ - private onDaveInvalidateTransition; - /** - * Propagates debug messages from the child WebSocket. - * - * @param message - The emitted debug message - */ - private onWsDebug; - /** - * Propagates debug messages from the child UDPSocket. - * - * @param message - The emitted debug message - */ - private onUdpDebug; - /** - * Propagates debug messages from the child DAVESession. - * - * @param message - The emitted debug message - */ - private onDaveDebug; - /** - * Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it. - * It will be stored within the instance, and can be played by dispatchAudio() - * - * @remarks - * Calling this method while there is already a prepared audio packet that has not yet been dispatched - * will overwrite the existing audio packet. This should be avoided. - * @param opusPacket - The Opus packet to encrypt - * @returns The audio packet that was prepared - */ - prepareAudioPacket(opusPacket: Buffer): Buffer | undefined; - /** - * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet - * is consumed and cannot be dispatched again. - */ - dispatchAudio(): boolean; - /** - * Plays an audio packet, updating timing metadata used for playback. - * - * @param audioPacket - The audio packet to play - */ - private playAudioPacket; - /** - * Sends a packet to the voice gateway indicating that the client has start/stopped sending - * audio. - * - * @param speaking - Whether or not the client should be shown as speaking - */ - setSpeaking(speaking: boolean): void; - /** - * Creates a new audio packet from an Opus packet. This involves encrypting the packet, - * then prepending a header that includes metadata. - * - * @param opusPacket - The Opus packet to prepare - * @param connectionData - The current connection data of the instance - * @param daveSession - The DAVE session to use for encryption - */ - private createAudioPacket; - /** - * Encrypts an Opus packet using the format agreed upon by the instance and Discord. - * - * @param opusPacket - The Opus packet to encrypt - * @param connectionData - The current connection data of the instance - * @param daveSession - The DAVE session to use for encryption - */ - private encryptOpusPacket; -} - -/** - * The different behaviors an audio receive stream can have for deciding when to end. - */ -declare enum EndBehaviorType { - /** - * The stream will only end when manually destroyed. - */ - Manual = 0, - /** - * The stream will end after a given time period of silence/no audio packets. - */ - AfterSilence = 1, - /** - * The stream will end after a given time period of no audio packets. - */ - AfterInactivity = 2 -} -type EndBehavior = { - behavior: EndBehaviorType.AfterInactivity | EndBehaviorType.AfterSilence; - duration: number; -} | { - behavior: EndBehaviorType.Manual; -}; -interface AudioReceiveStreamOptions extends ReadableOptions { - end: EndBehavior; -} -declare function createDefaultAudioReceiveStreamOptions(): AudioReceiveStreamOptions; -/** - * A readable stream of Opus packets received from a specific entity - * in a Discord voice connection. - */ -declare class AudioReceiveStream extends Readable { - /** - * The end behavior of the receive stream. - */ - readonly end: EndBehavior; - private endTimeout?; - constructor(options: AudioReceiveStreamOptions); - push(buffer: Buffer | null): boolean; - private renewEndTimeout; - _read(): void; -} - -/** - * The known data for a user in a Discord voice connection. - */ -interface VoiceUserData { - /** - * The SSRC of the user's audio stream. - */ - audioSSRC: number; - /** - * The Discord user id of the user. - */ - userId: string; - /** - * The SSRC of the user's video stream (if one exists) - * Cannot be 0. If undefined, the user has no video stream. - */ - videoSSRC?: number; -} -interface SSRCMap extends EventEmitter { - on(event: 'create', listener: (newData: VoiceUserData) => void): this; - on(event: 'update', listener: (oldData: VoiceUserData | undefined, newData: VoiceUserData) => void): this; - on(event: 'delete', listener: (deletedData: VoiceUserData) => void): this; -} -/** - * Maps audio SSRCs to data of users in voice connections. - */ -declare class SSRCMap extends EventEmitter { - /** - * The underlying map. - */ - private readonly map; - constructor(); - /** - * Updates the map with new user data - * - * @param data - The data to update with - */ - update(data: VoiceUserData): void; - /** - * Gets the stored voice data of a user. - * - * @param target - The target, either their user id or audio SSRC - */ - get(target: number | string): VoiceUserData | undefined; - /** - * Deletes the stored voice data about a user. - * - * @param target - The target of the delete operation, either their audio SSRC or user id - * @returns The data that was deleted, if any - */ - delete(target: number | string): VoiceUserData | undefined; -} - -interface SpeakingMap extends EventEmitter { - /** - * Emitted when a user starts speaking. - * - * @eventProperty - */ - on(event: 'start', listener: (userId: string) => void): this; - /** - * Emitted when a user ends speaking. - * - * @eventProperty - */ - on(event: 'end', listener: (userId: string) => void): this; -} -/** - * Tracks the speaking states of users in a voice channel. - */ -declare class SpeakingMap extends EventEmitter { - /** - * The delay after a packet is received from a user until they're marked as not speaking anymore. - */ - static readonly DELAY = 100; - /** - * The currently speaking users, mapped to the milliseconds since UNIX epoch at which they started speaking. - */ - readonly users: Map; - private readonly speakingTimeouts; - constructor(); - onPacket(userId: string): void; - private startTimeout; -} - -/** - * Attaches to a VoiceConnection, allowing you to receive audio packets from other - * users that are speaking. - * - * @beta - */ -declare class VoiceReceiver { - /** - * The attached connection of this receiver. - */ - readonly voiceConnection: VoiceConnection; - /** - * Maps SSRCs to Discord user ids. - */ - readonly ssrcMap: SSRCMap; - /** - * The current audio subscriptions of this receiver. - */ - readonly subscriptions: Map; - /** - * The connection data of the receiver. - * - * @internal - */ - connectionData: Partial; - /** - * The speaking map of the receiver. - */ - readonly speaking: SpeakingMap; - constructor(voiceConnection: VoiceConnection); - /** - * Called when a packet is received on the attached connection's WebSocket. - * - * @param packet - The received packet - * @internal - */ - onWsPacket(packet: VoiceReceivePayload): void; - private decrypt; - /** - * Parses an audio packet, decrypting it to yield an Opus packet. - * - * @param buffer - The buffer to parse - * @param mode - The encryption mode - * @param nonce - The nonce buffer used by the connection for encryption - * @param secretKey - The secret key used by the connection for encryption - * @param userId - The user id that sent the packet - * @returns The parsed Opus packet - */ - private parsePacket; - /** - * Called when the UDP socket of the attached connection receives a message. - * - * @param msg - The received message - * @internal - */ - onUdpMessage(msg: Buffer): void; - /** - * Creates a subscription for the given user id. - * - * @param target - The id of the user to subscribe to - * @returns A readable stream of Opus packets received from the target - */ - subscribe(userId: string, options?: Partial): AudioReceiveStream; -} - -/** - * Methods that are provided by the \@discordjs/voice library to implementations of - * Discord gateway DiscordGatewayAdapters. - */ -interface DiscordGatewayAdapterLibraryMethods { - /** - * Call this when the adapter can no longer be used (e.g. due to a disconnect from the main gateway) - */ - destroy(): void; - /** - * Call this when you receive a VOICE_SERVER_UPDATE payload that is relevant to the adapter. - * - * @param data - The inner data of the VOICE_SERVER_UPDATE payload - */ - onVoiceServerUpdate(data: GatewayVoiceServerUpdateDispatchData): void; - /** - * Call this when you receive a VOICE_STATE_UPDATE payload that is relevant to the adapter. - * - * @param data - The inner data of the VOICE_STATE_UPDATE payload - */ - onVoiceStateUpdate(data: GatewayVoiceStateUpdateDispatchData): void; -} -/** - * Methods that are provided by the implementer of a Discord gateway DiscordGatewayAdapter. - */ -interface DiscordGatewayAdapterImplementerMethods { - /** - * This will be called by \@discordjs/voice when the adapter can safely be destroyed as it will no - * longer be used. - */ - destroy(): void; - /** - * Implement this method such that the given payload is sent to the main Discord gateway connection. - * - * @param payload - The payload to send to the main Discord gateway connection - * @returns `false` if the payload definitely failed to send - in this case, the voice connection disconnects - */ - sendPayload(payload: any): boolean; -} -/** - * A function used to build adapters. It accepts a methods parameter that contains functions that - * can be called by the implementer when new data is received on its gateway connection. In return, - * the implementer will return some methods that the library can call - e.g. to send messages on - * the gateway, or to signal that the adapter can be removed. - */ -type DiscordGatewayAdapterCreator = (methods: DiscordGatewayAdapterLibraryMethods) => DiscordGatewayAdapterImplementerMethods; - -/** - * The various status codes a voice connection can hold at any one time. - */ -declare enum VoiceConnectionStatus { - /** - * The `VOICE_SERVER_UPDATE` and `VOICE_STATE_UPDATE` packets have been received, now attempting to establish a voice connection. - */ - Connecting = "connecting", - /** - * The voice connection has been destroyed and untracked, it cannot be reused. - */ - Destroyed = "destroyed", - /** - * The voice connection has either been severed or not established. - */ - Disconnected = "disconnected", - /** - * A voice connection has been established, and is ready to be used. - */ - Ready = "ready", - /** - * Sending a packet to the main Discord gateway to indicate we want to change our voice state. - */ - Signalling = "signalling" -} -/** - * The state that a VoiceConnection will be in when it is waiting to receive a VOICE_SERVER_UPDATE and - * VOICE_STATE_UPDATE packet from Discord, provided by the adapter. - */ -interface VoiceConnectionSignallingState { - adapter: DiscordGatewayAdapterImplementerMethods; - status: VoiceConnectionStatus.Signalling; - subscription?: PlayerSubscription | undefined; -} -/** - * The reasons a voice connection can be in the disconnected state. - */ -declare enum VoiceConnectionDisconnectReason { - /** - * When the WebSocket connection has been closed. - */ - WebSocketClose = 0, - /** - * When the adapter was unable to send a message requested by the VoiceConnection. - */ - AdapterUnavailable = 1, - /** - * When a VOICE_SERVER_UPDATE packet is received with a null endpoint, causing the connection to be severed. - */ - EndpointRemoved = 2, - /** - * When a manual disconnect was requested. - */ - Manual = 3 -} -/** - * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is - * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect. - */ -interface VoiceConnectionDisconnectedBaseState { - adapter: DiscordGatewayAdapterImplementerMethods; - status: VoiceConnectionStatus.Disconnected; - subscription?: PlayerSubscription | undefined; -} -/** - * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is - * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect. - */ -interface VoiceConnectionDisconnectedOtherState extends VoiceConnectionDisconnectedBaseState { - reason: Exclude; -} -/** - * The state that a VoiceConnection will be in when its WebSocket connection was closed. - * You can manually attempt to reconnect using VoiceConnection#reconnect. - */ -interface VoiceConnectionDisconnectedWebSocketState extends VoiceConnectionDisconnectedBaseState { - /** - * The close code of the WebSocket connection to the Discord voice server. - */ - closeCode: number; - reason: VoiceConnectionDisconnectReason.WebSocketClose; -} -/** - * The states that a VoiceConnection can be in when it is not connected to a Discord voice server nor is - * it attempting to connect. You can manually attempt to connect using VoiceConnection#reconnect. - */ -type VoiceConnectionDisconnectedState = VoiceConnectionDisconnectedOtherState | VoiceConnectionDisconnectedWebSocketState; -/** - * The state that a VoiceConnection will be in when it is establishing a connection to a Discord - * voice server. - */ -interface VoiceConnectionConnectingState { - adapter: DiscordGatewayAdapterImplementerMethods; - networking: Networking; - status: VoiceConnectionStatus.Connecting; - subscription?: PlayerSubscription | undefined; -} -/** - * The state that a VoiceConnection will be in when it has an active connection to a Discord - * voice server. - */ -interface VoiceConnectionReadyState { - adapter: DiscordGatewayAdapterImplementerMethods; - networking: Networking; - status: VoiceConnectionStatus.Ready; - subscription?: PlayerSubscription | undefined; -} -/** - * The state that a VoiceConnection will be in when it has been permanently been destroyed by the - * user and untracked by the library. It cannot be reconnected, instead, a new VoiceConnection - * needs to be established. - */ -interface VoiceConnectionDestroyedState { - status: VoiceConnectionStatus.Destroyed; -} -/** - * The various states that a voice connection can be in. - */ -type VoiceConnectionState = VoiceConnectionConnectingState | VoiceConnectionDestroyedState | VoiceConnectionDisconnectedState | VoiceConnectionReadyState | VoiceConnectionSignallingState; -interface VoiceConnection extends EventEmitter { - /** - * Emitted when there is an error emitted from the voice connection - * - * @eventProperty - */ - on(event: 'error', listener: (error: Error) => void): this; - /** - * Emitted debugging information about the voice connection - * - * @eventProperty - */ - on(event: 'debug', listener: (message: string) => void): this; - /** - * Emitted when the state of the voice connection changes - * - * @eventProperty - */ - on(event: 'stateChange', listener: (oldState: VoiceConnectionState, newState: VoiceConnectionState) => void): this; - /** - * Emitted when the end-to-end encrypted session has transitioned - * - * @eventProperty - */ - on(event: 'transitioned', listener: (transitionId: number) => void): this; - /** - * Emitted when the state of the voice connection changes to a specific status - * - * @eventProperty - */ - on(event: Event, listener: (oldState: VoiceConnectionState, newState: VoiceConnectionState & { - status: Event; - }) => void): this; -} -/** - * A connection to the voice server of a Guild, can be used to play audio in voice channels. - */ -declare class VoiceConnection extends EventEmitter { - /** - * The number of consecutive rejoin attempts. Initially 0, and increments for each rejoin. - * When a connection is successfully established, it resets to 0. - */ - rejoinAttempts: number; - /** - * The state of the voice connection. - */ - private _state; - /** - * A configuration storing all the data needed to reconnect to a Guild's voice server. - * - * @internal - */ - readonly joinConfig: JoinConfig; - /** - * The two packets needed to successfully establish a voice connection. They are received - * from the main Discord gateway after signalling to change the voice state. - */ - private readonly packets; - /** - * The receiver of this voice connection. You should join the voice channel with `selfDeaf` set - * to false for this feature to work properly. - */ - readonly receiver: VoiceReceiver; - /** - * The debug logger function, if debugging is enabled. - */ - private readonly debug; - /** - * The options used to create this voice connection. - */ - private readonly options; - /** - * Creates a new voice connection. - * - * @param joinConfig - The data required to establish the voice connection - * @param options - The options used to create this voice connection - */ - constructor(joinConfig: JoinConfig, options: CreateVoiceConnectionOptions); - /** - * The current state of the voice connection. - * - * @remarks - * The setter will perform clean-up operations where necessary. - */ - get state(): VoiceConnectionState; - set state(newState: VoiceConnectionState); - /** - * Registers a `VOICE_SERVER_UPDATE` packet to the voice connection. This will cause it to reconnect using the - * new data provided in the packet. - * - * @param packet - The received `VOICE_SERVER_UPDATE` packet - */ - private addServerPacket; - /** - * Registers a `VOICE_STATE_UPDATE` packet to the voice connection. Most importantly, it stores the id of the - * channel that the client is connected to. - * - * @param packet - The received `VOICE_STATE_UPDATE` packet - */ - private addStatePacket; - /** - * Called when the networking state changes, and the new ws/udp packet/message handlers need to be rebound - * to the new instances. - * - * @param newState - The new networking state - * @param oldState - The old networking state, if there is one - */ - private updateReceiveBindings; - /** - * Attempts to configure a networking instance for this voice connection using the received packets. - * Both packets are required, and any existing networking instance will be destroyed. - * - * @remarks - * This is called when the voice server of the connection changes, e.g. if the bot is moved into a - * different channel in the same guild but has a different voice server. In this instance, the connection - * needs to be re-established to the new voice server. - * - * The connection will transition to the Connecting state when this is called. - */ - configureNetworking(): void; - /** - * Called when the networking instance for this connection closes. If the close code is 4014 (do not reconnect), - * the voice connection will transition to the Disconnected state which will store the close code. You can - * decide whether or not to reconnect when this occurs by listening for the state change and calling reconnect(). - * - * @remarks - * If the close code was anything other than 4014, it is likely that the closing was not intended, and so the - * VoiceConnection will signal to Discord that it would like to rejoin the channel. This automatically attempts - * to re-establish the connection. This would be seen as a transition from the Ready state to the Signalling state. - * @param code - The close code - */ - private onNetworkingClose; - /** - * Called when the state of the networking instance changes. This is used to derive the state of the voice connection. - * - * @param oldState - The previous state - * @param newState - The new state - */ - private onNetworkingStateChange; - /** - * Propagates errors from the underlying network instance. - * - * @param error - The error to propagate - */ - private onNetworkingError; - /** - * Propagates debug messages from the underlying network instance. - * - * @param message - The debug message to propagate - */ - private onNetworkingDebug; - /** - * Propagates transitions from the underlying network instance. - * - * @param transitionId - The transition id - */ - private onNetworkingTransitioned; - /** - * Prepares an audio packet for dispatch. - * - * @param buffer - The Opus packet to prepare - */ - prepareAudioPacket(buffer: Buffer): Buffer | undefined; - /** - * Dispatches the previously prepared audio packet (if any) - */ - dispatchAudio(): boolean | undefined; - /** - * Prepares an audio packet and dispatches it immediately. - * - * @param buffer - The Opus packet to play - */ - playOpusPacket(buffer: Buffer): boolean | undefined; - /** - * Destroys the VoiceConnection, preventing it from connecting to voice again. - * This method should be called when you no longer require the VoiceConnection to - * prevent memory leaks. - * - * @param adapterAvailable - Whether the adapter can be used - */ - destroy(adapterAvailable?: boolean): void; - /** - * Disconnects the VoiceConnection, allowing the possibility of rejoining later on. - * - * @returns `true` if the connection was successfully disconnected - */ - disconnect(): boolean; - /** - * Attempts to rejoin (better explanation soon:tm:) - * - * @remarks - * Calling this method successfully will automatically increment the `rejoinAttempts` counter, - * which you can use to inform whether or not you'd like to keep attempting to reconnect your - * voice connection. - * - * A state transition from Disconnected to Signalling will be observed when this is called. - */ - rejoin(joinConfig?: Omit): boolean; - /** - * Updates the speaking status of the voice connection. This is used when audio players are done playing audio, - * and need to signal that the connection is no longer playing audio. - * - * @param enabled - Whether or not to show as speaking - */ - setSpeaking(enabled: boolean): false | void; - /** - * Subscribes to an audio player, allowing the player to play audio on this voice connection. - * - * @param player - The audio player to subscribe to - * @returns The created subscription - */ - subscribe(player: AudioPlayer): PlayerSubscription | undefined; - /** - * The latest ping (in milliseconds) for the WebSocket connection and audio playback for this voice - * connection, if this data is available. - * - * @remarks - * For this data to be available, the VoiceConnection must be in the Ready state, and its underlying - * WebSocket connection and UDP socket must have had at least one ping-pong exchange. - */ - get ping(): { - ws: number | undefined; - udp: number | undefined; - }; - /** - * The current voice privacy code of the encrypted session. - * - * @remarks - * For this data to be available, the VoiceConnection must be in the Ready state, - * and the connection would have to be end-to-end encrypted. - */ - get voicePrivacyCode(): string | undefined; - /** - * Gets the verification code for a user in the session. - * - * @throws Will throw if end-to-end encryption is not on or if the user id provided is not in the session. - */ - getVerificationCode(userId: string): Promise; - /** - * Called when a subscription of this voice connection to an audio player is removed. - * - * @param subscription - The removed subscription - */ - protected onSubscriptionRemoved(subscription: PlayerSubscription): void; -} - -/** - * The options that can be given when creating a voice connection. - */ -interface CreateVoiceConnectionOptions { - adapterCreator: DiscordGatewayAdapterCreator; - /** - * Whether to use the DAVE protocol for end-to-end encryption. Defaults to true. - */ - daveEncryption?: boolean | undefined; - /** - * If true, debug messages will be enabled for the voice connection and its - * related components. Defaults to false. - */ - debug?: boolean | undefined; - /** - * The amount of consecutive decryption failures needed to try to - * re-initialize the end-to-end encrypted session to recover. Defaults to 24. - */ - decryptionFailureTolerance?: number | undefined; -} -/** - * The options that can be given when joining a voice channel. - */ -interface JoinVoiceChannelOptions { - /** - * The id of the Discord voice channel to join. - */ - channelId: string; - /** - * An optional group identifier for the voice connection. - */ - group?: string; - /** - * The id of the guild that the voice channel belongs to. - */ - guildId: string; - /** - * Whether to join the channel deafened (defaults to true) - */ - selfDeaf?: boolean; - /** - * Whether to join the channel muted (defaults to true) - */ - selfMute?: boolean; -} -/** - * Creates a VoiceConnection to a Discord voice channel. - * - * @param options - the options for joining the voice channel - */ -declare function joinVoiceChannel(options: CreateVoiceConnectionOptions & JoinVoiceChannelOptions): VoiceConnection; - -declare const RTP_OPUS_PAYLOAD_TYPE = 120; - -/** - * Generates a report of the dependencies used by the \@discordjs/voice module. - * Useful for debugging. - */ -declare function generateDependencyReport(): string; - -/** - * Allows a voice connection a specified amount of time to enter a given state, otherwise rejects with an error. - * - * @param target - The voice connection that we want to observe the state change for - * @param status - The status that the voice connection should be in - * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation - */ -declare function entersState(target: VoiceConnection, status: VoiceConnectionStatus, timeoutOrSignal: AbortSignal | number): Promise; -/** - * Allows an audio player a specified amount of time to enter a given state, otherwise rejects with an error. - * - * @param target - The audio player that we want to observe the state change for - * @param status - The status that the audio player should be in - * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation - */ -declare function entersState(target: AudioPlayer, status: AudioPlayerStatus, timeoutOrSignal: AbortSignal | number): Promise; - -/** - * Takes an Opus Head, and verifies whether the associated Opus audio is suitable to play in a Discord voice channel. - * - * @param opusHead - The Opus Head to validate - * @returns `true` if suitable to play in a Discord voice channel, otherwise `false` - */ -declare function validateDiscordOpusHead(opusHead: Buffer): boolean; -/** - * The resulting information after probing an audio stream - */ -interface ProbeInfo { - /** - * The readable audio stream to use. You should use this rather than the input stream, as the probing - * function can sometimes read the input stream to its end and cause the stream to close. - */ - stream: Readable; - /** - * The recommended stream type for this audio stream. - */ - type: StreamType; -} -/** - * Attempt to probe a readable stream to figure out whether it can be demuxed using an Ogg or WebM Opus demuxer. - * - * @param stream - The readable stream to probe - * @param probeSize - The number of bytes to attempt to read before giving up on the probe - * @param validator - The Opus Head validator function - * @experimental - */ -declare function demuxProbe(stream: Readable, probeSize?: number, validator?: typeof validateDiscordOpusHead): Promise; - -/** - * The {@link https://github.com/discordjs/discord.js/blob/main/packages/voice#readme | @discordjs/voice} version - * that you are currently using. - */ -declare const version: string; - -export { AudioPlayer, type AudioPlayerBufferingState, AudioPlayerError, type AudioPlayerIdleState, type AudioPlayerPausedState, type AudioPlayerPlayingState, type AudioPlayerState, AudioPlayerStatus, AudioReceiveStream, type AudioReceiveStreamOptions, AudioResource, type ConnectionData, type ConnectionOptions, type CreateAudioPlayerOptions, type CreateAudioResourceOptions, type CreateVoiceConnectionOptions, DAVESession, type DiscordGatewayAdapterCreator, type DiscordGatewayAdapterImplementerMethods, type DiscordGatewayAdapterLibraryMethods, type Edge, type EndBehavior, EndBehaviorType, type JoinConfig, type JoinVoiceChannelOptions, Networking, type NetworkingClosedState, type NetworkingIdentifyingState, type NetworkingOpeningWsState, type NetworkingReadyState, type NetworkingResumingState, type NetworkingSelectingProtocolState, type NetworkingState, NetworkingStatusCode, type NetworkingUdpHandshakingState, NoSubscriberBehavior, Node, PlayerSubscription, type ProbeInfo, RTP_OPUS_PAYLOAD_TYPE, SSRCMap, type SocketConfig, SpeakingMap, StreamType, TransformerType, VoiceConnection, type VoiceConnectionConnectingState, type VoiceConnectionDestroyedState, VoiceConnectionDisconnectReason, type VoiceConnectionDisconnectedBaseState, type VoiceConnectionDisconnectedOtherState, type VoiceConnectionDisconnectedState, type VoiceConnectionDisconnectedWebSocketState, type VoiceConnectionReadyState, type VoiceConnectionSignallingState, type VoiceConnectionState, VoiceConnectionStatus, VoiceReceiver, VoiceUDPSocket, type VoiceUserData, VoiceWebSocket, createAudioPlayer, createAudioResource, createDefaultAudioReceiveStreamOptions, demuxProbe, entersState, generateDependencyReport, getGroups, getVoiceConnection, getVoiceConnections, joinVoiceChannel, validateDiscordOpusHead, version }; diff --git a/services/discord-gateway/vendor/discord-voice-fork/dist/index.js b/services/discord-gateway/vendor/discord-voice-fork/dist/index.js deleted file mode 100644 index d913721e..00000000 --- a/services/discord-gateway/vendor/discord-voice-fork/dist/index.js +++ /dev/null @@ -1,3275 +0,0 @@ -"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 __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); - -// src/index.ts -var index_exports = {}; -__export(index_exports, { - AudioPlayer: () => AudioPlayer, - AudioPlayerError: () => AudioPlayerError, - AudioPlayerStatus: () => AudioPlayerStatus, - AudioReceiveStream: () => AudioReceiveStream, - AudioResource: () => AudioResource, - DAVESession: () => DAVESession, - EndBehaviorType: () => EndBehaviorType, - Networking: () => Networking, - NetworkingStatusCode: () => NetworkingStatusCode, - NoSubscriberBehavior: () => NoSubscriberBehavior, - Node: () => Node, - PlayerSubscription: () => PlayerSubscription, - RTP_OPUS_PAYLOAD_TYPE: () => RTP_OPUS_PAYLOAD_TYPE, - SSRCMap: () => SSRCMap, - SpeakingMap: () => SpeakingMap, - StreamType: () => StreamType, - TransformerType: () => TransformerType, - VoiceConnection: () => VoiceConnection, - VoiceConnectionDisconnectReason: () => VoiceConnectionDisconnectReason, - VoiceConnectionStatus: () => VoiceConnectionStatus, - VoiceReceiver: () => VoiceReceiver, - VoiceUDPSocket: () => VoiceUDPSocket, - VoiceWebSocket: () => VoiceWebSocket, - createAudioPlayer: () => createAudioPlayer, - createAudioResource: () => createAudioResource, - createDefaultAudioReceiveStreamOptions: () => createDefaultAudioReceiveStreamOptions, - demuxProbe: () => demuxProbe, - entersState: () => entersState, - generateDependencyReport: () => generateDependencyReport, - getGroups: () => getGroups, - getVoiceConnection: () => getVoiceConnection, - getVoiceConnections: () => getVoiceConnections, - joinVoiceChannel: () => joinVoiceChannel, - validateDiscordOpusHead: () => validateDiscordOpusHead, - version: () => version2 -}); -module.exports = __toCommonJS(index_exports); - -// src/VoiceConnection.ts -var import_node_events8 = 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 !== -1) { - audioCycleInterval = setTimeout(() => audioCycleStep(), Math.max(1, nextTime - Date.now())); - } - 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 = { - "sodium-native": /* @__PURE__ */ __name((sodium) => ({ - crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => { - const message = import_node_buffer.Buffer.alloc(cipherText.length - sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES); - sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(message, null, cipherText, additionalData, nonce2, key); - return message; - }, "crypto_aead_xchacha20poly1305_ietf_decrypt"), - 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"), - sodium: /* @__PURE__ */ __name((sodium) => ({ - crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => sodium.api.crypto_aead_xchacha20poly1305_ietf_decrypt(cipherText, additionalData, null, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_decrypt"), - crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => sodium.api.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_encrypt") - }), "sodium"), - "libsodium-wrappers": /* @__PURE__ */ __name((sodium) => ({ - crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, cipherText, additionalData, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_decrypt"), - crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_encrypt") - }), "libsodium-wrappers"), - "@stablelib/xchacha20poly1305": /* @__PURE__ */ __name((stablelib) => ({ - crypto_aead_xchacha20poly1305_ietf_decrypt(plaintext, additionalData, nonce2, key) { - const crypto3 = new stablelib.XChaCha20Poly1305(key); - return crypto3.open(nonce2, plaintext, additionalData); - }, - crypto_aead_xchacha20poly1305_ietf_encrypt(cipherText, additionalData, nonce2, key) { - const crypto3 = new stablelib.XChaCha20Poly1305(key); - return crypto3.seal(nonce2, cipherText, additionalData); - } - }), "@stablelib/xchacha20poly1305"), - "@noble/ciphers/chacha.js": /* @__PURE__ */ __name((noble) => ({ - crypto_aead_xchacha20poly1305_ietf_decrypt(cipherText, additionalData, nonce2, key) { - const chacha = noble.xchacha20poly1305(key, nonce2, additionalData); - return chacha.decrypt(cipherText); - }, - crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, nonce2, key) { - const chacha = noble.xchacha20poly1305(key, nonce2, additionalData); - return chacha.encrypt(plaintext); - } - }), "@noble/ciphers/chacha.js") -}; -var fallbackError = /* @__PURE__ */ __name(() => { - throw new Error( - `Cannot play audio as no valid encryption package is installed. -- Install one of: - - sodium - - libsodium-wrappers - - @stablelib/xchacha20poly1305 - - @noble/ciphers. -- Use the generateDependencyReport() function for more information. -` - ); -}, "fallbackError"); -var methods = { - crypto_aead_xchacha20poly1305_ietf_encrypt: fallbackError, - crypto_aead_xchacha20poly1305_ietf_decrypt: fallbackError -}; -var secretboxLoadPromise = new Promise(async (resolve2) => { - 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 { - } - } - resolve2(); -}); - -// src/util/constants.ts -var RTP_OPUS_PAYLOAD_TYPE = 120; - -// 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"); -var import_davey = __toESM(require("@snazzah/davey")); - -// src/audio/AudioPlayer.ts -var import_node_buffer2 = require("buffer"); -var import_node_events = require("events"); - -// src/audio/AudioPlayerError.ts -var AudioPlayerError = class extends Error { - static { - __name(this, "AudioPlayerError"); - } - /** - * The resource associated with the audio player at the time the error was thrown. - */ - resource; - constructor(error, resource) { - super(error.message); - this.resource = resource; - this.name = error.name; - this.stack = error.stack; - } -}; - -// src/audio/PlayerSubscription.ts -var PlayerSubscription = class { - static { - __name(this, "PlayerSubscription"); - } - /** - * The voice connection of this subscription. - */ - connection; - /** - * The audio player of this subscription. - */ - player; - constructor(connection, 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); - } -}; - -// 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 extends import_node_events.EventEmitter { - static { - __name(this, "AudioPlayer"); - } - /** - * The state that the AudioPlayer is in. - */ - _state; - /** - * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio - * to the streams in this list. - */ - subscribers = []; - /** - * The behavior that the player should follow when it enters certain situations. - */ - behaviors; - /** - * The debug logger function, if debugging is enabled. - */ - debug; - /** - * Creates a new AudioPlayer. - */ - constructor(options = {}) { - super(); - 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. - * - * @remarks - * The setter will perform clean-up operations where necessary. - */ - get state() { - return this._state; - } - 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); - } - } -}; -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; -function getMaxProtocolVersion() { - return import_davey.default.DAVE_PROTOCOL_VERSION; -} -__name(getMaxProtocolVersion, "getMaxProtocolVersion"); -var DAVESession = class extends import_node_events2.EventEmitter { - static { - __name(this, "DAVESession"); - } - /** - * The channel id represented by this session. - */ - channelId; - /** - * The user id represented by this session. - */ - userId; - /** - * The protocol version being used. - */ - protocolVersion; - /** - * The last transition id executed. - */ - lastTransitionId; - /** - * The pending transitions, mapped by their ID and the protocol version. - */ - pendingTransitions = /* @__PURE__ */ new Map(); - /** - * Whether this session was downgraded previously. - */ - downgraded = false; - /** - * The amount of consecutive failures encountered when decrypting. - */ - consecutiveFailures = 0; - /** - * The amount of consecutive failures needed to attempt to recover. - */ - failureTolerance; - /** - * Whether this session is currently re-initializing due to an invalid transition. - */ - reinitializing = false; - /** - * The underlying DAVE Session of this wrapper. - */ - session; - constructor(protocolVersion, userId, channelId, options) { - super(); - 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 import_davey.default.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, import_davey.default.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 { - } - } -}; - -// 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 extends import_node_events3.EventEmitter { - static { - __name(this, "VoiceUDPSocket"); - } - /** - * The underlying network Socket for the VoiceUDPSocket. - */ - socket; - /** - * The socket details for Discord (remote) - */ - remote; - /** - * The counter used in the keep alive mechanism. - */ - keepAliveCounter = 0; - /** - * The buffer used to write the keep alive counter into. - */ - keepAliveBuffer; - /** - * The Node.js interval for the keep-alive mechanism. - */ - 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. - */ - ping; - /** - * Creates a new VoiceUDPSocket. - * - * @param remote - Details of the remote socket - */ - constructor(remote) { - super(); - 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((resolve2, reject) => { - const listener = /* @__PURE__ */ __name((message) => { - try { - if (message.readUInt16BE(0) !== 2) return; - const packet = parseLocalPacket(message); - this.socket.off("message", listener); - resolve2(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); - }); - } -}; - -// 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 extends import_node_events4.EventEmitter { - static { - __name(this, "VoiceWebSocket"); - } - /** - * The current heartbeat interval, if any. - */ - 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. - */ - 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. - */ - lastHeartbeatSend; - /** - * The number of consecutively missed heartbeats. - */ - missedHeartbeats = 0; - /** - * The last recorded ping. - */ - ping; - /** - * The last sequence number acknowledged from Discord. Will be `-1` if no sequence numbered messages have been received. - */ - sequence = -1; - /** - * The debug logger function, if debugging is enabled. - */ - debug; - /** - * The underlying WebSocket of this wrapper. - */ - ws; - /** - * Creates a new VoiceWebSocket. - * - * @param address - The address to connect to - */ - constructor(address, debug) { - super(); - 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); - } - } -}; - -// 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 NetworkingStatusCode = /* @__PURE__ */ ((NetworkingStatusCode2) => { - NetworkingStatusCode2[NetworkingStatusCode2["OpeningWs"] = 0] = "OpeningWs"; - NetworkingStatusCode2[NetworkingStatusCode2["Identifying"] = 1] = "Identifying"; - NetworkingStatusCode2[NetworkingStatusCode2["UdpHandshaking"] = 2] = "UdpHandshaking"; - NetworkingStatusCode2[NetworkingStatusCode2["SelectingProtocol"] = 3] = "SelectingProtocol"; - NetworkingStatusCode2[NetworkingStatusCode2["Ready"] = 4] = "Ready"; - NetworkingStatusCode2[NetworkingStatusCode2["Resuming"] = 5] = "Resuming"; - NetworkingStatusCode2[NetworkingStatusCode2["Closed"] = 6] = "Closed"; - return NetworkingStatusCode2; -})(NetworkingStatusCode || {}); -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 extends import_node_events5.EventEmitter { - static { - __name(this, "Networking"); - } - _state; - /** - * The debug logger function, if debugging is enabled. - */ - debug; - /** - * The options used to create this Networking instance. - */ - options; - /** - * Creates a new Networking instance. - */ - constructor(connectionOptions, options) { - super(); - 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(); - } - const oldState = this._state; - this._state = newState; - this.emit("stateChange", oldState, newState); - this.debug?.(`state change: -from ${stringifyState2(oldState)} -to ${stringifyState2(newState)}`); - } - /** - * Creates a new WebSocket to a Discord Voice gateway. - * - * @param endpoint - The endpoint to connect to - * @param lastSequence - The last sequence to set for this WebSocket - */ - createWebSocket(endpoint, lastSequence) { - const ws = new VoiceWebSocket(`wss://${endpoint}?v=8`, Boolean(this.debug)); - if (lastSequence !== void 0) { - ws.sequence = lastSequence; - } - ws.on("error", this.onChildError); - ws.once("open", this.onWsOpen); - ws.on("packet", this.onWsPacket); - ws.on("binary", this.onWsBinary); - ws.once("close", this.onWsClose); - ws.on("debug", this.onWsDebug); - return ws; - } - /** - * Creates a new DAVE session for this voice connection if we can create one. - * - * @param protocolVersion - The protocol version to use - */ - createDaveSession(protocolVersion) { - if (this.options.daveEncryption === false || this.state.code !== 3 /* SelectingProtocol */ && this.state.code !== 4 /* Ready */ && this.state.code !== 5 /* Resuming */) { - return; - } - const session = new DAVESession( - protocolVersion, - this.state.connectionOptions.userId, - this.state.connectionOptions.channelId, - { - decryptionFailureTolerance: this.options.decryptionFailureTolerance - } - ); - session.on("error", this.onChildError); - session.on("debug", this.onDaveDebug); - session.on("keyPackage", this.onDaveKeyPackage); - session.on("invalidateTransition", this.onDaveInvalidateTransition); - session.reinit(); - return session; - } - /** - * Propagates errors from the children VoiceWebSocket, VoiceUDPSocket and DAVESession. - * - * @param error - The error that was emitted by a child - */ - 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. - */ - onWsOpen() { - if (this.state.code === 0 /* OpeningWs */) { - this.state.ws.sendPacket({ - op: import_v82.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, - max_dave_protocol_version: this.options.daveEncryption === false ? 0 : getMaxProtocolVersion(), - // [GMW FORK] Declare video capability so Discord sends RTP video - // (camera + screen-share) for all members. Derived from - // Discord-RE/Discord-video-stream which uses a video-capable - // Identify. stream = [] because this client is a pure receiver - // (does not send its own video). Gated by DISCORD_VOICE_ENABLE_VIDEO. - video: true, - streams: [] - } - }); - this.state = { - ...this.state, - code: 1 /* Identifying */ - }; - } else if (this.state.code === 5 /* Resuming */) { - this.state.ws.sendPacket({ - op: import_v82.VoiceOpcodes.Resume, - d: { - server_id: this.state.connectionOptions.serverId, - session_id: this.state.connectionOptions.sessionId, - token: this.state.connectionOptions.token, - seq_ack: this.state.ws.sequence - } - }); - } - } - /** - * Called when the WebSocket closes. Based on the reason for closing (given by the code parameter), - * the instance will either attempt to resume, or enter the closed state and emit a 'close' event - * with the close code, allowing the user to decide whether or not they would like to reconnect. - * - * @param code - The close code - */ - onWsClose({ code }) { - const canResume = code === 4015 || code < 4e3; - if (canResume && this.state.code === 4 /* Ready */) { - const lastSequence = this.state.ws.sequence; - this.state = { - ...this.state, - code: 5 /* Resuming */, - ws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence) - }; - } else if (this.state.code !== 6 /* Closed */) { - this.destroy(); - this.emit("close", code); - } - } - /** - * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord. - */ - onUdpClose() { - if (this.state.code === 4 /* Ready */) { - const lastSequence = this.state.ws.sequence; - this.state = { - ...this.state, - code: 5 /* Resuming */, - ws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence) - }; - } - } - /** - * Called when a packet is received on the connection's WebSocket. - * - * @param packet - The received packet - */ - onWsPacket(packet) { - if (packet.op === import_v82.VoiceOpcodes.Hello && this.state.code !== 6 /* Closed */) { - this.state.ws.setHeartbeatInterval(packet.d.heartbeat_interval); - } else if (packet.op === import_v82.VoiceOpcodes.Ready && this.state.code === 1 /* 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 !== 2 /* UdpHandshaking */) return; - this.state.ws.sendPacket({ - op: import_v82.VoiceOpcodes.SelectProtocol, - d: { - protocol: "udp", - data: { - address: localConfig.ip, - port: localConfig.port, - mode: chooseEncryptionMode(modes) - } - } - }); - this.state = { - ...this.state, - code: 3 /* SelectingProtocol */ - }; - }).catch((error) => this.emit("error", error)); - this.state = { - ...this.state, - code: 2 /* UdpHandshaking */, - udp, - connectionData: { - ssrc, - connectedClients: /* @__PURE__ */ new Set() - } - }; - } else if (packet.op === import_v82.VoiceOpcodes.SessionDescription && this.state.code === 3 /* SelectingProtocol */) { - const { mode: encryptionMode, secret_key: secretKey, dave_protocol_version: daveProtocolVersion } = packet.d; - this.state = { - ...this.state, - code: 4 /* Ready */, - dave: this.createDaveSession(daveProtocolVersion), - connectionData: { - ...this.state.connectionData, - encryptionMode, - secretKey: new Uint8Array(secretKey), - sequence: randomNBit(16), - timestamp: randomNBit(32), - nonce: 0, - nonceBuffer: encryptionMode === "aead_aes256_gcm_rtpsize" ? import_node_buffer6.Buffer.alloc(12) : import_node_buffer6.Buffer.alloc(24), - speaking: false, - packetsPlayed: 0 - } - }; - } else if (packet.op === import_v82.VoiceOpcodes.Resumed && this.state.code === 5 /* Resuming */) { - this.state = { - ...this.state, - code: 4 /* Ready */ - }; - this.state.connectionData.speaking = false; - } else if ((packet.op === import_v82.VoiceOpcodes.ClientsConnect || packet.op === import_v82.VoiceOpcodes.ClientDisconnect) && (this.state.code === 4 /* Ready */ || this.state.code === 2 /* UdpHandshaking */ || this.state.code === 3 /* SelectingProtocol */ || this.state.code === 5 /* Resuming */)) { - const { connectionData } = this.state; - if (packet.op === import_v82.VoiceOpcodes.ClientsConnect) - for (const id of packet.d.user_ids) connectionData.connectedClients.add(id); - else { - connectionData.connectedClients.delete(packet.d.user_id); - } - } else if ((this.state.code === 4 /* Ready */ || this.state.code === 5 /* Resuming */) && this.state.dave) { - if (packet.op === import_v82.VoiceOpcodes.DavePrepareTransition) { - const sendReady = this.state.dave.prepareTransition(packet.d); - if (sendReady) - this.state.ws.sendPacket({ - op: import_v82.VoiceOpcodes.DaveTransitionReady, - d: { transition_id: packet.d.transition_id } - }); - if (packet.d.transition_id === 0) { - this.emit("transitioned", 0); - } - } else if (packet.op === import_v82.VoiceOpcodes.DaveExecuteTransition) { - const transitioned = this.state.dave.executeTransition(packet.d.transition_id); - if (transitioned) this.emit("transitioned", packet.d.transition_id); - } else if (packet.op === import_v82.VoiceOpcodes.DavePrepareEpoch) this.state.dave.prepareEpoch(packet.d); - } - } - /** - * Called when a binary message is received on the connection's WebSocket. - * - * @param message - The received message - */ - onWsBinary(message) { - if (this.state.code === 4 /* Ready */ && this.state.dave) { - if (message.op === import_v82.VoiceOpcodes.DaveMlsExternalSender) { - this.state.dave.setExternalSender(message.payload); - } else if (message.op === import_v82.VoiceOpcodes.DaveMlsProposals) { - const payload = this.state.dave.processProposals(message.payload, this.state.connectionData.connectedClients); - if (payload) this.state.ws.sendBinaryMessage(import_v82.VoiceOpcodes.DaveMlsCommitWelcome, payload); - } else if (message.op === import_v82.VoiceOpcodes.DaveMlsAnnounceCommitTransition) { - const { transitionId, success } = this.state.dave.processCommit(message.payload); - if (success) { - if (transitionId === 0) this.emit("transitioned", transitionId); - else - this.state.ws.sendPacket({ - op: import_v82.VoiceOpcodes.DaveTransitionReady, - d: { transition_id: transitionId } - }); - } - } else if (message.op === import_v82.VoiceOpcodes.DaveMlsWelcome) { - const { transitionId, success } = this.state.dave.processWelcome(message.payload); - if (success) { - if (transitionId === 0) this.emit("transitioned", transitionId); - else - this.state.ws.sendPacket({ - op: import_v82.VoiceOpcodes.DaveTransitionReady, - d: { transition_id: transitionId } - }); - } - } - } - } - /** - * Called when a new key package is ready to be sent to the voice server. - * - * @param keyPackage - The new key package - */ - onDaveKeyPackage(keyPackage) { - if (this.state.code === 3 /* SelectingProtocol */ || this.state.code === 4 /* Ready */) - this.state.ws.sendBinaryMessage(import_v82.VoiceOpcodes.DaveMlsKeyPackage, keyPackage); - } - /** - * Called when the DAVE session wants to invalidate their transition and re-initialize. - * - * @param transitionId - The transition to invalidate - */ - onDaveInvalidateTransition(transitionId) { - if (this.state.code === 3 /* SelectingProtocol */ || this.state.code === 4 /* Ready */) - this.state.ws.sendPacket({ - op: import_v82.VoiceOpcodes.DaveMlsInvalidCommitWelcome, - d: { transition_id: transitionId } - }); - } - /** - * Propagates debug messages from the child WebSocket. - * - * @param message - The emitted debug message - */ - onWsDebug(message) { - this.debug?.(`[WS] ${message}`); - } - /** - * Propagates debug messages from the child UDPSocket. - * - * @param message - The emitted debug message - */ - onUdpDebug(message) { - this.debug?.(`[UDP] ${message}`); - } - /** - * Propagates debug messages from the child DAVESession. - * - * @param message - The emitted debug message - */ - onDaveDebug(message) { - this.debug?.(`[DAVE] ${message}`); - } - /** - * Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it. - * It will be stored within the instance, and can be played by dispatchAudio() - * - * @remarks - * Calling this method while there is already a prepared audio packet that has not yet been dispatched - * will overwrite the existing audio packet. This should be avoided. - * @param opusPacket - The Opus packet to encrypt - * @returns The audio packet that was prepared - */ - prepareAudioPacket(opusPacket) { - const state = this.state; - if (state.code !== 4 /* Ready */) return; - state.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData, state.dave); - return state.preparedPacket; - } - /** - * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet - * is consumed and cannot be dispatched again. - */ - dispatchAudio() { - const state = this.state; - if (state.code !== 4 /* Ready */) return false; - if (state.preparedPacket !== void 0) { - this.playAudioPacket(state.preparedPacket); - state.preparedPacket = void 0; - return true; - } - return false; - } - /** - * Plays an audio packet, updating timing metadata used for playback. - * - * @param audioPacket - The audio packet to play - */ - playAudioPacket(audioPacket) { - const state = this.state; - if (state.code !== 4 /* 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); - } - /** - * Sends a packet to the voice gateway indicating that the client has start/stopped sending - * audio. - * - * @param speaking - Whether or not the client should be shown as speaking - */ - setSpeaking(speaking) { - const state = this.state; - if (state.code !== 4 /* Ready */) return; - if (state.connectionData.speaking === speaking) return; - state.connectionData.speaking = speaking; - state.ws.sendPacket({ - op: import_v82.VoiceOpcodes.Speaking, - d: { - speaking: speaking ? 1 : 0, - delay: 0, - ssrc: state.connectionData.ssrc - } - }); - } - /** - * Creates a new audio packet from an Opus packet. This involves encrypting the packet, - * then prepending a header that includes metadata. - * - * @param opusPacket - The Opus packet to prepare - * @param connectionData - The current connection data of the instance - * @param daveSession - The DAVE session to use for encryption - */ - createAudioPacket(opusPacket, connectionData, daveSession) { - const rtpHeader = import_node_buffer6.Buffer.alloc(12); - rtpHeader[0] = 128; - rtpHeader[1] = RTP_OPUS_PAYLOAD_TYPE; - const { sequence, timestamp, ssrc } = connectionData; - rtpHeader.writeUIntBE(sequence, 2, 2); - rtpHeader.writeUIntBE(timestamp, 4, 4); - rtpHeader.writeUIntBE(ssrc, 8, 4); - rtpHeader.copy(nonce, 0, 0, 12); - return import_node_buffer6.Buffer.concat([rtpHeader, ...this.encryptOpusPacket(opusPacket, connectionData, rtpHeader, daveSession)]); - } - /** - * Encrypts an Opus packet using the format agreed upon by the instance and Discord. - * - * @param opusPacket - The Opus packet to encrypt - * @param connectionData - The current connection data of the instance - * @param daveSession - The DAVE session to use for encryption - */ - encryptOpusPacket(opusPacket, connectionData, additionalData, daveSession) { - const { secretKey, encryptionMode } = connectionData; - const packet = daveSession?.encrypt(opusPacket) ?? opusPacket; - connectionData.nonce++; - if (connectionData.nonce > MAX_NONCE_SIZE) connectionData.nonce = 0; - connectionData.nonceBuffer.writeUInt32BE(connectionData.nonce, 0); - const noncePadding = connectionData.nonceBuffer.subarray(0, 4); - let encrypted; - switch (encryptionMode) { - case "aead_aes256_gcm_rtpsize": { - const cipher = import_node_crypto.default.createCipheriv("aes-256-gcm", secretKey, connectionData.nonceBuffer); - cipher.setAAD(additionalData); - encrypted = import_node_buffer6.Buffer.concat([cipher.update(packet), cipher.final(), cipher.getAuthTag()]); - return [encrypted, noncePadding]; - } - case "aead_xchacha20_poly1305_rtpsize": { - encrypted = methods.crypto_aead_xchacha20poly1305_ietf_encrypt( - packet, - additionalData, - connectionData.nonceBuffer, - secretKey - ); - return [encrypted, noncePadding]; - } - default: { - throw new RangeError(`Unsupported encryption method: ${encryptionMode}`); - } - } - } -}; - -// src/receive/VoiceReceiver.ts -var import_node_buffer7 = require("buffer"); -var import_node_crypto2 = __toESM(require("crypto")); -var import_v83 = require("discord-api-types/voice/v8"); - -// src/receive/AudioReceiveStream.ts -var import_node_process = __toESM(require("process")); -var import_node_stream = require("stream"); -var EndBehaviorType = /* @__PURE__ */ ((EndBehaviorType2) => { - EndBehaviorType2[EndBehaviorType2["Manual"] = 0] = "Manual"; - EndBehaviorType2[EndBehaviorType2["AfterSilence"] = 1] = "AfterSilence"; - EndBehaviorType2[EndBehaviorType2["AfterInactivity"] = 2] = "AfterInactivity"; - return EndBehaviorType2; -})(EndBehaviorType || {}); -function createDefaultAudioReceiveStreamOptions() { - return { - end: { - behavior: 0 /* Manual */ - } - }; -} -__name(createDefaultAudioReceiveStreamOptions, "createDefaultAudioReceiveStreamOptions"); -var AudioReceiveStream = class extends import_node_stream.Readable { - static { - __name(this, "AudioReceiveStream"); - } - /** - * The end behavior of the receive stream. - */ - end; - endTimeout; - constructor(options) { - const { end, ...rest } = options; - super({ - ...rest, - objectMode: true - }); - this.end = end; - } - push(buffer) { - if (buffer && (this.end.behavior === 2 /* AfterInactivity */ || this.end.behavior === 1 /* AfterSilence */ && (buffer.compare(SILENCE_FRAME) !== 0 || this.endTimeout === void 0))) { - this.renewEndTimeout(this.end); - } - if (buffer === null) { - import_node_process.default.nextTick(() => this.destroy()); - } - return super.push(buffer); - } - renewEndTimeout(end) { - if (this.endTimeout) { - clearTimeout(this.endTimeout); - } - this.endTimeout = setTimeout(() => this.push(null), end.duration); - } - _read() { - } -}; - -// src/receive/SSRCMap.ts -var import_node_events6 = require("events"); -var SSRCMap = class extends import_node_events6.EventEmitter { - static { - __name(this, "SSRCMap"); - } - /** - * The underlying map. - */ - map; - constructor() { - super(); - this.map = /* @__PURE__ */ new Map(); - } - /** - * Updates the map with new user data - * - * @param data - The data to update with - */ - update(data) { - const existing = this.map.get(data.audioSSRC); - const newValue = { - ...this.map.get(data.audioSSRC), - ...data - }; - this.map.set(data.audioSSRC, newValue); - if (!existing) this.emit("create", newValue); - this.emit("update", existing, newValue); - } - /** - * Gets the stored voice data of a user. - * - * @param target - The target, either their user id or audio SSRC - */ - get(target) { - if (typeof target === "number") { - // [GMW FORK] Also match a video SSRC (camera/screen-share) when the - // audio SSRC key does not match. Video RTP packets carry a different - // SSRC than the audio stream, so look it up across stored values. - const direct = this.map.get(target); - if (direct) return direct; - for (const data of this.map.values()) { - if (data.videoSSRC === target) { - return data; - } - } - return void 0; - } - for (const data of this.map.values()) { - if (data.userId === target) { - return data; - } - } - return void 0; - } - /** - * Deletes the stored voice data about a user. - * - * @param target - The target of the delete operation, either their audio SSRC or user id - * @returns The data that was deleted, if any - */ - delete(target) { - if (typeof target === "number") { - const existing = this.map.get(target); - if (existing) { - this.map.delete(target); - this.emit("delete", existing); - } - return existing; - } - for (const [audioSSRC, data] of this.map.entries()) { - if (data.userId === target) { - this.map.delete(audioSSRC); - this.emit("delete", data); - return data; - } - } - return void 0; - } -}; - -// src/receive/SpeakingMap.ts -var import_node_events7 = require("events"); -var SpeakingMap = class _SpeakingMap extends import_node_events7.EventEmitter { - static { - __name(this, "SpeakingMap"); - } - /** - * The delay after a packet is received from a user until they're marked as not speaking anymore. - */ - static DELAY = 100; - /** - * The currently speaking users, mapped to the milliseconds since UNIX epoch at which they started speaking. - */ - users; - speakingTimeouts; - constructor() { - super(); - this.users = /* @__PURE__ */ new Map(); - this.speakingTimeouts = /* @__PURE__ */ new Map(); - } - onPacket(userId) { - const timeout = this.speakingTimeouts.get(userId); - if (timeout) { - clearTimeout(timeout); - } else { - this.users.set(userId, Date.now()); - this.emit("start", userId); - } - this.startTimeout(userId); - } - startTimeout(userId) { - this.speakingTimeouts.set( - userId, - setTimeout(() => { - this.emit("end", userId); - this.speakingTimeouts.delete(userId); - this.users.delete(userId); - }, _SpeakingMap.DELAY) - ); - } -}; - -// src/receive/VoiceReceiver.ts -var HEADER_EXTENSION_BYTE = import_node_buffer7.Buffer.from([190, 222]); -var UNPADDED_NONCE_LENGTH = 4; -var AUTH_TAG_LENGTH = 16; -var VoiceReceiver = class { - static { - __name(this, "VoiceReceiver"); - } - /** - * The attached connection of this receiver. - */ - voiceConnection; - /** - * Maps SSRCs to Discord user ids. - */ - ssrcMap; - /** - * The current audio subscriptions of this receiver. - */ - subscriptions; - /** - * The connection data of the receiver. - * - * @internal - */ - connectionData; - /** - * The speaking map of the receiver. - */ - speaking; - constructor(voiceConnection) { - this.voiceConnection = voiceConnection; - this.ssrcMap = new SSRCMap(); - this.speaking = new SpeakingMap(); - this.subscriptions = /* @__PURE__ */ new Map(); - this.connectionData = {}; - this.onWsPacket = this.onWsPacket.bind(this); - this.onUdpMessage = this.onUdpMessage.bind(this); - } - /** - * Called when a packet is received on the attached connection's WebSocket. - * - * @param packet - The received packet - * @internal - */ - onWsPacket(packet) { - if (packet.op === import_v83.VoiceOpcodes.ClientDisconnect) { - this.ssrcMap.delete(packet.d.user_id); - } else if (packet.op === import_v83.VoiceOpcodes.Speaking) { - // [GMW FORK] Also register the user's video SSRC when present so the - // UDP receive path can decrypt/demux their H264 (camera/screen-share). - // op 12 (Speaking) carries `streams: [{ type, rid, quality, active, - // ssrc, rtx_ssrc, video_ssrc }]` when the member enables camera/ - // screen-share. We map the video stream SSRC -> userId alongside the - // audio SSRC. - let videoSSRC; - const streams = packet.d && Array.isArray(packet.d.streams) ? packet.d.streams : []; - for (const st of streams) { - const s = st && (st.ssrc ?? st.video_ssrc); - if (typeof s === "number" && s !== 0) { - videoSSRC = s; - break; - } - } - const update = { userId: packet.d.user_id, audioSSRC: packet.d.ssrc }; - if (typeof videoSSRC === "number") update.videoSSRC = videoSSRC; - this.ssrcMap.update(update); - } - } - decrypt(buffer, mode, nonce2, secretKey) { - buffer.copy(nonce2, 0, buffer.length - UNPADDED_NONCE_LENGTH); - let headerSize = 12; - const first = buffer.readUint8(); - if (first >> 4 & 1) headerSize += 4; - const header = buffer.subarray(0, headerSize); - const encrypted = buffer.subarray(headerSize, buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH); - const authTag = buffer.subarray( - buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH, - buffer.length - UNPADDED_NONCE_LENGTH - ); - switch (mode) { - case "aead_aes256_gcm_rtpsize": { - const decipheriv = import_node_crypto2.default.createDecipheriv("aes-256-gcm", secretKey, nonce2); - decipheriv.setAAD(header); - decipheriv.setAuthTag(authTag); - return import_node_buffer7.Buffer.concat([decipheriv.update(encrypted), decipheriv.final()]); - } - case "aead_xchacha20_poly1305_rtpsize": { - return import_node_buffer7.Buffer.from( - methods.crypto_aead_xchacha20poly1305_ietf_decrypt( - import_node_buffer7.Buffer.concat([encrypted, authTag]), - header, - nonce2, - secretKey - ) - ); - } - default: { - throw new RangeError(`Unsupported decryption method: ${mode}`); - } - } - } - /** - * Parses an audio packet, decrypting it to yield an Opus packet. - * - * @param buffer - The buffer to parse - * @param mode - The encryption mode - * @param nonce - The nonce buffer used by the connection for encryption - * @param secretKey - The secret key used by the connection for encryption - * @param userId - The user id that sent the packet - * @returns The parsed Opus packet - */ - parsePacket(buffer, mode, nonce2, secretKey, userId) { - let packet = this.decrypt(buffer, mode, nonce2, secretKey); - if (!packet) throw new Error("Failed to parse packet"); - const hasPadding = buffer[0] && Boolean(buffer[0] & 32); - if (hasPadding) { - const paddingAmount = packet[packet.length - 1]; - if (paddingAmount < packet.length) { - packet = packet.subarray(0, packet.length - paddingAmount); - } - } - if (buffer.subarray(12, 14).compare(HEADER_EXTENSION_BYTE) === 0) { - const headerExtensionLength = buffer.subarray(14).readUInt16BE(); - packet = packet.subarray(4 * headerExtensionLength); - } - if (this.voiceConnection.state.status === "ready" /* Ready */ && (this.voiceConnection.state.networking.state.code === 4 /* Ready */ || this.voiceConnection.state.networking.state.code === 5 /* Resuming */)) { - const daveSession = this.voiceConnection.state.networking.state.dave; - if (daveSession) packet = daveSession.decrypt(packet, userId); - } - return packet; - } - /** - * Called when the UDP socket of the attached connection receives a message. - * - * @param msg - The received message - * @internal - */ - onUdpMessage(msg) { - if (msg.length <= 8) return; - const ssrc = msg.readUInt32BE(8); - const userData = this.ssrcMap.get(ssrc); - if (!userData) return; - this.speaking.onPacket(userData.userId); - const stream = this.subscriptions.get(userData.userId); - if (!stream) return; - if (this.connectionData.encryptionMode && this.connectionData.nonceBuffer && this.connectionData.secretKey) { - if ((msg[1] & 127) !== RTP_OPUS_PAYLOAD_TYPE) return; - const rtpVersion = msg[0] >> 6; - if (rtpVersion !== 2) return; - try { - const packet = this.parsePacket( - msg, - this.connectionData.encryptionMode, - this.connectionData.nonceBuffer, - this.connectionData.secretKey, - userData.userId - ); - if (packet) stream.push(packet); - } catch (error) { - stream.destroy(error); - } - } - } - /** - * Creates a subscription for the given user id. - * - * @param target - The id of the user to subscribe to - * @returns A readable stream of Opus packets received from the target - */ - subscribe(userId, options) { - const existing = this.subscriptions.get(userId); - if (existing) return existing; - const stream = new AudioReceiveStream({ - ...createDefaultAudioReceiveStreamOptions(), - ...options - }); - stream.once("close", () => this.subscriptions.delete(userId)); - this.subscriptions.set(userId, stream); - return stream; - } -}; - -// src/VoiceConnection.ts -var VoiceConnectionStatus = /* @__PURE__ */ ((VoiceConnectionStatus2) => { - VoiceConnectionStatus2["Connecting"] = "connecting"; - VoiceConnectionStatus2["Destroyed"] = "destroyed"; - VoiceConnectionStatus2["Disconnected"] = "disconnected"; - VoiceConnectionStatus2["Ready"] = "ready"; - VoiceConnectionStatus2["Signalling"] = "signalling"; - return VoiceConnectionStatus2; -})(VoiceConnectionStatus || {}); -var VoiceConnectionDisconnectReason = /* @__PURE__ */ ((VoiceConnectionDisconnectReason2) => { - VoiceConnectionDisconnectReason2[VoiceConnectionDisconnectReason2["WebSocketClose"] = 0] = "WebSocketClose"; - VoiceConnectionDisconnectReason2[VoiceConnectionDisconnectReason2["AdapterUnavailable"] = 1] = "AdapterUnavailable"; - VoiceConnectionDisconnectReason2[VoiceConnectionDisconnectReason2["EndpointRemoved"] = 2] = "EndpointRemoved"; - VoiceConnectionDisconnectReason2[VoiceConnectionDisconnectReason2["Manual"] = 3] = "Manual"; - return VoiceConnectionDisconnectReason2; -})(VoiceConnectionDisconnectReason || {}); -var VoiceConnection = class extends import_node_events8.EventEmitter { - static { - __name(this, "VoiceConnection"); - } - /** - * The number of consecutive rejoin attempts. Initially 0, and increments for each rejoin. - * When a connection is successfully established, it resets to 0. - */ - rejoinAttempts; - /** - * The state of the voice connection. - */ - _state; - /** - * A configuration storing all the data needed to reconnect to a Guild's voice server. - * - * @internal - */ - joinConfig; - /** - * The two packets needed to successfully establish a voice connection. They are received - * from the main Discord gateway after signalling to change the voice state. - */ - packets; - /** - * The receiver of this voice connection. You should join the voice channel with `selfDeaf` set - * to false for this feature to work properly. - */ - receiver; - /** - * The debug logger function, if debugging is enabled. - */ - debug; - /** - * The options used to create this voice connection. - */ - options; - /** - * Creates a new voice connection. - * - * @param joinConfig - The data required to establish the voice connection - * @param options - The options used to create this voice connection - */ - constructor(joinConfig, options) { - super(); - this.debug = options.debug ? (message) => this.emit("debug", message) : null; - this.rejoinAttempts = 0; - this.receiver = new VoiceReceiver(this); - this.onNetworkingClose = this.onNetworkingClose.bind(this); - this.onNetworkingStateChange = this.onNetworkingStateChange.bind(this); - this.onNetworkingError = this.onNetworkingError.bind(this); - this.onNetworkingDebug = this.onNetworkingDebug.bind(this); - this.onNetworkingTransitioned = this.onNetworkingTransitioned.bind(this); - const adapter = options.adapterCreator({ - onVoiceServerUpdate: /* @__PURE__ */ __name((data) => this.addServerPacket(data), "onVoiceServerUpdate"), - onVoiceStateUpdate: /* @__PURE__ */ __name((data) => this.addStatePacket(data), "onVoiceStateUpdate"), - destroy: /* @__PURE__ */ __name(() => this.destroy(false), "destroy") - }); - this._state = { status: "signalling" /* Signalling */, adapter }; - this.packets = { - server: void 0, - state: void 0 - }; - this.joinConfig = joinConfig; - this.options = options; - } - /** - * The current state of the voice connection. - * - * @remarks - * The setter will perform clean-up operations where necessary. - */ - get state() { - return this._state; - } - set state(newState) { - const oldState = this._state; - const oldNetworking = Reflect.get(oldState, "networking"); - const newNetworking = Reflect.get(newState, "networking"); - const oldSubscription = Reflect.get(oldState, "subscription"); - const newSubscription = Reflect.get(newState, "subscription"); - if (oldNetworking !== newNetworking) { - if (oldNetworking) { - oldNetworking.on("error", noop); - oldNetworking.off("debug", this.onNetworkingDebug); - oldNetworking.off("error", this.onNetworkingError); - oldNetworking.off("close", this.onNetworkingClose); - oldNetworking.off("stateChange", this.onNetworkingStateChange); - oldNetworking.off("transitioned", this.onNetworkingTransitioned); - oldNetworking.destroy(); - } - if (newNetworking) this.updateReceiveBindings(newNetworking.state, oldNetworking?.state); - } - if (newState.status === "ready" /* Ready */) { - this.rejoinAttempts = 0; - } else if (newState.status === "destroyed" /* Destroyed */) { - for (const stream of this.receiver.subscriptions.values()) { - if (!stream.destroyed) stream.destroy(); - } - } - if (oldState.status !== "destroyed" /* Destroyed */ && newState.status === "destroyed" /* Destroyed */) { - oldState.adapter.destroy(); - } - this._state = newState; - if (oldSubscription && oldSubscription !== newSubscription) { - oldSubscription.unsubscribe(); - } - this.emit("stateChange", oldState, newState); - if (oldState.status !== newState.status) { - this.emit(newState.status, oldState, newState); - } - } - /** - * Registers a `VOICE_SERVER_UPDATE` packet to the voice connection. This will cause it to reconnect using the - * new data provided in the packet. - * - * @param packet - The received `VOICE_SERVER_UPDATE` packet - */ - addServerPacket(packet) { - this.packets.server = packet; - if (packet.endpoint) { - this.configureNetworking(); - } else if (this.state.status !== "destroyed" /* Destroyed */) { - this.state = { - ...this.state, - status: "disconnected" /* Disconnected */, - reason: 2 /* EndpointRemoved */ - }; - } - } - /** - * Registers a `VOICE_STATE_UPDATE` packet to the voice connection. Most importantly, it stores the id of the - * channel that the client is connected to. - * - * @param packet - The received `VOICE_STATE_UPDATE` packet - */ - addStatePacket(packet) { - this.packets.state = packet; - if (packet.self_deaf !== void 0) this.joinConfig.selfDeaf = packet.self_deaf; - if (packet.self_mute !== void 0) this.joinConfig.selfMute = packet.self_mute; - if (packet.channel_id) this.joinConfig.channelId = packet.channel_id; - } - /** - * Called when the networking state changes, and the new ws/udp packet/message handlers need to be rebound - * to the new instances. - * - * @param newState - The new networking state - * @param oldState - The old networking state, if there is one - */ - updateReceiveBindings(newState, oldState) { - const oldWs = Reflect.get(oldState ?? {}, "ws"); - const newWs = Reflect.get(newState, "ws"); - const oldUdp = Reflect.get(oldState ?? {}, "udp"); - const newUdp = Reflect.get(newState, "udp"); - if (oldWs !== newWs) { - oldWs?.off("packet", this.receiver.onWsPacket); - newWs?.on("packet", this.receiver.onWsPacket); - } - if (oldUdp !== newUdp) { - oldUdp?.off("message", this.receiver.onUdpMessage); - newUdp?.on("message", this.receiver.onUdpMessage); - } - this.receiver.connectionData = Reflect.get(newState, "connectionData") ?? {}; - } - /** - * Attempts to configure a networking instance for this voice connection using the received packets. - * Both packets are required, and any existing networking instance will be destroyed. - * - * @remarks - * This is called when the voice server of the connection changes, e.g. if the bot is moved into a - * different channel in the same guild but has a different voice server. In this instance, the connection - * needs to be re-established to the new voice server. - * - * The connection will transition to the Connecting state when this is called. - */ - configureNetworking() { - const { server, state } = this.packets; - if (!server || !state || this.state.status === "destroyed" /* Destroyed */ || !server.endpoint) return; - const networking = new Networking( - { - endpoint: server.endpoint, - serverId: server.guild_id, - token: server.token, - sessionId: state.session_id, - userId: state.user_id, - channelId: state.channel_id - }, - { - debug: Boolean(this.debug), - daveEncryption: this.options.daveEncryption ?? true, - decryptionFailureTolerance: this.options.decryptionFailureTolerance - } - ); - networking.once("close", this.onNetworkingClose); - networking.on("stateChange", this.onNetworkingStateChange); - networking.on("error", this.onNetworkingError); - networking.on("debug", this.onNetworkingDebug); - networking.on("transitioned", this.onNetworkingTransitioned); - this.state = { - ...this.state, - status: "connecting" /* Connecting */, - networking - }; - } - /** - * Called when the networking instance for this connection closes. If the close code is 4014 (do not reconnect), - * the voice connection will transition to the Disconnected state which will store the close code. You can - * decide whether or not to reconnect when this occurs by listening for the state change and calling reconnect(). - * - * @remarks - * If the close code was anything other than 4014, it is likely that the closing was not intended, and so the - * VoiceConnection will signal to Discord that it would like to rejoin the channel. This automatically attempts - * to re-establish the connection. This would be seen as a transition from the Ready state to the Signalling state. - * @param code - The close code - */ - onNetworkingClose(code) { - if (this.state.status === "destroyed" /* Destroyed */) return; - if (code === 4014) { - this.state = { - ...this.state, - status: "disconnected" /* Disconnected */, - reason: 0 /* WebSocketClose */, - closeCode: code - }; - } else { - this.state = { - ...this.state, - status: "signalling" /* Signalling */ - }; - this.rejoinAttempts++; - if (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) { - this.state = { - ...this.state, - status: "disconnected" /* Disconnected */, - reason: 1 /* AdapterUnavailable */ - }; - } - } - } - /** - * Called when the state of the networking instance changes. This is used to derive the state of the voice connection. - * - * @param oldState - The previous state - * @param newState - The new state - */ - onNetworkingStateChange(oldState, newState) { - this.updateReceiveBindings(newState, oldState); - if (oldState.code === newState.code) return; - if (this.state.status !== "connecting" /* Connecting */ && this.state.status !== "ready" /* Ready */) - return; - if (newState.code === 4 /* Ready */) { - this.state = { - ...this.state, - status: "ready" /* Ready */ - }; - } else if (newState.code !== 6 /* Closed */) { - this.state = { - ...this.state, - status: "connecting" /* Connecting */ - }; - } - } - /** - * Propagates errors from the underlying network instance. - * - * @param error - The error to propagate - */ - onNetworkingError(error) { - this.emit("error", error); - } - /** - * Propagates debug messages from the underlying network instance. - * - * @param message - The debug message to propagate - */ - onNetworkingDebug(message) { - this.debug?.(`[NW] ${message}`); - } - /** - * Propagates transitions from the underlying network instance. - * - * @param transitionId - The transition id - */ - onNetworkingTransitioned(transitionId) { - this.emit("transitioned", transitionId); - } - /** - * Prepares an audio packet for dispatch. - * - * @param buffer - The Opus packet to prepare - */ - prepareAudioPacket(buffer) { - const state = this.state; - if (state.status !== "ready" /* Ready */) return; - return state.networking.prepareAudioPacket(buffer); - } - /** - * Dispatches the previously prepared audio packet (if any) - */ - dispatchAudio() { - const state = this.state; - if (state.status !== "ready" /* Ready */) return; - return state.networking.dispatchAudio(); - } - /** - * Prepares an audio packet and dispatches it immediately. - * - * @param buffer - The Opus packet to play - */ - playOpusPacket(buffer) { - const state = this.state; - if (state.status !== "ready" /* Ready */) return; - state.networking.prepareAudioPacket(buffer); - return state.networking.dispatchAudio(); - } - /** - * Destroys the VoiceConnection, preventing it from connecting to voice again. - * This method should be called when you no longer require the VoiceConnection to - * prevent memory leaks. - * - * @param adapterAvailable - Whether the adapter can be used - */ - destroy(adapterAvailable = true) { - if (this.state.status === "destroyed" /* Destroyed */) { - throw new Error("Cannot destroy VoiceConnection - it has already been destroyed"); - } - if (getVoiceConnection(this.joinConfig.guildId, this.joinConfig.group) === this) { - untrackVoiceConnection(this); - } - if (adapterAvailable) { - this.state.adapter.sendPayload(createJoinVoiceChannelPayload({ ...this.joinConfig, channelId: null })); - } - this.state = { - status: "destroyed" /* Destroyed */ - }; - } - /** - * Disconnects the VoiceConnection, allowing the possibility of rejoining later on. - * - * @returns `true` if the connection was successfully disconnected - */ - disconnect() { - if (this.state.status === "destroyed" /* Destroyed */ || this.state.status === "signalling" /* Signalling */) { - return false; - } - this.joinConfig.channelId = null; - if (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) { - this.state = { - adapter: this.state.adapter, - subscription: this.state.subscription, - status: "disconnected" /* Disconnected */, - reason: 1 /* AdapterUnavailable */ - }; - return false; - } - this.state = { - adapter: this.state.adapter, - reason: 3 /* Manual */, - status: "disconnected" /* Disconnected */ - }; - return true; - } - /** - * Attempts to rejoin (better explanation soon:tm:) - * - * @remarks - * Calling this method successfully will automatically increment the `rejoinAttempts` counter, - * which you can use to inform whether or not you'd like to keep attempting to reconnect your - * voice connection. - * - * A state transition from Disconnected to Signalling will be observed when this is called. - */ - rejoin(joinConfig) { - if (this.state.status === "destroyed" /* Destroyed */) { - return false; - } - const notReady = this.state.status !== "ready" /* Ready */; - if (notReady) this.rejoinAttempts++; - Object.assign(this.joinConfig, joinConfig); - if (this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) { - if (notReady) { - this.state = { - ...this.state, - status: "signalling" /* Signalling */ - }; - } - return true; - } - this.state = { - adapter: this.state.adapter, - subscription: this.state.subscription, - status: "disconnected" /* Disconnected */, - reason: 1 /* AdapterUnavailable */ - }; - return false; - } - /** - * Updates the speaking status of the voice connection. This is used when audio players are done playing audio, - * and need to signal that the connection is no longer playing audio. - * - * @param enabled - Whether or not to show as speaking - */ - setSpeaking(enabled) { - if (this.state.status !== "ready" /* Ready */) return false; - return this.state.networking.setSpeaking(enabled); - } - /** - * Subscribes to an audio player, allowing the player to play audio on this voice connection. - * - * @param player - The audio player to subscribe to - * @returns The created subscription - */ - subscribe(player) { - if (this.state.status === "destroyed" /* Destroyed */) return; - const subscription = player["subscribe"](this); - this.state = { - ...this.state, - subscription - }; - return subscription; - } - /** - * The latest ping (in milliseconds) for the WebSocket connection and audio playback for this voice - * connection, if this data is available. - * - * @remarks - * For this data to be available, the VoiceConnection must be in the Ready state, and its underlying - * WebSocket connection and UDP socket must have had at least one ping-pong exchange. - */ - get ping() { - if (this.state.status === "ready" /* Ready */ && this.state.networking.state.code === 4 /* Ready */) { - return { - ws: this.state.networking.state.ws.ping, - udp: this.state.networking.state.udp.ping - }; - } - return { - ws: void 0, - udp: void 0 - }; - } - /** - * The current voice privacy code of the encrypted session. - * - * @remarks - * For this data to be available, the VoiceConnection must be in the Ready state, - * and the connection would have to be end-to-end encrypted. - */ - get voicePrivacyCode() { - if (this.state.status === "ready" /* Ready */ && this.state.networking.state.code === 4 /* Ready */) { - return this.state.networking.state.dave?.voicePrivacyCode ?? void 0; - } - return void 0; - } - /** - * Gets the verification code for a user in the session. - * - * @throws Will throw if end-to-end encryption is not on or if the user id provided is not in the session. - */ - async getVerificationCode(userId) { - if (this.state.status === "ready" /* Ready */ && this.state.networking.state.code === 4 /* Ready */ && this.state.networking.state.dave) { - return this.state.networking.state.dave.getVerificationCode(userId); - } - throw new Error("Session not available"); - } - /** - * Called when a subscription of this voice connection to an audio player is removed. - * - * @param subscription - The removed subscription - */ - onSubscriptionRemoved(subscription) { - if (this.state.status !== "destroyed" /* Destroyed */ && this.state.subscription === subscription) { - this.state = { - ...this.state, - subscription: void 0 - }; - } - } -}; -function createVoiceConnection(joinConfig, options) { - const payload = createJoinVoiceChannelPayload(joinConfig); - const existing = getVoiceConnection(joinConfig.guildId, joinConfig.group); - if (existing && existing.state.status !== "destroyed" /* Destroyed */) { - if (existing.state.status === "disconnected" /* Disconnected */) { - existing.rejoin({ - channelId: joinConfig.channelId, - selfDeaf: joinConfig.selfDeaf, - selfMute: joinConfig.selfMute - }); - } else if (!existing.state.adapter.sendPayload(payload)) { - existing.state = { - ...existing.state, - status: "disconnected" /* Disconnected */, - reason: 1 /* AdapterUnavailable */ - }; - } - return existing; - } - const voiceConnection = new VoiceConnection(joinConfig, options); - trackVoiceConnection(voiceConnection); - if (voiceConnection.state.status !== "destroyed" /* Destroyed */ && !voiceConnection.state.adapter.sendPayload(payload)) { - voiceConnection.state = { - ...voiceConnection.state, - status: "disconnected" /* Disconnected */, - reason: 1 /* AdapterUnavailable */ - }; - } - return voiceConnection; -} -__name(createVoiceConnection, "createVoiceConnection"); - -// src/joinVoiceChannel.ts -function joinVoiceChannel(options) { - const joinConfig = { - selfDeaf: true, - selfMute: false, - group: "default", - ...options - }; - return createVoiceConnection(joinConfig, { - adapterCreator: options.adapterCreator, - debug: options.debug, - daveEncryption: options.daveEncryption, - decryptionFailureTolerance: options.decryptionFailureTolerance - }); -} -__name(joinVoiceChannel, "joinVoiceChannel"); - -// src/audio/AudioResource.ts -var import_node_stream2 = require("stream"); -var import_prism_media2 = __toESM(require("prism-media")); - -// src/audio/TransformerGraph.ts -var import_prism_media = __toESM(require("prism-media")); -var FFMPEG_PCM_ARGUMENTS = ["-analyzeduration", "0", "-loglevel", "0", "-f", "s16le", "-ar", "48000", "-ac", "2"]; -var FFMPEG_OPUS_ARGUMENTS = [ - "-analyzeduration", - "0", - "-loglevel", - "0", - "-acodec", - "libopus", - "-f", - "opus", - "-ar", - "48000", - "-ac", - "2" -]; -var StreamType = /* @__PURE__ */ ((StreamType2) => { - StreamType2["Arbitrary"] = "arbitrary"; - StreamType2["OggOpus"] = "ogg/opus"; - StreamType2["Opus"] = "opus"; - StreamType2["Raw"] = "raw"; - StreamType2["WebmOpus"] = "webm/opus"; - return StreamType2; -})(StreamType || {}); -var TransformerType = /* @__PURE__ */ ((TransformerType2) => { - TransformerType2["FFmpegOgg"] = "ffmpeg ogg"; - TransformerType2["FFmpegPCM"] = "ffmpeg pcm"; - TransformerType2["InlineVolume"] = "volume transformer"; - TransformerType2["OggOpusDemuxer"] = "ogg/opus demuxer"; - TransformerType2["OpusDecoder"] = "opus decoder"; - TransformerType2["OpusEncoder"] = "opus encoder"; - TransformerType2["WebmOpusDemuxer"] = "webm/opus demuxer"; - return TransformerType2; -})(TransformerType || {}); -var Node = class { - static { - __name(this, "Node"); - } - /** - * The outbound edges from this node. - */ - edges = []; - /** - * The type of stream for this node. - */ - type; - constructor(type) { - this.type = type; - } - /** - * Creates an outbound edge from this node. - * - * @param edge - The edge to create - */ - addEdge(edge) { - this.edges.push({ ...edge, from: this }); - } -}; -var NODES = null; -function getNode(type) { - const node = (NODES ??= initializeNodes()).get(type); - if (!node) throw new Error(`Node type '${type}' does not exist!`); - return node; -} -__name(getNode, "getNode"); -function canEnableFFmpegOptimizations() { - try { - return import_prism_media.default.FFmpeg.getInfo().output.includes("--enable-libopus"); - } catch { - } - return false; -} -__name(canEnableFFmpegOptimizations, "canEnableFFmpegOptimizations"); -function initializeNodes() { - const nodes = /* @__PURE__ */ new Map(); - for (const streamType of Object.values(StreamType)) { - nodes.set(streamType, new Node(streamType)); - } - nodes.get("raw" /* Raw */).addEdge({ - type: "opus encoder" /* OpusEncoder */, - to: nodes.get("opus" /* Opus */), - cost: 1.5, - transformer: /* @__PURE__ */ __name(() => new import_prism_media.default.opus.Encoder({ rate: 48e3, channels: 2, frameSize: 960 }), "transformer") - }); - nodes.get("opus" /* Opus */).addEdge({ - type: "opus decoder" /* OpusDecoder */, - to: nodes.get("raw" /* Raw */), - cost: 1.5, - transformer: /* @__PURE__ */ __name(() => new import_prism_media.default.opus.Decoder({ rate: 48e3, channels: 2, frameSize: 960 }), "transformer") - }); - nodes.get("ogg/opus" /* OggOpus */).addEdge({ - type: "ogg/opus demuxer" /* OggOpusDemuxer */, - to: nodes.get("opus" /* Opus */), - cost: 1, - transformer: /* @__PURE__ */ __name(() => new import_prism_media.default.opus.OggDemuxer(), "transformer") - }); - nodes.get("webm/opus" /* WebmOpus */).addEdge({ - type: "webm/opus demuxer" /* WebmOpusDemuxer */, - to: nodes.get("opus" /* Opus */), - cost: 1, - transformer: /* @__PURE__ */ __name(() => new import_prism_media.default.opus.WebmDemuxer(), "transformer") - }); - const FFMPEG_PCM_EDGE = { - type: "ffmpeg pcm" /* FFmpegPCM */, - to: nodes.get("raw" /* Raw */), - cost: 2, - transformer: /* @__PURE__ */ __name((input) => new import_prism_media.default.FFmpeg({ - args: ["-i", typeof input === "string" ? input : "-", ...FFMPEG_PCM_ARGUMENTS] - }), "transformer") - }; - nodes.get("arbitrary" /* Arbitrary */).addEdge(FFMPEG_PCM_EDGE); - nodes.get("ogg/opus" /* OggOpus */).addEdge(FFMPEG_PCM_EDGE); - nodes.get("webm/opus" /* WebmOpus */).addEdge(FFMPEG_PCM_EDGE); - nodes.get("raw" /* Raw */).addEdge({ - type: "volume transformer" /* InlineVolume */, - to: nodes.get("raw" /* Raw */), - cost: 0.5, - transformer: /* @__PURE__ */ __name(() => new import_prism_media.default.VolumeTransformer({ type: "s16le" }), "transformer") - }); - if (canEnableFFmpegOptimizations()) { - const FFMPEG_OGG_EDGE = { - type: "ffmpeg ogg" /* FFmpegOgg */, - to: nodes.get("ogg/opus" /* OggOpus */), - cost: 2, - transformer: /* @__PURE__ */ __name((input) => new import_prism_media.default.FFmpeg({ - args: ["-i", typeof input === "string" ? input : "-", ...FFMPEG_OPUS_ARGUMENTS] - }), "transformer") - }; - nodes.get("arbitrary" /* Arbitrary */).addEdge(FFMPEG_OGG_EDGE); - nodes.get("ogg/opus" /* OggOpus */).addEdge(FFMPEG_OGG_EDGE); - nodes.get("webm/opus" /* WebmOpus */).addEdge(FFMPEG_OGG_EDGE); - } - return nodes; -} -__name(initializeNodes, "initializeNodes"); -function findPath(from, constraints, goal = getNode("opus" /* Opus */), path = [], depth = 5) { - if (from === goal && constraints(path)) { - return { cost: 0 }; - } else if (depth === 0) { - return { cost: Number.POSITIVE_INFINITY }; - } - let currentBest; - for (const edge of from.edges) { - if (currentBest && edge.cost > currentBest.cost) continue; - const next = findPath(edge.to, constraints, goal, [...path, edge], depth - 1); - const cost = edge.cost + next.cost; - if (!currentBest || cost < currentBest.cost) { - currentBest = { cost, edge, next }; - } - } - return currentBest ?? { cost: Number.POSITIVE_INFINITY }; -} -__name(findPath, "findPath"); -function constructPipeline(step) { - const edges = []; - let current = step; - while (current?.edge) { - edges.push(current.edge); - current = current.next; - } - return edges; -} -__name(constructPipeline, "constructPipeline"); -function findPipeline(from, constraint) { - return constructPipeline(findPath(getNode(from), constraint)); -} -__name(findPipeline, "findPipeline"); - -// src/audio/AudioResource.ts -var AudioResource = class { - static { - __name(this, "AudioResource"); - } - /** - * An object-mode Readable stream that emits Opus packets. This is what is played by audio players. - */ - playStream; - /** - * The pipeline used to convert the input stream into a playable format. For example, this may - * contain an FFmpeg component for arbitrary inputs, and it may contain a VolumeTransformer component - * for resources with inline volume transformation enabled. - */ - edges; - /** - * Optional metadata that can be used to identify the resource. - */ - metadata; - /** - * If the resource was created with inline volume transformation enabled, then this will be a - * prism-media VolumeTransformer. You can use this to alter the volume of the stream. - */ - volume; - /** - * If using an Opus encoder to create this audio resource, then this will be a prism-media opus.Encoder. - * You can use this to control settings such as bitrate, FEC, PLP. - */ - encoder; - /** - * The audio player that the resource is subscribed to, if any. - */ - audioPlayer; - /** - * The playback duration of this audio resource, given in milliseconds. - */ - playbackDuration = 0; - /** - * Whether or not the stream for this resource has started (data has become readable) - */ - started = false; - /** - * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches. - */ - silencePaddingFrames; - /** - * The number of remaining silence frames to play. If -1, the frames have not yet started playing. - */ - silenceRemaining = -1; - constructor(edges, streams, metadata, silencePaddingFrames) { - this.edges = edges; - this.playStream = streams.length > 1 ? (0, import_node_stream2.pipeline)(streams, noop) : streams[0]; - this.metadata = metadata; - this.silencePaddingFrames = silencePaddingFrames; - for (const stream of streams) { - if (stream instanceof import_prism_media2.default.VolumeTransformer) { - this.volume = stream; - } else if (stream instanceof import_prism_media2.default.opus.Encoder) { - this.encoder = stream; - } - } - this.playStream.once("readable", () => this.started = true); - } - /** - * Whether this resource is readable. If the underlying resource is no longer readable, this will still return true - * while there are silence padding frames left to play. - */ - get readable() { - if (this.silenceRemaining === 0) return false; - const real = this.playStream.readable; - if (!real) { - if (this.silenceRemaining === -1) this.silenceRemaining = this.silencePaddingFrames; - return this.silenceRemaining !== 0; - } - return real; - } - /** - * Whether this resource has ended or not. - */ - get ended() { - return this.playStream.readableEnded || this.playStream.destroyed || this.silenceRemaining === 0; - } - /** - * Attempts to read an Opus packet from the audio resource. If a packet is available, the playbackDuration - * is incremented. - * - * @remarks - * It is advisable to check that the playStream is readable before calling this method. While no runtime - * errors will be thrown, you should check that the resource is still available before attempting to - * read from it. - * @internal - */ - read() { - if (this.silenceRemaining === 0) { - return null; - } else if (this.silenceRemaining > 0) { - this.silenceRemaining--; - return SILENCE_FRAME; - } - const packet = this.playStream.read(); - if (packet) { - this.playbackDuration += 20; - } - return packet; - } -}; -var VOLUME_CONSTRAINT = /* @__PURE__ */ __name((path) => path.some((edge) => edge.type === "volume transformer" /* InlineVolume */), "VOLUME_CONSTRAINT"); -var NO_CONSTRAINT = /* @__PURE__ */ __name(() => true, "NO_CONSTRAINT"); -function inferStreamType(stream) { - if (stream instanceof import_prism_media2.default.opus.Encoder) { - return { streamType: "opus" /* Opus */, hasVolume: false }; - } else if (stream instanceof import_prism_media2.default.opus.Decoder) { - return { streamType: "raw" /* Raw */, hasVolume: false }; - } else if (stream instanceof import_prism_media2.default.VolumeTransformer) { - return { streamType: "raw" /* Raw */, hasVolume: true }; - } else if (stream instanceof import_prism_media2.default.opus.OggDemuxer) { - return { streamType: "opus" /* Opus */, hasVolume: false }; - } else if (stream instanceof import_prism_media2.default.opus.WebmDemuxer) { - return { streamType: "opus" /* Opus */, hasVolume: false }; - } - return { streamType: "arbitrary" /* Arbitrary */, hasVolume: false }; -} -__name(inferStreamType, "inferStreamType"); -function createAudioResource(input, options = {}) { - let inputType = options.inputType; - let needsInlineVolume = Boolean(options.inlineVolume); - if (typeof input === "string") { - inputType = "arbitrary" /* Arbitrary */; - } else if (inputType === void 0) { - const analysis = inferStreamType(input); - inputType = analysis.streamType; - needsInlineVolume = needsInlineVolume && !analysis.hasVolume; - } - const transformerPipeline = findPipeline(inputType, needsInlineVolume ? VOLUME_CONSTRAINT : NO_CONSTRAINT); - if (transformerPipeline.length === 0) { - if (typeof input === "string") throw new Error(`Invalid pipeline constructed for string resource '${input}'`); - return new AudioResource( - [], - [input], - options.metadata ?? null, - options.silencePaddingFrames ?? 5 - ); - } - const streams = transformerPipeline.map((edge) => edge.transformer(input)); - if (typeof input !== "string") streams.unshift(input); - return new AudioResource( - transformerPipeline, - streams, - options.metadata ?? null, - options.silencePaddingFrames ?? 5 - ); -} -__name(createAudioResource, "createAudioResource"); - -// src/util/generateDependencyReport.ts -var import_node_crypto3 = require("crypto"); -var import_node_path = require("path"); -var import_prism_media3 = __toESM(require("prism-media")); -function findPackageJSON(dir, packageName, depth) { - if (depth === 0) return void 0; - const attemptedPath = (0, import_node_path.resolve)(dir, "./package.json"); - try { - const pkg = require(attemptedPath); - if (pkg.name !== packageName) throw new Error("package.json does not match"); - return pkg; - } catch { - return findPackageJSON((0, import_node_path.resolve)(dir, ".."), packageName, depth - 1); - } -} -__name(findPackageJSON, "findPackageJSON"); -function version(name) { - try { - if (name === "@discordjs/voice") { - return "0.19.2"; - } - const pkg = findPackageJSON((0, import_node_path.dirname)(require.resolve(name)), name, 3); - return pkg?.version ?? "not found"; - } catch { - return "not found"; - } -} -__name(version, "version"); -function generateDependencyReport() { - const report = []; - const addVersion = /* @__PURE__ */ __name((name) => report.push(`- ${name}: ${version(name)}`), "addVersion"); - report.push("Core Dependencies"); - addVersion("@discordjs/voice"); - addVersion("prism-media"); - report.push(""); - report.push("Opus Libraries"); - addVersion("@discordjs/opus"); - addVersion("opusscript"); - report.push(""); - report.push("Encryption Libraries"); - report.push(`- native crypto support for aes-256-gcm: ${(0, import_node_crypto3.getCiphers)().includes("aes-256-gcm") ? "yes" : "no"}`); - addVersion("sodium-native"); - addVersion("sodium"); - addVersion("libsodium-wrappers"); - addVersion("@stablelib/xchacha20poly1305"); - addVersion("@noble/ciphers"); - report.push(""); - report.push("DAVE Libraries"); - addVersion("@snazzah/davey"); - report.push(""); - report.push("FFmpeg"); - try { - const info = import_prism_media3.default.FFmpeg.getInfo(); - report.push(`- version: ${info.version}`); - report.push(`- libopus: ${info.output.includes("--enable-libopus") ? "yes" : "no"}`); - } catch { - report.push("- not found"); - } - return ["-".repeat(50), ...report, "-".repeat(50)].join("\n"); -} -__name(generateDependencyReport, "generateDependencyReport"); - -// src/util/entersState.ts -var import_node_events9 = require("events"); - -// src/util/abortAfter.ts -function abortAfter(delay) { - const ac = new AbortController(); - const timeout = setTimeout(() => ac.abort(), delay); - ac.signal.addEventListener("abort", () => clearTimeout(timeout)); - return [ac, ac.signal]; -} -__name(abortAfter, "abortAfter"); - -// src/util/entersState.ts -async function entersState(target, status, timeoutOrSignal) { - if (target.state.status !== status) { - const [ac, signal] = typeof timeoutOrSignal === "number" ? abortAfter(timeoutOrSignal) : [void 0, timeoutOrSignal]; - try { - await (0, import_node_events9.once)(target, status, { signal }); - } finally { - ac?.abort(); - } - } - return target; -} -__name(entersState, "entersState"); - -// src/util/demuxProbe.ts -var import_node_buffer8 = require("buffer"); -var import_node_process2 = __toESM(require("process")); -var import_node_stream3 = require("stream"); -var import_prism_media4 = __toESM(require("prism-media")); -function validateDiscordOpusHead(opusHead) { - const channels = opusHead.readUInt8(9); - const sampleRate = opusHead.readUInt32LE(12); - return channels === 2 && sampleRate === 48e3; -} -__name(validateDiscordOpusHead, "validateDiscordOpusHead"); -async function demuxProbe(stream, probeSize = 1024, validator = validateDiscordOpusHead) { - return new Promise((resolve2, reject) => { - if (stream.readableObjectMode) { - reject(new Error("Cannot probe a readable stream in object mode")); - return; - } - if (stream.readableEnded) { - reject(new Error("Cannot probe a stream that has ended")); - return; - } - let readBuffer = import_node_buffer8.Buffer.alloc(0); - let resolved; - const finish = /* @__PURE__ */ __name((type) => { - stream.off("data", onData); - stream.off("close", onClose); - stream.off("end", onClose); - stream.pause(); - resolved = type; - if (stream.readableEnded) { - resolve2({ - stream: import_node_stream3.Readable.from(readBuffer), - type - }); - } else { - if (readBuffer.length > 0) { - stream.push(readBuffer); - } - resolve2({ - stream, - type - }); - } - }, "finish"); - const foundHead = /* @__PURE__ */ __name((type) => (head) => { - if (validator(head)) { - finish(type); - } - }, "foundHead"); - const webm = new import_prism_media4.default.opus.WebmDemuxer(); - webm.once("error", noop); - webm.on("head", foundHead("webm/opus" /* WebmOpus */)); - const ogg = new import_prism_media4.default.opus.OggDemuxer(); - ogg.once("error", noop); - ogg.on("head", foundHead("ogg/opus" /* OggOpus */)); - const onClose = /* @__PURE__ */ __name(() => { - if (!resolved) { - finish("arbitrary" /* Arbitrary */); - } - }, "onClose"); - const onData = /* @__PURE__ */ __name((buffer) => { - readBuffer = import_node_buffer8.Buffer.concat([readBuffer, buffer]); - webm.write(buffer); - ogg.write(buffer); - if (readBuffer.length >= probeSize) { - stream.off("data", onData); - stream.pause(); - import_node_process2.default.nextTick(onClose); - } - }, "onData"); - stream.once("error", reject); - stream.on("data", onData); - stream.once("close", onClose); - stream.once("end", onClose); - }); -} -__name(demuxProbe, "demuxProbe"); - -// src/index.ts -var version2 = "0.19.2"; -// Annotate the CommonJS export names for ESM import in node: -0 && (module.exports = { - AudioPlayer, - AudioPlayerError, - AudioPlayerStatus, - AudioReceiveStream, - AudioResource, - DAVESession, - EndBehaviorType, - Networking, - NetworkingStatusCode, - NoSubscriberBehavior, - Node, - PlayerSubscription, - RTP_OPUS_PAYLOAD_TYPE, - SSRCMap, - SpeakingMap, - StreamType, - TransformerType, - VoiceConnection, - VoiceConnectionDisconnectReason, - VoiceConnectionStatus, - VoiceReceiver, - VoiceUDPSocket, - VoiceWebSocket, - createAudioPlayer, - createAudioResource, - createDefaultAudioReceiveStreamOptions, - demuxProbe, - entersState, - generateDependencyReport, - getGroups, - getVoiceConnection, - getVoiceConnections, - joinVoiceChannel, - validateDiscordOpusHead, - version -}); -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/services/discord-gateway/vendor/discord-voice-fork/dist/index.js.map b/services/discord-gateway/vendor/discord-voice-fork/dist/index.js.map deleted file mode 100644 index 67d4d431..00000000 --- a/services/discord-gateway/vendor/discord-voice-fork/dist/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../src/index.ts","../src/VoiceConnection.ts","../src/DataStore.ts","../src/networking/Networking.ts","../src/util/Secretbox.ts","../src/util/constants.ts","../src/util/util.ts","../src/networking/DAVESession.ts","../src/audio/AudioPlayer.ts","../src/audio/AudioPlayerError.ts","../src/audio/PlayerSubscription.ts","../src/networking/VoiceUDPSocket.ts","../src/networking/VoiceWebSocket.ts","../src/receive/VoiceReceiver.ts","../src/receive/AudioReceiveStream.ts","../src/receive/SSRCMap.ts","../src/receive/SpeakingMap.ts","../src/joinVoiceChannel.ts","../src/audio/AudioResource.ts","../src/audio/TransformerGraph.ts","../src/util/generateDependencyReport.ts","../src/util/entersState.ts","../src/util/abortAfter.ts","../src/util/demuxProbe.ts"],"sourcesContent":["export * from './joinVoiceChannel';\nexport * from './audio/index';\nexport * from './util/index';\nexport * from './receive/index';\n\nexport {\n\tNetworking,\n\ttype ConnectionData,\n\ttype ConnectionOptions,\n\ttype NetworkingState,\n\ttype NetworkingResumingState,\n\ttype NetworkingSelectingProtocolState,\n\ttype NetworkingUdpHandshakingState,\n\ttype NetworkingClosedState,\n\ttype NetworkingIdentifyingState,\n\ttype NetworkingOpeningWsState,\n\ttype NetworkingReadyState,\n\tNetworkingStatusCode,\n\tVoiceUDPSocket,\n\tVoiceWebSocket,\n\ttype SocketConfig,\n\tDAVESession,\n} from './networking/index.js';\n\nexport {\n\tVoiceConnection,\n\ttype VoiceConnectionState,\n\tVoiceConnectionStatus,\n\ttype VoiceConnectionConnectingState,\n\ttype VoiceConnectionDestroyedState,\n\ttype VoiceConnectionDisconnectedState,\n\ttype VoiceConnectionDisconnectedBaseState,\n\ttype VoiceConnectionDisconnectedOtherState,\n\ttype VoiceConnectionDisconnectedWebSocketState,\n\tVoiceConnectionDisconnectReason,\n\ttype VoiceConnectionReadyState,\n\ttype VoiceConnectionSignallingState,\n} from './VoiceConnection';\n\nexport { type JoinConfig, getVoiceConnection, getVoiceConnections, getGroups } from './DataStore';\n\n/**\n * The {@link https://github.com/discordjs/discord.js/blob/main/packages/voice#readme | @discordjs/voice} version\n * that you are currently using.\n */\n// This needs to explicitly be `string` so it is not typed as a \"const string\" that gets injected by esbuild\nexport const version = '0.19.2' as string;\n","/* eslint-disable @typescript-eslint/unbound-method */\nimport type { Buffer } from 'node:buffer';\nimport { EventEmitter } from 'node:events';\nimport type { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v10';\nimport type { JoinConfig } from './DataStore';\nimport {\n\tgetVoiceConnection,\n\tcreateJoinVoiceChannelPayload,\n\ttrackVoiceConnection,\n\tuntrackVoiceConnection,\n} from './DataStore';\nimport type { AudioPlayer } from './audio/AudioPlayer';\nimport type { PlayerSubscription } from './audio/PlayerSubscription';\nimport type { VoiceWebSocket, VoiceUDPSocket } from './networking';\nimport { Networking, NetworkingStatusCode, type NetworkingState } from './networking/Networking';\nimport { VoiceReceiver } from './receive/index';\nimport type { DiscordGatewayAdapterImplementerMethods } from './util/adapter';\nimport { noop } from './util/util';\nimport type { CreateVoiceConnectionOptions } from './index';\n\n/**\n * The various status codes a voice connection can hold at any one time.\n */\nexport enum VoiceConnectionStatus {\n\t/**\n\t * The `VOICE_SERVER_UPDATE` and `VOICE_STATE_UPDATE` packets have been received, now attempting to establish a voice connection.\n\t */\n\tConnecting = 'connecting',\n\n\t/**\n\t * The voice connection has been destroyed and untracked, it cannot be reused.\n\t */\n\tDestroyed = 'destroyed',\n\n\t/**\n\t * The voice connection has either been severed or not established.\n\t */\n\tDisconnected = 'disconnected',\n\n\t/**\n\t * A voice connection has been established, and is ready to be used.\n\t */\n\tReady = 'ready',\n\n\t/**\n\t * Sending a packet to the main Discord gateway to indicate we want to change our voice state.\n\t */\n\tSignalling = 'signalling',\n}\n\n/**\n * The state that a VoiceConnection will be in when it is waiting to receive a VOICE_SERVER_UPDATE and\n * VOICE_STATE_UPDATE packet from Discord, provided by the adapter.\n */\nexport interface VoiceConnectionSignallingState {\n\tadapter: DiscordGatewayAdapterImplementerMethods;\n\tstatus: VoiceConnectionStatus.Signalling;\n\tsubscription?: PlayerSubscription | undefined;\n}\n\n/**\n * The reasons a voice connection can be in the disconnected state.\n */\nexport enum VoiceConnectionDisconnectReason {\n\t/**\n\t * When the WebSocket connection has been closed.\n\t */\n\tWebSocketClose,\n\n\t/**\n\t * When the adapter was unable to send a message requested by the VoiceConnection.\n\t */\n\tAdapterUnavailable,\n\n\t/**\n\t * When a VOICE_SERVER_UPDATE packet is received with a null endpoint, causing the connection to be severed.\n\t */\n\tEndpointRemoved,\n\n\t/**\n\t * When a manual disconnect was requested.\n\t */\n\tManual,\n}\n\n/**\n * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is\n * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect.\n */\nexport interface VoiceConnectionDisconnectedBaseState {\n\tadapter: DiscordGatewayAdapterImplementerMethods;\n\tstatus: VoiceConnectionStatus.Disconnected;\n\tsubscription?: PlayerSubscription | undefined;\n}\n\n/**\n * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is\n * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect.\n */\nexport interface VoiceConnectionDisconnectedOtherState extends VoiceConnectionDisconnectedBaseState {\n\treason: Exclude;\n}\n\n/**\n * The state that a VoiceConnection will be in when its WebSocket connection was closed.\n * You can manually attempt to reconnect using VoiceConnection#reconnect.\n */\nexport interface VoiceConnectionDisconnectedWebSocketState extends VoiceConnectionDisconnectedBaseState {\n\t/**\n\t * The close code of the WebSocket connection to the Discord voice server.\n\t */\n\tcloseCode: number;\n\n\treason: VoiceConnectionDisconnectReason.WebSocketClose;\n}\n\n/**\n * The states that a VoiceConnection can be in when it is not connected to a Discord voice server nor is\n * it attempting to connect. You can manually attempt to connect using VoiceConnection#reconnect.\n */\nexport type VoiceConnectionDisconnectedState =\n\t| VoiceConnectionDisconnectedOtherState\n\t| VoiceConnectionDisconnectedWebSocketState;\n\n/**\n * The state that a VoiceConnection will be in when it is establishing a connection to a Discord\n * voice server.\n */\nexport interface VoiceConnectionConnectingState {\n\tadapter: DiscordGatewayAdapterImplementerMethods;\n\tnetworking: Networking;\n\tstatus: VoiceConnectionStatus.Connecting;\n\tsubscription?: PlayerSubscription | undefined;\n}\n\n/**\n * The state that a VoiceConnection will be in when it has an active connection to a Discord\n * voice server.\n */\nexport interface VoiceConnectionReadyState {\n\tadapter: DiscordGatewayAdapterImplementerMethods;\n\tnetworking: Networking;\n\tstatus: VoiceConnectionStatus.Ready;\n\tsubscription?: PlayerSubscription | undefined;\n}\n\n/**\n * The state that a VoiceConnection will be in when it has been permanently been destroyed by the\n * user and untracked by the library. It cannot be reconnected, instead, a new VoiceConnection\n * needs to be established.\n */\nexport interface VoiceConnectionDestroyedState {\n\tstatus: VoiceConnectionStatus.Destroyed;\n}\n\n/**\n * The various states that a voice connection can be in.\n */\nexport type VoiceConnectionState =\n\t| VoiceConnectionConnectingState\n\t| VoiceConnectionDestroyedState\n\t| VoiceConnectionDisconnectedState\n\t| VoiceConnectionReadyState\n\t| VoiceConnectionSignallingState;\n\nexport interface VoiceConnection extends EventEmitter {\n\t/**\n\t * Emitted when there is an error emitted from the voice connection\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'error', listener: (error: Error) => void): this;\n\t/**\n\t * Emitted debugging information about the voice connection\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'debug', listener: (message: string) => void): this;\n\t/**\n\t * Emitted when the state of the voice connection changes\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'stateChange', listener: (oldState: VoiceConnectionState, newState: VoiceConnectionState) => void): this;\n\t/**\n\t * Emitted when the end-to-end encrypted session has transitioned\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'transitioned', listener: (transitionId: number) => void): this;\n\t/**\n\t * Emitted when the state of the voice connection changes to a specific status\n\t *\n\t * @eventProperty\n\t */\n\ton(\n\t\tevent: Event,\n\t\tlistener: (oldState: VoiceConnectionState, newState: VoiceConnectionState & { status: Event }) => void,\n\t): this;\n}\n\n/**\n * A connection to the voice server of a Guild, can be used to play audio in voice channels.\n */\nexport class VoiceConnection extends EventEmitter {\n\t/**\n\t * The number of consecutive rejoin attempts. Initially 0, and increments for each rejoin.\n\t * When a connection is successfully established, it resets to 0.\n\t */\n\tpublic rejoinAttempts: number;\n\n\t/**\n\t * The state of the voice connection.\n\t */\n\tprivate _state: VoiceConnectionState;\n\n\t/**\n\t * A configuration storing all the data needed to reconnect to a Guild's voice server.\n\t *\n\t * @internal\n\t */\n\tpublic readonly joinConfig: JoinConfig;\n\n\t/**\n\t * The two packets needed to successfully establish a voice connection. They are received\n\t * from the main Discord gateway after signalling to change the voice state.\n\t */\n\tprivate readonly packets: {\n\t\tserver: GatewayVoiceServerUpdateDispatchData | undefined;\n\t\tstate: GatewayVoiceStateUpdateDispatchData | undefined;\n\t};\n\n\t/**\n\t * The receiver of this voice connection. You should join the voice channel with `selfDeaf` set\n\t * to false for this feature to work properly.\n\t */\n\tpublic readonly receiver: VoiceReceiver;\n\n\t/**\n\t * The debug logger function, if debugging is enabled.\n\t */\n\tprivate readonly debug: ((message: string) => void) | null;\n\n\t/**\n\t * The options used to create this voice connection.\n\t */\n\tprivate readonly options: CreateVoiceConnectionOptions;\n\n\t/**\n\t * Creates a new voice connection.\n\t *\n\t * @param joinConfig - The data required to establish the voice connection\n\t * @param options - The options used to create this voice connection\n\t */\n\tpublic constructor(joinConfig: JoinConfig, options: CreateVoiceConnectionOptions) {\n\t\tsuper();\n\n\t\tthis.debug = options.debug ? (message: string) => this.emit('debug', message) : null;\n\t\tthis.rejoinAttempts = 0;\n\n\t\tthis.receiver = new VoiceReceiver(this);\n\n\t\tthis.onNetworkingClose = this.onNetworkingClose.bind(this);\n\t\tthis.onNetworkingStateChange = this.onNetworkingStateChange.bind(this);\n\t\tthis.onNetworkingError = this.onNetworkingError.bind(this);\n\t\tthis.onNetworkingDebug = this.onNetworkingDebug.bind(this);\n\t\tthis.onNetworkingTransitioned = this.onNetworkingTransitioned.bind(this);\n\n\t\tconst adapter = options.adapterCreator({\n\t\t\tonVoiceServerUpdate: (data) => this.addServerPacket(data),\n\t\t\tonVoiceStateUpdate: (data) => this.addStatePacket(data),\n\t\t\tdestroy: () => this.destroy(false),\n\t\t});\n\n\t\tthis._state = { status: VoiceConnectionStatus.Signalling, adapter };\n\n\t\tthis.packets = {\n\t\t\tserver: undefined,\n\t\t\tstate: undefined,\n\t\t};\n\n\t\tthis.joinConfig = joinConfig;\n\t\tthis.options = options;\n\t}\n\n\t/**\n\t * The current state of the voice connection.\n\t *\n\t * @remarks\n\t * The setter will perform clean-up operations where necessary.\n\t */\n\tpublic get state() {\n\t\treturn this._state;\n\t}\n\n\tpublic set state(newState: VoiceConnectionState) {\n\t\tconst oldState = this._state;\n\t\tconst oldNetworking = Reflect.get(oldState, 'networking') as Networking | undefined;\n\t\tconst newNetworking = Reflect.get(newState, 'networking') as Networking | undefined;\n\n\t\tconst oldSubscription = Reflect.get(oldState, 'subscription') as PlayerSubscription | undefined;\n\t\tconst newSubscription = Reflect.get(newState, 'subscription') as PlayerSubscription | undefined;\n\n\t\tif (oldNetworking !== newNetworking) {\n\t\t\tif (oldNetworking) {\n\t\t\t\toldNetworking.on('error', noop);\n\t\t\t\toldNetworking.off('debug', this.onNetworkingDebug);\n\t\t\t\toldNetworking.off('error', this.onNetworkingError);\n\t\t\t\toldNetworking.off('close', this.onNetworkingClose);\n\t\t\t\toldNetworking.off('stateChange', this.onNetworkingStateChange);\n\t\t\t\toldNetworking.off('transitioned', this.onNetworkingTransitioned);\n\t\t\t\toldNetworking.destroy();\n\t\t\t}\n\n\t\t\tif (newNetworking) this.updateReceiveBindings(newNetworking.state, oldNetworking?.state);\n\t\t}\n\n\t\tif (newState.status === VoiceConnectionStatus.Ready) {\n\t\t\tthis.rejoinAttempts = 0;\n\t\t} else if (newState.status === VoiceConnectionStatus.Destroyed) {\n\t\t\tfor (const stream of this.receiver.subscriptions.values()) {\n\t\t\t\tif (!stream.destroyed) stream.destroy();\n\t\t\t}\n\t\t}\n\n\t\t// If destroyed, the adapter can also be destroyed so it can be cleaned up by the user\n\t\tif (oldState.status !== VoiceConnectionStatus.Destroyed && newState.status === VoiceConnectionStatus.Destroyed) {\n\t\t\toldState.adapter.destroy();\n\t\t}\n\n\t\tthis._state = newState;\n\n\t\tif (oldSubscription && oldSubscription !== newSubscription) {\n\t\t\toldSubscription.unsubscribe();\n\t\t}\n\n\t\tthis.emit('stateChange', oldState, newState);\n\t\tif (oldState.status !== newState.status) {\n\t\t\tthis.emit(newState.status, oldState, newState as any);\n\t\t}\n\t}\n\n\t/**\n\t * Registers a `VOICE_SERVER_UPDATE` packet to the voice connection. This will cause it to reconnect using the\n\t * new data provided in the packet.\n\t *\n\t * @param packet - The received `VOICE_SERVER_UPDATE` packet\n\t */\n\tprivate addServerPacket(packet: GatewayVoiceServerUpdateDispatchData) {\n\t\tthis.packets.server = packet;\n\t\tif (packet.endpoint) {\n\t\t\tthis.configureNetworking();\n\t\t} else if (this.state.status !== VoiceConnectionStatus.Destroyed) {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\t\treason: VoiceConnectionDisconnectReason.EndpointRemoved,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Registers a `VOICE_STATE_UPDATE` packet to the voice connection. Most importantly, it stores the id of the\n\t * channel that the client is connected to.\n\t *\n\t * @param packet - The received `VOICE_STATE_UPDATE` packet\n\t */\n\tprivate addStatePacket(packet: GatewayVoiceStateUpdateDispatchData) {\n\t\tthis.packets.state = packet;\n\n\t\tif (packet.self_deaf !== undefined) this.joinConfig.selfDeaf = packet.self_deaf;\n\t\tif (packet.self_mute !== undefined) this.joinConfig.selfMute = packet.self_mute;\n\t\tif (packet.channel_id) this.joinConfig.channelId = packet.channel_id;\n\t\t/*\n\t\t\tthe channel_id being null doesn't necessarily mean it was intended for the client to leave the voice channel\n\t\t\tas it may have disconnected due to network failure. This will be gracefully handled once the voice websocket\n\t\t\tdies, and then it is up to the user to decide how they wish to handle this.\n\t\t*/\n\t}\n\n\t/**\n\t * Called when the networking state changes, and the new ws/udp packet/message handlers need to be rebound\n\t * to the new instances.\n\t *\n\t * @param newState - The new networking state\n\t * @param oldState - The old networking state, if there is one\n\t */\n\tprivate updateReceiveBindings(newState: NetworkingState, oldState?: NetworkingState) {\n\t\tconst oldWs = Reflect.get(oldState ?? {}, 'ws') as VoiceWebSocket | undefined;\n\t\tconst newWs = Reflect.get(newState, 'ws') as VoiceWebSocket | undefined;\n\t\tconst oldUdp = Reflect.get(oldState ?? {}, 'udp') as VoiceUDPSocket | undefined;\n\t\tconst newUdp = Reflect.get(newState, 'udp') as VoiceUDPSocket | undefined;\n\n\t\tif (oldWs !== newWs) {\n\t\t\toldWs?.off('packet', this.receiver.onWsPacket);\n\t\t\tnewWs?.on('packet', this.receiver.onWsPacket);\n\t\t}\n\n\t\tif (oldUdp !== newUdp) {\n\t\t\toldUdp?.off('message', this.receiver.onUdpMessage);\n\t\t\tnewUdp?.on('message', this.receiver.onUdpMessage);\n\t\t}\n\n\t\tthis.receiver.connectionData = Reflect.get(newState, 'connectionData') ?? {};\n\t}\n\n\t/**\n\t * Attempts to configure a networking instance for this voice connection using the received packets.\n\t * Both packets are required, and any existing networking instance will be destroyed.\n\t *\n\t * @remarks\n\t * This is called when the voice server of the connection changes, e.g. if the bot is moved into a\n\t * different channel in the same guild but has a different voice server. In this instance, the connection\n\t * needs to be re-established to the new voice server.\n\t *\n\t * The connection will transition to the Connecting state when this is called.\n\t */\n\tpublic configureNetworking() {\n\t\tconst { server, state } = this.packets;\n\t\tif (!server || !state || this.state.status === VoiceConnectionStatus.Destroyed || !server.endpoint) return;\n\n\t\tconst networking = new Networking(\n\t\t\t{\n\t\t\t\tendpoint: server.endpoint,\n\t\t\t\tserverId: server.guild_id,\n\t\t\t\ttoken: server.token,\n\t\t\t\tsessionId: state.session_id,\n\t\t\t\tuserId: state.user_id,\n\t\t\t\tchannelId: state.channel_id!,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdebug: Boolean(this.debug),\n\t\t\t\tdaveEncryption: this.options.daveEncryption ?? true,\n\t\t\t\tdecryptionFailureTolerance: this.options.decryptionFailureTolerance,\n\t\t\t},\n\t\t);\n\n\t\tnetworking.once('close', this.onNetworkingClose);\n\t\tnetworking.on('stateChange', this.onNetworkingStateChange);\n\t\tnetworking.on('error', this.onNetworkingError);\n\t\tnetworking.on('debug', this.onNetworkingDebug);\n\t\tnetworking.on('transitioned', this.onNetworkingTransitioned);\n\n\t\tthis.state = {\n\t\t\t...this.state,\n\t\t\tstatus: VoiceConnectionStatus.Connecting,\n\t\t\tnetworking,\n\t\t};\n\t}\n\n\t/**\n\t * Called when the networking instance for this connection closes. If the close code is 4014 (do not reconnect),\n\t * the voice connection will transition to the Disconnected state which will store the close code. You can\n\t * decide whether or not to reconnect when this occurs by listening for the state change and calling reconnect().\n\t *\n\t * @remarks\n\t * If the close code was anything other than 4014, it is likely that the closing was not intended, and so the\n\t * VoiceConnection will signal to Discord that it would like to rejoin the channel. This automatically attempts\n\t * to re-establish the connection. This would be seen as a transition from the Ready state to the Signalling state.\n\t * @param code - The close code\n\t */\n\tprivate onNetworkingClose(code: number) {\n\t\tif (this.state.status === VoiceConnectionStatus.Destroyed) return;\n\t\t// If networking closes, try to connect to the voice channel again.\n\t\tif (code === 4_014) {\n\t\t\t// Disconnected - networking is already destroyed here\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\t\treason: VoiceConnectionDisconnectReason.WebSocketClose,\n\t\t\t\tcloseCode: code,\n\t\t\t};\n\t\t} else {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Signalling,\n\t\t\t};\n\t\t\tthis.rejoinAttempts++;\n\t\t\tif (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) {\n\t\t\t\tthis.state = {\n\t\t\t\t\t...this.state,\n\t\t\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\t\t\treason: VoiceConnectionDisconnectReason.AdapterUnavailable,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Called when the state of the networking instance changes. This is used to derive the state of the voice connection.\n\t *\n\t * @param oldState - The previous state\n\t * @param newState - The new state\n\t */\n\tprivate onNetworkingStateChange(oldState: NetworkingState, newState: NetworkingState) {\n\t\tthis.updateReceiveBindings(newState, oldState);\n\t\tif (oldState.code === newState.code) return;\n\t\tif (this.state.status !== VoiceConnectionStatus.Connecting && this.state.status !== VoiceConnectionStatus.Ready)\n\t\t\treturn;\n\n\t\tif (newState.code === NetworkingStatusCode.Ready) {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Ready,\n\t\t\t};\n\t\t} else if (newState.code !== NetworkingStatusCode.Closed) {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Connecting,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Propagates errors from the underlying network instance.\n\t *\n\t * @param error - The error to propagate\n\t */\n\tprivate onNetworkingError(error: Error) {\n\t\tthis.emit('error', error);\n\t}\n\n\t/**\n\t * Propagates debug messages from the underlying network instance.\n\t *\n\t * @param message - The debug message to propagate\n\t */\n\tprivate onNetworkingDebug(message: string) {\n\t\tthis.debug?.(`[NW] ${message}`);\n\t}\n\n\t/**\n\t * Propagates transitions from the underlying network instance.\n\t *\n\t * @param transitionId - The transition id\n\t */\n\tprivate onNetworkingTransitioned(transitionId: number) {\n\t\tthis.emit('transitioned', transitionId);\n\t}\n\n\t/**\n\t * Prepares an audio packet for dispatch.\n\t *\n\t * @param buffer - The Opus packet to prepare\n\t */\n\tpublic prepareAudioPacket(buffer: Buffer) {\n\t\tconst state = this.state;\n\t\tif (state.status !== VoiceConnectionStatus.Ready) return;\n\t\treturn state.networking.prepareAudioPacket(buffer);\n\t}\n\n\t/**\n\t * Dispatches the previously prepared audio packet (if any)\n\t */\n\tpublic dispatchAudio() {\n\t\tconst state = this.state;\n\t\tif (state.status !== VoiceConnectionStatus.Ready) return;\n\t\treturn state.networking.dispatchAudio();\n\t}\n\n\t/**\n\t * Prepares an audio packet and dispatches it immediately.\n\t *\n\t * @param buffer - The Opus packet to play\n\t */\n\tpublic playOpusPacket(buffer: Buffer) {\n\t\tconst state = this.state;\n\t\tif (state.status !== VoiceConnectionStatus.Ready) return;\n\t\tstate.networking.prepareAudioPacket(buffer);\n\t\treturn state.networking.dispatchAudio();\n\t}\n\n\t/**\n\t * Destroys the VoiceConnection, preventing it from connecting to voice again.\n\t * This method should be called when you no longer require the VoiceConnection to\n\t * prevent memory leaks.\n\t *\n\t * @param adapterAvailable - Whether the adapter can be used\n\t */\n\tpublic destroy(adapterAvailable = true) {\n\t\tif (this.state.status === VoiceConnectionStatus.Destroyed) {\n\t\t\tthrow new Error('Cannot destroy VoiceConnection - it has already been destroyed');\n\t\t}\n\n\t\tif (getVoiceConnection(this.joinConfig.guildId, this.joinConfig.group) === this) {\n\t\t\tuntrackVoiceConnection(this);\n\t\t}\n\n\t\tif (adapterAvailable) {\n\t\t\tthis.state.adapter.sendPayload(createJoinVoiceChannelPayload({ ...this.joinConfig, channelId: null }));\n\t\t}\n\n\t\tthis.state = {\n\t\t\tstatus: VoiceConnectionStatus.Destroyed,\n\t\t};\n\t}\n\n\t/**\n\t * Disconnects the VoiceConnection, allowing the possibility of rejoining later on.\n\t *\n\t * @returns `true` if the connection was successfully disconnected\n\t */\n\tpublic disconnect() {\n\t\tif (\n\t\t\tthis.state.status === VoiceConnectionStatus.Destroyed ||\n\t\t\tthis.state.status === VoiceConnectionStatus.Signalling\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.joinConfig.channelId = null;\n\t\tif (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) {\n\t\t\tthis.state = {\n\t\t\t\tadapter: this.state.adapter,\n\t\t\t\tsubscription: this.state.subscription,\n\t\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\t\treason: VoiceConnectionDisconnectReason.AdapterUnavailable,\n\t\t\t};\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.state = {\n\t\t\tadapter: this.state.adapter,\n\t\t\treason: VoiceConnectionDisconnectReason.Manual,\n\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t};\n\t\treturn true;\n\t}\n\n\t/**\n\t * Attempts to rejoin (better explanation soon:tm:)\n\t *\n\t * @remarks\n\t * Calling this method successfully will automatically increment the `rejoinAttempts` counter,\n\t * which you can use to inform whether or not you'd like to keep attempting to reconnect your\n\t * voice connection.\n\t *\n\t * A state transition from Disconnected to Signalling will be observed when this is called.\n\t */\n\tpublic rejoin(joinConfig?: Omit) {\n\t\tif (this.state.status === VoiceConnectionStatus.Destroyed) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst notReady = this.state.status !== VoiceConnectionStatus.Ready;\n\n\t\tif (notReady) this.rejoinAttempts++;\n\t\tObject.assign(this.joinConfig, joinConfig);\n\t\tif (this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) {\n\t\t\tif (notReady) {\n\t\t\t\tthis.state = {\n\t\t\t\t\t...this.state,\n\t\t\t\t\tstatus: VoiceConnectionStatus.Signalling,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn true;\n\t\t}\n\n\t\tthis.state = {\n\t\t\tadapter: this.state.adapter,\n\t\t\tsubscription: this.state.subscription,\n\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\treason: VoiceConnectionDisconnectReason.AdapterUnavailable,\n\t\t};\n\t\treturn false;\n\t}\n\n\t/**\n\t * Updates the speaking status of the voice connection. This is used when audio players are done playing audio,\n\t * and need to signal that the connection is no longer playing audio.\n\t *\n\t * @param enabled - Whether or not to show as speaking\n\t */\n\tpublic setSpeaking(enabled: boolean) {\n\t\tif (this.state.status !== VoiceConnectionStatus.Ready) return false;\n\t\t// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression\n\t\treturn this.state.networking.setSpeaking(enabled);\n\t}\n\n\t/**\n\t * Subscribes to an audio player, allowing the player to play audio on this voice connection.\n\t *\n\t * @param player - The audio player to subscribe to\n\t * @returns The created subscription\n\t */\n\tpublic subscribe(player: AudioPlayer) {\n\t\tif (this.state.status === VoiceConnectionStatus.Destroyed) return;\n\n\t\t// eslint-disable-next-line @typescript-eslint/dot-notation\n\t\tconst subscription = player['subscribe'](this);\n\n\t\tthis.state = {\n\t\t\t...this.state,\n\t\t\tsubscription,\n\t\t};\n\n\t\treturn subscription;\n\t}\n\n\t/**\n\t * The latest ping (in milliseconds) for the WebSocket connection and audio playback for this voice\n\t * connection, if this data is available.\n\t *\n\t * @remarks\n\t * For this data to be available, the VoiceConnection must be in the Ready state, and its underlying\n\t * WebSocket connection and UDP socket must have had at least one ping-pong exchange.\n\t */\n\tpublic get ping() {\n\t\tif (\n\t\t\tthis.state.status === VoiceConnectionStatus.Ready &&\n\t\t\tthis.state.networking.state.code === NetworkingStatusCode.Ready\n\t\t) {\n\t\t\treturn {\n\t\t\t\tws: this.state.networking.state.ws.ping,\n\t\t\t\tudp: this.state.networking.state.udp.ping,\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tws: undefined,\n\t\t\tudp: undefined,\n\t\t};\n\t}\n\n\t/**\n\t * The current voice privacy code of the encrypted session.\n\t *\n\t * @remarks\n\t * For this data to be available, the VoiceConnection must be in the Ready state,\n\t * and the connection would have to be end-to-end encrypted.\n\t */\n\tpublic get voicePrivacyCode() {\n\t\tif (\n\t\t\tthis.state.status === VoiceConnectionStatus.Ready &&\n\t\t\tthis.state.networking.state.code === NetworkingStatusCode.Ready\n\t\t) {\n\t\t\treturn this.state.networking.state.dave?.voicePrivacyCode ?? undefined;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Gets the verification code for a user in the session.\n\t *\n\t * @throws Will throw if end-to-end encryption is not on or if the user id provided is not in the session.\n\t */\n\tpublic async getVerificationCode(userId: string): Promise {\n\t\tif (\n\t\t\tthis.state.status === VoiceConnectionStatus.Ready &&\n\t\t\tthis.state.networking.state.code === NetworkingStatusCode.Ready &&\n\t\t\tthis.state.networking.state.dave\n\t\t) {\n\t\t\treturn this.state.networking.state.dave.getVerificationCode(userId);\n\t\t}\n\n\t\tthrow new Error('Session not available');\n\t}\n\n\t/**\n\t * Called when a subscription of this voice connection to an audio player is removed.\n\t *\n\t * @param subscription - The removed subscription\n\t */\n\tprotected onSubscriptionRemoved(subscription: PlayerSubscription) {\n\t\tif (this.state.status !== VoiceConnectionStatus.Destroyed && this.state.subscription === subscription) {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tsubscription: undefined,\n\t\t\t};\n\t\t}\n\t}\n}\n\n/**\n * Creates a new voice connection.\n *\n * @param joinConfig - The data required to establish the voice connection\n * @param options - The options to use when joining the voice channel\n */\nexport function createVoiceConnection(joinConfig: JoinConfig, options: CreateVoiceConnectionOptions) {\n\tconst payload = createJoinVoiceChannelPayload(joinConfig);\n\tconst existing = getVoiceConnection(joinConfig.guildId, joinConfig.group);\n\tif (existing && existing.state.status !== VoiceConnectionStatus.Destroyed) {\n\t\tif (existing.state.status === VoiceConnectionStatus.Disconnected) {\n\t\t\texisting.rejoin({\n\t\t\t\tchannelId: joinConfig.channelId,\n\t\t\t\tselfDeaf: joinConfig.selfDeaf,\n\t\t\t\tselfMute: joinConfig.selfMute,\n\t\t\t});\n\t\t} else if (!existing.state.adapter.sendPayload(payload)) {\n\t\t\texisting.state = {\n\t\t\t\t...existing.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\t\treason: VoiceConnectionDisconnectReason.AdapterUnavailable,\n\t\t\t};\n\t\t}\n\n\t\treturn existing;\n\t}\n\n\tconst voiceConnection = new VoiceConnection(joinConfig, options);\n\ttrackVoiceConnection(voiceConnection);\n\tif (\n\t\tvoiceConnection.state.status !== VoiceConnectionStatus.Destroyed &&\n\t\t!voiceConnection.state.adapter.sendPayload(payload)\n\t) {\n\t\tvoiceConnection.state = {\n\t\t\t...voiceConnection.state,\n\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\treason: VoiceConnectionDisconnectReason.AdapterUnavailable,\n\t\t};\n\t}\n\n\treturn voiceConnection;\n}\n","import { GatewayOpcodes } from 'discord-api-types/v10';\nimport type { VoiceConnection } from './VoiceConnection';\nimport type { AudioPlayer } from './audio/index';\n\nexport interface JoinConfig {\n\tchannelId: string | null;\n\tgroup: string;\n\tguildId: string;\n\tselfDeaf: boolean;\n\tselfMute: boolean;\n}\n\n/**\n * Sends a voice state update to the main websocket shard of a guild, to indicate joining/leaving/moving across\n * voice channels.\n *\n * @param config - The configuration to use when joining the voice channel\n */\nexport function createJoinVoiceChannelPayload(config: JoinConfig) {\n\treturn {\n\t\top: GatewayOpcodes.VoiceStateUpdate,\n\t\t// eslint-disable-next-line id-length\n\t\td: {\n\t\t\tguild_id: config.guildId,\n\t\t\tchannel_id: config.channelId,\n\t\t\tself_deaf: config.selfDeaf,\n\t\t\tself_mute: config.selfMute,\n\t\t},\n\t};\n}\n\n// Voice Connections\nconst groups = new Map>();\ngroups.set('default', new Map());\n\nfunction getOrCreateGroup(group: string) {\n\tconst existing = groups.get(group);\n\tif (existing) return existing;\n\tconst map = new Map();\n\tgroups.set(group, map);\n\treturn map;\n}\n\n/**\n * Retrieves the map of group names to maps of voice connections. By default, all voice connections\n * are created under the 'default' group.\n *\n * @returns The group map\n */\nexport function getGroups() {\n\treturn groups;\n}\n\n/**\n * Retrieves all the voice connections under the 'default' group.\n *\n * @param group - The group to look up\n * @returns The map of voice connections\n */\nexport function getVoiceConnections(group?: 'default'): Map;\n\n/**\n * Retrieves all the voice connections under the given group name.\n *\n * @param group - The group to look up\n * @returns The map of voice connections\n */\nexport function getVoiceConnections(group: string): Map | undefined;\n\n/**\n * Retrieves all the voice connections under the given group name. Defaults to the 'default' group.\n *\n * @param group - The group to look up\n * @returns The map of voice connections\n */\nexport function getVoiceConnections(group = 'default') {\n\treturn groups.get(group);\n}\n\n/**\n * Finds a voice connection with the given guild id and group. Defaults to the 'default' group.\n *\n * @param guildId - The guild id of the voice connection\n * @param group - the group that the voice connection was registered with\n * @returns The voice connection, if it exists\n */\nexport function getVoiceConnection(guildId: string, group = 'default') {\n\treturn getVoiceConnections(group)?.get(guildId);\n}\n\nexport function untrackVoiceConnection(voiceConnection: VoiceConnection) {\n\treturn getVoiceConnections(voiceConnection.joinConfig.group)?.delete(voiceConnection.joinConfig.guildId);\n}\n\nexport function trackVoiceConnection(voiceConnection: VoiceConnection) {\n\treturn getOrCreateGroup(voiceConnection.joinConfig.group).set(voiceConnection.joinConfig.guildId, voiceConnection);\n}\n\n// Audio Players\n\n// Each audio packet is 20ms long\nconst FRAME_LENGTH = 20;\n\nlet audioCycleInterval: NodeJS.Timeout | undefined;\nlet nextTime = -1;\n\n/**\n * A list of created audio players that are still active and haven't been destroyed.\n */\nconst audioPlayers: AudioPlayer[] = [];\n\n/**\n * Called roughly every 20 milliseconds. Dispatches audio from all players, and then gets the players to prepare\n * the next audio frame.\n */\nfunction audioCycleStep() {\n\tif (nextTime === -1) return;\n\n\tnextTime += FRAME_LENGTH;\n\tconst available = audioPlayers.filter((player) => player.checkPlayable());\n\n\tfor (const player of available) {\n\t\t// eslint-disable-next-line @typescript-eslint/dot-notation\n\t\tplayer['_stepDispatch']();\n\t}\n\n\tprepareNextAudioFrame(available);\n}\n\n/**\n * Recursively gets the players that have been passed as parameters to prepare audio frames that can be played\n * at the start of the next cycle.\n */\nfunction prepareNextAudioFrame(players: AudioPlayer[]) {\n\tconst nextPlayer = players.shift();\n\n\tif (!nextPlayer) {\n\t\tif (nextTime !== -1) {\n\t\t\taudioCycleInterval = setTimeout(() => audioCycleStep(), Math.max(1, nextTime - Date.now()));\n\t\t}\n\n\t\treturn;\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/dot-notation\n\tnextPlayer['_stepPrepare']();\n\n\t// setImmediate to avoid long audio player chains blocking other scheduled tasks\n\tsetImmediate(() => prepareNextAudioFrame(players));\n}\n\n/**\n * Checks whether or not the given audio player is being driven by the data store clock.\n *\n * @param target - The target to test for\n * @returns `true` if it is being tracked, `false` otherwise\n */\nexport function hasAudioPlayer(target: AudioPlayer) {\n\treturn audioPlayers.includes(target);\n}\n\n/**\n * Adds an audio player to the data store tracking list, if it isn't already there.\n *\n * @param player - The player to track\n */\nexport function addAudioPlayer(player: AudioPlayer) {\n\tif (hasAudioPlayer(player)) return player;\n\taudioPlayers.push(player);\n\tif (audioPlayers.length === 1) {\n\t\tnextTime = Date.now();\n\t\tsetImmediate(() => audioCycleStep());\n\t}\n\n\treturn player;\n}\n\n/**\n * Removes an audio player from the data store tracking list, if it is present there.\n */\nexport function deleteAudioPlayer(player: AudioPlayer) {\n\tconst index = audioPlayers.indexOf(player);\n\tif (index === -1) return;\n\taudioPlayers.splice(index, 1);\n\tif (audioPlayers.length === 0) {\n\t\tnextTime = -1;\n\t\tif (audioCycleInterval !== undefined) clearTimeout(audioCycleInterval);\n\t}\n}\n","/* eslint-disable jsdoc/check-param-names */\n/* eslint-disable id-length */\n/* eslint-disable @typescript-eslint/unbound-method */\nimport { Buffer } from 'node:buffer';\nimport crypto from 'node:crypto';\nimport { EventEmitter } from 'node:events';\nimport type { VoiceReceivePayload, VoiceSpeakingFlags } from 'discord-api-types/voice/v8';\nimport { VoiceEncryptionMode, VoiceOpcodes } from 'discord-api-types/voice/v8';\nimport type { CloseEvent } from 'ws';\nimport * as secretbox from '../util/Secretbox';\nimport { RTP_OPUS_PAYLOAD_TYPE } from '../util/constants';\nimport { noop } from '../util/util';\nimport { DAVESession, getMaxProtocolVersion } from './DAVESession';\nimport { VoiceUDPSocket } from './VoiceUDPSocket';\nimport type { BinaryWebSocketMessage } from './VoiceWebSocket';\nimport { VoiceWebSocket } from './VoiceWebSocket';\n\n// The number of audio channels required by Discord\nconst CHANNELS = 2;\nconst TIMESTAMP_INC = (48_000 / 100) * CHANNELS;\nconst MAX_NONCE_SIZE = 2 ** 32 - 1;\n\nexport const SUPPORTED_ENCRYPTION_MODES: VoiceEncryptionMode[] = [VoiceEncryptionMode.AeadXChaCha20Poly1305RtpSize];\n\n// Just in case there's some system that doesn't come with aes-256-gcm, conditionally add it as supported\nif (crypto.getCiphers().includes('aes-256-gcm')) {\n\tSUPPORTED_ENCRYPTION_MODES.unshift(VoiceEncryptionMode.AeadAes256GcmRtpSize);\n}\n\n/**\n * The different statuses that a networking instance can hold. The order\n * of the states between OpeningWs and Ready is chronological (first the\n * instance enters OpeningWs, then it enters Identifying etc.)\n */\nexport enum NetworkingStatusCode {\n\tOpeningWs,\n\tIdentifying,\n\tUdpHandshaking,\n\tSelectingProtocol,\n\tReady,\n\tResuming,\n\tClosed,\n}\n\n/**\n * The initial Networking state. Instances will be in this state when a WebSocket connection to a Discord\n * voice gateway is being opened.\n */\nexport interface NetworkingOpeningWsState {\n\tcode: NetworkingStatusCode.OpeningWs;\n\tconnectionOptions: ConnectionOptions;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when it is attempting to authorize itself.\n */\nexport interface NetworkingIdentifyingState {\n\tcode: NetworkingStatusCode.Identifying;\n\tconnectionOptions: ConnectionOptions;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when opening a UDP connection to the IP and port provided\n * by Discord, as well as performing IP discovery.\n */\nexport interface NetworkingUdpHandshakingState {\n\tcode: NetworkingStatusCode.UdpHandshaking;\n\tconnectionData: Pick;\n\tconnectionOptions: ConnectionOptions;\n\tudp: VoiceUDPSocket;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when selecting an encryption protocol for audio packets.\n */\nexport interface NetworkingSelectingProtocolState {\n\tcode: NetworkingStatusCode.SelectingProtocol;\n\tconnectionData: Pick;\n\tconnectionOptions: ConnectionOptions;\n\tudp: VoiceUDPSocket;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when it has a fully established connection to a Discord\n * voice server.\n */\nexport interface NetworkingReadyState {\n\tcode: NetworkingStatusCode.Ready;\n\tconnectionData: ConnectionData;\n\tconnectionOptions: ConnectionOptions;\n\tdave?: DAVESession | undefined;\n\tpreparedPacket?: Buffer | undefined;\n\tudp: VoiceUDPSocket;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when its connection has been dropped unexpectedly, and it\n * is attempting to resume an existing session.\n */\nexport interface NetworkingResumingState {\n\tcode: NetworkingStatusCode.Resuming;\n\tconnectionData: ConnectionData;\n\tconnectionOptions: ConnectionOptions;\n\tdave?: DAVESession | undefined;\n\tpreparedPacket?: Buffer | undefined;\n\tudp: VoiceUDPSocket;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when it has been destroyed. It cannot be recovered from this\n * state.\n */\nexport interface NetworkingClosedState {\n\tcode: NetworkingStatusCode.Closed;\n}\n\n/**\n * The various states that a networking instance can be in.\n */\nexport type NetworkingState =\n\t| NetworkingClosedState\n\t| NetworkingIdentifyingState\n\t| NetworkingOpeningWsState\n\t| NetworkingReadyState\n\t| NetworkingResumingState\n\t| NetworkingSelectingProtocolState\n\t| NetworkingUdpHandshakingState;\n\n/**\n * Details required to connect to the Discord voice gateway. These details\n * are first received on the main bot gateway, in the form of VOICE_SERVER_UPDATE\n * and VOICE_STATE_UPDATE packets.\n */\nexport interface ConnectionOptions {\n\tchannelId: string;\n\tendpoint: string;\n\tserverId: string;\n\tsessionId: string;\n\ttoken: string;\n\tuserId: string;\n}\n\n/**\n * Information about the current connection, e.g. which encryption mode is to be used on\n * the connection, timing information for playback of streams.\n */\nexport interface ConnectionData {\n\tconnectedClients: Set;\n\tencryptionMode: string;\n\tnonce: number;\n\tnonceBuffer: Buffer;\n\tpacketsPlayed: number;\n\tsecretKey: Uint8Array;\n\tsequence: number;\n\tspeaking: boolean;\n\tssrc: number;\n\ttimestamp: number;\n}\n\n/**\n * Options for networking that dictate behavior.\n */\nexport interface NetworkingOptions {\n\tdaveEncryption?: boolean | undefined;\n\tdebug?: boolean | undefined;\n\tdecryptionFailureTolerance?: number | undefined;\n}\n\n/**\n * An empty buffer that is reused in packet encryption by many different networking instances.\n */\nconst nonce = Buffer.alloc(24);\n\nexport interface Networking extends EventEmitter {\n\t/**\n\t * Debug event for Networking.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'debug', listener: (message: string) => void): this;\n\ton(event: 'error', listener: (error: Error) => void): this;\n\ton(event: 'stateChange', listener: (oldState: NetworkingState, newState: NetworkingState) => void): this;\n\ton(event: 'close', listener: (code: number) => void): this;\n\ton(event: 'transitioned', listener: (transitionId: number) => void): this;\n}\n\n/**\n * Stringifies a NetworkingState.\n *\n * @param state - The state to stringify\n */\nfunction stringifyState(state: NetworkingState) {\n\treturn JSON.stringify({\n\t\t...state,\n\t\tws: Reflect.has(state, 'ws'),\n\t\tudp: Reflect.has(state, 'udp'),\n\t});\n}\n\n/**\n * Chooses an encryption mode from a list of given options. Chooses the most preferred option.\n *\n * @param options - The available encryption options\n */\nfunction chooseEncryptionMode(options: VoiceEncryptionMode[]): VoiceEncryptionMode {\n\tconst option = options.find((option) => SUPPORTED_ENCRYPTION_MODES.includes(option));\n\tif (!option) {\n\t\t// This should only ever happen if the gateway does not give us any encryption modes we support.\n\t\tthrow new Error(`No compatible encryption modes. Available include: ${options.join(', ')}`);\n\t}\n\n\treturn option;\n}\n\n/**\n * Returns a random number that is in the range of n bits.\n *\n * @param numberOfBits - The number of bits\n */\nfunction randomNBit(numberOfBits: number) {\n\treturn Math.floor(Math.random() * 2 ** numberOfBits);\n}\n\n/**\n * Manages the networking required to maintain a voice connection and dispatch audio packets\n */\nexport class Networking extends EventEmitter {\n\tprivate _state: NetworkingState;\n\n\t/**\n\t * The debug logger function, if debugging is enabled.\n\t */\n\tprivate readonly debug: ((message: string) => void) | null;\n\n\t/**\n\t * The options used to create this Networking instance.\n\t */\n\tprivate readonly options: NetworkingOptions;\n\n\t/**\n\t * Creates a new Networking instance.\n\t */\n\tpublic constructor(connectionOptions: ConnectionOptions, options: NetworkingOptions) {\n\t\tsuper();\n\n\t\tthis.onWsOpen = this.onWsOpen.bind(this);\n\t\tthis.onChildError = this.onChildError.bind(this);\n\t\tthis.onWsPacket = this.onWsPacket.bind(this);\n\t\tthis.onWsBinary = this.onWsBinary.bind(this);\n\t\tthis.onWsClose = this.onWsClose.bind(this);\n\t\tthis.onWsDebug = this.onWsDebug.bind(this);\n\t\tthis.onUdpDebug = this.onUdpDebug.bind(this);\n\t\tthis.onUdpClose = this.onUdpClose.bind(this);\n\t\tthis.onDaveDebug = this.onDaveDebug.bind(this);\n\t\tthis.onDaveKeyPackage = this.onDaveKeyPackage.bind(this);\n\t\tthis.onDaveInvalidateTransition = this.onDaveInvalidateTransition.bind(this);\n\n\t\tthis.debug = options?.debug ? (message: string) => this.emit('debug', message) : null;\n\n\t\tthis._state = {\n\t\t\tcode: NetworkingStatusCode.OpeningWs,\n\t\t\tws: this.createWebSocket(connectionOptions.endpoint),\n\t\t\tconnectionOptions,\n\t\t};\n\t\tthis.options = options;\n\t}\n\n\t/**\n\t * Destroys the Networking instance, transitioning it into the Closed state.\n\t */\n\tpublic destroy() {\n\t\tthis.state = {\n\t\t\tcode: NetworkingStatusCode.Closed,\n\t\t};\n\t}\n\n\t/**\n\t * The current state of the networking instance.\n\t *\n\t * @remarks\n\t * The setter will perform clean-up operations where necessary.\n\t */\n\tpublic get state(): NetworkingState {\n\t\treturn this._state;\n\t}\n\n\tpublic set state(newState: NetworkingState) {\n\t\tconst oldWs = Reflect.get(this._state, 'ws') as VoiceWebSocket | undefined;\n\t\tconst newWs = Reflect.get(newState, 'ws') as VoiceWebSocket | undefined;\n\t\tif (oldWs && oldWs !== newWs) {\n\t\t\t// The old WebSocket is being freed - remove all handlers from it\n\t\t\toldWs.off('debug', this.onWsDebug);\n\t\t\toldWs.on('error', noop);\n\t\t\toldWs.off('error', this.onChildError);\n\t\t\toldWs.off('open', this.onWsOpen);\n\t\t\toldWs.off('packet', this.onWsPacket);\n\t\t\toldWs.off('binary', this.onWsBinary);\n\t\t\toldWs.off('close', this.onWsClose);\n\t\t\toldWs.destroy();\n\t\t}\n\n\t\tconst oldUdp = Reflect.get(this._state, 'udp') as VoiceUDPSocket | undefined;\n\t\tconst newUdp = Reflect.get(newState, 'udp') as VoiceUDPSocket | undefined;\n\n\t\tif (oldUdp && oldUdp !== newUdp) {\n\t\t\toldUdp.on('error', noop);\n\t\t\toldUdp.off('error', this.onChildError);\n\t\t\toldUdp.off('close', this.onUdpClose);\n\t\t\toldUdp.off('debug', this.onUdpDebug);\n\t\t\toldUdp.destroy();\n\t\t}\n\n\t\tconst oldDave = Reflect.get(this._state, 'dave') as DAVESession | undefined;\n\t\tconst newDave = Reflect.get(newState, 'dave') as DAVESession | undefined;\n\n\t\tif (oldDave && oldDave !== newDave) {\n\t\t\toldDave.off('error', this.onChildError);\n\t\t\toldDave.off('debug', this.onDaveDebug);\n\t\t\toldDave.off('keyPackage', this.onDaveKeyPackage);\n\t\t\toldDave.off('invalidateTransition', this.onDaveInvalidateTransition);\n\t\t\toldDave.destroy();\n\t\t}\n\n\t\tconst oldState = this._state;\n\t\tthis._state = newState;\n\t\tthis.emit('stateChange', oldState, newState);\n\n\t\tthis.debug?.(`state change:\\nfrom ${stringifyState(oldState)}\\nto ${stringifyState(newState)}`);\n\t}\n\n\t/**\n\t * Creates a new WebSocket to a Discord Voice gateway.\n\t *\n\t * @param endpoint - The endpoint to connect to\n\t * @param lastSequence - The last sequence to set for this WebSocket\n\t */\n\tprivate createWebSocket(endpoint: string, lastSequence?: number) {\n\t\tconst ws = new VoiceWebSocket(`wss://${endpoint}?v=8`, Boolean(this.debug));\n\n\t\tif (lastSequence !== undefined) {\n\t\t\tws.sequence = lastSequence;\n\t\t}\n\n\t\tws.on('error', this.onChildError);\n\t\tws.once('open', this.onWsOpen);\n\t\tws.on('packet', this.onWsPacket);\n\t\tws.on('binary', this.onWsBinary);\n\t\tws.once('close', this.onWsClose);\n\t\tws.on('debug', this.onWsDebug);\n\n\t\treturn ws;\n\t}\n\n\t/**\n\t * Creates a new DAVE session for this voice connection if we can create one.\n\t *\n\t * @param protocolVersion - The protocol version to use\n\t */\n\tprivate createDaveSession(protocolVersion: number) {\n\t\tif (\n\t\t\tthis.options.daveEncryption === false ||\n\t\t\t(this.state.code !== NetworkingStatusCode.SelectingProtocol &&\n\t\t\t\tthis.state.code !== NetworkingStatusCode.Ready &&\n\t\t\t\tthis.state.code !== NetworkingStatusCode.Resuming)\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst session = new DAVESession(\n\t\t\tprotocolVersion,\n\t\t\tthis.state.connectionOptions.userId,\n\t\t\tthis.state.connectionOptions.channelId,\n\t\t\t{\n\t\t\t\tdecryptionFailureTolerance: this.options.decryptionFailureTolerance,\n\t\t\t},\n\t\t);\n\n\t\tsession.on('error', this.onChildError);\n\t\tsession.on('debug', this.onDaveDebug);\n\t\tsession.on('keyPackage', this.onDaveKeyPackage);\n\t\tsession.on('invalidateTransition', this.onDaveInvalidateTransition);\n\t\tsession.reinit();\n\n\t\treturn session;\n\t}\n\n\t/**\n\t * Propagates errors from the children VoiceWebSocket, VoiceUDPSocket and DAVESession.\n\t *\n\t * @param error - The error that was emitted by a child\n\t */\n\tprivate onChildError(error: Error) {\n\t\tthis.emit('error', error);\n\t}\n\n\t/**\n\t * Called when the WebSocket opens. Depending on the state that the instance is in,\n\t * it will either identify with a new session, or it will attempt to resume an existing session.\n\t */\n\tprivate onWsOpen() {\n\t\tif (this.state.code === NetworkingStatusCode.OpeningWs) {\n\t\t\tthis.state.ws.sendPacket({\n\t\t\t\top: VoiceOpcodes.Identify,\n\t\t\t\td: {\n\t\t\t\t\tserver_id: this.state.connectionOptions.serverId,\n\t\t\t\t\tuser_id: this.state.connectionOptions.userId,\n\t\t\t\t\tsession_id: this.state.connectionOptions.sessionId,\n\t\t\t\t\ttoken: this.state.connectionOptions.token,\n\t\t\t\t\tmax_dave_protocol_version: this.options.daveEncryption === false ? 0 : getMaxProtocolVersion(),\n\t\t\t\t},\n\t\t\t});\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.Identifying,\n\t\t\t};\n\t\t} else if (this.state.code === NetworkingStatusCode.Resuming) {\n\t\t\tthis.state.ws.sendPacket({\n\t\t\t\top: VoiceOpcodes.Resume,\n\t\t\t\td: {\n\t\t\t\t\tserver_id: this.state.connectionOptions.serverId,\n\t\t\t\t\tsession_id: this.state.connectionOptions.sessionId,\n\t\t\t\t\ttoken: this.state.connectionOptions.token,\n\t\t\t\t\tseq_ack: this.state.ws.sequence,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Called when the WebSocket closes. Based on the reason for closing (given by the code parameter),\n\t * the instance will either attempt to resume, or enter the closed state and emit a 'close' event\n\t * with the close code, allowing the user to decide whether or not they would like to reconnect.\n\t *\n\t * @param code - The close code\n\t */\n\tprivate onWsClose({ code }: CloseEvent) {\n\t\tconst canResume = code === 4_015 || code < 4_000;\n\t\tif (canResume && this.state.code === NetworkingStatusCode.Ready) {\n\t\t\tconst lastSequence = this.state.ws.sequence;\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.Resuming,\n\t\t\t\tws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence),\n\t\t\t};\n\t\t} else if (this.state.code !== NetworkingStatusCode.Closed) {\n\t\t\tthis.destroy();\n\t\t\tthis.emit('close', code);\n\t\t}\n\t}\n\n\t/**\n\t * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord.\n\t */\n\tprivate onUdpClose() {\n\t\tif (this.state.code === NetworkingStatusCode.Ready) {\n\t\t\tconst lastSequence = this.state.ws.sequence;\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.Resuming,\n\t\t\t\tws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence),\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Called when a packet is received on the connection's WebSocket.\n\t *\n\t * @param packet - The received packet\n\t */\n\tprivate onWsPacket(packet: VoiceReceivePayload) {\n\t\tif (packet.op === VoiceOpcodes.Hello && this.state.code !== NetworkingStatusCode.Closed) {\n\t\t\tthis.state.ws.setHeartbeatInterval(packet.d.heartbeat_interval);\n\t\t} else if (packet.op === VoiceOpcodes.Ready && this.state.code === NetworkingStatusCode.Identifying) {\n\t\t\tconst { ip, port, ssrc, modes } = packet.d;\n\n\t\t\tconst udp = new VoiceUDPSocket({ ip, port });\n\t\t\tudp.on('error', this.onChildError);\n\t\t\tudp.on('debug', this.onUdpDebug);\n\t\t\tudp.once('close', this.onUdpClose);\n\t\t\tudp\n\t\t\t\t.performIPDiscovery(ssrc)\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t\t\t.then((localConfig) => {\n\t\t\t\t\tif (this.state.code !== NetworkingStatusCode.UdpHandshaking) return;\n\t\t\t\t\tthis.state.ws.sendPacket({\n\t\t\t\t\t\top: VoiceOpcodes.SelectProtocol,\n\t\t\t\t\t\td: {\n\t\t\t\t\t\t\tprotocol: 'udp',\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\taddress: localConfig.ip,\n\t\t\t\t\t\t\t\tport: localConfig.port,\n\t\t\t\t\t\t\t\tmode: chooseEncryptionMode(modes),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tthis.state = {\n\t\t\t\t\t\t...this.state,\n\t\t\t\t\t\tcode: NetworkingStatusCode.SelectingProtocol,\n\t\t\t\t\t};\n\t\t\t\t})\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then, promise/prefer-await-to-callbacks\n\t\t\t\t.catch((error: Error) => this.emit('error', error));\n\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.UdpHandshaking,\n\t\t\t\tudp,\n\t\t\t\tconnectionData: {\n\t\t\t\t\tssrc,\n\t\t\t\t\tconnectedClients: new Set(),\n\t\t\t\t},\n\t\t\t};\n\t\t} else if (\n\t\t\tpacket.op === VoiceOpcodes.SessionDescription &&\n\t\t\tthis.state.code === NetworkingStatusCode.SelectingProtocol\n\t\t) {\n\t\t\tconst { mode: encryptionMode, secret_key: secretKey, dave_protocol_version: daveProtocolVersion } = packet.d;\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.Ready,\n\t\t\t\tdave: this.createDaveSession(daveProtocolVersion),\n\t\t\t\tconnectionData: {\n\t\t\t\t\t...this.state.connectionData,\n\t\t\t\t\tencryptionMode,\n\t\t\t\t\tsecretKey: new Uint8Array(secretKey),\n\t\t\t\t\tsequence: randomNBit(16),\n\t\t\t\t\ttimestamp: randomNBit(32),\n\t\t\t\t\tnonce: 0,\n\t\t\t\t\tnonceBuffer: encryptionMode === 'aead_aes256_gcm_rtpsize' ? Buffer.alloc(12) : Buffer.alloc(24),\n\t\t\t\t\tspeaking: false,\n\t\t\t\t\tpacketsPlayed: 0,\n\t\t\t\t},\n\t\t\t};\n\t\t} else if (packet.op === VoiceOpcodes.Resumed && this.state.code === NetworkingStatusCode.Resuming) {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.Ready,\n\t\t\t};\n\t\t\tthis.state.connectionData.speaking = false;\n\t\t} else if (\n\t\t\t(packet.op === VoiceOpcodes.ClientsConnect || packet.op === VoiceOpcodes.ClientDisconnect) &&\n\t\t\t(this.state.code === NetworkingStatusCode.Ready ||\n\t\t\t\tthis.state.code === NetworkingStatusCode.UdpHandshaking ||\n\t\t\t\tthis.state.code === NetworkingStatusCode.SelectingProtocol ||\n\t\t\t\tthis.state.code === NetworkingStatusCode.Resuming)\n\t\t) {\n\t\t\tconst { connectionData } = this.state;\n\t\t\tif (packet.op === VoiceOpcodes.ClientsConnect)\n\t\t\t\tfor (const id of packet.d.user_ids) connectionData.connectedClients.add(id);\n\t\t\telse {\n\t\t\t\tconnectionData.connectedClients.delete(packet.d.user_id);\n\t\t\t}\n\t\t} else if (\n\t\t\t(this.state.code === NetworkingStatusCode.Ready || this.state.code === NetworkingStatusCode.Resuming) &&\n\t\t\tthis.state.dave\n\t\t) {\n\t\t\tif (packet.op === VoiceOpcodes.DavePrepareTransition) {\n\t\t\t\tconst sendReady = this.state.dave.prepareTransition(packet.d);\n\t\t\t\tif (sendReady)\n\t\t\t\t\tthis.state.ws.sendPacket({\n\t\t\t\t\t\top: VoiceOpcodes.DaveTransitionReady,\n\t\t\t\t\t\td: { transition_id: packet.d.transition_id },\n\t\t\t\t\t});\n\t\t\t\tif (packet.d.transition_id === 0) {\n\t\t\t\t\tthis.emit('transitioned', 0);\n\t\t\t\t}\n\t\t\t} else if (packet.op === VoiceOpcodes.DaveExecuteTransition) {\n\t\t\t\tconst transitioned = this.state.dave.executeTransition(packet.d.transition_id);\n\t\t\t\tif (transitioned) this.emit('transitioned', packet.d.transition_id);\n\t\t\t} else if (packet.op === VoiceOpcodes.DavePrepareEpoch) this.state.dave.prepareEpoch(packet.d);\n\t\t}\n\t}\n\n\t/**\n\t * Called when a binary message is received on the connection's WebSocket.\n\t *\n\t * @param message - The received message\n\t */\n\tprivate onWsBinary(message: BinaryWebSocketMessage) {\n\t\tif (this.state.code === NetworkingStatusCode.Ready && this.state.dave) {\n\t\t\tif (message.op === VoiceOpcodes.DaveMlsExternalSender) {\n\t\t\t\tthis.state.dave.setExternalSender(message.payload);\n\t\t\t} else if (message.op === VoiceOpcodes.DaveMlsProposals) {\n\t\t\t\tconst payload = this.state.dave.processProposals(message.payload, this.state.connectionData.connectedClients);\n\t\t\t\tif (payload) this.state.ws.sendBinaryMessage(VoiceOpcodes.DaveMlsCommitWelcome, payload);\n\t\t\t} else if (message.op === VoiceOpcodes.DaveMlsAnnounceCommitTransition) {\n\t\t\t\tconst { transitionId, success } = this.state.dave.processCommit(message.payload);\n\t\t\t\tif (success) {\n\t\t\t\t\tif (transitionId === 0) this.emit('transitioned', transitionId);\n\t\t\t\t\telse\n\t\t\t\t\t\tthis.state.ws.sendPacket({\n\t\t\t\t\t\t\top: VoiceOpcodes.DaveTransitionReady,\n\t\t\t\t\t\t\td: { transition_id: transitionId },\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else if (message.op === VoiceOpcodes.DaveMlsWelcome) {\n\t\t\t\tconst { transitionId, success } = this.state.dave.processWelcome(message.payload);\n\t\t\t\tif (success) {\n\t\t\t\t\tif (transitionId === 0) this.emit('transitioned', transitionId);\n\t\t\t\t\telse\n\t\t\t\t\t\tthis.state.ws.sendPacket({\n\t\t\t\t\t\t\top: VoiceOpcodes.DaveTransitionReady,\n\t\t\t\t\t\t\td: { transition_id: transitionId },\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Called when a new key package is ready to be sent to the voice server.\n\t *\n\t * @param keyPackage - The new key package\n\t */\n\tprivate onDaveKeyPackage(keyPackage: Buffer) {\n\t\tif (this.state.code === NetworkingStatusCode.SelectingProtocol || this.state.code === NetworkingStatusCode.Ready)\n\t\t\tthis.state.ws.sendBinaryMessage(VoiceOpcodes.DaveMlsKeyPackage, keyPackage);\n\t}\n\n\t/**\n\t * Called when the DAVE session wants to invalidate their transition and re-initialize.\n\t *\n\t * @param transitionId - The transition to invalidate\n\t */\n\tprivate onDaveInvalidateTransition(transitionId: number) {\n\t\tif (this.state.code === NetworkingStatusCode.SelectingProtocol || this.state.code === NetworkingStatusCode.Ready)\n\t\t\tthis.state.ws.sendPacket({\n\t\t\t\top: VoiceOpcodes.DaveMlsInvalidCommitWelcome,\n\t\t\t\td: { transition_id: transitionId },\n\t\t\t});\n\t}\n\n\t/**\n\t * Propagates debug messages from the child WebSocket.\n\t *\n\t * @param message - The emitted debug message\n\t */\n\tprivate onWsDebug(message: string) {\n\t\tthis.debug?.(`[WS] ${message}`);\n\t}\n\n\t/**\n\t * Propagates debug messages from the child UDPSocket.\n\t *\n\t * @param message - The emitted debug message\n\t */\n\tprivate onUdpDebug(message: string) {\n\t\tthis.debug?.(`[UDP] ${message}`);\n\t}\n\n\t/**\n\t * Propagates debug messages from the child DAVESession.\n\t *\n\t * @param message - The emitted debug message\n\t */\n\tprivate onDaveDebug(message: string) {\n\t\tthis.debug?.(`[DAVE] ${message}`);\n\t}\n\n\t/**\n\t * Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it.\n\t * It will be stored within the instance, and can be played by dispatchAudio()\n\t *\n\t * @remarks\n\t * Calling this method while there is already a prepared audio packet that has not yet been dispatched\n\t * will overwrite the existing audio packet. This should be avoided.\n\t * @param opusPacket - The Opus packet to encrypt\n\t * @returns The audio packet that was prepared\n\t */\n\tpublic prepareAudioPacket(opusPacket: Buffer) {\n\t\tconst state = this.state;\n\t\tif (state.code !== NetworkingStatusCode.Ready) return;\n\t\tstate.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData, state.dave);\n\t\treturn state.preparedPacket;\n\t}\n\n\t/**\n\t * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet\n\t * is consumed and cannot be dispatched again.\n\t */\n\tpublic dispatchAudio() {\n\t\tconst state = this.state;\n\t\tif (state.code !== NetworkingStatusCode.Ready) return false;\n\t\tif (state.preparedPacket !== undefined) {\n\t\t\tthis.playAudioPacket(state.preparedPacket);\n\t\t\tstate.preparedPacket = undefined;\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Plays an audio packet, updating timing metadata used for playback.\n\t *\n\t * @param audioPacket - The audio packet to play\n\t */\n\tprivate playAudioPacket(audioPacket: Buffer) {\n\t\tconst state = this.state;\n\t\tif (state.code !== NetworkingStatusCode.Ready) return;\n\t\tconst { connectionData } = state;\n\t\tconnectionData.packetsPlayed++;\n\t\tconnectionData.sequence++;\n\t\tconnectionData.timestamp += TIMESTAMP_INC;\n\t\tif (connectionData.sequence >= 2 ** 16) connectionData.sequence = 0;\n\t\tif (connectionData.timestamp >= 2 ** 32) connectionData.timestamp = 0;\n\t\tthis.setSpeaking(true);\n\t\tstate.udp.send(audioPacket);\n\t}\n\n\t/**\n\t * Sends a packet to the voice gateway indicating that the client has start/stopped sending\n\t * audio.\n\t *\n\t * @param speaking - Whether or not the client should be shown as speaking\n\t */\n\tpublic setSpeaking(speaking: boolean) {\n\t\tconst state = this.state;\n\t\tif (state.code !== NetworkingStatusCode.Ready) return;\n\t\tif (state.connectionData.speaking === speaking) return;\n\t\tstate.connectionData.speaking = speaking;\n\t\tstate.ws.sendPacket({\n\t\t\top: VoiceOpcodes.Speaking,\n\t\t\td: {\n\t\t\t\tspeaking: (speaking ? 1 : 0) as VoiceSpeakingFlags,\n\t\t\t\tdelay: 0,\n\t\t\t\tssrc: state.connectionData.ssrc,\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Creates a new audio packet from an Opus packet. This involves encrypting the packet,\n\t * then prepending a header that includes metadata.\n\t *\n\t * @param opusPacket - The Opus packet to prepare\n\t * @param connectionData - The current connection data of the instance\n\t * @param daveSession - The DAVE session to use for encryption\n\t */\n\tprivate createAudioPacket(opusPacket: Buffer, connectionData: ConnectionData, daveSession?: DAVESession) {\n\t\tconst rtpHeader = Buffer.alloc(12);\n\t\trtpHeader[0] = 0x80;\n\t\trtpHeader[1] = RTP_OPUS_PAYLOAD_TYPE;\n\n\t\tconst { sequence, timestamp, ssrc } = connectionData;\n\n\t\trtpHeader.writeUIntBE(sequence, 2, 2);\n\t\trtpHeader.writeUIntBE(timestamp, 4, 4);\n\t\trtpHeader.writeUIntBE(ssrc, 8, 4);\n\n\t\trtpHeader.copy(nonce, 0, 0, 12);\n\t\treturn Buffer.concat([rtpHeader, ...this.encryptOpusPacket(opusPacket, connectionData, rtpHeader, daveSession)]);\n\t}\n\n\t/**\n\t * Encrypts an Opus packet using the format agreed upon by the instance and Discord.\n\t *\n\t * @param opusPacket - The Opus packet to encrypt\n\t * @param connectionData - The current connection data of the instance\n\t * @param daveSession - The DAVE session to use for encryption\n\t */\n\tprivate encryptOpusPacket(\n\t\topusPacket: Buffer,\n\t\tconnectionData: ConnectionData,\n\t\tadditionalData: Buffer,\n\t\tdaveSession?: DAVESession,\n\t) {\n\t\tconst { secretKey, encryptionMode } = connectionData;\n\n\t\tconst packet = daveSession?.encrypt(opusPacket) ?? opusPacket;\n\n\t\t// Both supported encryption methods want the nonce to be an incremental integer\n\t\tconnectionData.nonce++;\n\t\tif (connectionData.nonce > MAX_NONCE_SIZE) connectionData.nonce = 0;\n\t\tconnectionData.nonceBuffer.writeUInt32BE(connectionData.nonce, 0);\n\n\t\t// 4 extra bytes of padding on the end of the encrypted packet\n\t\tconst noncePadding = connectionData.nonceBuffer.subarray(0, 4);\n\n\t\tlet encrypted;\n\t\tswitch (encryptionMode) {\n\t\t\tcase 'aead_aes256_gcm_rtpsize': {\n\t\t\t\tconst cipher = crypto.createCipheriv('aes-256-gcm', secretKey, connectionData.nonceBuffer);\n\t\t\t\tcipher.setAAD(additionalData);\n\n\t\t\t\tencrypted = Buffer.concat([cipher.update(packet), cipher.final(), cipher.getAuthTag()]);\n\n\t\t\t\treturn [encrypted, noncePadding];\n\t\t\t}\n\n\t\t\tcase 'aead_xchacha20_poly1305_rtpsize': {\n\t\t\t\tencrypted = secretbox.methods.crypto_aead_xchacha20poly1305_ietf_encrypt(\n\t\t\t\t\tpacket,\n\t\t\t\t\tadditionalData,\n\t\t\t\t\tconnectionData.nonceBuffer,\n\t\t\t\t\tsecretKey,\n\t\t\t\t);\n\n\t\t\t\treturn [encrypted, noncePadding];\n\t\t\t}\n\n\t\t\tdefault: {\n\t\t\t\t// This should never happen. Our encryption mode is chosen from a list given to us by the gateway and checked with the ones we support.\n\t\t\t\tthrow new RangeError(`Unsupported encryption method: ${encryptionMode}`);\n\t\t\t}\n\t\t}\n\t}\n}\n","import { Buffer } from 'node:buffer';\n\ninterface Methods {\n\tcrypto_aead_xchacha20poly1305_ietf_decrypt(\n\t\tcipherText: Buffer,\n\t\tadditionalData: Buffer,\n\t\tnonce: Buffer,\n\t\tkey: Uint8Array,\n\t): Buffer;\n\tcrypto_aead_xchacha20poly1305_ietf_encrypt(\n\t\tplaintext: Buffer,\n\t\tadditionalData: Buffer,\n\t\tnonce: Buffer,\n\t\tkey: Uint8Array,\n\t): Buffer;\n}\n\nconst libs = {\n\t'sodium-native': (sodium: any): Methods => ({\n\t\tcrypto_aead_xchacha20poly1305_ietf_decrypt: (\n\t\t\tcipherText: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => {\n\t\t\tconst message = Buffer.alloc(cipherText.length - sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES);\n\t\t\tsodium.crypto_aead_xchacha20poly1305_ietf_decrypt(message, null, cipherText, additionalData, nonce, key);\n\t\t\treturn message;\n\t\t},\n\t\tcrypto_aead_xchacha20poly1305_ietf_encrypt: (\n\t\t\tplaintext: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => {\n\t\t\tconst cipherText = Buffer.alloc(plaintext.length + sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES);\n\t\t\tsodium.crypto_aead_xchacha20poly1305_ietf_encrypt(cipherText, plaintext, additionalData, null, nonce, key);\n\t\t\treturn cipherText;\n\t\t},\n\t}),\n\tsodium: (sodium: any): Methods => ({\n\t\tcrypto_aead_xchacha20poly1305_ietf_decrypt: (\n\t\t\tcipherText: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => sodium.api.crypto_aead_xchacha20poly1305_ietf_decrypt(cipherText, additionalData, null, nonce, key),\n\t\tcrypto_aead_xchacha20poly1305_ietf_encrypt: (\n\t\t\tplaintext: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => sodium.api.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce, key),\n\t}),\n\t'libsodium-wrappers': (sodium: any): Methods => ({\n\t\tcrypto_aead_xchacha20poly1305_ietf_decrypt: (\n\t\t\tcipherText: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, cipherText, additionalData, nonce, key),\n\t\tcrypto_aead_xchacha20poly1305_ietf_encrypt: (\n\t\t\tplaintext: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce, key),\n\t}),\n\t'@stablelib/xchacha20poly1305': (stablelib: any): Methods => ({\n\t\tcrypto_aead_xchacha20poly1305_ietf_decrypt(plaintext, additionalData, nonce, key) {\n\t\t\tconst crypto = new stablelib.XChaCha20Poly1305(key);\n\t\t\treturn crypto.open(nonce, plaintext, additionalData);\n\t\t},\n\t\tcrypto_aead_xchacha20poly1305_ietf_encrypt(cipherText, additionalData, nonce, key) {\n\t\t\tconst crypto = new stablelib.XChaCha20Poly1305(key);\n\t\t\treturn crypto.seal(nonce, cipherText, additionalData);\n\t\t},\n\t}),\n\t'@noble/ciphers/chacha.js': (noble: any): Methods => ({\n\t\tcrypto_aead_xchacha20poly1305_ietf_decrypt(cipherText, additionalData, nonce, key) {\n\t\t\tconst chacha = noble.xchacha20poly1305(key, nonce, additionalData);\n\t\t\treturn chacha.decrypt(cipherText);\n\t\t},\n\t\tcrypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, nonce, key) {\n\t\t\tconst chacha = noble.xchacha20poly1305(key, nonce, additionalData);\n\t\t\treturn chacha.encrypt(plaintext);\n\t\t},\n\t}),\n} as const;\n\nconst fallbackError = () => {\n\tthrow new Error(\n\t\t`Cannot play audio as no valid encryption package is installed.\n- Install one of:\n - sodium\n - libsodium-wrappers\n - @stablelib/xchacha20poly1305\n - @noble/ciphers.\n- Use the generateDependencyReport() function for more information.\\n`,\n\t);\n};\n\nconst methods: Methods = {\n\tcrypto_aead_xchacha20poly1305_ietf_encrypt: fallbackError,\n\tcrypto_aead_xchacha20poly1305_ietf_decrypt: fallbackError,\n};\n\n// eslint-disable-next-line no-async-promise-executor\nexport const secretboxLoadPromise = new Promise(async (resolve) => {\n\tfor (const libName of Object.keys(libs) as (keyof typeof libs)[]) {\n\t\ttry {\n\t\t\tconst lib = await import(libName);\n\n\t\t\tif (libName === 'libsodium-wrappers' && lib.ready) {\n\t\t\t\tawait lib.ready;\n\t\t\t}\n\n\t\t\tObject.assign(methods, libs[libName](lib));\n\n\t\t\tbreak;\n\t\t} catch {}\n\t}\n\n\tresolve();\n});\n\nexport { methods };\n","export const RTP_OPUS_PAYLOAD_TYPE = 0x78;\n","export const noop = () => {};\n","import { Buffer } from 'node:buffer';\nimport { EventEmitter } from 'node:events';\nimport Davey from '@snazzah/davey';\nimport type { VoiceDavePrepareEpochData, VoiceDavePrepareTransitionData } from 'discord-api-types/voice/v8';\nimport { SILENCE_FRAME } from '../audio/AudioPlayer';\n\ninterface SessionMethods {\n\tcanPassthrough(userId: string): boolean;\n\tdecrypt(userId: string, mediaType: 0 | 1, packet: Buffer): Buffer;\n\tencryptOpus(packet: Buffer): Buffer;\n\tgetSerializedKeyPackage(): Buffer;\n\tgetVerificationCode(userId: string): Promise;\n\tprocessCommit(commit: Buffer): void;\n\tprocessProposals(optype: 0 | 1, proposals: Buffer, recognizedUserIds?: string[]): ProposalsResult;\n\tprocessWelcome(welcome: Buffer): void;\n\tready: boolean;\n\treinit(protocolVersion: number, userId: string, channelId: string): void;\n\treset(): void;\n\tsetExternalSender(externalSender: Buffer): void;\n\tsetPassthroughMode(passthrough: boolean, expiry: number): void;\n\tvoicePrivacyCode: string;\n}\n\ninterface ProposalsResult {\n\tcommit?: Buffer;\n\twelcome?: Buffer;\n}\n\n/**\n * The amount of seconds that a previous transition should be valid for.\n */\nconst TRANSITION_EXPIRY = 10;\n\n/**\n * The arbitrary amount of seconds to allow passthrough for mid-downgrade.\n * Generally, transitions last about 3 seconds maximum, but this should cover for when connections are delayed.\n */\nconst TRANSITION_EXPIRY_PENDING_DOWNGRADE = 24;\n\n/**\n * The amount of packets to allow decryption failure for until we deem the transition bad and re-initialize.\n * Usually 4 packets on a good connection may slip past when entering a new session.\n * After re-initializing, 5-24 packets may fail to decrypt after.\n */\nexport const DEFAULT_DECRYPTION_FAILURE_TOLERANCE = 36;\n\ninterface TransitionResult {\n\tsuccess: boolean;\n\ttransitionId: number;\n}\n/**\n * Options that dictate the session behavior.\n */\nexport interface DAVESessionOptions {\n\tdecryptionFailureTolerance?: number | undefined;\n}\n\n/**\n * The maximum DAVE protocol version supported.\n */\nexport function getMaxProtocolVersion(): number {\n\treturn Davey.DAVE_PROTOCOL_VERSION;\n}\n\nexport interface DAVESession extends EventEmitter {\n\ton(event: 'error', listener: (error: Error) => void): this;\n\ton(event: 'debug', listener: (message: string) => void): this;\n\ton(event: 'keyPackage', listener: (message: Buffer) => void): this;\n\ton(event: 'invalidateTransition', listener: (transitionId: number) => void): this;\n}\n\n/**\n * Manages the DAVE protocol group session.\n */\nexport class DAVESession extends EventEmitter {\n\t/**\n\t * The channel id represented by this session.\n\t */\n\tpublic channelId: string;\n\n\t/**\n\t * The user id represented by this session.\n\t */\n\tpublic userId: string;\n\n\t/**\n\t * The protocol version being used.\n\t */\n\tpublic protocolVersion: number;\n\n\t/**\n\t * The last transition id executed.\n\t */\n\tpublic lastTransitionId?: number | undefined;\n\n\t/**\n\t * The pending transitions, mapped by their ID and the protocol version.\n\t */\n\tprivate pendingTransitions = new Map();\n\n\t/**\n\t * Whether this session was downgraded previously.\n\t */\n\tprivate downgraded = false;\n\n\t/**\n\t * The amount of consecutive failures encountered when decrypting.\n\t */\n\tprivate consecutiveFailures = 0;\n\n\t/**\n\t * The amount of consecutive failures needed to attempt to recover.\n\t */\n\tprivate readonly failureTolerance: number;\n\n\t/**\n\t * Whether this session is currently re-initializing due to an invalid transition.\n\t */\n\tpublic reinitializing = false;\n\n\t/**\n\t * The underlying DAVE Session of this wrapper.\n\t */\n\tpublic session: SessionMethods | undefined;\n\n\tpublic constructor(protocolVersion: number, userId: string, channelId: string, options: DAVESessionOptions) {\n\t\tsuper();\n\n\t\tthis.protocolVersion = protocolVersion;\n\t\tthis.userId = userId;\n\t\tthis.channelId = channelId;\n\t\tthis.failureTolerance = options.decryptionFailureTolerance ?? DEFAULT_DECRYPTION_FAILURE_TOLERANCE;\n\t}\n\n\t/**\n\t * The current voice privacy code of the session. Will be `null` if there is no session.\n\t */\n\tpublic get voicePrivacyCode(): string | null {\n\t\tif (this.protocolVersion === 0 || !this.session?.voicePrivacyCode) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.session.voicePrivacyCode;\n\t}\n\n\t/**\n\t * Gets the verification code for a user in the session.\n\t *\n\t * @throws Will throw if there is not an active session or the user id provided is invalid or not in the session.\n\t */\n\tpublic async getVerificationCode(userId: string): Promise {\n\t\tif (!this.session) throw new Error('Session not available');\n\t\treturn this.session.getVerificationCode(userId);\n\t}\n\n\t/**\n\t * Re-initializes (or initializes) the underlying session.\n\t */\n\tpublic reinit() {\n\t\tif (this.protocolVersion > 0) {\n\t\t\tif (this.session) {\n\t\t\t\tthis.session.reinit(this.protocolVersion, this.userId, this.channelId);\n\t\t\t\tthis.emit('debug', `Session reinitialized for protocol version ${this.protocolVersion}`);\n\t\t\t} else {\n\t\t\t\tthis.session = new Davey.DAVESession(this.protocolVersion, this.userId, this.channelId);\n\t\t\t\tthis.emit('debug', `Session initialized for protocol version ${this.protocolVersion}`);\n\t\t\t}\n\n\t\t\tthis.emit('keyPackage', this.session!.getSerializedKeyPackage());\n\t\t} else if (this.session) {\n\t\t\tthis.session.reset();\n\t\t\tthis.session.setPassthroughMode(true, TRANSITION_EXPIRY);\n\t\t\tthis.emit('debug', 'Session reset');\n\t\t}\n\t}\n\n\t/**\n\t * Set the external sender for this session.\n\t *\n\t * @param externalSender - The external sender\n\t */\n\tpublic setExternalSender(externalSender: Buffer) {\n\t\tif (!this.session) throw new Error('No session available');\n\t\tthis.session.setExternalSender(externalSender);\n\t\tthis.emit('debug', 'Set MLS external sender');\n\t}\n\n\t/**\n\t * Prepare for a transition.\n\t *\n\t * @param data - The transition data\n\t * @returns Whether we should signal to the voice server that we are ready\n\t */\n\tpublic prepareTransition(data: VoiceDavePrepareTransitionData) {\n\t\tthis.emit('debug', `Preparing for transition (${data.transition_id}, v${data.protocol_version})`);\n\t\tthis.pendingTransitions.set(data.transition_id, data.protocol_version);\n\n\t\t// When the included transition id is 0, the transition is for (re)initialization and it can be executed immediately.\n\t\tif (data.transition_id === 0) {\n\t\t\tthis.executeTransition(data.transition_id);\n\t\t} else {\n\t\t\tif (data.protocol_version === 0) this.session?.setPassthroughMode(true, TRANSITION_EXPIRY_PENDING_DOWNGRADE);\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Execute a transition.\n\t *\n\t * @param transitionId - The transition id to execute on\n\t */\n\tpublic executeTransition(transitionId: number) {\n\t\tthis.emit('debug', `Executing transition (${transitionId})`);\n\t\tif (!this.pendingTransitions.has(transitionId)) {\n\t\t\tthis.emit('debug', `Received execute transition, but we don't have a pending transition for ${transitionId}`);\n\t\t\treturn false;\n\t\t}\n\n\t\tconst oldVersion = this.protocolVersion;\n\t\tthis.protocolVersion = this.pendingTransitions.get(transitionId)!;\n\n\t\t// Handle upgrades & defer downgrades\n\t\tif (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {\n\t\t\tthis.downgraded = true;\n\t\t\tthis.emit('debug', 'Session downgraded');\n\t\t} else if (transitionId > 0 && this.downgraded) {\n\t\t\tthis.downgraded = false;\n\t\t\tthis.session?.setPassthroughMode(true, TRANSITION_EXPIRY);\n\t\t\tthis.emit('debug', 'Session upgraded');\n\t\t}\n\n\t\t// In the future we'd want to signal to the DAVESession to transition also, but it only supports v1 at this time\n\t\tthis.reinitializing = false;\n\t\tthis.lastTransitionId = transitionId;\n\t\tthis.emit('debug', `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);\n\n\t\tthis.pendingTransitions.delete(transitionId);\n\t\treturn true;\n\t}\n\n\t/**\n\t * Prepare for a new epoch.\n\t *\n\t * @param data - The epoch data\n\t */\n\tpublic prepareEpoch(data: VoiceDavePrepareEpochData) {\n\t\tthis.emit('debug', `Preparing for epoch (${data.epoch})`);\n\t\tif (data.epoch === 1) {\n\t\t\tthis.protocolVersion = data.protocol_version;\n\t\t\tthis.reinit();\n\t\t}\n\t}\n\n\t/**\n\t * Recover from an invalid transition by re-initializing.\n\t *\n\t * @param transitionId - The transition id to invalidate\n\t */\n\tpublic recoverFromInvalidTransition(transitionId: number) {\n\t\tif (this.reinitializing) return;\n\t\tthis.emit('debug', `Invalidating transition ${transitionId}`);\n\t\tthis.reinitializing = true;\n\t\tthis.consecutiveFailures = 0;\n\t\tthis.emit('invalidateTransition', transitionId);\n\t\tthis.reinit();\n\t}\n\n\t/**\n\t * Processes proposals from the MLS group.\n\t *\n\t * @param payload - The binary message payload\n\t * @param connectedClients - The set of connected client IDs\n\t * @returns The payload to send back to the voice server, if there is one\n\t */\n\tpublic processProposals(payload: Buffer, connectedClients: Set): Buffer | undefined {\n\t\tif (!this.session) throw new Error('No session available');\n\t\tconst optype = payload.readUInt8(0) as 0 | 1;\n\t\tconst { commit, welcome } = this.session.processProposals(\n\t\t\toptype,\n\t\t\tpayload.subarray(1),\n\t\t\tArray.from(connectedClients),\n\t\t);\n\t\tthis.emit('debug', 'MLS proposals processed');\n\t\tif (!commit) return;\n\t\treturn welcome ? Buffer.concat([commit, welcome]) : commit;\n\t}\n\n\t/**\n\t * Processes a commit from the MLS group.\n\t *\n\t * @param payload - The payload\n\t * @returns The transaction id and whether it was successful\n\t */\n\tpublic processCommit(payload: Buffer): TransitionResult {\n\t\tif (!this.session) throw new Error('No session available');\n\t\tconst transitionId = payload.readUInt16BE(0);\n\t\ttry {\n\t\t\tthis.session.processCommit(payload.subarray(2));\n\t\t\tif (transitionId === 0) {\n\t\t\t\tthis.reinitializing = false;\n\t\t\t\tthis.lastTransitionId = transitionId;\n\t\t\t} else {\n\t\t\t\tthis.pendingTransitions.set(transitionId, this.protocolVersion);\n\t\t\t}\n\n\t\t\tthis.emit('debug', `MLS commit processed (transition id: ${transitionId})`);\n\t\t\treturn { transitionId, success: true };\n\t\t} catch (error) {\n\t\t\tthis.emit('debug', `MLS commit errored from transition ${transitionId}: ${error}`);\n\t\t\tthis.recoverFromInvalidTransition(transitionId);\n\t\t\treturn { transitionId, success: false };\n\t\t}\n\t}\n\n\t/**\n\t * Processes a welcome from the MLS group.\n\t *\n\t * @param payload - The payload\n\t * @returns The transaction id and whether it was successful\n\t */\n\tpublic processWelcome(payload: Buffer): TransitionResult {\n\t\tif (!this.session) throw new Error('No session available');\n\t\tconst transitionId = payload.readUInt16BE(0);\n\t\ttry {\n\t\t\tthis.session.processWelcome(payload.subarray(2));\n\t\t\tif (transitionId === 0) {\n\t\t\t\tthis.reinitializing = false;\n\t\t\t\tthis.lastTransitionId = transitionId;\n\t\t\t} else {\n\t\t\t\tthis.pendingTransitions.set(transitionId, this.protocolVersion);\n\t\t\t}\n\n\t\t\tthis.emit('debug', `MLS welcome processed (transition id: ${transitionId})`);\n\t\t\treturn { transitionId, success: true };\n\t\t} catch (error) {\n\t\t\tthis.emit('debug', `MLS welcome errored from transition ${transitionId}: ${error}`);\n\t\t\tthis.recoverFromInvalidTransition(transitionId);\n\t\t\treturn { transitionId, success: false };\n\t\t}\n\t}\n\n\t/**\n\t * Encrypt a packet using end-to-end encryption.\n\t *\n\t * @param packet - The packet to encrypt\n\t */\n\tpublic encrypt(packet: Buffer) {\n\t\tif (this.protocolVersion === 0 || !this.session?.ready || packet.equals(SILENCE_FRAME)) return packet;\n\t\treturn this.session.encryptOpus(packet);\n\t}\n\n\t/**\n\t * Decrypt a packet using end-to-end encryption.\n\t *\n\t * @param packet - The packet to decrypt\n\t * @param userId - The user id that sent the packet\n\t * @returns The decrypted packet, or `null` if the decryption failed but should be ignored\n\t */\n\tpublic decrypt(packet: Buffer, userId: string) {\n\t\tconst canDecrypt = this.session?.ready && (this.protocolVersion !== 0 || this.session?.canPassthrough(userId));\n\t\tif (packet.equals(SILENCE_FRAME) || !canDecrypt || !this.session) return packet;\n\t\ttry {\n\t\t\t// @ts-expect-error - const enum is exported and works (todo: drop const modifier on Davey end)\n\t\t\tconst buffer = this.session.decrypt(userId, Davey.MediaType.AUDIO, packet);\n\t\t\tthis.consecutiveFailures = 0;\n\t\t\treturn buffer;\n\t\t} catch (error) {\n\t\t\tif (!this.reinitializing && this.pendingTransitions.size === 0) {\n\t\t\t\tthis.consecutiveFailures++;\n\t\t\t\tthis.emit('debug', `Failed to decrypt a packet (${this.consecutiveFailures} consecutive fails)`);\n\t\t\t\tif (this.consecutiveFailures > this.failureTolerance) {\n\t\t\t\t\tif (this.lastTransitionId) this.recoverFromInvalidTransition(this.lastTransitionId);\n\t\t\t\t\telse throw error;\n\t\t\t\t}\n\t\t\t} else if (this.reinitializing) {\n\t\t\t\tthis.emit('debug', 'Failed to decrypt a packet (reinitializing session)');\n\t\t\t} else if (this.pendingTransitions.size > 0) {\n\t\t\t\tthis.emit('debug', `Failed to decrypt a packet (${this.pendingTransitions.size} pending transition[s])`);\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * Resets the session.\n\t */\n\tpublic destroy() {\n\t\ttry {\n\t\t\tthis.session?.reset();\n\t\t} catch {}\n\t}\n}\n","/* eslint-disable @typescript-eslint/prefer-ts-expect-error, @typescript-eslint/method-signature-style */\nimport { Buffer } from 'node:buffer';\nimport { EventEmitter } from 'node:events';\nimport { addAudioPlayer, deleteAudioPlayer } from '../DataStore';\nimport { VoiceConnectionStatus, type VoiceConnection } from '../VoiceConnection';\nimport { noop } from '../util/util';\nimport { AudioPlayerError } from './AudioPlayerError';\nimport type { AudioResource } from './AudioResource';\nimport { PlayerSubscription } from './PlayerSubscription';\n\n// The Opus \"silent\" frame\nexport const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]);\n\n/**\n * Describes the behavior of the player when an audio packet is played but there are no available\n * voice connections to play to.\n */\nexport enum NoSubscriberBehavior {\n\t/**\n\t * Pauses playing the stream until a voice connection becomes available.\n\t */\n\tPause = 'pause',\n\n\t/**\n\t * Continues to play through the resource regardless.\n\t */\n\tPlay = 'play',\n\n\t/**\n\t * The player stops and enters the Idle state.\n\t */\n\tStop = 'stop',\n}\n\nexport enum AudioPlayerStatus {\n\t/**\n\t * When the player has paused itself. Only possible with the \"pause\" no subscriber behavior.\n\t */\n\tAutoPaused = 'autopaused',\n\n\t/**\n\t * When the player is waiting for an audio resource to become readable before transitioning to Playing.\n\t */\n\tBuffering = 'buffering',\n\n\t/**\n\t * When there is currently no resource for the player to be playing.\n\t */\n\tIdle = 'idle',\n\n\t/**\n\t * When the player has been manually paused.\n\t */\n\tPaused = 'paused',\n\n\t/**\n\t * When the player is actively playing an audio resource.\n\t */\n\tPlaying = 'playing',\n}\n\n/**\n * Options that can be passed when creating an audio player, used to specify its behavior.\n */\nexport interface CreateAudioPlayerOptions {\n\tbehaviors?: {\n\t\tmaxMissedFrames?: number;\n\t\tnoSubscriber?: NoSubscriberBehavior;\n\t};\n\tdebug?: boolean;\n}\n\n/**\n * The state that an AudioPlayer is in when it has no resource to play. This is the starting state.\n */\nexport interface AudioPlayerIdleState {\n\tstatus: AudioPlayerStatus.Idle;\n}\n\n/**\n * The state that an AudioPlayer is in when it is waiting for a resource to become readable. Once this\n * happens, the AudioPlayer will enter the Playing state. If the resource ends/errors before this, then\n * it will re-enter the Idle state.\n */\nexport interface AudioPlayerBufferingState {\n\tonFailureCallback: () => void;\n\tonReadableCallback: () => void;\n\tonStreamError: (error: Error) => void;\n\t/**\n\t * The resource that the AudioPlayer is waiting for\n\t */\n\tresource: AudioResource;\n\tstatus: AudioPlayerStatus.Buffering;\n}\n\n/**\n * The state that an AudioPlayer is in when it is actively playing an AudioResource. When playback ends,\n * it will enter the Idle state.\n */\nexport interface AudioPlayerPlayingState {\n\t/**\n\t * The number of consecutive times that the audio resource has been unable to provide an Opus frame.\n\t */\n\tmissedFrames: number;\n\tonStreamError: (error: Error) => void;\n\n\t/**\n\t * The playback duration in milliseconds of the current audio resource. This includes filler silence packets\n\t * that have been played when the resource was buffering.\n\t */\n\tplaybackDuration: number;\n\n\t/**\n\t * The resource that is being played.\n\t */\n\tresource: AudioResource;\n\n\tstatus: AudioPlayerStatus.Playing;\n}\n\n/**\n * The state that an AudioPlayer is in when it has either been explicitly paused by the user, or done\n * automatically by the AudioPlayer itself if there are no available subscribers.\n */\nexport interface AudioPlayerPausedState {\n\tonStreamError: (error: Error) => void;\n\t/**\n\t * The playback duration in milliseconds of the current audio resource. This includes filler silence packets\n\t * that have been played when the resource was buffering.\n\t */\n\tplaybackDuration: number;\n\n\t/**\n\t * The current resource of the audio player.\n\t */\n\tresource: AudioResource;\n\n\t/**\n\t * How many silence packets still need to be played to avoid audio interpolation due to the stream suddenly pausing.\n\t */\n\tsilencePacketsRemaining: number;\n\n\tstatus: AudioPlayerStatus.AutoPaused | AudioPlayerStatus.Paused;\n}\n\n/**\n * The various states that the player can be in.\n */\nexport type AudioPlayerState =\n\t| AudioPlayerBufferingState\n\t| AudioPlayerIdleState\n\t| AudioPlayerPausedState\n\t| AudioPlayerPlayingState;\n\nexport interface AudioPlayer extends EventEmitter {\n\t/**\n\t * Emitted when there is an error emitted from the audio resource played by the audio player\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'error', listener: (error: AudioPlayerError) => void): this;\n\t/**\n\t * Emitted debugging information about the audio player\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'debug', listener: (message: string) => void): this;\n\t/**\n\t * Emitted when the state of the audio player changes\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'stateChange', listener: (oldState: AudioPlayerState, newState: AudioPlayerState) => void): this;\n\t/**\n\t * Emitted when the audio player is subscribed to a voice connection\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'subscribe' | 'unsubscribe', listener: (subscription: PlayerSubscription) => void): this;\n\t/**\n\t * Emitted when the status of state changes to a specific status\n\t *\n\t * @eventProperty\n\t */\n\ton(\n\t\tevent: Event,\n\t\tlistener: (oldState: AudioPlayerState, newState: AudioPlayerState & { status: Event }) => void,\n\t): this;\n}\n\n/**\n * Stringifies an AudioPlayerState instance.\n *\n * @param state - The state to stringify\n */\nfunction stringifyState(state: AudioPlayerState) {\n\treturn JSON.stringify({\n\t\t...state,\n\t\tresource: Reflect.has(state, 'resource'),\n\t\tstepTimeout: Reflect.has(state, 'stepTimeout'),\n\t});\n}\n\n/**\n * Used to play audio resources (i.e. tracks, streams) to voice connections.\n *\n * @remarks\n * Audio players are designed to be re-used - even if a resource has finished playing, the player itself\n * can still be used.\n *\n * The AudioPlayer drives the timing of playback, and therefore is unaffected by voice connections\n * becoming unavailable. Its behavior in these scenarios can be configured.\n */\nexport class AudioPlayer extends EventEmitter {\n\t/**\n\t * The state that the AudioPlayer is in.\n\t */\n\tprivate _state: AudioPlayerState;\n\n\t/**\n\t * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio\n\t * to the streams in this list.\n\t */\n\tprivate readonly subscribers: PlayerSubscription[] = [];\n\n\t/**\n\t * The behavior that the player should follow when it enters certain situations.\n\t */\n\tprivate readonly behaviors: {\n\t\tmaxMissedFrames: number;\n\t\tnoSubscriber: NoSubscriberBehavior;\n\t};\n\n\t/**\n\t * The debug logger function, if debugging is enabled.\n\t */\n\tprivate readonly debug: ((message: string) => void) | null;\n\n\t/**\n\t * Creates a new AudioPlayer.\n\t */\n\tpublic constructor(options: CreateAudioPlayerOptions = {}) {\n\t\tsuper();\n\t\tthis._state = { status: AudioPlayerStatus.Idle };\n\t\tthis.behaviors = {\n\t\t\tnoSubscriber: NoSubscriberBehavior.Pause,\n\t\t\tmaxMissedFrames: 5,\n\t\t\t...options.behaviors,\n\t\t};\n\t\tthis.debug = options.debug === false ? null : (message: string) => this.emit('debug', message);\n\t}\n\n\t/**\n\t * A list of subscribed voice connections that can currently receive audio to play.\n\t */\n\tpublic get playable() {\n\t\treturn this.subscribers\n\t\t\t.filter(({ connection }) => connection.state.status === VoiceConnectionStatus.Ready)\n\t\t\t.map(({ connection }) => connection);\n\t}\n\n\t/**\n\t * Subscribes a VoiceConnection to the audio player's play list. If the VoiceConnection is already subscribed,\n\t * then the existing subscription is used.\n\t *\n\t * @remarks\n\t * This method should not be directly called. Instead, use VoiceConnection#subscribe.\n\t * @param connection - The connection to subscribe\n\t * @returns The new subscription if the voice connection is not yet subscribed, otherwise the existing subscription\n\t */\n\t// @ts-ignore\n\tprivate subscribe(connection: VoiceConnection) {\n\t\tconst existingSubscription = this.subscribers.find((subscription) => subscription.connection === connection);\n\t\tif (!existingSubscription) {\n\t\t\tconst subscription = new PlayerSubscription(connection, this);\n\t\t\tthis.subscribers.push(subscription);\n\t\t\tsetImmediate(() => this.emit('subscribe', subscription));\n\t\t\treturn subscription;\n\t\t}\n\n\t\treturn existingSubscription;\n\t}\n\n\t/**\n\t * Unsubscribes a subscription - i.e. removes a voice connection from the play list of the audio player.\n\t *\n\t * @remarks\n\t * This method should not be directly called. Instead, use PlayerSubscription#unsubscribe.\n\t * @param subscription - The subscription to remove\n\t * @returns Whether or not the subscription existed on the player and was removed\n\t */\n\t// @ts-ignore\n\tprivate unsubscribe(subscription: PlayerSubscription) {\n\t\tconst index = this.subscribers.indexOf(subscription);\n\t\tconst exists = index !== -1;\n\t\tif (exists) {\n\t\t\tthis.subscribers.splice(index, 1);\n\t\t\tsubscription.connection.setSpeaking(false);\n\t\t\tthis.emit('unsubscribe', subscription);\n\t\t}\n\n\t\treturn exists;\n\t}\n\n\t/**\n\t * The state that the player is in.\n\t *\n\t * @remarks\n\t * The setter will perform clean-up operations where necessary.\n\t */\n\tpublic get state() {\n\t\treturn this._state;\n\t}\n\n\tpublic set state(newState: AudioPlayerState) {\n\t\tconst oldState = this._state;\n\t\tconst newResource = Reflect.get(newState, 'resource') as AudioResource | undefined;\n\n\t\tif (oldState.status !== AudioPlayerStatus.Idle && oldState.resource !== newResource) {\n\t\t\toldState.resource.playStream.on('error', noop);\n\t\t\toldState.resource.playStream.off('error', oldState.onStreamError);\n\t\t\toldState.resource.audioPlayer = undefined;\n\t\t\toldState.resource.playStream.destroy();\n\t\t\toldState.resource.playStream.read(); // required to ensure buffered data is drained, prevents memory leak\n\t\t}\n\n\t\t// When leaving the Buffering state (or buffering a new resource), then remove the event listeners from it\n\t\tif (\n\t\t\toldState.status === AudioPlayerStatus.Buffering &&\n\t\t\t(newState.status !== AudioPlayerStatus.Buffering || newState.resource !== oldState.resource)\n\t\t) {\n\t\t\toldState.resource.playStream.off('end', oldState.onFailureCallback);\n\t\t\toldState.resource.playStream.off('close', oldState.onFailureCallback);\n\t\t\toldState.resource.playStream.off('finish', oldState.onFailureCallback);\n\t\t\toldState.resource.playStream.off('readable', oldState.onReadableCallback);\n\t\t}\n\n\t\t// transitioning into an idle should ensure that connections stop speaking\n\t\tif (newState.status === AudioPlayerStatus.Idle) {\n\t\t\tthis._signalStopSpeaking();\n\t\t\tdeleteAudioPlayer(this);\n\t\t}\n\n\t\t// attach to the global audio player timer\n\t\tif (newResource) {\n\t\t\taddAudioPlayer(this);\n\t\t}\n\n\t\t// playing -> playing state changes should still transition if a resource changed (seems like it would be useful!)\n\t\tconst didChangeResources =\n\t\t\toldState.status !== AudioPlayerStatus.Idle &&\n\t\t\tnewState.status === AudioPlayerStatus.Playing &&\n\t\t\toldState.resource !== newState.resource;\n\n\t\tthis._state = newState;\n\n\t\tthis.emit('stateChange', oldState, this._state);\n\t\tif (oldState.status !== newState.status || didChangeResources) {\n\t\t\tthis.emit(newState.status, oldState, this._state as any);\n\t\t}\n\n\t\tthis.debug?.(`state change:\\nfrom ${stringifyState(oldState)}\\nto ${stringifyState(newState)}`);\n\t}\n\n\t/**\n\t * Plays a new resource on the player. If the player is already playing a resource, the existing resource is destroyed\n\t * (it cannot be reused, even in another player) and is replaced with the new resource.\n\t *\n\t * @remarks\n\t * The player will transition to the Playing state once playback begins, and will return to the Idle state once\n\t * playback is ended.\n\t *\n\t * If the player was previously playing a resource and this method is called, the player will not transition to the\n\t * Idle state during the swap over.\n\t * @param resource - The resource to play\n\t * @throws Will throw if attempting to play an audio resource that has already ended, or is being played by another player\n\t */\n\tpublic play(resource: AudioResource) {\n\t\tif (resource.ended) {\n\t\t\tthrow new Error('Cannot play a resource that has already ended.');\n\t\t}\n\n\t\tif (resource.audioPlayer) {\n\t\t\tif (resource.audioPlayer === this) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthrow new Error('Resource is already being played by another audio player.');\n\t\t}\n\n\t\tresource.audioPlayer = this;\n\n\t\t// Attach error listeners to the stream that will propagate the error and then return to the Idle\n\t\t// state if the resource is still being used.\n\t\tconst onStreamError = (error: Error) => {\n\t\t\tif (this.state.status !== AudioPlayerStatus.Idle) {\n\t\t\t\tthis.emit('error', new AudioPlayerError(error, this.state.resource));\n\t\t\t}\n\n\t\t\tif (this.state.status !== AudioPlayerStatus.Idle && this.state.resource === resource) {\n\t\t\t\tthis.state = {\n\t\t\t\t\tstatus: AudioPlayerStatus.Idle,\n\t\t\t\t};\n\t\t\t}\n\t\t};\n\n\t\tresource.playStream.once('error', onStreamError);\n\n\t\tif (resource.started) {\n\t\t\tthis.state = {\n\t\t\t\tstatus: AudioPlayerStatus.Playing,\n\t\t\t\tmissedFrames: 0,\n\t\t\t\tplaybackDuration: 0,\n\t\t\t\tresource,\n\t\t\t\tonStreamError,\n\t\t\t};\n\t\t} else {\n\t\t\tconst onReadableCallback = () => {\n\t\t\t\tif (this.state.status === AudioPlayerStatus.Buffering && this.state.resource === resource) {\n\t\t\t\t\tthis.state = {\n\t\t\t\t\t\tstatus: AudioPlayerStatus.Playing,\n\t\t\t\t\t\tmissedFrames: 0,\n\t\t\t\t\t\tplaybackDuration: 0,\n\t\t\t\t\t\tresource,\n\t\t\t\t\t\tonStreamError,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst onFailureCallback = () => {\n\t\t\t\tif (this.state.status === AudioPlayerStatus.Buffering && this.state.resource === resource) {\n\t\t\t\t\tthis.state = {\n\t\t\t\t\t\tstatus: AudioPlayerStatus.Idle,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tresource.playStream.once('readable', onReadableCallback);\n\n\t\t\tresource.playStream.once('end', onFailureCallback);\n\t\t\tresource.playStream.once('close', onFailureCallback);\n\t\t\tresource.playStream.once('finish', onFailureCallback);\n\n\t\t\tthis.state = {\n\t\t\t\tstatus: AudioPlayerStatus.Buffering,\n\t\t\t\tresource,\n\t\t\t\tonReadableCallback,\n\t\t\t\tonFailureCallback,\n\t\t\t\tonStreamError,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Pauses playback of the current resource, if any.\n\t *\n\t * @param interpolateSilence - If true, the player will play 5 packets of silence after pausing to prevent audio glitches\n\t * @returns `true` if the player was successfully paused, otherwise `false`\n\t */\n\tpublic pause(interpolateSilence = true) {\n\t\tif (this.state.status !== AudioPlayerStatus.Playing) return false;\n\t\tthis.state = {\n\t\t\t...this.state,\n\t\t\tstatus: AudioPlayerStatus.Paused,\n\t\t\tsilencePacketsRemaining: interpolateSilence ? 5 : 0,\n\t\t};\n\t\treturn true;\n\t}\n\n\t/**\n\t * Unpauses playback of the current resource, if any.\n\t *\n\t * @returns `true` if the player was successfully unpaused, otherwise `false`\n\t */\n\tpublic unpause() {\n\t\tif (this.state.status !== AudioPlayerStatus.Paused) return false;\n\t\tthis.state = {\n\t\t\t...this.state,\n\t\t\tstatus: AudioPlayerStatus.Playing,\n\t\t\tmissedFrames: 0,\n\t\t};\n\t\treturn true;\n\t}\n\n\t/**\n\t * Stops playback of the current resource and destroys the resource. The player will either transition to the Idle state,\n\t * or remain in its current state until the silence padding frames of the resource have been played.\n\t *\n\t * @param force - If true, will force the player to enter the Idle state even if the resource has silence padding frames\n\t * @returns `true` if the player will come to a stop, otherwise `false`\n\t */\n\tpublic stop(force = false) {\n\t\tif (this.state.status === AudioPlayerStatus.Idle) return false;\n\t\tif (force || this.state.resource.silencePaddingFrames === 0) {\n\t\t\tthis.state = {\n\t\t\t\tstatus: AudioPlayerStatus.Idle,\n\t\t\t};\n\t\t} else if (this.state.resource.silenceRemaining === -1) {\n\t\t\tthis.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Checks whether the underlying resource (if any) is playable (readable)\n\t *\n\t * @returns `true` if the resource is playable, otherwise `false`\n\t */\n\tpublic checkPlayable() {\n\t\tconst state = this._state;\n\t\tif (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return false;\n\n\t\t// If the stream has been destroyed or is no longer readable, then transition to the Idle state.\n\t\tif (!state.resource.readable) {\n\t\t\tthis.state = {\n\t\t\t\tstatus: AudioPlayerStatus.Idle,\n\t\t\t};\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Called roughly every 20ms by the global audio player timer. Dispatches any audio packets that are buffered\n\t * by the active connections of this audio player.\n\t */\n\t// @ts-ignore\n\tprivate _stepDispatch() {\n\t\tconst state = this._state;\n\n\t\t// Guard against the Idle state\n\t\tif (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return;\n\n\t\t// Dispatch any audio packets that were prepared in the previous cycle\n\t\tfor (const connection of this.playable) {\n\t\t\tconnection.dispatchAudio();\n\t\t}\n\t}\n\n\t/**\n\t * Called roughly every 20ms by the global audio player timer. Attempts to read an audio packet from the\n\t * underlying resource of the stream, and then has all the active connections of the audio player prepare it\n\t * (encrypt it, append header data) so that it is ready to play at the start of the next cycle.\n\t */\n\t// @ts-ignore\n\tprivate _stepPrepare() {\n\t\tconst state = this._state;\n\n\t\t// Guard against the Idle state\n\t\tif (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return;\n\n\t\t// List of connections that can receive the packet\n\t\tconst playable = this.playable;\n\n\t\t/* If the player was previously in the AutoPaused state, check to see whether there are newly available\n\t\t connections, allowing us to transition out of the AutoPaused state back into the Playing state */\n\t\tif (state.status === AudioPlayerStatus.AutoPaused && playable.length > 0) {\n\t\t\tthis.state = {\n\t\t\t\t...state,\n\t\t\t\tstatus: AudioPlayerStatus.Playing,\n\t\t\t\tmissedFrames: 0,\n\t\t\t};\n\t\t}\n\n\t\t/* If the player is (auto)paused, check to see whether silence packets should be played and\n\t\t set a timeout to begin the next cycle, ending the current cycle here. */\n\t\tif (state.status === AudioPlayerStatus.Paused || state.status === AudioPlayerStatus.AutoPaused) {\n\t\t\tif (state.silencePacketsRemaining > 0) {\n\t\t\t\tstate.silencePacketsRemaining--;\n\t\t\t\tthis._preparePacket(SILENCE_FRAME, playable, state);\n\t\t\t\tif (state.silencePacketsRemaining === 0) {\n\t\t\t\t\tthis._signalStopSpeaking();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// If there are no available connections in this cycle, observe the configured \"no subscriber\" behavior.\n\t\tif (playable.length === 0) {\n\t\t\tif (this.behaviors.noSubscriber === NoSubscriberBehavior.Pause) {\n\t\t\t\tthis.state = {\n\t\t\t\t\t...state,\n\t\t\t\t\tstatus: AudioPlayerStatus.AutoPaused,\n\t\t\t\t\tsilencePacketsRemaining: 5,\n\t\t\t\t};\n\t\t\t\treturn;\n\t\t\t} else if (this.behaviors.noSubscriber === NoSubscriberBehavior.Stop) {\n\t\t\t\tthis.stop(true);\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Attempt to read an Opus packet from the resource. If there isn't an available packet,\n\t\t * play a silence packet. If there are 5 consecutive cycles with failed reads, then the\n\t\t * playback will end.\n\t\t */\n\t\tconst packet: Buffer | null = state.resource.read();\n\n\t\tif (state.status === AudioPlayerStatus.Playing) {\n\t\t\tif (packet) {\n\t\t\t\tthis._preparePacket(packet, playable, state);\n\t\t\t\tstate.missedFrames = 0;\n\t\t\t} else {\n\t\t\t\tthis._preparePacket(SILENCE_FRAME, playable, state);\n\t\t\t\tstate.missedFrames++;\n\t\t\t\tif (state.missedFrames >= this.behaviors.maxMissedFrames) {\n\t\t\t\t\tthis.stop();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Signals to all the subscribed connections that they should send a packet to Discord indicating\n\t * they are no longer speaking. Called once playback of a resource ends.\n\t */\n\tprivate _signalStopSpeaking() {\n\t\tfor (const { connection } of this.subscribers) {\n\t\t\tconnection.setSpeaking(false);\n\t\t}\n\t}\n\n\t/**\n\t * Instructs the given connections to each prepare this packet to be played at the start of the\n\t * next cycle.\n\t *\n\t * @param packet - The Opus packet to be prepared by each receiver\n\t * @param receivers - The connections that should play this packet\n\t */\n\tprivate _preparePacket(\n\t\tpacket: Buffer,\n\t\treceivers: VoiceConnection[],\n\t\tstate: AudioPlayerPausedState | AudioPlayerPlayingState,\n\t) {\n\t\tstate.playbackDuration += 20;\n\t\tfor (const connection of receivers) {\n\t\t\tconnection.prepareAudioPacket(packet);\n\t\t}\n\t}\n}\n\n/**\n * Creates a new AudioPlayer to be used.\n */\nexport function createAudioPlayer(options?: CreateAudioPlayerOptions) {\n\treturn new AudioPlayer(options);\n}\n","import type { AudioResource } from './AudioResource';\n\n/**\n * An error emitted by an AudioPlayer. Contains an attached resource to aid with\n * debugging and identifying where the error came from.\n */\nexport class AudioPlayerError extends Error {\n\t/**\n\t * The resource associated with the audio player at the time the error was thrown.\n\t */\n\tpublic readonly resource: AudioResource;\n\n\tpublic constructor(error: Error, resource: AudioResource) {\n\t\tsuper(error.message);\n\t\tthis.resource = resource;\n\t\tthis.name = error.name;\n\t\tthis.stack = error.stack!;\n\t}\n}\n","/* eslint-disable @typescript-eslint/dot-notation */\nimport type { VoiceConnection } from '../VoiceConnection';\nimport type { AudioPlayer } from './AudioPlayer';\n\n/**\n * Represents a subscription of a voice connection to an audio player, allowing\n * the audio player to play audio on the voice connection.\n */\nexport class PlayerSubscription {\n\t/**\n\t * The voice connection of this subscription.\n\t */\n\tpublic readonly connection: VoiceConnection;\n\n\t/**\n\t * The audio player of this subscription.\n\t */\n\tpublic readonly player: AudioPlayer;\n\n\tpublic constructor(connection: VoiceConnection, player: AudioPlayer) {\n\t\tthis.connection = connection;\n\t\tthis.player = player;\n\t}\n\n\t/**\n\t * Unsubscribes the connection from the audio player, meaning that the\n\t * audio player cannot stream audio to it until a new subscription is made.\n\t */\n\tpublic unsubscribe() {\n\t\tthis.connection['onSubscriptionRemoved'](this);\n\t\tthis.player['unsubscribe'](this);\n\t}\n}\n","import { Buffer } from 'node:buffer';\nimport { createSocket, type Socket } from 'node:dgram';\nimport { EventEmitter } from 'node:events';\nimport { isIPv4 } from 'node:net';\n\n/**\n * Stores an IP address and port. Used to store socket details for the local client as well as\n * for Discord.\n */\nexport interface SocketConfig {\n\tip: string;\n\tport: number;\n}\n\n/**\n * Parses the response from Discord to aid with local IP discovery.\n *\n * @param message - The received message\n */\nexport function parseLocalPacket(message: Buffer): SocketConfig {\n\tconst packet = Buffer.from(message);\n\n\tconst ip = packet.subarray(8, packet.indexOf(0, 8)).toString('utf8');\n\n\tif (!isIPv4(ip)) {\n\t\tthrow new Error('Malformed IP address');\n\t}\n\n\tconst port = packet.readUInt16BE(packet.length - 2);\n\n\treturn { ip, port };\n}\n\n/**\n * The interval in milliseconds at which keep alive datagrams are sent.\n */\nconst KEEP_ALIVE_INTERVAL = 5e3;\n\n/**\n * The maximum value of the keep alive counter.\n */\nconst MAX_COUNTER_VALUE = 2 ** 32 - 1;\n\nexport interface VoiceUDPSocket extends EventEmitter {\n\ton(event: 'error', listener: (error: Error) => void): this;\n\ton(event: 'close', listener: () => void): this;\n\ton(event: 'debug', listener: (message: string) => void): this;\n\ton(event: 'message', listener: (message: Buffer) => void): this;\n}\n\n/**\n * Manages the UDP networking for a voice connection.\n */\nexport class VoiceUDPSocket extends EventEmitter {\n\t/**\n\t * The underlying network Socket for the VoiceUDPSocket.\n\t */\n\tprivate readonly socket: Socket;\n\n\t/**\n\t * The socket details for Discord (remote)\n\t */\n\tprivate readonly remote: SocketConfig;\n\n\t/**\n\t * The counter used in the keep alive mechanism.\n\t */\n\tprivate keepAliveCounter = 0;\n\n\t/**\n\t * The buffer used to write the keep alive counter into.\n\t */\n\tprivate readonly keepAliveBuffer: Buffer;\n\n\t/**\n\t * The Node.js interval for the keep-alive mechanism.\n\t */\n\tprivate readonly keepAliveInterval: NodeJS.Timeout;\n\n\t/**\n\t * The time taken to receive a response to keep alive messages.\n\t *\n\t * @deprecated This field is no longer updated as keep alive messages are no longer tracked.\n\t */\n\tpublic ping?: number;\n\n\t/**\n\t * Creates a new VoiceUDPSocket.\n\t *\n\t * @param remote - Details of the remote socket\n\t */\n\tpublic constructor(remote: SocketConfig) {\n\t\tsuper();\n\t\tthis.socket = createSocket('udp4');\n\t\tthis.socket.on('error', (error: Error) => this.emit('error', error));\n\t\tthis.socket.on('message', (buffer: Buffer) => this.onMessage(buffer));\n\t\tthis.socket.on('close', () => this.emit('close'));\n\t\tthis.remote = remote;\n\t\tthis.keepAliveBuffer = Buffer.alloc(8);\n\t\tthis.keepAliveInterval = setInterval(() => this.keepAlive(), KEEP_ALIVE_INTERVAL);\n\t\tsetImmediate(() => this.keepAlive());\n\t}\n\n\t/**\n\t * Called when a message is received on the UDP socket.\n\t *\n\t * @param buffer - The received buffer\n\t */\n\tprivate onMessage(buffer: Buffer): void {\n\t\t// Propagate the message\n\t\tthis.emit('message', buffer);\n\t}\n\n\t/**\n\t * Called at a regular interval to check whether we are still able to send datagrams to Discord.\n\t */\n\tprivate keepAlive() {\n\t\tthis.keepAliveBuffer.writeUInt32LE(this.keepAliveCounter, 0);\n\t\tthis.send(this.keepAliveBuffer);\n\t\tthis.keepAliveCounter++;\n\t\tif (this.keepAliveCounter > MAX_COUNTER_VALUE) {\n\t\t\tthis.keepAliveCounter = 0;\n\t\t}\n\t}\n\n\t/**\n\t * Sends a buffer to Discord.\n\t *\n\t * @param buffer - The buffer to send\n\t */\n\tpublic send(buffer: Buffer) {\n\t\tthis.socket.send(buffer, this.remote.port, this.remote.ip);\n\t}\n\n\t/**\n\t * Closes the socket, the instance will not be able to be reused.\n\t */\n\tpublic destroy() {\n\t\ttry {\n\t\t\tthis.socket.close();\n\t\t} catch {}\n\n\t\tclearInterval(this.keepAliveInterval);\n\t}\n\n\t/**\n\t * Performs IP discovery to discover the local address and port to be used for the voice connection.\n\t *\n\t * @param ssrc - The SSRC received from Discord\n\t */\n\tpublic async performIPDiscovery(ssrc: number): Promise {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst listener = (message: Buffer) => {\n\t\t\t\ttry {\n\t\t\t\t\tif (message.readUInt16BE(0) !== 2) return;\n\t\t\t\t\tconst packet = parseLocalPacket(message);\n\t\t\t\t\tthis.socket.off('message', listener);\n\t\t\t\t\tresolve(packet);\n\t\t\t\t} catch {}\n\t\t\t};\n\n\t\t\tthis.socket.on('message', listener);\n\t\t\tthis.socket.once('close', () => reject(new Error('Cannot perform IP discovery - socket closed')));\n\n\t\t\tconst discoveryBuffer = Buffer.alloc(74);\n\n\t\t\tdiscoveryBuffer.writeUInt16BE(1, 0);\n\t\t\tdiscoveryBuffer.writeUInt16BE(70, 2);\n\t\t\tdiscoveryBuffer.writeUInt32BE(ssrc, 4);\n\t\t\tthis.send(discoveryBuffer);\n\t\t});\n\t}\n}\n","import { Buffer } from 'node:buffer';\nimport { EventEmitter } from 'node:events';\nimport type { VoiceSendPayload } from 'discord-api-types/voice/v8';\nimport { VoiceOpcodes } from 'discord-api-types/voice/v8';\nimport WebSocket, { type MessageEvent } from 'ws';\n\n/**\n * A binary WebSocket message.\n */\nexport interface BinaryWebSocketMessage {\n\top: VoiceOpcodes;\n\tpayload: Buffer;\n\tseq: number;\n}\n\nexport interface VoiceWebSocket extends EventEmitter {\n\ton(event: 'error', listener: (error: Error) => void): this;\n\ton(event: 'open', listener: (event: WebSocket.Event) => void): this;\n\ton(event: 'close', listener: (event: WebSocket.CloseEvent) => void): this;\n\t/**\n\t * Debug event for VoiceWebSocket.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'debug', listener: (message: string) => void): this;\n\t/**\n\t * Packet event.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'packet', listener: (packet: any) => void): this;\n\t/**\n\t * Binary message event.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'binary', listener: (message: BinaryWebSocketMessage) => void): this;\n}\n\n/**\n * An extension of the WebSocket class to provide helper functionality when interacting\n * with the Discord Voice gateway.\n */\nexport class VoiceWebSocket extends EventEmitter {\n\t/**\n\t * The current heartbeat interval, if any.\n\t */\n\tprivate heartbeatInterval?: NodeJS.Timeout;\n\n\t/**\n\t * The time (milliseconds since UNIX epoch) that the last heartbeat acknowledgement packet was received.\n\t * This is set to 0 if an acknowledgement packet hasn't been received yet.\n\t */\n\tprivate lastHeartbeatAck: number;\n\n\t/**\n\t * The time (milliseconds since UNIX epoch) that the last heartbeat was sent. This is set to 0 if a heartbeat\n\t * hasn't been sent yet.\n\t */\n\tprivate lastHeartbeatSend: number;\n\n\t/**\n\t * The number of consecutively missed heartbeats.\n\t */\n\tprivate missedHeartbeats = 0;\n\n\t/**\n\t * The last recorded ping.\n\t */\n\tpublic ping?: number;\n\n\t/**\n\t * The last sequence number acknowledged from Discord. Will be `-1` if no sequence numbered messages have been received.\n\t */\n\tpublic sequence = -1;\n\n\t/**\n\t * The debug logger function, if debugging is enabled.\n\t */\n\tprivate readonly debug: ((message: string) => void) | null;\n\n\t/**\n\t * The underlying WebSocket of this wrapper.\n\t */\n\tprivate readonly ws: WebSocket;\n\n\t/**\n\t * Creates a new VoiceWebSocket.\n\t *\n\t * @param address - The address to connect to\n\t */\n\tpublic constructor(address: string, debug: boolean) {\n\t\tsuper();\n\t\tthis.ws = new WebSocket(address);\n\t\tthis.ws.onmessage = (err) => this.onMessage(err);\n\t\tthis.ws.onopen = (err) => this.emit('open', err);\n\t\tthis.ws.onerror = (err: Error | WebSocket.ErrorEvent) => this.emit('error', err instanceof Error ? err : err.error);\n\t\tthis.ws.onclose = (err) => this.emit('close', err);\n\n\t\tthis.lastHeartbeatAck = 0;\n\t\tthis.lastHeartbeatSend = 0;\n\n\t\tthis.debug = debug ? (message: string) => this.emit('debug', message) : null;\n\t}\n\n\t/**\n\t * Destroys the VoiceWebSocket. The heartbeat interval is cleared, and the connection is closed.\n\t */\n\tpublic destroy() {\n\t\ttry {\n\t\t\tthis.debug?.('destroyed');\n\t\t\tthis.setHeartbeatInterval(-1);\n\t\t\tthis.ws.close(1_000);\n\t\t} catch (error) {\n\t\t\tconst err = error as Error;\n\t\t\tthis.emit('error', err);\n\t\t}\n\t}\n\n\t/**\n\t * Handles message events on the WebSocket. Attempts to JSON parse the messages and emit them\n\t * as packets. Binary messages will be parsed and emitted.\n\t *\n\t * @param event - The message event\n\t */\n\tpublic onMessage(event: MessageEvent) {\n\t\tif (event.data instanceof Buffer || event.data instanceof ArrayBuffer) {\n\t\t\tconst buffer = event.data instanceof ArrayBuffer ? Buffer.from(event.data) : event.data;\n\t\t\tconst seq = buffer.readUInt16BE(0);\n\t\t\tconst op = buffer.readUInt8(2);\n\t\t\tconst payload = buffer.subarray(3);\n\n\t\t\tthis.sequence = seq;\n\t\t\tthis.debug?.(`<< [bin] opcode ${op}, seq ${seq}, ${payload.byteLength} bytes`);\n\n\t\t\tthis.emit('binary', { op, seq, payload });\n\t\t\treturn;\n\t\t} else if (typeof event.data !== 'string') {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.debug?.(`<< ${event.data}`);\n\n\t\tlet packet: any;\n\t\ttry {\n\t\t\tpacket = JSON.parse(event.data);\n\t\t} catch (error) {\n\t\t\tconst err = error as Error;\n\t\t\tthis.emit('error', err);\n\t\t\treturn;\n\t\t}\n\n\t\tif (packet.seq) {\n\t\t\tthis.sequence = packet.seq;\n\t\t}\n\n\t\tif (packet.op === VoiceOpcodes.HeartbeatAck) {\n\t\t\tthis.lastHeartbeatAck = Date.now();\n\t\t\tthis.missedHeartbeats = 0;\n\t\t\tthis.ping = this.lastHeartbeatAck - this.lastHeartbeatSend;\n\t\t}\n\n\t\tthis.emit('packet', packet);\n\t}\n\n\t/**\n\t * Sends a JSON-stringifiable packet over the WebSocket.\n\t *\n\t * @param packet - The packet to send\n\t */\n\tpublic sendPacket(packet: VoiceSendPayload) {\n\t\ttry {\n\t\t\tconst stringified = JSON.stringify(packet);\n\t\t\tthis.debug?.(`>> ${stringified}`);\n\t\t\tthis.ws.send(stringified);\n\t\t} catch (error) {\n\t\t\tconst err = error as Error;\n\t\t\tthis.emit('error', err);\n\t\t}\n\t}\n\n\t/**\n\t * Sends a binary message over the WebSocket.\n\t *\n\t * @param opcode - The opcode to use\n\t * @param payload - The payload to send\n\t */\n\tpublic sendBinaryMessage(opcode: VoiceOpcodes, payload: Buffer) {\n\t\ttry {\n\t\t\tconst message = Buffer.concat([new Uint8Array([opcode]), payload]);\n\t\t\tthis.debug?.(`>> [bin] opcode ${opcode}, ${payload.byteLength} bytes`);\n\t\t\tthis.ws.send(message);\n\t\t} catch (error) {\n\t\t\tconst err = error as Error;\n\t\t\tthis.emit('error', err);\n\t\t}\n\t}\n\n\t/**\n\t * Sends a heartbeat over the WebSocket.\n\t */\n\tprivate sendHeartbeat() {\n\t\tthis.lastHeartbeatSend = Date.now();\n\t\tthis.missedHeartbeats++;\n\t\tconst nonce = this.lastHeartbeatSend;\n\t\tthis.sendPacket({\n\t\t\top: VoiceOpcodes.Heartbeat,\n\t\t\t// eslint-disable-next-line id-length\n\t\t\td: {\n\t\t\t\t// eslint-disable-next-line id-length\n\t\t\t\tt: nonce,\n\t\t\t\tseq_ack: this.sequence,\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Sets/clears an interval to send heartbeats over the WebSocket.\n\t *\n\t * @param ms - The interval in milliseconds. If negative, the interval will be unset\n\t */\n\tpublic setHeartbeatInterval(ms: number) {\n\t\tif (this.heartbeatInterval !== undefined) clearInterval(this.heartbeatInterval);\n\t\tif (ms > 0) {\n\t\t\tthis.heartbeatInterval = setInterval(() => {\n\t\t\t\tif (this.lastHeartbeatSend !== 0 && this.missedHeartbeats >= 3) {\n\t\t\t\t\t// Missed too many heartbeats - disconnect\n\t\t\t\t\tthis.ws.close();\n\t\t\t\t\tthis.setHeartbeatInterval(-1);\n\t\t\t\t}\n\n\t\t\t\tthis.sendHeartbeat();\n\t\t\t}, ms);\n\t\t}\n\t}\n}\n","/* eslint-disable jsdoc/check-param-names */\n\nimport { Buffer } from 'node:buffer';\nimport crypto from 'node:crypto';\nimport type { VoiceReceivePayload } from 'discord-api-types/voice/v8';\nimport { VoiceOpcodes } from 'discord-api-types/voice/v8';\nimport { VoiceConnectionStatus, type VoiceConnection } from '../VoiceConnection';\nimport { NetworkingStatusCode, type ConnectionData } from '../networking/Networking';\nimport { methods } from '../util/Secretbox';\nimport { RTP_OPUS_PAYLOAD_TYPE } from '../util/constants';\nimport {\n\tAudioReceiveStream,\n\tcreateDefaultAudioReceiveStreamOptions,\n\ttype AudioReceiveStreamOptions,\n} from './AudioReceiveStream';\nimport { SSRCMap } from './SSRCMap';\nimport { SpeakingMap } from './SpeakingMap';\n\nconst HEADER_EXTENSION_BYTE = Buffer.from([0xbe, 0xde]);\nconst UNPADDED_NONCE_LENGTH = 4;\nconst AUTH_TAG_LENGTH = 16;\n\n/**\n * Attaches to a VoiceConnection, allowing you to receive audio packets from other\n * users that are speaking.\n *\n * @beta\n */\nexport class VoiceReceiver {\n\t/**\n\t * The attached connection of this receiver.\n\t */\n\tpublic readonly voiceConnection;\n\n\t/**\n\t * Maps SSRCs to Discord user ids.\n\t */\n\tpublic readonly ssrcMap: SSRCMap;\n\n\t/**\n\t * The current audio subscriptions of this receiver.\n\t */\n\tpublic readonly subscriptions: Map;\n\n\t/**\n\t * The connection data of the receiver.\n\t *\n\t * @internal\n\t */\n\tpublic connectionData: Partial;\n\n\t/**\n\t * The speaking map of the receiver.\n\t */\n\tpublic readonly speaking: SpeakingMap;\n\n\tpublic constructor(voiceConnection: VoiceConnection) {\n\t\tthis.voiceConnection = voiceConnection;\n\t\tthis.ssrcMap = new SSRCMap();\n\t\tthis.speaking = new SpeakingMap();\n\t\tthis.subscriptions = new Map();\n\t\tthis.connectionData = {};\n\n\t\tthis.onWsPacket = this.onWsPacket.bind(this);\n\t\tthis.onUdpMessage = this.onUdpMessage.bind(this);\n\t}\n\n\t/**\n\t * Called when a packet is received on the attached connection's WebSocket.\n\t *\n\t * @param packet - The received packet\n\t * @internal\n\t */\n\tpublic onWsPacket(packet: VoiceReceivePayload) {\n\t\tif (packet.op === VoiceOpcodes.ClientDisconnect) {\n\t\t\tthis.ssrcMap.delete(packet.d.user_id);\n\t\t} else if (packet.op === VoiceOpcodes.Speaking) {\n\t\t\tthis.ssrcMap.update({ userId: packet.d.user_id, audioSSRC: packet.d.ssrc });\n\t\t}\n\t}\n\n\tprivate decrypt(buffer: Buffer, mode: string, nonce: Buffer, secretKey: Uint8Array) {\n\t\t// Copy the last 4 bytes of unpadded nonce to the padding of (12 - 4) or (24 - 4) bytes\n\t\tbuffer.copy(nonce, 0, buffer.length - UNPADDED_NONCE_LENGTH);\n\n\t\tlet headerSize = 12;\n\t\tconst first = buffer.readUint8();\n\t\tif ((first >> 4) & 0x01) headerSize += 4;\n\n\t\t// The unencrypted RTP header contains 12 bytes, HEADER_EXTENSION and the extension size\n\t\tconst header = buffer.subarray(0, headerSize);\n\n\t\t// Encrypted contains the extension, if any, the opus packet, and the auth tag\n\t\tconst encrypted = buffer.subarray(headerSize, buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH);\n\t\tconst authTag = buffer.subarray(\n\t\t\tbuffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH,\n\t\t\tbuffer.length - UNPADDED_NONCE_LENGTH,\n\t\t);\n\n\t\tswitch (mode) {\n\t\t\tcase 'aead_aes256_gcm_rtpsize': {\n\t\t\t\tconst decipheriv = crypto.createDecipheriv('aes-256-gcm', secretKey, nonce);\n\t\t\t\tdecipheriv.setAAD(header);\n\t\t\t\tdecipheriv.setAuthTag(authTag);\n\n\t\t\t\treturn Buffer.concat([decipheriv.update(encrypted), decipheriv.final()]);\n\t\t\t}\n\n\t\t\tcase 'aead_xchacha20_poly1305_rtpsize': {\n\t\t\t\t// Combined mode expects authtag in the encrypted message\n\t\t\t\treturn Buffer.from(\n\t\t\t\t\tmethods.crypto_aead_xchacha20poly1305_ietf_decrypt(\n\t\t\t\t\t\tBuffer.concat([encrypted, authTag]),\n\t\t\t\t\t\theader,\n\t\t\t\t\t\tnonce,\n\t\t\t\t\t\tsecretKey,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tdefault: {\n\t\t\t\tthrow new RangeError(`Unsupported decryption method: ${mode}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Parses an audio packet, decrypting it to yield an Opus packet.\n\t *\n\t * @param buffer - The buffer to parse\n\t * @param mode - The encryption mode\n\t * @param nonce - The nonce buffer used by the connection for encryption\n\t * @param secretKey - The secret key used by the connection for encryption\n\t * @param userId - The user id that sent the packet\n\t * @returns The parsed Opus packet\n\t */\n\tprivate parsePacket(buffer: Buffer, mode: string, nonce: Buffer, secretKey: Uint8Array, userId: string) {\n\t\tlet packet: Buffer = this.decrypt(buffer, mode, nonce, secretKey);\n\t\tif (!packet) throw new Error('Failed to parse packet');\n\n\t\t// Strip padding (RFC3550 5.1)\n\t\tconst hasPadding = buffer[0] && Boolean(buffer[0] & 0b100000);\n\t\tif (hasPadding) {\n\t\t\tconst paddingAmount = packet[packet.length - 1]!;\n\t\t\tif (paddingAmount < packet.length) {\n\t\t\t\tpacket = packet.subarray(0, packet.length - paddingAmount);\n\t\t\t}\n\t\t}\n\n\t\t// Strip decrypted RTP Header Extension if present\n\t\t// The header is only indicated in the original data, so compare with buffer first\n\t\tif (buffer.subarray(12, 14).compare(HEADER_EXTENSION_BYTE) === 0) {\n\t\t\tconst headerExtensionLength = buffer.subarray(14).readUInt16BE();\n\t\t\tpacket = packet.subarray(4 * headerExtensionLength);\n\t\t}\n\n\t\t// Decrypt packet if in a DAVE session.\n\t\tif (\n\t\t\tthis.voiceConnection.state.status === VoiceConnectionStatus.Ready &&\n\t\t\t(this.voiceConnection.state.networking.state.code === NetworkingStatusCode.Ready ||\n\t\t\t\tthis.voiceConnection.state.networking.state.code === NetworkingStatusCode.Resuming)\n\t\t) {\n\t\t\tconst daveSession = this.voiceConnection.state.networking.state.dave;\n\t\t\tif (daveSession) packet = daveSession.decrypt(packet, userId)!;\n\t\t}\n\n\t\treturn packet;\n\t}\n\n\t/**\n\t * Called when the UDP socket of the attached connection receives a message.\n\t *\n\t * @param msg - The received message\n\t * @internal\n\t */\n\tpublic onUdpMessage(msg: Buffer) {\n\t\tif (msg.length <= 8) return;\n\t\tconst ssrc = msg.readUInt32BE(8);\n\n\t\tconst userData = this.ssrcMap.get(ssrc);\n\t\tif (!userData) return;\n\n\t\tthis.speaking.onPacket(userData.userId);\n\n\t\tconst stream = this.subscriptions.get(userData.userId);\n\t\tif (!stream) return;\n\n\t\tif (this.connectionData.encryptionMode && this.connectionData.nonceBuffer && this.connectionData.secretKey) {\n\t\t\t// As a guard, we shouldn't parse packets that (1) aren't voice packets and (2) are not in the right RTP version\n\t\t\tif ((msg[1]! & 0x7f) !== RTP_OPUS_PAYLOAD_TYPE) return;\n\n\t\t\t// Ignore packets not in RTP version 2\n\t\t\tconst rtpVersion = msg[0]! >> 6;\n\t\t\tif (rtpVersion !== 2) return;\n\n\t\t\ttry {\n\t\t\t\tconst packet = this.parsePacket(\n\t\t\t\t\tmsg,\n\t\t\t\t\tthis.connectionData.encryptionMode,\n\t\t\t\t\tthis.connectionData.nonceBuffer,\n\t\t\t\t\tthis.connectionData.secretKey,\n\t\t\t\t\tuserData.userId,\n\t\t\t\t);\n\t\t\t\tif (packet) stream.push(packet);\n\t\t\t} catch (error) {\n\t\t\t\tstream.destroy(error as Error);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Creates a subscription for the given user id.\n\t *\n\t * @param target - The id of the user to subscribe to\n\t * @returns A readable stream of Opus packets received from the target\n\t */\n\tpublic subscribe(userId: string, options?: Partial) {\n\t\tconst existing = this.subscriptions.get(userId);\n\t\tif (existing) return existing;\n\n\t\tconst stream = new AudioReceiveStream({\n\t\t\t...createDefaultAudioReceiveStreamOptions(),\n\t\t\t...options,\n\t\t});\n\n\t\tstream.once('close', () => this.subscriptions.delete(userId));\n\t\tthis.subscriptions.set(userId, stream);\n\t\treturn stream;\n\t}\n}\n","import type { Buffer } from 'node:buffer';\nimport process from 'node:process';\nimport { Readable, type ReadableOptions } from 'node:stream';\nimport { SILENCE_FRAME } from '../audio/AudioPlayer';\n\n/**\n * The different behaviors an audio receive stream can have for deciding when to end.\n */\nexport enum EndBehaviorType {\n\t/**\n\t * The stream will only end when manually destroyed.\n\t */\n\tManual,\n\n\t/**\n\t * The stream will end after a given time period of silence/no audio packets.\n\t */\n\tAfterSilence,\n\n\t/**\n\t * The stream will end after a given time period of no audio packets.\n\t */\n\tAfterInactivity,\n}\n\nexport type EndBehavior =\n\t| {\n\t\t\tbehavior: EndBehaviorType.AfterInactivity | EndBehaviorType.AfterSilence;\n\t\t\tduration: number;\n\t }\n\t| {\n\t\t\tbehavior: EndBehaviorType.Manual;\n\t };\n\nexport interface AudioReceiveStreamOptions extends ReadableOptions {\n\tend: EndBehavior;\n}\n\nexport function createDefaultAudioReceiveStreamOptions(): AudioReceiveStreamOptions {\n\treturn {\n\t\tend: {\n\t\t\tbehavior: EndBehaviorType.Manual,\n\t\t},\n\t};\n}\n\n/**\n * A readable stream of Opus packets received from a specific entity\n * in a Discord voice connection.\n */\nexport class AudioReceiveStream extends Readable {\n\t/**\n\t * The end behavior of the receive stream.\n\t */\n\tpublic readonly end: EndBehavior;\n\n\tprivate endTimeout?: NodeJS.Timeout;\n\n\tpublic constructor(options: AudioReceiveStreamOptions) {\n\t\tconst { end, ...rest } = options;\n\n\t\tsuper({\n\t\t\t...rest,\n\t\t\tobjectMode: true,\n\t\t});\n\n\t\tthis.end = end;\n\t}\n\n\tpublic override push(buffer: Buffer | null) {\n\t\tif (\n\t\t\tbuffer &&\n\t\t\t(this.end.behavior === EndBehaviorType.AfterInactivity ||\n\t\t\t\t(this.end.behavior === EndBehaviorType.AfterSilence &&\n\t\t\t\t\t(buffer.compare(SILENCE_FRAME) !== 0 || this.endTimeout === undefined)))\n\t\t) {\n\t\t\tthis.renewEndTimeout(this.end);\n\t\t}\n\n\t\tif (buffer === null) {\n\t\t\t// null marks EOF for stream\n\t\t\tprocess.nextTick(() => this.destroy());\n\t\t}\n\n\t\treturn super.push(buffer);\n\t}\n\n\tprivate renewEndTimeout(end: EndBehavior & { duration: number }) {\n\t\tif (this.endTimeout) {\n\t\t\tclearTimeout(this.endTimeout);\n\t\t}\n\n\t\tthis.endTimeout = setTimeout(() => this.push(null), end.duration);\n\t}\n\n\tpublic override _read() {}\n}\n","import { EventEmitter } from 'node:events';\n\n/**\n * The known data for a user in a Discord voice connection.\n */\nexport interface VoiceUserData {\n\t/**\n\t * The SSRC of the user's audio stream.\n\t */\n\taudioSSRC: number;\n\n\t/**\n\t * The Discord user id of the user.\n\t */\n\tuserId: string;\n\n\t/**\n\t * The SSRC of the user's video stream (if one exists)\n\t * Cannot be 0. If undefined, the user has no video stream.\n\t */\n\tvideoSSRC?: number;\n}\n\nexport interface SSRCMap extends EventEmitter {\n\ton(event: 'create', listener: (newData: VoiceUserData) => void): this;\n\ton(event: 'update', listener: (oldData: VoiceUserData | undefined, newData: VoiceUserData) => void): this;\n\ton(event: 'delete', listener: (deletedData: VoiceUserData) => void): this;\n}\n\n/**\n * Maps audio SSRCs to data of users in voice connections.\n */\nexport class SSRCMap extends EventEmitter {\n\t/**\n\t * The underlying map.\n\t */\n\tprivate readonly map: Map;\n\n\tpublic constructor() {\n\t\tsuper();\n\t\tthis.map = new Map();\n\t}\n\n\t/**\n\t * Updates the map with new user data\n\t *\n\t * @param data - The data to update with\n\t */\n\tpublic update(data: VoiceUserData) {\n\t\tconst existing = this.map.get(data.audioSSRC);\n\n\t\tconst newValue = {\n\t\t\t...this.map.get(data.audioSSRC),\n\t\t\t...data,\n\t\t};\n\n\t\tthis.map.set(data.audioSSRC, newValue);\n\t\tif (!existing) this.emit('create', newValue);\n\t\tthis.emit('update', existing, newValue);\n\t}\n\n\t/**\n\t * Gets the stored voice data of a user.\n\t *\n\t * @param target - The target, either their user id or audio SSRC\n\t */\n\tpublic get(target: number | string) {\n\t\tif (typeof target === 'number') {\n\t\t\treturn this.map.get(target);\n\t\t}\n\n\t\tfor (const data of this.map.values()) {\n\t\t\tif (data.userId === target) {\n\t\t\t\treturn data;\n\t\t\t}\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Deletes the stored voice data about a user.\n\t *\n\t * @param target - The target of the delete operation, either their audio SSRC or user id\n\t * @returns The data that was deleted, if any\n\t */\n\tpublic delete(target: number | string) {\n\t\tif (typeof target === 'number') {\n\t\t\tconst existing = this.map.get(target);\n\t\t\tif (existing) {\n\t\t\t\tthis.map.delete(target);\n\t\t\t\tthis.emit('delete', existing);\n\t\t\t}\n\n\t\t\treturn existing;\n\t\t}\n\n\t\tfor (const [audioSSRC, data] of this.map.entries()) {\n\t\t\tif (data.userId === target) {\n\t\t\t\tthis.map.delete(audioSSRC);\n\t\t\t\tthis.emit('delete', data);\n\t\t\t\treturn data;\n\t\t\t}\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n","/* eslint-disable @typescript-eslint/unified-signatures */\nimport { EventEmitter } from 'node:events';\n\nexport interface SpeakingMap extends EventEmitter {\n\t/**\n\t * Emitted when a user starts speaking.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'start', listener: (userId: string) => void): this;\n\n\t/**\n\t * Emitted when a user ends speaking.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'end', listener: (userId: string) => void): this;\n}\n\n/**\n * Tracks the speaking states of users in a voice channel.\n */\nexport class SpeakingMap extends EventEmitter {\n\t/**\n\t * The delay after a packet is received from a user until they're marked as not speaking anymore.\n\t */\n\tpublic static readonly DELAY = 100;\n\n\t/**\n\t * The currently speaking users, mapped to the milliseconds since UNIX epoch at which they started speaking.\n\t */\n\tpublic readonly users: Map;\n\n\tprivate readonly speakingTimeouts: Map;\n\n\tpublic constructor() {\n\t\tsuper();\n\t\tthis.users = new Map();\n\t\tthis.speakingTimeouts = new Map();\n\t}\n\n\tpublic onPacket(userId: string) {\n\t\tconst timeout = this.speakingTimeouts.get(userId);\n\t\tif (timeout) {\n\t\t\tclearTimeout(timeout);\n\t\t} else {\n\t\t\tthis.users.set(userId, Date.now());\n\t\t\tthis.emit('start', userId);\n\t\t}\n\n\t\tthis.startTimeout(userId);\n\t}\n\n\tprivate startTimeout(userId: string) {\n\t\tthis.speakingTimeouts.set(\n\t\t\tuserId,\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.emit('end', userId);\n\t\t\t\tthis.speakingTimeouts.delete(userId);\n\t\t\t\tthis.users.delete(userId);\n\t\t\t}, SpeakingMap.DELAY),\n\t\t);\n\t}\n}\n","import type { JoinConfig } from './DataStore';\nimport { createVoiceConnection } from './VoiceConnection';\nimport type { DiscordGatewayAdapterCreator } from './util/adapter';\n\n/**\n * The options that can be given when creating a voice connection.\n */\nexport interface CreateVoiceConnectionOptions {\n\tadapterCreator: DiscordGatewayAdapterCreator;\n\n\t/**\n\t * Whether to use the DAVE protocol for end-to-end encryption. Defaults to true.\n\t */\n\tdaveEncryption?: boolean | undefined;\n\n\t/**\n\t * If true, debug messages will be enabled for the voice connection and its\n\t * related components. Defaults to false.\n\t */\n\tdebug?: boolean | undefined;\n\n\t/**\n\t * The amount of consecutive decryption failures needed to try to\n\t * re-initialize the end-to-end encrypted session to recover. Defaults to 24.\n\t */\n\tdecryptionFailureTolerance?: number | undefined;\n}\n\n/**\n * The options that can be given when joining a voice channel.\n */\nexport interface JoinVoiceChannelOptions {\n\t/**\n\t * The id of the Discord voice channel to join.\n\t */\n\tchannelId: string;\n\n\t/**\n\t * An optional group identifier for the voice connection.\n\t */\n\tgroup?: string;\n\n\t/**\n\t * The id of the guild that the voice channel belongs to.\n\t */\n\tguildId: string;\n\n\t/**\n\t * Whether to join the channel deafened (defaults to true)\n\t */\n\tselfDeaf?: boolean;\n\n\t/**\n\t * Whether to join the channel muted (defaults to true)\n\t */\n\tselfMute?: boolean;\n}\n\n/**\n * Creates a VoiceConnection to a Discord voice channel.\n *\n * @param options - the options for joining the voice channel\n */\nexport function joinVoiceChannel(options: CreateVoiceConnectionOptions & JoinVoiceChannelOptions) {\n\tconst joinConfig: JoinConfig = {\n\t\tselfDeaf: true,\n\t\tselfMute: false,\n\t\tgroup: 'default',\n\t\t...options,\n\t};\n\n\treturn createVoiceConnection(joinConfig, {\n\t\tadapterCreator: options.adapterCreator,\n\t\tdebug: options.debug,\n\t\tdaveEncryption: options.daveEncryption,\n\t\tdecryptionFailureTolerance: options.decryptionFailureTolerance,\n\t});\n}\n","import type { Buffer } from 'node:buffer';\nimport { pipeline, type Readable } from 'node:stream';\nimport prism from 'prism-media';\nimport { noop } from '../util/util';\nimport { SILENCE_FRAME, type AudioPlayer } from './AudioPlayer';\nimport { findPipeline, StreamType, TransformerType, type Edge } from './TransformerGraph';\n\n/**\n * Options that are set when creating a new audio resource.\n *\n * @typeParam Metadata - the type for the metadata (if any) of the audio resource\n */\nexport interface CreateAudioResourceOptions {\n\t/**\n\t * Whether or not inline volume should be enabled. If enabled, you will be able to change the volume\n\t * of the stream on-the-fly. However, this also increases the performance cost of playback. Defaults to `false`.\n\t */\n\tinlineVolume?: boolean;\n\n\t/**\n\t * The type of the input stream. Defaults to `StreamType.Arbitrary`.\n\t */\n\tinputType?: StreamType;\n\n\t/**\n\t * Optional metadata that can be attached to the resource (e.g. track title, random id).\n\t * This is useful for identification purposes when the resource is passed around in events.\n\t * See {@link AudioResource.metadata}\n\t */\n\tmetadata?: Metadata;\n\n\t/**\n\t * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches.\n\t * Defaults to 5.\n\t */\n\tsilencePaddingFrames?: number;\n}\n\n/**\n * Represents an audio resource that can be played by an audio player.\n *\n * @typeParam Metadata - the type for the metadata (if any) of the audio resource\n */\nexport class AudioResource {\n\t/**\n\t * An object-mode Readable stream that emits Opus packets. This is what is played by audio players.\n\t */\n\tpublic readonly playStream: Readable;\n\n\t/**\n\t * The pipeline used to convert the input stream into a playable format. For example, this may\n\t * contain an FFmpeg component for arbitrary inputs, and it may contain a VolumeTransformer component\n\t * for resources with inline volume transformation enabled.\n\t */\n\tpublic readonly edges: readonly Edge[];\n\n\t/**\n\t * Optional metadata that can be used to identify the resource.\n\t */\n\tpublic metadata: Metadata;\n\n\t/**\n\t * If the resource was created with inline volume transformation enabled, then this will be a\n\t * prism-media VolumeTransformer. You can use this to alter the volume of the stream.\n\t */\n\tpublic readonly volume?: prism.VolumeTransformer;\n\n\t/**\n\t * If using an Opus encoder to create this audio resource, then this will be a prism-media opus.Encoder.\n\t * You can use this to control settings such as bitrate, FEC, PLP.\n\t */\n\tpublic readonly encoder?: prism.opus.Encoder;\n\n\t/**\n\t * The audio player that the resource is subscribed to, if any.\n\t */\n\tpublic audioPlayer?: AudioPlayer | undefined;\n\n\t/**\n\t * The playback duration of this audio resource, given in milliseconds.\n\t */\n\tpublic playbackDuration = 0;\n\n\t/**\n\t * Whether or not the stream for this resource has started (data has become readable)\n\t */\n\tpublic started = false;\n\n\t/**\n\t * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches.\n\t */\n\tpublic readonly silencePaddingFrames: number;\n\n\t/**\n\t * The number of remaining silence frames to play. If -1, the frames have not yet started playing.\n\t */\n\tpublic silenceRemaining = -1;\n\n\tpublic constructor(\n\t\tedges: readonly Edge[],\n\t\tstreams: readonly Readable[],\n\t\tmetadata: Metadata,\n\t\tsilencePaddingFrames: number,\n\t) {\n\t\tthis.edges = edges;\n\t\tthis.playStream = streams.length > 1 ? (pipeline(streams, noop) as any as Readable) : streams[0]!;\n\t\tthis.metadata = metadata;\n\t\tthis.silencePaddingFrames = silencePaddingFrames;\n\n\t\tfor (const stream of streams) {\n\t\t\tif (stream instanceof prism.VolumeTransformer) {\n\t\t\t\tthis.volume = stream;\n\t\t\t} else if (stream instanceof prism.opus.Encoder) {\n\t\t\t\tthis.encoder = stream;\n\t\t\t}\n\t\t}\n\n\t\tthis.playStream.once('readable', () => (this.started = true));\n\t}\n\n\t/**\n\t * Whether this resource is readable. If the underlying resource is no longer readable, this will still return true\n\t * while there are silence padding frames left to play.\n\t */\n\tpublic get readable() {\n\t\tif (this.silenceRemaining === 0) return false;\n\t\tconst real = this.playStream.readable;\n\t\tif (!real) {\n\t\t\tif (this.silenceRemaining === -1) this.silenceRemaining = this.silencePaddingFrames;\n\t\t\treturn this.silenceRemaining !== 0;\n\t\t}\n\n\t\treturn real;\n\t}\n\n\t/**\n\t * Whether this resource has ended or not.\n\t */\n\tpublic get ended() {\n\t\treturn this.playStream.readableEnded || this.playStream.destroyed || this.silenceRemaining === 0;\n\t}\n\n\t/**\n\t * Attempts to read an Opus packet from the audio resource. If a packet is available, the playbackDuration\n\t * is incremented.\n\t *\n\t * @remarks\n\t * It is advisable to check that the playStream is readable before calling this method. While no runtime\n\t * errors will be thrown, you should check that the resource is still available before attempting to\n\t * read from it.\n\t * @internal\n\t */\n\tpublic read(): Buffer | null {\n\t\tif (this.silenceRemaining === 0) {\n\t\t\treturn null;\n\t\t} else if (this.silenceRemaining > 0) {\n\t\t\tthis.silenceRemaining--;\n\t\t\treturn SILENCE_FRAME;\n\t\t}\n\n\t\tconst packet = this.playStream.read() as Buffer | null;\n\t\tif (packet) {\n\t\t\tthis.playbackDuration += 20;\n\t\t}\n\n\t\treturn packet;\n\t}\n}\n\n/**\n * Ensures that a path contains at least one volume transforming component.\n *\n * @param path - The path to validate constraints on\n */\nexport const VOLUME_CONSTRAINT = (path: Edge[]) => path.some((edge) => edge.type === TransformerType.InlineVolume);\n\nexport const NO_CONSTRAINT = () => true;\n\n/**\n * Tries to infer the type of a stream to aid with transcoder pipelining.\n *\n * @param stream - The stream to infer the type of\n */\nexport function inferStreamType(stream: Readable): {\n\thasVolume: boolean;\n\tstreamType: StreamType;\n} {\n\tif (stream instanceof prism.opus.Encoder) {\n\t\treturn { streamType: StreamType.Opus, hasVolume: false };\n\t} else if (stream instanceof prism.opus.Decoder) {\n\t\treturn { streamType: StreamType.Raw, hasVolume: false };\n\t} else if (stream instanceof prism.VolumeTransformer) {\n\t\treturn { streamType: StreamType.Raw, hasVolume: true };\n\t} else if (stream instanceof prism.opus.OggDemuxer) {\n\t\treturn { streamType: StreamType.Opus, hasVolume: false };\n\t} else if (stream instanceof prism.opus.WebmDemuxer) {\n\t\treturn { streamType: StreamType.Opus, hasVolume: false };\n\t}\n\n\treturn { streamType: StreamType.Arbitrary, hasVolume: false };\n}\n\n/**\n * Creates an audio resource that can be played by audio players.\n *\n * @remarks\n * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used.\n *\n * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created\n * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg,\n * Opus transcoders, and Ogg/WebM demuxers.\n * @param input - The resource to play\n * @param options - Configurable options for creating the resource\n * @typeParam Metadata - the type for the metadata (if any) of the audio resource\n */\nexport function createAudioResource(\n\tinput: Readable | string,\n\toptions: CreateAudioResourceOptions &\n\t\tPick<\n\t\t\tMetadata extends null | undefined\n\t\t\t\t? CreateAudioResourceOptions\n\t\t\t\t: Required>,\n\t\t\t'metadata'\n\t\t>,\n): AudioResource;\n\n/**\n * Creates an audio resource that can be played by audio players.\n *\n * @remarks\n * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used.\n *\n * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created\n * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg,\n * Opus transcoders, and Ogg/WebM demuxers.\n * @param input - The resource to play\n * @param options - Configurable options for creating the resource\n * @typeParam Metadata - the type for the metadata (if any) of the audio resource\n */\nexport function createAudioResource(\n\tinput: Readable | string,\n\toptions?: Omit, 'metadata'>,\n): AudioResource;\n\n/**\n * Creates an audio resource that can be played by audio players.\n *\n * @remarks\n * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used.\n *\n * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created\n * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg,\n * Opus transcoders, and Ogg/WebM demuxers.\n * @param input - The resource to play\n * @param options - Configurable options for creating the resource\n * @typeParam Metadata - the type for the metadata (if any) of the audio resource\n */\nexport function createAudioResource(\n\tinput: Readable | string,\n\toptions: CreateAudioResourceOptions = {},\n): AudioResource {\n\tlet inputType = options.inputType;\n\tlet needsInlineVolume = Boolean(options.inlineVolume);\n\n\t// string inputs can only be used with FFmpeg\n\tif (typeof input === 'string') {\n\t\tinputType = StreamType.Arbitrary;\n\t} else if (inputType === undefined) {\n\t\tconst analysis = inferStreamType(input);\n\t\tinputType = analysis.streamType;\n\t\tneedsInlineVolume = needsInlineVolume && !analysis.hasVolume;\n\t}\n\n\tconst transformerPipeline = findPipeline(inputType, needsInlineVolume ? VOLUME_CONSTRAINT : NO_CONSTRAINT);\n\n\tif (transformerPipeline.length === 0) {\n\t\tif (typeof input === 'string') throw new Error(`Invalid pipeline constructed for string resource '${input}'`);\n\t\t// No adjustments required\n\t\treturn new AudioResource(\n\t\t\t[],\n\t\t\t[input],\n\t\t\t(options.metadata ?? null) as Metadata,\n\t\t\toptions.silencePaddingFrames ?? 5,\n\t\t);\n\t}\n\n\tconst streams = transformerPipeline.map((edge) => edge.transformer(input));\n\tif (typeof input !== 'string') streams.unshift(input);\n\n\treturn new AudioResource(\n\t\ttransformerPipeline,\n\t\tstreams,\n\t\t(options.metadata ?? null) as Metadata,\n\t\toptions.silencePaddingFrames ?? 5,\n\t);\n}\n","import type { Readable } from 'node:stream';\nimport prism from 'prism-media';\n\n/**\n * This module creates a Transformer Graph to figure out what the most efficient way\n * of transforming the input stream into something playable would be.\n */\n\nconst FFMPEG_PCM_ARGUMENTS = ['-analyzeduration', '0', '-loglevel', '0', '-f', 's16le', '-ar', '48000', '-ac', '2'];\nconst FFMPEG_OPUS_ARGUMENTS = [\n\t'-analyzeduration',\n\t'0',\n\t'-loglevel',\n\t'0',\n\t'-acodec',\n\t'libopus',\n\t'-f',\n\t'opus',\n\t'-ar',\n\t'48000',\n\t'-ac',\n\t'2',\n];\n\n/**\n * The different types of stream that can exist within the pipeline.\n */\nexport enum StreamType {\n\t/**\n\t * The type of the stream at this point is unknown.\n\t */\n\tArbitrary = 'arbitrary',\n\t/**\n\t * The stream at this point is Opus audio encoded in an Ogg wrapper.\n\t */\n\tOggOpus = 'ogg/opus',\n\t/**\n\t * The stream at this point is Opus audio, and the stream is in object-mode. This is ready to play.\n\t */\n\tOpus = 'opus',\n\t/**\n\t * The stream at this point is s16le PCM.\n\t */\n\tRaw = 'raw',\n\t/**\n\t * The stream at this point is Opus audio encoded in a WebM wrapper.\n\t */\n\tWebmOpus = 'webm/opus',\n}\n\n/**\n * The different types of transformers that can exist within the pipeline.\n */\nexport enum TransformerType {\n\tFFmpegOgg = 'ffmpeg ogg',\n\tFFmpegPCM = 'ffmpeg pcm',\n\tInlineVolume = 'volume transformer',\n\tOggOpusDemuxer = 'ogg/opus demuxer',\n\tOpusDecoder = 'opus decoder',\n\tOpusEncoder = 'opus encoder',\n\tWebmOpusDemuxer = 'webm/opus demuxer',\n}\n\n/**\n * Represents a pathway from one stream type to another using a transformer.\n */\nexport interface Edge {\n\tcost: number;\n\tfrom: Node;\n\tto: Node;\n\ttransformer(input: Readable | string): Readable;\n\ttype: TransformerType;\n}\n\n/**\n * Represents a type of stream within the graph, e.g. an Opus stream, or a stream of raw audio.\n */\nexport class Node {\n\t/**\n\t * The outbound edges from this node.\n\t */\n\tpublic readonly edges: Edge[] = [];\n\n\t/**\n\t * The type of stream for this node.\n\t */\n\tpublic readonly type: StreamType;\n\n\tpublic constructor(type: StreamType) {\n\t\tthis.type = type;\n\t}\n\n\t/**\n\t * Creates an outbound edge from this node.\n\t *\n\t * @param edge - The edge to create\n\t */\n\tpublic addEdge(edge: Omit) {\n\t\tthis.edges.push({ ...edge, from: this });\n\t}\n}\n\n// Create a node for each stream type\nlet NODES: Map | null = null;\n\n/**\n * Gets a node from its stream type.\n *\n * @param type - The stream type of the target node\n */\nexport function getNode(type: StreamType) {\n\tconst node = (NODES ??= initializeNodes()).get(type);\n\tif (!node) throw new Error(`Node type '${type}' does not exist!`);\n\treturn node;\n}\n\n// Try to enable FFmpeg Ogg optimizations\nfunction canEnableFFmpegOptimizations(): boolean {\n\ttry {\n\t\treturn prism.FFmpeg.getInfo().output.includes('--enable-libopus');\n\t} catch {}\n\n\treturn false;\n}\n\nfunction initializeNodes(): Map {\n\tconst nodes = new Map();\n\tfor (const streamType of Object.values(StreamType)) {\n\t\tnodes.set(streamType, new Node(streamType));\n\t}\n\n\tnodes.get(StreamType.Raw)!.addEdge({\n\t\ttype: TransformerType.OpusEncoder,\n\t\tto: nodes.get(StreamType.Opus)!,\n\t\tcost: 1.5,\n\t\ttransformer: () => new prism.opus.Encoder({ rate: 48_000, channels: 2, frameSize: 960 }),\n\t});\n\n\tnodes.get(StreamType.Opus)!.addEdge({\n\t\ttype: TransformerType.OpusDecoder,\n\t\tto: nodes.get(StreamType.Raw)!,\n\t\tcost: 1.5,\n\t\ttransformer: () => new prism.opus.Decoder({ rate: 48_000, channels: 2, frameSize: 960 }),\n\t});\n\n\tnodes.get(StreamType.OggOpus)!.addEdge({\n\t\ttype: TransformerType.OggOpusDemuxer,\n\t\tto: nodes.get(StreamType.Opus)!,\n\t\tcost: 1,\n\t\ttransformer: () => new prism.opus.OggDemuxer(),\n\t});\n\n\tnodes.get(StreamType.WebmOpus)!.addEdge({\n\t\ttype: TransformerType.WebmOpusDemuxer,\n\t\tto: nodes.get(StreamType.Opus)!,\n\t\tcost: 1,\n\t\ttransformer: () => new prism.opus.WebmDemuxer(),\n\t});\n\n\tconst FFMPEG_PCM_EDGE: Omit = {\n\t\ttype: TransformerType.FFmpegPCM,\n\t\tto: nodes.get(StreamType.Raw)!,\n\t\tcost: 2,\n\t\ttransformer: (input) =>\n\t\t\tnew prism.FFmpeg({\n\t\t\t\targs: ['-i', typeof input === 'string' ? input : '-', ...FFMPEG_PCM_ARGUMENTS],\n\t\t\t}),\n\t};\n\n\tnodes.get(StreamType.Arbitrary)!.addEdge(FFMPEG_PCM_EDGE);\n\tnodes.get(StreamType.OggOpus)!.addEdge(FFMPEG_PCM_EDGE);\n\tnodes.get(StreamType.WebmOpus)!.addEdge(FFMPEG_PCM_EDGE);\n\n\tnodes.get(StreamType.Raw)!.addEdge({\n\t\ttype: TransformerType.InlineVolume,\n\t\tto: nodes.get(StreamType.Raw)!,\n\t\tcost: 0.5,\n\t\ttransformer: () => new prism.VolumeTransformer({ type: 's16le' }),\n\t});\n\n\tif (canEnableFFmpegOptimizations()) {\n\t\tconst FFMPEG_OGG_EDGE: Omit = {\n\t\t\ttype: TransformerType.FFmpegOgg,\n\t\t\tto: nodes.get(StreamType.OggOpus)!,\n\t\t\tcost: 2,\n\t\t\ttransformer: (input) =>\n\t\t\t\tnew prism.FFmpeg({\n\t\t\t\t\targs: ['-i', typeof input === 'string' ? input : '-', ...FFMPEG_OPUS_ARGUMENTS],\n\t\t\t\t}),\n\t\t};\n\t\tnodes.get(StreamType.Arbitrary)!.addEdge(FFMPEG_OGG_EDGE);\n\t\t// Include Ogg and WebM as well in case they have different sampling rates or are mono instead of stereo\n\t\t// at the moment, this will not do anything. However, if/when detection for correct Opus headers is\n\t\t// implemented, this will help inform the voice engine that it is able to transcode the audio.\n\t\tnodes.get(StreamType.OggOpus)!.addEdge(FFMPEG_OGG_EDGE);\n\t\tnodes.get(StreamType.WebmOpus)!.addEdge(FFMPEG_OGG_EDGE);\n\t}\n\n\treturn nodes;\n}\n\n/**\n * Represents a step in the path from node A to node B.\n */\ninterface Step {\n\t/**\n\t * The cost of the steps after this step.\n\t */\n\tcost: number;\n\n\t/**\n\t * The edge associated with this step.\n\t */\n\tedge?: Edge;\n\n\t/**\n\t * The next step.\n\t */\n\tnext?: Step;\n}\n\n/**\n * Finds the shortest cost path from node A to node B.\n *\n * @param from - The start node\n * @param constraints - Extra validation for a potential solution. Takes a path, returns true if the path is valid\n * @param goal - The target node\n * @param path - The running path\n * @param depth - The number of remaining recursions\n */\nfunction findPath(\n\tfrom: Node,\n\tconstraints: (path: Edge[]) => boolean,\n\tgoal = getNode(StreamType.Opus),\n\tpath: Edge[] = [],\n\tdepth = 5,\n): Step {\n\tif (from === goal && constraints(path)) {\n\t\treturn { cost: 0 };\n\t} else if (depth === 0) {\n\t\treturn { cost: Number.POSITIVE_INFINITY };\n\t}\n\n\tlet currentBest: Step | undefined;\n\tfor (const edge of from.edges) {\n\t\tif (currentBest && edge.cost > currentBest.cost) continue;\n\t\tconst next = findPath(edge.to, constraints, goal, [...path, edge], depth - 1);\n\t\tconst cost = edge.cost + next.cost;\n\t\tif (!currentBest || cost < currentBest.cost) {\n\t\t\tcurrentBest = { cost, edge, next };\n\t\t}\n\t}\n\n\treturn currentBest ?? { cost: Number.POSITIVE_INFINITY };\n}\n\n/**\n * Takes the solution from findPath and assembles it into a list of edges.\n *\n * @param step - The first step of the path\n */\nfunction constructPipeline(step: Step) {\n\tconst edges = [];\n\tlet current: Step | undefined = step;\n\twhile (current?.edge) {\n\t\tedges.push(current.edge);\n\t\tcurrent = current.next;\n\t}\n\n\treturn edges;\n}\n\n/**\n * Finds the lowest-cost pipeline to convert the input stream type into an Opus stream.\n *\n * @param from - The stream type to start from\n * @param constraint - Extra constraints that may be imposed on potential solution\n */\nexport function findPipeline(from: StreamType, constraint: (path: Edge[]) => boolean) {\n\treturn constructPipeline(findPath(getNode(from), constraint));\n}\n","/* eslint-disable @typescript-eslint/no-var-requires */\n/* eslint-disable @typescript-eslint/no-require-imports */\nimport { getCiphers } from 'node:crypto';\nimport { resolve, dirname } from 'node:path';\nimport prism from 'prism-media';\n\n/**\n * Tries to find the package.json file for a given module.\n *\n * @param dir - The directory to look in\n * @param packageName - The name of the package to look for\n * @param depth - The maximum recursion depth\n */\nfunction findPackageJSON(\n\tdir: string,\n\tpackageName: string,\n\tdepth: number,\n): { name: string; version: string } | undefined {\n\tif (depth === 0) return undefined;\n\tconst attemptedPath = resolve(dir, './package.json');\n\ttry {\n\t\tconst pkg = require(attemptedPath);\n\t\tif (pkg.name !== packageName) throw new Error('package.json does not match');\n\t\treturn pkg;\n\t} catch {\n\t\treturn findPackageJSON(resolve(dir, '..'), packageName, depth - 1);\n\t}\n}\n\n/**\n * Tries to find the version of a dependency.\n *\n * @param name - The package to find the version of\n */\nfunction version(name: string): string {\n\ttry {\n\t\tif (name === '@discordjs/voice') {\n\t\t\treturn '0.19.2';\n\t\t}\n\n\t\tconst pkg = findPackageJSON(dirname(require.resolve(name)), name, 3);\n\t\treturn pkg?.version ?? 'not found';\n\t} catch {\n\t\treturn 'not found';\n\t}\n}\n\n/**\n * Generates a report of the dependencies used by the \\@discordjs/voice module.\n * Useful for debugging.\n */\nexport function generateDependencyReport() {\n\tconst report = [];\n\tconst addVersion = (name: string) => report.push(`- ${name}: ${version(name)}`);\n\t// general\n\treport.push('Core Dependencies');\n\taddVersion('@discordjs/voice');\n\taddVersion('prism-media');\n\treport.push('');\n\n\t// opus\n\treport.push('Opus Libraries');\n\taddVersion('@discordjs/opus');\n\taddVersion('opusscript');\n\treport.push('');\n\n\t// encryption\n\treport.push('Encryption Libraries');\n\treport.push(`- native crypto support for aes-256-gcm: ${getCiphers().includes('aes-256-gcm') ? 'yes' : 'no'}`);\n\taddVersion('sodium-native');\n\taddVersion('sodium');\n\taddVersion('libsodium-wrappers');\n\taddVersion('@stablelib/xchacha20poly1305');\n\taddVersion('@noble/ciphers');\n\treport.push('');\n\n\t// dave\n\treport.push('DAVE Libraries');\n\taddVersion('@snazzah/davey');\n\treport.push('');\n\n\t// ffmpeg\n\treport.push('FFmpeg');\n\ttry {\n\t\tconst info = prism.FFmpeg.getInfo();\n\t\treport.push(`- version: ${info.version}`);\n\t\treport.push(`- libopus: ${info.output.includes('--enable-libopus') ? 'yes' : 'no'}`);\n\t} catch {\n\t\treport.push('- not found');\n\t}\n\n\treturn ['-'.repeat(50), ...report, '-'.repeat(50)].join('\\n');\n}\n","import { type EventEmitter, once } from 'node:events';\nimport type { VoiceConnection, VoiceConnectionStatus } from '../VoiceConnection';\nimport type { AudioPlayer, AudioPlayerStatus } from '../audio/AudioPlayer';\nimport { abortAfter } from './abortAfter';\n\n/**\n * Allows a voice connection a specified amount of time to enter a given state, otherwise rejects with an error.\n *\n * @param target - The voice connection that we want to observe the state change for\n * @param status - The status that the voice connection should be in\n * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation\n */\nexport function entersState(\n\ttarget: VoiceConnection,\n\tstatus: VoiceConnectionStatus,\n\ttimeoutOrSignal: AbortSignal | number,\n): Promise;\n\n/**\n * Allows an audio player a specified amount of time to enter a given state, otherwise rejects with an error.\n *\n * @param target - The audio player that we want to observe the state change for\n * @param status - The status that the audio player should be in\n * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation\n */\nexport function entersState(\n\ttarget: AudioPlayer,\n\tstatus: AudioPlayerStatus,\n\ttimeoutOrSignal: AbortSignal | number,\n): Promise;\n\n/**\n * Allows a target a specified amount of time to enter a given state, otherwise rejects with an error.\n *\n * @param target - The object that we want to observe the state change for\n * @param status - The status that the target should be in\n * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation\n */\nexport async function entersState(\n\ttarget: Target,\n\tstatus: AudioPlayerStatus | VoiceConnectionStatus,\n\ttimeoutOrSignal: AbortSignal | number,\n) {\n\tif (target.state.status !== status) {\n\t\tconst [ac, signal] =\n\t\t\ttypeof timeoutOrSignal === 'number' ? abortAfter(timeoutOrSignal) : [undefined, timeoutOrSignal];\n\t\ttry {\n\t\t\tawait once(target as EventEmitter, status, { signal });\n\t\t} finally {\n\t\t\tac?.abort();\n\t\t}\n\t}\n\n\treturn target;\n}\n","/**\n * Creates an abort controller that aborts after the given time.\n *\n * @param delay - The time in milliseconds to wait before aborting\n */\nexport function abortAfter(delay: number): [AbortController, AbortSignal] {\n\tconst ac = new AbortController();\n\tconst timeout = setTimeout(() => ac.abort(), delay);\n\tac.signal.addEventListener('abort', () => clearTimeout(timeout));\n\treturn [ac, ac.signal];\n}\n","import { Buffer } from 'node:buffer';\nimport process from 'node:process';\nimport { Readable } from 'node:stream';\nimport prism from 'prism-media';\nimport { StreamType } from '..';\nimport { noop } from './util';\n\n/**\n * Takes an Opus Head, and verifies whether the associated Opus audio is suitable to play in a Discord voice channel.\n *\n * @param opusHead - The Opus Head to validate\n * @returns `true` if suitable to play in a Discord voice channel, otherwise `false`\n */\nexport function validateDiscordOpusHead(opusHead: Buffer): boolean {\n\tconst channels = opusHead.readUInt8(9);\n\tconst sampleRate = opusHead.readUInt32LE(12);\n\treturn channels === 2 && sampleRate === 48_000;\n}\n\n/**\n * The resulting information after probing an audio stream\n */\nexport interface ProbeInfo {\n\t/**\n\t * The readable audio stream to use. You should use this rather than the input stream, as the probing\n\t * function can sometimes read the input stream to its end and cause the stream to close.\n\t */\n\tstream: Readable;\n\n\t/**\n\t * The recommended stream type for this audio stream.\n\t */\n\ttype: StreamType;\n}\n\n/**\n * Attempt to probe a readable stream to figure out whether it can be demuxed using an Ogg or WebM Opus demuxer.\n *\n * @param stream - The readable stream to probe\n * @param probeSize - The number of bytes to attempt to read before giving up on the probe\n * @param validator - The Opus Head validator function\n * @experimental\n */\nexport async function demuxProbe(\n\tstream: Readable,\n\tprobeSize = 1_024,\n\tvalidator = validateDiscordOpusHead,\n): Promise {\n\treturn new Promise((resolve, reject) => {\n\t\t// Preconditions\n\t\tif (stream.readableObjectMode) {\n\t\t\treject(new Error('Cannot probe a readable stream in object mode'));\n\t\t\treturn;\n\t\t}\n\n\t\tif (stream.readableEnded) {\n\t\t\treject(new Error('Cannot probe a stream that has ended'));\n\t\t\treturn;\n\t\t}\n\n\t\tlet readBuffer = Buffer.alloc(0);\n\n\t\tlet resolved: StreamType | undefined;\n\n\t\tconst finish = (type: StreamType) => {\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-use-before-define\n\t\t\tstream.off('data', onData);\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-use-before-define\n\t\t\tstream.off('close', onClose);\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-use-before-define\n\t\t\tstream.off('end', onClose);\n\t\t\tstream.pause();\n\t\t\tresolved = type;\n\t\t\tif (stream.readableEnded) {\n\t\t\t\tresolve({\n\t\t\t\t\tstream: Readable.from(readBuffer),\n\t\t\t\t\ttype,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tif (readBuffer.length > 0) {\n\t\t\t\t\tstream.push(readBuffer);\n\t\t\t\t}\n\n\t\t\t\tresolve({\n\t\t\t\t\tstream,\n\t\t\t\t\ttype,\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\n\t\tconst foundHead = (type: StreamType) => (head: Buffer) => {\n\t\t\tif (validator(head)) {\n\t\t\t\tfinish(type);\n\t\t\t}\n\t\t};\n\n\t\tconst webm = new prism.opus.WebmDemuxer();\n\t\twebm.once('error', noop);\n\t\twebm.on('head', foundHead(StreamType.WebmOpus));\n\n\t\tconst ogg = new prism.opus.OggDemuxer();\n\t\togg.once('error', noop);\n\t\togg.on('head', foundHead(StreamType.OggOpus));\n\n\t\tconst onClose = () => {\n\t\t\tif (!resolved) {\n\t\t\t\tfinish(StreamType.Arbitrary);\n\t\t\t}\n\t\t};\n\n\t\tconst onData = (buffer: Buffer) => {\n\t\t\treadBuffer = Buffer.concat([readBuffer, buffer]);\n\n\t\t\twebm.write(buffer);\n\t\t\togg.write(buffer);\n\n\t\t\tif (readBuffer.length >= probeSize) {\n\t\t\t\tstream.off('data', onData);\n\t\t\t\tstream.pause();\n\t\t\t\tprocess.nextTick(onClose);\n\t\t\t}\n\t\t};\n\n\t\tstream.once('error', reject);\n\t\tstream.on('data', onData);\n\t\tstream.once('close', onClose);\n\t\tstream.once('end', onClose);\n\t});\n}\n"],"mappings":"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","names":["version","import_node_events","import_node_buffer","import_node_events","import_v8","nonce","crypto","resolve","import_node_buffer","import_node_events","import_node_buffer","NoSubscriberBehavior","AudioPlayerStatus","Davey","import_node_buffer","import_node_events","resolve","import_node_buffer","import_node_events","WebSocket","nonce","crypto","NetworkingStatusCode","stringifyState","option","import_node_buffer","import_node_crypto","import_v8","EndBehaviorType","process","import_node_events","import_node_events","nonce","crypto","VoiceConnectionStatus","VoiceConnectionDisconnectReason","import_node_stream","import_prism_media","StreamType","TransformerType","prism","prism","import_node_crypto","import_prism_media","prism","import_node_events","import_node_buffer","import_node_process","import_node_stream","import_prism_media","resolve","prism","process","version"]} \ No newline at end of file diff --git a/services/discord-gateway/vendor/discord-voice-fork/dist/index.mjs b/services/discord-gateway/vendor/discord-voice-fork/dist/index.mjs deleted file mode 100644 index 4c92f5fa..00000000 --- a/services/discord-gateway/vendor/discord-voice-fork/dist/index.mjs +++ /dev/null @@ -1,3214 +0,0 @@ -import { createRequire as topLevelCreateRequire } from "module"; -const require = topLevelCreateRequire(import.meta.url); -var __defProp = Object.defineProperty; -var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); -var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { - get: (a, b) => (typeof require !== "undefined" ? require : a)[b] -}) : x)(function(x) { - if (typeof require !== "undefined") return require.apply(this, arguments); - throw Error('Dynamic require of "' + x + '" is not supported'); -}); - -// src/VoiceConnection.ts -import { EventEmitter as EventEmitter8 } from "events"; - -// src/DataStore.ts -import { GatewayOpcodes } from "discord-api-types/v10"; -function createJoinVoiceChannelPayload(config) { - return { - op: 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 !== -1) { - audioCycleInterval = setTimeout(() => audioCycleStep(), Math.max(1, nextTime - Date.now())); - } - 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 -import { Buffer as Buffer7 } from "buffer"; -import crypto from "crypto"; -import { EventEmitter as EventEmitter5 } from "events"; -import { VoiceEncryptionMode, VoiceOpcodes as VoiceOpcodes2 } from "discord-api-types/voice/v8"; - -// src/util/Secretbox.ts -import { Buffer as Buffer2 } from "buffer"; -var libs = { - "sodium-native": /* @__PURE__ */ __name((sodium) => ({ - crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => { - const message = Buffer2.alloc(cipherText.length - sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES); - sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(message, null, cipherText, additionalData, nonce2, key); - return message; - }, "crypto_aead_xchacha20poly1305_ietf_decrypt"), - crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => { - const cipherText = Buffer2.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"), - sodium: /* @__PURE__ */ __name((sodium) => ({ - crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => sodium.api.crypto_aead_xchacha20poly1305_ietf_decrypt(cipherText, additionalData, null, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_decrypt"), - crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => sodium.api.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_encrypt") - }), "sodium"), - "libsodium-wrappers": /* @__PURE__ */ __name((sodium) => ({ - crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, cipherText, additionalData, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_decrypt"), - crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_encrypt") - }), "libsodium-wrappers"), - "@stablelib/xchacha20poly1305": /* @__PURE__ */ __name((stablelib) => ({ - crypto_aead_xchacha20poly1305_ietf_decrypt(plaintext, additionalData, nonce2, key) { - const crypto3 = new stablelib.XChaCha20Poly1305(key); - return crypto3.open(nonce2, plaintext, additionalData); - }, - crypto_aead_xchacha20poly1305_ietf_encrypt(cipherText, additionalData, nonce2, key) { - const crypto3 = new stablelib.XChaCha20Poly1305(key); - return crypto3.seal(nonce2, cipherText, additionalData); - } - }), "@stablelib/xchacha20poly1305"), - "@noble/ciphers/chacha.js": /* @__PURE__ */ __name((noble) => ({ - crypto_aead_xchacha20poly1305_ietf_decrypt(cipherText, additionalData, nonce2, key) { - const chacha = noble.xchacha20poly1305(key, nonce2, additionalData); - return chacha.decrypt(cipherText); - }, - crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, nonce2, key) { - const chacha = noble.xchacha20poly1305(key, nonce2, additionalData); - return chacha.encrypt(plaintext); - } - }), "@noble/ciphers/chacha.js") -}; -var fallbackError = /* @__PURE__ */ __name(() => { - throw new Error( - `Cannot play audio as no valid encryption package is installed. -- Install one of: - - sodium - - libsodium-wrappers - - @stablelib/xchacha20poly1305 - - @noble/ciphers. -- Use the generateDependencyReport() function for more information. -` - ); -}, "fallbackError"); -var methods = { - crypto_aead_xchacha20poly1305_ietf_encrypt: fallbackError, - crypto_aead_xchacha20poly1305_ietf_decrypt: fallbackError -}; -var secretboxLoadPromise = new Promise(async (resolve2) => { - 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 { - } - } - resolve2(); -}); - -// src/util/constants.ts -var RTP_OPUS_PAYLOAD_TYPE = 120; - -// src/util/util.ts -var noop = /* @__PURE__ */ __name(() => { -}, "noop"); - -// src/networking/DAVESession.ts -import { Buffer as Buffer4 } from "buffer"; -import { EventEmitter as EventEmitter2 } from "events"; -import Davey from "@snazzah/davey"; - -// src/audio/AudioPlayer.ts -import { Buffer as Buffer3 } from "buffer"; -import { EventEmitter } from "events"; - -// src/audio/AudioPlayerError.ts -var AudioPlayerError = class extends Error { - static { - __name(this, "AudioPlayerError"); - } - /** - * The resource associated with the audio player at the time the error was thrown. - */ - resource; - constructor(error, resource) { - super(error.message); - this.resource = resource; - this.name = error.name; - this.stack = error.stack; - } -}; - -// src/audio/PlayerSubscription.ts -var PlayerSubscription = class { - static { - __name(this, "PlayerSubscription"); - } - /** - * The voice connection of this subscription. - */ - connection; - /** - * The audio player of this subscription. - */ - player; - constructor(connection, 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); - } -}; - -// src/audio/AudioPlayer.ts -var SILENCE_FRAME = Buffer3.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 extends EventEmitter { - static { - __name(this, "AudioPlayer"); - } - /** - * The state that the AudioPlayer is in. - */ - _state; - /** - * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio - * to the streams in this list. - */ - subscribers = []; - /** - * The behavior that the player should follow when it enters certain situations. - */ - behaviors; - /** - * The debug logger function, if debugging is enabled. - */ - debug; - /** - * Creates a new AudioPlayer. - */ - constructor(options = {}) { - super(); - 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. - * - * @remarks - * The setter will perform clean-up operations where necessary. - */ - get state() { - return this._state; - } - 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); - } - } -}; -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; -function getMaxProtocolVersion() { - return Davey.DAVE_PROTOCOL_VERSION; -} -__name(getMaxProtocolVersion, "getMaxProtocolVersion"); -var DAVESession = class extends EventEmitter2 { - static { - __name(this, "DAVESession"); - } - /** - * The channel id represented by this session. - */ - channelId; - /** - * The user id represented by this session. - */ - userId; - /** - * The protocol version being used. - */ - protocolVersion; - /** - * The last transition id executed. - */ - lastTransitionId; - /** - * The pending transitions, mapped by their ID and the protocol version. - */ - pendingTransitions = /* @__PURE__ */ new Map(); - /** - * Whether this session was downgraded previously. - */ - downgraded = false; - /** - * The amount of consecutive failures encountered when decrypting. - */ - consecutiveFailures = 0; - /** - * The amount of consecutive failures needed to attempt to recover. - */ - failureTolerance; - /** - * Whether this session is currently re-initializing due to an invalid transition. - */ - reinitializing = false; - /** - * The underlying DAVE Session of this wrapper. - */ - session; - constructor(protocolVersion, userId, channelId, options) { - super(); - 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 ? Buffer4.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, 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 { - } - } -}; - -// src/networking/VoiceUDPSocket.ts -import { Buffer as Buffer5 } from "buffer"; -import { createSocket } from "dgram"; -import { EventEmitter as EventEmitter3 } from "events"; -import { isIPv4 } from "net"; -function parseLocalPacket(message) { - const packet = Buffer5.from(message); - const ip = packet.subarray(8, packet.indexOf(0, 8)).toString("utf8"); - if (!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 extends EventEmitter3 { - static { - __name(this, "VoiceUDPSocket"); - } - /** - * The underlying network Socket for the VoiceUDPSocket. - */ - socket; - /** - * The socket details for Discord (remote) - */ - remote; - /** - * The counter used in the keep alive mechanism. - */ - keepAliveCounter = 0; - /** - * The buffer used to write the keep alive counter into. - */ - keepAliveBuffer; - /** - * The Node.js interval for the keep-alive mechanism. - */ - 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. - */ - ping; - /** - * Creates a new VoiceUDPSocket. - * - * @param remote - Details of the remote socket - */ - constructor(remote) { - super(); - this.socket = 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 = Buffer5.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((resolve2, reject) => { - const listener = /* @__PURE__ */ __name((message) => { - try { - if (message.readUInt16BE(0) !== 2) return; - const packet = parseLocalPacket(message); - this.socket.off("message", listener); - resolve2(packet); - } catch { - } - }, "listener"); - this.socket.on("message", listener); - this.socket.once("close", () => reject(new Error("Cannot perform IP discovery - socket closed"))); - const discoveryBuffer = Buffer5.alloc(74); - discoveryBuffer.writeUInt16BE(1, 0); - discoveryBuffer.writeUInt16BE(70, 2); - discoveryBuffer.writeUInt32BE(ssrc, 4); - this.send(discoveryBuffer); - }); - } -}; - -// src/networking/VoiceWebSocket.ts -import { Buffer as Buffer6 } from "buffer"; -import { EventEmitter as EventEmitter4 } from "events"; -import { VoiceOpcodes } from "discord-api-types/voice/v8"; -import WebSocket from "ws"; -var VoiceWebSocket = class extends EventEmitter4 { - static { - __name(this, "VoiceWebSocket"); - } - /** - * The current heartbeat interval, if any. - */ - 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. - */ - 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. - */ - lastHeartbeatSend; - /** - * The number of consecutively missed heartbeats. - */ - missedHeartbeats = 0; - /** - * The last recorded ping. - */ - ping; - /** - * The last sequence number acknowledged from Discord. Will be `-1` if no sequence numbered messages have been received. - */ - sequence = -1; - /** - * The debug logger function, if debugging is enabled. - */ - debug; - /** - * The underlying WebSocket of this wrapper. - */ - ws; - /** - * Creates a new VoiceWebSocket. - * - * @param address - The address to connect to - */ - constructor(address, debug) { - super(); - this.ws = new WebSocket(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 Buffer6 || event.data instanceof ArrayBuffer) { - const buffer = event.data instanceof ArrayBuffer ? Buffer6.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 === 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 = Buffer6.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: 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); - } - } -}; - -// src/networking/Networking.ts -var CHANNELS = 2; -var TIMESTAMP_INC = 48e3 / 100 * CHANNELS; -var MAX_NONCE_SIZE = 2 ** 32 - 1; -var SUPPORTED_ENCRYPTION_MODES = [VoiceEncryptionMode.AeadXChaCha20Poly1305RtpSize]; -if (crypto.getCiphers().includes("aes-256-gcm")) { - SUPPORTED_ENCRYPTION_MODES.unshift(VoiceEncryptionMode.AeadAes256GcmRtpSize); -} -var NetworkingStatusCode = /* @__PURE__ */ ((NetworkingStatusCode2) => { - NetworkingStatusCode2[NetworkingStatusCode2["OpeningWs"] = 0] = "OpeningWs"; - NetworkingStatusCode2[NetworkingStatusCode2["Identifying"] = 1] = "Identifying"; - NetworkingStatusCode2[NetworkingStatusCode2["UdpHandshaking"] = 2] = "UdpHandshaking"; - NetworkingStatusCode2[NetworkingStatusCode2["SelectingProtocol"] = 3] = "SelectingProtocol"; - NetworkingStatusCode2[NetworkingStatusCode2["Ready"] = 4] = "Ready"; - NetworkingStatusCode2[NetworkingStatusCode2["Resuming"] = 5] = "Resuming"; - NetworkingStatusCode2[NetworkingStatusCode2["Closed"] = 6] = "Closed"; - return NetworkingStatusCode2; -})(NetworkingStatusCode || {}); -var nonce = Buffer7.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 extends EventEmitter5 { - static { - __name(this, "Networking"); - } - _state; - /** - * The debug logger function, if debugging is enabled. - */ - debug; - /** - * The options used to create this Networking instance. - */ - options; - /** - * Creates a new Networking instance. - */ - constructor(connectionOptions, options) { - super(); - 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(); - } - const oldState = this._state; - this._state = newState; - this.emit("stateChange", oldState, newState); - this.debug?.(`state change: -from ${stringifyState2(oldState)} -to ${stringifyState2(newState)}`); - } - /** - * Creates a new WebSocket to a Discord Voice gateway. - * - * @param endpoint - The endpoint to connect to - * @param lastSequence - The last sequence to set for this WebSocket - */ - createWebSocket(endpoint, lastSequence) { - const ws = new VoiceWebSocket(`wss://${endpoint}?v=8`, Boolean(this.debug)); - if (lastSequence !== void 0) { - ws.sequence = lastSequence; - } - ws.on("error", this.onChildError); - ws.once("open", this.onWsOpen); - ws.on("packet", this.onWsPacket); - ws.on("binary", this.onWsBinary); - ws.once("close", this.onWsClose); - ws.on("debug", this.onWsDebug); - return ws; - } - /** - * Creates a new DAVE session for this voice connection if we can create one. - * - * @param protocolVersion - The protocol version to use - */ - createDaveSession(protocolVersion) { - if (this.options.daveEncryption === false || this.state.code !== 3 /* SelectingProtocol */ && this.state.code !== 4 /* Ready */ && this.state.code !== 5 /* Resuming */) { - return; - } - const session = new DAVESession( - protocolVersion, - this.state.connectionOptions.userId, - this.state.connectionOptions.channelId, - { - decryptionFailureTolerance: this.options.decryptionFailureTolerance - } - ); - session.on("error", this.onChildError); - session.on("debug", this.onDaveDebug); - session.on("keyPackage", this.onDaveKeyPackage); - session.on("invalidateTransition", this.onDaveInvalidateTransition); - session.reinit(); - return session; - } - /** - * Propagates errors from the children VoiceWebSocket, VoiceUDPSocket and DAVESession. - * - * @param error - The error that was emitted by a child - */ - 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. - */ - onWsOpen() { - if (this.state.code === 0 /* OpeningWs */) { - this.state.ws.sendPacket({ - op: VoiceOpcodes2.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, - max_dave_protocol_version: this.options.daveEncryption === false ? 0 : getMaxProtocolVersion(), - // [GMW FORK] Declare video capability so Discord sends RTP video - // (camera + screen-share) for all members. Derived from - // Discord-RE/Discord-video-stream which uses a video-capable - // Identify. stream = [] because this client is a pure receiver - // (does not send its own video). Gated by DISCORD_VOICE_ENABLE_VIDEO. - video: true, - streams: [] - } - }); - this.state = { - ...this.state, - code: 1 /* Identifying */ - }; - } else if (this.state.code === 5 /* Resuming */) { - this.state.ws.sendPacket({ - op: VoiceOpcodes2.Resume, - d: { - server_id: this.state.connectionOptions.serverId, - session_id: this.state.connectionOptions.sessionId, - token: this.state.connectionOptions.token, - seq_ack: this.state.ws.sequence - } - }); - } - } - /** - * Called when the WebSocket closes. Based on the reason for closing (given by the code parameter), - * the instance will either attempt to resume, or enter the closed state and emit a 'close' event - * with the close code, allowing the user to decide whether or not they would like to reconnect. - * - * @param code - The close code - */ - onWsClose({ code }) { - const canResume = code === 4015 || code < 4e3; - if (canResume && this.state.code === 4 /* Ready */) { - const lastSequence = this.state.ws.sequence; - this.state = { - ...this.state, - code: 5 /* Resuming */, - ws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence) - }; - } else if (this.state.code !== 6 /* Closed */) { - this.destroy(); - this.emit("close", code); - } - } - /** - * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord. - */ - onUdpClose() { - if (this.state.code === 4 /* Ready */) { - const lastSequence = this.state.ws.sequence; - this.state = { - ...this.state, - code: 5 /* Resuming */, - ws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence) - }; - } - } - /** - * Called when a packet is received on the connection's WebSocket. - * - * @param packet - The received packet - */ - onWsPacket(packet) { - if (packet.op === VoiceOpcodes2.Hello && this.state.code !== 6 /* Closed */) { - this.state.ws.setHeartbeatInterval(packet.d.heartbeat_interval); - } else if (packet.op === VoiceOpcodes2.Ready && this.state.code === 1 /* 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 !== 2 /* UdpHandshaking */) return; - this.state.ws.sendPacket({ - op: VoiceOpcodes2.SelectProtocol, - d: { - protocol: "udp", - data: { - address: localConfig.ip, - port: localConfig.port, - mode: chooseEncryptionMode(modes) - } - } - }); - this.state = { - ...this.state, - code: 3 /* SelectingProtocol */ - }; - }).catch((error) => this.emit("error", error)); - this.state = { - ...this.state, - code: 2 /* UdpHandshaking */, - udp, - connectionData: { - ssrc, - connectedClients: /* @__PURE__ */ new Set() - } - }; - } else if (packet.op === VoiceOpcodes2.SessionDescription && this.state.code === 3 /* SelectingProtocol */) { - const { mode: encryptionMode, secret_key: secretKey, dave_protocol_version: daveProtocolVersion } = packet.d; - this.state = { - ...this.state, - code: 4 /* Ready */, - dave: this.createDaveSession(daveProtocolVersion), - connectionData: { - ...this.state.connectionData, - encryptionMode, - secretKey: new Uint8Array(secretKey), - sequence: randomNBit(16), - timestamp: randomNBit(32), - nonce: 0, - nonceBuffer: encryptionMode === "aead_aes256_gcm_rtpsize" ? Buffer7.alloc(12) : Buffer7.alloc(24), - speaking: false, - packetsPlayed: 0 - } - }; - } else if (packet.op === VoiceOpcodes2.Resumed && this.state.code === 5 /* Resuming */) { - this.state = { - ...this.state, - code: 4 /* Ready */ - }; - this.state.connectionData.speaking = false; - } else if ((packet.op === VoiceOpcodes2.ClientsConnect || packet.op === VoiceOpcodes2.ClientDisconnect) && (this.state.code === 4 /* Ready */ || this.state.code === 2 /* UdpHandshaking */ || this.state.code === 3 /* SelectingProtocol */ || this.state.code === 5 /* Resuming */)) { - const { connectionData } = this.state; - if (packet.op === VoiceOpcodes2.ClientsConnect) - for (const id of packet.d.user_ids) connectionData.connectedClients.add(id); - else { - connectionData.connectedClients.delete(packet.d.user_id); - } - } else if ((this.state.code === 4 /* Ready */ || this.state.code === 5 /* Resuming */) && this.state.dave) { - if (packet.op === VoiceOpcodes2.DavePrepareTransition) { - const sendReady = this.state.dave.prepareTransition(packet.d); - if (sendReady) - this.state.ws.sendPacket({ - op: VoiceOpcodes2.DaveTransitionReady, - d: { transition_id: packet.d.transition_id } - }); - if (packet.d.transition_id === 0) { - this.emit("transitioned", 0); - } - } else if (packet.op === VoiceOpcodes2.DaveExecuteTransition) { - const transitioned = this.state.dave.executeTransition(packet.d.transition_id); - if (transitioned) this.emit("transitioned", packet.d.transition_id); - } else if (packet.op === VoiceOpcodes2.DavePrepareEpoch) this.state.dave.prepareEpoch(packet.d); - } - } - /** - * Called when a binary message is received on the connection's WebSocket. - * - * @param message - The received message - */ - onWsBinary(message) { - if (this.state.code === 4 /* Ready */ && this.state.dave) { - if (message.op === VoiceOpcodes2.DaveMlsExternalSender) { - this.state.dave.setExternalSender(message.payload); - } else if (message.op === VoiceOpcodes2.DaveMlsProposals) { - const payload = this.state.dave.processProposals(message.payload, this.state.connectionData.connectedClients); - if (payload) this.state.ws.sendBinaryMessage(VoiceOpcodes2.DaveMlsCommitWelcome, payload); - } else if (message.op === VoiceOpcodes2.DaveMlsAnnounceCommitTransition) { - const { transitionId, success } = this.state.dave.processCommit(message.payload); - if (success) { - if (transitionId === 0) this.emit("transitioned", transitionId); - else - this.state.ws.sendPacket({ - op: VoiceOpcodes2.DaveTransitionReady, - d: { transition_id: transitionId } - }); - } - } else if (message.op === VoiceOpcodes2.DaveMlsWelcome) { - const { transitionId, success } = this.state.dave.processWelcome(message.payload); - if (success) { - if (transitionId === 0) this.emit("transitioned", transitionId); - else - this.state.ws.sendPacket({ - op: VoiceOpcodes2.DaveTransitionReady, - d: { transition_id: transitionId } - }); - } - } - } - } - /** - * Called when a new key package is ready to be sent to the voice server. - * - * @param keyPackage - The new key package - */ - onDaveKeyPackage(keyPackage) { - if (this.state.code === 3 /* SelectingProtocol */ || this.state.code === 4 /* Ready */) - this.state.ws.sendBinaryMessage(VoiceOpcodes2.DaveMlsKeyPackage, keyPackage); - } - /** - * Called when the DAVE session wants to invalidate their transition and re-initialize. - * - * @param transitionId - The transition to invalidate - */ - onDaveInvalidateTransition(transitionId) { - if (this.state.code === 3 /* SelectingProtocol */ || this.state.code === 4 /* Ready */) - this.state.ws.sendPacket({ - op: VoiceOpcodes2.DaveMlsInvalidCommitWelcome, - d: { transition_id: transitionId } - }); - } - /** - * Propagates debug messages from the child WebSocket. - * - * @param message - The emitted debug message - */ - onWsDebug(message) { - this.debug?.(`[WS] ${message}`); - } - /** - * Propagates debug messages from the child UDPSocket. - * - * @param message - The emitted debug message - */ - onUdpDebug(message) { - this.debug?.(`[UDP] ${message}`); - } - /** - * Propagates debug messages from the child DAVESession. - * - * @param message - The emitted debug message - */ - onDaveDebug(message) { - this.debug?.(`[DAVE] ${message}`); - } - /** - * Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it. - * It will be stored within the instance, and can be played by dispatchAudio() - * - * @remarks - * Calling this method while there is already a prepared audio packet that has not yet been dispatched - * will overwrite the existing audio packet. This should be avoided. - * @param opusPacket - The Opus packet to encrypt - * @returns The audio packet that was prepared - */ - prepareAudioPacket(opusPacket) { - const state = this.state; - if (state.code !== 4 /* Ready */) return; - state.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData, state.dave); - return state.preparedPacket; - } - /** - * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet - * is consumed and cannot be dispatched again. - */ - dispatchAudio() { - const state = this.state; - if (state.code !== 4 /* Ready */) return false; - if (state.preparedPacket !== void 0) { - this.playAudioPacket(state.preparedPacket); - state.preparedPacket = void 0; - return true; - } - return false; - } - /** - * Plays an audio packet, updating timing metadata used for playback. - * - * @param audioPacket - The audio packet to play - */ - playAudioPacket(audioPacket) { - const state = this.state; - if (state.code !== 4 /* 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); - } - /** - * Sends a packet to the voice gateway indicating that the client has start/stopped sending - * audio. - * - * @param speaking - Whether or not the client should be shown as speaking - */ - setSpeaking(speaking) { - const state = this.state; - if (state.code !== 4 /* Ready */) return; - if (state.connectionData.speaking === speaking) return; - state.connectionData.speaking = speaking; - state.ws.sendPacket({ - op: VoiceOpcodes2.Speaking, - d: { - speaking: speaking ? 1 : 0, - delay: 0, - ssrc: state.connectionData.ssrc - } - }); - } - /** - * Creates a new audio packet from an Opus packet. This involves encrypting the packet, - * then prepending a header that includes metadata. - * - * @param opusPacket - The Opus packet to prepare - * @param connectionData - The current connection data of the instance - * @param daveSession - The DAVE session to use for encryption - */ - createAudioPacket(opusPacket, connectionData, daveSession) { - const rtpHeader = Buffer7.alloc(12); - rtpHeader[0] = 128; - rtpHeader[1] = RTP_OPUS_PAYLOAD_TYPE; - const { sequence, timestamp, ssrc } = connectionData; - rtpHeader.writeUIntBE(sequence, 2, 2); - rtpHeader.writeUIntBE(timestamp, 4, 4); - rtpHeader.writeUIntBE(ssrc, 8, 4); - rtpHeader.copy(nonce, 0, 0, 12); - return Buffer7.concat([rtpHeader, ...this.encryptOpusPacket(opusPacket, connectionData, rtpHeader, daveSession)]); - } - /** - * Encrypts an Opus packet using the format agreed upon by the instance and Discord. - * - * @param opusPacket - The Opus packet to encrypt - * @param connectionData - The current connection data of the instance - * @param daveSession - The DAVE session to use for encryption - */ - encryptOpusPacket(opusPacket, connectionData, additionalData, daveSession) { - const { secretKey, encryptionMode } = connectionData; - const packet = daveSession?.encrypt(opusPacket) ?? opusPacket; - connectionData.nonce++; - if (connectionData.nonce > MAX_NONCE_SIZE) connectionData.nonce = 0; - connectionData.nonceBuffer.writeUInt32BE(connectionData.nonce, 0); - const noncePadding = connectionData.nonceBuffer.subarray(0, 4); - let encrypted; - switch (encryptionMode) { - case "aead_aes256_gcm_rtpsize": { - const cipher = crypto.createCipheriv("aes-256-gcm", secretKey, connectionData.nonceBuffer); - cipher.setAAD(additionalData); - encrypted = Buffer7.concat([cipher.update(packet), cipher.final(), cipher.getAuthTag()]); - return [encrypted, noncePadding]; - } - case "aead_xchacha20_poly1305_rtpsize": { - encrypted = methods.crypto_aead_xchacha20poly1305_ietf_encrypt( - packet, - additionalData, - connectionData.nonceBuffer, - secretKey - ); - return [encrypted, noncePadding]; - } - default: { - throw new RangeError(`Unsupported encryption method: ${encryptionMode}`); - } - } - } -}; - -// src/receive/VoiceReceiver.ts -import { Buffer as Buffer8 } from "buffer"; -import crypto2 from "crypto"; -import { VoiceOpcodes as VoiceOpcodes3 } from "discord-api-types/voice/v8"; - -// src/receive/AudioReceiveStream.ts -import process from "process"; -import { Readable } from "stream"; -var EndBehaviorType = /* @__PURE__ */ ((EndBehaviorType2) => { - EndBehaviorType2[EndBehaviorType2["Manual"] = 0] = "Manual"; - EndBehaviorType2[EndBehaviorType2["AfterSilence"] = 1] = "AfterSilence"; - EndBehaviorType2[EndBehaviorType2["AfterInactivity"] = 2] = "AfterInactivity"; - return EndBehaviorType2; -})(EndBehaviorType || {}); -function createDefaultAudioReceiveStreamOptions() { - return { - end: { - behavior: 0 /* Manual */ - } - }; -} -__name(createDefaultAudioReceiveStreamOptions, "createDefaultAudioReceiveStreamOptions"); -var AudioReceiveStream = class extends Readable { - static { - __name(this, "AudioReceiveStream"); - } - /** - * The end behavior of the receive stream. - */ - end; - endTimeout; - constructor(options) { - const { end, ...rest } = options; - super({ - ...rest, - objectMode: true - }); - this.end = end; - } - push(buffer) { - if (buffer && (this.end.behavior === 2 /* AfterInactivity */ || this.end.behavior === 1 /* AfterSilence */ && (buffer.compare(SILENCE_FRAME) !== 0 || this.endTimeout === void 0))) { - this.renewEndTimeout(this.end); - } - if (buffer === null) { - process.nextTick(() => this.destroy()); - } - return super.push(buffer); - } - renewEndTimeout(end) { - if (this.endTimeout) { - clearTimeout(this.endTimeout); - } - this.endTimeout = setTimeout(() => this.push(null), end.duration); - } - _read() { - } -}; - -// src/receive/SSRCMap.ts -import { EventEmitter as EventEmitter6 } from "events"; -var SSRCMap = class extends EventEmitter6 { - static { - __name(this, "SSRCMap"); - } - /** - * The underlying map. - */ - map; - constructor() { - super(); - this.map = /* @__PURE__ */ new Map(); - } - /** - * Updates the map with new user data - * - * @param data - The data to update with - */ - update(data) { - const existing = this.map.get(data.audioSSRC); - const newValue = { - ...this.map.get(data.audioSSRC), - ...data - }; - this.map.set(data.audioSSRC, newValue); - if (!existing) this.emit("create", newValue); - this.emit("update", existing, newValue); - } - /** - * Gets the stored voice data of a user. - * - * @param target - The target, either their user id or audio SSRC - */ - get(target) { - if (typeof target === "number") { - // [GMW FORK] Also match a video SSRC (camera/screen-share) when the - // audio SSRC key does not match. Video RTP packets carry a different - // SSRC than the audio stream, so look it up across stored values. - const direct = this.map.get(target); - if (direct) return direct; - for (const data of this.map.values()) { - if (data.videoSSRC === target) { - return data; - } - } - return void 0; - } - for (const data of this.map.values()) { - if (data.userId === target) { - return data; - } - } - return void 0; - } - /** - * Deletes the stored voice data about a user. - * - * @param target - The target of the delete operation, either their audio SSRC or user id - * @returns The data that was deleted, if any - */ - delete(target) { - if (typeof target === "number") { - const existing = this.map.get(target); - if (existing) { - this.map.delete(target); - this.emit("delete", existing); - } - return existing; - } - for (const [audioSSRC, data] of this.map.entries()) { - if (data.userId === target) { - this.map.delete(audioSSRC); - this.emit("delete", data); - return data; - } - } - return void 0; - } -}; - -// src/receive/SpeakingMap.ts -import { EventEmitter as EventEmitter7 } from "events"; -var SpeakingMap = class _SpeakingMap extends EventEmitter7 { - static { - __name(this, "SpeakingMap"); - } - /** - * The delay after a packet is received from a user until they're marked as not speaking anymore. - */ - static DELAY = 100; - /** - * The currently speaking users, mapped to the milliseconds since UNIX epoch at which they started speaking. - */ - users; - speakingTimeouts; - constructor() { - super(); - this.users = /* @__PURE__ */ new Map(); - this.speakingTimeouts = /* @__PURE__ */ new Map(); - } - onPacket(userId) { - const timeout = this.speakingTimeouts.get(userId); - if (timeout) { - clearTimeout(timeout); - } else { - this.users.set(userId, Date.now()); - this.emit("start", userId); - } - this.startTimeout(userId); - } - startTimeout(userId) { - this.speakingTimeouts.set( - userId, - setTimeout(() => { - this.emit("end", userId); - this.speakingTimeouts.delete(userId); - this.users.delete(userId); - }, _SpeakingMap.DELAY) - ); - } -}; - -// src/receive/VoiceReceiver.ts -var HEADER_EXTENSION_BYTE = Buffer8.from([190, 222]); -var UNPADDED_NONCE_LENGTH = 4; -var AUTH_TAG_LENGTH = 16; -var VoiceReceiver = class { - static { - __name(this, "VoiceReceiver"); - } - /** - * The attached connection of this receiver. - */ - voiceConnection; - /** - * Maps SSRCs to Discord user ids. - */ - ssrcMap; - /** - * The current audio subscriptions of this receiver. - */ - subscriptions; - /** - * The connection data of the receiver. - * - * @internal - */ - connectionData; - /** - * The speaking map of the receiver. - */ - speaking; - constructor(voiceConnection) { - this.voiceConnection = voiceConnection; - this.ssrcMap = new SSRCMap(); - this.speaking = new SpeakingMap(); - this.subscriptions = /* @__PURE__ */ new Map(); - this.connectionData = {}; - this.onWsPacket = this.onWsPacket.bind(this); - this.onUdpMessage = this.onUdpMessage.bind(this); - } - /** - * Called when a packet is received on the attached connection's WebSocket. - * - * @param packet - The received packet - * @internal - */ - onWsPacket(packet) { - if (packet.op === VoiceOpcodes3.ClientDisconnect) { - this.ssrcMap.delete(packet.d.user_id); - } else if (packet.op === VoiceOpcodes3.Speaking) { - // [GMW FORK] Also register the user's video SSRC when present so the - // UDP receive path can decrypt/demux their H264 (camera/screen-share). - // op 12 (Speaking) carries `streams: [{ type, rid, quality, active, - // ssrc, rtx_ssrc, video_ssrc }]` when the member enables camera/ - // screen-share. We map the video stream SSRC -> userId alongside the - // audio SSRC. - let videoSSRC; - const streams = packet.d && Array.isArray(packet.d.streams) ? packet.d.streams : []; - for (const st of streams) { - const s = st && (st.ssrc ?? st.video_ssrc); - if (typeof s === "number" && s !== 0) { - videoSSRC = s; - break; - } - } - const update = { userId: packet.d.user_id, audioSSRC: packet.d.ssrc }; - if (typeof videoSSRC === "number") update.videoSSRC = videoSSRC; - this.ssrcMap.update(update); - } - } - decrypt(buffer, mode, nonce2, secretKey) { - buffer.copy(nonce2, 0, buffer.length - UNPADDED_NONCE_LENGTH); - let headerSize = 12; - const first = buffer.readUint8(); - if (first >> 4 & 1) headerSize += 4; - const header = buffer.subarray(0, headerSize); - const encrypted = buffer.subarray(headerSize, buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH); - const authTag = buffer.subarray( - buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH, - buffer.length - UNPADDED_NONCE_LENGTH - ); - switch (mode) { - case "aead_aes256_gcm_rtpsize": { - const decipheriv = crypto2.createDecipheriv("aes-256-gcm", secretKey, nonce2); - decipheriv.setAAD(header); - decipheriv.setAuthTag(authTag); - return Buffer8.concat([decipheriv.update(encrypted), decipheriv.final()]); - } - case "aead_xchacha20_poly1305_rtpsize": { - return Buffer8.from( - methods.crypto_aead_xchacha20poly1305_ietf_decrypt( - Buffer8.concat([encrypted, authTag]), - header, - nonce2, - secretKey - ) - ); - } - default: { - throw new RangeError(`Unsupported decryption method: ${mode}`); - } - } - } - /** - * Parses an audio packet, decrypting it to yield an Opus packet. - * - * @param buffer - The buffer to parse - * @param mode - The encryption mode - * @param nonce - The nonce buffer used by the connection for encryption - * @param secretKey - The secret key used by the connection for encryption - * @param userId - The user id that sent the packet - * @returns The parsed Opus packet - */ - parsePacket(buffer, mode, nonce2, secretKey, userId) { - let packet = this.decrypt(buffer, mode, nonce2, secretKey); - if (!packet) throw new Error("Failed to parse packet"); - const hasPadding = buffer[0] && Boolean(buffer[0] & 32); - if (hasPadding) { - const paddingAmount = packet[packet.length - 1]; - if (paddingAmount < packet.length) { - packet = packet.subarray(0, packet.length - paddingAmount); - } - } - if (buffer.subarray(12, 14).compare(HEADER_EXTENSION_BYTE) === 0) { - const headerExtensionLength = buffer.subarray(14).readUInt16BE(); - packet = packet.subarray(4 * headerExtensionLength); - } - if (this.voiceConnection.state.status === "ready" /* Ready */ && (this.voiceConnection.state.networking.state.code === 4 /* Ready */ || this.voiceConnection.state.networking.state.code === 5 /* Resuming */)) { - const daveSession = this.voiceConnection.state.networking.state.dave; - if (daveSession) packet = daveSession.decrypt(packet, userId); - } - return packet; - } - /** - * Called when the UDP socket of the attached connection receives a message. - * - * @param msg - The received message - * @internal - */ - onUdpMessage(msg) { - if (msg.length <= 8) return; - const ssrc = msg.readUInt32BE(8); - const userData = this.ssrcMap.get(ssrc); - if (!userData) return; - this.speaking.onPacket(userData.userId); - const stream = this.subscriptions.get(userData.userId); - if (!stream) return; - if (this.connectionData.encryptionMode && this.connectionData.nonceBuffer && this.connectionData.secretKey) { - if ((msg[1] & 127) !== RTP_OPUS_PAYLOAD_TYPE) return; - const rtpVersion = msg[0] >> 6; - if (rtpVersion !== 2) return; - try { - const packet = this.parsePacket( - msg, - this.connectionData.encryptionMode, - this.connectionData.nonceBuffer, - this.connectionData.secretKey, - userData.userId - ); - if (packet) stream.push(packet); - } catch (error) { - stream.destroy(error); - } - } - } - /** - * Creates a subscription for the given user id. - * - * @param target - The id of the user to subscribe to - * @returns A readable stream of Opus packets received from the target - */ - subscribe(userId, options) { - const existing = this.subscriptions.get(userId); - if (existing) return existing; - const stream = new AudioReceiveStream({ - ...createDefaultAudioReceiveStreamOptions(), - ...options - }); - stream.once("close", () => this.subscriptions.delete(userId)); - this.subscriptions.set(userId, stream); - return stream; - } -}; - -// src/VoiceConnection.ts -var VoiceConnectionStatus = /* @__PURE__ */ ((VoiceConnectionStatus2) => { - VoiceConnectionStatus2["Connecting"] = "connecting"; - VoiceConnectionStatus2["Destroyed"] = "destroyed"; - VoiceConnectionStatus2["Disconnected"] = "disconnected"; - VoiceConnectionStatus2["Ready"] = "ready"; - VoiceConnectionStatus2["Signalling"] = "signalling"; - return VoiceConnectionStatus2; -})(VoiceConnectionStatus || {}); -var VoiceConnectionDisconnectReason = /* @__PURE__ */ ((VoiceConnectionDisconnectReason2) => { - VoiceConnectionDisconnectReason2[VoiceConnectionDisconnectReason2["WebSocketClose"] = 0] = "WebSocketClose"; - VoiceConnectionDisconnectReason2[VoiceConnectionDisconnectReason2["AdapterUnavailable"] = 1] = "AdapterUnavailable"; - VoiceConnectionDisconnectReason2[VoiceConnectionDisconnectReason2["EndpointRemoved"] = 2] = "EndpointRemoved"; - VoiceConnectionDisconnectReason2[VoiceConnectionDisconnectReason2["Manual"] = 3] = "Manual"; - return VoiceConnectionDisconnectReason2; -})(VoiceConnectionDisconnectReason || {}); -var VoiceConnection = class extends EventEmitter8 { - static { - __name(this, "VoiceConnection"); - } - /** - * The number of consecutive rejoin attempts. Initially 0, and increments for each rejoin. - * When a connection is successfully established, it resets to 0. - */ - rejoinAttempts; - /** - * The state of the voice connection. - */ - _state; - /** - * A configuration storing all the data needed to reconnect to a Guild's voice server. - * - * @internal - */ - joinConfig; - /** - * The two packets needed to successfully establish a voice connection. They are received - * from the main Discord gateway after signalling to change the voice state. - */ - packets; - /** - * The receiver of this voice connection. You should join the voice channel with `selfDeaf` set - * to false for this feature to work properly. - */ - receiver; - /** - * The debug logger function, if debugging is enabled. - */ - debug; - /** - * The options used to create this voice connection. - */ - options; - /** - * Creates a new voice connection. - * - * @param joinConfig - The data required to establish the voice connection - * @param options - The options used to create this voice connection - */ - constructor(joinConfig, options) { - super(); - this.debug = options.debug ? (message) => this.emit("debug", message) : null; - this.rejoinAttempts = 0; - this.receiver = new VoiceReceiver(this); - this.onNetworkingClose = this.onNetworkingClose.bind(this); - this.onNetworkingStateChange = this.onNetworkingStateChange.bind(this); - this.onNetworkingError = this.onNetworkingError.bind(this); - this.onNetworkingDebug = this.onNetworkingDebug.bind(this); - this.onNetworkingTransitioned = this.onNetworkingTransitioned.bind(this); - const adapter = options.adapterCreator({ - onVoiceServerUpdate: /* @__PURE__ */ __name((data) => this.addServerPacket(data), "onVoiceServerUpdate"), - onVoiceStateUpdate: /* @__PURE__ */ __name((data) => this.addStatePacket(data), "onVoiceStateUpdate"), - destroy: /* @__PURE__ */ __name(() => this.destroy(false), "destroy") - }); - this._state = { status: "signalling" /* Signalling */, adapter }; - this.packets = { - server: void 0, - state: void 0 - }; - this.joinConfig = joinConfig; - this.options = options; - } - /** - * The current state of the voice connection. - * - * @remarks - * The setter will perform clean-up operations where necessary. - */ - get state() { - return this._state; - } - set state(newState) { - const oldState = this._state; - const oldNetworking = Reflect.get(oldState, "networking"); - const newNetworking = Reflect.get(newState, "networking"); - const oldSubscription = Reflect.get(oldState, "subscription"); - const newSubscription = Reflect.get(newState, "subscription"); - if (oldNetworking !== newNetworking) { - if (oldNetworking) { - oldNetworking.on("error", noop); - oldNetworking.off("debug", this.onNetworkingDebug); - oldNetworking.off("error", this.onNetworkingError); - oldNetworking.off("close", this.onNetworkingClose); - oldNetworking.off("stateChange", this.onNetworkingStateChange); - oldNetworking.off("transitioned", this.onNetworkingTransitioned); - oldNetworking.destroy(); - } - if (newNetworking) this.updateReceiveBindings(newNetworking.state, oldNetworking?.state); - } - if (newState.status === "ready" /* Ready */) { - this.rejoinAttempts = 0; - } else if (newState.status === "destroyed" /* Destroyed */) { - for (const stream of this.receiver.subscriptions.values()) { - if (!stream.destroyed) stream.destroy(); - } - } - if (oldState.status !== "destroyed" /* Destroyed */ && newState.status === "destroyed" /* Destroyed */) { - oldState.adapter.destroy(); - } - this._state = newState; - if (oldSubscription && oldSubscription !== newSubscription) { - oldSubscription.unsubscribe(); - } - this.emit("stateChange", oldState, newState); - if (oldState.status !== newState.status) { - this.emit(newState.status, oldState, newState); - } - } - /** - * Registers a `VOICE_SERVER_UPDATE` packet to the voice connection. This will cause it to reconnect using the - * new data provided in the packet. - * - * @param packet - The received `VOICE_SERVER_UPDATE` packet - */ - addServerPacket(packet) { - this.packets.server = packet; - if (packet.endpoint) { - this.configureNetworking(); - } else if (this.state.status !== "destroyed" /* Destroyed */) { - this.state = { - ...this.state, - status: "disconnected" /* Disconnected */, - reason: 2 /* EndpointRemoved */ - }; - } - } - /** - * Registers a `VOICE_STATE_UPDATE` packet to the voice connection. Most importantly, it stores the id of the - * channel that the client is connected to. - * - * @param packet - The received `VOICE_STATE_UPDATE` packet - */ - addStatePacket(packet) { - this.packets.state = packet; - if (packet.self_deaf !== void 0) this.joinConfig.selfDeaf = packet.self_deaf; - if (packet.self_mute !== void 0) this.joinConfig.selfMute = packet.self_mute; - if (packet.channel_id) this.joinConfig.channelId = packet.channel_id; - } - /** - * Called when the networking state changes, and the new ws/udp packet/message handlers need to be rebound - * to the new instances. - * - * @param newState - The new networking state - * @param oldState - The old networking state, if there is one - */ - updateReceiveBindings(newState, oldState) { - const oldWs = Reflect.get(oldState ?? {}, "ws"); - const newWs = Reflect.get(newState, "ws"); - const oldUdp = Reflect.get(oldState ?? {}, "udp"); - const newUdp = Reflect.get(newState, "udp"); - if (oldWs !== newWs) { - oldWs?.off("packet", this.receiver.onWsPacket); - newWs?.on("packet", this.receiver.onWsPacket); - } - if (oldUdp !== newUdp) { - oldUdp?.off("message", this.receiver.onUdpMessage); - newUdp?.on("message", this.receiver.onUdpMessage); - } - this.receiver.connectionData = Reflect.get(newState, "connectionData") ?? {}; - } - /** - * Attempts to configure a networking instance for this voice connection using the received packets. - * Both packets are required, and any existing networking instance will be destroyed. - * - * @remarks - * This is called when the voice server of the connection changes, e.g. if the bot is moved into a - * different channel in the same guild but has a different voice server. In this instance, the connection - * needs to be re-established to the new voice server. - * - * The connection will transition to the Connecting state when this is called. - */ - configureNetworking() { - const { server, state } = this.packets; - if (!server || !state || this.state.status === "destroyed" /* Destroyed */ || !server.endpoint) return; - const networking = new Networking( - { - endpoint: server.endpoint, - serverId: server.guild_id, - token: server.token, - sessionId: state.session_id, - userId: state.user_id, - channelId: state.channel_id - }, - { - debug: Boolean(this.debug), - daveEncryption: this.options.daveEncryption ?? true, - decryptionFailureTolerance: this.options.decryptionFailureTolerance - } - ); - networking.once("close", this.onNetworkingClose); - networking.on("stateChange", this.onNetworkingStateChange); - networking.on("error", this.onNetworkingError); - networking.on("debug", this.onNetworkingDebug); - networking.on("transitioned", this.onNetworkingTransitioned); - this.state = { - ...this.state, - status: "connecting" /* Connecting */, - networking - }; - } - /** - * Called when the networking instance for this connection closes. If the close code is 4014 (do not reconnect), - * the voice connection will transition to the Disconnected state which will store the close code. You can - * decide whether or not to reconnect when this occurs by listening for the state change and calling reconnect(). - * - * @remarks - * If the close code was anything other than 4014, it is likely that the closing was not intended, and so the - * VoiceConnection will signal to Discord that it would like to rejoin the channel. This automatically attempts - * to re-establish the connection. This would be seen as a transition from the Ready state to the Signalling state. - * @param code - The close code - */ - onNetworkingClose(code) { - if (this.state.status === "destroyed" /* Destroyed */) return; - if (code === 4014) { - this.state = { - ...this.state, - status: "disconnected" /* Disconnected */, - reason: 0 /* WebSocketClose */, - closeCode: code - }; - } else { - this.state = { - ...this.state, - status: "signalling" /* Signalling */ - }; - this.rejoinAttempts++; - if (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) { - this.state = { - ...this.state, - status: "disconnected" /* Disconnected */, - reason: 1 /* AdapterUnavailable */ - }; - } - } - } - /** - * Called when the state of the networking instance changes. This is used to derive the state of the voice connection. - * - * @param oldState - The previous state - * @param newState - The new state - */ - onNetworkingStateChange(oldState, newState) { - this.updateReceiveBindings(newState, oldState); - if (oldState.code === newState.code) return; - if (this.state.status !== "connecting" /* Connecting */ && this.state.status !== "ready" /* Ready */) - return; - if (newState.code === 4 /* Ready */) { - this.state = { - ...this.state, - status: "ready" /* Ready */ - }; - } else if (newState.code !== 6 /* Closed */) { - this.state = { - ...this.state, - status: "connecting" /* Connecting */ - }; - } - } - /** - * Propagates errors from the underlying network instance. - * - * @param error - The error to propagate - */ - onNetworkingError(error) { - this.emit("error", error); - } - /** - * Propagates debug messages from the underlying network instance. - * - * @param message - The debug message to propagate - */ - onNetworkingDebug(message) { - this.debug?.(`[NW] ${message}`); - } - /** - * Propagates transitions from the underlying network instance. - * - * @param transitionId - The transition id - */ - onNetworkingTransitioned(transitionId) { - this.emit("transitioned", transitionId); - } - /** - * Prepares an audio packet for dispatch. - * - * @param buffer - The Opus packet to prepare - */ - prepareAudioPacket(buffer) { - const state = this.state; - if (state.status !== "ready" /* Ready */) return; - return state.networking.prepareAudioPacket(buffer); - } - /** - * Dispatches the previously prepared audio packet (if any) - */ - dispatchAudio() { - const state = this.state; - if (state.status !== "ready" /* Ready */) return; - return state.networking.dispatchAudio(); - } - /** - * Prepares an audio packet and dispatches it immediately. - * - * @param buffer - The Opus packet to play - */ - playOpusPacket(buffer) { - const state = this.state; - if (state.status !== "ready" /* Ready */) return; - state.networking.prepareAudioPacket(buffer); - return state.networking.dispatchAudio(); - } - /** - * Destroys the VoiceConnection, preventing it from connecting to voice again. - * This method should be called when you no longer require the VoiceConnection to - * prevent memory leaks. - * - * @param adapterAvailable - Whether the adapter can be used - */ - destroy(adapterAvailable = true) { - if (this.state.status === "destroyed" /* Destroyed */) { - throw new Error("Cannot destroy VoiceConnection - it has already been destroyed"); - } - if (getVoiceConnection(this.joinConfig.guildId, this.joinConfig.group) === this) { - untrackVoiceConnection(this); - } - if (adapterAvailable) { - this.state.adapter.sendPayload(createJoinVoiceChannelPayload({ ...this.joinConfig, channelId: null })); - } - this.state = { - status: "destroyed" /* Destroyed */ - }; - } - /** - * Disconnects the VoiceConnection, allowing the possibility of rejoining later on. - * - * @returns `true` if the connection was successfully disconnected - */ - disconnect() { - if (this.state.status === "destroyed" /* Destroyed */ || this.state.status === "signalling" /* Signalling */) { - return false; - } - this.joinConfig.channelId = null; - if (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) { - this.state = { - adapter: this.state.adapter, - subscription: this.state.subscription, - status: "disconnected" /* Disconnected */, - reason: 1 /* AdapterUnavailable */ - }; - return false; - } - this.state = { - adapter: this.state.adapter, - reason: 3 /* Manual */, - status: "disconnected" /* Disconnected */ - }; - return true; - } - /** - * Attempts to rejoin (better explanation soon:tm:) - * - * @remarks - * Calling this method successfully will automatically increment the `rejoinAttempts` counter, - * which you can use to inform whether or not you'd like to keep attempting to reconnect your - * voice connection. - * - * A state transition from Disconnected to Signalling will be observed when this is called. - */ - rejoin(joinConfig) { - if (this.state.status === "destroyed" /* Destroyed */) { - return false; - } - const notReady = this.state.status !== "ready" /* Ready */; - if (notReady) this.rejoinAttempts++; - Object.assign(this.joinConfig, joinConfig); - if (this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) { - if (notReady) { - this.state = { - ...this.state, - status: "signalling" /* Signalling */ - }; - } - return true; - } - this.state = { - adapter: this.state.adapter, - subscription: this.state.subscription, - status: "disconnected" /* Disconnected */, - reason: 1 /* AdapterUnavailable */ - }; - return false; - } - /** - * Updates the speaking status of the voice connection. This is used when audio players are done playing audio, - * and need to signal that the connection is no longer playing audio. - * - * @param enabled - Whether or not to show as speaking - */ - setSpeaking(enabled) { - if (this.state.status !== "ready" /* Ready */) return false; - return this.state.networking.setSpeaking(enabled); - } - /** - * Subscribes to an audio player, allowing the player to play audio on this voice connection. - * - * @param player - The audio player to subscribe to - * @returns The created subscription - */ - subscribe(player) { - if (this.state.status === "destroyed" /* Destroyed */) return; - const subscription = player["subscribe"](this); - this.state = { - ...this.state, - subscription - }; - return subscription; - } - /** - * The latest ping (in milliseconds) for the WebSocket connection and audio playback for this voice - * connection, if this data is available. - * - * @remarks - * For this data to be available, the VoiceConnection must be in the Ready state, and its underlying - * WebSocket connection and UDP socket must have had at least one ping-pong exchange. - */ - get ping() { - if (this.state.status === "ready" /* Ready */ && this.state.networking.state.code === 4 /* Ready */) { - return { - ws: this.state.networking.state.ws.ping, - udp: this.state.networking.state.udp.ping - }; - } - return { - ws: void 0, - udp: void 0 - }; - } - /** - * The current voice privacy code of the encrypted session. - * - * @remarks - * For this data to be available, the VoiceConnection must be in the Ready state, - * and the connection would have to be end-to-end encrypted. - */ - get voicePrivacyCode() { - if (this.state.status === "ready" /* Ready */ && this.state.networking.state.code === 4 /* Ready */) { - return this.state.networking.state.dave?.voicePrivacyCode ?? void 0; - } - return void 0; - } - /** - * Gets the verification code for a user in the session. - * - * @throws Will throw if end-to-end encryption is not on or if the user id provided is not in the session. - */ - async getVerificationCode(userId) { - if (this.state.status === "ready" /* Ready */ && this.state.networking.state.code === 4 /* Ready */ && this.state.networking.state.dave) { - return this.state.networking.state.dave.getVerificationCode(userId); - } - throw new Error("Session not available"); - } - /** - * Called when a subscription of this voice connection to an audio player is removed. - * - * @param subscription - The removed subscription - */ - onSubscriptionRemoved(subscription) { - if (this.state.status !== "destroyed" /* Destroyed */ && this.state.subscription === subscription) { - this.state = { - ...this.state, - subscription: void 0 - }; - } - } -}; -function createVoiceConnection(joinConfig, options) { - const payload = createJoinVoiceChannelPayload(joinConfig); - const existing = getVoiceConnection(joinConfig.guildId, joinConfig.group); - if (existing && existing.state.status !== "destroyed" /* Destroyed */) { - if (existing.state.status === "disconnected" /* Disconnected */) { - existing.rejoin({ - channelId: joinConfig.channelId, - selfDeaf: joinConfig.selfDeaf, - selfMute: joinConfig.selfMute - }); - } else if (!existing.state.adapter.sendPayload(payload)) { - existing.state = { - ...existing.state, - status: "disconnected" /* Disconnected */, - reason: 1 /* AdapterUnavailable */ - }; - } - return existing; - } - const voiceConnection = new VoiceConnection(joinConfig, options); - trackVoiceConnection(voiceConnection); - if (voiceConnection.state.status !== "destroyed" /* Destroyed */ && !voiceConnection.state.adapter.sendPayload(payload)) { - voiceConnection.state = { - ...voiceConnection.state, - status: "disconnected" /* Disconnected */, - reason: 1 /* AdapterUnavailable */ - }; - } - return voiceConnection; -} -__name(createVoiceConnection, "createVoiceConnection"); - -// src/joinVoiceChannel.ts -function joinVoiceChannel(options) { - const joinConfig = { - selfDeaf: true, - selfMute: false, - group: "default", - ...options - }; - return createVoiceConnection(joinConfig, { - adapterCreator: options.adapterCreator, - debug: options.debug, - daveEncryption: options.daveEncryption, - decryptionFailureTolerance: options.decryptionFailureTolerance - }); -} -__name(joinVoiceChannel, "joinVoiceChannel"); - -// src/audio/AudioResource.ts -import { pipeline } from "stream"; -import prism2 from "prism-media"; - -// src/audio/TransformerGraph.ts -import prism from "prism-media"; -var FFMPEG_PCM_ARGUMENTS = ["-analyzeduration", "0", "-loglevel", "0", "-f", "s16le", "-ar", "48000", "-ac", "2"]; -var FFMPEG_OPUS_ARGUMENTS = [ - "-analyzeduration", - "0", - "-loglevel", - "0", - "-acodec", - "libopus", - "-f", - "opus", - "-ar", - "48000", - "-ac", - "2" -]; -var StreamType = /* @__PURE__ */ ((StreamType2) => { - StreamType2["Arbitrary"] = "arbitrary"; - StreamType2["OggOpus"] = "ogg/opus"; - StreamType2["Opus"] = "opus"; - StreamType2["Raw"] = "raw"; - StreamType2["WebmOpus"] = "webm/opus"; - return StreamType2; -})(StreamType || {}); -var TransformerType = /* @__PURE__ */ ((TransformerType2) => { - TransformerType2["FFmpegOgg"] = "ffmpeg ogg"; - TransformerType2["FFmpegPCM"] = "ffmpeg pcm"; - TransformerType2["InlineVolume"] = "volume transformer"; - TransformerType2["OggOpusDemuxer"] = "ogg/opus demuxer"; - TransformerType2["OpusDecoder"] = "opus decoder"; - TransformerType2["OpusEncoder"] = "opus encoder"; - TransformerType2["WebmOpusDemuxer"] = "webm/opus demuxer"; - return TransformerType2; -})(TransformerType || {}); -var Node = class { - static { - __name(this, "Node"); - } - /** - * The outbound edges from this node. - */ - edges = []; - /** - * The type of stream for this node. - */ - type; - constructor(type) { - this.type = type; - } - /** - * Creates an outbound edge from this node. - * - * @param edge - The edge to create - */ - addEdge(edge) { - this.edges.push({ ...edge, from: this }); - } -}; -var NODES = null; -function getNode(type) { - const node = (NODES ??= initializeNodes()).get(type); - if (!node) throw new Error(`Node type '${type}' does not exist!`); - return node; -} -__name(getNode, "getNode"); -function canEnableFFmpegOptimizations() { - try { - return prism.FFmpeg.getInfo().output.includes("--enable-libopus"); - } catch { - } - return false; -} -__name(canEnableFFmpegOptimizations, "canEnableFFmpegOptimizations"); -function initializeNodes() { - const nodes = /* @__PURE__ */ new Map(); - for (const streamType of Object.values(StreamType)) { - nodes.set(streamType, new Node(streamType)); - } - nodes.get("raw" /* Raw */).addEdge({ - type: "opus encoder" /* OpusEncoder */, - to: nodes.get("opus" /* Opus */), - cost: 1.5, - transformer: /* @__PURE__ */ __name(() => new prism.opus.Encoder({ rate: 48e3, channels: 2, frameSize: 960 }), "transformer") - }); - nodes.get("opus" /* Opus */).addEdge({ - type: "opus decoder" /* OpusDecoder */, - to: nodes.get("raw" /* Raw */), - cost: 1.5, - transformer: /* @__PURE__ */ __name(() => new prism.opus.Decoder({ rate: 48e3, channels: 2, frameSize: 960 }), "transformer") - }); - nodes.get("ogg/opus" /* OggOpus */).addEdge({ - type: "ogg/opus demuxer" /* OggOpusDemuxer */, - to: nodes.get("opus" /* Opus */), - cost: 1, - transformer: /* @__PURE__ */ __name(() => new prism.opus.OggDemuxer(), "transformer") - }); - nodes.get("webm/opus" /* WebmOpus */).addEdge({ - type: "webm/opus demuxer" /* WebmOpusDemuxer */, - to: nodes.get("opus" /* Opus */), - cost: 1, - transformer: /* @__PURE__ */ __name(() => new prism.opus.WebmDemuxer(), "transformer") - }); - const FFMPEG_PCM_EDGE = { - type: "ffmpeg pcm" /* FFmpegPCM */, - to: nodes.get("raw" /* Raw */), - cost: 2, - transformer: /* @__PURE__ */ __name((input) => new prism.FFmpeg({ - args: ["-i", typeof input === "string" ? input : "-", ...FFMPEG_PCM_ARGUMENTS] - }), "transformer") - }; - nodes.get("arbitrary" /* Arbitrary */).addEdge(FFMPEG_PCM_EDGE); - nodes.get("ogg/opus" /* OggOpus */).addEdge(FFMPEG_PCM_EDGE); - nodes.get("webm/opus" /* WebmOpus */).addEdge(FFMPEG_PCM_EDGE); - nodes.get("raw" /* Raw */).addEdge({ - type: "volume transformer" /* InlineVolume */, - to: nodes.get("raw" /* Raw */), - cost: 0.5, - transformer: /* @__PURE__ */ __name(() => new prism.VolumeTransformer({ type: "s16le" }), "transformer") - }); - if (canEnableFFmpegOptimizations()) { - const FFMPEG_OGG_EDGE = { - type: "ffmpeg ogg" /* FFmpegOgg */, - to: nodes.get("ogg/opus" /* OggOpus */), - cost: 2, - transformer: /* @__PURE__ */ __name((input) => new prism.FFmpeg({ - args: ["-i", typeof input === "string" ? input : "-", ...FFMPEG_OPUS_ARGUMENTS] - }), "transformer") - }; - nodes.get("arbitrary" /* Arbitrary */).addEdge(FFMPEG_OGG_EDGE); - nodes.get("ogg/opus" /* OggOpus */).addEdge(FFMPEG_OGG_EDGE); - nodes.get("webm/opus" /* WebmOpus */).addEdge(FFMPEG_OGG_EDGE); - } - return nodes; -} -__name(initializeNodes, "initializeNodes"); -function findPath(from, constraints, goal = getNode("opus" /* Opus */), path = [], depth = 5) { - if (from === goal && constraints(path)) { - return { cost: 0 }; - } else if (depth === 0) { - return { cost: Number.POSITIVE_INFINITY }; - } - let currentBest; - for (const edge of from.edges) { - if (currentBest && edge.cost > currentBest.cost) continue; - const next = findPath(edge.to, constraints, goal, [...path, edge], depth - 1); - const cost = edge.cost + next.cost; - if (!currentBest || cost < currentBest.cost) { - currentBest = { cost, edge, next }; - } - } - return currentBest ?? { cost: Number.POSITIVE_INFINITY }; -} -__name(findPath, "findPath"); -function constructPipeline(step) { - const edges = []; - let current = step; - while (current?.edge) { - edges.push(current.edge); - current = current.next; - } - return edges; -} -__name(constructPipeline, "constructPipeline"); -function findPipeline(from, constraint) { - return constructPipeline(findPath(getNode(from), constraint)); -} -__name(findPipeline, "findPipeline"); - -// src/audio/AudioResource.ts -var AudioResource = class { - static { - __name(this, "AudioResource"); - } - /** - * An object-mode Readable stream that emits Opus packets. This is what is played by audio players. - */ - playStream; - /** - * The pipeline used to convert the input stream into a playable format. For example, this may - * contain an FFmpeg component for arbitrary inputs, and it may contain a VolumeTransformer component - * for resources with inline volume transformation enabled. - */ - edges; - /** - * Optional metadata that can be used to identify the resource. - */ - metadata; - /** - * If the resource was created with inline volume transformation enabled, then this will be a - * prism-media VolumeTransformer. You can use this to alter the volume of the stream. - */ - volume; - /** - * If using an Opus encoder to create this audio resource, then this will be a prism-media opus.Encoder. - * You can use this to control settings such as bitrate, FEC, PLP. - */ - encoder; - /** - * The audio player that the resource is subscribed to, if any. - */ - audioPlayer; - /** - * The playback duration of this audio resource, given in milliseconds. - */ - playbackDuration = 0; - /** - * Whether or not the stream for this resource has started (data has become readable) - */ - started = false; - /** - * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches. - */ - silencePaddingFrames; - /** - * The number of remaining silence frames to play. If -1, the frames have not yet started playing. - */ - silenceRemaining = -1; - constructor(edges, streams, metadata, silencePaddingFrames) { - this.edges = edges; - this.playStream = streams.length > 1 ? pipeline(streams, noop) : streams[0]; - this.metadata = metadata; - this.silencePaddingFrames = silencePaddingFrames; - for (const stream of streams) { - if (stream instanceof prism2.VolumeTransformer) { - this.volume = stream; - } else if (stream instanceof prism2.opus.Encoder) { - this.encoder = stream; - } - } - this.playStream.once("readable", () => this.started = true); - } - /** - * Whether this resource is readable. If the underlying resource is no longer readable, this will still return true - * while there are silence padding frames left to play. - */ - get readable() { - if (this.silenceRemaining === 0) return false; - const real = this.playStream.readable; - if (!real) { - if (this.silenceRemaining === -1) this.silenceRemaining = this.silencePaddingFrames; - return this.silenceRemaining !== 0; - } - return real; - } - /** - * Whether this resource has ended or not. - */ - get ended() { - return this.playStream.readableEnded || this.playStream.destroyed || this.silenceRemaining === 0; - } - /** - * Attempts to read an Opus packet from the audio resource. If a packet is available, the playbackDuration - * is incremented. - * - * @remarks - * It is advisable to check that the playStream is readable before calling this method. While no runtime - * errors will be thrown, you should check that the resource is still available before attempting to - * read from it. - * @internal - */ - read() { - if (this.silenceRemaining === 0) { - return null; - } else if (this.silenceRemaining > 0) { - this.silenceRemaining--; - return SILENCE_FRAME; - } - const packet = this.playStream.read(); - if (packet) { - this.playbackDuration += 20; - } - return packet; - } -}; -var VOLUME_CONSTRAINT = /* @__PURE__ */ __name((path) => path.some((edge) => edge.type === "volume transformer" /* InlineVolume */), "VOLUME_CONSTRAINT"); -var NO_CONSTRAINT = /* @__PURE__ */ __name(() => true, "NO_CONSTRAINT"); -function inferStreamType(stream) { - if (stream instanceof prism2.opus.Encoder) { - return { streamType: "opus" /* Opus */, hasVolume: false }; - } else if (stream instanceof prism2.opus.Decoder) { - return { streamType: "raw" /* Raw */, hasVolume: false }; - } else if (stream instanceof prism2.VolumeTransformer) { - return { streamType: "raw" /* Raw */, hasVolume: true }; - } else if (stream instanceof prism2.opus.OggDemuxer) { - return { streamType: "opus" /* Opus */, hasVolume: false }; - } else if (stream instanceof prism2.opus.WebmDemuxer) { - return { streamType: "opus" /* Opus */, hasVolume: false }; - } - return { streamType: "arbitrary" /* Arbitrary */, hasVolume: false }; -} -__name(inferStreamType, "inferStreamType"); -function createAudioResource(input, options = {}) { - let inputType = options.inputType; - let needsInlineVolume = Boolean(options.inlineVolume); - if (typeof input === "string") { - inputType = "arbitrary" /* Arbitrary */; - } else if (inputType === void 0) { - const analysis = inferStreamType(input); - inputType = analysis.streamType; - needsInlineVolume = needsInlineVolume && !analysis.hasVolume; - } - const transformerPipeline = findPipeline(inputType, needsInlineVolume ? VOLUME_CONSTRAINT : NO_CONSTRAINT); - if (transformerPipeline.length === 0) { - if (typeof input === "string") throw new Error(`Invalid pipeline constructed for string resource '${input}'`); - return new AudioResource( - [], - [input], - options.metadata ?? null, - options.silencePaddingFrames ?? 5 - ); - } - const streams = transformerPipeline.map((edge) => edge.transformer(input)); - if (typeof input !== "string") streams.unshift(input); - return new AudioResource( - transformerPipeline, - streams, - options.metadata ?? null, - options.silencePaddingFrames ?? 5 - ); -} -__name(createAudioResource, "createAudioResource"); - -// src/util/generateDependencyReport.ts -import { getCiphers } from "crypto"; -import { resolve, dirname } from "path"; -import prism3 from "prism-media"; -function findPackageJSON(dir, packageName, depth) { - if (depth === 0) return void 0; - const attemptedPath = resolve(dir, "./package.json"); - try { - const pkg = __require(attemptedPath); - if (pkg.name !== packageName) throw new Error("package.json does not match"); - return pkg; - } catch { - return findPackageJSON(resolve(dir, ".."), packageName, depth - 1); - } -} -__name(findPackageJSON, "findPackageJSON"); -function version(name) { - try { - if (name === "@discordjs/voice") { - return "0.19.2"; - } - const pkg = findPackageJSON(dirname(__require.resolve(name)), name, 3); - return pkg?.version ?? "not found"; - } catch { - return "not found"; - } -} -__name(version, "version"); -function generateDependencyReport() { - const report = []; - const addVersion = /* @__PURE__ */ __name((name) => report.push(`- ${name}: ${version(name)}`), "addVersion"); - report.push("Core Dependencies"); - addVersion("@discordjs/voice"); - addVersion("prism-media"); - report.push(""); - report.push("Opus Libraries"); - addVersion("@discordjs/opus"); - addVersion("opusscript"); - report.push(""); - report.push("Encryption Libraries"); - report.push(`- native crypto support for aes-256-gcm: ${getCiphers().includes("aes-256-gcm") ? "yes" : "no"}`); - addVersion("sodium-native"); - addVersion("sodium"); - addVersion("libsodium-wrappers"); - addVersion("@stablelib/xchacha20poly1305"); - addVersion("@noble/ciphers"); - report.push(""); - report.push("DAVE Libraries"); - addVersion("@snazzah/davey"); - report.push(""); - report.push("FFmpeg"); - try { - const info = prism3.FFmpeg.getInfo(); - report.push(`- version: ${info.version}`); - report.push(`- libopus: ${info.output.includes("--enable-libopus") ? "yes" : "no"}`); - } catch { - report.push("- not found"); - } - return ["-".repeat(50), ...report, "-".repeat(50)].join("\n"); -} -__name(generateDependencyReport, "generateDependencyReport"); - -// src/util/entersState.ts -import { once } from "events"; - -// src/util/abortAfter.ts -function abortAfter(delay) { - const ac = new AbortController(); - const timeout = setTimeout(() => ac.abort(), delay); - ac.signal.addEventListener("abort", () => clearTimeout(timeout)); - return [ac, ac.signal]; -} -__name(abortAfter, "abortAfter"); - -// src/util/entersState.ts -async function entersState(target, status, timeoutOrSignal) { - if (target.state.status !== status) { - const [ac, signal] = typeof timeoutOrSignal === "number" ? abortAfter(timeoutOrSignal) : [void 0, timeoutOrSignal]; - try { - await once(target, status, { signal }); - } finally { - ac?.abort(); - } - } - return target; -} -__name(entersState, "entersState"); - -// src/util/demuxProbe.ts -import { Buffer as Buffer9 } from "buffer"; -import process2 from "process"; -import { Readable as Readable2 } from "stream"; -import prism4 from "prism-media"; -function validateDiscordOpusHead(opusHead) { - const channels = opusHead.readUInt8(9); - const sampleRate = opusHead.readUInt32LE(12); - return channels === 2 && sampleRate === 48e3; -} -__name(validateDiscordOpusHead, "validateDiscordOpusHead"); -async function demuxProbe(stream, probeSize = 1024, validator = validateDiscordOpusHead) { - return new Promise((resolve2, reject) => { - if (stream.readableObjectMode) { - reject(new Error("Cannot probe a readable stream in object mode")); - return; - } - if (stream.readableEnded) { - reject(new Error("Cannot probe a stream that has ended")); - return; - } - let readBuffer = Buffer9.alloc(0); - let resolved; - const finish = /* @__PURE__ */ __name((type) => { - stream.off("data", onData); - stream.off("close", onClose); - stream.off("end", onClose); - stream.pause(); - resolved = type; - if (stream.readableEnded) { - resolve2({ - stream: Readable2.from(readBuffer), - type - }); - } else { - if (readBuffer.length > 0) { - stream.push(readBuffer); - } - resolve2({ - stream, - type - }); - } - }, "finish"); - const foundHead = /* @__PURE__ */ __name((type) => (head) => { - if (validator(head)) { - finish(type); - } - }, "foundHead"); - const webm = new prism4.opus.WebmDemuxer(); - webm.once("error", noop); - webm.on("head", foundHead("webm/opus" /* WebmOpus */)); - const ogg = new prism4.opus.OggDemuxer(); - ogg.once("error", noop); - ogg.on("head", foundHead("ogg/opus" /* OggOpus */)); - const onClose = /* @__PURE__ */ __name(() => { - if (!resolved) { - finish("arbitrary" /* Arbitrary */); - } - }, "onClose"); - const onData = /* @__PURE__ */ __name((buffer) => { - readBuffer = Buffer9.concat([readBuffer, buffer]); - webm.write(buffer); - ogg.write(buffer); - if (readBuffer.length >= probeSize) { - stream.off("data", onData); - stream.pause(); - process2.nextTick(onClose); - } - }, "onData"); - stream.once("error", reject); - stream.on("data", onData); - stream.once("close", onClose); - stream.once("end", onClose); - }); -} -__name(demuxProbe, "demuxProbe"); - -// src/index.ts -var version2 = "0.19.2"; -export { - AudioPlayer, - AudioPlayerError, - AudioPlayerStatus, - AudioReceiveStream, - AudioResource, - DAVESession, - EndBehaviorType, - Networking, - NetworkingStatusCode, - NoSubscriberBehavior, - Node, - PlayerSubscription, - RTP_OPUS_PAYLOAD_TYPE, - SSRCMap, - SpeakingMap, - StreamType, - TransformerType, - VoiceConnection, - VoiceConnectionDisconnectReason, - VoiceConnectionStatus, - VoiceReceiver, - VoiceUDPSocket, - VoiceWebSocket, - createAudioPlayer, - createAudioResource, - createDefaultAudioReceiveStreamOptions, - demuxProbe, - entersState, - generateDependencyReport, - getGroups, - getVoiceConnection, - getVoiceConnections, - joinVoiceChannel, - validateDiscordOpusHead, - version2 as version -}; -//# sourceMappingURL=index.mjs.map \ No newline at end of file diff --git a/services/discord-gateway/vendor/discord-voice-fork/dist/index.mjs.map b/services/discord-gateway/vendor/discord-voice-fork/dist/index.mjs.map deleted file mode 100644 index fa39c389..00000000 --- a/services/discord-gateway/vendor/discord-voice-fork/dist/index.mjs.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../src/VoiceConnection.ts","../src/DataStore.ts","../src/networking/Networking.ts","../src/util/Secretbox.ts","../src/util/constants.ts","../src/util/util.ts","../src/networking/DAVESession.ts","../src/audio/AudioPlayer.ts","../src/audio/AudioPlayerError.ts","../src/audio/PlayerSubscription.ts","../src/networking/VoiceUDPSocket.ts","../src/networking/VoiceWebSocket.ts","../src/receive/VoiceReceiver.ts","../src/receive/AudioReceiveStream.ts","../src/receive/SSRCMap.ts","../src/receive/SpeakingMap.ts","../src/joinVoiceChannel.ts","../src/audio/AudioResource.ts","../src/audio/TransformerGraph.ts","../src/util/generateDependencyReport.ts","../src/util/entersState.ts","../src/util/abortAfter.ts","../src/util/demuxProbe.ts","../src/index.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/unbound-method */\nimport type { Buffer } from 'node:buffer';\nimport { EventEmitter } from 'node:events';\nimport type { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v10';\nimport type { JoinConfig } from './DataStore';\nimport {\n\tgetVoiceConnection,\n\tcreateJoinVoiceChannelPayload,\n\ttrackVoiceConnection,\n\tuntrackVoiceConnection,\n} from './DataStore';\nimport type { AudioPlayer } from './audio/AudioPlayer';\nimport type { PlayerSubscription } from './audio/PlayerSubscription';\nimport type { VoiceWebSocket, VoiceUDPSocket } from './networking';\nimport { Networking, NetworkingStatusCode, type NetworkingState } from './networking/Networking';\nimport { VoiceReceiver } from './receive/index';\nimport type { DiscordGatewayAdapterImplementerMethods } from './util/adapter';\nimport { noop } from './util/util';\nimport type { CreateVoiceConnectionOptions } from './index';\n\n/**\n * The various status codes a voice connection can hold at any one time.\n */\nexport enum VoiceConnectionStatus {\n\t/**\n\t * The `VOICE_SERVER_UPDATE` and `VOICE_STATE_UPDATE` packets have been received, now attempting to establish a voice connection.\n\t */\n\tConnecting = 'connecting',\n\n\t/**\n\t * The voice connection has been destroyed and untracked, it cannot be reused.\n\t */\n\tDestroyed = 'destroyed',\n\n\t/**\n\t * The voice connection has either been severed or not established.\n\t */\n\tDisconnected = 'disconnected',\n\n\t/**\n\t * A voice connection has been established, and is ready to be used.\n\t */\n\tReady = 'ready',\n\n\t/**\n\t * Sending a packet to the main Discord gateway to indicate we want to change our voice state.\n\t */\n\tSignalling = 'signalling',\n}\n\n/**\n * The state that a VoiceConnection will be in when it is waiting to receive a VOICE_SERVER_UPDATE and\n * VOICE_STATE_UPDATE packet from Discord, provided by the adapter.\n */\nexport interface VoiceConnectionSignallingState {\n\tadapter: DiscordGatewayAdapterImplementerMethods;\n\tstatus: VoiceConnectionStatus.Signalling;\n\tsubscription?: PlayerSubscription | undefined;\n}\n\n/**\n * The reasons a voice connection can be in the disconnected state.\n */\nexport enum VoiceConnectionDisconnectReason {\n\t/**\n\t * When the WebSocket connection has been closed.\n\t */\n\tWebSocketClose,\n\n\t/**\n\t * When the adapter was unable to send a message requested by the VoiceConnection.\n\t */\n\tAdapterUnavailable,\n\n\t/**\n\t * When a VOICE_SERVER_UPDATE packet is received with a null endpoint, causing the connection to be severed.\n\t */\n\tEndpointRemoved,\n\n\t/**\n\t * When a manual disconnect was requested.\n\t */\n\tManual,\n}\n\n/**\n * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is\n * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect.\n */\nexport interface VoiceConnectionDisconnectedBaseState {\n\tadapter: DiscordGatewayAdapterImplementerMethods;\n\tstatus: VoiceConnectionStatus.Disconnected;\n\tsubscription?: PlayerSubscription | undefined;\n}\n\n/**\n * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is\n * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect.\n */\nexport interface VoiceConnectionDisconnectedOtherState extends VoiceConnectionDisconnectedBaseState {\n\treason: Exclude;\n}\n\n/**\n * The state that a VoiceConnection will be in when its WebSocket connection was closed.\n * You can manually attempt to reconnect using VoiceConnection#reconnect.\n */\nexport interface VoiceConnectionDisconnectedWebSocketState extends VoiceConnectionDisconnectedBaseState {\n\t/**\n\t * The close code of the WebSocket connection to the Discord voice server.\n\t */\n\tcloseCode: number;\n\n\treason: VoiceConnectionDisconnectReason.WebSocketClose;\n}\n\n/**\n * The states that a VoiceConnection can be in when it is not connected to a Discord voice server nor is\n * it attempting to connect. You can manually attempt to connect using VoiceConnection#reconnect.\n */\nexport type VoiceConnectionDisconnectedState =\n\t| VoiceConnectionDisconnectedOtherState\n\t| VoiceConnectionDisconnectedWebSocketState;\n\n/**\n * The state that a VoiceConnection will be in when it is establishing a connection to a Discord\n * voice server.\n */\nexport interface VoiceConnectionConnectingState {\n\tadapter: DiscordGatewayAdapterImplementerMethods;\n\tnetworking: Networking;\n\tstatus: VoiceConnectionStatus.Connecting;\n\tsubscription?: PlayerSubscription | undefined;\n}\n\n/**\n * The state that a VoiceConnection will be in when it has an active connection to a Discord\n * voice server.\n */\nexport interface VoiceConnectionReadyState {\n\tadapter: DiscordGatewayAdapterImplementerMethods;\n\tnetworking: Networking;\n\tstatus: VoiceConnectionStatus.Ready;\n\tsubscription?: PlayerSubscription | undefined;\n}\n\n/**\n * The state that a VoiceConnection will be in when it has been permanently been destroyed by the\n * user and untracked by the library. It cannot be reconnected, instead, a new VoiceConnection\n * needs to be established.\n */\nexport interface VoiceConnectionDestroyedState {\n\tstatus: VoiceConnectionStatus.Destroyed;\n}\n\n/**\n * The various states that a voice connection can be in.\n */\nexport type VoiceConnectionState =\n\t| VoiceConnectionConnectingState\n\t| VoiceConnectionDestroyedState\n\t| VoiceConnectionDisconnectedState\n\t| VoiceConnectionReadyState\n\t| VoiceConnectionSignallingState;\n\nexport interface VoiceConnection extends EventEmitter {\n\t/**\n\t * Emitted when there is an error emitted from the voice connection\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'error', listener: (error: Error) => void): this;\n\t/**\n\t * Emitted debugging information about the voice connection\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'debug', listener: (message: string) => void): this;\n\t/**\n\t * Emitted when the state of the voice connection changes\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'stateChange', listener: (oldState: VoiceConnectionState, newState: VoiceConnectionState) => void): this;\n\t/**\n\t * Emitted when the end-to-end encrypted session has transitioned\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'transitioned', listener: (transitionId: number) => void): this;\n\t/**\n\t * Emitted when the state of the voice connection changes to a specific status\n\t *\n\t * @eventProperty\n\t */\n\ton(\n\t\tevent: Event,\n\t\tlistener: (oldState: VoiceConnectionState, newState: VoiceConnectionState & { status: Event }) => void,\n\t): this;\n}\n\n/**\n * A connection to the voice server of a Guild, can be used to play audio in voice channels.\n */\nexport class VoiceConnection extends EventEmitter {\n\t/**\n\t * The number of consecutive rejoin attempts. Initially 0, and increments for each rejoin.\n\t * When a connection is successfully established, it resets to 0.\n\t */\n\tpublic rejoinAttempts: number;\n\n\t/**\n\t * The state of the voice connection.\n\t */\n\tprivate _state: VoiceConnectionState;\n\n\t/**\n\t * A configuration storing all the data needed to reconnect to a Guild's voice server.\n\t *\n\t * @internal\n\t */\n\tpublic readonly joinConfig: JoinConfig;\n\n\t/**\n\t * The two packets needed to successfully establish a voice connection. They are received\n\t * from the main Discord gateway after signalling to change the voice state.\n\t */\n\tprivate readonly packets: {\n\t\tserver: GatewayVoiceServerUpdateDispatchData | undefined;\n\t\tstate: GatewayVoiceStateUpdateDispatchData | undefined;\n\t};\n\n\t/**\n\t * The receiver of this voice connection. You should join the voice channel with `selfDeaf` set\n\t * to false for this feature to work properly.\n\t */\n\tpublic readonly receiver: VoiceReceiver;\n\n\t/**\n\t * The debug logger function, if debugging is enabled.\n\t */\n\tprivate readonly debug: ((message: string) => void) | null;\n\n\t/**\n\t * The options used to create this voice connection.\n\t */\n\tprivate readonly options: CreateVoiceConnectionOptions;\n\n\t/**\n\t * Creates a new voice connection.\n\t *\n\t * @param joinConfig - The data required to establish the voice connection\n\t * @param options - The options used to create this voice connection\n\t */\n\tpublic constructor(joinConfig: JoinConfig, options: CreateVoiceConnectionOptions) {\n\t\tsuper();\n\n\t\tthis.debug = options.debug ? (message: string) => this.emit('debug', message) : null;\n\t\tthis.rejoinAttempts = 0;\n\n\t\tthis.receiver = new VoiceReceiver(this);\n\n\t\tthis.onNetworkingClose = this.onNetworkingClose.bind(this);\n\t\tthis.onNetworkingStateChange = this.onNetworkingStateChange.bind(this);\n\t\tthis.onNetworkingError = this.onNetworkingError.bind(this);\n\t\tthis.onNetworkingDebug = this.onNetworkingDebug.bind(this);\n\t\tthis.onNetworkingTransitioned = this.onNetworkingTransitioned.bind(this);\n\n\t\tconst adapter = options.adapterCreator({\n\t\t\tonVoiceServerUpdate: (data) => this.addServerPacket(data),\n\t\t\tonVoiceStateUpdate: (data) => this.addStatePacket(data),\n\t\t\tdestroy: () => this.destroy(false),\n\t\t});\n\n\t\tthis._state = { status: VoiceConnectionStatus.Signalling, adapter };\n\n\t\tthis.packets = {\n\t\t\tserver: undefined,\n\t\t\tstate: undefined,\n\t\t};\n\n\t\tthis.joinConfig = joinConfig;\n\t\tthis.options = options;\n\t}\n\n\t/**\n\t * The current state of the voice connection.\n\t *\n\t * @remarks\n\t * The setter will perform clean-up operations where necessary.\n\t */\n\tpublic get state() {\n\t\treturn this._state;\n\t}\n\n\tpublic set state(newState: VoiceConnectionState) {\n\t\tconst oldState = this._state;\n\t\tconst oldNetworking = Reflect.get(oldState, 'networking') as Networking | undefined;\n\t\tconst newNetworking = Reflect.get(newState, 'networking') as Networking | undefined;\n\n\t\tconst oldSubscription = Reflect.get(oldState, 'subscription') as PlayerSubscription | undefined;\n\t\tconst newSubscription = Reflect.get(newState, 'subscription') as PlayerSubscription | undefined;\n\n\t\tif (oldNetworking !== newNetworking) {\n\t\t\tif (oldNetworking) {\n\t\t\t\toldNetworking.on('error', noop);\n\t\t\t\toldNetworking.off('debug', this.onNetworkingDebug);\n\t\t\t\toldNetworking.off('error', this.onNetworkingError);\n\t\t\t\toldNetworking.off('close', this.onNetworkingClose);\n\t\t\t\toldNetworking.off('stateChange', this.onNetworkingStateChange);\n\t\t\t\toldNetworking.off('transitioned', this.onNetworkingTransitioned);\n\t\t\t\toldNetworking.destroy();\n\t\t\t}\n\n\t\t\tif (newNetworking) this.updateReceiveBindings(newNetworking.state, oldNetworking?.state);\n\t\t}\n\n\t\tif (newState.status === VoiceConnectionStatus.Ready) {\n\t\t\tthis.rejoinAttempts = 0;\n\t\t} else if (newState.status === VoiceConnectionStatus.Destroyed) {\n\t\t\tfor (const stream of this.receiver.subscriptions.values()) {\n\t\t\t\tif (!stream.destroyed) stream.destroy();\n\t\t\t}\n\t\t}\n\n\t\t// If destroyed, the adapter can also be destroyed so it can be cleaned up by the user\n\t\tif (oldState.status !== VoiceConnectionStatus.Destroyed && newState.status === VoiceConnectionStatus.Destroyed) {\n\t\t\toldState.adapter.destroy();\n\t\t}\n\n\t\tthis._state = newState;\n\n\t\tif (oldSubscription && oldSubscription !== newSubscription) {\n\t\t\toldSubscription.unsubscribe();\n\t\t}\n\n\t\tthis.emit('stateChange', oldState, newState);\n\t\tif (oldState.status !== newState.status) {\n\t\t\tthis.emit(newState.status, oldState, newState as any);\n\t\t}\n\t}\n\n\t/**\n\t * Registers a `VOICE_SERVER_UPDATE` packet to the voice connection. This will cause it to reconnect using the\n\t * new data provided in the packet.\n\t *\n\t * @param packet - The received `VOICE_SERVER_UPDATE` packet\n\t */\n\tprivate addServerPacket(packet: GatewayVoiceServerUpdateDispatchData) {\n\t\tthis.packets.server = packet;\n\t\tif (packet.endpoint) {\n\t\t\tthis.configureNetworking();\n\t\t} else if (this.state.status !== VoiceConnectionStatus.Destroyed) {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\t\treason: VoiceConnectionDisconnectReason.EndpointRemoved,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Registers a `VOICE_STATE_UPDATE` packet to the voice connection. Most importantly, it stores the id of the\n\t * channel that the client is connected to.\n\t *\n\t * @param packet - The received `VOICE_STATE_UPDATE` packet\n\t */\n\tprivate addStatePacket(packet: GatewayVoiceStateUpdateDispatchData) {\n\t\tthis.packets.state = packet;\n\n\t\tif (packet.self_deaf !== undefined) this.joinConfig.selfDeaf = packet.self_deaf;\n\t\tif (packet.self_mute !== undefined) this.joinConfig.selfMute = packet.self_mute;\n\t\tif (packet.channel_id) this.joinConfig.channelId = packet.channel_id;\n\t\t/*\n\t\t\tthe channel_id being null doesn't necessarily mean it was intended for the client to leave the voice channel\n\t\t\tas it may have disconnected due to network failure. This will be gracefully handled once the voice websocket\n\t\t\tdies, and then it is up to the user to decide how they wish to handle this.\n\t\t*/\n\t}\n\n\t/**\n\t * Called when the networking state changes, and the new ws/udp packet/message handlers need to be rebound\n\t * to the new instances.\n\t *\n\t * @param newState - The new networking state\n\t * @param oldState - The old networking state, if there is one\n\t */\n\tprivate updateReceiveBindings(newState: NetworkingState, oldState?: NetworkingState) {\n\t\tconst oldWs = Reflect.get(oldState ?? {}, 'ws') as VoiceWebSocket | undefined;\n\t\tconst newWs = Reflect.get(newState, 'ws') as VoiceWebSocket | undefined;\n\t\tconst oldUdp = Reflect.get(oldState ?? {}, 'udp') as VoiceUDPSocket | undefined;\n\t\tconst newUdp = Reflect.get(newState, 'udp') as VoiceUDPSocket | undefined;\n\n\t\tif (oldWs !== newWs) {\n\t\t\toldWs?.off('packet', this.receiver.onWsPacket);\n\t\t\tnewWs?.on('packet', this.receiver.onWsPacket);\n\t\t}\n\n\t\tif (oldUdp !== newUdp) {\n\t\t\toldUdp?.off('message', this.receiver.onUdpMessage);\n\t\t\tnewUdp?.on('message', this.receiver.onUdpMessage);\n\t\t}\n\n\t\tthis.receiver.connectionData = Reflect.get(newState, 'connectionData') ?? {};\n\t}\n\n\t/**\n\t * Attempts to configure a networking instance for this voice connection using the received packets.\n\t * Both packets are required, and any existing networking instance will be destroyed.\n\t *\n\t * @remarks\n\t * This is called when the voice server of the connection changes, e.g. if the bot is moved into a\n\t * different channel in the same guild but has a different voice server. In this instance, the connection\n\t * needs to be re-established to the new voice server.\n\t *\n\t * The connection will transition to the Connecting state when this is called.\n\t */\n\tpublic configureNetworking() {\n\t\tconst { server, state } = this.packets;\n\t\tif (!server || !state || this.state.status === VoiceConnectionStatus.Destroyed || !server.endpoint) return;\n\n\t\tconst networking = new Networking(\n\t\t\t{\n\t\t\t\tendpoint: server.endpoint,\n\t\t\t\tserverId: server.guild_id,\n\t\t\t\ttoken: server.token,\n\t\t\t\tsessionId: state.session_id,\n\t\t\t\tuserId: state.user_id,\n\t\t\t\tchannelId: state.channel_id!,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdebug: Boolean(this.debug),\n\t\t\t\tdaveEncryption: this.options.daveEncryption ?? true,\n\t\t\t\tdecryptionFailureTolerance: this.options.decryptionFailureTolerance,\n\t\t\t},\n\t\t);\n\n\t\tnetworking.once('close', this.onNetworkingClose);\n\t\tnetworking.on('stateChange', this.onNetworkingStateChange);\n\t\tnetworking.on('error', this.onNetworkingError);\n\t\tnetworking.on('debug', this.onNetworkingDebug);\n\t\tnetworking.on('transitioned', this.onNetworkingTransitioned);\n\n\t\tthis.state = {\n\t\t\t...this.state,\n\t\t\tstatus: VoiceConnectionStatus.Connecting,\n\t\t\tnetworking,\n\t\t};\n\t}\n\n\t/**\n\t * Called when the networking instance for this connection closes. If the close code is 4014 (do not reconnect),\n\t * the voice connection will transition to the Disconnected state which will store the close code. You can\n\t * decide whether or not to reconnect when this occurs by listening for the state change and calling reconnect().\n\t *\n\t * @remarks\n\t * If the close code was anything other than 4014, it is likely that the closing was not intended, and so the\n\t * VoiceConnection will signal to Discord that it would like to rejoin the channel. This automatically attempts\n\t * to re-establish the connection. This would be seen as a transition from the Ready state to the Signalling state.\n\t * @param code - The close code\n\t */\n\tprivate onNetworkingClose(code: number) {\n\t\tif (this.state.status === VoiceConnectionStatus.Destroyed) return;\n\t\t// If networking closes, try to connect to the voice channel again.\n\t\tif (code === 4_014) {\n\t\t\t// Disconnected - networking is already destroyed here\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\t\treason: VoiceConnectionDisconnectReason.WebSocketClose,\n\t\t\t\tcloseCode: code,\n\t\t\t};\n\t\t} else {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Signalling,\n\t\t\t};\n\t\t\tthis.rejoinAttempts++;\n\t\t\tif (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) {\n\t\t\t\tthis.state = {\n\t\t\t\t\t...this.state,\n\t\t\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\t\t\treason: VoiceConnectionDisconnectReason.AdapterUnavailable,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Called when the state of the networking instance changes. This is used to derive the state of the voice connection.\n\t *\n\t * @param oldState - The previous state\n\t * @param newState - The new state\n\t */\n\tprivate onNetworkingStateChange(oldState: NetworkingState, newState: NetworkingState) {\n\t\tthis.updateReceiveBindings(newState, oldState);\n\t\tif (oldState.code === newState.code) return;\n\t\tif (this.state.status !== VoiceConnectionStatus.Connecting && this.state.status !== VoiceConnectionStatus.Ready)\n\t\t\treturn;\n\n\t\tif (newState.code === NetworkingStatusCode.Ready) {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Ready,\n\t\t\t};\n\t\t} else if (newState.code !== NetworkingStatusCode.Closed) {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Connecting,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Propagates errors from the underlying network instance.\n\t *\n\t * @param error - The error to propagate\n\t */\n\tprivate onNetworkingError(error: Error) {\n\t\tthis.emit('error', error);\n\t}\n\n\t/**\n\t * Propagates debug messages from the underlying network instance.\n\t *\n\t * @param message - The debug message to propagate\n\t */\n\tprivate onNetworkingDebug(message: string) {\n\t\tthis.debug?.(`[NW] ${message}`);\n\t}\n\n\t/**\n\t * Propagates transitions from the underlying network instance.\n\t *\n\t * @param transitionId - The transition id\n\t */\n\tprivate onNetworkingTransitioned(transitionId: number) {\n\t\tthis.emit('transitioned', transitionId);\n\t}\n\n\t/**\n\t * Prepares an audio packet for dispatch.\n\t *\n\t * @param buffer - The Opus packet to prepare\n\t */\n\tpublic prepareAudioPacket(buffer: Buffer) {\n\t\tconst state = this.state;\n\t\tif (state.status !== VoiceConnectionStatus.Ready) return;\n\t\treturn state.networking.prepareAudioPacket(buffer);\n\t}\n\n\t/**\n\t * Dispatches the previously prepared audio packet (if any)\n\t */\n\tpublic dispatchAudio() {\n\t\tconst state = this.state;\n\t\tif (state.status !== VoiceConnectionStatus.Ready) return;\n\t\treturn state.networking.dispatchAudio();\n\t}\n\n\t/**\n\t * Prepares an audio packet and dispatches it immediately.\n\t *\n\t * @param buffer - The Opus packet to play\n\t */\n\tpublic playOpusPacket(buffer: Buffer) {\n\t\tconst state = this.state;\n\t\tif (state.status !== VoiceConnectionStatus.Ready) return;\n\t\tstate.networking.prepareAudioPacket(buffer);\n\t\treturn state.networking.dispatchAudio();\n\t}\n\n\t/**\n\t * Destroys the VoiceConnection, preventing it from connecting to voice again.\n\t * This method should be called when you no longer require the VoiceConnection to\n\t * prevent memory leaks.\n\t *\n\t * @param adapterAvailable - Whether the adapter can be used\n\t */\n\tpublic destroy(adapterAvailable = true) {\n\t\tif (this.state.status === VoiceConnectionStatus.Destroyed) {\n\t\t\tthrow new Error('Cannot destroy VoiceConnection - it has already been destroyed');\n\t\t}\n\n\t\tif (getVoiceConnection(this.joinConfig.guildId, this.joinConfig.group) === this) {\n\t\t\tuntrackVoiceConnection(this);\n\t\t}\n\n\t\tif (adapterAvailable) {\n\t\t\tthis.state.adapter.sendPayload(createJoinVoiceChannelPayload({ ...this.joinConfig, channelId: null }));\n\t\t}\n\n\t\tthis.state = {\n\t\t\tstatus: VoiceConnectionStatus.Destroyed,\n\t\t};\n\t}\n\n\t/**\n\t * Disconnects the VoiceConnection, allowing the possibility of rejoining later on.\n\t *\n\t * @returns `true` if the connection was successfully disconnected\n\t */\n\tpublic disconnect() {\n\t\tif (\n\t\t\tthis.state.status === VoiceConnectionStatus.Destroyed ||\n\t\t\tthis.state.status === VoiceConnectionStatus.Signalling\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.joinConfig.channelId = null;\n\t\tif (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) {\n\t\t\tthis.state = {\n\t\t\t\tadapter: this.state.adapter,\n\t\t\t\tsubscription: this.state.subscription,\n\t\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\t\treason: VoiceConnectionDisconnectReason.AdapterUnavailable,\n\t\t\t};\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.state = {\n\t\t\tadapter: this.state.adapter,\n\t\t\treason: VoiceConnectionDisconnectReason.Manual,\n\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t};\n\t\treturn true;\n\t}\n\n\t/**\n\t * Attempts to rejoin (better explanation soon:tm:)\n\t *\n\t * @remarks\n\t * Calling this method successfully will automatically increment the `rejoinAttempts` counter,\n\t * which you can use to inform whether or not you'd like to keep attempting to reconnect your\n\t * voice connection.\n\t *\n\t * A state transition from Disconnected to Signalling will be observed when this is called.\n\t */\n\tpublic rejoin(joinConfig?: Omit) {\n\t\tif (this.state.status === VoiceConnectionStatus.Destroyed) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst notReady = this.state.status !== VoiceConnectionStatus.Ready;\n\n\t\tif (notReady) this.rejoinAttempts++;\n\t\tObject.assign(this.joinConfig, joinConfig);\n\t\tif (this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) {\n\t\t\tif (notReady) {\n\t\t\t\tthis.state = {\n\t\t\t\t\t...this.state,\n\t\t\t\t\tstatus: VoiceConnectionStatus.Signalling,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn true;\n\t\t}\n\n\t\tthis.state = {\n\t\t\tadapter: this.state.adapter,\n\t\t\tsubscription: this.state.subscription,\n\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\treason: VoiceConnectionDisconnectReason.AdapterUnavailable,\n\t\t};\n\t\treturn false;\n\t}\n\n\t/**\n\t * Updates the speaking status of the voice connection. This is used when audio players are done playing audio,\n\t * and need to signal that the connection is no longer playing audio.\n\t *\n\t * @param enabled - Whether or not to show as speaking\n\t */\n\tpublic setSpeaking(enabled: boolean) {\n\t\tif (this.state.status !== VoiceConnectionStatus.Ready) return false;\n\t\t// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression\n\t\treturn this.state.networking.setSpeaking(enabled);\n\t}\n\n\t/**\n\t * Subscribes to an audio player, allowing the player to play audio on this voice connection.\n\t *\n\t * @param player - The audio player to subscribe to\n\t * @returns The created subscription\n\t */\n\tpublic subscribe(player: AudioPlayer) {\n\t\tif (this.state.status === VoiceConnectionStatus.Destroyed) return;\n\n\t\t// eslint-disable-next-line @typescript-eslint/dot-notation\n\t\tconst subscription = player['subscribe'](this);\n\n\t\tthis.state = {\n\t\t\t...this.state,\n\t\t\tsubscription,\n\t\t};\n\n\t\treturn subscription;\n\t}\n\n\t/**\n\t * The latest ping (in milliseconds) for the WebSocket connection and audio playback for this voice\n\t * connection, if this data is available.\n\t *\n\t * @remarks\n\t * For this data to be available, the VoiceConnection must be in the Ready state, and its underlying\n\t * WebSocket connection and UDP socket must have had at least one ping-pong exchange.\n\t */\n\tpublic get ping() {\n\t\tif (\n\t\t\tthis.state.status === VoiceConnectionStatus.Ready &&\n\t\t\tthis.state.networking.state.code === NetworkingStatusCode.Ready\n\t\t) {\n\t\t\treturn {\n\t\t\t\tws: this.state.networking.state.ws.ping,\n\t\t\t\tudp: this.state.networking.state.udp.ping,\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tws: undefined,\n\t\t\tudp: undefined,\n\t\t};\n\t}\n\n\t/**\n\t * The current voice privacy code of the encrypted session.\n\t *\n\t * @remarks\n\t * For this data to be available, the VoiceConnection must be in the Ready state,\n\t * and the connection would have to be end-to-end encrypted.\n\t */\n\tpublic get voicePrivacyCode() {\n\t\tif (\n\t\t\tthis.state.status === VoiceConnectionStatus.Ready &&\n\t\t\tthis.state.networking.state.code === NetworkingStatusCode.Ready\n\t\t) {\n\t\t\treturn this.state.networking.state.dave?.voicePrivacyCode ?? undefined;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Gets the verification code for a user in the session.\n\t *\n\t * @throws Will throw if end-to-end encryption is not on or if the user id provided is not in the session.\n\t */\n\tpublic async getVerificationCode(userId: string): Promise {\n\t\tif (\n\t\t\tthis.state.status === VoiceConnectionStatus.Ready &&\n\t\t\tthis.state.networking.state.code === NetworkingStatusCode.Ready &&\n\t\t\tthis.state.networking.state.dave\n\t\t) {\n\t\t\treturn this.state.networking.state.dave.getVerificationCode(userId);\n\t\t}\n\n\t\tthrow new Error('Session not available');\n\t}\n\n\t/**\n\t * Called when a subscription of this voice connection to an audio player is removed.\n\t *\n\t * @param subscription - The removed subscription\n\t */\n\tprotected onSubscriptionRemoved(subscription: PlayerSubscription) {\n\t\tif (this.state.status !== VoiceConnectionStatus.Destroyed && this.state.subscription === subscription) {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tsubscription: undefined,\n\t\t\t};\n\t\t}\n\t}\n}\n\n/**\n * Creates a new voice connection.\n *\n * @param joinConfig - The data required to establish the voice connection\n * @param options - The options to use when joining the voice channel\n */\nexport function createVoiceConnection(joinConfig: JoinConfig, options: CreateVoiceConnectionOptions) {\n\tconst payload = createJoinVoiceChannelPayload(joinConfig);\n\tconst existing = getVoiceConnection(joinConfig.guildId, joinConfig.group);\n\tif (existing && existing.state.status !== VoiceConnectionStatus.Destroyed) {\n\t\tif (existing.state.status === VoiceConnectionStatus.Disconnected) {\n\t\t\texisting.rejoin({\n\t\t\t\tchannelId: joinConfig.channelId,\n\t\t\t\tselfDeaf: joinConfig.selfDeaf,\n\t\t\t\tselfMute: joinConfig.selfMute,\n\t\t\t});\n\t\t} else if (!existing.state.adapter.sendPayload(payload)) {\n\t\t\texisting.state = {\n\t\t\t\t...existing.state,\n\t\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\t\treason: VoiceConnectionDisconnectReason.AdapterUnavailable,\n\t\t\t};\n\t\t}\n\n\t\treturn existing;\n\t}\n\n\tconst voiceConnection = new VoiceConnection(joinConfig, options);\n\ttrackVoiceConnection(voiceConnection);\n\tif (\n\t\tvoiceConnection.state.status !== VoiceConnectionStatus.Destroyed &&\n\t\t!voiceConnection.state.adapter.sendPayload(payload)\n\t) {\n\t\tvoiceConnection.state = {\n\t\t\t...voiceConnection.state,\n\t\t\tstatus: VoiceConnectionStatus.Disconnected,\n\t\t\treason: VoiceConnectionDisconnectReason.AdapterUnavailable,\n\t\t};\n\t}\n\n\treturn voiceConnection;\n}\n","import { GatewayOpcodes } from 'discord-api-types/v10';\nimport type { VoiceConnection } from './VoiceConnection';\nimport type { AudioPlayer } from './audio/index';\n\nexport interface JoinConfig {\n\tchannelId: string | null;\n\tgroup: string;\n\tguildId: string;\n\tselfDeaf: boolean;\n\tselfMute: boolean;\n}\n\n/**\n * Sends a voice state update to the main websocket shard of a guild, to indicate joining/leaving/moving across\n * voice channels.\n *\n * @param config - The configuration to use when joining the voice channel\n */\nexport function createJoinVoiceChannelPayload(config: JoinConfig) {\n\treturn {\n\t\top: GatewayOpcodes.VoiceStateUpdate,\n\t\t// eslint-disable-next-line id-length\n\t\td: {\n\t\t\tguild_id: config.guildId,\n\t\t\tchannel_id: config.channelId,\n\t\t\tself_deaf: config.selfDeaf,\n\t\t\tself_mute: config.selfMute,\n\t\t},\n\t};\n}\n\n// Voice Connections\nconst groups = new Map>();\ngroups.set('default', new Map());\n\nfunction getOrCreateGroup(group: string) {\n\tconst existing = groups.get(group);\n\tif (existing) return existing;\n\tconst map = new Map();\n\tgroups.set(group, map);\n\treturn map;\n}\n\n/**\n * Retrieves the map of group names to maps of voice connections. By default, all voice connections\n * are created under the 'default' group.\n *\n * @returns The group map\n */\nexport function getGroups() {\n\treturn groups;\n}\n\n/**\n * Retrieves all the voice connections under the 'default' group.\n *\n * @param group - The group to look up\n * @returns The map of voice connections\n */\nexport function getVoiceConnections(group?: 'default'): Map;\n\n/**\n * Retrieves all the voice connections under the given group name.\n *\n * @param group - The group to look up\n * @returns The map of voice connections\n */\nexport function getVoiceConnections(group: string): Map | undefined;\n\n/**\n * Retrieves all the voice connections under the given group name. Defaults to the 'default' group.\n *\n * @param group - The group to look up\n * @returns The map of voice connections\n */\nexport function getVoiceConnections(group = 'default') {\n\treturn groups.get(group);\n}\n\n/**\n * Finds a voice connection with the given guild id and group. Defaults to the 'default' group.\n *\n * @param guildId - The guild id of the voice connection\n * @param group - the group that the voice connection was registered with\n * @returns The voice connection, if it exists\n */\nexport function getVoiceConnection(guildId: string, group = 'default') {\n\treturn getVoiceConnections(group)?.get(guildId);\n}\n\nexport function untrackVoiceConnection(voiceConnection: VoiceConnection) {\n\treturn getVoiceConnections(voiceConnection.joinConfig.group)?.delete(voiceConnection.joinConfig.guildId);\n}\n\nexport function trackVoiceConnection(voiceConnection: VoiceConnection) {\n\treturn getOrCreateGroup(voiceConnection.joinConfig.group).set(voiceConnection.joinConfig.guildId, voiceConnection);\n}\n\n// Audio Players\n\n// Each audio packet is 20ms long\nconst FRAME_LENGTH = 20;\n\nlet audioCycleInterval: NodeJS.Timeout | undefined;\nlet nextTime = -1;\n\n/**\n * A list of created audio players that are still active and haven't been destroyed.\n */\nconst audioPlayers: AudioPlayer[] = [];\n\n/**\n * Called roughly every 20 milliseconds. Dispatches audio from all players, and then gets the players to prepare\n * the next audio frame.\n */\nfunction audioCycleStep() {\n\tif (nextTime === -1) return;\n\n\tnextTime += FRAME_LENGTH;\n\tconst available = audioPlayers.filter((player) => player.checkPlayable());\n\n\tfor (const player of available) {\n\t\t// eslint-disable-next-line @typescript-eslint/dot-notation\n\t\tplayer['_stepDispatch']();\n\t}\n\n\tprepareNextAudioFrame(available);\n}\n\n/**\n * Recursively gets the players that have been passed as parameters to prepare audio frames that can be played\n * at the start of the next cycle.\n */\nfunction prepareNextAudioFrame(players: AudioPlayer[]) {\n\tconst nextPlayer = players.shift();\n\n\tif (!nextPlayer) {\n\t\tif (nextTime !== -1) {\n\t\t\taudioCycleInterval = setTimeout(() => audioCycleStep(), Math.max(1, nextTime - Date.now()));\n\t\t}\n\n\t\treturn;\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/dot-notation\n\tnextPlayer['_stepPrepare']();\n\n\t// setImmediate to avoid long audio player chains blocking other scheduled tasks\n\tsetImmediate(() => prepareNextAudioFrame(players));\n}\n\n/**\n * Checks whether or not the given audio player is being driven by the data store clock.\n *\n * @param target - The target to test for\n * @returns `true` if it is being tracked, `false` otherwise\n */\nexport function hasAudioPlayer(target: AudioPlayer) {\n\treturn audioPlayers.includes(target);\n}\n\n/**\n * Adds an audio player to the data store tracking list, if it isn't already there.\n *\n * @param player - The player to track\n */\nexport function addAudioPlayer(player: AudioPlayer) {\n\tif (hasAudioPlayer(player)) return player;\n\taudioPlayers.push(player);\n\tif (audioPlayers.length === 1) {\n\t\tnextTime = Date.now();\n\t\tsetImmediate(() => audioCycleStep());\n\t}\n\n\treturn player;\n}\n\n/**\n * Removes an audio player from the data store tracking list, if it is present there.\n */\nexport function deleteAudioPlayer(player: AudioPlayer) {\n\tconst index = audioPlayers.indexOf(player);\n\tif (index === -1) return;\n\taudioPlayers.splice(index, 1);\n\tif (audioPlayers.length === 0) {\n\t\tnextTime = -1;\n\t\tif (audioCycleInterval !== undefined) clearTimeout(audioCycleInterval);\n\t}\n}\n","/* eslint-disable jsdoc/check-param-names */\n/* eslint-disable id-length */\n/* eslint-disable @typescript-eslint/unbound-method */\nimport { Buffer } from 'node:buffer';\nimport crypto from 'node:crypto';\nimport { EventEmitter } from 'node:events';\nimport type { VoiceReceivePayload, VoiceSpeakingFlags } from 'discord-api-types/voice/v8';\nimport { VoiceEncryptionMode, VoiceOpcodes } from 'discord-api-types/voice/v8';\nimport type { CloseEvent } from 'ws';\nimport * as secretbox from '../util/Secretbox';\nimport { RTP_OPUS_PAYLOAD_TYPE } from '../util/constants';\nimport { noop } from '../util/util';\nimport { DAVESession, getMaxProtocolVersion } from './DAVESession';\nimport { VoiceUDPSocket } from './VoiceUDPSocket';\nimport type { BinaryWebSocketMessage } from './VoiceWebSocket';\nimport { VoiceWebSocket } from './VoiceWebSocket';\n\n// The number of audio channels required by Discord\nconst CHANNELS = 2;\nconst TIMESTAMP_INC = (48_000 / 100) * CHANNELS;\nconst MAX_NONCE_SIZE = 2 ** 32 - 1;\n\nexport const SUPPORTED_ENCRYPTION_MODES: VoiceEncryptionMode[] = [VoiceEncryptionMode.AeadXChaCha20Poly1305RtpSize];\n\n// Just in case there's some system that doesn't come with aes-256-gcm, conditionally add it as supported\nif (crypto.getCiphers().includes('aes-256-gcm')) {\n\tSUPPORTED_ENCRYPTION_MODES.unshift(VoiceEncryptionMode.AeadAes256GcmRtpSize);\n}\n\n/**\n * The different statuses that a networking instance can hold. The order\n * of the states between OpeningWs and Ready is chronological (first the\n * instance enters OpeningWs, then it enters Identifying etc.)\n */\nexport enum NetworkingStatusCode {\n\tOpeningWs,\n\tIdentifying,\n\tUdpHandshaking,\n\tSelectingProtocol,\n\tReady,\n\tResuming,\n\tClosed,\n}\n\n/**\n * The initial Networking state. Instances will be in this state when a WebSocket connection to a Discord\n * voice gateway is being opened.\n */\nexport interface NetworkingOpeningWsState {\n\tcode: NetworkingStatusCode.OpeningWs;\n\tconnectionOptions: ConnectionOptions;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when it is attempting to authorize itself.\n */\nexport interface NetworkingIdentifyingState {\n\tcode: NetworkingStatusCode.Identifying;\n\tconnectionOptions: ConnectionOptions;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when opening a UDP connection to the IP and port provided\n * by Discord, as well as performing IP discovery.\n */\nexport interface NetworkingUdpHandshakingState {\n\tcode: NetworkingStatusCode.UdpHandshaking;\n\tconnectionData: Pick;\n\tconnectionOptions: ConnectionOptions;\n\tudp: VoiceUDPSocket;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when selecting an encryption protocol for audio packets.\n */\nexport interface NetworkingSelectingProtocolState {\n\tcode: NetworkingStatusCode.SelectingProtocol;\n\tconnectionData: Pick;\n\tconnectionOptions: ConnectionOptions;\n\tudp: VoiceUDPSocket;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when it has a fully established connection to a Discord\n * voice server.\n */\nexport interface NetworkingReadyState {\n\tcode: NetworkingStatusCode.Ready;\n\tconnectionData: ConnectionData;\n\tconnectionOptions: ConnectionOptions;\n\tdave?: DAVESession | undefined;\n\tpreparedPacket?: Buffer | undefined;\n\tudp: VoiceUDPSocket;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when its connection has been dropped unexpectedly, and it\n * is attempting to resume an existing session.\n */\nexport interface NetworkingResumingState {\n\tcode: NetworkingStatusCode.Resuming;\n\tconnectionData: ConnectionData;\n\tconnectionOptions: ConnectionOptions;\n\tdave?: DAVESession | undefined;\n\tpreparedPacket?: Buffer | undefined;\n\tudp: VoiceUDPSocket;\n\tws: VoiceWebSocket;\n}\n\n/**\n * The state that a Networking instance will be in when it has been destroyed. It cannot be recovered from this\n * state.\n */\nexport interface NetworkingClosedState {\n\tcode: NetworkingStatusCode.Closed;\n}\n\n/**\n * The various states that a networking instance can be in.\n */\nexport type NetworkingState =\n\t| NetworkingClosedState\n\t| NetworkingIdentifyingState\n\t| NetworkingOpeningWsState\n\t| NetworkingReadyState\n\t| NetworkingResumingState\n\t| NetworkingSelectingProtocolState\n\t| NetworkingUdpHandshakingState;\n\n/**\n * Details required to connect to the Discord voice gateway. These details\n * are first received on the main bot gateway, in the form of VOICE_SERVER_UPDATE\n * and VOICE_STATE_UPDATE packets.\n */\nexport interface ConnectionOptions {\n\tchannelId: string;\n\tendpoint: string;\n\tserverId: string;\n\tsessionId: string;\n\ttoken: string;\n\tuserId: string;\n}\n\n/**\n * Information about the current connection, e.g. which encryption mode is to be used on\n * the connection, timing information for playback of streams.\n */\nexport interface ConnectionData {\n\tconnectedClients: Set;\n\tencryptionMode: string;\n\tnonce: number;\n\tnonceBuffer: Buffer;\n\tpacketsPlayed: number;\n\tsecretKey: Uint8Array;\n\tsequence: number;\n\tspeaking: boolean;\n\tssrc: number;\n\ttimestamp: number;\n}\n\n/**\n * Options for networking that dictate behavior.\n */\nexport interface NetworkingOptions {\n\tdaveEncryption?: boolean | undefined;\n\tdebug?: boolean | undefined;\n\tdecryptionFailureTolerance?: number | undefined;\n}\n\n/**\n * An empty buffer that is reused in packet encryption by many different networking instances.\n */\nconst nonce = Buffer.alloc(24);\n\nexport interface Networking extends EventEmitter {\n\t/**\n\t * Debug event for Networking.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'debug', listener: (message: string) => void): this;\n\ton(event: 'error', listener: (error: Error) => void): this;\n\ton(event: 'stateChange', listener: (oldState: NetworkingState, newState: NetworkingState) => void): this;\n\ton(event: 'close', listener: (code: number) => void): this;\n\ton(event: 'transitioned', listener: (transitionId: number) => void): this;\n}\n\n/**\n * Stringifies a NetworkingState.\n *\n * @param state - The state to stringify\n */\nfunction stringifyState(state: NetworkingState) {\n\treturn JSON.stringify({\n\t\t...state,\n\t\tws: Reflect.has(state, 'ws'),\n\t\tudp: Reflect.has(state, 'udp'),\n\t});\n}\n\n/**\n * Chooses an encryption mode from a list of given options. Chooses the most preferred option.\n *\n * @param options - The available encryption options\n */\nfunction chooseEncryptionMode(options: VoiceEncryptionMode[]): VoiceEncryptionMode {\n\tconst option = options.find((option) => SUPPORTED_ENCRYPTION_MODES.includes(option));\n\tif (!option) {\n\t\t// This should only ever happen if the gateway does not give us any encryption modes we support.\n\t\tthrow new Error(`No compatible encryption modes. Available include: ${options.join(', ')}`);\n\t}\n\n\treturn option;\n}\n\n/**\n * Returns a random number that is in the range of n bits.\n *\n * @param numberOfBits - The number of bits\n */\nfunction randomNBit(numberOfBits: number) {\n\treturn Math.floor(Math.random() * 2 ** numberOfBits);\n}\n\n/**\n * Manages the networking required to maintain a voice connection and dispatch audio packets\n */\nexport class Networking extends EventEmitter {\n\tprivate _state: NetworkingState;\n\n\t/**\n\t * The debug logger function, if debugging is enabled.\n\t */\n\tprivate readonly debug: ((message: string) => void) | null;\n\n\t/**\n\t * The options used to create this Networking instance.\n\t */\n\tprivate readonly options: NetworkingOptions;\n\n\t/**\n\t * Creates a new Networking instance.\n\t */\n\tpublic constructor(connectionOptions: ConnectionOptions, options: NetworkingOptions) {\n\t\tsuper();\n\n\t\tthis.onWsOpen = this.onWsOpen.bind(this);\n\t\tthis.onChildError = this.onChildError.bind(this);\n\t\tthis.onWsPacket = this.onWsPacket.bind(this);\n\t\tthis.onWsBinary = this.onWsBinary.bind(this);\n\t\tthis.onWsClose = this.onWsClose.bind(this);\n\t\tthis.onWsDebug = this.onWsDebug.bind(this);\n\t\tthis.onUdpDebug = this.onUdpDebug.bind(this);\n\t\tthis.onUdpClose = this.onUdpClose.bind(this);\n\t\tthis.onDaveDebug = this.onDaveDebug.bind(this);\n\t\tthis.onDaveKeyPackage = this.onDaveKeyPackage.bind(this);\n\t\tthis.onDaveInvalidateTransition = this.onDaveInvalidateTransition.bind(this);\n\n\t\tthis.debug = options?.debug ? (message: string) => this.emit('debug', message) : null;\n\n\t\tthis._state = {\n\t\t\tcode: NetworkingStatusCode.OpeningWs,\n\t\t\tws: this.createWebSocket(connectionOptions.endpoint),\n\t\t\tconnectionOptions,\n\t\t};\n\t\tthis.options = options;\n\t}\n\n\t/**\n\t * Destroys the Networking instance, transitioning it into the Closed state.\n\t */\n\tpublic destroy() {\n\t\tthis.state = {\n\t\t\tcode: NetworkingStatusCode.Closed,\n\t\t};\n\t}\n\n\t/**\n\t * The current state of the networking instance.\n\t *\n\t * @remarks\n\t * The setter will perform clean-up operations where necessary.\n\t */\n\tpublic get state(): NetworkingState {\n\t\treturn this._state;\n\t}\n\n\tpublic set state(newState: NetworkingState) {\n\t\tconst oldWs = Reflect.get(this._state, 'ws') as VoiceWebSocket | undefined;\n\t\tconst newWs = Reflect.get(newState, 'ws') as VoiceWebSocket | undefined;\n\t\tif (oldWs && oldWs !== newWs) {\n\t\t\t// The old WebSocket is being freed - remove all handlers from it\n\t\t\toldWs.off('debug', this.onWsDebug);\n\t\t\toldWs.on('error', noop);\n\t\t\toldWs.off('error', this.onChildError);\n\t\t\toldWs.off('open', this.onWsOpen);\n\t\t\toldWs.off('packet', this.onWsPacket);\n\t\t\toldWs.off('binary', this.onWsBinary);\n\t\t\toldWs.off('close', this.onWsClose);\n\t\t\toldWs.destroy();\n\t\t}\n\n\t\tconst oldUdp = Reflect.get(this._state, 'udp') as VoiceUDPSocket | undefined;\n\t\tconst newUdp = Reflect.get(newState, 'udp') as VoiceUDPSocket | undefined;\n\n\t\tif (oldUdp && oldUdp !== newUdp) {\n\t\t\toldUdp.on('error', noop);\n\t\t\toldUdp.off('error', this.onChildError);\n\t\t\toldUdp.off('close', this.onUdpClose);\n\t\t\toldUdp.off('debug', this.onUdpDebug);\n\t\t\toldUdp.destroy();\n\t\t}\n\n\t\tconst oldDave = Reflect.get(this._state, 'dave') as DAVESession | undefined;\n\t\tconst newDave = Reflect.get(newState, 'dave') as DAVESession | undefined;\n\n\t\tif (oldDave && oldDave !== newDave) {\n\t\t\toldDave.off('error', this.onChildError);\n\t\t\toldDave.off('debug', this.onDaveDebug);\n\t\t\toldDave.off('keyPackage', this.onDaveKeyPackage);\n\t\t\toldDave.off('invalidateTransition', this.onDaveInvalidateTransition);\n\t\t\toldDave.destroy();\n\t\t}\n\n\t\tconst oldState = this._state;\n\t\tthis._state = newState;\n\t\tthis.emit('stateChange', oldState, newState);\n\n\t\tthis.debug?.(`state change:\\nfrom ${stringifyState(oldState)}\\nto ${stringifyState(newState)}`);\n\t}\n\n\t/**\n\t * Creates a new WebSocket to a Discord Voice gateway.\n\t *\n\t * @param endpoint - The endpoint to connect to\n\t * @param lastSequence - The last sequence to set for this WebSocket\n\t */\n\tprivate createWebSocket(endpoint: string, lastSequence?: number) {\n\t\tconst ws = new VoiceWebSocket(`wss://${endpoint}?v=8`, Boolean(this.debug));\n\n\t\tif (lastSequence !== undefined) {\n\t\t\tws.sequence = lastSequence;\n\t\t}\n\n\t\tws.on('error', this.onChildError);\n\t\tws.once('open', this.onWsOpen);\n\t\tws.on('packet', this.onWsPacket);\n\t\tws.on('binary', this.onWsBinary);\n\t\tws.once('close', this.onWsClose);\n\t\tws.on('debug', this.onWsDebug);\n\n\t\treturn ws;\n\t}\n\n\t/**\n\t * Creates a new DAVE session for this voice connection if we can create one.\n\t *\n\t * @param protocolVersion - The protocol version to use\n\t */\n\tprivate createDaveSession(protocolVersion: number) {\n\t\tif (\n\t\t\tthis.options.daveEncryption === false ||\n\t\t\t(this.state.code !== NetworkingStatusCode.SelectingProtocol &&\n\t\t\t\tthis.state.code !== NetworkingStatusCode.Ready &&\n\t\t\t\tthis.state.code !== NetworkingStatusCode.Resuming)\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst session = new DAVESession(\n\t\t\tprotocolVersion,\n\t\t\tthis.state.connectionOptions.userId,\n\t\t\tthis.state.connectionOptions.channelId,\n\t\t\t{\n\t\t\t\tdecryptionFailureTolerance: this.options.decryptionFailureTolerance,\n\t\t\t},\n\t\t);\n\n\t\tsession.on('error', this.onChildError);\n\t\tsession.on('debug', this.onDaveDebug);\n\t\tsession.on('keyPackage', this.onDaveKeyPackage);\n\t\tsession.on('invalidateTransition', this.onDaveInvalidateTransition);\n\t\tsession.reinit();\n\n\t\treturn session;\n\t}\n\n\t/**\n\t * Propagates errors from the children VoiceWebSocket, VoiceUDPSocket and DAVESession.\n\t *\n\t * @param error - The error that was emitted by a child\n\t */\n\tprivate onChildError(error: Error) {\n\t\tthis.emit('error', error);\n\t}\n\n\t/**\n\t * Called when the WebSocket opens. Depending on the state that the instance is in,\n\t * it will either identify with a new session, or it will attempt to resume an existing session.\n\t */\n\tprivate onWsOpen() {\n\t\tif (this.state.code === NetworkingStatusCode.OpeningWs) {\n\t\t\tthis.state.ws.sendPacket({\n\t\t\t\top: VoiceOpcodes.Identify,\n\t\t\t\td: {\n\t\t\t\t\tserver_id: this.state.connectionOptions.serverId,\n\t\t\t\t\tuser_id: this.state.connectionOptions.userId,\n\t\t\t\t\tsession_id: this.state.connectionOptions.sessionId,\n\t\t\t\t\ttoken: this.state.connectionOptions.token,\n\t\t\t\t\tmax_dave_protocol_version: this.options.daveEncryption === false ? 0 : getMaxProtocolVersion(),\n\t\t\t\t},\n\t\t\t});\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.Identifying,\n\t\t\t};\n\t\t} else if (this.state.code === NetworkingStatusCode.Resuming) {\n\t\t\tthis.state.ws.sendPacket({\n\t\t\t\top: VoiceOpcodes.Resume,\n\t\t\t\td: {\n\t\t\t\t\tserver_id: this.state.connectionOptions.serverId,\n\t\t\t\t\tsession_id: this.state.connectionOptions.sessionId,\n\t\t\t\t\ttoken: this.state.connectionOptions.token,\n\t\t\t\t\tseq_ack: this.state.ws.sequence,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Called when the WebSocket closes. Based on the reason for closing (given by the code parameter),\n\t * the instance will either attempt to resume, or enter the closed state and emit a 'close' event\n\t * with the close code, allowing the user to decide whether or not they would like to reconnect.\n\t *\n\t * @param code - The close code\n\t */\n\tprivate onWsClose({ code }: CloseEvent) {\n\t\tconst canResume = code === 4_015 || code < 4_000;\n\t\tif (canResume && this.state.code === NetworkingStatusCode.Ready) {\n\t\t\tconst lastSequence = this.state.ws.sequence;\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.Resuming,\n\t\t\t\tws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence),\n\t\t\t};\n\t\t} else if (this.state.code !== NetworkingStatusCode.Closed) {\n\t\t\tthis.destroy();\n\t\t\tthis.emit('close', code);\n\t\t}\n\t}\n\n\t/**\n\t * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord.\n\t */\n\tprivate onUdpClose() {\n\t\tif (this.state.code === NetworkingStatusCode.Ready) {\n\t\t\tconst lastSequence = this.state.ws.sequence;\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.Resuming,\n\t\t\t\tws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence),\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Called when a packet is received on the connection's WebSocket.\n\t *\n\t * @param packet - The received packet\n\t */\n\tprivate onWsPacket(packet: VoiceReceivePayload) {\n\t\tif (packet.op === VoiceOpcodes.Hello && this.state.code !== NetworkingStatusCode.Closed) {\n\t\t\tthis.state.ws.setHeartbeatInterval(packet.d.heartbeat_interval);\n\t\t} else if (packet.op === VoiceOpcodes.Ready && this.state.code === NetworkingStatusCode.Identifying) {\n\t\t\tconst { ip, port, ssrc, modes } = packet.d;\n\n\t\t\tconst udp = new VoiceUDPSocket({ ip, port });\n\t\t\tudp.on('error', this.onChildError);\n\t\t\tudp.on('debug', this.onUdpDebug);\n\t\t\tudp.once('close', this.onUdpClose);\n\t\t\tudp\n\t\t\t\t.performIPDiscovery(ssrc)\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t\t\t.then((localConfig) => {\n\t\t\t\t\tif (this.state.code !== NetworkingStatusCode.UdpHandshaking) return;\n\t\t\t\t\tthis.state.ws.sendPacket({\n\t\t\t\t\t\top: VoiceOpcodes.SelectProtocol,\n\t\t\t\t\t\td: {\n\t\t\t\t\t\t\tprotocol: 'udp',\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\taddress: localConfig.ip,\n\t\t\t\t\t\t\t\tport: localConfig.port,\n\t\t\t\t\t\t\t\tmode: chooseEncryptionMode(modes),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tthis.state = {\n\t\t\t\t\t\t...this.state,\n\t\t\t\t\t\tcode: NetworkingStatusCode.SelectingProtocol,\n\t\t\t\t\t};\n\t\t\t\t})\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then, promise/prefer-await-to-callbacks\n\t\t\t\t.catch((error: Error) => this.emit('error', error));\n\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.UdpHandshaking,\n\t\t\t\tudp,\n\t\t\t\tconnectionData: {\n\t\t\t\t\tssrc,\n\t\t\t\t\tconnectedClients: new Set(),\n\t\t\t\t},\n\t\t\t};\n\t\t} else if (\n\t\t\tpacket.op === VoiceOpcodes.SessionDescription &&\n\t\t\tthis.state.code === NetworkingStatusCode.SelectingProtocol\n\t\t) {\n\t\t\tconst { mode: encryptionMode, secret_key: secretKey, dave_protocol_version: daveProtocolVersion } = packet.d;\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.Ready,\n\t\t\t\tdave: this.createDaveSession(daveProtocolVersion),\n\t\t\t\tconnectionData: {\n\t\t\t\t\t...this.state.connectionData,\n\t\t\t\t\tencryptionMode,\n\t\t\t\t\tsecretKey: new Uint8Array(secretKey),\n\t\t\t\t\tsequence: randomNBit(16),\n\t\t\t\t\ttimestamp: randomNBit(32),\n\t\t\t\t\tnonce: 0,\n\t\t\t\t\tnonceBuffer: encryptionMode === 'aead_aes256_gcm_rtpsize' ? Buffer.alloc(12) : Buffer.alloc(24),\n\t\t\t\t\tspeaking: false,\n\t\t\t\t\tpacketsPlayed: 0,\n\t\t\t\t},\n\t\t\t};\n\t\t} else if (packet.op === VoiceOpcodes.Resumed && this.state.code === NetworkingStatusCode.Resuming) {\n\t\t\tthis.state = {\n\t\t\t\t...this.state,\n\t\t\t\tcode: NetworkingStatusCode.Ready,\n\t\t\t};\n\t\t\tthis.state.connectionData.speaking = false;\n\t\t} else if (\n\t\t\t(packet.op === VoiceOpcodes.ClientsConnect || packet.op === VoiceOpcodes.ClientDisconnect) &&\n\t\t\t(this.state.code === NetworkingStatusCode.Ready ||\n\t\t\t\tthis.state.code === NetworkingStatusCode.UdpHandshaking ||\n\t\t\t\tthis.state.code === NetworkingStatusCode.SelectingProtocol ||\n\t\t\t\tthis.state.code === NetworkingStatusCode.Resuming)\n\t\t) {\n\t\t\tconst { connectionData } = this.state;\n\t\t\tif (packet.op === VoiceOpcodes.ClientsConnect)\n\t\t\t\tfor (const id of packet.d.user_ids) connectionData.connectedClients.add(id);\n\t\t\telse {\n\t\t\t\tconnectionData.connectedClients.delete(packet.d.user_id);\n\t\t\t}\n\t\t} else if (\n\t\t\t(this.state.code === NetworkingStatusCode.Ready || this.state.code === NetworkingStatusCode.Resuming) &&\n\t\t\tthis.state.dave\n\t\t) {\n\t\t\tif (packet.op === VoiceOpcodes.DavePrepareTransition) {\n\t\t\t\tconst sendReady = this.state.dave.prepareTransition(packet.d);\n\t\t\t\tif (sendReady)\n\t\t\t\t\tthis.state.ws.sendPacket({\n\t\t\t\t\t\top: VoiceOpcodes.DaveTransitionReady,\n\t\t\t\t\t\td: { transition_id: packet.d.transition_id },\n\t\t\t\t\t});\n\t\t\t\tif (packet.d.transition_id === 0) {\n\t\t\t\t\tthis.emit('transitioned', 0);\n\t\t\t\t}\n\t\t\t} else if (packet.op === VoiceOpcodes.DaveExecuteTransition) {\n\t\t\t\tconst transitioned = this.state.dave.executeTransition(packet.d.transition_id);\n\t\t\t\tif (transitioned) this.emit('transitioned', packet.d.transition_id);\n\t\t\t} else if (packet.op === VoiceOpcodes.DavePrepareEpoch) this.state.dave.prepareEpoch(packet.d);\n\t\t}\n\t}\n\n\t/**\n\t * Called when a binary message is received on the connection's WebSocket.\n\t *\n\t * @param message - The received message\n\t */\n\tprivate onWsBinary(message: BinaryWebSocketMessage) {\n\t\tif (this.state.code === NetworkingStatusCode.Ready && this.state.dave) {\n\t\t\tif (message.op === VoiceOpcodes.DaveMlsExternalSender) {\n\t\t\t\tthis.state.dave.setExternalSender(message.payload);\n\t\t\t} else if (message.op === VoiceOpcodes.DaveMlsProposals) {\n\t\t\t\tconst payload = this.state.dave.processProposals(message.payload, this.state.connectionData.connectedClients);\n\t\t\t\tif (payload) this.state.ws.sendBinaryMessage(VoiceOpcodes.DaveMlsCommitWelcome, payload);\n\t\t\t} else if (message.op === VoiceOpcodes.DaveMlsAnnounceCommitTransition) {\n\t\t\t\tconst { transitionId, success } = this.state.dave.processCommit(message.payload);\n\t\t\t\tif (success) {\n\t\t\t\t\tif (transitionId === 0) this.emit('transitioned', transitionId);\n\t\t\t\t\telse\n\t\t\t\t\t\tthis.state.ws.sendPacket({\n\t\t\t\t\t\t\top: VoiceOpcodes.DaveTransitionReady,\n\t\t\t\t\t\t\td: { transition_id: transitionId },\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else if (message.op === VoiceOpcodes.DaveMlsWelcome) {\n\t\t\t\tconst { transitionId, success } = this.state.dave.processWelcome(message.payload);\n\t\t\t\tif (success) {\n\t\t\t\t\tif (transitionId === 0) this.emit('transitioned', transitionId);\n\t\t\t\t\telse\n\t\t\t\t\t\tthis.state.ws.sendPacket({\n\t\t\t\t\t\t\top: VoiceOpcodes.DaveTransitionReady,\n\t\t\t\t\t\t\td: { transition_id: transitionId },\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Called when a new key package is ready to be sent to the voice server.\n\t *\n\t * @param keyPackage - The new key package\n\t */\n\tprivate onDaveKeyPackage(keyPackage: Buffer) {\n\t\tif (this.state.code === NetworkingStatusCode.SelectingProtocol || this.state.code === NetworkingStatusCode.Ready)\n\t\t\tthis.state.ws.sendBinaryMessage(VoiceOpcodes.DaveMlsKeyPackage, keyPackage);\n\t}\n\n\t/**\n\t * Called when the DAVE session wants to invalidate their transition and re-initialize.\n\t *\n\t * @param transitionId - The transition to invalidate\n\t */\n\tprivate onDaveInvalidateTransition(transitionId: number) {\n\t\tif (this.state.code === NetworkingStatusCode.SelectingProtocol || this.state.code === NetworkingStatusCode.Ready)\n\t\t\tthis.state.ws.sendPacket({\n\t\t\t\top: VoiceOpcodes.DaveMlsInvalidCommitWelcome,\n\t\t\t\td: { transition_id: transitionId },\n\t\t\t});\n\t}\n\n\t/**\n\t * Propagates debug messages from the child WebSocket.\n\t *\n\t * @param message - The emitted debug message\n\t */\n\tprivate onWsDebug(message: string) {\n\t\tthis.debug?.(`[WS] ${message}`);\n\t}\n\n\t/**\n\t * Propagates debug messages from the child UDPSocket.\n\t *\n\t * @param message - The emitted debug message\n\t */\n\tprivate onUdpDebug(message: string) {\n\t\tthis.debug?.(`[UDP] ${message}`);\n\t}\n\n\t/**\n\t * Propagates debug messages from the child DAVESession.\n\t *\n\t * @param message - The emitted debug message\n\t */\n\tprivate onDaveDebug(message: string) {\n\t\tthis.debug?.(`[DAVE] ${message}`);\n\t}\n\n\t/**\n\t * Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it.\n\t * It will be stored within the instance, and can be played by dispatchAudio()\n\t *\n\t * @remarks\n\t * Calling this method while there is already a prepared audio packet that has not yet been dispatched\n\t * will overwrite the existing audio packet. This should be avoided.\n\t * @param opusPacket - The Opus packet to encrypt\n\t * @returns The audio packet that was prepared\n\t */\n\tpublic prepareAudioPacket(opusPacket: Buffer) {\n\t\tconst state = this.state;\n\t\tif (state.code !== NetworkingStatusCode.Ready) return;\n\t\tstate.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData, state.dave);\n\t\treturn state.preparedPacket;\n\t}\n\n\t/**\n\t * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet\n\t * is consumed and cannot be dispatched again.\n\t */\n\tpublic dispatchAudio() {\n\t\tconst state = this.state;\n\t\tif (state.code !== NetworkingStatusCode.Ready) return false;\n\t\tif (state.preparedPacket !== undefined) {\n\t\t\tthis.playAudioPacket(state.preparedPacket);\n\t\t\tstate.preparedPacket = undefined;\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Plays an audio packet, updating timing metadata used for playback.\n\t *\n\t * @param audioPacket - The audio packet to play\n\t */\n\tprivate playAudioPacket(audioPacket: Buffer) {\n\t\tconst state = this.state;\n\t\tif (state.code !== NetworkingStatusCode.Ready) return;\n\t\tconst { connectionData } = state;\n\t\tconnectionData.packetsPlayed++;\n\t\tconnectionData.sequence++;\n\t\tconnectionData.timestamp += TIMESTAMP_INC;\n\t\tif (connectionData.sequence >= 2 ** 16) connectionData.sequence = 0;\n\t\tif (connectionData.timestamp >= 2 ** 32) connectionData.timestamp = 0;\n\t\tthis.setSpeaking(true);\n\t\tstate.udp.send(audioPacket);\n\t}\n\n\t/**\n\t * Sends a packet to the voice gateway indicating that the client has start/stopped sending\n\t * audio.\n\t *\n\t * @param speaking - Whether or not the client should be shown as speaking\n\t */\n\tpublic setSpeaking(speaking: boolean) {\n\t\tconst state = this.state;\n\t\tif (state.code !== NetworkingStatusCode.Ready) return;\n\t\tif (state.connectionData.speaking === speaking) return;\n\t\tstate.connectionData.speaking = speaking;\n\t\tstate.ws.sendPacket({\n\t\t\top: VoiceOpcodes.Speaking,\n\t\t\td: {\n\t\t\t\tspeaking: (speaking ? 1 : 0) as VoiceSpeakingFlags,\n\t\t\t\tdelay: 0,\n\t\t\t\tssrc: state.connectionData.ssrc,\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Creates a new audio packet from an Opus packet. This involves encrypting the packet,\n\t * then prepending a header that includes metadata.\n\t *\n\t * @param opusPacket - The Opus packet to prepare\n\t * @param connectionData - The current connection data of the instance\n\t * @param daveSession - The DAVE session to use for encryption\n\t */\n\tprivate createAudioPacket(opusPacket: Buffer, connectionData: ConnectionData, daveSession?: DAVESession) {\n\t\tconst rtpHeader = Buffer.alloc(12);\n\t\trtpHeader[0] = 0x80;\n\t\trtpHeader[1] = RTP_OPUS_PAYLOAD_TYPE;\n\n\t\tconst { sequence, timestamp, ssrc } = connectionData;\n\n\t\trtpHeader.writeUIntBE(sequence, 2, 2);\n\t\trtpHeader.writeUIntBE(timestamp, 4, 4);\n\t\trtpHeader.writeUIntBE(ssrc, 8, 4);\n\n\t\trtpHeader.copy(nonce, 0, 0, 12);\n\t\treturn Buffer.concat([rtpHeader, ...this.encryptOpusPacket(opusPacket, connectionData, rtpHeader, daveSession)]);\n\t}\n\n\t/**\n\t * Encrypts an Opus packet using the format agreed upon by the instance and Discord.\n\t *\n\t * @param opusPacket - The Opus packet to encrypt\n\t * @param connectionData - The current connection data of the instance\n\t * @param daveSession - The DAVE session to use for encryption\n\t */\n\tprivate encryptOpusPacket(\n\t\topusPacket: Buffer,\n\t\tconnectionData: ConnectionData,\n\t\tadditionalData: Buffer,\n\t\tdaveSession?: DAVESession,\n\t) {\n\t\tconst { secretKey, encryptionMode } = connectionData;\n\n\t\tconst packet = daveSession?.encrypt(opusPacket) ?? opusPacket;\n\n\t\t// Both supported encryption methods want the nonce to be an incremental integer\n\t\tconnectionData.nonce++;\n\t\tif (connectionData.nonce > MAX_NONCE_SIZE) connectionData.nonce = 0;\n\t\tconnectionData.nonceBuffer.writeUInt32BE(connectionData.nonce, 0);\n\n\t\t// 4 extra bytes of padding on the end of the encrypted packet\n\t\tconst noncePadding = connectionData.nonceBuffer.subarray(0, 4);\n\n\t\tlet encrypted;\n\t\tswitch (encryptionMode) {\n\t\t\tcase 'aead_aes256_gcm_rtpsize': {\n\t\t\t\tconst cipher = crypto.createCipheriv('aes-256-gcm', secretKey, connectionData.nonceBuffer);\n\t\t\t\tcipher.setAAD(additionalData);\n\n\t\t\t\tencrypted = Buffer.concat([cipher.update(packet), cipher.final(), cipher.getAuthTag()]);\n\n\t\t\t\treturn [encrypted, noncePadding];\n\t\t\t}\n\n\t\t\tcase 'aead_xchacha20_poly1305_rtpsize': {\n\t\t\t\tencrypted = secretbox.methods.crypto_aead_xchacha20poly1305_ietf_encrypt(\n\t\t\t\t\tpacket,\n\t\t\t\t\tadditionalData,\n\t\t\t\t\tconnectionData.nonceBuffer,\n\t\t\t\t\tsecretKey,\n\t\t\t\t);\n\n\t\t\t\treturn [encrypted, noncePadding];\n\t\t\t}\n\n\t\t\tdefault: {\n\t\t\t\t// This should never happen. Our encryption mode is chosen from a list given to us by the gateway and checked with the ones we support.\n\t\t\t\tthrow new RangeError(`Unsupported encryption method: ${encryptionMode}`);\n\t\t\t}\n\t\t}\n\t}\n}\n","import { Buffer } from 'node:buffer';\n\ninterface Methods {\n\tcrypto_aead_xchacha20poly1305_ietf_decrypt(\n\t\tcipherText: Buffer,\n\t\tadditionalData: Buffer,\n\t\tnonce: Buffer,\n\t\tkey: Uint8Array,\n\t): Buffer;\n\tcrypto_aead_xchacha20poly1305_ietf_encrypt(\n\t\tplaintext: Buffer,\n\t\tadditionalData: Buffer,\n\t\tnonce: Buffer,\n\t\tkey: Uint8Array,\n\t): Buffer;\n}\n\nconst libs = {\n\t'sodium-native': (sodium: any): Methods => ({\n\t\tcrypto_aead_xchacha20poly1305_ietf_decrypt: (\n\t\t\tcipherText: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => {\n\t\t\tconst message = Buffer.alloc(cipherText.length - sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES);\n\t\t\tsodium.crypto_aead_xchacha20poly1305_ietf_decrypt(message, null, cipherText, additionalData, nonce, key);\n\t\t\treturn message;\n\t\t},\n\t\tcrypto_aead_xchacha20poly1305_ietf_encrypt: (\n\t\t\tplaintext: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => {\n\t\t\tconst cipherText = Buffer.alloc(plaintext.length + sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES);\n\t\t\tsodium.crypto_aead_xchacha20poly1305_ietf_encrypt(cipherText, plaintext, additionalData, null, nonce, key);\n\t\t\treturn cipherText;\n\t\t},\n\t}),\n\tsodium: (sodium: any): Methods => ({\n\t\tcrypto_aead_xchacha20poly1305_ietf_decrypt: (\n\t\t\tcipherText: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => sodium.api.crypto_aead_xchacha20poly1305_ietf_decrypt(cipherText, additionalData, null, nonce, key),\n\t\tcrypto_aead_xchacha20poly1305_ietf_encrypt: (\n\t\t\tplaintext: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => sodium.api.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce, key),\n\t}),\n\t'libsodium-wrappers': (sodium: any): Methods => ({\n\t\tcrypto_aead_xchacha20poly1305_ietf_decrypt: (\n\t\t\tcipherText: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, cipherText, additionalData, nonce, key),\n\t\tcrypto_aead_xchacha20poly1305_ietf_encrypt: (\n\t\t\tplaintext: Buffer,\n\t\t\tadditionalData: Buffer,\n\t\t\tnonce: Buffer,\n\t\t\tkey: Uint8Array,\n\t\t) => sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce, key),\n\t}),\n\t'@stablelib/xchacha20poly1305': (stablelib: any): Methods => ({\n\t\tcrypto_aead_xchacha20poly1305_ietf_decrypt(plaintext, additionalData, nonce, key) {\n\t\t\tconst crypto = new stablelib.XChaCha20Poly1305(key);\n\t\t\treturn crypto.open(nonce, plaintext, additionalData);\n\t\t},\n\t\tcrypto_aead_xchacha20poly1305_ietf_encrypt(cipherText, additionalData, nonce, key) {\n\t\t\tconst crypto = new stablelib.XChaCha20Poly1305(key);\n\t\t\treturn crypto.seal(nonce, cipherText, additionalData);\n\t\t},\n\t}),\n\t'@noble/ciphers/chacha.js': (noble: any): Methods => ({\n\t\tcrypto_aead_xchacha20poly1305_ietf_decrypt(cipherText, additionalData, nonce, key) {\n\t\t\tconst chacha = noble.xchacha20poly1305(key, nonce, additionalData);\n\t\t\treturn chacha.decrypt(cipherText);\n\t\t},\n\t\tcrypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, nonce, key) {\n\t\t\tconst chacha = noble.xchacha20poly1305(key, nonce, additionalData);\n\t\t\treturn chacha.encrypt(plaintext);\n\t\t},\n\t}),\n} as const;\n\nconst fallbackError = () => {\n\tthrow new Error(\n\t\t`Cannot play audio as no valid encryption package is installed.\n- Install one of:\n - sodium\n - libsodium-wrappers\n - @stablelib/xchacha20poly1305\n - @noble/ciphers.\n- Use the generateDependencyReport() function for more information.\\n`,\n\t);\n};\n\nconst methods: Methods = {\n\tcrypto_aead_xchacha20poly1305_ietf_encrypt: fallbackError,\n\tcrypto_aead_xchacha20poly1305_ietf_decrypt: fallbackError,\n};\n\n// eslint-disable-next-line no-async-promise-executor\nexport const secretboxLoadPromise = new Promise(async (resolve) => {\n\tfor (const libName of Object.keys(libs) as (keyof typeof libs)[]) {\n\t\ttry {\n\t\t\tconst lib = await import(libName);\n\n\t\t\tif (libName === 'libsodium-wrappers' && lib.ready) {\n\t\t\t\tawait lib.ready;\n\t\t\t}\n\n\t\t\tObject.assign(methods, libs[libName](lib));\n\n\t\t\tbreak;\n\t\t} catch {}\n\t}\n\n\tresolve();\n});\n\nexport { methods };\n","export const RTP_OPUS_PAYLOAD_TYPE = 0x78;\n","export const noop = () => {};\n","import { Buffer } from 'node:buffer';\nimport { EventEmitter } from 'node:events';\nimport Davey from '@snazzah/davey';\nimport type { VoiceDavePrepareEpochData, VoiceDavePrepareTransitionData } from 'discord-api-types/voice/v8';\nimport { SILENCE_FRAME } from '../audio/AudioPlayer';\n\ninterface SessionMethods {\n\tcanPassthrough(userId: string): boolean;\n\tdecrypt(userId: string, mediaType: 0 | 1, packet: Buffer): Buffer;\n\tencryptOpus(packet: Buffer): Buffer;\n\tgetSerializedKeyPackage(): Buffer;\n\tgetVerificationCode(userId: string): Promise;\n\tprocessCommit(commit: Buffer): void;\n\tprocessProposals(optype: 0 | 1, proposals: Buffer, recognizedUserIds?: string[]): ProposalsResult;\n\tprocessWelcome(welcome: Buffer): void;\n\tready: boolean;\n\treinit(protocolVersion: number, userId: string, channelId: string): void;\n\treset(): void;\n\tsetExternalSender(externalSender: Buffer): void;\n\tsetPassthroughMode(passthrough: boolean, expiry: number): void;\n\tvoicePrivacyCode: string;\n}\n\ninterface ProposalsResult {\n\tcommit?: Buffer;\n\twelcome?: Buffer;\n}\n\n/**\n * The amount of seconds that a previous transition should be valid for.\n */\nconst TRANSITION_EXPIRY = 10;\n\n/**\n * The arbitrary amount of seconds to allow passthrough for mid-downgrade.\n * Generally, transitions last about 3 seconds maximum, but this should cover for when connections are delayed.\n */\nconst TRANSITION_EXPIRY_PENDING_DOWNGRADE = 24;\n\n/**\n * The amount of packets to allow decryption failure for until we deem the transition bad and re-initialize.\n * Usually 4 packets on a good connection may slip past when entering a new session.\n * After re-initializing, 5-24 packets may fail to decrypt after.\n */\nexport const DEFAULT_DECRYPTION_FAILURE_TOLERANCE = 36;\n\ninterface TransitionResult {\n\tsuccess: boolean;\n\ttransitionId: number;\n}\n/**\n * Options that dictate the session behavior.\n */\nexport interface DAVESessionOptions {\n\tdecryptionFailureTolerance?: number | undefined;\n}\n\n/**\n * The maximum DAVE protocol version supported.\n */\nexport function getMaxProtocolVersion(): number {\n\treturn Davey.DAVE_PROTOCOL_VERSION;\n}\n\nexport interface DAVESession extends EventEmitter {\n\ton(event: 'error', listener: (error: Error) => void): this;\n\ton(event: 'debug', listener: (message: string) => void): this;\n\ton(event: 'keyPackage', listener: (message: Buffer) => void): this;\n\ton(event: 'invalidateTransition', listener: (transitionId: number) => void): this;\n}\n\n/**\n * Manages the DAVE protocol group session.\n */\nexport class DAVESession extends EventEmitter {\n\t/**\n\t * The channel id represented by this session.\n\t */\n\tpublic channelId: string;\n\n\t/**\n\t * The user id represented by this session.\n\t */\n\tpublic userId: string;\n\n\t/**\n\t * The protocol version being used.\n\t */\n\tpublic protocolVersion: number;\n\n\t/**\n\t * The last transition id executed.\n\t */\n\tpublic lastTransitionId?: number | undefined;\n\n\t/**\n\t * The pending transitions, mapped by their ID and the protocol version.\n\t */\n\tprivate pendingTransitions = new Map();\n\n\t/**\n\t * Whether this session was downgraded previously.\n\t */\n\tprivate downgraded = false;\n\n\t/**\n\t * The amount of consecutive failures encountered when decrypting.\n\t */\n\tprivate consecutiveFailures = 0;\n\n\t/**\n\t * The amount of consecutive failures needed to attempt to recover.\n\t */\n\tprivate readonly failureTolerance: number;\n\n\t/**\n\t * Whether this session is currently re-initializing due to an invalid transition.\n\t */\n\tpublic reinitializing = false;\n\n\t/**\n\t * The underlying DAVE Session of this wrapper.\n\t */\n\tpublic session: SessionMethods | undefined;\n\n\tpublic constructor(protocolVersion: number, userId: string, channelId: string, options: DAVESessionOptions) {\n\t\tsuper();\n\n\t\tthis.protocolVersion = protocolVersion;\n\t\tthis.userId = userId;\n\t\tthis.channelId = channelId;\n\t\tthis.failureTolerance = options.decryptionFailureTolerance ?? DEFAULT_DECRYPTION_FAILURE_TOLERANCE;\n\t}\n\n\t/**\n\t * The current voice privacy code of the session. Will be `null` if there is no session.\n\t */\n\tpublic get voicePrivacyCode(): string | null {\n\t\tif (this.protocolVersion === 0 || !this.session?.voicePrivacyCode) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.session.voicePrivacyCode;\n\t}\n\n\t/**\n\t * Gets the verification code for a user in the session.\n\t *\n\t * @throws Will throw if there is not an active session or the user id provided is invalid or not in the session.\n\t */\n\tpublic async getVerificationCode(userId: string): Promise {\n\t\tif (!this.session) throw new Error('Session not available');\n\t\treturn this.session.getVerificationCode(userId);\n\t}\n\n\t/**\n\t * Re-initializes (or initializes) the underlying session.\n\t */\n\tpublic reinit() {\n\t\tif (this.protocolVersion > 0) {\n\t\t\tif (this.session) {\n\t\t\t\tthis.session.reinit(this.protocolVersion, this.userId, this.channelId);\n\t\t\t\tthis.emit('debug', `Session reinitialized for protocol version ${this.protocolVersion}`);\n\t\t\t} else {\n\t\t\t\tthis.session = new Davey.DAVESession(this.protocolVersion, this.userId, this.channelId);\n\t\t\t\tthis.emit('debug', `Session initialized for protocol version ${this.protocolVersion}`);\n\t\t\t}\n\n\t\t\tthis.emit('keyPackage', this.session!.getSerializedKeyPackage());\n\t\t} else if (this.session) {\n\t\t\tthis.session.reset();\n\t\t\tthis.session.setPassthroughMode(true, TRANSITION_EXPIRY);\n\t\t\tthis.emit('debug', 'Session reset');\n\t\t}\n\t}\n\n\t/**\n\t * Set the external sender for this session.\n\t *\n\t * @param externalSender - The external sender\n\t */\n\tpublic setExternalSender(externalSender: Buffer) {\n\t\tif (!this.session) throw new Error('No session available');\n\t\tthis.session.setExternalSender(externalSender);\n\t\tthis.emit('debug', 'Set MLS external sender');\n\t}\n\n\t/**\n\t * Prepare for a transition.\n\t *\n\t * @param data - The transition data\n\t * @returns Whether we should signal to the voice server that we are ready\n\t */\n\tpublic prepareTransition(data: VoiceDavePrepareTransitionData) {\n\t\tthis.emit('debug', `Preparing for transition (${data.transition_id}, v${data.protocol_version})`);\n\t\tthis.pendingTransitions.set(data.transition_id, data.protocol_version);\n\n\t\t// When the included transition id is 0, the transition is for (re)initialization and it can be executed immediately.\n\t\tif (data.transition_id === 0) {\n\t\t\tthis.executeTransition(data.transition_id);\n\t\t} else {\n\t\t\tif (data.protocol_version === 0) this.session?.setPassthroughMode(true, TRANSITION_EXPIRY_PENDING_DOWNGRADE);\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Execute a transition.\n\t *\n\t * @param transitionId - The transition id to execute on\n\t */\n\tpublic executeTransition(transitionId: number) {\n\t\tthis.emit('debug', `Executing transition (${transitionId})`);\n\t\tif (!this.pendingTransitions.has(transitionId)) {\n\t\t\tthis.emit('debug', `Received execute transition, but we don't have a pending transition for ${transitionId}`);\n\t\t\treturn false;\n\t\t}\n\n\t\tconst oldVersion = this.protocolVersion;\n\t\tthis.protocolVersion = this.pendingTransitions.get(transitionId)!;\n\n\t\t// Handle upgrades & defer downgrades\n\t\tif (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {\n\t\t\tthis.downgraded = true;\n\t\t\tthis.emit('debug', 'Session downgraded');\n\t\t} else if (transitionId > 0 && this.downgraded) {\n\t\t\tthis.downgraded = false;\n\t\t\tthis.session?.setPassthroughMode(true, TRANSITION_EXPIRY);\n\t\t\tthis.emit('debug', 'Session upgraded');\n\t\t}\n\n\t\t// In the future we'd want to signal to the DAVESession to transition also, but it only supports v1 at this time\n\t\tthis.reinitializing = false;\n\t\tthis.lastTransitionId = transitionId;\n\t\tthis.emit('debug', `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);\n\n\t\tthis.pendingTransitions.delete(transitionId);\n\t\treturn true;\n\t}\n\n\t/**\n\t * Prepare for a new epoch.\n\t *\n\t * @param data - The epoch data\n\t */\n\tpublic prepareEpoch(data: VoiceDavePrepareEpochData) {\n\t\tthis.emit('debug', `Preparing for epoch (${data.epoch})`);\n\t\tif (data.epoch === 1) {\n\t\t\tthis.protocolVersion = data.protocol_version;\n\t\t\tthis.reinit();\n\t\t}\n\t}\n\n\t/**\n\t * Recover from an invalid transition by re-initializing.\n\t *\n\t * @param transitionId - The transition id to invalidate\n\t */\n\tpublic recoverFromInvalidTransition(transitionId: number) {\n\t\tif (this.reinitializing) return;\n\t\tthis.emit('debug', `Invalidating transition ${transitionId}`);\n\t\tthis.reinitializing = true;\n\t\tthis.consecutiveFailures = 0;\n\t\tthis.emit('invalidateTransition', transitionId);\n\t\tthis.reinit();\n\t}\n\n\t/**\n\t * Processes proposals from the MLS group.\n\t *\n\t * @param payload - The binary message payload\n\t * @param connectedClients - The set of connected client IDs\n\t * @returns The payload to send back to the voice server, if there is one\n\t */\n\tpublic processProposals(payload: Buffer, connectedClients: Set): Buffer | undefined {\n\t\tif (!this.session) throw new Error('No session available');\n\t\tconst optype = payload.readUInt8(0) as 0 | 1;\n\t\tconst { commit, welcome } = this.session.processProposals(\n\t\t\toptype,\n\t\t\tpayload.subarray(1),\n\t\t\tArray.from(connectedClients),\n\t\t);\n\t\tthis.emit('debug', 'MLS proposals processed');\n\t\tif (!commit) return;\n\t\treturn welcome ? Buffer.concat([commit, welcome]) : commit;\n\t}\n\n\t/**\n\t * Processes a commit from the MLS group.\n\t *\n\t * @param payload - The payload\n\t * @returns The transaction id and whether it was successful\n\t */\n\tpublic processCommit(payload: Buffer): TransitionResult {\n\t\tif (!this.session) throw new Error('No session available');\n\t\tconst transitionId = payload.readUInt16BE(0);\n\t\ttry {\n\t\t\tthis.session.processCommit(payload.subarray(2));\n\t\t\tif (transitionId === 0) {\n\t\t\t\tthis.reinitializing = false;\n\t\t\t\tthis.lastTransitionId = transitionId;\n\t\t\t} else {\n\t\t\t\tthis.pendingTransitions.set(transitionId, this.protocolVersion);\n\t\t\t}\n\n\t\t\tthis.emit('debug', `MLS commit processed (transition id: ${transitionId})`);\n\t\t\treturn { transitionId, success: true };\n\t\t} catch (error) {\n\t\t\tthis.emit('debug', `MLS commit errored from transition ${transitionId}: ${error}`);\n\t\t\tthis.recoverFromInvalidTransition(transitionId);\n\t\t\treturn { transitionId, success: false };\n\t\t}\n\t}\n\n\t/**\n\t * Processes a welcome from the MLS group.\n\t *\n\t * @param payload - The payload\n\t * @returns The transaction id and whether it was successful\n\t */\n\tpublic processWelcome(payload: Buffer): TransitionResult {\n\t\tif (!this.session) throw new Error('No session available');\n\t\tconst transitionId = payload.readUInt16BE(0);\n\t\ttry {\n\t\t\tthis.session.processWelcome(payload.subarray(2));\n\t\t\tif (transitionId === 0) {\n\t\t\t\tthis.reinitializing = false;\n\t\t\t\tthis.lastTransitionId = transitionId;\n\t\t\t} else {\n\t\t\t\tthis.pendingTransitions.set(transitionId, this.protocolVersion);\n\t\t\t}\n\n\t\t\tthis.emit('debug', `MLS welcome processed (transition id: ${transitionId})`);\n\t\t\treturn { transitionId, success: true };\n\t\t} catch (error) {\n\t\t\tthis.emit('debug', `MLS welcome errored from transition ${transitionId}: ${error}`);\n\t\t\tthis.recoverFromInvalidTransition(transitionId);\n\t\t\treturn { transitionId, success: false };\n\t\t}\n\t}\n\n\t/**\n\t * Encrypt a packet using end-to-end encryption.\n\t *\n\t * @param packet - The packet to encrypt\n\t */\n\tpublic encrypt(packet: Buffer) {\n\t\tif (this.protocolVersion === 0 || !this.session?.ready || packet.equals(SILENCE_FRAME)) return packet;\n\t\treturn this.session.encryptOpus(packet);\n\t}\n\n\t/**\n\t * Decrypt a packet using end-to-end encryption.\n\t *\n\t * @param packet - The packet to decrypt\n\t * @param userId - The user id that sent the packet\n\t * @returns The decrypted packet, or `null` if the decryption failed but should be ignored\n\t */\n\tpublic decrypt(packet: Buffer, userId: string) {\n\t\tconst canDecrypt = this.session?.ready && (this.protocolVersion !== 0 || this.session?.canPassthrough(userId));\n\t\tif (packet.equals(SILENCE_FRAME) || !canDecrypt || !this.session) return packet;\n\t\ttry {\n\t\t\t// @ts-expect-error - const enum is exported and works (todo: drop const modifier on Davey end)\n\t\t\tconst buffer = this.session.decrypt(userId, Davey.MediaType.AUDIO, packet);\n\t\t\tthis.consecutiveFailures = 0;\n\t\t\treturn buffer;\n\t\t} catch (error) {\n\t\t\tif (!this.reinitializing && this.pendingTransitions.size === 0) {\n\t\t\t\tthis.consecutiveFailures++;\n\t\t\t\tthis.emit('debug', `Failed to decrypt a packet (${this.consecutiveFailures} consecutive fails)`);\n\t\t\t\tif (this.consecutiveFailures > this.failureTolerance) {\n\t\t\t\t\tif (this.lastTransitionId) this.recoverFromInvalidTransition(this.lastTransitionId);\n\t\t\t\t\telse throw error;\n\t\t\t\t}\n\t\t\t} else if (this.reinitializing) {\n\t\t\t\tthis.emit('debug', 'Failed to decrypt a packet (reinitializing session)');\n\t\t\t} else if (this.pendingTransitions.size > 0) {\n\t\t\t\tthis.emit('debug', `Failed to decrypt a packet (${this.pendingTransitions.size} pending transition[s])`);\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * Resets the session.\n\t */\n\tpublic destroy() {\n\t\ttry {\n\t\t\tthis.session?.reset();\n\t\t} catch {}\n\t}\n}\n","/* eslint-disable @typescript-eslint/prefer-ts-expect-error, @typescript-eslint/method-signature-style */\nimport { Buffer } from 'node:buffer';\nimport { EventEmitter } from 'node:events';\nimport { addAudioPlayer, deleteAudioPlayer } from '../DataStore';\nimport { VoiceConnectionStatus, type VoiceConnection } from '../VoiceConnection';\nimport { noop } from '../util/util';\nimport { AudioPlayerError } from './AudioPlayerError';\nimport type { AudioResource } from './AudioResource';\nimport { PlayerSubscription } from './PlayerSubscription';\n\n// The Opus \"silent\" frame\nexport const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]);\n\n/**\n * Describes the behavior of the player when an audio packet is played but there are no available\n * voice connections to play to.\n */\nexport enum NoSubscriberBehavior {\n\t/**\n\t * Pauses playing the stream until a voice connection becomes available.\n\t */\n\tPause = 'pause',\n\n\t/**\n\t * Continues to play through the resource regardless.\n\t */\n\tPlay = 'play',\n\n\t/**\n\t * The player stops and enters the Idle state.\n\t */\n\tStop = 'stop',\n}\n\nexport enum AudioPlayerStatus {\n\t/**\n\t * When the player has paused itself. Only possible with the \"pause\" no subscriber behavior.\n\t */\n\tAutoPaused = 'autopaused',\n\n\t/**\n\t * When the player is waiting for an audio resource to become readable before transitioning to Playing.\n\t */\n\tBuffering = 'buffering',\n\n\t/**\n\t * When there is currently no resource for the player to be playing.\n\t */\n\tIdle = 'idle',\n\n\t/**\n\t * When the player has been manually paused.\n\t */\n\tPaused = 'paused',\n\n\t/**\n\t * When the player is actively playing an audio resource.\n\t */\n\tPlaying = 'playing',\n}\n\n/**\n * Options that can be passed when creating an audio player, used to specify its behavior.\n */\nexport interface CreateAudioPlayerOptions {\n\tbehaviors?: {\n\t\tmaxMissedFrames?: number;\n\t\tnoSubscriber?: NoSubscriberBehavior;\n\t};\n\tdebug?: boolean;\n}\n\n/**\n * The state that an AudioPlayer is in when it has no resource to play. This is the starting state.\n */\nexport interface AudioPlayerIdleState {\n\tstatus: AudioPlayerStatus.Idle;\n}\n\n/**\n * The state that an AudioPlayer is in when it is waiting for a resource to become readable. Once this\n * happens, the AudioPlayer will enter the Playing state. If the resource ends/errors before this, then\n * it will re-enter the Idle state.\n */\nexport interface AudioPlayerBufferingState {\n\tonFailureCallback: () => void;\n\tonReadableCallback: () => void;\n\tonStreamError: (error: Error) => void;\n\t/**\n\t * The resource that the AudioPlayer is waiting for\n\t */\n\tresource: AudioResource;\n\tstatus: AudioPlayerStatus.Buffering;\n}\n\n/**\n * The state that an AudioPlayer is in when it is actively playing an AudioResource. When playback ends,\n * it will enter the Idle state.\n */\nexport interface AudioPlayerPlayingState {\n\t/**\n\t * The number of consecutive times that the audio resource has been unable to provide an Opus frame.\n\t */\n\tmissedFrames: number;\n\tonStreamError: (error: Error) => void;\n\n\t/**\n\t * The playback duration in milliseconds of the current audio resource. This includes filler silence packets\n\t * that have been played when the resource was buffering.\n\t */\n\tplaybackDuration: number;\n\n\t/**\n\t * The resource that is being played.\n\t */\n\tresource: AudioResource;\n\n\tstatus: AudioPlayerStatus.Playing;\n}\n\n/**\n * The state that an AudioPlayer is in when it has either been explicitly paused by the user, or done\n * automatically by the AudioPlayer itself if there are no available subscribers.\n */\nexport interface AudioPlayerPausedState {\n\tonStreamError: (error: Error) => void;\n\t/**\n\t * The playback duration in milliseconds of the current audio resource. This includes filler silence packets\n\t * that have been played when the resource was buffering.\n\t */\n\tplaybackDuration: number;\n\n\t/**\n\t * The current resource of the audio player.\n\t */\n\tresource: AudioResource;\n\n\t/**\n\t * How many silence packets still need to be played to avoid audio interpolation due to the stream suddenly pausing.\n\t */\n\tsilencePacketsRemaining: number;\n\n\tstatus: AudioPlayerStatus.AutoPaused | AudioPlayerStatus.Paused;\n}\n\n/**\n * The various states that the player can be in.\n */\nexport type AudioPlayerState =\n\t| AudioPlayerBufferingState\n\t| AudioPlayerIdleState\n\t| AudioPlayerPausedState\n\t| AudioPlayerPlayingState;\n\nexport interface AudioPlayer extends EventEmitter {\n\t/**\n\t * Emitted when there is an error emitted from the audio resource played by the audio player\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'error', listener: (error: AudioPlayerError) => void): this;\n\t/**\n\t * Emitted debugging information about the audio player\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'debug', listener: (message: string) => void): this;\n\t/**\n\t * Emitted when the state of the audio player changes\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'stateChange', listener: (oldState: AudioPlayerState, newState: AudioPlayerState) => void): this;\n\t/**\n\t * Emitted when the audio player is subscribed to a voice connection\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'subscribe' | 'unsubscribe', listener: (subscription: PlayerSubscription) => void): this;\n\t/**\n\t * Emitted when the status of state changes to a specific status\n\t *\n\t * @eventProperty\n\t */\n\ton(\n\t\tevent: Event,\n\t\tlistener: (oldState: AudioPlayerState, newState: AudioPlayerState & { status: Event }) => void,\n\t): this;\n}\n\n/**\n * Stringifies an AudioPlayerState instance.\n *\n * @param state - The state to stringify\n */\nfunction stringifyState(state: AudioPlayerState) {\n\treturn JSON.stringify({\n\t\t...state,\n\t\tresource: Reflect.has(state, 'resource'),\n\t\tstepTimeout: Reflect.has(state, 'stepTimeout'),\n\t});\n}\n\n/**\n * Used to play audio resources (i.e. tracks, streams) to voice connections.\n *\n * @remarks\n * Audio players are designed to be re-used - even if a resource has finished playing, the player itself\n * can still be used.\n *\n * The AudioPlayer drives the timing of playback, and therefore is unaffected by voice connections\n * becoming unavailable. Its behavior in these scenarios can be configured.\n */\nexport class AudioPlayer extends EventEmitter {\n\t/**\n\t * The state that the AudioPlayer is in.\n\t */\n\tprivate _state: AudioPlayerState;\n\n\t/**\n\t * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio\n\t * to the streams in this list.\n\t */\n\tprivate readonly subscribers: PlayerSubscription[] = [];\n\n\t/**\n\t * The behavior that the player should follow when it enters certain situations.\n\t */\n\tprivate readonly behaviors: {\n\t\tmaxMissedFrames: number;\n\t\tnoSubscriber: NoSubscriberBehavior;\n\t};\n\n\t/**\n\t * The debug logger function, if debugging is enabled.\n\t */\n\tprivate readonly debug: ((message: string) => void) | null;\n\n\t/**\n\t * Creates a new AudioPlayer.\n\t */\n\tpublic constructor(options: CreateAudioPlayerOptions = {}) {\n\t\tsuper();\n\t\tthis._state = { status: AudioPlayerStatus.Idle };\n\t\tthis.behaviors = {\n\t\t\tnoSubscriber: NoSubscriberBehavior.Pause,\n\t\t\tmaxMissedFrames: 5,\n\t\t\t...options.behaviors,\n\t\t};\n\t\tthis.debug = options.debug === false ? null : (message: string) => this.emit('debug', message);\n\t}\n\n\t/**\n\t * A list of subscribed voice connections that can currently receive audio to play.\n\t */\n\tpublic get playable() {\n\t\treturn this.subscribers\n\t\t\t.filter(({ connection }) => connection.state.status === VoiceConnectionStatus.Ready)\n\t\t\t.map(({ connection }) => connection);\n\t}\n\n\t/**\n\t * Subscribes a VoiceConnection to the audio player's play list. If the VoiceConnection is already subscribed,\n\t * then the existing subscription is used.\n\t *\n\t * @remarks\n\t * This method should not be directly called. Instead, use VoiceConnection#subscribe.\n\t * @param connection - The connection to subscribe\n\t * @returns The new subscription if the voice connection is not yet subscribed, otherwise the existing subscription\n\t */\n\t// @ts-ignore\n\tprivate subscribe(connection: VoiceConnection) {\n\t\tconst existingSubscription = this.subscribers.find((subscription) => subscription.connection === connection);\n\t\tif (!existingSubscription) {\n\t\t\tconst subscription = new PlayerSubscription(connection, this);\n\t\t\tthis.subscribers.push(subscription);\n\t\t\tsetImmediate(() => this.emit('subscribe', subscription));\n\t\t\treturn subscription;\n\t\t}\n\n\t\treturn existingSubscription;\n\t}\n\n\t/**\n\t * Unsubscribes a subscription - i.e. removes a voice connection from the play list of the audio player.\n\t *\n\t * @remarks\n\t * This method should not be directly called. Instead, use PlayerSubscription#unsubscribe.\n\t * @param subscription - The subscription to remove\n\t * @returns Whether or not the subscription existed on the player and was removed\n\t */\n\t// @ts-ignore\n\tprivate unsubscribe(subscription: PlayerSubscription) {\n\t\tconst index = this.subscribers.indexOf(subscription);\n\t\tconst exists = index !== -1;\n\t\tif (exists) {\n\t\t\tthis.subscribers.splice(index, 1);\n\t\t\tsubscription.connection.setSpeaking(false);\n\t\t\tthis.emit('unsubscribe', subscription);\n\t\t}\n\n\t\treturn exists;\n\t}\n\n\t/**\n\t * The state that the player is in.\n\t *\n\t * @remarks\n\t * The setter will perform clean-up operations where necessary.\n\t */\n\tpublic get state() {\n\t\treturn this._state;\n\t}\n\n\tpublic set state(newState: AudioPlayerState) {\n\t\tconst oldState = this._state;\n\t\tconst newResource = Reflect.get(newState, 'resource') as AudioResource | undefined;\n\n\t\tif (oldState.status !== AudioPlayerStatus.Idle && oldState.resource !== newResource) {\n\t\t\toldState.resource.playStream.on('error', noop);\n\t\t\toldState.resource.playStream.off('error', oldState.onStreamError);\n\t\t\toldState.resource.audioPlayer = undefined;\n\t\t\toldState.resource.playStream.destroy();\n\t\t\toldState.resource.playStream.read(); // required to ensure buffered data is drained, prevents memory leak\n\t\t}\n\n\t\t// When leaving the Buffering state (or buffering a new resource), then remove the event listeners from it\n\t\tif (\n\t\t\toldState.status === AudioPlayerStatus.Buffering &&\n\t\t\t(newState.status !== AudioPlayerStatus.Buffering || newState.resource !== oldState.resource)\n\t\t) {\n\t\t\toldState.resource.playStream.off('end', oldState.onFailureCallback);\n\t\t\toldState.resource.playStream.off('close', oldState.onFailureCallback);\n\t\t\toldState.resource.playStream.off('finish', oldState.onFailureCallback);\n\t\t\toldState.resource.playStream.off('readable', oldState.onReadableCallback);\n\t\t}\n\n\t\t// transitioning into an idle should ensure that connections stop speaking\n\t\tif (newState.status === AudioPlayerStatus.Idle) {\n\t\t\tthis._signalStopSpeaking();\n\t\t\tdeleteAudioPlayer(this);\n\t\t}\n\n\t\t// attach to the global audio player timer\n\t\tif (newResource) {\n\t\t\taddAudioPlayer(this);\n\t\t}\n\n\t\t// playing -> playing state changes should still transition if a resource changed (seems like it would be useful!)\n\t\tconst didChangeResources =\n\t\t\toldState.status !== AudioPlayerStatus.Idle &&\n\t\t\tnewState.status === AudioPlayerStatus.Playing &&\n\t\t\toldState.resource !== newState.resource;\n\n\t\tthis._state = newState;\n\n\t\tthis.emit('stateChange', oldState, this._state);\n\t\tif (oldState.status !== newState.status || didChangeResources) {\n\t\t\tthis.emit(newState.status, oldState, this._state as any);\n\t\t}\n\n\t\tthis.debug?.(`state change:\\nfrom ${stringifyState(oldState)}\\nto ${stringifyState(newState)}`);\n\t}\n\n\t/**\n\t * Plays a new resource on the player. If the player is already playing a resource, the existing resource is destroyed\n\t * (it cannot be reused, even in another player) and is replaced with the new resource.\n\t *\n\t * @remarks\n\t * The player will transition to the Playing state once playback begins, and will return to the Idle state once\n\t * playback is ended.\n\t *\n\t * If the player was previously playing a resource and this method is called, the player will not transition to the\n\t * Idle state during the swap over.\n\t * @param resource - The resource to play\n\t * @throws Will throw if attempting to play an audio resource that has already ended, or is being played by another player\n\t */\n\tpublic play(resource: AudioResource) {\n\t\tif (resource.ended) {\n\t\t\tthrow new Error('Cannot play a resource that has already ended.');\n\t\t}\n\n\t\tif (resource.audioPlayer) {\n\t\t\tif (resource.audioPlayer === this) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthrow new Error('Resource is already being played by another audio player.');\n\t\t}\n\n\t\tresource.audioPlayer = this;\n\n\t\t// Attach error listeners to the stream that will propagate the error and then return to the Idle\n\t\t// state if the resource is still being used.\n\t\tconst onStreamError = (error: Error) => {\n\t\t\tif (this.state.status !== AudioPlayerStatus.Idle) {\n\t\t\t\tthis.emit('error', new AudioPlayerError(error, this.state.resource));\n\t\t\t}\n\n\t\t\tif (this.state.status !== AudioPlayerStatus.Idle && this.state.resource === resource) {\n\t\t\t\tthis.state = {\n\t\t\t\t\tstatus: AudioPlayerStatus.Idle,\n\t\t\t\t};\n\t\t\t}\n\t\t};\n\n\t\tresource.playStream.once('error', onStreamError);\n\n\t\tif (resource.started) {\n\t\t\tthis.state = {\n\t\t\t\tstatus: AudioPlayerStatus.Playing,\n\t\t\t\tmissedFrames: 0,\n\t\t\t\tplaybackDuration: 0,\n\t\t\t\tresource,\n\t\t\t\tonStreamError,\n\t\t\t};\n\t\t} else {\n\t\t\tconst onReadableCallback = () => {\n\t\t\t\tif (this.state.status === AudioPlayerStatus.Buffering && this.state.resource === resource) {\n\t\t\t\t\tthis.state = {\n\t\t\t\t\t\tstatus: AudioPlayerStatus.Playing,\n\t\t\t\t\t\tmissedFrames: 0,\n\t\t\t\t\t\tplaybackDuration: 0,\n\t\t\t\t\t\tresource,\n\t\t\t\t\t\tonStreamError,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst onFailureCallback = () => {\n\t\t\t\tif (this.state.status === AudioPlayerStatus.Buffering && this.state.resource === resource) {\n\t\t\t\t\tthis.state = {\n\t\t\t\t\t\tstatus: AudioPlayerStatus.Idle,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tresource.playStream.once('readable', onReadableCallback);\n\n\t\t\tresource.playStream.once('end', onFailureCallback);\n\t\t\tresource.playStream.once('close', onFailureCallback);\n\t\t\tresource.playStream.once('finish', onFailureCallback);\n\n\t\t\tthis.state = {\n\t\t\t\tstatus: AudioPlayerStatus.Buffering,\n\t\t\t\tresource,\n\t\t\t\tonReadableCallback,\n\t\t\t\tonFailureCallback,\n\t\t\t\tonStreamError,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Pauses playback of the current resource, if any.\n\t *\n\t * @param interpolateSilence - If true, the player will play 5 packets of silence after pausing to prevent audio glitches\n\t * @returns `true` if the player was successfully paused, otherwise `false`\n\t */\n\tpublic pause(interpolateSilence = true) {\n\t\tif (this.state.status !== AudioPlayerStatus.Playing) return false;\n\t\tthis.state = {\n\t\t\t...this.state,\n\t\t\tstatus: AudioPlayerStatus.Paused,\n\t\t\tsilencePacketsRemaining: interpolateSilence ? 5 : 0,\n\t\t};\n\t\treturn true;\n\t}\n\n\t/**\n\t * Unpauses playback of the current resource, if any.\n\t *\n\t * @returns `true` if the player was successfully unpaused, otherwise `false`\n\t */\n\tpublic unpause() {\n\t\tif (this.state.status !== AudioPlayerStatus.Paused) return false;\n\t\tthis.state = {\n\t\t\t...this.state,\n\t\t\tstatus: AudioPlayerStatus.Playing,\n\t\t\tmissedFrames: 0,\n\t\t};\n\t\treturn true;\n\t}\n\n\t/**\n\t * Stops playback of the current resource and destroys the resource. The player will either transition to the Idle state,\n\t * or remain in its current state until the silence padding frames of the resource have been played.\n\t *\n\t * @param force - If true, will force the player to enter the Idle state even if the resource has silence padding frames\n\t * @returns `true` if the player will come to a stop, otherwise `false`\n\t */\n\tpublic stop(force = false) {\n\t\tif (this.state.status === AudioPlayerStatus.Idle) return false;\n\t\tif (force || this.state.resource.silencePaddingFrames === 0) {\n\t\t\tthis.state = {\n\t\t\t\tstatus: AudioPlayerStatus.Idle,\n\t\t\t};\n\t\t} else if (this.state.resource.silenceRemaining === -1) {\n\t\t\tthis.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Checks whether the underlying resource (if any) is playable (readable)\n\t *\n\t * @returns `true` if the resource is playable, otherwise `false`\n\t */\n\tpublic checkPlayable() {\n\t\tconst state = this._state;\n\t\tif (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return false;\n\n\t\t// If the stream has been destroyed or is no longer readable, then transition to the Idle state.\n\t\tif (!state.resource.readable) {\n\t\t\tthis.state = {\n\t\t\t\tstatus: AudioPlayerStatus.Idle,\n\t\t\t};\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Called roughly every 20ms by the global audio player timer. Dispatches any audio packets that are buffered\n\t * by the active connections of this audio player.\n\t */\n\t// @ts-ignore\n\tprivate _stepDispatch() {\n\t\tconst state = this._state;\n\n\t\t// Guard against the Idle state\n\t\tif (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return;\n\n\t\t// Dispatch any audio packets that were prepared in the previous cycle\n\t\tfor (const connection of this.playable) {\n\t\t\tconnection.dispatchAudio();\n\t\t}\n\t}\n\n\t/**\n\t * Called roughly every 20ms by the global audio player timer. Attempts to read an audio packet from the\n\t * underlying resource of the stream, and then has all the active connections of the audio player prepare it\n\t * (encrypt it, append header data) so that it is ready to play at the start of the next cycle.\n\t */\n\t// @ts-ignore\n\tprivate _stepPrepare() {\n\t\tconst state = this._state;\n\n\t\t// Guard against the Idle state\n\t\tif (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return;\n\n\t\t// List of connections that can receive the packet\n\t\tconst playable = this.playable;\n\n\t\t/* If the player was previously in the AutoPaused state, check to see whether there are newly available\n\t\t connections, allowing us to transition out of the AutoPaused state back into the Playing state */\n\t\tif (state.status === AudioPlayerStatus.AutoPaused && playable.length > 0) {\n\t\t\tthis.state = {\n\t\t\t\t...state,\n\t\t\t\tstatus: AudioPlayerStatus.Playing,\n\t\t\t\tmissedFrames: 0,\n\t\t\t};\n\t\t}\n\n\t\t/* If the player is (auto)paused, check to see whether silence packets should be played and\n\t\t set a timeout to begin the next cycle, ending the current cycle here. */\n\t\tif (state.status === AudioPlayerStatus.Paused || state.status === AudioPlayerStatus.AutoPaused) {\n\t\t\tif (state.silencePacketsRemaining > 0) {\n\t\t\t\tstate.silencePacketsRemaining--;\n\t\t\t\tthis._preparePacket(SILENCE_FRAME, playable, state);\n\t\t\t\tif (state.silencePacketsRemaining === 0) {\n\t\t\t\t\tthis._signalStopSpeaking();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// If there are no available connections in this cycle, observe the configured \"no subscriber\" behavior.\n\t\tif (playable.length === 0) {\n\t\t\tif (this.behaviors.noSubscriber === NoSubscriberBehavior.Pause) {\n\t\t\t\tthis.state = {\n\t\t\t\t\t...state,\n\t\t\t\t\tstatus: AudioPlayerStatus.AutoPaused,\n\t\t\t\t\tsilencePacketsRemaining: 5,\n\t\t\t\t};\n\t\t\t\treturn;\n\t\t\t} else if (this.behaviors.noSubscriber === NoSubscriberBehavior.Stop) {\n\t\t\t\tthis.stop(true);\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Attempt to read an Opus packet from the resource. If there isn't an available packet,\n\t\t * play a silence packet. If there are 5 consecutive cycles with failed reads, then the\n\t\t * playback will end.\n\t\t */\n\t\tconst packet: Buffer | null = state.resource.read();\n\n\t\tif (state.status === AudioPlayerStatus.Playing) {\n\t\t\tif (packet) {\n\t\t\t\tthis._preparePacket(packet, playable, state);\n\t\t\t\tstate.missedFrames = 0;\n\t\t\t} else {\n\t\t\t\tthis._preparePacket(SILENCE_FRAME, playable, state);\n\t\t\t\tstate.missedFrames++;\n\t\t\t\tif (state.missedFrames >= this.behaviors.maxMissedFrames) {\n\t\t\t\t\tthis.stop();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Signals to all the subscribed connections that they should send a packet to Discord indicating\n\t * they are no longer speaking. Called once playback of a resource ends.\n\t */\n\tprivate _signalStopSpeaking() {\n\t\tfor (const { connection } of this.subscribers) {\n\t\t\tconnection.setSpeaking(false);\n\t\t}\n\t}\n\n\t/**\n\t * Instructs the given connections to each prepare this packet to be played at the start of the\n\t * next cycle.\n\t *\n\t * @param packet - The Opus packet to be prepared by each receiver\n\t * @param receivers - The connections that should play this packet\n\t */\n\tprivate _preparePacket(\n\t\tpacket: Buffer,\n\t\treceivers: VoiceConnection[],\n\t\tstate: AudioPlayerPausedState | AudioPlayerPlayingState,\n\t) {\n\t\tstate.playbackDuration += 20;\n\t\tfor (const connection of receivers) {\n\t\t\tconnection.prepareAudioPacket(packet);\n\t\t}\n\t}\n}\n\n/**\n * Creates a new AudioPlayer to be used.\n */\nexport function createAudioPlayer(options?: CreateAudioPlayerOptions) {\n\treturn new AudioPlayer(options);\n}\n","import type { AudioResource } from './AudioResource';\n\n/**\n * An error emitted by an AudioPlayer. Contains an attached resource to aid with\n * debugging and identifying where the error came from.\n */\nexport class AudioPlayerError extends Error {\n\t/**\n\t * The resource associated with the audio player at the time the error was thrown.\n\t */\n\tpublic readonly resource: AudioResource;\n\n\tpublic constructor(error: Error, resource: AudioResource) {\n\t\tsuper(error.message);\n\t\tthis.resource = resource;\n\t\tthis.name = error.name;\n\t\tthis.stack = error.stack!;\n\t}\n}\n","/* eslint-disable @typescript-eslint/dot-notation */\nimport type { VoiceConnection } from '../VoiceConnection';\nimport type { AudioPlayer } from './AudioPlayer';\n\n/**\n * Represents a subscription of a voice connection to an audio player, allowing\n * the audio player to play audio on the voice connection.\n */\nexport class PlayerSubscription {\n\t/**\n\t * The voice connection of this subscription.\n\t */\n\tpublic readonly connection: VoiceConnection;\n\n\t/**\n\t * The audio player of this subscription.\n\t */\n\tpublic readonly player: AudioPlayer;\n\n\tpublic constructor(connection: VoiceConnection, player: AudioPlayer) {\n\t\tthis.connection = connection;\n\t\tthis.player = player;\n\t}\n\n\t/**\n\t * Unsubscribes the connection from the audio player, meaning that the\n\t * audio player cannot stream audio to it until a new subscription is made.\n\t */\n\tpublic unsubscribe() {\n\t\tthis.connection['onSubscriptionRemoved'](this);\n\t\tthis.player['unsubscribe'](this);\n\t}\n}\n","import { Buffer } from 'node:buffer';\nimport { createSocket, type Socket } from 'node:dgram';\nimport { EventEmitter } from 'node:events';\nimport { isIPv4 } from 'node:net';\n\n/**\n * Stores an IP address and port. Used to store socket details for the local client as well as\n * for Discord.\n */\nexport interface SocketConfig {\n\tip: string;\n\tport: number;\n}\n\n/**\n * Parses the response from Discord to aid with local IP discovery.\n *\n * @param message - The received message\n */\nexport function parseLocalPacket(message: Buffer): SocketConfig {\n\tconst packet = Buffer.from(message);\n\n\tconst ip = packet.subarray(8, packet.indexOf(0, 8)).toString('utf8');\n\n\tif (!isIPv4(ip)) {\n\t\tthrow new Error('Malformed IP address');\n\t}\n\n\tconst port = packet.readUInt16BE(packet.length - 2);\n\n\treturn { ip, port };\n}\n\n/**\n * The interval in milliseconds at which keep alive datagrams are sent.\n */\nconst KEEP_ALIVE_INTERVAL = 5e3;\n\n/**\n * The maximum value of the keep alive counter.\n */\nconst MAX_COUNTER_VALUE = 2 ** 32 - 1;\n\nexport interface VoiceUDPSocket extends EventEmitter {\n\ton(event: 'error', listener: (error: Error) => void): this;\n\ton(event: 'close', listener: () => void): this;\n\ton(event: 'debug', listener: (message: string) => void): this;\n\ton(event: 'message', listener: (message: Buffer) => void): this;\n}\n\n/**\n * Manages the UDP networking for a voice connection.\n */\nexport class VoiceUDPSocket extends EventEmitter {\n\t/**\n\t * The underlying network Socket for the VoiceUDPSocket.\n\t */\n\tprivate readonly socket: Socket;\n\n\t/**\n\t * The socket details for Discord (remote)\n\t */\n\tprivate readonly remote: SocketConfig;\n\n\t/**\n\t * The counter used in the keep alive mechanism.\n\t */\n\tprivate keepAliveCounter = 0;\n\n\t/**\n\t * The buffer used to write the keep alive counter into.\n\t */\n\tprivate readonly keepAliveBuffer: Buffer;\n\n\t/**\n\t * The Node.js interval for the keep-alive mechanism.\n\t */\n\tprivate readonly keepAliveInterval: NodeJS.Timeout;\n\n\t/**\n\t * The time taken to receive a response to keep alive messages.\n\t *\n\t * @deprecated This field is no longer updated as keep alive messages are no longer tracked.\n\t */\n\tpublic ping?: number;\n\n\t/**\n\t * Creates a new VoiceUDPSocket.\n\t *\n\t * @param remote - Details of the remote socket\n\t */\n\tpublic constructor(remote: SocketConfig) {\n\t\tsuper();\n\t\tthis.socket = createSocket('udp4');\n\t\tthis.socket.on('error', (error: Error) => this.emit('error', error));\n\t\tthis.socket.on('message', (buffer: Buffer) => this.onMessage(buffer));\n\t\tthis.socket.on('close', () => this.emit('close'));\n\t\tthis.remote = remote;\n\t\tthis.keepAliveBuffer = Buffer.alloc(8);\n\t\tthis.keepAliveInterval = setInterval(() => this.keepAlive(), KEEP_ALIVE_INTERVAL);\n\t\tsetImmediate(() => this.keepAlive());\n\t}\n\n\t/**\n\t * Called when a message is received on the UDP socket.\n\t *\n\t * @param buffer - The received buffer\n\t */\n\tprivate onMessage(buffer: Buffer): void {\n\t\t// Propagate the message\n\t\tthis.emit('message', buffer);\n\t}\n\n\t/**\n\t * Called at a regular interval to check whether we are still able to send datagrams to Discord.\n\t */\n\tprivate keepAlive() {\n\t\tthis.keepAliveBuffer.writeUInt32LE(this.keepAliveCounter, 0);\n\t\tthis.send(this.keepAliveBuffer);\n\t\tthis.keepAliveCounter++;\n\t\tif (this.keepAliveCounter > MAX_COUNTER_VALUE) {\n\t\t\tthis.keepAliveCounter = 0;\n\t\t}\n\t}\n\n\t/**\n\t * Sends a buffer to Discord.\n\t *\n\t * @param buffer - The buffer to send\n\t */\n\tpublic send(buffer: Buffer) {\n\t\tthis.socket.send(buffer, this.remote.port, this.remote.ip);\n\t}\n\n\t/**\n\t * Closes the socket, the instance will not be able to be reused.\n\t */\n\tpublic destroy() {\n\t\ttry {\n\t\t\tthis.socket.close();\n\t\t} catch {}\n\n\t\tclearInterval(this.keepAliveInterval);\n\t}\n\n\t/**\n\t * Performs IP discovery to discover the local address and port to be used for the voice connection.\n\t *\n\t * @param ssrc - The SSRC received from Discord\n\t */\n\tpublic async performIPDiscovery(ssrc: number): Promise {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst listener = (message: Buffer) => {\n\t\t\t\ttry {\n\t\t\t\t\tif (message.readUInt16BE(0) !== 2) return;\n\t\t\t\t\tconst packet = parseLocalPacket(message);\n\t\t\t\t\tthis.socket.off('message', listener);\n\t\t\t\t\tresolve(packet);\n\t\t\t\t} catch {}\n\t\t\t};\n\n\t\t\tthis.socket.on('message', listener);\n\t\t\tthis.socket.once('close', () => reject(new Error('Cannot perform IP discovery - socket closed')));\n\n\t\t\tconst discoveryBuffer = Buffer.alloc(74);\n\n\t\t\tdiscoveryBuffer.writeUInt16BE(1, 0);\n\t\t\tdiscoveryBuffer.writeUInt16BE(70, 2);\n\t\t\tdiscoveryBuffer.writeUInt32BE(ssrc, 4);\n\t\t\tthis.send(discoveryBuffer);\n\t\t});\n\t}\n}\n","import { Buffer } from 'node:buffer';\nimport { EventEmitter } from 'node:events';\nimport type { VoiceSendPayload } from 'discord-api-types/voice/v8';\nimport { VoiceOpcodes } from 'discord-api-types/voice/v8';\nimport WebSocket, { type MessageEvent } from 'ws';\n\n/**\n * A binary WebSocket message.\n */\nexport interface BinaryWebSocketMessage {\n\top: VoiceOpcodes;\n\tpayload: Buffer;\n\tseq: number;\n}\n\nexport interface VoiceWebSocket extends EventEmitter {\n\ton(event: 'error', listener: (error: Error) => void): this;\n\ton(event: 'open', listener: (event: WebSocket.Event) => void): this;\n\ton(event: 'close', listener: (event: WebSocket.CloseEvent) => void): this;\n\t/**\n\t * Debug event for VoiceWebSocket.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'debug', listener: (message: string) => void): this;\n\t/**\n\t * Packet event.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'packet', listener: (packet: any) => void): this;\n\t/**\n\t * Binary message event.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'binary', listener: (message: BinaryWebSocketMessage) => void): this;\n}\n\n/**\n * An extension of the WebSocket class to provide helper functionality when interacting\n * with the Discord Voice gateway.\n */\nexport class VoiceWebSocket extends EventEmitter {\n\t/**\n\t * The current heartbeat interval, if any.\n\t */\n\tprivate heartbeatInterval?: NodeJS.Timeout;\n\n\t/**\n\t * The time (milliseconds since UNIX epoch) that the last heartbeat acknowledgement packet was received.\n\t * This is set to 0 if an acknowledgement packet hasn't been received yet.\n\t */\n\tprivate lastHeartbeatAck: number;\n\n\t/**\n\t * The time (milliseconds since UNIX epoch) that the last heartbeat was sent. This is set to 0 if a heartbeat\n\t * hasn't been sent yet.\n\t */\n\tprivate lastHeartbeatSend: number;\n\n\t/**\n\t * The number of consecutively missed heartbeats.\n\t */\n\tprivate missedHeartbeats = 0;\n\n\t/**\n\t * The last recorded ping.\n\t */\n\tpublic ping?: number;\n\n\t/**\n\t * The last sequence number acknowledged from Discord. Will be `-1` if no sequence numbered messages have been received.\n\t */\n\tpublic sequence = -1;\n\n\t/**\n\t * The debug logger function, if debugging is enabled.\n\t */\n\tprivate readonly debug: ((message: string) => void) | null;\n\n\t/**\n\t * The underlying WebSocket of this wrapper.\n\t */\n\tprivate readonly ws: WebSocket;\n\n\t/**\n\t * Creates a new VoiceWebSocket.\n\t *\n\t * @param address - The address to connect to\n\t */\n\tpublic constructor(address: string, debug: boolean) {\n\t\tsuper();\n\t\tthis.ws = new WebSocket(address);\n\t\tthis.ws.onmessage = (err) => this.onMessage(err);\n\t\tthis.ws.onopen = (err) => this.emit('open', err);\n\t\tthis.ws.onerror = (err: Error | WebSocket.ErrorEvent) => this.emit('error', err instanceof Error ? err : err.error);\n\t\tthis.ws.onclose = (err) => this.emit('close', err);\n\n\t\tthis.lastHeartbeatAck = 0;\n\t\tthis.lastHeartbeatSend = 0;\n\n\t\tthis.debug = debug ? (message: string) => this.emit('debug', message) : null;\n\t}\n\n\t/**\n\t * Destroys the VoiceWebSocket. The heartbeat interval is cleared, and the connection is closed.\n\t */\n\tpublic destroy() {\n\t\ttry {\n\t\t\tthis.debug?.('destroyed');\n\t\t\tthis.setHeartbeatInterval(-1);\n\t\t\tthis.ws.close(1_000);\n\t\t} catch (error) {\n\t\t\tconst err = error as Error;\n\t\t\tthis.emit('error', err);\n\t\t}\n\t}\n\n\t/**\n\t * Handles message events on the WebSocket. Attempts to JSON parse the messages and emit them\n\t * as packets. Binary messages will be parsed and emitted.\n\t *\n\t * @param event - The message event\n\t */\n\tpublic onMessage(event: MessageEvent) {\n\t\tif (event.data instanceof Buffer || event.data instanceof ArrayBuffer) {\n\t\t\tconst buffer = event.data instanceof ArrayBuffer ? Buffer.from(event.data) : event.data;\n\t\t\tconst seq = buffer.readUInt16BE(0);\n\t\t\tconst op = buffer.readUInt8(2);\n\t\t\tconst payload = buffer.subarray(3);\n\n\t\t\tthis.sequence = seq;\n\t\t\tthis.debug?.(`<< [bin] opcode ${op}, seq ${seq}, ${payload.byteLength} bytes`);\n\n\t\t\tthis.emit('binary', { op, seq, payload });\n\t\t\treturn;\n\t\t} else if (typeof event.data !== 'string') {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.debug?.(`<< ${event.data}`);\n\n\t\tlet packet: any;\n\t\ttry {\n\t\t\tpacket = JSON.parse(event.data);\n\t\t} catch (error) {\n\t\t\tconst err = error as Error;\n\t\t\tthis.emit('error', err);\n\t\t\treturn;\n\t\t}\n\n\t\tif (packet.seq) {\n\t\t\tthis.sequence = packet.seq;\n\t\t}\n\n\t\tif (packet.op === VoiceOpcodes.HeartbeatAck) {\n\t\t\tthis.lastHeartbeatAck = Date.now();\n\t\t\tthis.missedHeartbeats = 0;\n\t\t\tthis.ping = this.lastHeartbeatAck - this.lastHeartbeatSend;\n\t\t}\n\n\t\tthis.emit('packet', packet);\n\t}\n\n\t/**\n\t * Sends a JSON-stringifiable packet over the WebSocket.\n\t *\n\t * @param packet - The packet to send\n\t */\n\tpublic sendPacket(packet: VoiceSendPayload) {\n\t\ttry {\n\t\t\tconst stringified = JSON.stringify(packet);\n\t\t\tthis.debug?.(`>> ${stringified}`);\n\t\t\tthis.ws.send(stringified);\n\t\t} catch (error) {\n\t\t\tconst err = error as Error;\n\t\t\tthis.emit('error', err);\n\t\t}\n\t}\n\n\t/**\n\t * Sends a binary message over the WebSocket.\n\t *\n\t * @param opcode - The opcode to use\n\t * @param payload - The payload to send\n\t */\n\tpublic sendBinaryMessage(opcode: VoiceOpcodes, payload: Buffer) {\n\t\ttry {\n\t\t\tconst message = Buffer.concat([new Uint8Array([opcode]), payload]);\n\t\t\tthis.debug?.(`>> [bin] opcode ${opcode}, ${payload.byteLength} bytes`);\n\t\t\tthis.ws.send(message);\n\t\t} catch (error) {\n\t\t\tconst err = error as Error;\n\t\t\tthis.emit('error', err);\n\t\t}\n\t}\n\n\t/**\n\t * Sends a heartbeat over the WebSocket.\n\t */\n\tprivate sendHeartbeat() {\n\t\tthis.lastHeartbeatSend = Date.now();\n\t\tthis.missedHeartbeats++;\n\t\tconst nonce = this.lastHeartbeatSend;\n\t\tthis.sendPacket({\n\t\t\top: VoiceOpcodes.Heartbeat,\n\t\t\t// eslint-disable-next-line id-length\n\t\t\td: {\n\t\t\t\t// eslint-disable-next-line id-length\n\t\t\t\tt: nonce,\n\t\t\t\tseq_ack: this.sequence,\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Sets/clears an interval to send heartbeats over the WebSocket.\n\t *\n\t * @param ms - The interval in milliseconds. If negative, the interval will be unset\n\t */\n\tpublic setHeartbeatInterval(ms: number) {\n\t\tif (this.heartbeatInterval !== undefined) clearInterval(this.heartbeatInterval);\n\t\tif (ms > 0) {\n\t\t\tthis.heartbeatInterval = setInterval(() => {\n\t\t\t\tif (this.lastHeartbeatSend !== 0 && this.missedHeartbeats >= 3) {\n\t\t\t\t\t// Missed too many heartbeats - disconnect\n\t\t\t\t\tthis.ws.close();\n\t\t\t\t\tthis.setHeartbeatInterval(-1);\n\t\t\t\t}\n\n\t\t\t\tthis.sendHeartbeat();\n\t\t\t}, ms);\n\t\t}\n\t}\n}\n","/* eslint-disable jsdoc/check-param-names */\n\nimport { Buffer } from 'node:buffer';\nimport crypto from 'node:crypto';\nimport type { VoiceReceivePayload } from 'discord-api-types/voice/v8';\nimport { VoiceOpcodes } from 'discord-api-types/voice/v8';\nimport { VoiceConnectionStatus, type VoiceConnection } from '../VoiceConnection';\nimport { NetworkingStatusCode, type ConnectionData } from '../networking/Networking';\nimport { methods } from '../util/Secretbox';\nimport { RTP_OPUS_PAYLOAD_TYPE } from '../util/constants';\nimport {\n\tAudioReceiveStream,\n\tcreateDefaultAudioReceiveStreamOptions,\n\ttype AudioReceiveStreamOptions,\n} from './AudioReceiveStream';\nimport { SSRCMap } from './SSRCMap';\nimport { SpeakingMap } from './SpeakingMap';\n\nconst HEADER_EXTENSION_BYTE = Buffer.from([0xbe, 0xde]);\nconst UNPADDED_NONCE_LENGTH = 4;\nconst AUTH_TAG_LENGTH = 16;\n\n/**\n * Attaches to a VoiceConnection, allowing you to receive audio packets from other\n * users that are speaking.\n *\n * @beta\n */\nexport class VoiceReceiver {\n\t/**\n\t * The attached connection of this receiver.\n\t */\n\tpublic readonly voiceConnection;\n\n\t/**\n\t * Maps SSRCs to Discord user ids.\n\t */\n\tpublic readonly ssrcMap: SSRCMap;\n\n\t/**\n\t * The current audio subscriptions of this receiver.\n\t */\n\tpublic readonly subscriptions: Map;\n\n\t/**\n\t * The connection data of the receiver.\n\t *\n\t * @internal\n\t */\n\tpublic connectionData: Partial;\n\n\t/**\n\t * The speaking map of the receiver.\n\t */\n\tpublic readonly speaking: SpeakingMap;\n\n\tpublic constructor(voiceConnection: VoiceConnection) {\n\t\tthis.voiceConnection = voiceConnection;\n\t\tthis.ssrcMap = new SSRCMap();\n\t\tthis.speaking = new SpeakingMap();\n\t\tthis.subscriptions = new Map();\n\t\tthis.connectionData = {};\n\n\t\tthis.onWsPacket = this.onWsPacket.bind(this);\n\t\tthis.onUdpMessage = this.onUdpMessage.bind(this);\n\t}\n\n\t/**\n\t * Called when a packet is received on the attached connection's WebSocket.\n\t *\n\t * @param packet - The received packet\n\t * @internal\n\t */\n\tpublic onWsPacket(packet: VoiceReceivePayload) {\n\t\tif (packet.op === VoiceOpcodes.ClientDisconnect) {\n\t\t\tthis.ssrcMap.delete(packet.d.user_id);\n\t\t} else if (packet.op === VoiceOpcodes.Speaking) {\n\t\t\tthis.ssrcMap.update({ userId: packet.d.user_id, audioSSRC: packet.d.ssrc });\n\t\t}\n\t}\n\n\tprivate decrypt(buffer: Buffer, mode: string, nonce: Buffer, secretKey: Uint8Array) {\n\t\t// Copy the last 4 bytes of unpadded nonce to the padding of (12 - 4) or (24 - 4) bytes\n\t\tbuffer.copy(nonce, 0, buffer.length - UNPADDED_NONCE_LENGTH);\n\n\t\tlet headerSize = 12;\n\t\tconst first = buffer.readUint8();\n\t\tif ((first >> 4) & 0x01) headerSize += 4;\n\n\t\t// The unencrypted RTP header contains 12 bytes, HEADER_EXTENSION and the extension size\n\t\tconst header = buffer.subarray(0, headerSize);\n\n\t\t// Encrypted contains the extension, if any, the opus packet, and the auth tag\n\t\tconst encrypted = buffer.subarray(headerSize, buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH);\n\t\tconst authTag = buffer.subarray(\n\t\t\tbuffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH,\n\t\t\tbuffer.length - UNPADDED_NONCE_LENGTH,\n\t\t);\n\n\t\tswitch (mode) {\n\t\t\tcase 'aead_aes256_gcm_rtpsize': {\n\t\t\t\tconst decipheriv = crypto.createDecipheriv('aes-256-gcm', secretKey, nonce);\n\t\t\t\tdecipheriv.setAAD(header);\n\t\t\t\tdecipheriv.setAuthTag(authTag);\n\n\t\t\t\treturn Buffer.concat([decipheriv.update(encrypted), decipheriv.final()]);\n\t\t\t}\n\n\t\t\tcase 'aead_xchacha20_poly1305_rtpsize': {\n\t\t\t\t// Combined mode expects authtag in the encrypted message\n\t\t\t\treturn Buffer.from(\n\t\t\t\t\tmethods.crypto_aead_xchacha20poly1305_ietf_decrypt(\n\t\t\t\t\t\tBuffer.concat([encrypted, authTag]),\n\t\t\t\t\t\theader,\n\t\t\t\t\t\tnonce,\n\t\t\t\t\t\tsecretKey,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tdefault: {\n\t\t\t\tthrow new RangeError(`Unsupported decryption method: ${mode}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Parses an audio packet, decrypting it to yield an Opus packet.\n\t *\n\t * @param buffer - The buffer to parse\n\t * @param mode - The encryption mode\n\t * @param nonce - The nonce buffer used by the connection for encryption\n\t * @param secretKey - The secret key used by the connection for encryption\n\t * @param userId - The user id that sent the packet\n\t * @returns The parsed Opus packet\n\t */\n\tprivate parsePacket(buffer: Buffer, mode: string, nonce: Buffer, secretKey: Uint8Array, userId: string) {\n\t\tlet packet: Buffer = this.decrypt(buffer, mode, nonce, secretKey);\n\t\tif (!packet) throw new Error('Failed to parse packet');\n\n\t\t// Strip padding (RFC3550 5.1)\n\t\tconst hasPadding = buffer[0] && Boolean(buffer[0] & 0b100000);\n\t\tif (hasPadding) {\n\t\t\tconst paddingAmount = packet[packet.length - 1]!;\n\t\t\tif (paddingAmount < packet.length) {\n\t\t\t\tpacket = packet.subarray(0, packet.length - paddingAmount);\n\t\t\t}\n\t\t}\n\n\t\t// Strip decrypted RTP Header Extension if present\n\t\t// The header is only indicated in the original data, so compare with buffer first\n\t\tif (buffer.subarray(12, 14).compare(HEADER_EXTENSION_BYTE) === 0) {\n\t\t\tconst headerExtensionLength = buffer.subarray(14).readUInt16BE();\n\t\t\tpacket = packet.subarray(4 * headerExtensionLength);\n\t\t}\n\n\t\t// Decrypt packet if in a DAVE session.\n\t\tif (\n\t\t\tthis.voiceConnection.state.status === VoiceConnectionStatus.Ready &&\n\t\t\t(this.voiceConnection.state.networking.state.code === NetworkingStatusCode.Ready ||\n\t\t\t\tthis.voiceConnection.state.networking.state.code === NetworkingStatusCode.Resuming)\n\t\t) {\n\t\t\tconst daveSession = this.voiceConnection.state.networking.state.dave;\n\t\t\tif (daveSession) packet = daveSession.decrypt(packet, userId)!;\n\t\t}\n\n\t\treturn packet;\n\t}\n\n\t/**\n\t * Called when the UDP socket of the attached connection receives a message.\n\t *\n\t * @param msg - The received message\n\t * @internal\n\t */\n\tpublic onUdpMessage(msg: Buffer) {\n\t\tif (msg.length <= 8) return;\n\t\tconst ssrc = msg.readUInt32BE(8);\n\n\t\tconst userData = this.ssrcMap.get(ssrc);\n\t\tif (!userData) return;\n\n\t\tthis.speaking.onPacket(userData.userId);\n\n\t\tconst stream = this.subscriptions.get(userData.userId);\n\t\tif (!stream) return;\n\n\t\tif (this.connectionData.encryptionMode && this.connectionData.nonceBuffer && this.connectionData.secretKey) {\n\t\t\t// As a guard, we shouldn't parse packets that (1) aren't voice packets and (2) are not in the right RTP version\n\t\t\tif ((msg[1]! & 0x7f) !== RTP_OPUS_PAYLOAD_TYPE) return;\n\n\t\t\t// Ignore packets not in RTP version 2\n\t\t\tconst rtpVersion = msg[0]! >> 6;\n\t\t\tif (rtpVersion !== 2) return;\n\n\t\t\ttry {\n\t\t\t\tconst packet = this.parsePacket(\n\t\t\t\t\tmsg,\n\t\t\t\t\tthis.connectionData.encryptionMode,\n\t\t\t\t\tthis.connectionData.nonceBuffer,\n\t\t\t\t\tthis.connectionData.secretKey,\n\t\t\t\t\tuserData.userId,\n\t\t\t\t);\n\t\t\t\tif (packet) stream.push(packet);\n\t\t\t} catch (error) {\n\t\t\t\tstream.destroy(error as Error);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Creates a subscription for the given user id.\n\t *\n\t * @param target - The id of the user to subscribe to\n\t * @returns A readable stream of Opus packets received from the target\n\t */\n\tpublic subscribe(userId: string, options?: Partial) {\n\t\tconst existing = this.subscriptions.get(userId);\n\t\tif (existing) return existing;\n\n\t\tconst stream = new AudioReceiveStream({\n\t\t\t...createDefaultAudioReceiveStreamOptions(),\n\t\t\t...options,\n\t\t});\n\n\t\tstream.once('close', () => this.subscriptions.delete(userId));\n\t\tthis.subscriptions.set(userId, stream);\n\t\treturn stream;\n\t}\n}\n","import type { Buffer } from 'node:buffer';\nimport process from 'node:process';\nimport { Readable, type ReadableOptions } from 'node:stream';\nimport { SILENCE_FRAME } from '../audio/AudioPlayer';\n\n/**\n * The different behaviors an audio receive stream can have for deciding when to end.\n */\nexport enum EndBehaviorType {\n\t/**\n\t * The stream will only end when manually destroyed.\n\t */\n\tManual,\n\n\t/**\n\t * The stream will end after a given time period of silence/no audio packets.\n\t */\n\tAfterSilence,\n\n\t/**\n\t * The stream will end after a given time period of no audio packets.\n\t */\n\tAfterInactivity,\n}\n\nexport type EndBehavior =\n\t| {\n\t\t\tbehavior: EndBehaviorType.AfterInactivity | EndBehaviorType.AfterSilence;\n\t\t\tduration: number;\n\t }\n\t| {\n\t\t\tbehavior: EndBehaviorType.Manual;\n\t };\n\nexport interface AudioReceiveStreamOptions extends ReadableOptions {\n\tend: EndBehavior;\n}\n\nexport function createDefaultAudioReceiveStreamOptions(): AudioReceiveStreamOptions {\n\treturn {\n\t\tend: {\n\t\t\tbehavior: EndBehaviorType.Manual,\n\t\t},\n\t};\n}\n\n/**\n * A readable stream of Opus packets received from a specific entity\n * in a Discord voice connection.\n */\nexport class AudioReceiveStream extends Readable {\n\t/**\n\t * The end behavior of the receive stream.\n\t */\n\tpublic readonly end: EndBehavior;\n\n\tprivate endTimeout?: NodeJS.Timeout;\n\n\tpublic constructor(options: AudioReceiveStreamOptions) {\n\t\tconst { end, ...rest } = options;\n\n\t\tsuper({\n\t\t\t...rest,\n\t\t\tobjectMode: true,\n\t\t});\n\n\t\tthis.end = end;\n\t}\n\n\tpublic override push(buffer: Buffer | null) {\n\t\tif (\n\t\t\tbuffer &&\n\t\t\t(this.end.behavior === EndBehaviorType.AfterInactivity ||\n\t\t\t\t(this.end.behavior === EndBehaviorType.AfterSilence &&\n\t\t\t\t\t(buffer.compare(SILENCE_FRAME) !== 0 || this.endTimeout === undefined)))\n\t\t) {\n\t\t\tthis.renewEndTimeout(this.end);\n\t\t}\n\n\t\tif (buffer === null) {\n\t\t\t// null marks EOF for stream\n\t\t\tprocess.nextTick(() => this.destroy());\n\t\t}\n\n\t\treturn super.push(buffer);\n\t}\n\n\tprivate renewEndTimeout(end: EndBehavior & { duration: number }) {\n\t\tif (this.endTimeout) {\n\t\t\tclearTimeout(this.endTimeout);\n\t\t}\n\n\t\tthis.endTimeout = setTimeout(() => this.push(null), end.duration);\n\t}\n\n\tpublic override _read() {}\n}\n","import { EventEmitter } from 'node:events';\n\n/**\n * The known data for a user in a Discord voice connection.\n */\nexport interface VoiceUserData {\n\t/**\n\t * The SSRC of the user's audio stream.\n\t */\n\taudioSSRC: number;\n\n\t/**\n\t * The Discord user id of the user.\n\t */\n\tuserId: string;\n\n\t/**\n\t * The SSRC of the user's video stream (if one exists)\n\t * Cannot be 0. If undefined, the user has no video stream.\n\t */\n\tvideoSSRC?: number;\n}\n\nexport interface SSRCMap extends EventEmitter {\n\ton(event: 'create', listener: (newData: VoiceUserData) => void): this;\n\ton(event: 'update', listener: (oldData: VoiceUserData | undefined, newData: VoiceUserData) => void): this;\n\ton(event: 'delete', listener: (deletedData: VoiceUserData) => void): this;\n}\n\n/**\n * Maps audio SSRCs to data of users in voice connections.\n */\nexport class SSRCMap extends EventEmitter {\n\t/**\n\t * The underlying map.\n\t */\n\tprivate readonly map: Map;\n\n\tpublic constructor() {\n\t\tsuper();\n\t\tthis.map = new Map();\n\t}\n\n\t/**\n\t * Updates the map with new user data\n\t *\n\t * @param data - The data to update with\n\t */\n\tpublic update(data: VoiceUserData) {\n\t\tconst existing = this.map.get(data.audioSSRC);\n\n\t\tconst newValue = {\n\t\t\t...this.map.get(data.audioSSRC),\n\t\t\t...data,\n\t\t};\n\n\t\tthis.map.set(data.audioSSRC, newValue);\n\t\tif (!existing) this.emit('create', newValue);\n\t\tthis.emit('update', existing, newValue);\n\t}\n\n\t/**\n\t * Gets the stored voice data of a user.\n\t *\n\t * @param target - The target, either their user id or audio SSRC\n\t */\n\tpublic get(target: number | string) {\n\t\tif (typeof target === 'number') {\n\t\t\treturn this.map.get(target);\n\t\t}\n\n\t\tfor (const data of this.map.values()) {\n\t\t\tif (data.userId === target) {\n\t\t\t\treturn data;\n\t\t\t}\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Deletes the stored voice data about a user.\n\t *\n\t * @param target - The target of the delete operation, either their audio SSRC or user id\n\t * @returns The data that was deleted, if any\n\t */\n\tpublic delete(target: number | string) {\n\t\tif (typeof target === 'number') {\n\t\t\tconst existing = this.map.get(target);\n\t\t\tif (existing) {\n\t\t\t\tthis.map.delete(target);\n\t\t\t\tthis.emit('delete', existing);\n\t\t\t}\n\n\t\t\treturn existing;\n\t\t}\n\n\t\tfor (const [audioSSRC, data] of this.map.entries()) {\n\t\t\tif (data.userId === target) {\n\t\t\t\tthis.map.delete(audioSSRC);\n\t\t\t\tthis.emit('delete', data);\n\t\t\t\treturn data;\n\t\t\t}\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n","/* eslint-disable @typescript-eslint/unified-signatures */\nimport { EventEmitter } from 'node:events';\n\nexport interface SpeakingMap extends EventEmitter {\n\t/**\n\t * Emitted when a user starts speaking.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'start', listener: (userId: string) => void): this;\n\n\t/**\n\t * Emitted when a user ends speaking.\n\t *\n\t * @eventProperty\n\t */\n\ton(event: 'end', listener: (userId: string) => void): this;\n}\n\n/**\n * Tracks the speaking states of users in a voice channel.\n */\nexport class SpeakingMap extends EventEmitter {\n\t/**\n\t * The delay after a packet is received from a user until they're marked as not speaking anymore.\n\t */\n\tpublic static readonly DELAY = 100;\n\n\t/**\n\t * The currently speaking users, mapped to the milliseconds since UNIX epoch at which they started speaking.\n\t */\n\tpublic readonly users: Map;\n\n\tprivate readonly speakingTimeouts: Map;\n\n\tpublic constructor() {\n\t\tsuper();\n\t\tthis.users = new Map();\n\t\tthis.speakingTimeouts = new Map();\n\t}\n\n\tpublic onPacket(userId: string) {\n\t\tconst timeout = this.speakingTimeouts.get(userId);\n\t\tif (timeout) {\n\t\t\tclearTimeout(timeout);\n\t\t} else {\n\t\t\tthis.users.set(userId, Date.now());\n\t\t\tthis.emit('start', userId);\n\t\t}\n\n\t\tthis.startTimeout(userId);\n\t}\n\n\tprivate startTimeout(userId: string) {\n\t\tthis.speakingTimeouts.set(\n\t\t\tuserId,\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.emit('end', userId);\n\t\t\t\tthis.speakingTimeouts.delete(userId);\n\t\t\t\tthis.users.delete(userId);\n\t\t\t}, SpeakingMap.DELAY),\n\t\t);\n\t}\n}\n","import type { JoinConfig } from './DataStore';\nimport { createVoiceConnection } from './VoiceConnection';\nimport type { DiscordGatewayAdapterCreator } from './util/adapter';\n\n/**\n * The options that can be given when creating a voice connection.\n */\nexport interface CreateVoiceConnectionOptions {\n\tadapterCreator: DiscordGatewayAdapterCreator;\n\n\t/**\n\t * Whether to use the DAVE protocol for end-to-end encryption. Defaults to true.\n\t */\n\tdaveEncryption?: boolean | undefined;\n\n\t/**\n\t * If true, debug messages will be enabled for the voice connection and its\n\t * related components. Defaults to false.\n\t */\n\tdebug?: boolean | undefined;\n\n\t/**\n\t * The amount of consecutive decryption failures needed to try to\n\t * re-initialize the end-to-end encrypted session to recover. Defaults to 24.\n\t */\n\tdecryptionFailureTolerance?: number | undefined;\n}\n\n/**\n * The options that can be given when joining a voice channel.\n */\nexport interface JoinVoiceChannelOptions {\n\t/**\n\t * The id of the Discord voice channel to join.\n\t */\n\tchannelId: string;\n\n\t/**\n\t * An optional group identifier for the voice connection.\n\t */\n\tgroup?: string;\n\n\t/**\n\t * The id of the guild that the voice channel belongs to.\n\t */\n\tguildId: string;\n\n\t/**\n\t * Whether to join the channel deafened (defaults to true)\n\t */\n\tselfDeaf?: boolean;\n\n\t/**\n\t * Whether to join the channel muted (defaults to true)\n\t */\n\tselfMute?: boolean;\n}\n\n/**\n * Creates a VoiceConnection to a Discord voice channel.\n *\n * @param options - the options for joining the voice channel\n */\nexport function joinVoiceChannel(options: CreateVoiceConnectionOptions & JoinVoiceChannelOptions) {\n\tconst joinConfig: JoinConfig = {\n\t\tselfDeaf: true,\n\t\tselfMute: false,\n\t\tgroup: 'default',\n\t\t...options,\n\t};\n\n\treturn createVoiceConnection(joinConfig, {\n\t\tadapterCreator: options.adapterCreator,\n\t\tdebug: options.debug,\n\t\tdaveEncryption: options.daveEncryption,\n\t\tdecryptionFailureTolerance: options.decryptionFailureTolerance,\n\t});\n}\n","import type { Buffer } from 'node:buffer';\nimport { pipeline, type Readable } from 'node:stream';\nimport prism from 'prism-media';\nimport { noop } from '../util/util';\nimport { SILENCE_FRAME, type AudioPlayer } from './AudioPlayer';\nimport { findPipeline, StreamType, TransformerType, type Edge } from './TransformerGraph';\n\n/**\n * Options that are set when creating a new audio resource.\n *\n * @typeParam Metadata - the type for the metadata (if any) of the audio resource\n */\nexport interface CreateAudioResourceOptions {\n\t/**\n\t * Whether or not inline volume should be enabled. If enabled, you will be able to change the volume\n\t * of the stream on-the-fly. However, this also increases the performance cost of playback. Defaults to `false`.\n\t */\n\tinlineVolume?: boolean;\n\n\t/**\n\t * The type of the input stream. Defaults to `StreamType.Arbitrary`.\n\t */\n\tinputType?: StreamType;\n\n\t/**\n\t * Optional metadata that can be attached to the resource (e.g. track title, random id).\n\t * This is useful for identification purposes when the resource is passed around in events.\n\t * See {@link AudioResource.metadata}\n\t */\n\tmetadata?: Metadata;\n\n\t/**\n\t * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches.\n\t * Defaults to 5.\n\t */\n\tsilencePaddingFrames?: number;\n}\n\n/**\n * Represents an audio resource that can be played by an audio player.\n *\n * @typeParam Metadata - the type for the metadata (if any) of the audio resource\n */\nexport class AudioResource {\n\t/**\n\t * An object-mode Readable stream that emits Opus packets. This is what is played by audio players.\n\t */\n\tpublic readonly playStream: Readable;\n\n\t/**\n\t * The pipeline used to convert the input stream into a playable format. For example, this may\n\t * contain an FFmpeg component for arbitrary inputs, and it may contain a VolumeTransformer component\n\t * for resources with inline volume transformation enabled.\n\t */\n\tpublic readonly edges: readonly Edge[];\n\n\t/**\n\t * Optional metadata that can be used to identify the resource.\n\t */\n\tpublic metadata: Metadata;\n\n\t/**\n\t * If the resource was created with inline volume transformation enabled, then this will be a\n\t * prism-media VolumeTransformer. You can use this to alter the volume of the stream.\n\t */\n\tpublic readonly volume?: prism.VolumeTransformer;\n\n\t/**\n\t * If using an Opus encoder to create this audio resource, then this will be a prism-media opus.Encoder.\n\t * You can use this to control settings such as bitrate, FEC, PLP.\n\t */\n\tpublic readonly encoder?: prism.opus.Encoder;\n\n\t/**\n\t * The audio player that the resource is subscribed to, if any.\n\t */\n\tpublic audioPlayer?: AudioPlayer | undefined;\n\n\t/**\n\t * The playback duration of this audio resource, given in milliseconds.\n\t */\n\tpublic playbackDuration = 0;\n\n\t/**\n\t * Whether or not the stream for this resource has started (data has become readable)\n\t */\n\tpublic started = false;\n\n\t/**\n\t * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches.\n\t */\n\tpublic readonly silencePaddingFrames: number;\n\n\t/**\n\t * The number of remaining silence frames to play. If -1, the frames have not yet started playing.\n\t */\n\tpublic silenceRemaining = -1;\n\n\tpublic constructor(\n\t\tedges: readonly Edge[],\n\t\tstreams: readonly Readable[],\n\t\tmetadata: Metadata,\n\t\tsilencePaddingFrames: number,\n\t) {\n\t\tthis.edges = edges;\n\t\tthis.playStream = streams.length > 1 ? (pipeline(streams, noop) as any as Readable) : streams[0]!;\n\t\tthis.metadata = metadata;\n\t\tthis.silencePaddingFrames = silencePaddingFrames;\n\n\t\tfor (const stream of streams) {\n\t\t\tif (stream instanceof prism.VolumeTransformer) {\n\t\t\t\tthis.volume = stream;\n\t\t\t} else if (stream instanceof prism.opus.Encoder) {\n\t\t\t\tthis.encoder = stream;\n\t\t\t}\n\t\t}\n\n\t\tthis.playStream.once('readable', () => (this.started = true));\n\t}\n\n\t/**\n\t * Whether this resource is readable. If the underlying resource is no longer readable, this will still return true\n\t * while there are silence padding frames left to play.\n\t */\n\tpublic get readable() {\n\t\tif (this.silenceRemaining === 0) return false;\n\t\tconst real = this.playStream.readable;\n\t\tif (!real) {\n\t\t\tif (this.silenceRemaining === -1) this.silenceRemaining = this.silencePaddingFrames;\n\t\t\treturn this.silenceRemaining !== 0;\n\t\t}\n\n\t\treturn real;\n\t}\n\n\t/**\n\t * Whether this resource has ended or not.\n\t */\n\tpublic get ended() {\n\t\treturn this.playStream.readableEnded || this.playStream.destroyed || this.silenceRemaining === 0;\n\t}\n\n\t/**\n\t * Attempts to read an Opus packet from the audio resource. If a packet is available, the playbackDuration\n\t * is incremented.\n\t *\n\t * @remarks\n\t * It is advisable to check that the playStream is readable before calling this method. While no runtime\n\t * errors will be thrown, you should check that the resource is still available before attempting to\n\t * read from it.\n\t * @internal\n\t */\n\tpublic read(): Buffer | null {\n\t\tif (this.silenceRemaining === 0) {\n\t\t\treturn null;\n\t\t} else if (this.silenceRemaining > 0) {\n\t\t\tthis.silenceRemaining--;\n\t\t\treturn SILENCE_FRAME;\n\t\t}\n\n\t\tconst packet = this.playStream.read() as Buffer | null;\n\t\tif (packet) {\n\t\t\tthis.playbackDuration += 20;\n\t\t}\n\n\t\treturn packet;\n\t}\n}\n\n/**\n * Ensures that a path contains at least one volume transforming component.\n *\n * @param path - The path to validate constraints on\n */\nexport const VOLUME_CONSTRAINT = (path: Edge[]) => path.some((edge) => edge.type === TransformerType.InlineVolume);\n\nexport const NO_CONSTRAINT = () => true;\n\n/**\n * Tries to infer the type of a stream to aid with transcoder pipelining.\n *\n * @param stream - The stream to infer the type of\n */\nexport function inferStreamType(stream: Readable): {\n\thasVolume: boolean;\n\tstreamType: StreamType;\n} {\n\tif (stream instanceof prism.opus.Encoder) {\n\t\treturn { streamType: StreamType.Opus, hasVolume: false };\n\t} else if (stream instanceof prism.opus.Decoder) {\n\t\treturn { streamType: StreamType.Raw, hasVolume: false };\n\t} else if (stream instanceof prism.VolumeTransformer) {\n\t\treturn { streamType: StreamType.Raw, hasVolume: true };\n\t} else if (stream instanceof prism.opus.OggDemuxer) {\n\t\treturn { streamType: StreamType.Opus, hasVolume: false };\n\t} else if (stream instanceof prism.opus.WebmDemuxer) {\n\t\treturn { streamType: StreamType.Opus, hasVolume: false };\n\t}\n\n\treturn { streamType: StreamType.Arbitrary, hasVolume: false };\n}\n\n/**\n * Creates an audio resource that can be played by audio players.\n *\n * @remarks\n * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used.\n *\n * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created\n * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg,\n * Opus transcoders, and Ogg/WebM demuxers.\n * @param input - The resource to play\n * @param options - Configurable options for creating the resource\n * @typeParam Metadata - the type for the metadata (if any) of the audio resource\n */\nexport function createAudioResource(\n\tinput: Readable | string,\n\toptions: CreateAudioResourceOptions &\n\t\tPick<\n\t\t\tMetadata extends null | undefined\n\t\t\t\t? CreateAudioResourceOptions\n\t\t\t\t: Required>,\n\t\t\t'metadata'\n\t\t>,\n): AudioResource;\n\n/**\n * Creates an audio resource that can be played by audio players.\n *\n * @remarks\n * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used.\n *\n * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created\n * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg,\n * Opus transcoders, and Ogg/WebM demuxers.\n * @param input - The resource to play\n * @param options - Configurable options for creating the resource\n * @typeParam Metadata - the type for the metadata (if any) of the audio resource\n */\nexport function createAudioResource(\n\tinput: Readable | string,\n\toptions?: Omit, 'metadata'>,\n): AudioResource;\n\n/**\n * Creates an audio resource that can be played by audio players.\n *\n * @remarks\n * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used.\n *\n * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created\n * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg,\n * Opus transcoders, and Ogg/WebM demuxers.\n * @param input - The resource to play\n * @param options - Configurable options for creating the resource\n * @typeParam Metadata - the type for the metadata (if any) of the audio resource\n */\nexport function createAudioResource(\n\tinput: Readable | string,\n\toptions: CreateAudioResourceOptions = {},\n): AudioResource {\n\tlet inputType = options.inputType;\n\tlet needsInlineVolume = Boolean(options.inlineVolume);\n\n\t// string inputs can only be used with FFmpeg\n\tif (typeof input === 'string') {\n\t\tinputType = StreamType.Arbitrary;\n\t} else if (inputType === undefined) {\n\t\tconst analysis = inferStreamType(input);\n\t\tinputType = analysis.streamType;\n\t\tneedsInlineVolume = needsInlineVolume && !analysis.hasVolume;\n\t}\n\n\tconst transformerPipeline = findPipeline(inputType, needsInlineVolume ? VOLUME_CONSTRAINT : NO_CONSTRAINT);\n\n\tif (transformerPipeline.length === 0) {\n\t\tif (typeof input === 'string') throw new Error(`Invalid pipeline constructed for string resource '${input}'`);\n\t\t// No adjustments required\n\t\treturn new AudioResource(\n\t\t\t[],\n\t\t\t[input],\n\t\t\t(options.metadata ?? null) as Metadata,\n\t\t\toptions.silencePaddingFrames ?? 5,\n\t\t);\n\t}\n\n\tconst streams = transformerPipeline.map((edge) => edge.transformer(input));\n\tif (typeof input !== 'string') streams.unshift(input);\n\n\treturn new AudioResource(\n\t\ttransformerPipeline,\n\t\tstreams,\n\t\t(options.metadata ?? null) as Metadata,\n\t\toptions.silencePaddingFrames ?? 5,\n\t);\n}\n","import type { Readable } from 'node:stream';\nimport prism from 'prism-media';\n\n/**\n * This module creates a Transformer Graph to figure out what the most efficient way\n * of transforming the input stream into something playable would be.\n */\n\nconst FFMPEG_PCM_ARGUMENTS = ['-analyzeduration', '0', '-loglevel', '0', '-f', 's16le', '-ar', '48000', '-ac', '2'];\nconst FFMPEG_OPUS_ARGUMENTS = [\n\t'-analyzeduration',\n\t'0',\n\t'-loglevel',\n\t'0',\n\t'-acodec',\n\t'libopus',\n\t'-f',\n\t'opus',\n\t'-ar',\n\t'48000',\n\t'-ac',\n\t'2',\n];\n\n/**\n * The different types of stream that can exist within the pipeline.\n */\nexport enum StreamType {\n\t/**\n\t * The type of the stream at this point is unknown.\n\t */\n\tArbitrary = 'arbitrary',\n\t/**\n\t * The stream at this point is Opus audio encoded in an Ogg wrapper.\n\t */\n\tOggOpus = 'ogg/opus',\n\t/**\n\t * The stream at this point is Opus audio, and the stream is in object-mode. This is ready to play.\n\t */\n\tOpus = 'opus',\n\t/**\n\t * The stream at this point is s16le PCM.\n\t */\n\tRaw = 'raw',\n\t/**\n\t * The stream at this point is Opus audio encoded in a WebM wrapper.\n\t */\n\tWebmOpus = 'webm/opus',\n}\n\n/**\n * The different types of transformers that can exist within the pipeline.\n */\nexport enum TransformerType {\n\tFFmpegOgg = 'ffmpeg ogg',\n\tFFmpegPCM = 'ffmpeg pcm',\n\tInlineVolume = 'volume transformer',\n\tOggOpusDemuxer = 'ogg/opus demuxer',\n\tOpusDecoder = 'opus decoder',\n\tOpusEncoder = 'opus encoder',\n\tWebmOpusDemuxer = 'webm/opus demuxer',\n}\n\n/**\n * Represents a pathway from one stream type to another using a transformer.\n */\nexport interface Edge {\n\tcost: number;\n\tfrom: Node;\n\tto: Node;\n\ttransformer(input: Readable | string): Readable;\n\ttype: TransformerType;\n}\n\n/**\n * Represents a type of stream within the graph, e.g. an Opus stream, or a stream of raw audio.\n */\nexport class Node {\n\t/**\n\t * The outbound edges from this node.\n\t */\n\tpublic readonly edges: Edge[] = [];\n\n\t/**\n\t * The type of stream for this node.\n\t */\n\tpublic readonly type: StreamType;\n\n\tpublic constructor(type: StreamType) {\n\t\tthis.type = type;\n\t}\n\n\t/**\n\t * Creates an outbound edge from this node.\n\t *\n\t * @param edge - The edge to create\n\t */\n\tpublic addEdge(edge: Omit) {\n\t\tthis.edges.push({ ...edge, from: this });\n\t}\n}\n\n// Create a node for each stream type\nlet NODES: Map | null = null;\n\n/**\n * Gets a node from its stream type.\n *\n * @param type - The stream type of the target node\n */\nexport function getNode(type: StreamType) {\n\tconst node = (NODES ??= initializeNodes()).get(type);\n\tif (!node) throw new Error(`Node type '${type}' does not exist!`);\n\treturn node;\n}\n\n// Try to enable FFmpeg Ogg optimizations\nfunction canEnableFFmpegOptimizations(): boolean {\n\ttry {\n\t\treturn prism.FFmpeg.getInfo().output.includes('--enable-libopus');\n\t} catch {}\n\n\treturn false;\n}\n\nfunction initializeNodes(): Map {\n\tconst nodes = new Map();\n\tfor (const streamType of Object.values(StreamType)) {\n\t\tnodes.set(streamType, new Node(streamType));\n\t}\n\n\tnodes.get(StreamType.Raw)!.addEdge({\n\t\ttype: TransformerType.OpusEncoder,\n\t\tto: nodes.get(StreamType.Opus)!,\n\t\tcost: 1.5,\n\t\ttransformer: () => new prism.opus.Encoder({ rate: 48_000, channels: 2, frameSize: 960 }),\n\t});\n\n\tnodes.get(StreamType.Opus)!.addEdge({\n\t\ttype: TransformerType.OpusDecoder,\n\t\tto: nodes.get(StreamType.Raw)!,\n\t\tcost: 1.5,\n\t\ttransformer: () => new prism.opus.Decoder({ rate: 48_000, channels: 2, frameSize: 960 }),\n\t});\n\n\tnodes.get(StreamType.OggOpus)!.addEdge({\n\t\ttype: TransformerType.OggOpusDemuxer,\n\t\tto: nodes.get(StreamType.Opus)!,\n\t\tcost: 1,\n\t\ttransformer: () => new prism.opus.OggDemuxer(),\n\t});\n\n\tnodes.get(StreamType.WebmOpus)!.addEdge({\n\t\ttype: TransformerType.WebmOpusDemuxer,\n\t\tto: nodes.get(StreamType.Opus)!,\n\t\tcost: 1,\n\t\ttransformer: () => new prism.opus.WebmDemuxer(),\n\t});\n\n\tconst FFMPEG_PCM_EDGE: Omit = {\n\t\ttype: TransformerType.FFmpegPCM,\n\t\tto: nodes.get(StreamType.Raw)!,\n\t\tcost: 2,\n\t\ttransformer: (input) =>\n\t\t\tnew prism.FFmpeg({\n\t\t\t\targs: ['-i', typeof input === 'string' ? input : '-', ...FFMPEG_PCM_ARGUMENTS],\n\t\t\t}),\n\t};\n\n\tnodes.get(StreamType.Arbitrary)!.addEdge(FFMPEG_PCM_EDGE);\n\tnodes.get(StreamType.OggOpus)!.addEdge(FFMPEG_PCM_EDGE);\n\tnodes.get(StreamType.WebmOpus)!.addEdge(FFMPEG_PCM_EDGE);\n\n\tnodes.get(StreamType.Raw)!.addEdge({\n\t\ttype: TransformerType.InlineVolume,\n\t\tto: nodes.get(StreamType.Raw)!,\n\t\tcost: 0.5,\n\t\ttransformer: () => new prism.VolumeTransformer({ type: 's16le' }),\n\t});\n\n\tif (canEnableFFmpegOptimizations()) {\n\t\tconst FFMPEG_OGG_EDGE: Omit = {\n\t\t\ttype: TransformerType.FFmpegOgg,\n\t\t\tto: nodes.get(StreamType.OggOpus)!,\n\t\t\tcost: 2,\n\t\t\ttransformer: (input) =>\n\t\t\t\tnew prism.FFmpeg({\n\t\t\t\t\targs: ['-i', typeof input === 'string' ? input : '-', ...FFMPEG_OPUS_ARGUMENTS],\n\t\t\t\t}),\n\t\t};\n\t\tnodes.get(StreamType.Arbitrary)!.addEdge(FFMPEG_OGG_EDGE);\n\t\t// Include Ogg and WebM as well in case they have different sampling rates or are mono instead of stereo\n\t\t// at the moment, this will not do anything. However, if/when detection for correct Opus headers is\n\t\t// implemented, this will help inform the voice engine that it is able to transcode the audio.\n\t\tnodes.get(StreamType.OggOpus)!.addEdge(FFMPEG_OGG_EDGE);\n\t\tnodes.get(StreamType.WebmOpus)!.addEdge(FFMPEG_OGG_EDGE);\n\t}\n\n\treturn nodes;\n}\n\n/**\n * Represents a step in the path from node A to node B.\n */\ninterface Step {\n\t/**\n\t * The cost of the steps after this step.\n\t */\n\tcost: number;\n\n\t/**\n\t * The edge associated with this step.\n\t */\n\tedge?: Edge;\n\n\t/**\n\t * The next step.\n\t */\n\tnext?: Step;\n}\n\n/**\n * Finds the shortest cost path from node A to node B.\n *\n * @param from - The start node\n * @param constraints - Extra validation for a potential solution. Takes a path, returns true if the path is valid\n * @param goal - The target node\n * @param path - The running path\n * @param depth - The number of remaining recursions\n */\nfunction findPath(\n\tfrom: Node,\n\tconstraints: (path: Edge[]) => boolean,\n\tgoal = getNode(StreamType.Opus),\n\tpath: Edge[] = [],\n\tdepth = 5,\n): Step {\n\tif (from === goal && constraints(path)) {\n\t\treturn { cost: 0 };\n\t} else if (depth === 0) {\n\t\treturn { cost: Number.POSITIVE_INFINITY };\n\t}\n\n\tlet currentBest: Step | undefined;\n\tfor (const edge of from.edges) {\n\t\tif (currentBest && edge.cost > currentBest.cost) continue;\n\t\tconst next = findPath(edge.to, constraints, goal, [...path, edge], depth - 1);\n\t\tconst cost = edge.cost + next.cost;\n\t\tif (!currentBest || cost < currentBest.cost) {\n\t\t\tcurrentBest = { cost, edge, next };\n\t\t}\n\t}\n\n\treturn currentBest ?? { cost: Number.POSITIVE_INFINITY };\n}\n\n/**\n * Takes the solution from findPath and assembles it into a list of edges.\n *\n * @param step - The first step of the path\n */\nfunction constructPipeline(step: Step) {\n\tconst edges = [];\n\tlet current: Step | undefined = step;\n\twhile (current?.edge) {\n\t\tedges.push(current.edge);\n\t\tcurrent = current.next;\n\t}\n\n\treturn edges;\n}\n\n/**\n * Finds the lowest-cost pipeline to convert the input stream type into an Opus stream.\n *\n * @param from - The stream type to start from\n * @param constraint - Extra constraints that may be imposed on potential solution\n */\nexport function findPipeline(from: StreamType, constraint: (path: Edge[]) => boolean) {\n\treturn constructPipeline(findPath(getNode(from), constraint));\n}\n","/* eslint-disable @typescript-eslint/no-var-requires */\n/* eslint-disable @typescript-eslint/no-require-imports */\nimport { getCiphers } from 'node:crypto';\nimport { resolve, dirname } from 'node:path';\nimport prism from 'prism-media';\n\n/**\n * Tries to find the package.json file for a given module.\n *\n * @param dir - The directory to look in\n * @param packageName - The name of the package to look for\n * @param depth - The maximum recursion depth\n */\nfunction findPackageJSON(\n\tdir: string,\n\tpackageName: string,\n\tdepth: number,\n): { name: string; version: string } | undefined {\n\tif (depth === 0) return undefined;\n\tconst attemptedPath = resolve(dir, './package.json');\n\ttry {\n\t\tconst pkg = require(attemptedPath);\n\t\tif (pkg.name !== packageName) throw new Error('package.json does not match');\n\t\treturn pkg;\n\t} catch {\n\t\treturn findPackageJSON(resolve(dir, '..'), packageName, depth - 1);\n\t}\n}\n\n/**\n * Tries to find the version of a dependency.\n *\n * @param name - The package to find the version of\n */\nfunction version(name: string): string {\n\ttry {\n\t\tif (name === '@discordjs/voice') {\n\t\t\treturn '0.19.2';\n\t\t}\n\n\t\tconst pkg = findPackageJSON(dirname(require.resolve(name)), name, 3);\n\t\treturn pkg?.version ?? 'not found';\n\t} catch {\n\t\treturn 'not found';\n\t}\n}\n\n/**\n * Generates a report of the dependencies used by the \\@discordjs/voice module.\n * Useful for debugging.\n */\nexport function generateDependencyReport() {\n\tconst report = [];\n\tconst addVersion = (name: string) => report.push(`- ${name}: ${version(name)}`);\n\t// general\n\treport.push('Core Dependencies');\n\taddVersion('@discordjs/voice');\n\taddVersion('prism-media');\n\treport.push('');\n\n\t// opus\n\treport.push('Opus Libraries');\n\taddVersion('@discordjs/opus');\n\taddVersion('opusscript');\n\treport.push('');\n\n\t// encryption\n\treport.push('Encryption Libraries');\n\treport.push(`- native crypto support for aes-256-gcm: ${getCiphers().includes('aes-256-gcm') ? 'yes' : 'no'}`);\n\taddVersion('sodium-native');\n\taddVersion('sodium');\n\taddVersion('libsodium-wrappers');\n\taddVersion('@stablelib/xchacha20poly1305');\n\taddVersion('@noble/ciphers');\n\treport.push('');\n\n\t// dave\n\treport.push('DAVE Libraries');\n\taddVersion('@snazzah/davey');\n\treport.push('');\n\n\t// ffmpeg\n\treport.push('FFmpeg');\n\ttry {\n\t\tconst info = prism.FFmpeg.getInfo();\n\t\treport.push(`- version: ${info.version}`);\n\t\treport.push(`- libopus: ${info.output.includes('--enable-libopus') ? 'yes' : 'no'}`);\n\t} catch {\n\t\treport.push('- not found');\n\t}\n\n\treturn ['-'.repeat(50), ...report, '-'.repeat(50)].join('\\n');\n}\n","import { type EventEmitter, once } from 'node:events';\nimport type { VoiceConnection, VoiceConnectionStatus } from '../VoiceConnection';\nimport type { AudioPlayer, AudioPlayerStatus } from '../audio/AudioPlayer';\nimport { abortAfter } from './abortAfter';\n\n/**\n * Allows a voice connection a specified amount of time to enter a given state, otherwise rejects with an error.\n *\n * @param target - The voice connection that we want to observe the state change for\n * @param status - The status that the voice connection should be in\n * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation\n */\nexport function entersState(\n\ttarget: VoiceConnection,\n\tstatus: VoiceConnectionStatus,\n\ttimeoutOrSignal: AbortSignal | number,\n): Promise;\n\n/**\n * Allows an audio player a specified amount of time to enter a given state, otherwise rejects with an error.\n *\n * @param target - The audio player that we want to observe the state change for\n * @param status - The status that the audio player should be in\n * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation\n */\nexport function entersState(\n\ttarget: AudioPlayer,\n\tstatus: AudioPlayerStatus,\n\ttimeoutOrSignal: AbortSignal | number,\n): Promise;\n\n/**\n * Allows a target a specified amount of time to enter a given state, otherwise rejects with an error.\n *\n * @param target - The object that we want to observe the state change for\n * @param status - The status that the target should be in\n * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation\n */\nexport async function entersState(\n\ttarget: Target,\n\tstatus: AudioPlayerStatus | VoiceConnectionStatus,\n\ttimeoutOrSignal: AbortSignal | number,\n) {\n\tif (target.state.status !== status) {\n\t\tconst [ac, signal] =\n\t\t\ttypeof timeoutOrSignal === 'number' ? abortAfter(timeoutOrSignal) : [undefined, timeoutOrSignal];\n\t\ttry {\n\t\t\tawait once(target as EventEmitter, status, { signal });\n\t\t} finally {\n\t\t\tac?.abort();\n\t\t}\n\t}\n\n\treturn target;\n}\n","/**\n * Creates an abort controller that aborts after the given time.\n *\n * @param delay - The time in milliseconds to wait before aborting\n */\nexport function abortAfter(delay: number): [AbortController, AbortSignal] {\n\tconst ac = new AbortController();\n\tconst timeout = setTimeout(() => ac.abort(), delay);\n\tac.signal.addEventListener('abort', () => clearTimeout(timeout));\n\treturn [ac, ac.signal];\n}\n","import { Buffer } from 'node:buffer';\nimport process from 'node:process';\nimport { Readable } from 'node:stream';\nimport prism from 'prism-media';\nimport { StreamType } from '..';\nimport { noop } from './util';\n\n/**\n * Takes an Opus Head, and verifies whether the associated Opus audio is suitable to play in a Discord voice channel.\n *\n * @param opusHead - The Opus Head to validate\n * @returns `true` if suitable to play in a Discord voice channel, otherwise `false`\n */\nexport function validateDiscordOpusHead(opusHead: Buffer): boolean {\n\tconst channels = opusHead.readUInt8(9);\n\tconst sampleRate = opusHead.readUInt32LE(12);\n\treturn channels === 2 && sampleRate === 48_000;\n}\n\n/**\n * The resulting information after probing an audio stream\n */\nexport interface ProbeInfo {\n\t/**\n\t * The readable audio stream to use. You should use this rather than the input stream, as the probing\n\t * function can sometimes read the input stream to its end and cause the stream to close.\n\t */\n\tstream: Readable;\n\n\t/**\n\t * The recommended stream type for this audio stream.\n\t */\n\ttype: StreamType;\n}\n\n/**\n * Attempt to probe a readable stream to figure out whether it can be demuxed using an Ogg or WebM Opus demuxer.\n *\n * @param stream - The readable stream to probe\n * @param probeSize - The number of bytes to attempt to read before giving up on the probe\n * @param validator - The Opus Head validator function\n * @experimental\n */\nexport async function demuxProbe(\n\tstream: Readable,\n\tprobeSize = 1_024,\n\tvalidator = validateDiscordOpusHead,\n): Promise {\n\treturn new Promise((resolve, reject) => {\n\t\t// Preconditions\n\t\tif (stream.readableObjectMode) {\n\t\t\treject(new Error('Cannot probe a readable stream in object mode'));\n\t\t\treturn;\n\t\t}\n\n\t\tif (stream.readableEnded) {\n\t\t\treject(new Error('Cannot probe a stream that has ended'));\n\t\t\treturn;\n\t\t}\n\n\t\tlet readBuffer = Buffer.alloc(0);\n\n\t\tlet resolved: StreamType | undefined;\n\n\t\tconst finish = (type: StreamType) => {\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-use-before-define\n\t\t\tstream.off('data', onData);\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-use-before-define\n\t\t\tstream.off('close', onClose);\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-use-before-define\n\t\t\tstream.off('end', onClose);\n\t\t\tstream.pause();\n\t\t\tresolved = type;\n\t\t\tif (stream.readableEnded) {\n\t\t\t\tresolve({\n\t\t\t\t\tstream: Readable.from(readBuffer),\n\t\t\t\t\ttype,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tif (readBuffer.length > 0) {\n\t\t\t\t\tstream.push(readBuffer);\n\t\t\t\t}\n\n\t\t\t\tresolve({\n\t\t\t\t\tstream,\n\t\t\t\t\ttype,\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\n\t\tconst foundHead = (type: StreamType) => (head: Buffer) => {\n\t\t\tif (validator(head)) {\n\t\t\t\tfinish(type);\n\t\t\t}\n\t\t};\n\n\t\tconst webm = new prism.opus.WebmDemuxer();\n\t\twebm.once('error', noop);\n\t\twebm.on('head', foundHead(StreamType.WebmOpus));\n\n\t\tconst ogg = new prism.opus.OggDemuxer();\n\t\togg.once('error', noop);\n\t\togg.on('head', foundHead(StreamType.OggOpus));\n\n\t\tconst onClose = () => {\n\t\t\tif (!resolved) {\n\t\t\t\tfinish(StreamType.Arbitrary);\n\t\t\t}\n\t\t};\n\n\t\tconst onData = (buffer: Buffer) => {\n\t\t\treadBuffer = Buffer.concat([readBuffer, buffer]);\n\n\t\t\twebm.write(buffer);\n\t\t\togg.write(buffer);\n\n\t\t\tif (readBuffer.length >= probeSize) {\n\t\t\t\tstream.off('data', onData);\n\t\t\t\tstream.pause();\n\t\t\t\tprocess.nextTick(onClose);\n\t\t\t}\n\t\t};\n\n\t\tstream.once('error', reject);\n\t\tstream.on('data', onData);\n\t\tstream.once('close', onClose);\n\t\tstream.once('end', onClose);\n\t});\n}\n","export * from './joinVoiceChannel';\nexport * from './audio/index';\nexport * from './util/index';\nexport * from './receive/index';\n\nexport {\n\tNetworking,\n\ttype ConnectionData,\n\ttype ConnectionOptions,\n\ttype NetworkingState,\n\ttype NetworkingResumingState,\n\ttype NetworkingSelectingProtocolState,\n\ttype NetworkingUdpHandshakingState,\n\ttype NetworkingClosedState,\n\ttype NetworkingIdentifyingState,\n\ttype NetworkingOpeningWsState,\n\ttype NetworkingReadyState,\n\tNetworkingStatusCode,\n\tVoiceUDPSocket,\n\tVoiceWebSocket,\n\ttype SocketConfig,\n\tDAVESession,\n} from './networking/index.js';\n\nexport {\n\tVoiceConnection,\n\ttype VoiceConnectionState,\n\tVoiceConnectionStatus,\n\ttype VoiceConnectionConnectingState,\n\ttype VoiceConnectionDestroyedState,\n\ttype VoiceConnectionDisconnectedState,\n\ttype VoiceConnectionDisconnectedBaseState,\n\ttype VoiceConnectionDisconnectedOtherState,\n\ttype VoiceConnectionDisconnectedWebSocketState,\n\tVoiceConnectionDisconnectReason,\n\ttype VoiceConnectionReadyState,\n\ttype VoiceConnectionSignallingState,\n} from './VoiceConnection';\n\nexport { type JoinConfig, getVoiceConnection, getVoiceConnections, getGroups } from './DataStore';\n\n/**\n * The {@link https://github.com/discordjs/discord.js/blob/main/packages/voice#readme | @discordjs/voice} version\n * that you are currently using.\n */\n// This needs to explicitly be `string` so it is not typed as a \"const string\" that gets injected by esbuild\nexport const version = '0.19.2' as string;\n"],"mappings":"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","names":["EventEmitter","Buffer","EventEmitter","VoiceOpcodes","Buffer","nonce","Buffer","crypto","resolve","Buffer","EventEmitter","Buffer","Buffer","NoSubscriberBehavior","AudioPlayerStatus","EventEmitter","Buffer","Buffer","EventEmitter","Buffer","EventEmitter","resolve","Buffer","EventEmitter","EventEmitter","Buffer","nonce","NetworkingStatusCode","Buffer","stringifyState","option","EventEmitter","VoiceOpcodes","Buffer","crypto","VoiceOpcodes","EndBehaviorType","EventEmitter","EventEmitter","EventEmitter","EventEmitter","Buffer","VoiceOpcodes","nonce","crypto","VoiceConnectionStatus","VoiceConnectionDisconnectReason","EventEmitter","prism","StreamType","TransformerType","prism","prism","prism","Buffer","process","Readable","prism","resolve","Buffer","Readable","prism","process","version"]} \ No newline at end of file diff --git a/services/discord-gateway/vendor/discord-voice-fork/package.json b/services/discord-gateway/vendor/discord-voice-fork/package.json deleted file mode 100644 index 9d708bdb..00000000 --- a/services/discord-gateway/vendor/discord-voice-fork/package.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "name": "@discordjs/voice", - "version": "0.19.2", - "description": "Implementation of the Discord Voice API for Node.js", - "exports": { - ".": { - "require": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "import": { - "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" - } - } - }, - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "directories": { - "lib": "src", - "test": "__tests__" - }, - "files": [ - "dist" - ], - "contributors": [ - "Crawl ", - "Amish Shah ", - "SpaceEEC ", - "Vlad Frangu ", - "Aura Román " - ], - "license": "Apache-2.0", - "keywords": [ - "discord", - "discord.js", - "audio", - "voice", - "streaming" - ], - "repository": { - "type": "git", - "url": "https://github.com/discordjs/discord.js.git", - "directory": "packages/voice" - }, - "bugs": { - "url": "https://github.com/discordjs/discord.js/issues" - }, - "homepage": "https://discord.js.org", - "funding": "https://github.com/discordjs/discord.js?sponsor", - "dependencies": { - "@snazzah/davey": "^0.1.9", - "@types/ws": "^8.18.1", - "discord-api-types": "^0.38.41", - "prism-media": "^1.3.5", - "tslib": "^2.8.1", - "ws": "^8.19.0" - }, - "devDependencies": { - "@discordjs/opus": "^0.10.0", - "@favware/cliff-jumper": "^6.0.0", - "@noble/ciphers": "^2.1.1", - "@types/node": "^22.19.11", - "@vitest/coverage-v8": "^4.0.18", - "cross-env": "^10.1.0", - "esbuild-plugin-version-injector": "^1.2.1", - "eslint": "^9.39.2", - "eslint-config-neon": "^0.3.2", - "eslint-formatter-compact": "^9.0.1", - "eslint-formatter-pretty": "^7.0.0", - "prettier": "^3.8.1", - "tsup": "^8.5.1", - "turbo": "^2.8.10", - "typescript": "~5.9.3", - "vitest": "^4.0.18", - "vitest-websocket-mock": "^0.5.0", - "@discordjs/api-extractor": "^7.52.7", - "@discordjs/scripts": "^0.1.0" - }, - "engines": { - "node": ">=22.12.0" - }, - "publishConfig": { - "access": "public", - "provenance": true - }, - "scripts": { - "build": "tsc --noEmit && tsup && node scripts/postbuild.mjs", - "build:docs": "tsc -p tsconfig.docs.json", - "test": "vitest run --config ../../vitest.config.ts", - "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", - "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", - "fmt": "pnpm run format", - "docs": "pnpm run build:docs && api-extractor run --local --minify && generate-split-documentation", - "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/voice/*'", - "release": "cliff-jumper" - } -} \ No newline at end of file