refactor: atomic, DRY, and logging improvements across codebase

- Split llmModerationClient.ts (2170 lines) into 5 focused sub-modules
- Split aiAnalyzer.ts (1282 lines) into 4 modular pipelines
- Split messages.db.ts (826 lines) into 5 domain-specific modules
- Moved shared schema to @bete/shared, eliminated backend duplication
- Added createChildLogger to all voice-recording and AI moderation modules
- Extracted tryCommandThenFallback, normalizeMediaState, DEFAULT_VOICE_STATUS
- Created shared pagination.ts utility, eliminated 5+ cursor-pagination duplications
- Created shared messageMapper.ts for row mapping
- Standardized backend error handling with asyncHandler
- Added frontend createLogger utility and useAsyncAction hook
- Added structured logging to frontend hooks, socket, and API client

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-09 19:46:08 +07:00
co-authored by Claude Opus 4.8
parent b68789fffc
commit 07032ab521
61 changed files with 3808 additions and 3043 deletions
@@ -1,4 +1,7 @@
import { spawn } from "node:child_process";
import { createChildLogger } from "@bete/shared/logger";
const logger = createChildLogger("ffmpeg-process");
export interface MuxFfmpegArgsOptions {
inputs: string[];
@@ -42,19 +45,24 @@ export function buildMuxFfmpegArgs(options: MuxFfmpegArgsOptions): string[] {
*/
export function runFfmpeg(args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
logger.debug({ args }, "Starting ffmpeg");
const proc = spawn("ffmpeg", args, {
stdio: ["ignore", "inherit", "inherit"],
});
proc.on("close", (code) => {
if (code === 0) {
logger.debug("ffmpeg completed successfully");
resolve();
} else {
logger.warn({ exitCode: code }, "ffmpeg exited with non-zero code");
reject(new Error(`ffmpeg exited with code ${code}`));
}
});
proc.on("error", (err) => {
logger.error({ error: err.message }, "ffmpeg process error");
reject(err);
});
});
@@ -1,4 +1,7 @@
import { Transform, TransformCallback } from "node:stream";
import { createChildLogger } from "@bete/shared/logger";
const logger = createChildLogger("packet-filter");
/**
* Transform stream to filter out audio packets that are too small.
@@ -31,6 +34,11 @@ export class PacketFilter extends Transform {
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();
}
@@ -1,6 +1,9 @@
import { createChildLogger } from "@bete/shared/logger";
import { EndBehaviorType, type VoiceReceiver } from "@discordjs/voice";
import { config } from "../../../shared/config/config.js";
const logger = createChildLogger("audio-stream");
export interface AudioStreamHandlers {
onPacket: (chunk: Buffer) => void;
onEnd: () => void;
@@ -12,6 +15,8 @@ export function subscribeToAudioStream(
userId: string,
handlers: AudioStreamHandlers,
): NodeJS.ReadableStream {
logger.debug({ userId }, "Subscribing to audio stream");
const audioStream = receiver.subscribe(userId, {
end: {
behavior: EndBehaviorType.AfterSilence,
@@ -20,8 +25,14 @@ export function subscribeToAudioStream(
});
audioStream.on("data", handlers.onPacket);
audioStream.on("end", handlers.onEnd);
audioStream.on("error", handlers.onError);
audioStream.on("end", () => {
logger.debug({ userId }, "Audio stream ended");
handlers.onEnd();
});
audioStream.on("error", (error: Error) => {
logger.warn({ userId, error: error.message }, "Audio stream error");
handlers.onError(error);
});
return audioStream;
}
@@ -1,6 +1,10 @@
import path from "node:path";
import { createChildLogger } from "@bete/shared/logger";
import type { Client, VoiceChannel } from "discord.js-selfbot-v13";
import { config } from "../../../shared/config/config.js";
const logger = createChildLogger("voice-metadata");
import type {
SegmentMetadata,
SegmentState,
@@ -12,12 +16,20 @@ export async function collectUserMetadata(
userId: string,
channel: VoiceChannel,
): Promise<UserMetadata> {
logger.debug({ userId }, "Collecting user metadata");
const user =
client.users.cache.get(userId) ||
(await client.users.fetch(userId).catch(() => null));
(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(() => null));
(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
@@ -1,8 +1,11 @@
import fs from "node:fs";
import path from "node:path";
import { createChildLogger } from "@bete/shared/logger";
import * as prism from "prism-media";
import type { SegmentState } from "../../message-capture/types.js";
const logger = createChildLogger("segment");
export function buildSegmentPaths(
userDir: string,
startTime: number,
@@ -54,6 +57,11 @@ export class SegmentManager {
oggStream,
out,
};
logger.debug(
{ index, startTime, filename, userDir: this.userDir },
"Segment opened",
);
return this.currentSegment;
}
@@ -64,6 +72,25 @@ export class SegmentManager {
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;
}
@@ -77,6 +104,15 @@ export class SegmentManager {
)
)
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);
}
@@ -1,11 +1,14 @@
import fs, { promises as fsPromises } from "node:fs";
import path from "node:path";
import { createChildLogger } from "@bete/shared/logger";
import type { UserMetadata } from "../../message-capture/types.js";
import {
buildMuxFfmpegArgs,
runFfmpeg as defaultRunFfmpeg,
} from "../ffmpegProcess.js";
const logger = createChildLogger("recording-session");
export type SessionRecordingStatus =
| "pending"
| "completed"
@@ -86,6 +89,16 @@ export function createRecordingSession(
const participants = new Map<string, SessionParticipant>();
const segments: SessionSegmentRef[] = [];
logger.info(
{
sessionId,
guildId: options.guildId,
channelId: options.channelId,
channelName: options.channelName,
},
"Recording session created",
);
return {
sessionId,
recordingsDir: options.recordingsDir,
@@ -108,6 +121,10 @@ export function createRecordingSession(
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 {
@@ -132,6 +149,11 @@ export function buildSessionMuxFilter(
segments: Array<{ startTime: number }>,
sessionStartTime: number,
): string {
if (segments.length === 0) {
logger.debug("Building mux filter with no segments");
return "";
}
const filters = segments.map((segment, index) => {
const delayMs = Math.max(0, segment.startTime - sessionStartTime);
return `[${index}:a]adelay=${delayMs}|${delayMs}[pad${index}]`;
@@ -140,6 +162,11 @@ export function buildSessionMuxFilter(
filters.push(
`${inputs}amix=inputs=${segments.length}:dropout_transition=0[out]`,
);
logger.debug(
{ segmentCount: segments.length, filter: filters.join(";") },
"Built mux filter",
);
return filters.join(";");
}
@@ -166,26 +193,66 @@ export async function finalizeRecordingSession(
await mkdir(sessionDir);
const metadata = session.snapshot(endTime);
logger.info(
{
sessionId: session.sessionId,
segmentCount: metadata.segments.length,
outputFile,
},
"Finalizing recording session",
);
if (metadata.segments.length === 0) {
await writeJson(metadataFile, { ...metadata, status: "empty" });
logger.info(
{ sessionId: session.sessionId },
"Recording session finalized with no segments",
);
return;
}
try {
await runFfmpeg(
buildMuxFfmpegArgs({
inputs: metadata.segments.map((segment) => segment.oggPath),
filter: buildSessionMuxFilter(metadata.segments, metadata.startTime),
output: outputFile,
codec: "libopus",
}),
const ffmpegArgs = buildMuxFfmpegArgs({
inputs: metadata.segments.map((segment) => segment.oggPath),
filter: buildSessionMuxFilter(metadata.segments, metadata.startTime),
output: outputFile,
codec: "libopus",
});
logger.debug(
{ sessionId: session.sessionId, ffmpegArgs },
"Running FFmpeg mux for session",
);
await runFfmpeg(ffmpegArgs);
// Get output file size
let outputSize = 0;
try {
const outStat = await fsPromises.stat(outputFile);
outputSize = outStat.size;
} catch {
// File might not exist yet, ignore
}
await writeJson(metadataFile, {
...metadata,
status: "completed",
outputFile,
});
logger.info(
{ sessionId: session.sessionId, outputFile, outputSize },
"Recording session finalized successfully",
);
} catch (error) {
logger.error(
{
sessionId: session.sessionId,
error: error instanceof Error ? error.message : String(error),
},
"Failed to finalize recording session via FFmpeg",
);
await writeJson(metadataFile, {
...metadata,
status: "failed",
@@ -1,5 +1,8 @@
import { createChildLogger } from "@bete/shared/logger";
import { retryWithBackoff } from "@bete/shared/utils";
const logger = createChildLogger("tele-upload");
export interface TeleUploadResponse {
download_url: string;
public_id?: string;
@@ -40,6 +43,8 @@ export async function uploadToTele(input: {
const { buffer, filename, contentType, uploadUrl, timeoutMs, retries } =
input;
logger.debug({ filename, uploadUrl }, "Starting tele upload");
const response = await retryWithBackoff(
async () => {
const fileBlob = new Blob([new Uint8Array(buffer)], {