feat(bot): add automatic env discovery and resilient gateway intent fallback

husteno-o committed 1 months ago
f0edb98
1 parent f781485
commit f0edb98
2 files changed
+60 -9
▼apps/bot/src/index.ts
+35-9
8
8
import { handleMessageCreate } from "./events/messageCreate.js";
9
9
import { handleReady } from "./events/ready.js";
10
10
11
-export function createBotClient(): Client {
11
+export function createBotClient(options?: { enableMessageContent?: boolean }): Client {
12
+ const allowMessageContent = options?.enableMessageContent ?? config.ENABLE_MESSAGE_CONTENT_INTENT;
13
+ const intents = [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages];
14
+
15
+ if (allowMessageContent) {
16
+ intents.push(GatewayIntentBits.MessageContent);
17
+ }
18
+
12
19
const client = new Client({
13
- intents: [
14
- GatewayIntentBits.Guilds,
15
- GatewayIntentBits.GuildMessages,
16
- GatewayIntentBits.MessageContent,
17
- ],
20
+ intents,
18
21
partials: [Partials.Message, Partials.Channel],
19
22
});
20
23
21
24
client.once("ready", (c) => handleReady(c));
22
25
client.on("interactionCreate", (i) => handleInteraction(i));
23
- client.on("messageCreate", (m) => handleMessageCreate(m));
26
+ if (allowMessageContent) {
27
+ client.on("messageCreate", (m) => handleMessageCreate(m));
28
+ }
24
29
25
30
return client;
26
31
}
95
100
try {
96
101
logger.info("Connecting to Discord Gateway...");
97
102
await client.login(config.DISCORD_TOKEN);
98
- } catch (err) {
99
- logger.error({ err }, "Failed to login to Discord Gateway. Check your DISCORD_TOKEN in .env");
103
+ } catch (err: any) {
104
+ if (
105
+ err?.message?.includes("disallowed intents") ||
106
+ String(err).includes("disallowed intents")
107
+ ) {
108
+ logger.warn(
109
+ "Used disallowed intents: Message Content Intent is not enabled in the Discord Developer Portal.",
110
+ );
111
+ logger.warn(
112
+ "Retrying connection without MessageContent intent (slash commands and UI will function completely)...",
113
+ );
114
+ const fallbackClient = createBotClient({ enableMessageContent: false });
115
+ try {
116
+ await fallbackClient.login(config.DISCORD_TOKEN);
117
+ } catch (retryErr) {
118
+ logger.error({ err: retryErr }, "Failed to login to Discord Gateway after fallback.");
119
+ }
120
+ } else {
121
+ logger.error(
122
+ { err },
123
+ "Failed to login to Discord Gateway. Check your DISCORD_TOKEN in .env",
124
+ );
125
+ }
100
126
}
101
127
}
102
128
103
129
▼packages/config/src/index.ts
+25-0
1
+import fs from "node:fs";
2
+import path from "node:path";
1
3
import { z } from "zod";
2
4
5
+function discoverAndLoadEnv(): void {
6
+ let curr = process.cwd();
7
+ for (let i = 0; i < 5; i++) {
8
+ const candidate = path.join(curr, ".env");
9
+ if (fs.existsSync(candidate)) {
10
+ try {
11
+ if (typeof process.loadEnvFile === "function") {
12
+ process.loadEnvFile(candidate);
13
+ }
14
+ } catch {
15
+ // Continue if already loaded or permission denied
16
+ }
17
+ break;
18
+ }
19
+ const parent = path.dirname(curr);
20
+ if (parent === curr) break;
21
+ curr = parent;
22
+ }
23
+}
24
+
25
+discoverAndLoadEnv();
26
+
3
27
export const ConfigSchema = z.object({
4
28
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
5
29
LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info"),
11
35
DISCORD_TOKEN: z.string().min(1).default("dummy-token-for-dev-and-tests"),
12
36
DISCORD_CLIENT_ID: z.string().min(1).default("123456789012345678"),
13
37
DISCORD_PUBLIC_KEY: z.string().optional(),
38
+ ENABLE_MESSAGE_CONTENT_INTENT: z.coerce.boolean().default(false),
14
39
15
40
// Turso / libSQL
16
41
TURSO_DATABASE_URL: z.string().default("file:devpulse.db"),
17
42