feat: unify Discord bot and HTTP API into single synchronized runtime
swadhinbiswas committed 1 months ago
6bdafc3
1 parent 4a8d707
commit 6bdafc3
7 files changed
+95 -27
Showing 7 changed files+95-27
▼Dockerfile
+1-1
| @@ -27,4 +27,4 @@ ENV HOST=0.0.0.0 | ||
27 | 27 | |
28 | 28 | EXPOSE 3000 |
29 | 29 | |
30 | -CMD ["bun", "run", "start:bot"] | |
30 | +CMD ["bun", "run", "start"] | |
31 | 31 | |
▼apps/api/src/server.ts
+26-1
| @@ -5,18 +5,37 @@ import { logger } from "@devpulse/logger"; | ||
5 | 5 | import { sql } from "drizzle-orm"; |
6 | 6 | import Fastify, { type FastifyInstance } from "fastify"; |
7 | 7 | |
8 | -export function buildServer(): FastifyInstance { | |
8 | +export interface ServerOptions { | |
9 | + discordClient?: { | |
10 | + isReady?: () => boolean; | |
11 | + ws?: { ping?: number }; | |
12 | + guilds?: { cache?: { size: number } }; | |
13 | + user?: { tag?: string } | null; | |
14 | + }; | |
15 | +} | |
16 | + | |
17 | +export function buildServer(options?: ServerOptions): FastifyInstance { | |
9 | 18 | const server = Fastify({ |
10 | 19 | loggerInstance: logger as any, |
11 | 20 | }); |
12 | 21 | |
13 | 22 | server.get("/health", async () => { |
14 | 23 | const memory = process.memoryUsage(); |
24 | + const discordStatus = options?.discordClient | |
25 | + ? { | |
26 | + connected: options.discordClient.isReady ? options.discordClient.isReady() : false, | |
27 | + pingMs: options.discordClient.ws?.ping ?? -1, | |
28 | + guildsCount: options.discordClient.guilds?.cache?.size ?? 0, | |
29 | + user: options.discordClient.user?.tag ?? null, | |
30 | + } | |
31 | + : undefined; | |
32 | + | |
15 | 33 | return { |
16 | 34 | status: "healthy", |
17 | 35 | service: "DevPulse API", |
18 | 36 | uptimeSeconds: Math.floor(process.uptime()), |
19 | 37 | timestamp: new Date().toISOString(), |
38 | + ...(discordStatus ? { discord: discordStatus } : {}), | |
20 | 39 | memory: { |
21 | 40 | rss: formatBytes(memory.rss), |
22 | 41 | heapUsed: formatBytes(memory.heapUsed), |
| @@ -41,6 +60,9 @@ export function buildServer(): FastifyInstance { | ||
41 | 60 | status: "ready", |
42 | 61 | database: "connected", |
43 | 62 | cache: "connected", |
63 | + ...(options?.discordClient | |
64 | + ? { discord: options.discordClient.isReady?.() ? "connected" : "connecting" } | |
65 | + : {}), | |
44 | 66 | timestamp: new Date().toISOString(), |
45 | 67 | }; |
46 | 68 | } catch (err: any) { |
| @@ -62,6 +84,9 @@ export function buildServer(): FastifyInstance { | ||
62 | 84 | repositoriesTracked: allRepos.length, |
63 | 85 | monitorsActive: allMonitors.filter((m) => m.isActive).length, |
64 | 86 | monitorsTotal: allMonitors.length, |
87 | + ...(options?.discordClient?.guilds?.cache | |
88 | + ? { discordGuilds: options.discordClient.guilds.cache.size } | |
89 | + : {}), | |
65 | 90 | nodeVersion: process.version, |
66 | 91 | platform: process.platform, |
67 | 92 | }; |
68 | 93 | |
▼apps/bot/package.json
+1-0
| @@ -13,6 +13,7 @@ | ||
13 | 13 | "dependencies": { |
14 | 14 | "@devpulse/ai": "workspace:*", |
15 | 15 | "@devpulse/analytics": "workspace:*", |
16 | + "@devpulse/api": "workspace:*", | |
16 | 17 | "@devpulse/cache": "workspace:*", |
17 | 18 | "@devpulse/config": "workspace:*", |
18 | 19 | "@devpulse/core": "workspace:*", |
19 | 20 | |
▼apps/bot/src/index.ts
+60-12
| @@ -1,4 +1,6 @@ | ||
1 | +import { buildServer } from "@devpulse/api"; | |
1 | 2 | import { config } from "@devpulse/config"; |
3 | +import { initDatabase } from "@devpulse/database"; | |
2 | 4 | import { logger } from "@devpulse/logger"; |
3 | 5 | import { scheduler } from "@devpulse/scheduler"; |
4 | 6 | import { Client, GatewayIntentBits, Partials } from "discord.js"; |
| @@ -23,30 +25,76 @@ export function createBotClient(): Client { | ||
23 | 25 | return client; |
24 | 26 | } |
25 | 27 | |
26 | -export async function startBot() { | |
28 | +export async function startUnifiedApp() { | |
29 | + logger.info("Initializing DevPulse Unified Platform (API + Discord Bot)..."); | |
30 | + | |
31 | + // 1. Initialize SQLite / libSQL database tables and indexes | |
32 | + await initDatabase(); | |
33 | + | |
34 | + // 2. Initialize Discord Client | |
27 | 35 | const client = createBotClient(); |
28 | 36 | |
37 | + // 3. Initialize & start Fastify HTTP API Server (exposes /health, /ready, /metrics) | |
38 | + const apiServer = buildServer({ discordClient: client }); | |
39 | + try { | |
40 | + const address = await apiServer.listen({ | |
41 | + port: config.PORT, | |
42 | + host: config.HOST, | |
43 | + }); | |
44 | + logger.info({ address }, `DevPulse HTTP API server listening on ${address}`); | |
45 | + } catch (err) { | |
46 | + logger.error({ err }, "Failed to bind DevPulse HTTP API server"); | |
47 | + process.exit(1); | |
48 | + } | |
49 | + | |
50 | + // 4. Graceful shutdown handler | |
51 | + let isShuttingDown = false; | |
29 | 52 | const shutdown = async (signal: string) => { |
30 | - logger.info({ signal }, "Gracefully shutting down DevPulse bot..."); | |
53 | + if (isShuttingDown) return; | |
54 | + isShuttingDown = true; | |
55 | + logger.info({ signal }, "Gracefully shutting down DevPulse unified platform..."); | |
31 | 56 | scheduler.stop(); |
32 | - client.destroy(); | |
33 | - process.exit(0); | |
57 | + try { | |
58 | + await apiServer.close(); | |
59 | + client.destroy(); | |
60 | + logger.info("DevPulse shutdown completed successfully."); | |
61 | + } catch (err) { | |
62 | + logger.error({ err }, "Error occurred during shutdown"); | |
63 | + } finally { | |
64 | + process.exit(0); | |
65 | + } | |
34 | 66 | }; |
35 | 67 | |
36 | 68 | process.on("SIGINT", () => shutdown("SIGINT")); |
37 | 69 | process.on("SIGTERM", () => shutdown("SIGTERM")); |
38 | 70 | |
39 | - try { | |
40 | - logger.info("Logging into Discord gateway..."); | |
41 | - await client.login(config.DISCORD_TOKEN); | |
42 | - } catch (err) { | |
43 | - logger.error({ err }, "Failed to login to Discord gateway"); | |
44 | - process.exit(1); | |
71 | + // 5. Connect to Discord Gateway | |
72 | + const isDefaultOrPlaceholderToken = | |
73 | + !config.DISCORD_TOKEN || | |
74 | + config.DISCORD_TOKEN === "dummy-token-for-dev-and-tests" || | |
75 | + config.DISCORD_TOKEN.startsWith("your_discord_bot_token"); | |
76 | + | |
77 | + if (isDefaultOrPlaceholderToken) { | |
78 | + logger.warn( | |
79 | + `⚠️ DevPulse HTTP API is running on port ${config.PORT}, but DISCORD_TOKEN in .env is not configured.`, | |
80 | + ); | |
81 | + logger.warn( | |
82 | + "👉 Please edit /home/swadhin/discordbot/.env and set your DISCORD_TOKEN and DISCORD_CLIENT_ID to connect to Discord.", | |
83 | + ); | |
84 | + } else { | |
85 | + try { | |
86 | + logger.info("Connecting to Discord Gateway..."); | |
87 | + await client.login(config.DISCORD_TOKEN); | |
88 | + } catch (err) { | |
89 | + logger.error({ err }, "Failed to login to Discord Gateway. Check your DISCORD_TOKEN in .env"); | |
90 | + } | |
45 | 91 | } |
46 | 92 | |
47 | - return client; | |
93 | + return { client, apiServer }; | |
48 | 94 | } |
49 | 95 | |
96 | +export const startBot = startUnifiedApp; | |
97 | + | |
50 | 98 | if (import.meta.main) { |
51 | - startBot(); | |
99 | + startUnifiedApp(); | |
52 | 100 | } |
53 | 101 | |
▼bun.lock
+1-0
| @@ -35,6 +35,7 @@ | ||
35 | 35 | "dependencies": { |
36 | 36 | "@devpulse/ai": "workspace:*", |
37 | 37 | "@devpulse/analytics": "workspace:*", |
38 | + "@devpulse/api": "workspace:*", | |
38 | 39 | "@devpulse/cache": "workspace:*", |
39 | 40 | "@devpulse/config": "workspace:*", |
40 | 41 | "@devpulse/core": "workspace:*", |
41 | 42 | |
▼docker-compose.yml
+2-13
| @@ -1,20 +1,9 @@ | ||
1 | 1 | services: |
2 | - devpulse-bot: | |
2 | + devpulse: | |
3 | 3 | build: |
4 | 4 | context: . |
5 | 5 | dockerfile: Dockerfile |
6 | - command: ["bun", "run", "start:bot"] | |
7 | - restart: unless-stopped | |
8 | - env_file: | |
9 | - - .env | |
10 | - volumes: | |
11 | - - devpulse-data:/app/data | |
12 | - | |
13 | - devpulse-api: | |
14 | - build: | |
15 | - context: . | |
16 | - dockerfile: Dockerfile | |
17 | - command: ["bun", "run", "start:api"] | |
6 | + command: ["bun", "run", "start"] | |
18 | 7 | restart: unless-stopped |
19 | 8 | ports: |
20 | 9 | - "3000:3000" |
21 | 10 | |
▼package.json
+4-0
| @@ -10,6 +10,10 @@ | ||
10 | 10 | "lint:fix": "biome check --write .", |
11 | 11 | "test": "vitest run", |
12 | 12 | "test:watch": "vitest", |
13 | + "start": "bun run --filter @devpulse/bot start", | |
14 | + "dev": "bun run --filter @devpulse/bot dev", | |
15 | + "start:unified": "bun run --filter @devpulse/bot start", | |
16 | + "dev:unified": "bun run --filter @devpulse/bot dev", | |
13 | 17 | "start:bot": "bun run --filter @devpulse/bot start", |
14 | 18 | "start:api": "bun run --filter @devpulse/api start", |
15 | 19 | "dev:bot": "bun run --filter @devpulse/bot dev", |
16 | 20 | |