176 lines
6.8 KiB
TypeScript
176 lines
6.8 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;
|
||
maintenanceMode: boolean = false;
|
||
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);
|
||
}
|
||
|
||
public async setupSystemEvents() {
|
||
const { systemEvents, EVENTS } = await import("@shared/lib/events");
|
||
|
||
systemEvents.on(EVENTS.ACTIONS.RELOAD_COMMANDS, async () => {
|
||
console.log("🔄 System Action: Reloading commands...");
|
||
try {
|
||
await this.loadCommands(true);
|
||
await this.deployCommands();
|
||
|
||
const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service");
|
||
await dashboardService.recordEvent({
|
||
type: "success",
|
||
message: "Bot: Commands reloaded and redeployed",
|
||
icon: "✅"
|
||
});
|
||
} catch (error) {
|
||
console.error("Failed to reload commands:", error);
|
||
}
|
||
});
|
||
|
||
systemEvents.on(EVENTS.ACTIONS.CLEAR_CACHE, async () => {
|
||
console.log("<22> System Action: Clearing all internal caches...");
|
||
|
||
try {
|
||
// 1. Lootdrop Service
|
||
const { lootdropService } = await import("@shared/modules/economy/lootdrop.service");
|
||
await lootdropService.clearCaches();
|
||
|
||
// 2. Trade Service
|
||
const { tradeService } = await import("@shared/modules/trade/trade.service");
|
||
tradeService.clearSessions();
|
||
|
||
// 3. Item Wizard
|
||
const { clearDraftSessions } = await import("@/modules/admin/item_wizard");
|
||
clearDraftSessions();
|
||
|
||
const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service");
|
||
await dashboardService.recordEvent({
|
||
type: "success",
|
||
message: "Bot: All internal caches and sessions cleared",
|
||
icon: "🧼"
|
||
});
|
||
} catch (error) {
|
||
console.error("Failed to clear caches:", error);
|
||
}
|
||
});
|
||
|
||
systemEvents.on(EVENTS.ACTIONS.MAINTENANCE_MODE, async (data: { enabled: boolean, reason?: string }) => {
|
||
const { enabled, reason } = data;
|
||
console.log(`🛠️ System Action: Maintenance mode ${enabled ? "ON" : "OFF"}${reason ? ` (${reason})` : ""}`);
|
||
this.maintenanceMode = enabled;
|
||
});
|
||
}
|
||
|
||
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] }); |