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:
co-authored by
Claude Opus 4.8
parent
b68789fffc
commit
07032ab521
@@ -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)], {
|
||||
|
||||
Reference in New Issue
Block a user