From 3ae28157a3ba2d8aeb07a5a8006fbedc51800688 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Wed, 13 May 2026 16:25:01 +0700 Subject: [PATCH] feat: implement logging and retry mechanism with pino and p-retry --- bun.lockb | Bin 135033 -> 135968 bytes package.json | 8 ++--- src/index.ts | 28 +++++++++------- src/logger.ts | 22 +++++++++++++ src/recorder.ts | 83 +++++++++++++++++++++++++++-------------------- src/retry.ts | 42 ++++++++++++++++++++++++ src/webserver.ts | 26 +++++++-------- 7 files changed, 144 insertions(+), 65 deletions(-) create mode 100644 src/logger.ts create mode 100644 src/retry.ts diff --git a/bun.lockb b/bun.lockb index 3cca5751de5fa505ced52be0a293c896f31c8449..784d3c2593c7c10eecd11e2b7f4cb5a514103372 100755 GIT binary patch delta 26442 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
    ehBqea%uO1_4~qVMxfKa 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 C_>Hpw delta 26556 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 Ouo%mt*E&rt-Twio*gLua diff --git a/package.json b/package.json index 75d5681..01a59e0 100644 --- a/package.json +++ b/package.json @@ -15,18 +15,16 @@ "dependencies": { "@discordjs/opus": "^0.10.0", "@discordjs/voice": "^0.19.1", - "@ffmpeg-installer/ffmpeg": "^1.1.0", "@snazzah/davey": "^0.1.10", - "crc-32": "^1.2.2", "discord.js-selfbot-v13": "^3.7.1", "express": "^5.2.1", - "ffmpeg-static": "^5.3.0", "fluent-ffmpeg": "^2.1.3", "libsodium-wrappers": "^0.8.2", - "node-crc": "^4.0.0", "prism-media": "2.0.0-alpha.0", "sodium-native": "^4.3.2", - "ws": "^8.20.1" + "ws": "^8.20.1", + "pino": "^9.4.0", + "p-retry": "^6.2.0" }, "devDependencies": { "@biomejs/biome": "latest", diff --git a/src/index.ts b/src/index.ts index de79fa7..d5a0ec8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,10 @@ import { config } from "./config"; import { discordPlayer } from "./player"; import { startRecording } from "./recorder"; import { startWebserver } from "./webserver"; +import { createChildLogger } from "./logger"; +import { retryWithBackoff } from "./retry"; + +const logger = createChildLogger("bot"); // Validasi environment variables const token = process.env.DISCORD_TOKEN; @@ -17,18 +21,18 @@ if (!token) throw new Error("Missing DISCORD_TOKEN in .env"); if (!voiceChannelId) throw new Error("Missing VOICE_CHANNEL_ID in .env"); if (!guildId) throw new Error("Missing GUILD_ID in .env"); -// Inisialisasi selfbot client (gunakan checkUpdate: false supaya tidak ada prompt update) +// Inisialisasi selfbot client const client = new Client(); client.on("ready", async () => { if (config.verbose) { - console.log(`[bot] Logged in as ${client.user!.tag}`); + logger.info({ user: client.user?.tag }, "Bot logged in"); } // Ambil guild const guild = client.guilds.cache.get(guildId!); if (!guild) { - console.error(`[bot] Guild not found: ${guildId}`); + logger.error({ guildId }, "Guild not found"); process.exit(1); } @@ -38,24 +42,24 @@ client.on("ready", async () => { (await guild.channels.fetch(voiceChannelId!).catch(() => null)); if (!channel || channel.type !== "GUILD_VOICE") { - console.error( - `[bot] Voice channel not found or wrong type: ${voiceChannelId}`, - ); + logger.error({ voiceChannelId }, "Voice channel not found or wrong type"); process.exit(1); } if (config.verbose) { - console.log( - `[bot] Joining voice channel: #${channel.name} (${channel.id})`, + logger.info( + { channelName: channel.name, channelId: channel.id }, + "Joining voice channel", ); } + await startRecording(client, channel as any); // Set up player connection const connection = getVoiceConnection(guildId!); if (connection) { discordPlayer.setConnection(connection); - console.log("[bot] Player connected to voice channel"); + logger.info("Player connected to voice channel"); } // Start Webserver @@ -63,13 +67,13 @@ client.on("ready", async () => { }); client.on("error", (err) => { - console.error("[bot] Client error:", err); + logger.error({ error: err }, "Client error"); }); // Graceful shutdown process.on("SIGINT", () => { if (config.verbose) { - console.log("\n[bot] Shutting down..."); + logger.info("Shutting down gracefully..."); } client.destroy(); process.exit(0); @@ -77,7 +81,7 @@ process.on("SIGINT", () => { process.on("SIGTERM", () => { if (config.verbose) { - console.log("[bot] Terminating..."); + logger.info("Terminating..."); } client.destroy(); process.exit(0); diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..ea65876 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,22 @@ +import pino from "pino"; + +const isDev = process.env.NODE_ENV !== "production"; + +export const logger = pino({ + level: process.env.LOG_LEVEL || (isDev ? "debug" : "info"), + transport: isDev + ? { + target: "pino-pretty", + options: { + colorize: true, + translateTime: "SYS:standard", + ignore: "pid,hostname", + }, + } + : undefined, + timestamp: pino.stdTimeFunctions.isoTime, +}); + +export const createChildLogger = (context: string) => { + return logger.child({ context }); +}; diff --git a/src/recorder.ts b/src/recorder.ts index 8d064e7..a208991 100644 --- a/src/recorder.ts +++ b/src/recorder.ts @@ -8,7 +8,6 @@ import { VoiceConnectionStatus, } from "@discordjs/voice"; import type { Client, VoiceChannel } from "discord.js-selfbot-v13"; -import prism from "prism-media"; import { config } from "./config"; import { PacketFilter } from "./packetFilter"; import { subscribeToAudioStream } from "./recorder/audioStream"; @@ -19,6 +18,10 @@ import { } from "./recorder/metadata"; import { SegmentManager } from "./recorder/segment"; import type { PcmBroadcaster } from "./types"; +import { createChildLogger } from "./logger"; +import { retryWithBackoff } from "./retry"; + +const logger = createChildLogger("recorder"); const recordingsDir = config.recordingsDir; @@ -43,32 +46,37 @@ export async function startRecording( debug: true, }); - if (config.verbose) { - console.log(`[recorder] Joining voice channel: #${channel.name}`); - } + logger.info({ channelName: channel.name }, "Joining voice channel"); connection.on("debug", (msg) => { if (config.verbose) { - console.log(`[voice-debug] ${msg}`); + logger.debug({ message: msg }, "Voice debug"); } }); connection.on("error", (err) => { - console.error(`[voice-error]`, err); + logger.error({ error: err }, "Voice connection error"); }); - // Tunggu sampai benar-benar terhubung + // Tunggu sampai benar-benar terhubung dengan retry logic try { - await entersState( - connection, - VoiceConnectionStatus.Ready, - config.voiceConnectionTimeoutMs, + await retryWithBackoff( + () => + entersState( + connection, + VoiceConnectionStatus.Ready, + config.voiceConnectionTimeoutMs, + ), + { + retries: 3, + minTimeout: 1000, + maxTimeout: 5000, + logger, + }, ); - if (config.verbose) { - console.log("[recorder] Connected to voice channel. Recording started."); - } + logger.info("Connected to voice channel. Recording started"); } catch (err) { - console.error("[recorder] Failed to connect:", err); + logger.error({ error: err }, "Failed to connect to voice channel"); connection.destroy(); return; } @@ -79,7 +87,7 @@ export async function startRecording( // Dengarkan siapapun yang mulai bicara receiver.speaking.on("start", async (userId) => { const userMetadata = await collectUserMetadata(client, userId, channel); - console.log(`${userMetadata.username} [voice activity]`); + logger.info({ userId, username: userMetadata.username }, "Voice activity detected"); // Notify webserver broadcaster.updateActiveUser?.(userId, { @@ -132,7 +140,7 @@ export async function startRecording( let currentSegment = segmentManager.open(oggPacketStream); currentSegment.out.on("finish", () => { if (config.verbose) { - console.log(`[recorder] Saved: ${currentSegment.filename}`); + logger.info({ filename: currentSegment.filename }, "Segment saved"); } const metadata = createSegmentMetadata( userMetadata, @@ -146,14 +154,18 @@ export async function startRecording( JSON.stringify(metadata, null, 2), ); if (config.verbose) { - console.log( - `[recorder] Saved metadata: ${currentSegment.jsonFilename}`, + logger.info( + { jsonFile: currentSegment.jsonFilename }, + "Metadata saved", ); } }); currentSegment.out.on("error", (err) => { - console.error(`[recorder] File write error ${userId}:`, err.message); + logger.error( + { userId, error: err.message }, + "File write error", + ); }); // Feed Opus packets one-by-one @@ -166,7 +178,7 @@ export async function startRecording( decoder.write(chunk); }, onEnd: () => { - const segment = segmentManager.close(oggPacketStream); + segmentManager.close(oggPacketStream); decoder.destroy(); broadcaster.updateActiveUser?.(userId, { username: userMetadata.username, @@ -177,31 +189,32 @@ export async function startRecording( onError: (error) => { segmentManager.close(oggPacketStream); decoder.destroy(); - console.error( - `[recorder] Audio Stream error ${userId}:`, - error.message, + logger.error( + { userId, error: error.message }, + "Audio stream error", ); }, }); packetFilterForOgg.on("error", (err) => { segmentManager.close(oggPacketStream); - console.error( - `[recorder] PacketFilter(ogg) error ${userId}:`, - err.message, + logger.error( + { userId, error: err.message }, + "PacketFilter error", ); }); } catch (e) { - console.error(`[recorder] Failed to create stream for ${userId}:`, e); + logger.error( + { userId, error: e instanceof Error ? e.message : String(e) }, + "Failed to create stream", + ); } }); // Handle disconnect yang tidak disengaja connection.on(VoiceConnectionStatus.Disconnected, async () => { if (config.verbose) { - console.warn( - "[recorder] Disconnected from voice channel. Reconnecting...", - ); + logger.warn("Disconnected from voice channel. Reconnecting..."); } try { await Promise.race([ @@ -218,14 +231,14 @@ export async function startRecording( ]); // Berhasil reconnect } catch { - console.error("[recorder] Could not reconnect. Destroying connection."); + logger.error("Could not reconnect. Destroying connection"); connection.destroy(); } }); connection.on(VoiceConnectionStatus.Destroyed, () => { if (config.verbose) { - console.log("[recorder] Voice connection destroyed."); + logger.info("Voice connection destroyed"); } }); } @@ -238,9 +251,9 @@ export function stopRecording(guildId: string): void { if (connection) { connection.destroy(); if (config.verbose) { - console.log("[recorder] Recording stopped and disconnected."); + logger.info("Recording stopped and disconnected"); } } else { - console.warn("[recorder] No active connection to stop."); + logger.warn("No active connection to stop"); } } diff --git a/src/retry.ts b/src/retry.ts new file mode 100644 index 0000000..4934b67 --- /dev/null +++ b/src/retry.ts @@ -0,0 +1,42 @@ +import pRetry from "p-retry"; +import type { Logger } from "pino"; + +export interface RetryOptions { + retries?: number; + minTimeout?: number; + maxTimeout?: number; + factor?: number; + logger?: Logger; +} + +export async function retryWithBackoff( + fn: () => Promise, + options: RetryOptions = {}, +): Promise { + const { + retries = 3, + minTimeout = 1000, + maxTimeout = 30000, + factor = 2, + logger, + } = options; + + return pRetry(fn, { + retries, + minTimeout, + maxTimeout, + factor, + onFailedAttempt: (error) => { + if (logger) { + logger.warn( + { + attempt: error.attemptNumber, + retriesLeft: error.retriesLeft, + error: error.message, + }, + "Retry attempt", + ); + } + }, + }); +} diff --git a/src/webserver.ts b/src/webserver.ts index 558fc40..f28b1e5 100644 --- a/src/webserver.ts +++ b/src/webserver.ts @@ -4,6 +4,9 @@ import path from "path"; import prism from "prism-media"; import { WebSocketServer } from "ws"; import { discordPlayer } from "./player"; +import { createChildLogger } from "./logger"; + +const logger = createChildLogger("webserver"); const activeUsers = new Map< string, @@ -42,9 +45,7 @@ export function startWebserver(port: number = 3000) { const wsPort = port + 1; const wss = new WebSocketServer({ port: wsPort, host: "0.0.0.0" }); - console.log( - `[webserver] WebSocket server listening on ws://0.0.0.0:${wsPort}`, - ); + logger.info({ wsPort }, "WebSocket server listening"); app.use(express.static(path.join(__dirname, "../public"))); @@ -124,8 +125,9 @@ export function startWebserver(port: number = 3000) { setInterval(() => { if (dbCount > 0) { const avg = dbAccum / dbCount; - console.log( - `[transmit] Audio level: ${avg.toFixed(1)} dBFS (${dbCount} frames/2s)`, + logger.info( + { level: avg.toFixed(1), frames: dbCount }, + "Audio level", ); dbAccum = 0; dbCount = 0; @@ -140,8 +142,8 @@ export function startWebserver(port: number = 3000) { if (pcmBuffer.length >= BYTES_PER_FRAME) { // Real audio available - frame = pcmBuffer.slice(0, BYTES_PER_FRAME); - pcmBuffer = pcmBuffer.slice(BYTES_PER_FRAME); + frame = pcmBuffer.subarray(0, BYTES_PER_FRAME); + pcmBuffer = pcmBuffer.subarray(BYTES_PER_FRAME); // Track level for logging dbAccum += rmsDb(frame); @@ -150,7 +152,7 @@ export function startWebserver(port: number = 3000) { if (playerPaused) { discordPlayer.unpause(); playerPaused = false; - console.log("[transmit] Transmitting — Discord indicator ON"); + logger.info("Transmitting — Discord indicator ON"); } } else if (msSinceAudio < SILENCE_TAIL_MS && msSinceAudio > 0) { // Buffer drained but audio was recent — pad silence to avoid OGG gap @@ -159,7 +161,7 @@ export function startWebserver(port: number = 3000) { // No audio for a while — pause Discord indicator discordPlayer.pause(); playerPaused = true; - console.log("[transmit] Stopped — Discord indicator OFF"); + logger.info("Stopped — Discord indicator OFF"); return; } else { return; // already paused, nothing to do @@ -173,7 +175,7 @@ export function startWebserver(port: number = 3000) { }, 20); wss.on("connection", (ws) => { - console.log("[webserver] New WebSocket connection on port " + wsPort); + logger.info({ wsPort }, "New WebSocket connection"); wsClients.add(ws); ws.send( @@ -208,8 +210,6 @@ export function startWebserver(port: number = 3000) { }); server.listen(port, "0.0.0.0", () => { - console.log( - `[webserver] Web interface listening on http://0.0.0.0:${port}`, - ); + logger.info({ port }, "Web interface listening"); }); }