-
-
Notifications
You must be signed in to change notification settings - Fork 36
Expand file tree
/
Copy pathswgohBot.ts
More file actions
178 lines (149 loc) · 5.75 KB
/
swgohBot.ts
File metadata and controls
178 lines (149 loc) · 5.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import { inspect } from "node:util";
import { RESTJSONErrorCodes as APIErrors, Client, DiscordAPIError, TextChannel } from "discord.js";
import { env } from "./config/config.ts";
import constants from "./data/constants/constants.ts";
import { cleanupIntervals } from "./events/clientReady.ts";
import eventHandler from "./handlers/eventHandler.ts";
import slashHandler from "./handlers/slashHandler.ts";
import cache from "./modules/cache.ts";
import commandStats from "./modules/commandStats.ts";
import database from "./modules/database.ts";
import eventFuncs from "./modules/eventFuncs.ts";
import { reloadLanguages } from "./modules/functions.ts";
import logger from "./modules/Logger.ts";
import patreonFuncs from "./modules/patreonFuncs.ts";
import swgohAPI from "./modules/swapi.ts";
import userReg from "./modules/users.ts";
const client = new Client({
intents: constants.botIntents,
partials: constants.partials,
closeTimeout: 30_000,
}) as Client<true>;
// Regex to replace absolute paths with relative paths in error messages
const CWD_REGEX = new RegExp(process.cwd(), "g");
const logErrorToChannel = (errorMsg: string) => {
try {
if (!env.LOG_TO_CHANNEL) return;
const thisChannel = client.channels.cache.get(env.LOG_CHANNEL_ID);
if (!thisChannel || !(thisChannel instanceof TextChannel) || !thisChannel?.send) return;
thisChannel.send(`\`\`\`${inspect(errorMsg)}\`\`\``);
} catch {
// Silently fail - we're already in error handling
}
};
// Prevent multiple simultaneous shutdown attempts
let isShuttingDown = false;
/**
* Gracefully shuts down the bot, cleaning up resources
*/
async function gracefulShutdown(signal: string): Promise<void> {
if (isShuttingDown) return;
isShuttingDown = true;
logger.log(`Received ${signal}, starting graceful shutdown...`);
const shutdown = async () => {
// Stop accepting new interactions
client.removeAllListeners();
// Clean up intervals from clientReady
cleanupIntervals();
// Clean up SWAPI reload interval
swgohAPI.cleanup();
// Flush any pending command stats
await commandStats.shutdown();
// Destroy Discord client connection
await client.destroy();
logger.log("Discord client destroyed");
// Close MongoDB connection
if (database.isConnected()) {
await database.close();
}
};
const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error("Shutdown timed out after 10s")), 10_000));
try {
await Promise.race([shutdown(), timeout]);
logger.log("Graceful shutdown complete");
process.exit(0);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
logger.error(`Error during shutdown: ${errorMsg}`);
process.exit(1);
}
}
const init = async () => {
try {
await database.connect(env.MONGODB_URL);
} catch (err) {
logger.error(`Failed to connect to MongoDB: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
// Load the language files
try {
await reloadLanguages();
} catch (err) {
logger.error(`Failed to load languages: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
// Set up the caching
cache.init(database.getClient());
userReg.init(cache);
if (env.SWAPI_CLIENT_URL) {
// Load up the api connector/ helpers
try {
swgohAPI.init();
} catch (err) {
logger.error(`Failed to initialize swgohAPI: ${err instanceof Error ? err.message : String(err)}`);
}
} else {
logger.error("Failed to load swapi: No swapiConfig found");
}
// Initialize patreon functions
patreonFuncs.init(client);
// Initialize event functions
eventFuncs.init(client);
await slashHandler();
await eventHandler(client);
// Register graceful shutdown handlers
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
process.on("uncaughtException", (err) => {
const errorMsg = err.stack?.replace(CWD_REGEX, ".") || String(err);
logger.error(`Uncaught Exception: ${errorMsg}`);
// If it's that error, don't bother showing it again
if (!errorMsg.includes("RSV2 and RSV3 must be clear")) {
logErrorToChannel(errorMsg);
}
if (database.isConnected()) {
database.close();
}
process.exit(1);
});
const IGNORED_ERRORS = [
APIErrors.UnknownMessage,
APIErrors.UnknownChannel,
APIErrors.UnknownGuild,
APIErrors.UnknownMember,
APIErrors.UnknownUser,
APIErrors.UnknownInteraction,
APIErrors.MissingAccess,
];
process.on("unhandledRejection", (err: Error) => {
// If it's something I can't do anything about, ignore it
if (err instanceof DiscordAPIError && typeof err.code === "number" && IGNORED_ERRORS.includes(err.code)) {
return;
}
const errorMsg = err?.stack?.replace(CWD_REGEX, ".") || String(err);
if (errorMsg.includes("ShardClientUtil._handleMessage") && errorMsg.includes("client is not defined")) {
logger.error("The following error probably has to do with a 'client' inside a broadcastEval");
}
logger.error(`Uncaught Promise Error: ${errorMsg}`);
logErrorToChannel(errorMsg);
});
};
init()
.then(() => {
logger.log("Bot initialization complete");
return client.login();
})
.catch((err) => {
logger.error(`Failed to initialize bot: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
});