Files
discord-rpg-concept/bot/lib/BotClient.ts
2026-01-08 16:09:26 +01:00

121 lines
4.6 KiB
TypeScript

import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes } from "discord.js";
import { join } from "node:path";
import type { Command } from "@shared/lib/types";
import { env } from "@shared/lib/env";
import { CommandLoader } from "@lib/loaders/CommandLoader";
import { EventLoader } from "@lib/loaders/EventLoader";
export class Client extends DiscordClient {
commands: Collection<string, Command>;
lastCommandTimestamp: number | null = null;
private commandLoader: CommandLoader;
private eventLoader: EventLoader;
constructor({ intents }: { intents: number[] }) {
super({ intents });
this.commands = new Collection<string, Command>();
this.commandLoader = new CommandLoader(this);
this.eventLoader = new EventLoader(this);
}
async loadCommands(reload: boolean = false) {
if (reload) {
this.commands.clear();
console.log("♻️ Reloading commands...");
}
const commandsPath = join(import.meta.dir, '../commands');
const result = await this.commandLoader.loadFromDirectory(commandsPath, reload);
console.log(`📦 Command loading complete: ${result.loaded} loaded, ${result.skipped} skipped, ${result.errors.length} errors`);
}
async loadEvents(reload: boolean = false) {
if (reload) {
this.removeAllListeners();
console.log("♻️ Reloading events...");
}
const eventsPath = join(import.meta.dir, '../events');
const result = await this.eventLoader.loadFromDirectory(eventsPath, reload);
console.log(`📦 Event loading complete: ${result.loaded} loaded, ${result.skipped} skipped, ${result.errors.length} errors`);
}
async deployCommands() {
// We use env.DISCORD_BOT_TOKEN directly so this can run without client.login()
const token = env.DISCORD_BOT_TOKEN;
if (!token) {
console.error("DISCORD_BOT_TOKEN is not set.");
return;
}
const rest = new REST().setToken(token);
const commandsData = this.commands.map(c => c.data.toJSON());
const guildId = env.DISCORD_GUILD_ID;
const clientId = env.DISCORD_CLIENT_ID;
if (!clientId) {
console.error("DISCORD_CLIENT_ID is not set.");
return;
}
try {
console.log(`Started refreshing ${commandsData.length} application (/) commands.`);
let data;
if (guildId) {
console.log(`Registering commands to guild: ${guildId}`);
data = await rest.put(
Routes.applicationGuildCommands(clientId, guildId),
{ body: commandsData },
);
// Clear global commands to avoid duplicates
await rest.put(Routes.applicationCommands(clientId), { body: [] });
} else {
console.log('Registering commands globally');
data = await rest.put(
Routes.applicationCommands(clientId),
{ body: commandsData },
);
}
console.log(`Successfully reloaded ${(data as any).length} application (/) commands.`);
} catch (error: any) {
if (error.code === 50001) {
console.warn("Missing Access: The bot is not in the guild or lacks 'applications.commands' scope.");
console.warn(" If you are testing locally, make sure you invited the bot with scope 'bot applications.commands'.");
} else {
console.error(error);
}
}
}
async shutdown() {
const { setShuttingDown, waitForTransactions } = await import("./shutdown");
const { closeDatabase } = await import("@shared/db/DrizzleClient");
console.log("🛑 Shutdown signal received. Starting graceful shutdown...");
setShuttingDown(true);
// Wait for transactions to complete
console.log("⏳ Waiting for active transactions to complete...");
await waitForTransactions(10000);
// Destroy Discord client
console.log("🔌 Disconnecting from Discord...");
this.destroy();
// Close database
console.log("🗄️ Closing database connection...");
await closeDatabase();
console.log("👋 Graceful shutdown complete. Exiting.");
process.exit(0);
}
}
export const AuroraClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMembers] });