Estructura del Proyecto
🏗️ Estructura del Proyecto
Documentación completa de la arquitectura y organización del código del bot.
📁 Estructura de Directorios
discordbot-tk/├── 📁 src/ # Código fuente principal│ ├── 📁 commands/ # Comandos slash organizados por categoría│ │ ├── 📁 admin/ # Comandos de administración│ │ ├── 📁 info/ # Comandos informativos│ │ ├── 📁 levels/ # Sistema de niveles│ │ ├── 📁 moderation/ # Comandos de moderación│ │ ├── 📁 roles/ # Sistema de roles│ │ ├── 📁 tickets/ # Sistema de tickets│ │ └── 📁 welcome/ # Sistema de bienvenida│ ├── 📁 events/ # Eventos de Discord.js│ │ ├── 📁 guild/ # Eventos de servidor│ │ ├── 📁 interaction/ # Eventos de interacciones│ │ ├── 📁 message/ # Eventos de mensajes│ │ └── 📁 reaction/ # Eventos de reacciones│ ├── 📁 handlers/ # Manejadores de funcionalidades│ │ ├── 📁 commands/ # Handler de comandos│ │ ├── 📁 tickets/ # Handler de tickets│ │ └── 📁 welcome/ # Handler de bienvenida│ ├── 📁 utils/ # Utilidades y helpers│ │ ├── 📄 database.js # Gestor de base de datos│ │ ├── 📄 embeds.js # Constructores de embeds│ │ ├── 📄 logger.js # Sistema de logs│ │ └── 📄 i18n.js # Sistema de traducciones│ ├── 📁 locales/ # Archivos de traducciones│ │ ├── 📄 es.json # Español│ │ └── 📄 en.json # Inglés│ └── 📄 index.js # Punto de entrada del bot├── 📁 data/ # Datos persistentes│ ├── 📄 bot.db # Base de datos SQLite│ └── 📁 backups/ # Respaldos automáticos├── 📁 logs/ # Archivos de log│ ├── 📄 bot-YYYY-MM-DD.log # Logs diarios│ └── 📄 error-YYYY-MM-DD.log # Logs de errores├── 📁 docs/ # Documentación│ └── 📁 src/content/docs/ # Contenido de Starlight├── 📁 .vscode/ # Configuración de VS Code├── 📄 .env # Variables de entorno├── 📄 .env.example # Ejemplo de variables├── 📄 .gitignore # Archivos ignorados por Git├── 📄 package.json # Dependencias y scripts├── 📄 pnpm-lock.yaml # Lock file de PNPM└── 📄 README.md # Documentación principal🧩 Arquitectura del Bot
Patrón de Diseño
El bot sigue una arquitectura modular basada en eventos con los siguientes principios:
- Separación de responsabilidades
- Acoplamiento bajo
- Cohesión alta
- Escalabilidad horizontal
- Mantenibilidad
Flujo de Datos
Discord API ↓Event Handlers ↓Business Logic ↓Database Layer ↓Response Generation ↓Discord API📂 Comandos (src/commands/)
Estructura de un Comando
const { SlashCommandBuilder } = require('discord.js');
module.exports = { // Definición del comando data: new SlashCommandBuilder() .setName('comando') .setDescription('Descripción del comando') .addStringOption(option => option.setName('parametro') .setDescription('Descripción del parámetro') .setRequired(true) ),
// Permisos requeridos permissions: ['SendMessages', 'EmbedLinks'],
// Si requiere permisos de administrador adminOnly: false,
// Cooldown en segundos cooldown: 5,
// Función de ejecución async execute(interaction) { try { // Lógica del comando await interaction.reply('Respuesta del comando'); } catch (error) { console.error('Error en comando:', error); await interaction.reply({ content: 'Ocurrió un error al ejecutar el comando.', ephemeral: true }); } }};Categorías de Comandos
Admin - Administración del Bot
config- Configuración general del servidorlanguage- Cambiar idioma del botbackup- Crear respaldo de datos (solo owner)
Info - Comandos Informativos
ping- Latencia del bothelp- Ayuda y comandos disponiblesstats- Estadísticas del bot y servidor
Levels - Sistema de Niveles
rank- Ver nivel de usuarioleaderboard- Ranking del servidor
Moderation - Moderación
ban- Banear usuariokick- Expulsar usuariotimeout- Aislar usuario temporalmentewarn- Advertir usuarioclear- Limpiar mensajes
Roles - Sistema de Roles
setup-roles- Configurar roles por reacción
Tickets - Sistema de Tickets
ticket-setup- Configurar sistema de ticketsclose- Cerrar ticketadd- Agregar usuario a ticketremove- Quitar usuario de ticket
Welcome - Sistema de Bienvenida
welcome-setup- Configurar mensajes de bienvenida
🎯 Eventos (src/events/)
Estructura de un Evento
module.exports = { // Nombre del evento de Discord.js name: 'eventName',
// Si solo debe ejecutarse una vez once: false,
// Función de ejecución async execute(...args) { try { // Lógica del evento } catch (error) { console.error('Error en evento:', error); } }};Categorías de Eventos
Guild - Eventos de Servidor
guildCreate- Bot agregado a servidorguildDelete- Bot removido de servidorguildMemberAdd- Nuevo miembroguildMemberRemove- Miembro saleguildMemberUpdate- Cambios en miembro
Interaction - Interacciones
interactionCreate- Todas las interacciones- Maneja: Comandos, Botones, Select Menus, Modals
Message - Mensajes
messageCreate- Nuevo mensaje (XP, auto-mod)messageDelete- Mensaje eliminado (logs)messageUpdate- Mensaje editado (logs)
Reaction - Reacciones
messageReactionAdd- Reacción agregada (roles)messageReactionRemove- Reacción quitada (roles)
🔧 Handlers (src/handlers/)
Command Handler
const fs = require('fs');const path = require('path');const { Collection } = require('discord.js');
class CommandHandler { static async loadCommands(client) { client.commands = new Collection(); const commandsPath = path.join(__dirname, '..', 'commands');
await this.loadCommandsFromDirectory(client, commandsPath);
console.log(`✅ ${client.commands.size} comandos cargados`); }
static async loadCommandsFromDirectory(client, directory) { const files = fs.readdirSync(directory);
for (const file of files) { const filePath = path.join(directory, file); const stat = fs.statSync(filePath);
if (stat.isDirectory()) { await this.loadCommandsFromDirectory(client, filePath); } else if (file.endsWith('.js')) { const command = require(filePath);
if (command.data && command.execute) { client.commands.set(command.data.name, command); } } } }
static async registerCommands(client) { const commands = client.commands.map(cmd => cmd.data.toJSON());
try { const rest = new REST().setToken(process.env.BOT_TOKEN);
await rest.put( Routes.applicationCommands(process.env.CLIENT_ID), { body: commands } );
console.log('✅ Comandos slash registrados globalmente'); } catch (error) { console.error('❌ Error registrando comandos:', error); } }}Event Handler
const fs = require('fs');const path = require('path');
class EventHandler { static loadEvents(client) { const eventsPath = path.join(__dirname, '..', 'events'); this.loadEventsFromDirectory(client, eventsPath); }
static loadEventsFromDirectory(client, directory) { const files = fs.readdirSync(directory);
for (const file of files) { const filePath = path.join(directory, file); const stat = fs.statSync(filePath);
if (stat.isDirectory()) { this.loadEventsFromDirectory(client, filePath); } else if (file.endsWith('.js')) { const event = require(filePath);
if (event.once) { client.once(event.name, (...args) => event.execute(...args)); } else { client.on(event.name, (...args) => event.execute(...args)); } } } }}🛠️ Utilidades (src/utils/)
Database Manager
const Database = require('better-sqlite3');
class DatabaseManager { constructor() { this.db = new Database('./data/bot.db'); this.initTables(); }
initTables() { // Configuraciones por servidor this.db.exec(` CREATE TABLE IF NOT EXISTS guild_configs ( guild_id TEXT PRIMARY KEY, config TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `);
// Sistema de niveles this.db.exec(` CREATE TABLE IF NOT EXISTS user_levels ( user_id TEXT NOT NULL, guild_id TEXT NOT NULL, xp INTEGER DEFAULT 0, level INTEGER DEFAULT 0, total_messages INTEGER DEFAULT 0, last_xp_gain DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, guild_id) ) `); }
// Métodos de configuración getGuildConfig(guildId) { const stmt = this.db.prepare('SELECT config FROM guild_configs WHERE guild_id = ?'); return stmt.get(guildId); }
setGuildConfig(guildId, config) { const stmt = this.db.prepare(` INSERT OR REPLACE INTO guild_configs (guild_id, config, updated_at) VALUES (?, ?, datetime('now')) `); return stmt.run(guildId, JSON.stringify(config)); }}
module.exports = new DatabaseManager();Logger
const fs = require('fs');const path = require('path');
class Logger { static log(level, message, data = null) { const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, message, data };
// Console output console.log(`[${level}] ${timestamp} - ${message}`);
// File output const date = timestamp.split('T')[0]; const logFile = path.join(__dirname, '..', '..', 'logs', `bot-${date}.log`);
// Asegurar que el directorio existe const logDir = path.dirname(logFile); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); }
fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n'); }
static info(message, data) { this.log('INFO', message, data); } static warn(message, data) { this.log('WARN', message, data); } static error(message, data) { this.log('ERROR', message, data); } static debug(message, data) { this.log('DEBUG', message, data); }}
module.exports = Logger;🌍 Internacionalización (src/locales/)
Estructura de Traducciones
{ "commands": { "ping": { "description": "Muestra la latencia del bot", "response": "🏓 Pong! Latencia: {latency}ms" } }, "errors": { "noPermission": "No tienes permisos para usar este comando", "userNotFound": "Usuario no encontrado", "invalidChannel": "Canal inválido" }, "success": { "configUpdated": "Configuración actualizada correctamente", "userBanned": "Usuario baneado exitosamente" }}Sistema de Traducciones
const fs = require('fs');const path = require('path');
class I18nManager { constructor() { this.translations = new Map(); this.loadTranslations(); }
loadTranslations() { const localesPath = path.join(__dirname, '..', 'locales'); const files = fs.readdirSync(localesPath);
for (const file of files) { if (file.endsWith('.json')) { const locale = file.replace('.json', ''); const filePath = path.join(localesPath, file); const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); this.translations.set(locale, content); } } }
get(locale, key, replacements = {}) { const translation = this.translations.get(locale); if (!translation) return key;
const keys = key.split('.'); let value = translation;
for (const k of keys) { if (value && typeof value === 'object' && k in value) { value = value[k]; } else { return key; } }
if (typeof value === 'string') { return this.replacePlaceholders(value, replacements); }
return key; }
replacePlaceholders(text, replacements) { return text.replace(/{(\w+)}/g, (match, key) => { return replacements[key] || match; }); }}
module.exports = new I18nManager();🚀 Punto de Entrada (src/index.js)
const { Client, GatewayIntentBits, Collection } = require('discord.js');const dotenv = require('dotenv');const CommandHandler = require('./handlers/commands');const EventHandler = require('./handlers/events');const Logger = require('./utils/logger');
// Cargar variables de entornodotenv.config();
// Crear clienteconst client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.MessageContent ]});
// Inicializar botasync function init() { try { // Cargar comandos y eventos await CommandHandler.loadCommands(client); EventHandler.loadEvents(client);
// Conectar a Discord await client.login(process.env.BOT_TOKEN);
Logger.info('Bot iniciado correctamente'); } catch (error) { Logger.error('Error iniciando bot', error); process.exit(1); }}
// Manejo de errores no capturadosprocess.on('unhandledRejection', error => { Logger.error('Unhandled promise rejection', error);});
process.on('uncaughtException', error => { Logger.error('Uncaught exception', error); process.exit(1);});
// Iniciar botinit();📦 Gestión de Dependencias
package.json
{ "name": "discordbot-tk", "version": "1.0.0", "description": "Bot de Discord completo con sistema de administración avanzado", "main": "src/index.js", "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js", "test": "jest", "lint": "eslint src/", "format": "prettier --write src/" }, "dependencies": { "discord.js": "^14.14.1", "better-sqlite3": "^11.10.0", "dotenv": "^16.3.1" }, "devDependencies": { "nodemon": "^3.0.2", "eslint": "^8.0.0", "prettier": "^3.0.0", "jest": "^29.0.0" }}🔧 Configuración de Desarrollo
.vscode/settings.json
{ "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "javascript.suggest.autoImports": true, "typescript.suggest.autoImports": true, "files.exclude": { "node_modules": true, "data/*.db": true, "logs/*.log": true }}.eslintrc.js
module.exports = { env: { es2021: true, node: true }, extends: ['eslint:recommended'], parserOptions: { ecmaVersion: 12, sourceType: 'module' }, rules: { 'no-console': 'off', 'no-unused-vars': 'warn', 'prefer-const': 'error' }};Esta estructura está diseñada para ser escalable, mantenible y fácil de entender para nuevos desarrolladores que se unan al proyecto.