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
▼Dockerfile
+1-1
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
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
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
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
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
+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
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
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
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
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