import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes } from "discord.js"; import { readdir } from "node:fs/promises"; import { join } from "node:path"; import type { Command } from "@lib/types"; import { env } from "@lib/env"; class Client extends DiscordClient { commands: Collection; constructor({ intents }: { intents: number[] }) { super({ intents }); this.commands = new Collection(); } async loadCommands(reload: boolean = false) { if (reload) { this.commands.clear(); console.log("♻️ Reloading commands..."); } const commandsPath = join(import.meta.dir, '../commands'); await this.readCommandsRecursively(commandsPath, reload); } private async readCommandsRecursively(dir: string, reload: boolean = false) { try { const files = await readdir(dir, { withFileTypes: true }); for (const file of files) { const filePath = join(dir, file.name); if (file.isDirectory()) { await this.readCommandsRecursively(filePath, reload); continue; } if (!file.name.endsWith('.ts') && !file.name.endsWith('.js')) continue; try { const importPath = reload ? `${filePath}?t=${Date.now()}` : filePath; const commandModule = await import(importPath); const commands = Object.values(commandModule); if (commands.length === 0) { console.warn(`⚠️ No commands found in ${file.name}`); continue; } for (const command of commands) { if (this.isValidCommand(command)) { this.commands.set(command.data.name, command); console.log(`✅ Loaded command: ${command.data.name}`); } else { console.warn(`⚠️ Skipping invalid command in ${file.name}`); } } } catch (error) { console.error(`❌ Failed to load command from ${filePath}:`, error); } } } catch (error) { console.error(`Error reading directory ${dir}:`, error); } } private isValidCommand(command: any): command is Command { return command && typeof command === 'object' && 'data' in command && 'execute' in command; } 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); } } } } export const KyokoClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages] });