Saltearse al contenido

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

src/commands/categoria/comando.js
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 servidor
  • language - Cambiar idioma del bot
  • backup - Crear respaldo de datos (solo owner)

Info - Comandos Informativos

  • ping - Latencia del bot
  • help - Ayuda y comandos disponibles
  • stats - Estadísticas del bot y servidor

Levels - Sistema de Niveles

  • rank - Ver nivel de usuario
  • leaderboard - Ranking del servidor

Moderation - Moderación

  • ban - Banear usuario
  • kick - Expulsar usuario
  • timeout - Aislar usuario temporalmente
  • warn - Advertir usuario
  • clear - Limpiar mensajes

Roles - Sistema de Roles

  • setup-roles - Configurar roles por reacción

Tickets - Sistema de Tickets

  • ticket-setup - Configurar sistema de tickets
  • close - Cerrar ticket
  • add - Agregar usuario a ticket
  • remove - Quitar usuario de ticket

Welcome - Sistema de Bienvenida

  • welcome-setup - Configurar mensajes de bienvenida

🎯 Eventos (src/events/)

Estructura de un Evento

src/events/categoria/evento.js
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 servidor
  • guildDelete - Bot removido de servidor
  • guildMemberAdd - Nuevo miembro
  • guildMemberRemove - Miembro sale
  • guildMemberUpdate - 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

src/handlers/commands.js
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

src/handlers/events.js
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

src/utils/database.js
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

src/utils/logger.js
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

src/locales/es.json
{
"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

src/utils/i18n.js
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)

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 entorno
dotenv.config();
// Crear cliente
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.MessageContent
]
});
// Inicializar bot
async 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 capturados
process.on('unhandledRejection', error => {
Logger.error('Unhandled promise rejection', error);
});
process.on('uncaughtException', error => {
Logger.error('Uncaught exception', error);
process.exit(1);
});
// Iniciar bot
init();

📦 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.