From dcbe20479506f543352b33be364b3172361a07d7 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Tue, 19 May 2026 15:08:02 +0700 Subject: [PATCH] refactor: extract application bootstrap --- src/app/bootstrap.ts | 98 ++++++++++++++++++++++++++++++++++ src/app/shutdown.ts | 56 +++++++++++++++++++ src/index.ts | 124 +------------------------------------------ 3 files changed, 155 insertions(+), 123 deletions(-) create mode 100644 src/app/bootstrap.ts create mode 100644 src/app/shutdown.ts diff --git a/src/app/bootstrap.ts b/src/app/bootstrap.ts new file mode 100644 index 0000000..0a888ee --- /dev/null +++ b/src/app/bootstrap.ts @@ -0,0 +1,98 @@ +import { Client } from "discord.js-selfbot-v13"; +import { config } from "../config"; +import { closeDatabase, initializeDatabase } from "../database/drizzle"; +import { createDiscordClientOptions } from "../discordClientOptions"; +import { createChildLogger } from "../logger"; +import { startPendingAIAnalysisWorker } from "../moderation/aiAnalyzer"; +import { syncBacklogMessages } from "../moderation/backlogSync"; +import { registerMessageCapture } from "../moderation/messageCapture"; +import { discordPlayer } from "../player"; +import { VoiceController } from "../voiceController"; +import { startWebserver } from "../webserver"; +import { createGracefulShutdown } from "./shutdown"; + +export async function initializeApp() { + const logger = createChildLogger("bot"); + + const token = config.DISCORD_TOKEN; + logger.info( + { hasToken: token.length > 0, tokenLength: token.length }, + "Config loaded", + ); + + logger.info("Creating Discord client"); + const client = new Client(createDiscordClientOptions()); + const voiceController = new VoiceController(client); + + const gracefulShutdown = createGracefulShutdown({ + logger, + closeDatabase, + voiceController, + discordPlayer, + client, + }); + + try { + logger.info("Initializing database"); + await initializeDatabase(); + logger.info({ type: config.DATABASE_TYPE }, "Database initialized"); + } catch (err) { + logger.error({ error: err }, "Failed to initialize database"); + process.exit(1); + } + + client.on("debug", (msg) => { + if ( + msg.includes("[VOICE") || + msg.includes("[ffmpeg") || + msg.toLowerCase().includes("error") || + msg.toLowerCase().includes("stream") + ) { + logger.info({ debugMsg: msg }, "Discord Client Debug"); + } else if (config.VERBOSE) { + logger.debug({ debugMsg: msg }, "Discord Client Debug"); + } + }); + + client.on("ready", async () => { + logger.info({ user: client.user?.tag }, "Bot logged in"); + registerMessageCapture(client); + startPendingAIAnalysisWorker(); + syncBacklogMessages(client).catch((error) => { + logger.warn({ error }, "Backlog sync failed"); + }); + await startWebserver(config.WEBSERVER_PORT, client, voiceController); + }); + + client.on("error", (err) => { + logger.error({ error: err }, "Client error"); + }); + + process.on("SIGINT", () => { + gracefulShutdown("SIGINT"); + }); + + process.on("SIGTERM", () => { + gracefulShutdown("SIGTERM"); + }); + + process.on("uncaughtException", (err) => { + logger.error({ error: err }, "Uncaught exception"); + gracefulShutdown("uncaughtException"); + }); + + process.on("unhandledRejection", (reason, promise) => { + logger.error({ reason, promise }, "Unhandled rejection"); + gracefulShutdown("unhandledRejection"); + }); + + logger.info("Calling Discord client.login"); + client + .login(token) + .then(() => { + logger.info("Discord client.login resolved"); + }) + .catch((error) => { + logger.error({ error }, "Discord client.login failed"); + }); +} diff --git a/src/app/shutdown.ts b/src/app/shutdown.ts new file mode 100644 index 0000000..fc5791e --- /dev/null +++ b/src/app/shutdown.ts @@ -0,0 +1,56 @@ +import type { Client } from "discord.js-selfbot-v13"; +import type { closeDatabase } from "../database/drizzle"; +import type { createChildLogger } from "../logger"; +import type { discordPlayer } from "../player"; +import type { VoiceController } from "../voiceController"; + +type Logger = ReturnType; +type CloseDatabase = typeof closeDatabase; +type DiscordPlayer = typeof discordPlayer; + +export interface GracefulShutdownOptions { + logger: Logger; + closeDatabase: CloseDatabase; + voiceController: VoiceController; + discordPlayer: DiscordPlayer; + client: Client; +} + +export function createGracefulShutdown(options: GracefulShutdownOptions) { + let isShuttingDown = false; + + return async function gracefulShutdown(signal: string) { + if (isShuttingDown) { + options.logger.warn(`Already shutting down, ignoring ${signal}`); + return; + } + + isShuttingDown = true; + options.logger.info({ signal }, "Graceful shutdown initiated"); + + try { + options.logger.info("Closing database..."); + await options.closeDatabase(); + options.logger.info("Database closed"); + + options.logger.info("Stopping voice connection..."); + await options.voiceController.disconnect(); + + options.logger.info("Pausing player..."); + options.discordPlayer.pause(); + + options.logger.info("Destroying Discord client..."); + try { + options.client.destroy(); + } catch (err) { + options.logger.warn({ error: err }, "Error destroying client"); + } + + options.logger.info("Graceful shutdown completed"); + process.exit(0); + } catch (err) { + options.logger.error({ error: err }, "Error during graceful shutdown"); + process.exit(1); + } + }; +} diff --git a/src/index.ts b/src/index.ts index 861b01c..313cf58 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,133 +2,11 @@ import "./mock-crc"; import "libsodium-wrappers"; import "@snazzah/davey"; import "dotenv/config"; -import { Client } from "discord.js-selfbot-v13"; -import { config } from "./config"; -import { closeDatabase, initializeDatabase } from "./database/drizzle"; -import { createDiscordClientOptions } from "./discordClientOptions"; +import { initializeApp } from "./app/bootstrap"; import { createChildLogger } from "./logger"; -import { startPendingAIAnalysisWorker } from "./moderation/aiAnalyzer"; -import { syncBacklogMessages } from "./moderation/backlogSync"; -import { registerMessageCapture } from "./moderation/messageCapture"; -import { discordPlayer } from "./player"; -import { VoiceController } from "./voiceController"; -import { startWebserver } from "./webserver"; const logger = createChildLogger("bot"); -const token = config.DISCORD_TOKEN; -logger.info( - { hasToken: token.length > 0, tokenLength: token.length }, - "Config loaded", -); - -logger.info("Creating Discord client"); -const client = new Client(createDiscordClientOptions()); -const voiceController = new VoiceController(client); - -let isShuttingDown = false; - -async function gracefulShutdown(signal: string) { - if (isShuttingDown) { - logger.warn(`Already shutting down, ignoring ${signal}`); - return; - } - - isShuttingDown = true; - logger.info({ signal }, "Graceful shutdown initiated"); - - try { - logger.info("Closing database..."); - await closeDatabase(); - logger.info("Database closed"); - - logger.info("Stopping voice connection..."); - await voiceController.disconnect(); - - logger.info("Pausing player..."); - discordPlayer.pause(); - - logger.info("Destroying Discord client..."); - try { - client.destroy(); - } catch (err) { - logger.warn({ error: err }, "Error destroying client"); - } - - logger.info("Graceful shutdown completed"); - process.exit(0); - } catch (err) { - logger.error({ error: err }, "Error during graceful shutdown"); - process.exit(1); - } -} - -async function initializeApp() { - try { - logger.info("Initializing database"); - await initializeDatabase(); - logger.info({ type: config.DATABASE_TYPE }, "Database initialized"); - } catch (err) { - logger.error({ error: err }, "Failed to initialize database"); - process.exit(1); - } - - client.on("debug", (msg) => { - if ( - msg.includes("[VOICE") || - msg.includes("[ffmpeg") || - msg.toLowerCase().includes("error") || - msg.toLowerCase().includes("stream") - ) { - logger.info({ debugMsg: msg }, "Discord Client Debug"); - } else if (config.VERBOSE) { - logger.debug({ debugMsg: msg }, "Discord Client Debug"); - } - }); - - client.on("ready", async () => { - logger.info({ user: client.user?.tag }, "Bot logged in"); - registerMessageCapture(client); - startPendingAIAnalysisWorker(); - syncBacklogMessages(client).catch((error) => { - logger.warn({ error }, "Backlog sync failed"); - }); - await startWebserver(config.WEBSERVER_PORT, client, voiceController); - }); - - client.on("error", (err) => { - logger.error({ error: err }, "Client error"); - }); - - process.on("SIGINT", () => { - gracefulShutdown("SIGINT"); - }); - - process.on("SIGTERM", () => { - gracefulShutdown("SIGTERM"); - }); - - process.on("uncaughtException", (err) => { - logger.error({ error: err }, "Uncaught exception"); - gracefulShutdown("uncaughtException"); - }); - - process.on("unhandledRejection", (reason, promise) => { - logger.error({ reason, promise }, "Unhandled rejection"); - gracefulShutdown("unhandledRejection"); - }); - - logger.info("Calling Discord client.login"); - client - .login(token) - .then(() => { - logger.info("Discord client.login resolved"); - }) - .catch((error) => { - logger.error({ error }, "Discord client.login failed"); - }); -} - initializeApp().catch((error) => { logger.error({ error }, "Failed to initialize app"); process.exit(1);