feat: enhance configuration management and add error handling

This commit is contained in:
MythEclipse
2026-05-13 16:57:07 +07:00
parent 673a06376c
commit 978c2c468d
14 changed files with 243 additions and 115 deletions
+39 -54
View File
@@ -1,61 +1,46 @@
// Configuration for the bot
export interface AppConfig {
verbose: boolean;
recordingsDir: string;
recordingSegmentMs: number;
decoderRotateMs: number;
decoderCooldownMs: number;
webserverPort: number;
voiceConnectionTimeoutMs: number;
reconnectTimeoutMs: number;
audioStreamSilenceDurationMs: number;
packetFilterMinSize: number;
opusFrameSize: number;
audioSampleRate: number;
audioChannels: number;
avatarSize: number;
}
import { z } from "zod";
import { ConfigError } from "./errors";
export function parseBoolean(
value: string | undefined,
fallback: boolean,
): boolean {
if (value === "true") return true;
if (value === "false") return false;
return fallback;
}
const configSchema = z.object({
DISCORD_TOKEN: z.string().min(1, "DISCORD_TOKEN is required"),
VOICE_CHANNEL_ID: z.string().min(1, "VOICE_CHANNEL_ID is required"),
GUILD_ID: z.string().min(1, "GUILD_ID is required"),
VERBOSE: z
.string()
.optional()
.transform((v) => v === "true")
.default(false),
RECORDINGS_DIR: z.string().default("./recordings"),
RECORDING_SEGMENT_MS: z.coerce.number().positive().default(5000),
DECODER_ROTATE_MS: z.coerce.number().positive().default(5000),
DECODER_COOLDOWN_MS: z.coerce.number().positive().default(30000),
WEBSERVER_PORT: z.coerce.number().positive().default(3000),
VOICE_CONNECTION_TIMEOUT_MS: z.coerce.number().positive().default(15000),
RECONNECT_TIMEOUT_MS: z.coerce.number().positive().default(5000),
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),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
NODE_ENV: z.enum(["development", "production"]).default("development"),
});
export function parsePositiveNumber(
value: string | undefined,
fallback: number,
): number {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
export type AppConfig = z.infer<typeof configSchema>;
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
return {
verbose: parseBoolean(env.VERBOSE, false),
recordingsDir: env.RECORDINGS_DIR ?? "./recordings",
recordingSegmentMs: parsePositiveNumber(env.RECORDING_SEGMENT_MS, 5_000),
decoderRotateMs: parsePositiveNumber(env.DECODER_ROTATE_MS, 5_000),
decoderCooldownMs: parsePositiveNumber(env.DECODER_COOLDOWN_MS, 30_000),
webserverPort: parsePositiveNumber(env.WEBSERVER_PORT, 3000),
voiceConnectionTimeoutMs: parsePositiveNumber(
env.VOICE_CONNECTION_TIMEOUT_MS,
15_000,
),
reconnectTimeoutMs: parsePositiveNumber(env.RECONNECT_TIMEOUT_MS, 5_000),
audioStreamSilenceDurationMs: parsePositiveNumber(
env.AUDIO_STREAM_SILENCE_DURATION_MS,
3000,
),
packetFilterMinSize: parsePositiveNumber(env.PACKET_FILTER_MIN_SIZE, 8),
opusFrameSize: parsePositiveNumber(env.OPUS_FRAME_SIZE, 960),
audioSampleRate: parsePositiveNumber(env.AUDIO_SAMPLE_RATE, 48000),
audioChannels: parsePositiveNumber(env.AUDIO_CHANNELS, 2),
avatarSize: parsePositiveNumber(env.AVATAR_SIZE, 64),
};
try {
return configSchema.parse(env);
} catch (error) {
if (error instanceof z.ZodError) {
const messages = error.issues
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join("\n");
throw new ConfigError(`Configuration validation failed:\n${messages}`);
}
throw error;
}
}
export const config = loadConfig();