UNPKG

simplify-cord

Version:

SimplifyCord is an unofficial extension of the 'discord.js' library. Our extension aims to simplify the development of Discord bots, promoting cleaner code and easier maintenance.

331 lines (277 loc) 13.8 kB
import { Client, ClientOptions, GatewayIntentBits, Collection, Events, ChatInputCommandInteraction, ButtonInteraction, AnySelectMenuInteraction, ModalSubmitInteraction, Interaction, CommandInteraction, ApplicationCommandType, ApplicationCommandOptionData } from "discord.js"; import SlashCommand, { slashCommandHandlers } from "../handlers/commands/index"; import { interactionHandlers } from "../handlers/interactions/index"; import { Event } from "./Event"; import { pathToFileURL } from 'url'; import { utils } from "../functions/index" import { z } from "zod"; import * as path from 'path'; import { ISlashCommandHandler } from "../handlers/commands/index"; import { Logger } from '../functions/logger'; import { version as djsVersion } from 'discord.js'; import chalk from "chalk"; import { DEFAULT_INTENTS } from "../lib/constants/intents"; import { BootstrapAppOptions, ISimplifyClient } from "../types/index"; export default class bootstrapApp extends Client implements ISimplifyClient { customOptions?: BootstrapAppOptions; slashCommands: Collection<string, ISlashCommandHandler<ApplicationCommandType>> = new Collection(); slashArray: any[] = []; commands?: { guilds?: string[]; }; constructor(options: BootstrapAppOptions) { const intentsValidation = z.array(z.nativeEnum(GatewayIntentBits), { invalid_type_error: "Intents list must be a GatewayIntentBits object from discord" }); intentsValidation.parse(options.intents || DEFAULT_INTENTS); const tokenValidation = z.string({ required_error: "Token is required", invalid_type_error: "Token must be a string" }); tokenValidation.parse(options.token); const clientOptions: ClientOptions = { intents: options.intents || DEFAULT_INTENTS }; super(clientOptions); this.customOptions = options; this.commands = options.commands; this.slashCommands = new Collection(); this.slashArray = []; this.startListening(); this.loadAutoImportPaths().then(() => { Event.register(this); this.login(options.token); }); } public async invokeInteraction(interactionName: string, interaction: CommandInteraction | ButtonInteraction | AnySelectMenuInteraction | ModalSubmitInteraction, params: { [key: string]: string } = {}): Promise<any> { try { const handler = interactionHandlers.get(interactionName); if (!handler) { Logger.error(`No handler found for interaction: ${interactionName}`); if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: 'Interaction handler not found.', ephemeral: true }); } return; } return await handler.run(this, interaction, params); } catch (error) { Logger.error(`Error invoking interaction "${interactionName}"`, error); if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: 'An error occurred while processing this interaction.', ephemeral: true }); } } } public async invokeCommand(commandName: string, interaction: ChatInputCommandInteraction) { const command = this.slashCommands.get(commandName); if (!command){ Logger.error(`Command "${commandName}" not found!`); return; } try { await command.run(this, interaction); } catch (error) { Logger.error(`Error in command "${commandName}"`, error); } } public async reloadCommands() { try { this.slashCommands = new Collection(); this.slashArray = []; for (const [name, command] of slashCommandHandlers) { if (this.slashCommands.has(name)) { continue; } this.slashCommands.set(name, command); const commandData = { name: command.name, description: command.description, type: command.type, options: (command.options as ApplicationCommandOptionData[] || []).map(option => ({ ...option, autocomplete: option.autocomplete ?? false })) }; this.slashArray.push(commandData); } if (this.slashArray.length === 0) { Logger.warn("Warning: No commands to register."); return; } if (this.guilds.cache.size === 0) { Logger.warn("Warning: No guilds found. Commands will be registered when joining a guild."); return; } if (this.commands?.guilds && this.commands.guilds.length > 0) { for (const guildId of this.commands.guilds) { const guild = this.guilds.cache.get(guildId); if (!guild) { Logger.warn(`Guild with ID ${guildId} not found. Make sure the bot is in the guild.`); continue; } try { await guild.commands.set(this.slashArray); Logger.success(`Commands registered in guild: ${guild.name} (${this.slashArray.length} commands)`); } catch (error) { Logger.error(`Failed to register commands in guild ${guild.name}`, error); } } } else { try { await this.application?.commands.set(this.slashArray); Logger.success(`Commands registered globally (${this.slashArray.length} commands)`); } catch (error) { Logger.error("Failed to register commands globally", error); } } } catch (error) { Logger.error("Error reloading commands", error); } } private async loadAutoImportPaths() { const root_path = path.resolve(); const autoImportPath = this.customOptions?.autoImport; if (autoImportPath) { for (const importPath of autoImportPath) { const files = utils.getRecursiveFiles(`${root_path}/${importPath}`); if (!files) { Logger.warn(`Auto Import path not found: '${importPath}'`); Logger.info("Make sure to provide a valid path to the components folder"); continue; } for (const file of files) { const isValidFile = file.endsWith('.mjs') || file.endsWith('.js') || file.endsWith(".ts"); if (!isValidFile) continue; try { const componentPath = pathToFileURL(file).href; await import(componentPath); } catch (error) { Logger.error(`Failed to import component: ${file}`, error); } } } } this.slashCommands = new Collection(); this.slashArray = []; for (const [name, command] of slashCommandHandlers) { this.slashCommands.set(name, command); const commandData = { name: command.name, description: command.description, type: command.type, options: (command.options as ApplicationCommandOptionData[] || []).map(option => ({ ...option, autocomplete: option.autocomplete ?? false })) }; this.slashArray.push(commandData); } } private startListening() { this.once(Events.ClientReady, async (client) => { console.log() await this.loadAutoImportPaths(); if (this.customOptions?.loadLogs !== false) { SlashCommand.loadLogs(); Event.loadLogs(); Logger.separator(); } Logger.info(`${chalk.green.bold(`discord.js`)} @${chalk.white.bold(djsVersion)} / ${chalk.green.bold(`NodeJs`)} @${chalk.white.bold(process.versions.node)}`); Logger.separator(); Logger.ready(`Online as ${Logger.highlight(client.user.username)}`); await this.reloadCommands(); }); this.on(Events.InteractionCreate, async (interaction: Interaction) => { if (interaction.isCommand()) { const command = this.slashCommands.get(interaction.commandName); if (!command) { Logger.error(`Command "${interaction.commandName}" not found!`); Logger.info(`Available commands: ${Array.from(this.slashCommands.keys()).join(", ")}`); if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: 'Command not found. Please try again later.', ephemeral: true }); } return; } try { await command.run(this, interaction); } catch (error) { Logger.error(`Error in command "${interaction.commandName}"`, error); if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: 'An error occurred while executing this command.', ephemeral: true }); } } } if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) { try { const runInteractionHandler = this.getInteractionCallback(interaction.customId, interaction); if (!runInteractionHandler) { Logger.error(`No handler found for interaction: ${interaction.customId}`); if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: 'Interaction handler not found.', ephemeral: true }); } return; } await runInteractionHandler(); } catch (error) { Logger.error(`Error in interaction "${interaction.customId}"`, error); if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: 'An error occurred while processing this interaction.', ephemeral: true }); } } } if (interaction.isAutocomplete()){ const command = this.slashCommands.get(interaction.commandName) if (!command){ return Logger.error('Error on interaction autocomplete! Command not found.'); } if (command.autocomplete){ try { await command.autocomplete(interaction); } catch (error) { Logger.error('Error in autocomplete interaction', error); } } } }); this.on(Events.GuildCreate, async () => { await this.reloadCommands(); }); } private parsePattern(pattern: string, customId: string) { const patternParts = pattern.split('/'); const customIdParts = customId.split('/'); if (patternParts.length !== customIdParts.length) { return null; } const params: { [key: string]: string } = {}; for (let i = 0; i < patternParts.length; i++) { if (patternParts[i].startsWith(':')) { const paramName = patternParts[i].slice(1); params[paramName] = customIdParts[i]; } else if (patternParts[i] !== customIdParts[i]) { return null; } } return params; } private getInteractionCallback(customId: string, interaction: Interaction | ChatInputCommandInteraction) { if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isCommand() || interaction.isModalSubmit()) { try { const useOptionInLastParam = customId.includes("(OILP)"); const cleanCustomId = customId.replace("(OILP)", ""); for (const [pattern, handler] of interactionHandlers.entries()) { const params = this.parsePattern(pattern, cleanCustomId); if (params) { if (interaction.isAnySelectMenu() && useOptionInLastParam && interaction.values.length > 0) { params.value = interaction.values[0]; } const callback = handler.run; if (!callback) { Logger.error(`Callback not found for pattern: ${pattern}`); return; } return callback.bind(null, this, interaction, params); } } Logger.warn(`No matching handler found for customId: ${customId}`); } catch (error) { Logger.error(`Error processing interaction for customId ${customId}`, error); } } } } export { bootstrapApp };